UNPKG

three-stdlib

Version:

stand-alone library of threejs examples

1 lines 160 kB
{"version":3,"file":"FBXLoader.cjs","sources":["../../src/loaders/FBXLoader.js"],"sourcesContent":["import {\n AmbientLight,\n AnimationClip,\n Bone,\n BufferGeometry,\n ClampToEdgeWrapping,\n Color,\n DirectionalLight,\n EquirectangularReflectionMapping,\n Euler,\n FileLoader,\n Float32BufferAttribute,\n Group,\n Line,\n LineBasicMaterial,\n Loader,\n LoaderUtils,\n MathUtils,\n Matrix3,\n Matrix4,\n Mesh,\n MeshLambertMaterial,\n MeshPhongMaterial,\n NumberKeyframeTrack,\n Object3D,\n OrthographicCamera,\n PerspectiveCamera,\n PointLight,\n PropertyBinding,\n Quaternion,\n QuaternionKeyframeTrack,\n RepeatWrapping,\n Skeleton,\n SkinnedMesh,\n SpotLight,\n Texture,\n TextureLoader,\n Uint16BufferAttribute,\n Vector3,\n Vector4,\n VectorKeyframeTrack,\n} from 'three'\nimport { unzlibSync } from 'fflate'\nimport { NURBSCurve } from '../curves/NURBSCurve'\nimport { decodeText } from '../_polyfill/LoaderUtils'\nimport { UV1 } from '../_polyfill/uv1'\n\n/**\n * Loader loads FBX file and generates Group representing FBX scene.\n * Requires FBX file to be >= 7.0 and in ASCII or >= 6400 in Binary format\n * Versions lower than this may load but will probably have errors\n *\n * Needs Support:\n * Morph normals / blend shape normals\n *\n * FBX format references:\n * \thttps://help.autodesk.com/view/FBX/2017/ENU/?guid=__cpp_ref_index_html (C++ SDK reference)\n *\n * Binary format specification:\n *\thttps://code.blender.org/2013/08/fbx-binary-file-format-specification/\n */\n\nlet fbxTree\nlet connections\nlet sceneGraph\n\nclass FBXLoader extends Loader {\n constructor(manager) {\n super(manager)\n }\n\n load(url, onLoad, onProgress, onError) {\n const scope = this\n\n const path = scope.path === '' ? LoaderUtils.extractUrlBase(url) : scope.path\n\n const loader = new FileLoader(this.manager)\n loader.setPath(scope.path)\n loader.setResponseType('arraybuffer')\n loader.setRequestHeader(scope.requestHeader)\n loader.setWithCredentials(scope.withCredentials)\n\n loader.load(\n url,\n function (buffer) {\n try {\n onLoad(scope.parse(buffer, path))\n } catch (e) {\n if (onError) {\n onError(e)\n } else {\n console.error(e)\n }\n\n scope.manager.itemError(url)\n }\n },\n onProgress,\n onError,\n )\n }\n\n parse(FBXBuffer, path) {\n if (isFbxFormatBinary(FBXBuffer)) {\n fbxTree = new BinaryParser().parse(FBXBuffer)\n } else {\n const FBXText = convertArrayBufferToString(FBXBuffer)\n\n if (!isFbxFormatASCII(FBXText)) {\n throw new Error('THREE.FBXLoader: Unknown format.')\n }\n\n if (getFbxVersion(FBXText) < 7000) {\n throw new Error('THREE.FBXLoader: FBX version not supported, FileVersion: ' + getFbxVersion(FBXText))\n }\n\n fbxTree = new TextParser().parse(FBXText)\n }\n\n // console.log( fbxTree );\n\n const textureLoader = new TextureLoader(this.manager)\n .setPath(this.resourcePath || path)\n .setCrossOrigin(this.crossOrigin)\n\n return new FBXTreeParser(textureLoader, this.manager).parse(fbxTree)\n }\n}\n\n// Parse the FBXTree object returned by the BinaryParser or TextParser and return a Group\nclass FBXTreeParser {\n constructor(textureLoader, manager) {\n this.textureLoader = textureLoader\n this.manager = manager\n }\n\n parse() {\n connections = this.parseConnections()\n\n const images = this.parseImages()\n const textures = this.parseTextures(images)\n const materials = this.parseMaterials(textures)\n const deformers = this.parseDeformers()\n const geometryMap = new GeometryParser().parse(deformers)\n\n this.parseScene(deformers, geometryMap, materials)\n\n return sceneGraph\n }\n\n // Parses FBXTree.Connections which holds parent-child connections between objects (e.g. material -> texture, model->geometry )\n // and details the connection type\n parseConnections() {\n const connectionMap = new Map()\n\n if ('Connections' in fbxTree) {\n const rawConnections = fbxTree.Connections.connections\n\n rawConnections.forEach(function (rawConnection) {\n const fromID = rawConnection[0]\n const toID = rawConnection[1]\n const relationship = rawConnection[2]\n\n if (!connectionMap.has(fromID)) {\n connectionMap.set(fromID, {\n parents: [],\n children: [],\n })\n }\n\n const parentRelationship = { ID: toID, relationship: relationship }\n connectionMap.get(fromID).parents.push(parentRelationship)\n\n if (!connectionMap.has(toID)) {\n connectionMap.set(toID, {\n parents: [],\n children: [],\n })\n }\n\n const childRelationship = { ID: fromID, relationship: relationship }\n connectionMap.get(toID).children.push(childRelationship)\n })\n }\n\n return connectionMap\n }\n\n // Parse FBXTree.Objects.Video for embedded image data\n // These images are connected to textures in FBXTree.Objects.Textures\n // via FBXTree.Connections.\n parseImages() {\n const images = {}\n const blobs = {}\n\n if ('Video' in fbxTree.Objects) {\n const videoNodes = fbxTree.Objects.Video\n\n for (const nodeID in videoNodes) {\n const videoNode = videoNodes[nodeID]\n\n const id = parseInt(nodeID)\n\n images[id] = videoNode.RelativeFilename || videoNode.Filename\n\n // raw image data is in videoNode.Content\n if ('Content' in videoNode) {\n const arrayBufferContent = videoNode.Content instanceof ArrayBuffer && videoNode.Content.byteLength > 0\n const base64Content = typeof videoNode.Content === 'string' && videoNode.Content !== ''\n\n if (arrayBufferContent || base64Content) {\n const image = this.parseImage(videoNodes[nodeID])\n\n blobs[videoNode.RelativeFilename || videoNode.Filename] = image\n }\n }\n }\n }\n\n for (const id in images) {\n const filename = images[id]\n\n if (blobs[filename] !== undefined) images[id] = blobs[filename]\n else images[id] = images[id].split('\\\\').pop()\n }\n\n return images\n }\n\n // Parse embedded image data in FBXTree.Video.Content\n parseImage(videoNode) {\n const content = videoNode.Content\n const fileName = videoNode.RelativeFilename || videoNode.Filename\n const extension = fileName.slice(fileName.lastIndexOf('.') + 1).toLowerCase()\n\n let type\n\n switch (extension) {\n case 'bmp':\n type = 'image/bmp'\n break\n\n case 'jpg':\n case 'jpeg':\n type = 'image/jpeg'\n break\n\n case 'png':\n type = 'image/png'\n break\n\n case 'tif':\n type = 'image/tiff'\n break\n\n case 'tga':\n if (this.manager.getHandler('.tga') === null) {\n console.warn('FBXLoader: TGA loader not found, skipping ', fileName)\n }\n\n type = 'image/tga'\n break\n\n default:\n console.warn('FBXLoader: Image type \"' + extension + '\" is not supported.')\n return\n }\n\n if (typeof content === 'string') {\n // ASCII format\n\n return 'data:' + type + ';base64,' + content\n } else {\n // Binary Format\n\n const array = new Uint8Array(content)\n return window.URL.createObjectURL(new Blob([array], { type: type }))\n }\n }\n\n // Parse nodes in FBXTree.Objects.Texture\n // These contain details such as UV scaling, cropping, rotation etc and are connected\n // to images in FBXTree.Objects.Video\n parseTextures(images) {\n const textureMap = new Map()\n\n if ('Texture' in fbxTree.Objects) {\n const textureNodes = fbxTree.Objects.Texture\n for (const nodeID in textureNodes) {\n const texture = this.parseTexture(textureNodes[nodeID], images)\n textureMap.set(parseInt(nodeID), texture)\n }\n }\n\n return textureMap\n }\n\n // Parse individual node in FBXTree.Objects.Texture\n parseTexture(textureNode, images) {\n const texture = this.loadTexture(textureNode, images)\n\n texture.ID = textureNode.id\n\n texture.name = textureNode.attrName\n\n const wrapModeU = textureNode.WrapModeU\n const wrapModeV = textureNode.WrapModeV\n\n const valueU = wrapModeU !== undefined ? wrapModeU.value : 0\n const valueV = wrapModeV !== undefined ? wrapModeV.value : 0\n\n // http://download.autodesk.com/us/fbx/SDKdocs/FBX_SDK_Help/files/fbxsdkref/class_k_fbx_texture.html#889640e63e2e681259ea81061b85143a\n // 0: repeat(default), 1: clamp\n\n texture.wrapS = valueU === 0 ? RepeatWrapping : ClampToEdgeWrapping\n texture.wrapT = valueV === 0 ? RepeatWrapping : ClampToEdgeWrapping\n\n if ('Scaling' in textureNode) {\n const values = textureNode.Scaling.value\n\n texture.repeat.x = values[0]\n texture.repeat.y = values[1]\n }\n\n return texture\n }\n\n // load a texture specified as a blob or data URI, or via an external URL using TextureLoader\n loadTexture(textureNode, images) {\n let fileName\n\n const currentPath = this.textureLoader.path\n\n const children = connections.get(textureNode.id).children\n\n if (children !== undefined && children.length > 0 && images[children[0].ID] !== undefined) {\n fileName = images[children[0].ID]\n\n if (fileName.indexOf('blob:') === 0 || fileName.indexOf('data:') === 0) {\n this.textureLoader.setPath(undefined)\n }\n }\n\n let texture\n\n const extension = textureNode.FileName.slice(-3).toLowerCase()\n\n if (extension === 'tga') {\n const loader = this.manager.getHandler('.tga')\n\n if (loader === null) {\n console.warn('FBXLoader: TGA loader not found, creating placeholder texture for', textureNode.RelativeFilename)\n texture = new Texture()\n } else {\n loader.setPath(this.textureLoader.path)\n texture = loader.load(fileName)\n }\n } else if (extension === 'psd') {\n console.warn(\n 'FBXLoader: PSD textures are not supported, creating placeholder texture for',\n textureNode.RelativeFilename,\n )\n texture = new Texture()\n } else {\n texture = this.textureLoader.load(fileName)\n }\n\n this.textureLoader.setPath(currentPath)\n\n return texture\n }\n\n // Parse nodes in FBXTree.Objects.Material\n parseMaterials(textureMap) {\n const materialMap = new Map()\n\n if ('Material' in fbxTree.Objects) {\n const materialNodes = fbxTree.Objects.Material\n\n for (const nodeID in materialNodes) {\n const material = this.parseMaterial(materialNodes[nodeID], textureMap)\n\n if (material !== null) materialMap.set(parseInt(nodeID), material)\n }\n }\n\n return materialMap\n }\n\n // Parse single node in FBXTree.Objects.Material\n // Materials are connected to texture maps in FBXTree.Objects.Textures\n // FBX format currently only supports Lambert and Phong shading models\n parseMaterial(materialNode, textureMap) {\n const ID = materialNode.id\n const name = materialNode.attrName\n let type = materialNode.ShadingModel\n\n // Case where FBX wraps shading model in property object.\n if (typeof type === 'object') {\n type = type.value\n }\n\n // Ignore unused materials which don't have any connections.\n if (!connections.has(ID)) return null\n\n const parameters = this.parseParameters(materialNode, textureMap, ID)\n\n let material\n\n switch (type.toLowerCase()) {\n case 'phong':\n material = new MeshPhongMaterial()\n break\n case 'lambert':\n material = new MeshLambertMaterial()\n break\n default:\n console.warn('THREE.FBXLoader: unknown material type \"%s\". Defaulting to MeshPhongMaterial.', type)\n material = new MeshPhongMaterial()\n break\n }\n\n material.setValues(parameters)\n material.name = name\n\n return material\n }\n\n // Parse FBX material and return parameters suitable for a three.js material\n // Also parse the texture map and return any textures associated with the material\n parseParameters(materialNode, textureMap, ID) {\n const parameters = {}\n\n if (materialNode.BumpFactor) {\n parameters.bumpScale = materialNode.BumpFactor.value\n }\n\n if (materialNode.Diffuse) {\n parameters.color = new Color().fromArray(materialNode.Diffuse.value)\n } else if (\n materialNode.DiffuseColor &&\n (materialNode.DiffuseColor.type === 'Color' || materialNode.DiffuseColor.type === 'ColorRGB')\n ) {\n // The blender exporter exports diffuse here instead of in materialNode.Diffuse\n parameters.color = new Color().fromArray(materialNode.DiffuseColor.value)\n }\n\n if (materialNode.DisplacementFactor) {\n parameters.displacementScale = materialNode.DisplacementFactor.value\n }\n\n if (materialNode.Emissive) {\n parameters.emissive = new Color().fromArray(materialNode.Emissive.value)\n } else if (\n materialNode.EmissiveColor &&\n (materialNode.EmissiveColor.type === 'Color' || materialNode.EmissiveColor.type === 'ColorRGB')\n ) {\n // The blender exporter exports emissive color here instead of in materialNode.Emissive\n parameters.emissive = new Color().fromArray(materialNode.EmissiveColor.value)\n }\n\n if (materialNode.EmissiveFactor) {\n parameters.emissiveIntensity = parseFloat(materialNode.EmissiveFactor.value)\n }\n\n if (materialNode.Opacity) {\n parameters.opacity = parseFloat(materialNode.Opacity.value)\n }\n\n if (parameters.opacity < 1.0) {\n parameters.transparent = true\n }\n\n if (materialNode.ReflectionFactor) {\n parameters.reflectivity = materialNode.ReflectionFactor.value\n }\n\n if (materialNode.Shininess) {\n parameters.shininess = materialNode.Shininess.value\n }\n\n if (materialNode.Specular) {\n parameters.specular = new Color().fromArray(materialNode.Specular.value)\n } else if (materialNode.SpecularColor && materialNode.SpecularColor.type === 'Color') {\n // The blender exporter exports specular color here instead of in materialNode.Specular\n parameters.specular = new Color().fromArray(materialNode.SpecularColor.value)\n }\n\n const scope = this\n connections.get(ID).children.forEach(function (child) {\n const type = child.relationship\n\n switch (type) {\n case 'Bump':\n parameters.bumpMap = scope.getTexture(textureMap, child.ID)\n break\n\n case 'Maya|TEX_ao_map':\n parameters.aoMap = scope.getTexture(textureMap, child.ID)\n break\n\n case 'DiffuseColor':\n case 'Maya|TEX_color_map':\n parameters.map = scope.getTexture(textureMap, child.ID)\n if (parameters.map !== undefined) {\n if ('colorSpace' in parameters.map) parameters.map.colorSpace = 'srgb'\n else parameters.map.encoding = 3001 // sRGBEncoding\n }\n\n break\n\n case 'DisplacementColor':\n parameters.displacementMap = scope.getTexture(textureMap, child.ID)\n break\n\n case 'EmissiveColor':\n parameters.emissiveMap = scope.getTexture(textureMap, child.ID)\n if (parameters.emissiveMap !== undefined) {\n if ('colorSpace' in parameters.emissiveMap) parameters.emissiveMap.colorSpace = 'srgb'\n else parameters.emissiveMap.encoding = 3001 // sRGBEncoding\n }\n\n break\n\n case 'NormalMap':\n case 'Maya|TEX_normal_map':\n parameters.normalMap = scope.getTexture(textureMap, child.ID)\n break\n\n case 'ReflectionColor':\n parameters.envMap = scope.getTexture(textureMap, child.ID)\n if (parameters.envMap !== undefined) {\n parameters.envMap.mapping = EquirectangularReflectionMapping\n\n if ('colorSpace' in parameters.envMap) parameters.envMap.colorSpace = 'srgb'\n else parameters.envMap.encoding = 3001 // sRGBEncoding\n }\n\n break\n\n case 'SpecularColor':\n parameters.specularMap = scope.getTexture(textureMap, child.ID)\n if (parameters.specularMap !== undefined) {\n if ('colorSpace' in parameters.specularMap) parameters.specularMap.colorSpace = 'srgb'\n else parameters.specularMap.encoding = 3001 // sRGBEncoding\n }\n\n break\n\n case 'TransparentColor':\n case 'TransparencyFactor':\n parameters.alphaMap = scope.getTexture(textureMap, child.ID)\n parameters.transparent = true\n break\n\n case 'AmbientColor':\n case 'ShininessExponent': // AKA glossiness map\n case 'SpecularFactor': // AKA specularLevel\n case 'VectorDisplacementColor': // NOTE: Seems to be a copy of DisplacementColor\n default:\n console.warn('THREE.FBXLoader: %s map is not supported in three.js, skipping texture.', type)\n break\n }\n })\n\n return parameters\n }\n\n // get a texture from the textureMap for use by a material.\n getTexture(textureMap, id) {\n // if the texture is a layered texture, just use the first layer and issue a warning\n if ('LayeredTexture' in fbxTree.Objects && id in fbxTree.Objects.LayeredTexture) {\n console.warn('THREE.FBXLoader: layered textures are not supported in three.js. Discarding all but first layer.')\n id = connections.get(id).children[0].ID\n }\n\n return textureMap.get(id)\n }\n\n // Parse nodes in FBXTree.Objects.Deformer\n // Deformer node can contain skinning or Vertex Cache animation data, however only skinning is supported here\n // Generates map of Skeleton-like objects for use later when generating and binding skeletons.\n parseDeformers() {\n const skeletons = {}\n const morphTargets = {}\n\n if ('Deformer' in fbxTree.Objects) {\n const DeformerNodes = fbxTree.Objects.Deformer\n\n for (const nodeID in DeformerNodes) {\n const deformerNode = DeformerNodes[nodeID]\n\n const relationships = connections.get(parseInt(nodeID))\n\n if (deformerNode.attrType === 'Skin') {\n const skeleton = this.parseSkeleton(relationships, DeformerNodes)\n skeleton.ID = nodeID\n\n if (relationships.parents.length > 1) {\n console.warn('THREE.FBXLoader: skeleton attached to more than one geometry is not supported.')\n }\n skeleton.geometryID = relationships.parents[0].ID\n\n skeletons[nodeID] = skeleton\n } else if (deformerNode.attrType === 'BlendShape') {\n const morphTarget = {\n id: nodeID,\n }\n\n morphTarget.rawTargets = this.parseMorphTargets(relationships, DeformerNodes)\n morphTarget.id = nodeID\n\n if (relationships.parents.length > 1) {\n console.warn('THREE.FBXLoader: morph target attached to more than one geometry is not supported.')\n }\n\n morphTargets[nodeID] = morphTarget\n }\n }\n }\n\n return {\n skeletons: skeletons,\n morphTargets: morphTargets,\n }\n }\n\n // Parse single nodes in FBXTree.Objects.Deformer\n // The top level skeleton node has type 'Skin' and sub nodes have type 'Cluster'\n // Each skin node represents a skeleton and each cluster node represents a bone\n parseSkeleton(relationships, deformerNodes) {\n const rawBones = []\n\n relationships.children.forEach(function (child) {\n const boneNode = deformerNodes[child.ID]\n\n if (boneNode.attrType !== 'Cluster') return\n\n const rawBone = {\n ID: child.ID,\n indices: [],\n weights: [],\n transformLink: new Matrix4().fromArray(boneNode.TransformLink.a),\n // transform: new Matrix4().fromArray( boneNode.Transform.a ),\n // linkMode: boneNode.Mode,\n }\n\n if ('Indexes' in boneNode) {\n rawBone.indices = boneNode.Indexes.a\n rawBone.weights = boneNode.Weights.a\n }\n\n rawBones.push(rawBone)\n })\n\n return {\n rawBones: rawBones,\n bones: [],\n }\n }\n\n // The top level morph deformer node has type \"BlendShape\" and sub nodes have type \"BlendShapeChannel\"\n parseMorphTargets(relationships, deformerNodes) {\n const rawMorphTargets = []\n\n for (let i = 0; i < relationships.children.length; i++) {\n const child = relationships.children[i]\n\n const morphTargetNode = deformerNodes[child.ID]\n\n const rawMorphTarget = {\n name: morphTargetNode.attrName,\n initialWeight: morphTargetNode.DeformPercent,\n id: morphTargetNode.id,\n fullWeights: morphTargetNode.FullWeights.a,\n }\n\n if (morphTargetNode.attrType !== 'BlendShapeChannel') return\n\n rawMorphTarget.geoID = connections.get(parseInt(child.ID)).children.filter(function (child) {\n return child.relationship === undefined\n })[0].ID\n\n rawMorphTargets.push(rawMorphTarget)\n }\n\n return rawMorphTargets\n }\n\n // create the main Group() to be returned by the loader\n parseScene(deformers, geometryMap, materialMap) {\n sceneGraph = new Group()\n\n const modelMap = this.parseModels(deformers.skeletons, geometryMap, materialMap)\n\n const modelNodes = fbxTree.Objects.Model\n\n const scope = this\n modelMap.forEach(function (model) {\n const modelNode = modelNodes[model.ID]\n scope.setLookAtProperties(model, modelNode)\n\n const parentConnections = connections.get(model.ID).parents\n\n parentConnections.forEach(function (connection) {\n const parent = modelMap.get(connection.ID)\n if (parent !== undefined) parent.add(model)\n })\n\n if (model.parent === null) {\n sceneGraph.add(model)\n }\n })\n\n this.bindSkeleton(deformers.skeletons, geometryMap, modelMap)\n\n this.createAmbientLight()\n\n sceneGraph.traverse(function (node) {\n if (node.userData.transformData) {\n if (node.parent) {\n node.userData.transformData.parentMatrix = node.parent.matrix\n node.userData.transformData.parentMatrixWorld = node.parent.matrixWorld\n }\n\n const transform = generateTransform(node.userData.transformData)\n\n node.applyMatrix4(transform)\n node.updateWorldMatrix()\n }\n })\n\n const animations = new AnimationParser().parse()\n\n // if all the models where already combined in a single group, just return that\n if (sceneGraph.children.length === 1 && sceneGraph.children[0].isGroup) {\n sceneGraph.children[0].animations = animations\n sceneGraph = sceneGraph.children[0]\n }\n\n sceneGraph.animations = animations\n }\n\n // parse nodes in FBXTree.Objects.Model\n parseModels(skeletons, geometryMap, materialMap) {\n const modelMap = new Map()\n const modelNodes = fbxTree.Objects.Model\n\n for (const nodeID in modelNodes) {\n const id = parseInt(nodeID)\n const node = modelNodes[nodeID]\n const relationships = connections.get(id)\n\n let model = this.buildSkeleton(relationships, skeletons, id, node.attrName)\n\n if (!model) {\n switch (node.attrType) {\n case 'Camera':\n model = this.createCamera(relationships)\n break\n case 'Light':\n model = this.createLight(relationships)\n break\n case 'Mesh':\n model = this.createMesh(relationships, geometryMap, materialMap)\n break\n case 'NurbsCurve':\n model = this.createCurve(relationships, geometryMap)\n break\n case 'LimbNode':\n case 'Root':\n model = new Bone()\n break\n case 'Null':\n default:\n model = new Group()\n break\n }\n\n model.name = node.attrName ? PropertyBinding.sanitizeNodeName(node.attrName) : ''\n\n model.ID = id\n }\n\n this.getTransformData(model, node)\n modelMap.set(id, model)\n }\n\n return modelMap\n }\n\n buildSkeleton(relationships, skeletons, id, name) {\n let bone = null\n\n relationships.parents.forEach(function (parent) {\n for (const ID in skeletons) {\n const skeleton = skeletons[ID]\n\n skeleton.rawBones.forEach(function (rawBone, i) {\n if (rawBone.ID === parent.ID) {\n const subBone = bone\n bone = new Bone()\n\n bone.matrixWorld.copy(rawBone.transformLink)\n\n // set name and id here - otherwise in cases where \"subBone\" is created it will not have a name / id\n\n bone.name = name ? PropertyBinding.sanitizeNodeName(name) : ''\n bone.ID = id\n\n skeleton.bones[i] = bone\n\n // In cases where a bone is shared between multiple meshes\n // duplicate the bone here and and it as a child of the first bone\n if (subBone !== null) {\n bone.add(subBone)\n }\n }\n })\n }\n })\n\n return bone\n }\n\n // create a PerspectiveCamera or OrthographicCamera\n createCamera(relationships) {\n let model\n let cameraAttribute\n\n relationships.children.forEach(function (child) {\n const attr = fbxTree.Objects.NodeAttribute[child.ID]\n\n if (attr !== undefined) {\n cameraAttribute = attr\n }\n })\n\n if (cameraAttribute === undefined) {\n model = new Object3D()\n } else {\n let type = 0\n if (cameraAttribute.CameraProjectionType !== undefined && cameraAttribute.CameraProjectionType.value === 1) {\n type = 1\n }\n\n let nearClippingPlane = 1\n if (cameraAttribute.NearPlane !== undefined) {\n nearClippingPlane = cameraAttribute.NearPlane.value / 1000\n }\n\n let farClippingPlane = 1000\n if (cameraAttribute.FarPlane !== undefined) {\n farClippingPlane = cameraAttribute.FarPlane.value / 1000\n }\n\n let width = window.innerWidth\n let height = window.innerHeight\n\n if (cameraAttribute.AspectWidth !== undefined && cameraAttribute.AspectHeight !== undefined) {\n width = cameraAttribute.AspectWidth.value\n height = cameraAttribute.AspectHeight.value\n }\n\n const aspect = width / height\n\n let fov = 45\n if (cameraAttribute.FieldOfView !== undefined) {\n fov = cameraAttribute.FieldOfView.value\n }\n\n const focalLength = cameraAttribute.FocalLength ? cameraAttribute.FocalLength.value : null\n\n switch (type) {\n case 0: // Perspective\n model = new PerspectiveCamera(fov, aspect, nearClippingPlane, farClippingPlane)\n if (focalLength !== null) model.setFocalLength(focalLength)\n break\n\n case 1: // Orthographic\n model = new OrthographicCamera(\n -width / 2,\n width / 2,\n height / 2,\n -height / 2,\n nearClippingPlane,\n farClippingPlane,\n )\n break\n\n default:\n console.warn('THREE.FBXLoader: Unknown camera type ' + type + '.')\n model = new Object3D()\n break\n }\n }\n\n return model\n }\n\n // Create a DirectionalLight, PointLight or SpotLight\n createLight(relationships) {\n let model\n let lightAttribute\n\n relationships.children.forEach(function (child) {\n const attr = fbxTree.Objects.NodeAttribute[child.ID]\n\n if (attr !== undefined) {\n lightAttribute = attr\n }\n })\n\n if (lightAttribute === undefined) {\n model = new Object3D()\n } else {\n let type\n\n // LightType can be undefined for Point lights\n if (lightAttribute.LightType === undefined) {\n type = 0\n } else {\n type = lightAttribute.LightType.value\n }\n\n let color = 0xffffff\n\n if (lightAttribute.Color !== undefined) {\n color = new Color().fromArray(lightAttribute.Color.value)\n }\n\n let intensity = lightAttribute.Intensity === undefined ? 1 : lightAttribute.Intensity.value / 100\n\n // light disabled\n if (lightAttribute.CastLightOnObject !== undefined && lightAttribute.CastLightOnObject.value === 0) {\n intensity = 0\n }\n\n let distance = 0\n if (lightAttribute.FarAttenuationEnd !== undefined) {\n if (lightAttribute.EnableFarAttenuation !== undefined && lightAttribute.EnableFarAttenuation.value === 0) {\n distance = 0\n } else {\n distance = lightAttribute.FarAttenuationEnd.value\n }\n }\n\n // TODO: could this be calculated linearly from FarAttenuationStart to FarAttenuationEnd?\n const decay = 1\n\n switch (type) {\n case 0: // Point\n model = new PointLight(color, intensity, distance, decay)\n break\n\n case 1: // Directional\n model = new DirectionalLight(color, intensity)\n break\n\n case 2: // Spot\n let angle = Math.PI / 3\n\n if (lightAttribute.InnerAngle !== undefined) {\n angle = MathUtils.degToRad(lightAttribute.InnerAngle.value)\n }\n\n let penumbra = 0\n if (lightAttribute.OuterAngle !== undefined) {\n // TODO: this is not correct - FBX calculates outer and inner angle in degrees\n // with OuterAngle > InnerAngle && OuterAngle <= Math.PI\n // while three.js uses a penumbra between (0, 1) to attenuate the inner angle\n penumbra = MathUtils.degToRad(lightAttribute.OuterAngle.value)\n penumbra = Math.max(penumbra, 1)\n }\n\n model = new SpotLight(color, intensity, distance, angle, penumbra, decay)\n break\n\n default:\n console.warn(\n 'THREE.FBXLoader: Unknown light type ' + lightAttribute.LightType.value + ', defaulting to a PointLight.',\n )\n model = new PointLight(color, intensity)\n break\n }\n\n if (lightAttribute.CastShadows !== undefined && lightAttribute.CastShadows.value === 1) {\n model.castShadow = true\n }\n }\n\n return model\n }\n\n createMesh(relationships, geometryMap, materialMap) {\n let model\n let geometry = null\n let material = null\n const materials = []\n\n // get geometry and materials(s) from connections\n relationships.children.forEach(function (child) {\n if (geometryMap.has(child.ID)) {\n geometry = geometryMap.get(child.ID)\n }\n\n if (materialMap.has(child.ID)) {\n materials.push(materialMap.get(child.ID))\n }\n })\n\n if (materials.length > 1) {\n material = materials\n } else if (materials.length > 0) {\n material = materials[0]\n } else {\n material = new MeshPhongMaterial({ color: 0xcccccc })\n materials.push(material)\n }\n\n if ('color' in geometry.attributes) {\n materials.forEach(function (material) {\n material.vertexColors = true\n })\n }\n\n if (geometry.FBX_Deformer) {\n model = new SkinnedMesh(geometry, material)\n model.normalizeSkinWeights()\n } else {\n model = new Mesh(geometry, material)\n }\n\n return model\n }\n\n createCurve(relationships, geometryMap) {\n const geometry = relationships.children.reduce(function (geo, child) {\n if (geometryMap.has(child.ID)) geo = geometryMap.get(child.ID)\n\n return geo\n }, null)\n\n // FBX does not list materials for Nurbs lines, so we'll just put our own in here.\n const material = new LineBasicMaterial({ color: 0x3300ff, linewidth: 1 })\n return new Line(geometry, material)\n }\n\n // parse the model node for transform data\n getTransformData(model, modelNode) {\n const transformData = {}\n\n if ('InheritType' in modelNode) transformData.inheritType = parseInt(modelNode.InheritType.value)\n\n if ('RotationOrder' in modelNode) transformData.eulerOrder = getEulerOrder(modelNode.RotationOrder.value)\n else transformData.eulerOrder = 'ZYX'\n\n if ('Lcl_Translation' in modelNode) transformData.translation = modelNode.Lcl_Translation.value\n\n if ('PreRotation' in modelNode) transformData.preRotation = modelNode.PreRotation.value\n if ('Lcl_Rotation' in modelNode) transformData.rotation = modelNode.Lcl_Rotation.value\n if ('PostRotation' in modelNode) transformData.postRotation = modelNode.PostRotation.value\n\n if ('Lcl_Scaling' in modelNode) transformData.scale = modelNode.Lcl_Scaling.value\n\n if ('ScalingOffset' in modelNode) transformData.scalingOffset = modelNode.ScalingOffset.value\n if ('ScalingPivot' in modelNode) transformData.scalingPivot = modelNode.ScalingPivot.value\n\n if ('RotationOffset' in modelNode) transformData.rotationOffset = modelNode.RotationOffset.value\n if ('RotationPivot' in modelNode) transformData.rotationPivot = modelNode.RotationPivot.value\n\n model.userData.transformData = transformData\n }\n\n setLookAtProperties(model, modelNode) {\n if ('LookAtProperty' in modelNode) {\n const children = connections.get(model.ID).children\n\n children.forEach(function (child) {\n if (child.relationship === 'LookAtProperty') {\n const lookAtTarget = fbxTree.Objects.Model[child.ID]\n\n if ('Lcl_Translation' in lookAtTarget) {\n const pos = lookAtTarget.Lcl_Translation.value\n\n // DirectionalLight, SpotLight\n if (model.target !== undefined) {\n model.target.position.fromArray(pos)\n sceneGraph.add(model.target)\n } else {\n // Cameras and other Object3Ds\n\n model.lookAt(new Vector3().fromArray(pos))\n }\n }\n }\n })\n }\n }\n\n bindSkeleton(skeletons, geometryMap, modelMap) {\n const bindMatrices = this.parsePoseNodes()\n\n for (const ID in skeletons) {\n const skeleton = skeletons[ID]\n\n const parents = connections.get(parseInt(skeleton.ID)).parents\n\n parents.forEach(function (parent) {\n if (geometryMap.has(parent.ID)) {\n const geoID = parent.ID\n const geoRelationships = connections.get(geoID)\n\n geoRelationships.parents.forEach(function (geoConnParent) {\n if (modelMap.has(geoConnParent.ID)) {\n const model = modelMap.get(geoConnParent.ID)\n\n model.bind(new Skeleton(skeleton.bones), bindMatrices[geoConnParent.ID])\n }\n })\n }\n })\n }\n }\n\n parsePoseNodes() {\n const bindMatrices = {}\n\n if ('Pose' in fbxTree.Objects) {\n const BindPoseNode = fbxTree.Objects.Pose\n\n for (const nodeID in BindPoseNode) {\n if (BindPoseNode[nodeID].attrType === 'BindPose' && BindPoseNode[nodeID].NbPoseNodes > 0) {\n const poseNodes = BindPoseNode[nodeID].PoseNode\n\n if (Array.isArray(poseNodes)) {\n poseNodes.forEach(function (poseNode) {\n bindMatrices[poseNode.Node] = new Matrix4().fromArray(poseNode.Matrix.a)\n })\n } else {\n bindMatrices[poseNodes.Node] = new Matrix4().fromArray(poseNodes.Matrix.a)\n }\n }\n }\n }\n\n return bindMatrices\n }\n\n // Parse ambient color in FBXTree.GlobalSettings - if it's not set to black (default), create an ambient light\n createAmbientLight() {\n if ('GlobalSettings' in fbxTree && 'AmbientColor' in fbxTree.GlobalSettings) {\n const ambientColor = fbxTree.GlobalSettings.AmbientColor.value\n const r = ambientColor[0]\n const g = ambientColor[1]\n const b = ambientColor[2]\n\n if (r !== 0 || g !== 0 || b !== 0) {\n const color = new Color(r, g, b)\n sceneGraph.add(new AmbientLight(color, 1))\n }\n }\n }\n}\n\n// parse Geometry data from FBXTree and return map of BufferGeometries\nclass GeometryParser {\n // Parse nodes in FBXTree.Objects.Geometry\n parse(deformers) {\n const geometryMap = new Map()\n\n if ('Geometry' in fbxTree.Objects) {\n const geoNodes = fbxTree.Objects.Geometry\n\n for (const nodeID in geoNodes) {\n const relationships = connections.get(parseInt(nodeID))\n const geo = this.parseGeometry(relationships, geoNodes[nodeID], deformers)\n\n geometryMap.set(parseInt(nodeID), geo)\n }\n }\n\n return geometryMap\n }\n\n // Parse single node in FBXTree.Objects.Geometry\n parseGeometry(relationships, geoNode, deformers) {\n switch (geoNode.attrType) {\n case 'Mesh':\n return this.parseMeshGeometry(relationships, geoNode, deformers)\n break\n\n case 'NurbsCurve':\n return this.parseNurbsGeometry(geoNode)\n break\n }\n }\n\n // Parse single node mesh geometry in FBXTree.Objects.Geometry\n parseMeshGeometry(relationships, geoNode, deformers) {\n const skeletons = deformers.skeletons\n const morphTargets = []\n\n const modelNodes = relationships.parents.map(function (parent) {\n return fbxTree.Objects.Model[parent.ID]\n })\n\n // don't create geometry if it is not associated with any models\n if (modelNodes.length === 0) return\n\n const skeleton = relationships.children.reduce(function (skeleton, child) {\n if (skeletons[child.ID] !== undefined) skeleton = skeletons[child.ID]\n\n return skeleton\n }, null)\n\n relationships.children.forEach(function (child) {\n if (deformers.morphTargets[child.ID] !== undefined) {\n morphTargets.push(deformers.morphTargets[child.ID])\n }\n })\n\n // Assume one model and get the preRotation from that\n // if there is more than one model associated with the geometry this may cause problems\n const modelNode = modelNodes[0]\n\n const transformData = {}\n\n if ('RotationOrder' in modelNode) transformData.eulerOrder = getEulerOrder(modelNode.RotationOrder.value)\n if ('InheritType' in modelNode) transformData.inheritType = parseInt(modelNode.InheritType.value)\n\n if ('GeometricTranslation' in modelNode) transformData.translation = modelNode.GeometricTranslation.value\n if ('GeometricRotation' in modelNode) transformData.rotation = modelNode.GeometricRotation.value\n if ('GeometricScaling' in modelNode) transformData.scale = modelNode.GeometricScaling.value\n\n const transform = generateTransform(transformData)\n\n return this.genGeometry(geoNode, skeleton, morphTargets, transform)\n }\n\n // Generate a BufferGeometry from a node in FBXTree.Objects.Geometry\n genGeometry(geoNode, skeleton, morphTargets, preTransform) {\n const geo = new BufferGeometry()\n if (geoNode.attrName) geo.name = geoNode.attrName\n\n const geoInfo = this.parseGeoNode(geoNode, skeleton)\n const buffers = this.genBuffers(geoInfo)\n\n const positionAttribute = new Float32BufferAttribute(buffers.vertex, 3)\n\n positionAttribute.applyMatrix4(preTransform)\n\n geo.setAttribute('position', positionAttribute)\n\n if (buffers.colors.length > 0) {\n geo.setAttribute('color', new Float32BufferAttribute(buffers.colors, 3))\n }\n\n if (skeleton) {\n geo.setAttribute('skinIndex', new Uint16BufferAttribute(buffers.weightsIndices, 4))\n\n geo.setAttribute('skinWeight', new Float32BufferAttribute(buffers.vertexWeights, 4))\n\n // used later to bind the skeleton to the model\n geo.FBX_Deformer = skeleton\n }\n\n if (buffers.normal.length > 0) {\n const normalMatrix = new Matrix3().getNormalMatrix(preTransform)\n\n const normalAttribute = new Float32BufferAttribute(buffers.normal, 3)\n normalAttribute.applyNormalMatrix(normalMatrix)\n\n geo.setAttribute('normal', normalAttribute)\n }\n\n buffers.uvs.forEach(function (uvBuffer, i) {\n if (UV1 === 'uv2') i++\n const name = i === 0 ? 'uv' : `uv${i}`\n\n geo.setAttribute(name, new Float32BufferAttribute(buffers.uvs[i], 2))\n })\n\n if (geoInfo.material && geoInfo.material.mappingType !== 'AllSame') {\n // Convert the material indices of each vertex into rendering groups on the geometry.\n let prevMaterialIndex = buffers.materialIndex[0]\n let startIndex = 0\n\n buffers.materialIndex.forEach(function (currentIndex, i) {\n if (currentIndex !== prevMaterialIndex) {\n geo.addGroup(startIndex, i - startIndex, prevMaterialIndex)\n\n prevMaterialIndex = currentIndex\n startIndex = i\n }\n })\n\n // the loop above doesn't add the last group, do that here.\n if (geo.groups.length > 0) {\n const lastGroup = geo.groups[geo.groups.length - 1]\n const lastIndex = lastGroup.start + lastGroup.count\n\n if (lastIndex !== buffers.materialIndex.length) {\n geo.addGroup(lastIndex, buffers.materialIndex.length - lastIndex, prevMaterialIndex)\n }\n }\n\n // case where there are multiple materials but the whole geometry is only\n // using one of them\n if (geo.groups.length === 0) {\n geo.addGroup(0, buffers.materialIndex.length, buffers.materialIndex[0])\n }\n }\n\n this.addMorphTargets(geo, geoNode, morphTargets, preTransform)\n\n return geo\n }\n\n parseGeoNode(geoNode, skeleton) {\n const geoInfo = {}\n\n geoInfo.vertexPositions = geoNode.Vertices !== undefined ? geoNode.Vertices.a : []\n geoInfo.vertexIndices = geoNode.PolygonVertexIndex !== undefined ? geoNode.PolygonVertexIndex.a : []\n\n if (geoNode.LayerElementColor) {\n geoInfo.color = this.parseVertexColors(geoNode.LayerElementColor[0])\n }\n\n if (geoNode.LayerElementMaterial) {\n geoInfo.material = this.parseMaterialIndices(geoNode.LayerElementMaterial[0])\n }\n\n if (geoNode.LayerElementNormal) {\n geoInfo.normal = this.parseNormals(geoNode.LayerElementNormal[0])\n }\n\n if (geoNode.LayerElementUV) {\n geoInfo.uv = []\n\n let i = 0\n while (geoNode.LayerElementUV[i]) {\n if (geoNode.LayerElementUV[i].UV) {\n geoInfo.uv.push(this.parseUVs(geoNode.LayerElementUV[i]))\n }\n\n i++\n }\n }\n\n geoInfo.weightTable = {}\n\n if (skeleton !== null) {\n geoInfo.skeleton = skeleton\n\n skeleton.rawBones.forEach(function (rawBone, i) {\n // loop over the bone's vertex indices and weights\n rawBone.indices.forEach(function (index, j) {\n if (geoInfo.weightTable[index] === undefined) geoInfo.weightTable[index] = []\n\n geoInfo.weightTable[index].push({\n id: i,\n weight: rawBone.weights[j],\n })\n })\n })\n }\n\n return geoInfo\n }\n\n genBuffers(geoInfo) {\n const buffers = {\n vertex: [],\n normal: [],\n colors: [],\n uvs: [],\n materialIndex: [],\n vertexWeights: [],\n weightsIndices: [],\n }\n\n let polygonIndex = 0\n let faceLength = 0\n let displayedWeightsWarning = false\n\n // these will hold data for a single face\n let facePositionIndexes = []\n let faceNormals = []\n let faceColors = []\n let faceUVs = []\n let faceWeights = []\n let faceWeightIndices = []\n\n const scope = this\n geoInfo.vertexIndices.forEach(function (vertexIndex, polygonVertexIndex) {\n let materialIndex\n let endOfFace = false\n\n // Face index and vertex index arrays are combined in a single array\n // A cube with quad faces looks like this:\n // PolygonVertexIndex: *24 {\n // 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\n // }\n // Negative numbers mark the end of a face - first face here is 0, 1, 3, -3\n // to find index of last vertex bit shift the index: ^ - 1\n if (vertexIndex < 0) {\n vertexIndex = vertexIndex ^ -1 // equivalent to ( x * -1 ) - 1\n endOfFace = true\n }\n\n let weightIndices = []\n let weights = []\n\n facePositionIndexes.push(vertexIndex * 3, vertexIndex * 3 + 1, vertexIndex * 3 + 2)\n\n if (geoInfo.color) {\n const data = getData(polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.color)\n\n faceColors.push(data[0], data[1], data[2])\n }\n\n if (geoInfo.skeleton) {\n if (geoInfo.weightTable[vertexIndex] !== undefined) {\n geoInfo.weightTable[vertexIndex].forEach(function (wt) {\n weights.push(wt.weight)\n weightIndices.push(wt.id)\n })\n }\n\n if (weights.length > 4) {\n if (!displayedWeightsWarning) {\n console.warn(\n 'THREE.FBXLoader: Vertex has more than 4 skinning weights assigned to vertex. Deleting additional weights.',\n )\n displayedWeightsWarning = true\n }\n\n const wIndex = [0, 0, 0, 0]\n const Weight = [0, 0, 0, 0]\n\n weights.forEach(function (weight, weightIndex) {\n let currentWeight = weight\n let currentIndex = weightIndices[weightIndex]\n\n Weight.forEach(function (comparedWeight, comparedWeightIndex, comparedWeightArray) {\n if (currentWeight > comparedWeight) {\n comparedWeightArray[comparedWeightIndex] = currentWeight\n currentWeight = comparedWeight\n\n const tmp = wIndex[comparedWeightIndex]\n wIndex[comparedWeightIndex] = currentIndex\n currentIndex = tmp\n }\n })\n })\n\n weightIndices = wIndex\n weights = Weight\n }\n\n // if the weight array is shorter than 4 pad with 0s\n while (weights.length < 4) {\n weights.push(0)\n weightIndices.push(0)\n }\n\n for (let i = 0; i < 4; ++i) {\n faceWeights.push(weights[i])\n faceWeightIndices.push(weightIndices[i])\n }\n }\n\n if (geoInfo.normal) {\n const data = getData(polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.normal)\n\n faceNormals.push(data[0], data[1], data[2])\n }\n\n if (geoInfo.material && geoInfo.material.mappingType !== 'AllSame') {\n materialIndex = getData(polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.material)[0]\n }\n\n if (geoInfo.uv) {\n geoInfo.uv.forEach(function (uv, i) {\n const data = getData(polygonVertexIndex, polygonIndex, vertexIndex, uv)\n\n if (faceUVs[i] === undefined) {\n faceUVs[i] = []\n }\n\n faceUVs[i].push(data[0])\n faceUVs[i].push(data[1])\n })\n }\n\n faceLength++\n\n if (endOfFace) {\n scope.genFace(\n buffers,\n geoInfo,\n facePositionIndexes,\n materialIndex,\n faceNormals,\n faceColors,\n faceUVs,\n faceWeights,\n faceWeightIndices,\n faceLength,\n )\n\n polygonIndex++\n faceLength = 0\n\n // reset arrays for the next face\n facePositionIndexes = []\n faceNormals = []\n faceColors = []\n faceUVs = []\n faceWeights = []\n faceWeightIndices = []\n }\n })\n\n return buffers\n }\n\n // Generate data for a single face in a geometry. If the face is a quad then split it into 2 tris\n genFace(\n buffers,\n geoInfo,\n facePositionIndexes,\n materialIndex,\n faceNormals,\n faceColors,\n faceUVs,\n faceWeights,\n faceWeightIndices,\n faceLength,\n ) {\n for (let i = 2; i < faceLength; i++) {\n buffers.vertex.push(geoInfo.vertexPositions[facePositionIndexes[0]])\n buffers.vertex.push(geoInfo.vertexPositions[facePositionIndexes[1]])\n buffers.vertex.push(geoInfo.vertexPositions[facePositionIndexes[2]])\n\n buffers.vertex.push(geoInfo.vertexPositions[facePositionIndexes[(i - 1) * 3]])\n buffers.vertex.push(geoInfo.vertexPositions[facePositionIndexes[(i - 1) * 3 + 1]])\n buffers.vertex.push(geoInfo.vertexPositions[facePositionIndexes[(i - 1) * 3 + 2]])\n\n buffers.vertex.push(geoInfo.vertexPositions[facePositionIndexes[i * 3]])\n buffers.vertex.push(geoInfo.vertexPositions[facePositionIndexes[i * 3 + 1]])\n buffers.vertex.push(geoInfo.vertexPositions[facePositionIndexes[i * 3 + 2]])\n\n if (geoInfo.skeleton) {\n buffers.vertexWeights.push(faceWeights[0])\n buffers.vertexWeights.push(faceWeights[1])\n buffers.vertexWeights.push(faceWeights[2])\n buffers.vertexWeights.push(faceWeights[3])\n\n buffers.vertexWeights.push(faceWeights[(i - 1) * 4])\n buffers.vertexWeights.push(faceWeights[(i - 1) * 4 + 1])\n buffers.vertexWeights.push(faceWeights[(i - 1) * 4 + 2])\n buffers.vertexWeights.push(faceWeights[(i - 1) * 4 + 3])\n\n buffers.vertexWeights.push(faceWeights[i * 4])\n buffers.vertexWeights.push(faceWeights[i * 4 + 1])\n buffers.vertexWeights.push(faceWeights[i * 4 + 2])\n buffers.vertexWeights.push(faceWeights[i * 4 + 3])\n\n buffers.weightsIndices.push(faceWeightIndices[0])\n buffers.weightsIndices.push(faceWeightIndices[1])\n buffers.weightsIndices.push(faceWeightIndices[2])\n buffers.weightsIndices.push(faceWeightIndices[3])\n\n buffers.weightsIndices.push(faceWeightIndices[(i - 1) * 4])\n buffers.weightsIndices.push(faceWeightIndices[(i - 1) * 4 + 1])\n buffers.weightsIndices.push(faceWeightIndices[(i - 1) * 4 + 2