three-fbxloader-offical-for-pyt
Version:
Threejs fbxload with offical apply 'inflate.min.js' its fluent
1,661 lines (1,360 loc) • 99.8 kB
JavaScript
const THREE = require('three');
var Zlib = require('./inflate.min').Zlib;
/**
* @author Kyle-Larson https://github.com/Kyle-Larson
* @author Takahiro https://github.com/takahirox
* @author Lewy Blue https://github.com/looeee
*
* Loader loads FBX file and generates Group representing FBX scene.
* Requires FBX file to be >= 7.0 and in ASCII or >= 6400 in Binary format
* Versions lower than this may load but will probably have errors
*
* Needs Support:
* Morph normals / blend shape normals
* Animation tracks for morph targets
*
* Euler rotation order
*
* FBX format references:
* https://wiki.blender.org/index.php/User:Mont29/Foundation/FBX_File_Structure
* http://help.autodesk.com/view/FBX/2017/ENU/?guid=__cpp_ref_index_html (C++ SDK reference)
*
* Binary format specification:
* https://code.blender.org/2013/08/fbx-binary-file-format-specification/
*/
module.exports = (function() {
THREE.FBXLoader = function(manager) {
this.manager =
manager !== undefined ? manager : THREE.DefaultLoadingManager;
};
Object.assign(THREE.FBXLoader.prototype, {
load: function(url, onLoad, onProgress, onError) {
var self = this;
var resourceDirectory = THREE.LoaderUtils.extractUrlBase(url);
var loader = new THREE.FileLoader(this.manager);
loader.setResponseType('arraybuffer');
loader.load(
url,
function(buffer) {
try {
var scene = self.parse(buffer, resourceDirectory);
onLoad(scene);
} catch (error) {
window.setTimeout(function() {
if (onError) onError(error);
self.manager.itemError(url);
}, 0);
}
},
onProgress,
onError
);
},
parse: function(FBXBuffer, resourceDirectory) {
var FBXTree;
if (isFbxFormatBinary(FBXBuffer)) {
FBXTree = new BinaryParser().parse(FBXBuffer);
} else {
var FBXText = convertArrayBufferToString(FBXBuffer);
if (!isFbxFormatASCII(FBXText)) {
throw new Error('THREE.FBXLoader: Unknown format.');
}
if (getFbxVersion(FBXText) < 7000) {
throw new Error(
'THREE.FBXLoader: FBX version not supported, FileVersion: ' +
getFbxVersion(FBXText)
);
}
FBXTree = new TextParser().parse(FBXText);
}
// console.log( FBXTree );
var connections = parseConnections(FBXTree);
var images = parseImages(FBXTree);
var textures = parseTextures(
FBXTree,
new THREE.TextureLoader(this.manager).setPath(resourceDirectory),
images,
connections
);
var materials = parseMaterials(FBXTree, textures, connections);
var deformers = parseDeformers(FBXTree, connections);
var geometryMap = parseGeometries(FBXTree, connections, deformers);
var sceneGraph = parseScene(
FBXTree,
connections,
deformers.skeletons,
geometryMap,
materials
);
return sceneGraph;
}
});
// Parses FBXTree.Connections which holds parent-child connections between objects (e.g. material -> texture, model->geometry )
// and details the connection type
function parseConnections(FBXTree) {
var connectionMap = new Map();
if ('Connections' in FBXTree) {
var rawConnections = FBXTree.Connections.connections;
rawConnections.forEach(function(rawConnection) {
var fromID = rawConnection[0];
var toID = rawConnection[1];
var relationship = rawConnection[2];
if (!connectionMap.has(fromID)) {
connectionMap.set(fromID, {
parents: [],
children: []
});
}
var parentRelationship = { ID: toID, relationship: relationship };
connectionMap.get(fromID).parents.push(parentRelationship);
if (!connectionMap.has(toID)) {
connectionMap.set(toID, {
parents: [],
children: []
});
}
var childRelationship = { ID: fromID, relationship: relationship };
connectionMap.get(toID).children.push(childRelationship);
});
}
return connectionMap;
}
// Parse FBXTree.Objects.Video for embedded image data
// These images are connected to textures in FBXTree.Objects.Textures
// via FBXTree.Connections.
function parseImages(FBXTree) {
var images = {};
var blobs = {};
if ('Video' in FBXTree.Objects) {
var videoNodes = FBXTree.Objects.Video;
for (var nodeID in videoNodes) {
var videoNode = videoNodes[nodeID];
var id = parseInt(nodeID);
images[id] = videoNode.RelativeFilename || videoNode.Filename;
// raw image data is in videoNode.Content
if ('Content' in videoNode) {
var arrayBufferContent =
videoNode.Content instanceof ArrayBuffer &&
videoNode.Content.byteLength > 0;
var base64Content =
typeof videoNode.Content === 'string' && videoNode.Content !== '';
if (arrayBufferContent || base64Content) {
var image = parseImage(videoNodes[nodeID]);
blobs[videoNode.RelativeFilename || videoNode.Filename] = image;
}
}
}
}
for (var id in images) {
var filename = images[id];
if (blobs[filename] !== undefined) images[id] = blobs[filename];
else images[id] = images[id].split('\\').pop();
}
return images;
}
// Parse embedded image data in FBXTree.Video.Content
function parseImage(videoNode) {
var content = videoNode.Content;
var fileName = videoNode.RelativeFilename || videoNode.Filename;
var extension = fileName.slice(fileName.lastIndexOf('.') + 1).toLowerCase();
var type;
switch (extension) {
case 'bmp':
type = 'image/bmp';
break;
case 'jpg':
case 'jpeg':
type = 'image/jpeg';
break;
case 'png':
type = 'image/png';
break;
case 'tif':
type = 'image/tiff';
break;
case 'tga':
if (THREE.Loader.Handlers.get('.tga') === null) {
console.warn('FBXLoader: TGA loader not found, skipping ', fileName);
}
type = 'image/tga';
break;
default:
console.warn(
'FBXLoader: Image type "' + extension + '" is not supported.'
);
return;
}
if (typeof content === 'string') {
// ASCII format
return 'data:' + type + ';base64,' + content;
} else {
// Binary Format
var array = new Uint8Array(content);
return window.URL.createObjectURL(new Blob([array], { type: type }));
}
}
// Parse nodes in FBXTree.Objects.Texture
// These contain details such as UV scaling, cropping, rotation etc and are connected
// to images in FBXTree.Objects.Video
function parseTextures(FBXTree, loader, images, connections) {
var textureMap = new Map();
if ('Texture' in FBXTree.Objects) {
var textureNodes = FBXTree.Objects.Texture;
for (var nodeID in textureNodes) {
var texture = parseTexture(
textureNodes[nodeID],
loader,
images,
connections
);
textureMap.set(parseInt(nodeID), texture);
}
}
return textureMap;
}
// Parse individual node in FBXTree.Objects.Texture
function parseTexture(textureNode, loader, images, connections) {
var texture = loadTexture(textureNode, loader, images, connections);
console.log(textureNode);
// texture.ID = textureNode.id;
// texture.name = textureNode.attrName;
var wrapModeU = textureNode.WrapModeU;
var wrapModeV = textureNode.WrapModeV;
var valueU = wrapModeU !== undefined ? wrapModeU.value : 0;
var valueV = wrapModeV !== undefined ? wrapModeV.value : 0;
// http://download.autodesk.com/us/fbx/SDKdocs/FBX_SDK_Help/files/fbxsdkref/class_k_fbx_texture.html#889640e63e2e681259ea81061b85143a
// 0: repeat(default), 1: clamp
// texture.wrapS = valueU === 0 ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping;
// texture.wrapT = valueV === 0 ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping;
if ('Scaling' in textureNode) {
var values = textureNode.Scaling.value;
texture.repeat.x = values[0];
texture.repeat.y = values[1];
}
return texture;
}
// load a texture specified as a blob or data URI, or via an external URL using THREE.TextureLoader
function loadTexture(textureNode, loader, images, connections) {
var fileName;
var currentPath = loader.path;
var children = connections.get(textureNode.id).children;
if (
children !== undefined &&
children.length > 0 &&
images[children[0].ID] !== undefined
) {
fileName = images[children[0].ID];
if (fileName.indexOf('blob:') === 0 || fileName.indexOf('data:') === 0) {
loader.setPath(undefined);
}
}
var texture;
var extension = textureNode.FileName.slice(-3).toLowerCase();
if (extension === 'tga') {
var loader = THREE.Loader.Handlers.get('.tga');
if (loader === null) {
console.warn(
'FBXLoader: TGA loader not found, creating placeholder texture for',
textureNode.RelativeFilename
);
texture = new THREE.Texture();
} else {
texture = loader.load(fileName);
}
} else if (extension === 'psd') {
console.warn(
'FBXLoader: PSD textures are not supported, creating placeholder texture for',
textureNode.RelativeFilename
);
texture = new THREE.Texture();
} else {
// texture = this.textureLoader.load && this.textureLoader.load(fileName);
}
// this.textureLoader.setPath( currentPath );
return texture;
}
// Parse nodes in FBXTree.Objects.Material
function parseMaterials(FBXTree, textureMap, connections) {
var materialMap = new Map();
if ('Material' in FBXTree.Objects) {
var materialNodes = FBXTree.Objects.Material;
for (var nodeID in materialNodes) {
var material = parseMaterial(
FBXTree,
materialNodes[nodeID],
textureMap,
connections
);
if (material !== null) materialMap.set(parseInt(nodeID), material);
}
}
return materialMap;
}
// Parse single node in FBXTree.Objects.Material
// Materials are connected to texture maps in FBXTree.Objects.Textures
// FBX format currently only supports Lambert and Phong shading models
function parseMaterial(FBXTree, materialNode, textureMap, connections) {
var ID = materialNode.id;
var name = materialNode.attrName;
var type = materialNode.ShadingModel;
//Case where FBX wraps shading model in property object.
if (typeof type === 'object') {
type = type.value;
}
// Ignore unused materials which don't have any connections.
if (!connections.has(ID)) return null;
var parameters = parseParameters(
FBXTree,
materialNode,
textureMap,
ID,
connections
);
var material;
switch (type.toLowerCase()) {
case 'phong':
material = new THREE.MeshPhongMaterial();
break;
case 'lambert':
material = new THREE.MeshLambertMaterial();
break;
default:
console.warn(
'THREE.FBXLoader: unknown material type "%s". Defaulting to MeshPhongMaterial.',
type
);
material = new THREE.MeshPhongMaterial({ color: 0x3300ff });
break;
}
material.setValues(parameters);
material.name = name;
return material;
}
// Parse FBX material and return parameters suitable for a three.js material
// Also parse the texture map and return any textures associated with the material
function parseParameters(FBXTree, properties, textureMap, ID, connections) {
var parameters = {};
if (properties.BumpFactor) {
parameters.bumpScale = properties.BumpFactor.value;
}
if (properties.Diffuse) {
parameters.color = new THREE.Color().fromArray(properties.Diffuse.value);
} else if (
properties.DiffuseColor &&
properties.DiffuseColor.type === 'Color'
) {
// The blender exporter exports diffuse here instead of in properties.Diffuse
parameters.color = new THREE.Color().fromArray(
properties.DiffuseColor.value
);
}
if (properties.DisplacementFactor) {
parameters.displacementScale = properties.DisplacementFactor.value;
}
if (properties.Emissive) {
parameters.emissive = new THREE.Color().fromArray(
properties.Emissive.value
);
} else if (
properties.EmissiveColor &&
properties.EmissiveColor.type === 'Color'
) {
// The blender exporter exports emissive color here instead of in properties.Emissive
parameters.emissive = new THREE.Color().fromArray(
properties.EmissiveColor.value
);
}
if (properties.EmissiveFactor) {
parameters.emissiveIntensity = parseFloat(
properties.EmissiveFactor.value
);
}
if (properties.Opacity) {
parameters.opacity = parseFloat(properties.Opacity.value);
}
if (parameters.opacity < 1.0) {
parameters.transparent = true;
}
if (properties.ReflectionFactor) {
parameters.reflectivity = properties.ReflectionFactor.value;
}
if (properties.Shininess) {
parameters.shininess = properties.Shininess.value;
}
if (properties.Specular) {
parameters.specular = new THREE.Color().fromArray(
properties.Specular.value
);
} else if (
properties.SpecularColor &&
properties.SpecularColor.type === 'Color'
) {
// The blender exporter exports specular color here instead of in properties.Specular
parameters.specular = new THREE.Color().fromArray(
properties.SpecularColor.value
);
}
connections.get(ID).children.forEach(function(child) {
var type = child.relationship;
switch (type) {
case 'Bump':
parameters.bumpMap = textureMap.get(child.ID);
break;
case 'DiffuseColor':
parameters.map = getTexture(
FBXTree,
textureMap,
child.ID,
connections
);
break;
case 'DisplacementColor':
parameters.displacementMap = getTexture(
FBXTree,
textureMap,
child.ID,
connections
);
break;
case 'EmissiveColor':
parameters.emissiveMap = getTexture(
FBXTree,
textureMap,
child.ID,
connections
);
break;
case 'NormalMap':
parameters.normalMap = getTexture(
FBXTree,
textureMap,
child.ID,
connections
);
break;
case 'ReflectionColor':
parameters.envMap = getTexture(
FBXTree,
textureMap,
child.ID,
connections
);
parameters.envMap.mapping = THREE.EquirectangularReflectionMapping;
break;
case 'SpecularColor':
parameters.specularMap = getTexture(
FBXTree,
textureMap,
child.ID,
connections
);
break;
case 'TransparentColor':
parameters.alphaMap = getTexture(
FBXTree,
textureMap,
child.ID,
connections
);
parameters.transparent = true;
break;
case 'AmbientColor':
case 'ShininessExponent': // AKA glossiness map
case 'SpecularFactor': // AKA specularLevel
case 'VectorDisplacementColor': // NOTE: Seems to be a copy of DisplacementColor
default:
console.warn(
'THREE.FBXLoader: %s map is not supported in three.js, skipping texture.',
type
);
break;
}
});
return parameters;
}
// get a texture from the textureMap for use by a material.
function getTexture(FBXTree, textureMap, id, connections) {
// if the texture is a layered texture, just use the first layer and issue a warning
if (
'LayeredTexture' in FBXTree.Objects &&
id in FBXTree.Objects.LayeredTexture
) {
console.warn(
'THREE.FBXLoader: layered textures are not supported in three.js. Discarding all but first layer.'
);
id = connections.get(id).children[0].ID;
}
return textureMap.get(id);
}
// Parse nodes in FBXTree.Objects.Deformer
// Deformer node can contain skinning or Vertex Cache animation data, however only skinning is supported here
// Generates map of Skeleton-like objects for use later when generating and binding skeletons.
function parseDeformers(FBXTree, connections) {
var skeletons = {};
var morphTargets = {};
if ('Deformer' in FBXTree.Objects) {
var DeformerNodes = FBXTree.Objects.Deformer;
for (var nodeID in DeformerNodes) {
var deformerNode = DeformerNodes[nodeID];
var relationships = connections.get(parseInt(nodeID));
if (deformerNode.attrType === 'Skin') {
var skeleton = parseSkeleton(relationships, DeformerNodes);
skeleton.ID = nodeID;
if (relationships.parents.length > 1)
console.warn(
'THREE.FBXLoader: skeleton attached to more than one geometry is not supported.'
);
skeleton.geometryID = relationships.parents[0].ID;
skeletons[nodeID] = skeleton;
} else if (deformerNode.attrType === 'BlendShape') {
var morphTarget = {
id: nodeID
};
morphTarget.rawTargets = parseMorphTargets(
relationships,
deformerNode,
DeformerNodes,
connections,
FBXTree
);
morphTarget.id = nodeID;
if (relationships.parents.length > 1)
console.warn(
'THREE.FBXLoader: morph target attached to more than one geometry is not supported.'
);
morphTarget.parentGeoID = relationships.parents[0].ID;
morphTargets[nodeID] = morphTarget;
}
}
}
return {
skeletons: skeletons,
morphTargets: morphTargets
};
}
// Parse single nodes in FBXTree.Objects.Deformer
// The top level skeleton node has type 'Skin' and sub nodes have type 'Cluster'
// Each skin node represents a skeleton and each cluster node represents a bone
function parseSkeleton(connections, deformerNodes) {
var rawBones = [];
connections.children.forEach(function(child) {
var boneNode = deformerNodes[child.ID];
if (boneNode.attrType !== 'Cluster') return;
var rawBone = {
ID: child.ID,
indices: [],
weights: [],
transform: new THREE.Matrix4().fromArray(boneNode.Transform.a),
transformLink: new THREE.Matrix4().fromArray(boneNode.TransformLink.a),
linkMode: boneNode.Mode
};
if ('Indexes' in boneNode) {
rawBone.indices = boneNode.Indexes.a;
rawBone.weights = boneNode.Weights.a;
}
rawBones.push(rawBone);
});
return {
rawBones: rawBones,
bones: []
};
}
// The top level morph deformer node has type "BlendShape" and sub nodes have type "BlendShapeChannel"
function parseMorphTargets(
relationships,
deformerNode,
deformerNodes,
connections
) {
var rawMorphTargets = [];
for (var i = 0; i < relationships.children.length; i++) {
if (i === 8) {
console.warn(
'FBXLoader: maximum of 8 morph targets supported. Ignoring additional targets.'
);
break;
}
var child = relationships.children[i];
var morphTargetNode = deformerNodes[child.ID];
var rawMorphTarget = {
name: morphTargetNode.attrName,
initialWeight: morphTargetNode.DeformPercent,
id: morphTargetNode.id,
fullWeights: morphTargetNode.FullWeights.a
};
if (morphTargetNode.attrType !== 'BlendShapeChannel') return;
var targetRelationships = connections.get(parseInt(child.ID));
targetRelationships.children.forEach(function(child) {
if (child.relationship === 'DeformPercent') {
// TODO: animation of morph targets is currently unsupported
rawMorphTarget.weightCurveID = child.ID;
// weightCurve = FBXTree.Objects.AnimationCurveNode[ weightCurveID ];
} else {
rawMorphTarget.geoID = child.ID;
// morphGeo = FBXTree.Objects.Geometry[ geoID ];
}
});
rawMorphTargets.push(rawMorphTarget);
}
return rawMorphTargets;
}
// Parse nodes in FBXTree.Objects.Geometry
function parseGeometries(FBXTree, connections, deformers) {
var geometryMap = new Map();
if ('Geometry' in FBXTree.Objects) {
var geoNodes = FBXTree.Objects.Geometry;
for (var nodeID in geoNodes) {
var relationships = connections.get(parseInt(nodeID));
var geo = parseGeometry(
FBXTree,
relationships,
geoNodes[nodeID],
deformers
);
geometryMap.set(parseInt(nodeID), geo);
}
}
return geometryMap;
}
// Parse single node in FBXTree.Objects.Geometry
function parseGeometry(FBXTree, relationships, geoNode, deformers) {
switch (geoNode.attrType) {
case 'Mesh':
return parseMeshGeometry(FBXTree, relationships, geoNode, deformers);
break;
case 'NurbsCurve':
return parseNurbsGeometry(geoNode);
break;
}
}
// Parse single node mesh geometry in FBXTree.Objects.Geometry
function parseMeshGeometry(FBXTree, relationships, geoNode, deformers) {
var skeletons = deformers.skeletons;
var morphTargets = deformers.morphTargets;
var modelNodes = relationships.parents.map(function(parent) {
return FBXTree.Objects.Model[parent.ID];
});
// don't create geometry if it is not associated with any models
if (modelNodes.length === 0) return;
var skeleton = relationships.children.reduce(function(skeleton, child) {
if (skeletons[child.ID] !== undefined) skeleton = skeletons[child.ID];
return skeleton;
}, null);
var morphTarget = relationships.children.reduce(function(
morphTarget,
child
) {
if (morphTargets[child.ID] !== undefined)
morphTarget = morphTargets[child.ID];
return morphTarget;
},
null);
var preTransform = new THREE.Matrix4();
// TODO: if there is more than one model associated with the geometry, AND the models have
// different geometric transforms, then this will cause problems
// if ( modelNodes.length > 1 ) { }
// For now just assume one model and get the preRotations from that
var modelNode = modelNodes[0];
if ('GeometricRotation' in modelNode) {
var array = modelNode.GeometricRotation.value.map(THREE.Math.degToRad);
array[3] = 'ZYX';
preTransform.makeRotationFromEuler(new THREE.Euler().fromArray(array));
}
if ('GeometricTranslation' in modelNode) {
preTransform.setPosition(
new THREE.Vector3().fromArray(modelNode.GeometricTranslation.value)
);
}
if ('GeometricScaling' in modelNode) {
preTransform.scale(
new THREE.Vector3().fromArray(modelNode.GeometricScaling.value)
);
}
return genGeometry(FBXTree, geoNode, skeleton, morphTarget, preTransform);
}
// Generate a THREE.BufferGeometry from a node in FBXTree.Objects.Geometry
function genGeometry(FBXTree, geoNode, skeleton, morphTarget, preTransform) {
var geo = new THREE.BufferGeometry();
if (geoNode.attrName) geo.name = geoNode.attrName;
var geoInfo = getGeoInfo(geoNode, skeleton);
var buffers = genBuffers(geoInfo);
var positionAttribute = new THREE.Float32BufferAttribute(buffers.vertex, 3);
preTransform.applyToBufferAttribute(positionAttribute);
geo.addAttribute('position', positionAttribute);
if (buffers.colors.length > 0) {
geo.addAttribute(
'color',
new THREE.Float32BufferAttribute(buffers.colors, 3)
);
}
if (skeleton) {
geo.addAttribute(
'skinIndex',
new THREE.Uint16BufferAttribute(buffers.weightsIndices, 4)
);
geo.addAttribute(
'skinWeight',
new THREE.Float32BufferAttribute(buffers.vertexWeights, 4)
);
// used later to bind the skeleton to the model
geo.FBX_Deformer = skeleton;
}
if (buffers.normal.length > 0) {
var normalAttribute = new THREE.Float32BufferAttribute(buffers.normal, 3);
var normalMatrix = new THREE.Matrix3().getNormalMatrix(preTransform);
normalMatrix.applyToBufferAttribute(normalAttribute);
geo.addAttribute('normal', normalAttribute);
}
buffers.uvs.forEach(function(uvBuffer, i) {
// subsequent uv buffers are called 'uv1', 'uv2', ...
var name = 'uv' + (i + 1).toString();
// the first uv buffer is just called 'uv'
if (i === 0) {
name = 'uv';
}
geo.addAttribute(
name,
new THREE.Float32BufferAttribute(buffers.uvs[i], 2)
);
});
if (geoInfo.material && geoInfo.material.mappingType !== 'AllSame') {
// Convert the material indices of each vertex into rendering groups on the geometry.
var prevMaterialIndex = buffers.materialIndex[0];
var startIndex = 0;
buffers.materialIndex.forEach(function(currentIndex, i) {
if (currentIndex !== prevMaterialIndex) {
geo.addGroup(startIndex, i - startIndex, prevMaterialIndex);
prevMaterialIndex = currentIndex;
startIndex = i;
}
});
// the loop above doesn't add the last group, do that here.
if (geo.groups.length > 0) {
var lastGroup = geo.groups[geo.groups.length - 1];
var lastIndex = lastGroup.start + lastGroup.count;
if (lastIndex !== buffers.materialIndex.length) {
geo.addGroup(
lastIndex,
buffers.materialIndex.length - lastIndex,
prevMaterialIndex
);
}
}
// case where there are multiple materials but the whole geometry is only
// using one of them
if (geo.groups.length === 0) {
geo.addGroup(0, buffers.materialIndex.length, buffers.materialIndex[0]);
}
}
addMorphTargets(FBXTree, geo, geoNode, morphTarget, preTransform);
return geo;
}
function getGeoInfo(geoNode, skeleton) {
var geoInfo = {};
geoInfo.vertexPositions =
geoNode.Vertices !== undefined ? geoNode.Vertices.a : [];
geoInfo.vertexIndices =
geoNode.PolygonVertexIndex !== undefined
? geoNode.PolygonVertexIndex.a
: [];
if (geoNode.LayerElementColor) {
geoInfo.color = getColors(geoNode.LayerElementColor[0]);
}
if (geoNode.LayerElementMaterial) {
geoInfo.material = getMaterials(geoNode.LayerElementMaterial[0]);
}
if (geoNode.LayerElementNormal) {
geoInfo.normal = getNormals(geoNode.LayerElementNormal[0]);
}
if (geoNode.LayerElementUV) {
geoInfo.uv = [];
var i = 0;
while (geoNode.LayerElementUV[i]) {
geoInfo.uv.push(getUVs(geoNode.LayerElementUV[i]));
i++;
}
}
geoInfo.weightTable = {};
if (skeleton !== null) {
geoInfo.skeleton = skeleton;
skeleton.rawBones.forEach(function(rawBone, i) {
// loop over the bone's vertex indices and weights
rawBone.indices.forEach(function(index, j) {
if (geoInfo.weightTable[index] === undefined)
geoInfo.weightTable[index] = [];
geoInfo.weightTable[index].push({
id: i,
weight: rawBone.weights[j]
});
});
});
}
return geoInfo;
}
function genBuffers(geoInfo) {
var buffers = {
vertex: [],
normal: [],
colors: [],
uvs: [],
materialIndex: [],
vertexWeights: [],
weightsIndices: []
};
var polygonIndex = 0;
var faceLength = 0;
var displayedWeightsWarning = false;
// these will hold data for a single face
var facePositionIndexes = [];
var faceNormals = [];
var faceColors = [];
var faceUVs = [];
var faceWeights = [];
var faceWeightIndices = [];
geoInfo.vertexIndices.forEach(function(vertexIndex, polygonVertexIndex) {
var endOfFace = false;
// Face index and vertex index arrays are combined in a single array
// A cube with quad faces looks like this:
// PolygonVertexIndex: *24 {
// a: 0, 1, 3, -3, 2, 3, 5, -5, 4, 5, 7, -7, 6, 7, 1, -1, 1, 7, 5, -4, 6, 0, 2, -5
// }
// Negative numbers mark the end of a face - first face here is 0, 1, 3, -3
// to find index of last vertex bit shift the index: ^ - 1
if (vertexIndex < 0) {
vertexIndex = vertexIndex ^ -1; // equivalent to ( x * -1 ) - 1
endOfFace = true;
}
var weightIndices = [];
var weights = [];
facePositionIndexes.push(
vertexIndex * 3,
vertexIndex * 3 + 1,
vertexIndex * 3 + 2
);
if (geoInfo.color) {
var data = getData(
polygonVertexIndex,
polygonIndex,
vertexIndex,
geoInfo.color
);
faceColors.push(data[0], data[1], data[2]);
}
if (geoInfo.skeleton) {
if (geoInfo.weightTable[vertexIndex] !== undefined) {
geoInfo.weightTable[vertexIndex].forEach(function(wt) {
weights.push(wt.weight);
weightIndices.push(wt.id);
});
}
if (weights.length > 4) {
if (!displayedWeightsWarning) {
console.warn(
'THREE.FBXLoader: Vertex has more than 4 skinning weights assigned to vertex. Deleting additional weights.'
);
displayedWeightsWarning = true;
}
var wIndex = [0, 0, 0, 0];
var Weight = [0, 0, 0, 0];
weights.forEach(function(weight, weightIndex) {
var currentWeight = weight;
var currentIndex = weightIndices[weightIndex];
Weight.forEach(function(
comparedWeight,
comparedWeightIndex,
comparedWeightArray
) {
if (currentWeight > comparedWeight) {
comparedWeightArray[comparedWeightIndex] = currentWeight;
currentWeight = comparedWeight;
var tmp = wIndex[comparedWeightIndex];
wIndex[comparedWeightIndex] = currentIndex;
currentIndex = tmp;
}
});
});
weightIndices = wIndex;
weights = Weight;
}
// if the weight array is shorter than 4 pad with 0s
while (weights.length < 4) {
weights.push(0);
weightIndices.push(0);
}
for (var i = 0; i < 4; ++i) {
faceWeights.push(weights[i]);
faceWeightIndices.push(weightIndices[i]);
}
}
if (geoInfo.normal) {
var data = getData(
polygonVertexIndex,
polygonIndex,
vertexIndex,
geoInfo.normal
);
faceNormals.push(data[0], data[1], data[2]);
}
if (geoInfo.material && geoInfo.material.mappingType !== 'AllSame') {
var materialIndex = getData(
polygonVertexIndex,
polygonIndex,
vertexIndex,
geoInfo.material
)[0];
}
if (geoInfo.uv) {
geoInfo.uv.forEach(function(uv, i) {
var data = getData(polygonVertexIndex, polygonIndex, vertexIndex, uv);
if (faceUVs[i] === undefined) {
faceUVs[i] = [];
}
faceUVs[i].push(data[0]);
faceUVs[i].push(data[1]);
});
}
faceLength++;
if (endOfFace) {
genFace(
buffers,
geoInfo,
facePositionIndexes,
materialIndex,
faceNormals,
faceColors,
faceUVs,
faceWeights,
faceWeightIndices,
faceLength
);
polygonIndex++;
faceLength = 0;
// reset arrays for the next face
facePositionIndexes = [];
faceNormals = [];
faceColors = [];
faceUVs = [];
faceWeights = [];
faceWeightIndices = [];
}
});
return buffers;
}
// Generate data for a single face in a geometry. If the face is a quad then split it into 2 tris
function genFace(
buffers,
geoInfo,
facePositionIndexes,
materialIndex,
faceNormals,
faceColors,
faceUVs,
faceWeights,
faceWeightIndices,
faceLength
) {
for (var i = 2; i < faceLength; i++) {
buffers.vertex.push(geoInfo.vertexPositions[facePositionIndexes[0]]);
buffers.vertex.push(geoInfo.vertexPositions[facePositionIndexes[1]]);
buffers.vertex.push(geoInfo.vertexPositions[facePositionIndexes[2]]);
buffers.vertex.push(
geoInfo.vertexPositions[facePositionIndexes[(i - 1) * 3]]
);
buffers.vertex.push(
geoInfo.vertexPositions[facePositionIndexes[(i - 1) * 3 + 1]]
);
buffers.vertex.push(
geoInfo.vertexPositions[facePositionIndexes[(i - 1) * 3 + 2]]
);
buffers.vertex.push(geoInfo.vertexPositions[facePositionIndexes[i * 3]]);
buffers.vertex.push(
geoInfo.vertexPositions[facePositionIndexes[i * 3 + 1]]
);
buffers.vertex.push(
geoInfo.vertexPositions[facePositionIndexes[i * 3 + 2]]
);
if (geoInfo.skeleton) {
buffers.vertexWeights.push(faceWeights[0]);
buffers.vertexWeights.push(faceWeights[1]);
buffers.vertexWeights.push(faceWeights[2]);
buffers.vertexWeights.push(faceWeights[3]);
buffers.vertexWeights.push(faceWeights[(i - 1) * 4]);
buffers.vertexWeights.push(faceWeights[(i - 1) * 4 + 1]);
buffers.vertexWeights.push(faceWeights[(i - 1) * 4 + 2]);
buffers.vertexWeights.push(faceWeights[(i - 1) * 4 + 3]);
buffers.vertexWeights.push(faceWeights[i * 4]);
buffers.vertexWeights.push(faceWeights[i * 4 + 1]);
buffers.vertexWeights.push(faceWeights[i * 4 + 2]);
buffers.vertexWeights.push(faceWeights[i * 4 + 3]);
buffers.weightsIndices.push(faceWeightIndices[0]);
buffers.weightsIndices.push(faceWeightIndices[1]);
buffers.weightsIndices.push(faceWeightIndices[2]);
buffers.weightsIndices.push(faceWeightIndices[3]);
buffers.weightsIndices.push(faceWeightIndices[(i - 1) * 4]);
buffers.weightsIndices.push(faceWeightIndices[(i - 1) * 4 + 1]);
buffers.weightsIndices.push(faceWeightIndices[(i - 1) * 4 + 2]);
buffers.weightsIndices.push(faceWeightIndices[(i - 1) * 4 + 3]);
buffers.weightsIndices.push(faceWeightIndices[i * 4]);
buffers.weightsIndices.push(faceWeightIndices[i * 4 + 1]);
buffers.weightsIndices.push(faceWeightIndices[i * 4 + 2]);
buffers.weightsIndices.push(faceWeightIndices[i * 4 + 3]);
}
if (geoInfo.color) {
buffers.colors.push(faceColors[0]);
buffers.colors.push(faceColors[1]);
buffers.colors.push(faceColors[2]);
buffers.colors.push(faceColors[(i - 1) * 3]);
buffers.colors.push(faceColors[(i - 1) * 3 + 1]);
buffers.colors.push(faceColors[(i - 1) * 3 + 2]);
buffers.colors.push(faceColors[i * 3]);
buffers.colors.push(faceColors[i * 3 + 1]);
buffers.colors.push(faceColors[i * 3 + 2]);
}
if (geoInfo.material && geoInfo.material.mappingType !== 'AllSame') {
buffers.materialIndex.push(materialIndex);
buffers.materialIndex.push(materialIndex);
buffers.materialIndex.push(materialIndex);
}
if (geoInfo.normal) {
buffers.normal.push(faceNormals[0]);
buffers.normal.push(faceNormals[1]);
buffers.normal.push(faceNormals[2]);
buffers.normal.push(faceNormals[(i - 1) * 3]);
buffers.normal.push(faceNormals[(i - 1) * 3 + 1]);
buffers.normal.push(faceNormals[(i - 1) * 3 + 2]);
buffers.normal.push(faceNormals[i * 3]);
buffers.normal.push(faceNormals[i * 3 + 1]);
buffers.normal.push(faceNormals[i * 3 + 2]);
}
if (geoInfo.uv) {
geoInfo.uv.forEach(function(uv, j) {
if (buffers.uvs[j] === undefined) buffers.uvs[j] = [];
buffers.uvs[j].push(faceUVs[j][0]);
buffers.uvs[j].push(faceUVs[j][1]);
buffers.uvs[j].push(faceUVs[j][(i - 1) * 2]);
buffers.uvs[j].push(faceUVs[j][(i - 1) * 2 + 1]);
buffers.uvs[j].push(faceUVs[j][i * 2]);
buffers.uvs[j].push(faceUVs[j][i * 2 + 1]);
});
}
}
}
function addMorphTargets(
FBXTree,
parentGeo,
parentGeoNode,
morphTarget,
preTransform
) {
if (morphTarget === null) return;
parentGeo.morphAttributes.position = [];
parentGeo.morphAttributes.normal = [];
morphTarget.rawTargets.forEach(function(rawTarget) {
var morphGeoNode = FBXTree.Objects.Geometry[rawTarget.geoID];
if (morphGeoNode !== undefined) {
genMorphGeometry(parentGeo, parentGeoNode, morphGeoNode, preTransform);
}
});
}
// a morph geometry node is similar to a standard node, and the node is also contained
// in FBXTree.Objects.Geometry, however it can only have attributes for position, normal
// and a special attribute Index defining which vertices of the original geometry are affected
// Normal and position attributes only have data for the vertices that are affected by the morph
function genMorphGeometry(
parentGeo,
parentGeoNode,
morphGeoNode,
preTransform
) {
var morphGeo = new THREE.BufferGeometry();
if (morphGeoNode.attrName) morphGeo.name = morphGeoNode.attrName;
var vertexIndices =
parentGeoNode.PolygonVertexIndex !== undefined
? parentGeoNode.PolygonVertexIndex.a
: [];
// make a copy of the parent's vertex positions
var vertexPositions =
parentGeoNode.Vertices !== undefined
? parentGeoNode.Vertices.a.slice()
: [];
var morphPositions =
morphGeoNode.Vertices !== undefined ? morphGeoNode.Vertices.a : [];
var indices =
morphGeoNode.Indexes !== undefined ? morphGeoNode.Indexes.a : [];
for (var i = 0; i < indices.length; i++) {
var morphIndex = indices[i] * 3;
// FBX format uses blend shapes rather than morph targets. This can be converted
// by additively combining the blend shape positions with the original geometry's positions
vertexPositions[morphIndex] += morphPositions[i * 3];
vertexPositions[morphIndex + 1] += morphPositions[i * 3 + 1];
vertexPositions[morphIndex + 2] += morphPositions[i * 3 + 2];
}
// TODO: add morph normal support
var morphGeoInfo = {
vertexIndices: vertexIndices,
vertexPositions: vertexPositions
};
var morphBuffers = genBuffers(morphGeoInfo);
var positionAttribute = new THREE.Float32BufferAttribute(
morphBuffers.vertex,
3
);
positionAttribute.name = morphGeoNode.attrName;
preTransform.applyToBufferAttribute(positionAttribute);
parentGeo.morphAttributes.position.push(positionAttribute);
}
// Parse normal from FBXTree.Objects.Geometry.LayerElementNormal if it exists
function getNormals(NormalNode) {
var mappingType = NormalNode.MappingInformationType;
var referenceType = NormalNode.ReferenceInformationType;
var buffer = NormalNode.Normals.a;
var indexBuffer = [];
if (referenceType === 'IndexToDirect') {
if ('NormalIndex' in NormalNode) {
indexBuffer = NormalNode.NormalIndex.a;
} else if ('NormalsIndex' in NormalNode) {
indexBuffer = NormalNode.NormalsIndex.a;
}
}
return {
dataSize: 3,
buffer: buffer,
indices: indexBuffer,
mappingType: mappingType,
referenceType: referenceType
};
}
// Parse UVs from FBXTree.Objects.Geometry.LayerElementUV if it exists
function getUVs(UVNode) {
var mappingType = UVNode.MappingInformationType;
var referenceType = UVNode.ReferenceInformationType;
var buffer = UVNode.UV.a;
var indexBuffer = [];
if (referenceType === 'IndexToDirect') {
indexBuffer = UVNode.UVIndex.a;
}
return {
dataSize: 2,
buffer: buffer,
indices: indexBuffer,
mappingType: mappingType,
referenceType: referenceType
};
}
// Parse Vertex Colors from FBXTree.Objects.Geometry.LayerElementColor if it exists
function getColors(ColorNode) {
var mappingType = ColorNode.MappingInformationType;
var referenceType = ColorNode.ReferenceInformationType;
var buffer = ColorNode.Colors.a;
var indexBuffer = [];
if (referenceType === 'IndexToDirect') {
indexBuffer = ColorNode.ColorIndex.a;
}
return {
dataSize: 4,
buffer: buffer,
indices: indexBuffer,
mappingType: mappingType,
referenceType: referenceType
};
}
// Parse mapping and material data in FBXTree.Objects.Geometry.LayerElementMaterial if it exists
function getMaterials(MaterialNode) {
var mappingType = MaterialNode.MappingInformationType;
var referenceType = MaterialNode.ReferenceInformationType;
if (mappingType === 'NoMappingInformation') {
return {
dataSize: 1,
buffer: [0],
indices: [0],
mappingType: 'AllSame',
referenceType: referenceType
};
}
var materialIndexBuffer = MaterialNode.Materials.a;
// Since materials are stored as indices, there's a bit of a mismatch between FBX and what
// we expect.So we create an intermediate buffer that points to the index in the buffer,
// for conforming with the other functions we've written for other data.
var materialIndices = [];
for (var i = 0; i < materialIndexBuffer.length; ++i) {
materialIndices.push(i);
}
return {
dataSize: 1,
buffer: materialIndexBuffer,
indices: materialIndices,
mappingType: mappingType,
referenceType: referenceType
};
}
var dataArray = [];
function getData(polygonVertexIndex, polygonIndex, vertexIndex, infoObject) {
var index;
switch (infoObject.mappingType) {
case 'ByPolygonVertex':
index = polygonVertexIndex;
break;
case 'ByPolygon':
index = polygonIndex;
break;
case 'ByVertice':
index = vertexIndex;
break;
case 'AllSame':
index = infoObject.indices[0];
break;
default:
console.warn(
'THREE.FBXLoader: unknown attribute mapping type ' +
infoObject.mappingType
);
}
if (infoObject.referenceType === 'IndexToDirect')
index = infoObject.indices[index];
var from = index * infoObject.dataSize;
var to = from + infoObject.dataSize;
return slice(dataArray, infoObject.buffer, from, to);
}
// Generate a NurbGeometry from a node in FBXTree.Objects.Geometry
function parseNurbsGeometry(geoNode) {
if (THREE.NURBSCurve === undefined) {
console.error(
'THREE.FBXLoader: The loader relies on THREE.NURBSCurve for any nurbs present in the model. Nurbs will show up as empty geometry.'
);
return new THREE.BufferGeometry();
}
var order = parseInt(geoNode.Order);
if (isNaN(order)) {
console.error(
'THREE.FBXLoader: Invalid Order %s given for geometry ID: %s',
geoNode.Order,
geoNode.id
);
return new THREE.BufferGeometry();
}
var degree = order - 1;
var knots = geoNode.KnotVector.a;
var controlPoints = [];
var pointsValues = geoNode.Points.a;
for (var i = 0, l = pointsValues.length; i < l; i += 4) {
controlPoints.push(new THREE.Vector4().fromArray(pointsValues, i));
}
var startKnot, endKnot;
if (geoNode.Form === 'Closed') {
controlPoints.push(controlPoints[0]);
} else if (geoNode.Form === 'Periodic') {
startKnot = degree;
endKnot = knots.length - 1 - startKnot;
for (var i = 0; i < degree; ++i) {
controlPoints.push(controlPoints[i]);
}
}
var curve = new THREE.NURBSCurve(
degree,
knots,
controlPoints,
startKnot,
endKnot
);
var vertices = curve.getPoints(controlPoints.length * 7);
var positions = new Float32Array(vertices.length * 3);
vertices.forEach(function(vertex, i) {
vertex.toArray(positions, i * 3);
});
var geometry = new THREE.BufferGeometry();
geometry.addAttribute('position', new THREE.BufferAttribute(positions, 3));
return geometry;
}
// create the main THREE.Group() to be returned by the loader
function parseScene(
FBXTree,
connections,
skeletons,
geometryMap,
materialMap
) {
var sceneGraph = new THREE.Group();
var modelMap = parseModels(
FBXTree,
skeletons,
geometryMap,
materialMap,
connections
);
var modelNodes = FBXTree.Objects.Model;
modelMap.forEach(function(model) {
var modelNode = modelNodes[model.ID];
setLookAtProperties(FBXTree, model, modelNode, connections, sceneGraph);
var parentConnections = connections.get(model.ID).parents;
parentConnections.forEach(function(connection) {
var parent = modelMap.get(connection.ID);
if (parent !== undefined) parent.add(model);
});
if (model.parent === null) {
sceneGraph.add(model);
}
});
bindSkeleton(FBXTree, skeletons, geometryMap, modelMap, connections);
addAnimations(FBXTree, connections, sceneGraph);
createAmbientLight(FBXTree, sceneGraph);
return sceneGraph;
}
// parse nodes in FBXTree.Objects.Model
function parseModels(
FBXTree,
skeletons,
geometryMap,
materialMap,
connections
) {
var modelMap = new Map();
var modelNodes = FBXTree.Objects.Model;
for (var nodeID in modelNodes) {
var id = parseInt(nodeID);
var node = modelNodes[nodeID];
var relationships = connections.get(id);
var model = buildSkeleton(relationships, skeletons, id, node.attrName);
if (!model) {
switch (node.attrType) {
case 'Camera':
model = createCamera(FBXTree, relationships);
break;
case 'Light':
model = createLight(FBXTree, relationships);
break;
case 'Mesh':
model = createMesh(
FBXTree,
relationships,
geometryMap,
materialMap
);
break;
case 'NurbsCurve':
model = createCurve(relationships, geometryMap);
break;
case 'LimbNode': // usually associated with a Bone, however if a Bone was not created we'll make a Group instead
case 'Null':
default:
model = new THREE.Group();
break;
}
model.name = THREE.PropertyBinding.sanitizeNodeName(node.attrName);
model.ID = id;
}
setModelTransforms(FBXTree, model, node);
modelMap.set(id, model);
}
return modelMap;
}
function buildSkeleton(relationships, skeletons, id, name) {
var bone = null;
relationships.parents.forEach(function(parent) {
for (var ID in skeletons) {
var skeleton = skeletons[ID];
skeleton.rawBones.forEach(function(rawBone, i) {
if (rawBone.ID === parent.ID) {
var subBone = bone;
bone = new THREE.Bone();
bone.matrixWorld.copy(rawBone.transformLink);
//