ng2-3d-editor
Version:
Angular 2 3D Editor/viewer
2,924 lines • 142 kB
JavaScript
/**
* @author Kyle-Larson https://github.com/Kyle-Larson
*
* Loader loads FBX file and generates Group representing FBX scene.
* Requires FBX file to be >= 7.0 and in ASCII format.
*
* Supports:
* Mesh Generation (Positional Data)
* Normal Data (Per Vertex Drawing Instance)
* UV Data (Per Vertex Drawing Instance)
* Skinning
* Animation
* - Separated Animations based on stacks.
* - Skeletal & Non-Skeletal Animations
*
* Needs Support:
* Indexed Buffers
* PreRotation support.
*/
"use strict";
var THREE = require("three");
/**
* Generates a loader for loading FBX files from URL and parsing into
* a THREE.Group.
* @param {THREE.LoadingManager} manager - Loading Manager for loader to use.
*/
THREE.FBXLoader = function (manager) {
THREE.Loader.call(this);
this.manager = (manager !== undefined) ? manager : THREE.DefaultLoadingManager;
this.fileLoader = new THREE.FileLoader(this.manager);
this.textureLoader = new THREE.TextureLoader(this.manager);
};
Object.assign(THREE.FBXLoader.prototype, THREE.Loader.prototype);
THREE.FBXLoader.prototype.constructor = THREE.FBXLoader;
Object.assign(THREE.FBXLoader.prototype, {
/**
* Loads an ASCII FBX file from URL and parses into a THREE.Group.
* THREE.Group will have an animations property of AnimationClips
* of the different animations exported with the FBX.
* @param {string} url - URL of the FBX file.
* @param {function(THREE.Group):void} onLoad - Callback for when FBX file is loaded and parsed.
* @param {function(ProgressEvent):void} onProgress - Callback fired periodically when file is being retrieved from server.
* @param {function(Event):void} onError - Callback fired when error occurs (Currently only with retrieving file, not with parsing errors).
*/
load: function (url, onLoad, onProgress, onError) {
var self = this;
var resourceDirectory = url.split(/[\\\/]/);
resourceDirectory.pop();
resourceDirectory = resourceDirectory.join('/');
this.fileLoader.load(url, function (text) {
if (!isFbxFormatASCII(text)) {
console.error('FBXLoader: FBX Binary format not supported.');
self.manager.itemError(url);
return;
}
if (getFbxVersion(text) < 7000) {
console.error('FBXLoader: FBX version not supported for file at ' + url + ', FileVersion: ' + getFbxVersion(text));
self.manager.itemError(url);
return;
}
var scene = self.parse(text, resourceDirectory);
onLoad(scene);
}, onProgress, onError);
},
/**
* Parses an ASCII FBX file and returns a THREE.Group.
* THREE.Group will have an animations property of AnimationClips
* of the different animations within the FBX file.
* @param {string} FBXText - Contents of FBX file to parse.
* @param {string} resourceDirectory - Directory to load external assets (e.g. textures ) from.
* @returns {THREE.Group}
*/
parse: function (FBXText, resourceDirectory) {
var loader = this;
var FBXTree = new TextParser().parse(FBXText);
var connections = parseConnections(FBXTree);
var textures = parseTextures(FBXTree);
var materials = parseMaterials(FBXTree, textures, connections);
var deformerMap = parseDeformers(FBXTree, connections);
var geometryMap = parseGeometries(FBXTree, connections, deformerMap);
var sceneGraph = parseScene(FBXTree, connections, deformerMap, geometryMap, materials);
return sceneGraph;
/**
* @typedef {{value: number}} FBXValue
*/
/**
* @typedef {{value: {x: string, y: string, z: string}}} FBXVector3
*/
/**
* @typedef {{properties: {a: string}}} FBXArrayNode
*/
/**
* @typedef {{properties: {MappingInformationType: string, ReferenceInformationType: string }, subNodes: Object<string, FBXArrayNode>}} FBXMappedArrayNode
*/
/**
* @typedef {{id: number, name: string, properties: {FileName: string}}} FBXTextureNode
*/
/**
* @typedef {{id: number, attrName: string, properties: {ShadingModel: string, Diffuse: FBXVector3, Specular: FBXVector3, Shininess: FBXValue, Emissive: FBXVector3, EmissiveFactor: FBXValue, Opacity: FBXValue}}} FBXMaterialNode
*/
/**
* @typedef {{subNodes: {Indexes: FBXArrayNode, Weights: FBXArrayNode, Transform: FBXArrayNode, TransformLink: FBXArrayNode}, properties: { Mode: string }}} FBXSubDeformerNode
*/
/**
* @typedef {{id: number, attrName: string, attrType: string, subNodes: {Vertices: FBXArrayNode, PolygonVertexIndex: FBXArrayNode, LayerElementNormal: FBXMappedArrayNode[], LayerElementMaterial: FBXMappedArrayNode[], LayerElementUV: FBXMappedArrayNode[]}}} FBXGeometryNode
*/
/**
* @typedef {{id: number, attrName: string, attrType: string, properties: {Lcl_Translation: FBXValue, Lcl_Rotation: FBXValue, Lcl_Scaling: FBXValue}}} FBXModelNode
*/
/**
* Parses map of relationships between objects.
* @param {{Connections: { properties: { connections: [number, number, string][]}}}} FBXTree
* @returns {Map<number, {parents: {ID: number, relationship: string}[], children: {ID: number, relationship: string}[]}>}
*/
function parseConnections(FBXTree) {
/**
* @type {Map<number, { parents: {ID: number, relationship: string}[], children: {ID: number, relationship: string}[]}>}
*/
var connectionMap = new Map();
if ('Connections' in FBXTree) {
/**
* @type {[number, number, string][]}
*/
var connectionArray = FBXTree.Connections.properties.connections;
connectionArray.forEach(function (connection) {
if (!connectionMap.has(connection[0])) {
connectionMap.set(connection[0], {
parents: [],
children: []
});
}
var parentRelationship = { ID: connection[1], relationship: connection[2] };
connectionMap.get(connection[0]).parents.push(parentRelationship);
if (!connectionMap.has(connection[1])) {
connectionMap.set(connection[1], {
parents: [],
children: []
});
}
var childRelationship = { ID: connection[0], relationship: connection[2] };
connectionMap.get(connection[1]).children.push(childRelationship);
});
}
return connectionMap;
}
/**
* Parses map of textures referenced in FBXTree.
* @param {{Objects: {subNodes: {Texture: Object.<string, FBXTextureNode>}}}} FBXTree
* @returns {Map<number, THREE.Texture>}
*/
function parseTextures(FBXTree) {
/**
* @type {Map<number, THREE.Texture>}
*/
var textureMap = new Map();
if ('Texture' in FBXTree.Objects.subNodes) {
var textureNodes = FBXTree.Objects.subNodes.Texture;
for (var nodeID in textureNodes) {
var texture = parseTexture(textureNodes[nodeID]);
textureMap.set(parseInt(nodeID), texture);
}
}
return textureMap;
/**
* @param {textureNode} textureNode - Node to get texture information from.
* @returns {THREE.Texture}
*/
function parseTexture(textureNode) {
var FBX_ID = textureNode.id;
var name = textureNode.name;
var filePath = textureNode.properties.FileName;
var split = filePath.split(/[\\\/]/);
if (split.length > 0) {
var fileName = split[split.length - 1];
}
else {
var fileName = filePath;
}
/**
* @type {THREE.Texture}
*/
var texture = loader.textureLoader.load(resourceDirectory + '/' + fileName);
texture.name = name;
texture.FBX_ID = FBX_ID;
return texture;
}
}
/**
* Parses map of Material information.
* @param {{Objects: {subNodes: {Material: Object.<number, FBXMaterialNode>}}}} FBXTree
* @param {Map<number, THREE.Texture>} textureMap
* @param {Map<number, {parents: {ID: number, relationship: string}[], children: {ID: number, relationship: string}[]}>} connections
* @returns {Map<number, THREE.Material>}
*/
function parseMaterials(FBXTree, textureMap, connections) {
var materialMap = new Map();
if ('Material' in FBXTree.Objects.subNodes) {
var materialNodes = FBXTree.Objects.subNodes.Material;
for (var nodeID in materialNodes) {
var material = parseMaterial(materialNodes[nodeID], textureMap, connections);
materialMap.set(parseInt(nodeID), material);
}
}
return materialMap;
/**
* Takes information from Material node and returns a generated THREE.Material
* @param {FBXMaterialNode} materialNode
* @param {Map<number, THREE.Texture>} textureMap
* @param {Map<number, {parents: {ID: number, relationship: string}[], children: {ID: number, relationship: string}[]}>} connections
* @returns {THREE.Material}
*/
function parseMaterial(materialNode, textureMap, connections) {
var FBX_ID = materialNode.id;
var name = materialNode.attrName;
var type = materialNode.properties.ShadingModel;
var children = connections.get(FBX_ID).children;
var parameters = parseParameters(materialNode.properties, textureMap, children);
var material;
switch (type) {
case 'phong':
material = new THREE.MeshPhongMaterial();
break;
case 'lambert':
material = new THREE.MeshLambertMaterial();
break;
default:
console.warn('No implementation given for material type ' + type + ' in FBXLoader.js. Defaulting to basic material');
material = new THREE.MeshBasicMaterial({ color: 0x3300ff });
break;
}
material.setValues(parameters);
material.name = name;
return material;
/**
* @typedef {{Diffuse: FBXVector3, Specular: FBXVector3, Shininess: FBXValue, Emissive: FBXVector3, EmissiveFactor: FBXValue, Opacity: FBXValue}} FBXMaterialProperties
*/
/**
* @typedef {{color: THREE.Color=, specular: THREE.Color=, shininess: number=, emissive: THREE.Color=, emissiveIntensity: number=, opacity: number=, transparent: boolean=, map: THREE.Texture=}} THREEMaterialParameterPack
*/
/**
* @param {FBXMaterialProperties} properties
* @param {Map<number, THREE.Texture>} textureMap
* @param {{ID: number, relationship: string}[]} childrenRelationships
* @returns {THREEMaterialParameterPack}
*/
function parseParameters(properties, textureMap, childrenRelationships) {
var parameters = {};
if (properties.Diffuse) {
parameters.color = parseColor(properties.Diffuse);
}
if (properties.Specular) {
parameters.specular = parseColor(properties.Specular);
}
if (properties.Shininess) {
parameters.shininess = properties.Shininess.value;
}
if (properties.Emissive) {
parameters.emissive = parseColor(properties.Emissive);
}
if (properties.EmissiveFactor) {
parameters.emissiveIntensity = properties.EmissiveFactor.value;
}
if (properties.Opacity) {
parameters.opacity = properties.Opacity.value;
}
if (parameters.opacity < 1.0) {
parameters.transparent = true;
}
childrenRelationships.forEach(function (relationship) {
var type = relationship.relationship;
switch (type) {
case " \"AmbientColor":
//TODO: Support AmbientColor textures
break;
case " \"DiffuseColor":
parameters.map = textureMap.get(relationship.ID);
break;
default:
console.warn('Unknown texture application of type ' + type + ', skipping texture');
break;
}
});
return parameters;
}
}
}
/**
* Generates map of Skeleton-like objects for use later when generating and binding skeletons.
* @param {{Objects: {subNodes: {Deformer: Object.<number, FBXSubDeformerNode>}}}} FBXTree
* @param {Map<number, {parents: {ID: number, relationship: string}[], children: {ID: number, relationship: string}[]}>} connections
* @returns {Map<number, {map: Map<number, {FBX_ID: number, indices: number[], weights: number[], transform: number[], transformLink: number[], linkMode: string}>, array: {FBX_ID: number, indices: number[], weights: number[], transform: number[], transformLink: number[], linkMode: string}[], skeleton: THREE.Skeleton|null}>}
*/
function parseDeformers(FBXTree, connections) {
var skeletonMap = new Map();
if ('Deformer' in FBXTree.Objects.subNodes) {
var DeformerNodes = FBXTree.Objects.subNodes.Deformer;
for (var nodeID in DeformerNodes) {
var deformerNode = DeformerNodes[nodeID];
if (deformerNode.attrType === 'Skin') {
var conns = connections.get(parseInt(nodeID));
var skeleton = parseSkeleton(conns, DeformerNodes);
skeleton.FBX_ID = parseInt(nodeID);
skeletonMap.set(parseInt(nodeID), skeleton);
}
}
}
return skeletonMap;
/**
* Generates a "Skeleton Representation" of FBX nodes based on an FBX Skin Deformer's connections and an object containing SubDeformer nodes.
* @param {{parents: {ID: number, relationship: string}[], children: {ID: number, relationship: string}[]}} connections
* @param {Object.<number, FBXSubDeformerNode>} DeformerNodes
* @returns {{map: Map<number, {FBX_ID: number, indices: number[], weights: number[], transform: number[], transformLink: number[], linkMode: string}>, array: {FBX_ID: number, indices: number[], weights: number[], transform: number[], transformLink: number[], linkMode: string}[], skeleton: THREE.Skeleton|null}}
*/
function parseSkeleton(connections, DeformerNodes) {
var subDeformers = new Map();
var subDeformerArray = [];
connections.children.forEach(function (child) {
var subDeformerNode = DeformerNodes[child.ID];
var subDeformer = {
FBX_ID: child.ID,
indices: parseIntArray(subDeformerNode.subNodes.Indexes.properties.a),
weights: parseFloatArray(subDeformerNode.subNodes.Weights.properties.a),
transform: parseMatrixArray(subDeformerNode.subNodes.Transform.properties.a),
transformLink: parseMatrixArray(subDeformerNode.subNodes.TransformLink.properties.a),
linkMode: subDeformerNode.properties.Mode
};
subDeformers.set(child.ID, subDeformer);
subDeformerArray.push(subDeformer);
});
return {
map: subDeformers,
array: subDeformerArray,
bones: []
};
}
}
/**
* Generates Buffer geometries from geometry information in FBXTree, and generates map of THREE.BufferGeometries
* @param {{Objects: {subNodes: {Geometry: Object.<number, FBXGeometryNode}}}} FBXTree
* @param {Map<number, {parents: {ID: number, relationship: string}[], children: {ID: number, relationship: string}[]}>} connections
* @param {Map<number, {map: Map<number, {FBX_ID: number, indices: number[], weights: number[], transform: number[], transformLink: number[], linkMode: string}>, array: {FBX_ID: number, indices: number[], weights: number[], transform: number[], transformLink: number[], linkMode: string}[], skeleton: THREE.Skeleton|null}>} deformerMap
* @returns {Map<number, THREE.BufferGeometry>}
*/
function parseGeometries(FBXTree, connections, deformerMap) {
var geometryMap = new Map();
if ('Geometry' in FBXTree.Objects.subNodes) {
var geometryNodes = FBXTree.Objects.subNodes.Geometry;
for (var nodeID in geometryNodes) {
var relationships = connections.get(parseInt(nodeID));
var geo = parseGeometry(geometryNodes[nodeID], relationships, deformerMap);
geometryMap.set(parseInt(nodeID), geo);
}
}
return geometryMap;
/**
* Generates BufferGeometry from FBXGeometryNode.
* @param {FBXGeometryNode} geometryNode
* @param {{parents: {ID: number, relationship: string}[], children: {ID: number, relationship: string}[]}} relationships
* @param {Map<number, {map: Map<number, {FBX_ID: number, indices: number[], weights: number[], transform: number[], transformLink: number[], linkMode: string}>, array: {FBX_ID: number, indices: number[], weights: number[], transform: number[], transformLink: number[], linkMode: string}[]}>} deformerMap
* @returns {THREE.BufferGeometry}
*/
function parseGeometry(geometryNode, relationships, deformerMap) {
if (geometryNode.attrType === 'Mesh') {
return parseMeshGeometry(geometryNode, relationships, deformerMap);
}
else if (geometryNode.attrType === 'NurbsCurve') {
return parseNurbsGeometry(geometryNode, relationships);
}
/**
* Specialty function for parsing Mesh based Geometry Nodes.
* @param {FBXGeometryNode} geometryNode
* @param {{parents: {ID: number, relationship: string}[], children: {ID: number, relationship: string}[]}} relationships - Object representing relationships between specific geometry node and other nodes.
* @param {Map<number, {map: Map<number, {FBX_ID: number, indices: number[], weights: number[], transform: number[], transformLink: number[], linkMode: string}>, array: {FBX_ID: number, indices: number[], weights: number[], transform: number[], transformLink: number[], linkMode: string}[]}>} deformerMap - Map object of deformers and subDeformers by ID.
* @returns {THREE.BufferGeometry}
*/
function parseMeshGeometry(geometryNode, relationships, deformerMap) {
var FBX_ID = geometryNode.id;
var name = geometryNode.attrName;
for (var i = 0; i < relationships.children.length; ++i) {
if (deformerMap.has(relationships.children[i].ID)) {
var deformer = deformerMap.get(relationships.children[i].ID);
break;
}
}
var geometry = genGeometry(geometryNode, deformer);
return geometry;
/**
* @param {{map: Map<number, {FBX_ID: number, indices: number[], weights: number[], transform: number[], transformLink: number[], linkMode: string}>, array: {FBX_ID: number, indices: number[], weights: number[], transform: number[], transformLink: number[], linkMode: string}[]}} deformer - Skeleton representation for geometry instance.
* @returns {THREE.BufferGeometry}
*/
function genGeometry(geometryNode, deformer) {
var geometry = new Geometry();
//First, each index is going to be its own vertex.
var vertexBuffer = parseFloatArray(geometryNode.subNodes.Vertices.properties.a);
var indexBuffer = parseIntArray(geometryNode.subNodes.PolygonVertexIndex.properties.a);
if ('LayerElementNormal' in geometryNode.subNodes) {
var normalInfo = getNormals(geometryNode);
}
if ('LayerElementUV' in geometryNode.subNodes) {
var uvInfo = getUVs(geometryNode);
}
if ('LayerElementMaterial' in geometryNode.subNodes) {
var materialInfo = getMaterials(geometryNode);
}
var faceVertexBuffer = [];
var polygonIndex = 0;
for (var polygonVertexIndex = 0; polygonVertexIndex < indexBuffer.length; ++polygonVertexIndex) {
var endOfFace;
var vertexIndex = indexBuffer[polygonVertexIndex];
if (indexBuffer[polygonVertexIndex] < 0) {
vertexIndex = vertexIndex ^ -1;
indexBuffer[polygonVertexIndex] = vertexIndex;
endOfFace = true;
}
var vertex = new Vertex();
var weightIndices = [];
var weights = [];
vertex.position.fromArray(vertexBuffer, vertexIndex * 3);
// If we have a deformer for this geometry, get the skinIndex and skinWeights for this object.
// They are stored as vertex indices on each deformer, and we need them as deformer indices
// for each vertex.
if (deformer) {
for (var j = 0; j < deformer.array.length; ++j) {
var index = deformer.array[j].indices.findIndex(function (index) {
return index === indexBuffer[polygonVertexIndex];
});
if (index !== -1) {
weights.push(deformer.array[j].weights[index]);
weightIndices.push(j);
}
}
if (weights.length > 4) {
console.warn('FBXLoader: Vertex has more than 4 skinning weights assigned to vertex. Deleting additional weights.');
var WIndex = [0, 0, 0, 0];
var Weight = [0, 0, 0, 0];
for (var polygonVertexIndex = 0; polygonVertexIndex < weights.length; ++polygonVertexIndex) {
var currentWeight = weights[polygonVertexIndex];
var currentIndex = weightIndices[polygonVertexIndex];
for (var j = 0; j < Weight.length; ++j) {
if (currentWeight > Weight[j]) {
var tmp = Weight[j];
Weight[j] = currentWeight;
currentWeight = tmp;
tmp = WIndex[j];
WIndex[j] = currentIndex;
currentIndex = tmp;
}
}
}
weightIndices = WIndex;
weights = Weight;
}
for (var i = weights.length; i < 4; i++) {
weights[i] = 0;
weightIndices[i] = 0;
}
vertex.skinWeights.fromArray(weights);
vertex.skinIndices.fromArray(weightIndices);
}
if (normalInfo) {
vertex.normal.fromArray(getData(polygonVertexIndex, polygonIndex, vertexIndex, normalInfo));
}
if (uvInfo) {
vertex.uv.fromArray(getData(polygonVertexIndex, polygonIndex, vertexIndex, uvInfo));
}
//Add vertex to face buffer.
faceVertexBuffer.push(vertex);
// If index was negative to start with, we have finished this individual face
// and can generate the face data to the geometry.
if (endOfFace) {
var face = new Face();
var materials = getData(polygonVertexIndex, polygonIndex, vertexIndex, materialInfo);
face.genTrianglesFromVertices(faceVertexBuffer);
face.materialIndex = materials[0];
geometry.faces.push(face);
faceVertexBuffer = [];
polygonIndex++;
endOfFace = false;
}
}
/**
* @type {{vertexBuffer: number[], normalBuffer: number[], uvBuffer: number[], skinIndexBuffer: number[], skinWeightBuffer: number[], materialIndexBuffer: number[]}}
*/
var bufferInfo = geometry.flattenToBuffers();
var geo = new THREE.BufferGeometry();
geo.name = geometryNode.name;
geo.addAttribute('position', new THREE.BufferAttribute(new Float32Array(bufferInfo.vertexBuffer), 3));
if (bufferInfo.normalBuffer.length > 0) {
geo.addAttribute('normal', new THREE.BufferAttribute(new Float32Array(bufferInfo.normalBuffer), 3));
}
if (bufferInfo.uvBuffer.length > 0) {
geo.addAttribute('uv', new THREE.BufferAttribute(new Float32Array(bufferInfo.uvBuffer), 2));
}
if (deformer) {
geo.addAttribute('skinIndex', new THREE.BufferAttribute(new Float32Array(bufferInfo.skinIndexBuffer), 4));
geo.addAttribute('skinWeight', new THREE.BufferAttribute(new Float32Array(bufferInfo.skinWeightBuffer), 4));
geo.FBX_Deformer = deformer;
}
// Convert the material indices of each vertex into rendering groups on the geometry.
var prevMaterialIndex = bufferInfo.materialIndexBuffer[0];
var startIndex = 0;
for (var materialBufferIndex = 0; materialBufferIndex < bufferInfo.materialIndexBuffer.length; ++materialBufferIndex) {
if (bufferInfo.materialIndexBuffer[materialBufferIndex] !== prevMaterialIndex) {
geo.addGroup(startIndex, materialBufferIndex - startIndex, prevMaterialIndex);
startIndex = materialBufferIndex;
prevMaterialIndex = bufferInfo.materialIndexBuffer[materialBufferIndex];
}
}
return geo;
/**
* Parses normal information for geometry.
* @param {FBXGeometryNode} geometryNode
* @returns {{dataSize: number, buffer: number[], indices: number[], mappingType: string, referenceType: string}}
*/
function getNormals(geometryNode) {
var NormalNode = geometryNode.subNodes.LayerElementNormal[0];
var mappingType = NormalNode.properties.MappingInformationType;
var referenceType = NormalNode.properties.ReferenceInformationType;
var buffer = parseFloatArray(NormalNode.subNodes.Normals.properties.a);
var indexBuffer = [];
if (referenceType === 'IndexToDirect') {
indexBuffer = parseIntArray(NormalNode.subNodes.NormalIndex.properties.a);
}
return {
dataSize: 3,
buffer: buffer,
indices: indexBuffer,
mappingType: mappingType,
referenceType: referenceType
};
}
/**
* Parses UV information for geometry.
* @param {FBXGeometryNode} geometryNode
* @returns {{dataSize: number, buffer: number[], indices: number[], mappingType: string, referenceType: string}}
*/
function getUVs(geometryNode) {
var UVNode = geometryNode.subNodes.LayerElementUV[0];
var mappingType = UVNode.properties.MappingInformationType;
var referenceType = UVNode.properties.ReferenceInformationType;
var buffer = parseFloatArray(UVNode.subNodes.UV.properties.a);
var indexBuffer = [];
if (referenceType === 'IndexToDirect') {
indexBuffer = parseIntArray(UVNode.subNodes.UVIndex.properties.a);
}
return {
dataSize: 2,
buffer: buffer,
indices: indexBuffer,
mappingType: mappingType,
referenceType: referenceType
};
}
/**
* Parses material application information for geometry.
* @param {FBXGeometryNode}
* @returns {{dataSize: number, buffer: number[], indices: number[], mappingType: string, referenceType: string}}
*/
function getMaterials(geometryNode) {
var MaterialNode = geometryNode.subNodes.LayerElementMaterial[0];
var mappingType = MaterialNode.properties.MappingInformationType;
var referenceType = MaterialNode.properties.ReferenceInformationType;
var materialIndexBuffer = parseIntArray(MaterialNode.subNodes.Materials.properties.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 = [];
materialIndexBuffer.forEach(function (materialIndex, index) {
materialIndices.push(index);
});
return {
dataSize: 1,
buffer: materialIndexBuffer,
indices: materialIndices,
mappingType: mappingType,
referenceType: referenceType
};
}
/**
* Function uses the infoObject and given indices to return value array of object.
* @param {number} polygonVertexIndex - Index of vertex in draw order (which index of the index buffer refers to this vertex).
* @param {number} polygonIndex - Index of polygon in geometry.
* @param {number} vertexIndex - Index of vertex inside vertex buffer (used because some data refers to old index buffer that we don't use anymore).
* @param {{datasize: number, buffer: number[], indices: number[], mappingType: string, referenceType: string}} infoObject - Object containing data and how to access data.
* @returns {number[]}
*/
function getData(polygonVertexIndex, polygonIndex, vertexIndex, infoObject) {
var GetData = {
ByPolygonVertex: {
/**
* Function uses the infoObject and given indices to return value array of object.
* @param {number} polygonVertexIndex - Index of vertex in draw order (which index of the index buffer refers to this vertex).
* @param {number} polygonIndex - Index of polygon in geometry.
* @param {number} vertexIndex - Index of vertex inside vertex buffer (used because some data refers to old index buffer that we don't use anymore).
* @param {{datasize: number, buffer: number[], indices: number[], mappingType: string, referenceType: string}} infoObject - Object containing data and how to access data.
* @returns {number[]}
*/
Direct: function (polygonVertexIndex, polygonIndex, vertexIndex, infoObject) {
return infoObject.buffer.slice((polygonVertexIndex * infoObject.dataSize), (polygonVertexIndex * infoObject.dataSize) + infoObject.dataSize);
},
/**
* Function uses the infoObject and given indices to return value array of object.
* @param {number} polygonVertexIndex - Index of vertex in draw order (which index of the index buffer refers to this vertex).
* @param {number} polygonIndex - Index of polygon in geometry.
* @param {number} vertexIndex - Index of vertex inside vertex buffer (used because some data refers to old index buffer that we don't use anymore).
* @param {{datasize: number, buffer: number[], indices: number[], mappingType: string, referenceType: string}} infoObject - Object containing data and how to access data.
* @returns {number[]}
*/
IndexToDirect: function (polygonVertexIndex, polygonIndex, vertexIndex, infoObject) {
var index = infoObject.indices[polygonVertexIndex];
return infoObject.buffer.slice((index * infoObject.dataSize), (index * infoObject.dataSize) + infoObject.dataSize);
}
},
ByPolygon: {
/**
* Function uses the infoObject and given indices to return value array of object.
* @param {number} polygonVertexIndex - Index of vertex in draw order (which index of the index buffer refers to this vertex).
* @param {number} polygonIndex - Index of polygon in geometry.
* @param {number} vertexIndex - Index of vertex inside vertex buffer (used because some data refers to old index buffer that we don't use anymore).
* @param {{datasize: number, buffer: number[], indices: number[], mappingType: string, referenceType: string}} infoObject - Object containing data and how to access data.
* @returns {number[]}
*/
Direct: function (polygonVertexIndex, polygonIndex, vertexIndex, infoObject) {
return infoObject.buffer.slice(polygonIndex * infoObject.dataSize, polygonIndex * infoObject.dataSize + infoObject.dataSize);
},
/**
* Function uses the infoObject and given indices to return value array of object.
* @param {number} polygonVertexIndex - Index of vertex in draw order (which index of the index buffer refers to this vertex).
* @param {number} polygonIndex - Index of polygon in geometry.
* @param {number} vertexIndex - Index of vertex inside vertex buffer (used because some data refers to old index buffer that we don't use anymore).
* @param {{datasize: number, buffer: number[], indices: number[], mappingType: string, referenceType: string}} infoObject - Object containing data and how to access data.
* @returns {number[]}
*/
IndexToDirect: function (polygonVertexIndex, polygonIndex, vertexIndex, infoObject) {
var index = infoObject.indices[polygonIndex];
return infoObject.buffer.slice(index * infoObject.dataSize, index * infoObject.dataSize + infoObject.dataSize);
}
},
AllSame: {
/**
* Function uses the infoObject and given indices to return value array of object.
* @param {number} polygonVertexIndex - Index of vertex in draw order (which index of the index buffer refers to this vertex).
* @param {number} polygonIndex - Index of polygon in geometry.
* @param {number} vertexIndex - Index of vertex inside vertex buffer (used because some data refers to old index buffer that we don't use anymore).
* @param {{datasize: number, buffer: number[], indices: number[], mappingType: string, referenceType: string}} infoObject - Object containing data and how to access data.
* @returns {number[]}
*/
IndexToDirect: function (polygonVertexIndex, polygonIndex, vertexIndex, infoObject) {
return infoObject.buffer.slice(infoObject.indices[0] * infoObject.dataSize, infoObject.indices[0] * infoObject.dataSize + infoObject.dataSize);
}
}
};
return GetData[infoObject.mappingType][infoObject.referenceType](polygonVertexIndex, polygonIndex, vertexIndex, infoObject);
}
}
}
/**
* Specialty function for parsing NurbsCurve based Geometry Nodes.
* @param {FBXGeometryNode} geometryNode
* @param {{parents: {ID: number, relationship: string}[], children: {ID: number, relationship: string}[]}} relationships
* @returns {THREE.BufferGeometry}
*/
function parseNurbsGeometry(geometryNode, relationships) {
if (THREE.NURBSCurve === undefined) {
console.error("THREE.FBXLoader 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(geometryNode.properties.Order);
if (isNaN(order)) {
console.error("FBXLoader: Invalid Order " + geometryNode.properties.Order + " given for geometry ID: " + geometryNode.id);
return new THREE.BufferGeometry();
}
var knots = parseFloatArray(geometryNode.subNodes.KnotVector.properties.a);
var controlPoints = [];
var pointsValues = parseFloatArray(geometryNode.subNodes.Points.properties.a);
for (var i = 0; i < pointsValues.length; i += 4) {
controlPoints.push(new THREE.Vector4(pointsValues[i], pointsValues[i + 1], pointsValues[i + 2], pointsValues[i + 3]));
}
if (geometryNode.properties.Form === 'Closed') {
controlPoints.push(controlPoints[0]);
}
var curve = new THREE.NURBSCurve(order - 1, knots, controlPoints);
var vertices = curve.getPoints(controlPoints.length * 1.5);
var vertexBuffer = [];
vertices.forEach(function (position) {
var array = position.toArray();
vertexBuffer = vertexBuffer.concat(array);
});
var geometry = new THREE.BufferGeometry();
geometry.addAttribute('position', new THREE.BufferAttribute(new Float32Array(vertexBuffer), 3));
return geometry;
}
}
}
/**
* Finally generates Scene graph and Scene graph Objects.
* @param {{Objects: {subNodes: {Model: Object.<number, FBXModelNode>}}}} FBXTree
* @param {Map<number, {parents: {ID: number, relationship: string}[], children: {ID: number, relationship: string}[]}>} connections
* @param {Map<number, {map: Map<number, {FBX_ID: number, indices: number[], weights: number[], transform: number[], transformLink: number[], linkMode: string}>, array: {FBX_ID: number, indices: number[], weights: number[], transform: number[], transformLink: number[], linkMode: string}[], skeleton: THREE.Skeleton|null}>} deformerMap
* @param {Map<number, THREE.BufferGeometry>} geometryMap
* @param {Map<number, THREE.Material>} materialMap
* @returns {THREE.Group}
*/
function parseScene(FBXTree, connections, deformerMap, geometryMap, materialMap) {
var sceneGraph = new THREE.Group();
var ModelNode = FBXTree.Objects.subNodes.Model;
/**
* @type {Array.<THREE.Object3D>}
*/
var modelArray = [];
/**
* @type {Map.<number, THREE.Object3D>}
*/
var modelMap = new Map();
for (var nodeID in ModelNode) {
var id = parseInt(nodeID);
var node = ModelNode[nodeID];
var conns = connections.get(id);
var model = null;
for (var i = 0; i < conns.parents.length; ++i) {
deformerMap.forEach(function (deformer) {
if (deformer.map.has(conns.parents[i].ID)) {
model = new THREE.Bone();
var index = deformer.array.findIndex(function (subDeformer) {
return subDeformer.FBX_ID === conns.parents[i].ID;
});
deformer.bones[index] = model;
}
});
}
if (!model) {
switch (node.attrType) {
case "Mesh":
/**
* @type {?THREE.BufferGeometry}
*/
var geometry = null;
/**
* @type {THREE.MultiMaterial|THREE.Material}
*/
var material = null;
/**
* @type {Array.<THREE.Material>}
*/
var materials = [];
conns.children.forEach(function (child) {
if (geometryMap.has(child.ID)) {
geometry = geometryMap.get(child.ID);
}
if (materialMap.has(child.ID)) {
materials.push(materialMap.get(child.ID));
}
});
if (materials.length > 1) {
material = new THREE.MultiMaterial(materials);
}
else if (materials.length > 0) {
material = materials[0];
}
else {
material = new THREE.MeshBasicMaterial({ color: 0x3300ff });
}
if (geometry.FBX_Deformer) {
materials.forEach(function (material) {
material.skinning = true;
});
material.skinning = true;
model = new THREE.SkinnedMesh(geometry, material);
}
else {
model = new THREE.Mesh(geometry, material);
}
break;
case "NurbsCurve":
var geometry = null;
conns.children.forEach(function (child) {
if (geometryMap.has(child.ID)) {
geometry = geometryMap.get(child.ID);
}
});
// FBX does not list materials for Nurbs lines, so we'll just put our own in here.
material = new THREE.LineBasicMaterial({ color: 0x3300ff, linewidth: 5 });
model = new THREE.Line(geometry, material);
break;
default:
model = new THREE.Object3D();
break;
}
}
model.name = node.attrName.replace(/:/, '').replace(/_/, '').replace(/-/, '');
model.FBX_ID = id;
modelArray.push(model);
modelMap.set(id, model);
}
modelArray.forEach(function (model) {
var node = ModelNode[model.FBX_ID];
if ('Lcl_Translation' in node.properties) {
model.position.fromArray(parseFloatArray(node.properties.Lcl_Translation.value));
}
if ('Lcl_Rotation' in node.properties) {
var rotation = parseFloatArray(node.properties.Lcl_Rotation.value).map(function (value) {
return value * Math.PI / 180;
});
rotation.push('ZYX');
model.rotation.fromArray(rotation);
}
if ('Lcl_Scaling' in node.properties) {
model.scale.fromArray(parseFloatArray(node.properties.Lcl_Scaling.value));
}
var conns = connections.get(model.FBX_ID);
for (var parentIndex = 0; parentIndex < conns.parents.length; parentIndex++) {
var pIndex = modelArray.findIndex(function (mod) {
return mod.FBX_ID === conns.parents[parentIndex].ID;
});
if (pIndex > -1) {
modelArray[pIndex].add(model);
break;
}
}
if (model.parent === null) {
sceneGraph.add(model);
}
});
// Now with the bones created, we can update the skeletons and bind them to the skinned meshes.
sceneGraph.updateMatrixWorld(true);
// Put skeleton into bind pose.
var BindPoseNode = FBXTree.Objects.subNodes.Pose;
for (var nodeID in BindPoseNode) {
if (BindPoseNode[nodeID].attrType === 'BindPose') {
BindPoseNode = BindPoseNode[nodeID];
break;
}
}
if (BindPoseNode) {
var PoseNode = BindPoseNode.subNodes.PoseNode;
var worldMatrices = new Map();
PoseNode.forEach(function (node) {
var rawMatWrd = parseMatrixArray(node.subNodes.Matrix.properties.a);
worldMatrices.set(parseInt(node.id), rawMatWrd);
});
}
deformerMap.forEach(function (deformer, FBX_ID) {
deformer.array.forEach(function (subDeformer, subDeformerIndex) {
/**
* @type {THREE.Bone}
*/
var bone = deformer.bones[subDeformerIndex];
if (!worldMatrices.has(bone.FBX_ID)) {
return;
}
var mat = worldMatrices.get(bone.FBX_ID);
bone.matrixWorld.copy(mat);
});
// Now that skeleton is in bind pose, bind to model.
deformer.skeleton = new THREE.Skeleton(deformer.bones);
var conns = connections.get(FBX_ID);
conns.parents.forEach(function (parent) {
if (geometryMap.has(parent.ID)) {
var geoID = parent.ID;
var geoConns = connections.get(geoID);
for (var i = 0; i < geoConns.parents.length; ++i) {
if (modelMap.has(geoConns.parents[i].ID)) {
var model = modelMap.get(geoConns.parents[i].ID);
//ASSERT model typeof SkinnedMesh
model.bind(deformer.skeleton, model.matrixWorld);
break;
}
}
}
});
});
// Skeleton is now bound, we are now free to set up the
// scene graph.
modelArray.forEach(function (model) {
var node = ModelNode[model.FBX_ID];
if ('Lcl_Translation' in node.properties) {
model.position.fromArray(parseFloatArray(node.properties.Lcl_Translation.value));
}
if ('Lcl_Rotation' in node.properties) {
var rotation = parseFloatArray(node.properties.Lcl_Rotation.value).map(function (value) {
return value * Math.PI / 180;
});
rotation.push('ZYX');
model.rotation.fromArray(rotation);
}
if ('Lcl_Scaling' in node.properties) {
model.scale.fromArray(parseFloatArray(node.properties.Lcl_Scaling.value));
}
});
// Silly hack with the animation parsing. We're gonna pretend the scene graph has a skeleton
// to attach animations to, since FBXs treat animations as animations for the entire scene,
// not just for individual objects.
sceneGraph.skeleton = {
bones: modelArray
};
var animations = parseAnimations(FBXTree, connections, sceneGraph);
addAnimations(sceneGraph, animations);
return sceneGraph;
}
/**
* Parses animation information from FBXTree and generates an AnimationInfoObject.
* @param {{Objects: {subNodes: {AnimationCurveNode: any, AnimationCurve: any, AnimationLayer: any, AnimationStack: any}}}} FBXTree
* @param {Map<number, {parents: {ID: number, relationship: string}[], children: {ID: number, relationship: string}[]}>} connections
*/
function parseAnimations(FBXTree, connections, sceneGraph) {
var rawNodes = FBXTree.Objects.subNodes.AnimationCurveNode;
var rawCurves = FBXTree.Objects.subNodes.AnimationCurve;
var rawLayers = FBXTree.Objects.subNodes.AnimationLayer;
var rawStacks = FBXTree.Objects.subNodes.AnimationStack;
/**
* @type {{
curves: Map<number, {
T: {
id: number;
attr: string;
internalID: number;
attrX: boolean;
attrY: boolean;
attrZ: boolean;
containerBoneID: number;
containerID: number;
curves: {
x: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
y: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
z: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
};
},
R: {
id: number;
attr: string;
internalID: number;
attrX: boolean;
attrY: boolean;
attrZ: boolean;
containerBoneID: number;
containerID: number;
curves: {
x: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
y: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
z: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
};
},
S: {
id: number;
attr: string;
internalID: number;
attrX: boolean;
attrY: boolean;
attrZ: boolean;
containerBoneID: number;
containerID: number;
curves: {
x: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
y: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
z: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
};
}
}>,
layers: Map<number, {
T: {
id: number;
attr: string;
internalID: number;
attrX: boolean;
attrY: boolean;
attrZ: boolean;
containerBoneID: number;
containerID: number;
curves: {
x: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
y: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
z: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
},
},
R: {
id: number;
attr: string;
internalID: number;
attrX: boolean;
attrY: boolean;
attrZ: boolean;
containerBoneID: number;
containerID: number;
curves: {
x: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
y: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
z: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
},
},
S: {
id: number;
attr: string;
internalID: number;
attrX: boolean;
attrY: boolean;
attrZ: boolean;
containerBoneID: number;
containerID: number;
curves: {
x: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
y: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
z: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
},
}
}[]>,
stacks: Map<number, {
name: string,
layers: {
T: {
id: number;
attr: string;
internalID: number;
attrX: boolean;
attrY: boolean;
attrZ: boolean;
containerBoneID: number;
containerID: number;
curves: {
x: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
y: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
z: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
};
};
R: {
id: number;
attr: string;
internalID: number;
attrX: boolean;
attrY: boolean;
attrZ: boolean;
containerBoneID: number;
containerID: number;
curves: {
x: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
y: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
z: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
};
};
S: {
id: number;
attr: string;
internalID: number;
attrX: boolean;
attrY: boolean;
attrZ: boolean;
containerBoneID: number;
containerID: number;
curves: {
x: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
y: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
z: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
};
};
}[][],
length: number,
frames: number }>,
length: number,
fps: number,
frames: number
}}
*/
var returnObject = {
curves: new Map(),
layers: new Map(),
stacks: new Map(),
length: 0,
fps: 30,
frames: 0
};
/**
* @type {Array.<{
id: number;
attr: string;
internalID: number;
attrX: boolean;
attrY: boolean;
attrZ: boolean;
containerBoneID: number;
containerID: number;
}>}
*/
var animationCurveNodes = [];
for (var nodeID in rawNodes) {
if (nodeID.match(/\d+/)) {
var animationNode = parseAnimationNode(FBXTree, rawNodes[nodeID], connections, sceneGraph);
animationCurveNodes.push(animationNode);
}
}
/**
* @type {Map.<number, {
id: number,
attr: string,
internalID: number,
attrX: boolean,
attrY: boolean,
attrZ: boolean,
containerBoneID: number,
containerID: number,
curves: {
x: {
version: any,
id: number,
internalID: number,
times: number[],
values: number[],
attrFlag: number[],
attrData: number[],
},
y: {
version: any,
id: number,
internalID: number,
times: number[],
values: number[],
attrFlag: number[],
attrData: number[],
},
z: {
version: any,
id: number,
internalID: number,
times: number[],
values: number[],
attrFlag: number[],
attrData: number[],
}
}
}>}
*/
var tmpMap = new Map();
for (var animationCurveNodeIndex = 0; animationCurveNodeIndex < animationCurveNodes.length; ++animationCurveNodeIndex) {
if (animationCurveNodes[animationCurveNodeIndex] === null) {
continue;
}
tmpMap.set(animationCurveNodes[animationCurveNodeIndex].id, animationCurveNodes[animationCurveNodeIndex]);
}
/**
* @type {{
version: any,
id: number,
internalID: number,
times: number[],
values: number[],
attrFlag: number[],
attrData: number[],
}[]}
*/
var animationCurves = [];
for (nodeID in rawCurves) {
if (nodeID.match(/\d+/)) {
var animationCurve = parseAnimationCurve(rawCurves[nodeID]);
animationCurves.push(animationCurve);
var firstParentConn = connections.get(animationCurve.id).parents[0];
var firstParentID = firstParentConn.ID;
var firstParentRelationship = firstParentConn.relationship;
var axis = '';
if (firstParentRelationship.match(/X/)) {
axis = 'x';
}
else if (firstParentRelationship.match(/Y/)) {
axis = 'y';
}
else if (firstParentRelationship.match(/Z/)) {
axis = 'z';
}
else {
continue;
}
tmpMap.get(firstParentID).curves[axis] = animationCurve;
}
}
tmpMap.forEach(function (curveNode) {
var id = curveNode.containerBoneID;
if (!returnObject.curves.has(id)) {
returnObject.curves.set(id, { T: null, R: null, S: null });
}
returnObject.curves.get(id)[curveNode.attr] = curveNode;
});
for (var nodeID in rawLayers) {
/**
* @type {{
T: {
id: number;
attr: string;
internalID: number;
attrX: boolean;
attrY: boolean;
attrZ: boolean;
containerBoneID: number;
containerID: number;
curves: {
x: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
y: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
z: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
},
},
R: {
id: number;
attr: string;
internalID: number;
attrX: boolean;
attrY: boolean;
attrZ: boolean;
containerBoneID: number;
containerID: number;
curves: {
x: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
y: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
z: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
},
},
S: {
id: number;
attr: string;
internalID: number;
attrX: boolean;
attrY: boolean;
attrZ: boolean;
containerBoneID: number;
containerID: number;
curves: {
x: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
y: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
z: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
},
}
}[]}
*/
var layer = [];
var children = connections.get(parseInt(nodeID)).children;
for (var childIndex = 0; childIndex < children.length; childIndex++) {
// Skip lockInfluenceWeights
if (tmpMap.has(children[childIndex].ID)) {
var curveNode = tmpMap.get(children[childIndex].ID);
var boneID = curveNode.containerBoneID;
if (layer[boneID] === undefined) {
layer[boneID] = {
T: null,
R: null,
S: null
};
}
layer[boneID][curveNode.attr] = curveNode;
}
}
returnObject.layers.set(parseInt(nodeID), layer);
}
for (var nodeID in rawStacks) {
var layers = [];
var children = connections.get(parseInt(nodeID)).children;
var maxTimeStamp = 0;
var minTimeStamp = Number.MAX_VALUE;
for (var childIndex = 0; childIndex < children.length; ++childIndex) {
if (returnObject.layers.has(children[childIndex].ID)) {
var currentLayer = returnObject.layers.get(children[childIndex].ID);
layers.push(currentLayer);
currentLayer.forEach(function (layer) {
if (layer) {
getCurveNodeMaxMinTimeStamps(layer);
}
/**
* Sets the maxTimeStamp and minTimeStamp variables if it has timeStamps that are either larger or smaller
* than the max or min respectively.
* @param {{
T: {
id: number,
attr: string,
internalID: number,
attrX: boolean,
attrY: boolean,
attrZ: boolean,
containerBoneID: number,
containerID: number,
curves: {
x: {
version: any,
id: number,
internalID: number,
times: number[],
values: number[],
attrFlag: number[],
attrData: number[],
},
y: {
version: any,
id: number,
internalID: number,
times: number[],
values: number[],
attrFlag: number[],
attrData: number[],
},
z: {
version: any,
id: number,
internalID: number,
times: number[],
values: number[],
attrFlag: number[],
attrData: number[],
},
},
},
R: {
id: number,
attr: string,
internalID: number,
attrX: boolean,
attrY: boolean,
attrZ: boolean,
containerBoneID: number,
containerID: number,
curves: {
x: {
version: any,
id: number,
internalID: number,
times: number[],
values: number[],
attrFlag: number[],
attrData: number[],
},
y: {
version: any,
id: number,
internalID: number,
times: number[],
values: number[],
attrFlag: number[],
attrData: number[],
},
z: {
version: any,
id: number,
internalID: number,
times: number[],
values: number[],
attrFlag: number[],
attrData: number[],
},
},
},
S: {
id: number,
attr: string,
internalID: number,
attrX: boolean,
attrY: boolean,
attrZ: boolean,
containerBoneID: number,
containerID: number,
curves: {
x: {
version: any,
id: number,
internalID: number,
times: number[],
values: number[],
attrFlag: number[],
attrData: number[],
},
y: {
version: any,
id: number,
internalID: number,
times: number[],
values: number[],
attrFlag: number[],
attrData: number[],
},
z: {
version: any,
id: number,
internalID: number,
times: number[],
values: number[],
attrFlag: number[],
attrData: number[],
},
},
},
}} layer
*/
function getCurveNodeMaxMinTimeStamps(layer) {
/**
* Sets the maxTimeStamp and minTimeStamp if one of the curve's time stamps
* exceeds the maximum or minimum.
* @param {{
x: {
version: any,
id: number,
internalID: number,
times: number[],
values: number[],
attrFlag: number[],
attrData: number[],
},
y: {
version: any,
id: number,
internalID: number,
times: number[],
values: number[],
attrFlag: number[],
attrData: number[],
},
z: {
version: any,
id: number,
internalID: number,
times: number[],
values: number[],
attrFlag: number[],
attrData: number[],
}
}} curve
*/
function getCurveMaxMinTimeStamp(curve) {
/**
* Sets the maxTimeStamp and minTimeStamp if one of its timestamps exceeds the maximum or minimum.
* @param {{times: number[]}} axis
*/
function getCurveAxisMaxMinTimeStamps(axis) {
maxTimeStamp = axis.times[axis.times.length - 1] > maxTimeStamp ? axis.times[axis.times.length - 1] : maxTimeStamp;
minTimeStamp = axis.times[0] < minTimeStamp ? axis.times[0] : minTimeStamp;
}
if (curve.x) {
getCurveAxisMaxMinTimeStamps(curve.x);
}
if (curve.y) {
getCurveAxisMaxMinTimeStamps(curve.y);
}
if (curve.z) {
getCurveAxisMaxMinTimeStamps(curve.z);
}
}
if (layer.R) {
getCurveMaxMinTimeStamp(layer.R.curves);
}
if (layer.S) {
getCurveMaxMinTimeStamp(layer.S.curves);
}
if (layer.T) {
getCurveMaxMinTimeStamp(layer.T.curves);
}
}
});
}
}
// Do we have an animation clip with actual length?
if (maxTimeStamp > minTimeStamp) {
returnObject.stacks.set(parseInt(nodeID), {
name: rawStacks[nodeID].attrName,
layers: layers,
length: maxTimeStamp - minTimeStamp,
frames: (maxTimeStamp - minTimeStamp) * 30
});
}
}
return returnObject;
/**
* @param {Object} FBXTree
* @param {{id: number, attrName: string, properties: Object<string, any>}} animationCurveNode
* @param {Map<number, {parents: {ID: number, relationship: string}[], children: {ID: number, relationship: string}[]}>} connections
* @param {{skeleton: {bones: {FBX_ID: number}[]}}} sceneGraph
*/
function parseAnimationNode(FBXTree, animationCurveNode, connections, sceneGraph) {
var returnObject = {
/**
* @type {number}
*/
id: animationCurveNode.id,
/**
* @type {string}
*/
attr: animationCurveNode.attrName,
/**
* @type {number}
*/
internalID: animationCurveNode.id,
/**
* @type {boolean}
*/
attrX: false,
/**
* @type {boolean}
*/
attrY: false,
/**
* @type {boolean}
*/
attrZ: false,
/**
* @type {number}
*/
containerBoneID: -1,
/**
* @type {number}
*/
containerID: -1,
curves: {
x: null,
y: null,
z: null
}
};
if (returnObject.attr.match(/S|R|T/)) {
for (var attributeKey in animationCurveNode.properties) {
if (attributeKey.match(/X/)) {
returnObject.attrX = true;
}
if (attributeKey.match(/Y/)) {
returnObject.attrY = true;
}
if (attributeKey.match(/Z/)) {
returnObject.attrZ = true;
}
}
}
else {
return null;
}
var conns = connections.get(returnObject.id);
var containerIndices = conns.parents;
for (var containerIndicesIndex = containerIndices.length - 1; containerIndicesIndex >= 0; --containerIndicesIndex) {
var boneID = sceneGraph.skeleton.bones.findIndex(function (bone) {
return bone.FBX_ID === containerIndices[containerIndicesIndex].ID;
});
if (boneID > -1) {
returnObject.containerBoneID = boneID;
returnObject.containerID = containerIndices[containerIndicesIndex].ID;
break;
}
}
return returnObject;
}
/**
* @param {{id: number, subNodes: {KeyTime: {properties: {a: string}}, KeyValueFloat: {properties: {a: string}}, KeyAttrFlags: {properties: {a: string}}, KeyAttrDataFloat: {properties: {a: string}}}}} animationCurve
*/
function parseAnimationCurve(animationCurve) {
return {
version: null,
id: animationCurve.id,
internalID: animationCurve.id,
times: parseFloatArray(animationCurve.subNodes.KeyTime.properties.a).map(function (time) {
return ConvertFBXTimeToSeconds(time);
}),
values: parseFloatArray(animationCurve.subNodes.KeyValueFloat.properties.a),
attrFlag: parseIntArray(animationCurve.subNodes.KeyAttrFlags.properties.a),
attrData: parseFloatArray(animationCurve.subNodes.KeyAttrDataFloat.properties.a)
};
}
}
/**
* @param {{
curves: Map<number, {
T: {
id: number;
attr: string;
internalID: number;
attrX: boolean;
attrY: boolean;
attrZ: boolean;
containerBoneID: number;
containerID: number;
curves: {
x: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
y: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
z: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
};
};
R: {
id: number;
attr: string;
internalID: number;
attrX: boolean;
attrY: boolean;
attrZ: boolean;
containerBoneID: number;
containerID: number;
curves: {
x: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
y: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
z: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
};
};
S: {
id: number;
attr: string;
internalID: number;
attrX: boolean;
attrY: boolean;
attrZ: boolean;
containerBoneID: number;
containerID: number;
curves: {
x: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
y: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
z: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
};
};
}>;
layers: Map<number, {
T: {
id: number;
attr: string;
internalID: number;
attrX: boolean;
attrY: boolean;
attrZ: boolean;
containerBoneID: number;
containerID: number;
curves: {
x: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
y: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
z: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
};
};
R: {
id: number;
attr: string;
internalID: number;
attrX: boolean;
attrY: boolean;
attrZ: boolean;
containerBoneID: number;
containerID: number;
curves: {
x: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
y: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
z: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
};
};
S: {
id: number;
attr: string;
internalID: number;
attrX: boolean;
attrY: boolean;
attrZ: boolean;
containerBoneID: number;
containerID: number;
curves: {
x: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
y: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
z: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
};
};
}[]>;
stacks: Map<number, {
name: string;
layers: {
T: {
id: number;
attr: string;
internalID: number;
attrX: boolean;
attrY: boolean;
attrZ: boolean;
containerBoneID: number;
containerID: number;
curves: {
x: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
y: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
z: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
};
};
R: {
id: number;
attr: string;
internalID: number;
attrX: boolean;
attrY: boolean;
attrZ: boolean;
containerBoneID: number;
containerID: number;
curves: {
x: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
y: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
z: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
};
};
S: {
id: number;
attr: string;
internalID: number;
attrX: boolean;
attrY: boolean;
attrZ: boolean;
containerBoneID: number;
containerID: number;
curves: {
x: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
y: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
z: {
version: any;
id: number;
internalID: number;
times: number[];
values: number[];
attrFlag: number[];
attrData: number[];
};
};
};
}[][];
length: number;
frames: number;
}>;
length: number;
fps: number;
frames: number;
}} animations,
* @param {{skeleton: { bones: THREE.Bone[]}}} group
*/
function addAnimations(group, animations) {
if (group.animations === undefined) {
group.animations = [];
}
animations.stacks.forEach(function (stack) {
var animationData = {
name: stack.name,
fps: 30,
length: stack.length,
hierarchy: []
};
var bones = group.skeleton.bones;
bones.forEach(function (bone) {
var name = bone.name.replace(/.*:/, '');
var parentIndex = bones.findIndex(function (parentBone) {
return bone.parent === parentBone;
});
animationData.hierarchy.push({ parent: parentIndex, name: name, keys: [] });
});
for (var frame = 0; frame < stack.frames; frame++) {
bones.forEach(function (bone, boneIndex) {
var animationNode = stack.layers[0][boneIndex];
animationData.hierarchy.forEach(function (node) {
if (node.name === bone.name) {
node.keys.push(generateKey(animationNode, bone, frame));
}
});
});
}
group.animations.push(THREE.AnimationClip.parseAnimation(animationData, bones));
/**
* @param {THREE.Bone} bone
*/
function generateKey(animationNode, bone, frame) {
var key = {
time: frame / animations.fps,
pos: bone.position.toArray(),
rot: bone.quaternion.toArray(),
scl: bone.scale.toArray()
};
if (animationNode === undefined) {
return key;
}
try {
if (hasCurve(animationNode, 'T') && hasKeyOnFrame(animationNode.T, frame)) {
key.pos = [animationNode.T.curves.x.values[frame], animationNode.T.curves.y.values[frame], animationNode.T.curves.z.values[frame]];
}
if (hasCurve(animationNode, 'R') && hasKeyOnFrame(animationNode.R, frame)) {
var rotationX = degreeToRadian(animationNode.R.curves.x.values[frame]);
var rotationY = degreeToRadian(animationNode.R.curves.y.values[frame]);
var rotationZ = degreeToRadian(animationNode.R.curves.z.values[frame]);
var euler = new THREE.Euler(rotationX, rotationY, rotationZ, 'ZYX');
key.rot = new THREE.Quaternion().setFromEuler(euler).toArray();
}
if (hasCurve(animationNode, 'S') && hasKeyOnFrame(animationNode.S, frame)) {
key.scl = [animationNode.S.curves.x.values[frame], animationNode.S.curves.y.values[frame], animationNode.S.curves.z.values[frame]];
}
}
catch (error) {
// Curve is not fully plotted.
console.log(bone);
console.log(error);
}
return key;
function hasCurve(animationNode, attribute) {
if (animationNode === undefined) {
return false;
}
var attributeNode = animationNode[attribute];
if (!attributeNode) {
return false;
}
return ['x', 'y', 'z'].every(function (key) {
return attributeNode.curves[key] !== undefined;
});
}
function hasKeyOnFrame(attributeNode, frame) {
return ['x', 'y', 'z'].every(function (key) {
return isKeyExistOnFrame(attributeNode.curves[key], frame);
function isKeyExistOnFrame(curve, frame) {
return curve.values[frame] !== undefined;
}
});
}
}
});
}
// UTILS
/**
* Parses Vector3 property from FBXTree. Property is given as .value.x, .value.y, etc.
* @param {FBXVector3} property - Property to parse as Vector3.
* @returns {THREE.Vector3}
*/
function parseVector3(property) {
return new THREE.Vector3(parseFloat(property.value.x), parseFloat(property.value.y), parseFloat(property.value.z));
}
/**
* Parses Color property from FBXTree. Property is given as .value.x, .value.y, etc.
* @param {FBXVector3} property - Property to parse as Color.
* @returns {THREE.Color}
*/
function parseColor(property) {
return new THREE.Color().fromArray(parseVector3(property).toArray());
}
}
});
/**
* An instance of a Vertex with data for drawing vertices to the screen.
* @constructor
*/
function Vertex() {
/**
* Position of the vertex.
* @type {THREE.Vector3}
*/
this.position = new THREE.Vector3();
/**
* Normal of the vertex
* @type {THREE.Vector3}
*/
this.normal = new THREE.Vector3();
/**
* UV coordinates of the vertex.
* @type {THREE.Vector2}
*/
this.uv = new THREE.Vector2();
/**
* Indices of the bones vertex is influenced by.
* @type {THREE.Vector4}
*/
this.skinIndices = new THREE.Vector4(0, 0, 0, 0);
/**
* Weights that each bone influences the vertex.
* @type {THREE.Vector4}
*/
this.skinWeights = new THREE.Vector4(0, 0, 0, 0);
}
Object.assign(Vertex.prototype, {
copy: function (target) {
var returnVar = target || new Vertex();
returnVar.position.copy(this.position);
returnVar.normal.copy(this.normal);
returnVar.uv.copy(this.uv);
returnVar.skinIndices.copy(this.skinIndices);
returnVar.skinWeights.copy(this.skinWeights);
return returnVar;
},
flattenToBuffers: function () {
var vertexBuffer = this.position.toArray();
var normalBuffer = this.normal.toArray();
var uvBuffer = this.uv.toArray();
var skinIndexBuffer = this.skinIndices.toArray();
var skinWeightBuffer = this.skinWeights.toArray();
return {
vertexBuffer: vertexBuffer,
normalBuffer: normalBuffer,
uvBuffer: uvBuffer,
skinIndexBuffer: skinIndexBuffer,
skinWeightBuffer: skinWeightBuffer,
};
}
});
/**
* @constructor
*/
function Triangle() {
/**
* @type {{position: THREE.Vector3, normal: THREE.Vector3, uv: THREE.Vector2, skinIndices: THREE.Vector4, skinWeights: THREE.Vector4}[]}
*/
this.vertices = [];
}
Object.assign(Triangle.prototype, {
copy: function (target) {
var returnVar = target || new Triangle();
for (var i = 0; i < this.vertices.length; ++i) {
this.vertices[i].copy(returnVar.vertices[i]);
}
return returnVar;
},
flattenToBuffers: function () {
var vertexBuffer = [];
var normalBuffer = [];
var uvBuffer = [];
var skinIndexBuffer = [];
var skinWeightBuffer = [];
this.vertices.forEach(function (vertex) {
var flatVertex = vertex.flattenToBuffers();
vertexBuffer = vertexBuffer.concat(flatVertex.vertexBuffer);
normalBuffer = normalBuffer.concat(flatVertex.normalBuffer);
uvBuffer = uvBuffer.concat(flatVertex.uvBuffer);
skinIndexBuffer = skinIndexBuffer.concat(flatVertex.skinIndexBuffer);
skinWeightBuffer = skinWeightBuffer.concat(flatVertex.skinWeightBuffer);
});
return {
vertexBuffer: vertexBuffer,
normalBuffer: normalBuffer,
uvBuffer: uvBuffer,
skinIndexBuffer: skinIndexBuffer,
skinWeightBuffer: skinWeightBuffer,
};
}
});
/**
* @constructor
*/
function Face() {
/**
* @type {{vertices: {position: THREE.Vector3, normal: THREE.Vector3, uv: THREE.Vector2, skinIndices: THREE.Vector4, skinWeights: THREE.Vector4}[]}[]}
*/
this.triangles = [];
this.materialIndex = 0;
}
Object.assign(Face.prototype, {
copy: function (target) {
var returnVar = target || new Face();
for (var i = 0; i < this.triangles.length; ++i) {
this.triangles[i].copy(returnVar.triangles[i]);
}
returnVar.materialIndex = this.materialIndex;
return returnVar;
},
genTrianglesFromVertices: function (vertexArray) {
for (var i = 2; i < vertexArray.length; ++i) {
var triangle = new Triangle();
triangle.vertices[0] = vertexArray[0];
triangle.vertices[1] = vertexArray[i - 1];
triangle.vertices[2] = vertexArray[i];
this.triangles.push(triangle);
}
},
flattenToBuffers: function () {
var vertexBuffer = [];
var normalBuffer = [];
var uvBuffer = [];
var skinIndexBuffer = [];
var skinWeightBuffer = [];
var materialIndexBuffer = [];
var materialIndex = this.materialIndex;
this.triangles.forEach(function (triangle) {
var flatTriangle = triangle.flattenToBuffers();
vertexBuffer = vertexBuffer.concat(flatTriangle.vertexBuffer);
normalBuffer = normalBuffer.concat(flatTriangle.normalBuffer);
uvBuffer = uvBuffer.concat(flatTriangle.uvBuffer);
skinIndexBuffer = skinIndexBuffer.concat(flatTriangle.skinIndexBuffer);
skinWeightBuffer = skinWeightBuffer.concat(flatTriangle.skinWeightBuffer);
materialIndexBuffer = materialIndexBuffer.concat([materialIndex, materialIndex, materialIndex]);
});
return {
vertexBuffer: vertexBuffer,
normalBuffer: normalBuffer,
uvBuffer: uvBuffer,
skinIndexBuffer: skinIndexBuffer,
skinWeightBuffer: skinWeightBuffer,
materialIndexBuffer: materialIndexBuffer
};
}
});
/**
* @constructor
*/
function Geometry() {
/**
* @type {{triangles: {vertices: {position: THREE.Vector3, normal: THREE.Vector3, uv: THREE.Vector2, skinIndices: THREE.Vector4, skinWeights: THREE.Vector4}[]}[], materialIndex: number}[]}
*/
this.faces = [];
/**
* @type {{}|THREE.Skeleton}
*/
this.skeleton = null;
}
Object.assign(Geometry.prototype, {
/**
* @returns {{vertexBuffer: number[], normalBuffer: number[], uvBuffer: number[], skinIndexBuffer: number[], skinWeightBuffer: number[], materialIndexBuffer: number[]}}
*/
flattenToBuffers: function () {
var vertexBuffer = [];
var normalBuffer = [];
var uvBuffer = [];
var skinIndexBuffer = [];
var skinWeightBuffer = [];
var materialIndexBuffer = [];
this.faces.forEach(function (face) {
var flatFace = face.flattenToBuffers();
vertexBuffer = vertexBuffer.concat(flatFace.vertexBuffer);
normalBuffer = normalBuffer.concat(flatFace.normalBuffer);
uvBuffer = uvBuffer.concat(flatFace.uvBuffer);
skinIndexBuffer = skinIndexBuffer.concat(flatFace.skinIndexBuffer);
skinWeightBuffer = skinWeightBuffer.concat(flatFace.skinWeightBuffer);
materialIndexBuffer = materialIndexBuffer.concat(flatFace.materialIndexBuffer);
});
return {
vertexBuffer: vertexBuffer,
normalBuffer: normalBuffer,
uvBuffer: uvBuffer,
skinIndexBuffer: skinIndexBuffer,
skinWeightBuffer: skinWeightBuffer,
materialIndexBuffer: materialIndexBuffer
};
}
});
function TextParser() { }
Object.assign(TextParser.prototype, {
getPrevNode: function () {
return this.nodeStack[this.currentIndent - 2];
},
getCurrentNode: function () {
return this.nodeStack[this.currentIndent - 1];
},
getCurrentProp: function () {
return this.currentProp;
},
pushStack: function (node) {
this.nodeStack.push(node);
this.currentIndent += 1;
},
popStack: function () {
this.nodeStack.pop();
this.currentIndent -= 1;
},
setCurrentProp: function (val, name) {
this.currentProp = val;
this.currentPropName = name;
},
// ----------parse ---------------------------------------------------
parse: function (text) {
this.currentIndent = 0;
this.allNodes = new FBXTree();
this.nodeStack = [];
this.currentProp = [];
this.currentPropName = '';
var split = text.split("\n");
for (var line in split) {
var l = split[line];
// short cut
if (l.match(/^[\s\t]*;/)) {
continue;
} // skip comment line
if (l.match(/^[\s\t]*$/)) {
continue;
} // skip empty line
// beginning of node
var beginningOfNodeExp = new RegExp("^\\t{" + this.currentIndent + "}(\\w+):(.*){", '');
var match = l.match(beginningOfNodeExp);
if (match) {
var nodeName = match[1].trim().replace(/^"/, '').replace(/"$/, "");
var nodeAttrs = match[2].split(',').map(function (element) {
return element.trim().replace(/^"/, '').replace(/"$/, '');
});
this.parseNodeBegin(l, nodeName, nodeAttrs || null);
continue;
}
// node's property
var propExp = new RegExp("^\\t{" + (this.currentIndent) + "}(\\w+):[\\s\\t\\r\\n](.*)");
var match = l.match(propExp);
if (match) {
var propName = match[1].replace(/^"/, '').replace(/"$/, "").trim();
var propValue = match[2].replace(/^"/, '').replace(/"$/, "").trim();
this.parseNodeProperty(l, propName, propValue);
continue;
}
// end of node
var endOfNodeExp = new RegExp("^\\t{" + (this.currentIndent - 1) + "}}");
if (l.match(endOfNodeExp)) {
this.nodeEnd();
continue;
}
// for special case,
//
// Vertices: *8670 {
// a: 0.0356229953467846,13.9599733352661,-0.399196773.....(snip)
// -0.0612030513584614,13.960485458374,-0.409748703241348,-0.10.....
// 0.12490539252758,13.7450733184814,-0.454119384288788,0.09272.....
// 0.0836158767342567,13.5432004928589,-0.435397416353226,0.028.....
//
// these case the lines must contiue with previous line
if (l.match(/^[^\s\t}]/)) {
this.parseNodePropertyContinued(l);
}
}
return this.allNodes;
},
parseNodeBegin: function (line, nodeName, nodeAttrs) {
// var nodeName = match[1];
var node = { 'name': nodeName, properties: {}, 'subNodes': {} };
var attrs = this.parseNodeAttr(nodeAttrs);
var currentNode = this.getCurrentNode();
// a top node
if (this.currentIndent === 0) {
this.allNodes.add(nodeName, node);
}
else {
// a subnode
// already exists subnode, then append it
if (nodeName in currentNode.subNodes) {
var tmp = currentNode.subNodes[nodeName];
// console.log( "duped entry found\nkey: " + nodeName + "\nvalue: " + propValue );
if (this.isFlattenNode(currentNode.subNodes[nodeName])) {
if (attrs.id === '') {
currentNode.subNodes[nodeName] = [];
currentNode.subNodes[nodeName].push(tmp);
}
else {
currentNode.subNodes[nodeName] = {};
currentNode.subNodes[nodeName][tmp.id] = tmp;
}
}
if (attrs.id === '') {
currentNode.subNodes[nodeName].push(node);
}
else {
currentNode.subNodes[nodeName][attrs.id] = node;
}
}
else if (typeof attrs.id === 'number' || attrs.id.match(/^\d+$/)) {
currentNode.subNodes[nodeName] = {};
currentNode.subNodes[nodeName][attrs.id] = node;
}
else {
currentNode.subNodes[nodeName] = node;
}
}
// for this ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
// NodeAttribute: 1001463072, "NodeAttribute::", "LimbNode" {
if (nodeAttrs) {
node.id = attrs.id;
node.attrName = attrs.name;
node.attrType = attrs.type;
}
this.pushStack(node);
},
parseNodeAttr: function (attrs) {
var id = attrs[0];
if (attrs[0] !== "") {
id = parseInt(attrs[0]);
if (isNaN(id)) {
// PolygonVertexIndex: *16380 {
id = attrs[0];
}
}
var name;
var type;
if (attrs.length > 1) {
name = attrs[1].replace(/^(\w+)::/, '');
type = attrs[2];
}
return { id: id, name: name || '', type: type || '' };
},
parseNodeProperty: function (line, propName, propValue) {
var currentNode = this.getCurrentNode();
var parentName = currentNode.name;
// special case parent node's is like "Properties70"
// these chilren nodes must treat with careful
if (parentName !== undefined) {
var propMatch = parentName.match(/Properties(\d)+/);
if (propMatch) {
this.parseNodeSpecialProperty(line, propName, propValue);
return;
}
}
// special case Connections
if (propName == 'C') {
var connProps = propValue.split(',').slice(1);
var from = parseInt(connProps[0]);
var to = parseInt(connProps[1]);
var rest = propValue.split(',').slice(3);
propName = 'connections';
propValue = [from, to];
propValue = propValue.concat(rest);
if (currentNode.properties[propName] === undefined) {
currentNode.properties[propName] = [];
}
}
// special case Connections
if (propName == 'Node') {
var id = parseInt(propValue);
currentNode.properties.id = id;
currentNode.id = id;
}
// already exists in properties, then append this
if (propName in currentNode.properties) {
// console.log( "duped entry found\nkey: " + propName + "\nvalue: " + propValue );
if (Array.isArray(currentNode.properties[propName])) {
currentNode.properties[propName].push(propValue);
}
else {
currentNode.properties[propName] += propValue;
}
}
else {
// console.log( propName + ": " + propValue );
if (Array.isArray(currentNode.properties[propName])) {
currentNode.properties[propName].push(propValue);
}
else {
currentNode.properties[propName] = propValue;
}
}
this.setCurrentProp(currentNode.properties, propName);
},
// TODO:
parseNodePropertyContinued: function (line) {
this.currentProp[this.currentPropName] += line;
},
parseNodeSpecialProperty: function (line, propName, propValue) {
// split this
// P: "Lcl Scaling", "Lcl Scaling", "", "A",1,1,1
// into array like below
// ["Lcl Scaling", "Lcl Scaling", "", "A", "1,1,1" ]
var props = propValue.split('",').map(function (element) {
return element.trim().replace(/^\"/, '').replace(/\s/, '_');
});
var innerPropName = props[0];
var innerPropType1 = props[1];
var innerPropType2 = props[2];
var innerPropFlag = props[3];
var innerPropValue = props[4];
/*
if ( innerPropValue === undefined ) {
innerPropValue = props[3];
}
*/
// cast value in its type
switch (innerPropType1) {
case "int":
innerPropValue = parseInt(innerPropValue);
break;
case "double":
innerPropValue = parseFloat(innerPropValue);
break;
case "ColorRGB":
case "Vector3D":
var tmp = innerPropValue.split(',');
innerPropValue = new THREE.Vector3(tmp[0], tmp[1], tmp[2]);
break;
}
// CAUTION: these props must append to parent's parent
this.getPrevNode().properties[innerPropName] = {
'type': innerPropType1,
'type2': innerPropType2,
'flag': innerPropFlag,
'value': innerPropValue
};
this.setCurrentProp(this.getPrevNode().properties, innerPropName);
},
nodeEnd: function () {
this.popStack();
},
/* ---------------------------------------------------------------- */
/* util */
isFlattenNode: function (node) {
return ('subNodes' in node && 'properties' in node) ? true : false;
}
});
function FBXTree() { }
Object.assign(FBXTree.prototype, {
add: function (key, val) {
this[key] = val;
},
searchConnectionParent: function (id) {
if (this.__cache_search_connection_parent === undefined) {
this.__cache_search_connection_parent = [];
}
if (this.__cache_search_connection_parent[id] !== undefined) {
return this.__cache_search_connection_parent[id];
}
else {
this.__cache_search_connection_parent[id] = [];
}
var conns = this.Connections.properties.connections;
var results = [];
for (var i = 0; i < conns.length; ++i) {
if (conns[i][0] == id) {
// 0 means scene root
var res = conns[i][1] === 0 ? -1 : conns[i][1];
results.push(res);
}
}
if (results.length > 0) {
this.__cache_search_connection_parent[id] = this.__cache_search_connection_parent[id].concat(results);
return results;
}
else {
this.__cache_search_connection_parent[id] = [-1];
return [-1];
}
},
searchConnectionChildren: function (id) {
if (this.__cache_search_connection_children === undefined) {
this.__cache_search_connection_children = [];
}
if (this.__cache_search_connection_children[id] !== undefined) {
return this.__cache_search_connection_children[id];
}
else {
this.__cache_search_connection_children[id] = [];
}
var conns = this.Connections.properties.connections;
var res = [];
for (var i = 0; i < conns.length; ++i) {
if (conns[i][1] == id) {
// 0 means scene root
res.push(conns[i][0] === 0 ? -1 : conns[i][0]);
}
}
if (res.length > 0) {
this.__cache_search_connection_children[id] = this.__cache_search_connection_children[id].concat(res);
return res;
}
else {
this.__cache_search_connection_children[id] = [];
return [];
}
},
searchConnectionType: function (id, to) {
var key = id + ',' + to; // TODO: to hash
if (this.__cache_search_connection_type === undefined) {
this.__cache_search_connection_type = {};
}
if (this.__cache_search_connection_type[key] !== undefined) {
return this.__cache_search_connection_type[key];
}
else {
this.__cache_search_connection_type[key] = '';
}
var conns = this.Connections.properties.connections;
for (var i = 0; i < conns.length; ++i) {
if (conns[i][0] == id && conns[i][1] == to) {
// 0 means scene root
this.__cache_search_connection_type[key] = conns[i][2];
return conns[i][2];
}
}
this.__cache_search_connection_type[id] = null;
return null;
}
});
/**
* @returns {boolean}
*/
function isFbxFormatASCII(text) {
var CORRECT = ['K', 'a', 'y', 'd', 'a', 'r', 'a', '\\', 'F', 'B', 'X', '\\', 'B', 'i', 'n', 'a', 'r', 'y', '\\', '\\'];
var cursor = 0;
var read = function (offset) {
var result = text[offset - 1];
text = text.slice(cursor + offset);
cursor++;
return result;
};
for (var i = 0; i < CORRECT.length; ++i) {
var num = read(1);
if (num == CORRECT[i]) {
return false;
}
}
return true;
}
/**
* @returns {number}
*/
function getFbxVersion(text) {
var versionRegExp = /FBXVersion: (\d+)/;
var match = text.match(versionRegExp);
if (match) {
var version = parseInt(match[1]);
return version;
}
throw new Error('FBXLoader: Cannot find the version number for the file given.');
}
/**
* Converts FBX ticks into real time seconds.
* @param {number} time - FBX tick timestamp to convert.
* @returns {number} - FBX tick in real world time.
*/
function ConvertFBXTimeToSeconds(time) {
// Constant is FBX ticks per second.
return time / 46186158000;
}
/**
* Parses comma separated list of float numbers and returns them in an array.
* @example
* // Returns [ 5.6, 9.4, 2.5, 1.4 ]
* parseFloatArray( "5.6,9.4,2.5,1.4" )
* @returns {number[]}
*/
function parseFloatArray(floatString) {
return floatString.split(',').map(function (stringValue) {
return parseFloat(stringValue);
});
}
/**
* Parses comma separated list of int numbers and returns them in an array.
* @example
* // Returns [ 5, 8, 2, 3 ]
* parseFloatArray( "5,8,2,3" )
* @returns {number[]}
*/
function parseIntArray(intString) {
return intString.split(',').map(function (stringValue) {
return parseInt(stringValue);
});
}
function parseMatrixArray(floatString) {
return new THREE.Matrix4().fromArray(parseFloatArray(floatString));
}
/**
* Converts number from degrees into radians.
* @param {number} value
* @returns {number}
*/
function degreeToRadian(value) {
return value * Math.PI / 180;
}