UNPKG

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
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); //