UNPKG

@needle-tools/engine

Version:

Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.

789 lines (785 loc) 30.7 kB
import { Box3, Box3Helper, Color, Euler, GridHelper, Mesh, MeshStandardMaterial, OrthographicCamera, PerspectiveCamera, PlaneGeometry, Quaternion, Scene, ShadowMaterial, Texture, Uniform, Vector3 } from "three"; import { ShaderMaterial, WebGLRenderer } from "three"; import { GroundedSkybox } from "three/examples/jsm/objects/GroundedSkybox.js"; import { useForAutoFit } from "./engine_camera.js"; import { Mathf } from "./engine_math.js"; import { CircularBuffer } from "./engine_utils.js"; /** * Slerp between two vectors */ export function slerp(vec, end, t) { const len1 = vec.length(); const len2 = end.length(); const targetLen = Mathf.lerp(len1, len2, t); return vec.lerp(end, t).normalize().multiplyScalar(targetLen); } const _tempQuat = new Quaternion(); const flipYQuat = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI); export function lookAtInverse(obj, target) { obj.lookAt(target); obj.quaternion.multiply(flipYQuat); } /** Better lookAt * @param object the object that the lookAt should be applied to * @param target the target to look at * @param keepUpDirection if true the up direction will be kept * @param copyTargetRotation if true the target rotation will be copied so the rotation is not skewed */ export function lookAtObject(object, target, keepUpDirection = true, copyTargetRotation = false) { if (object === target) return; _tempQuat.copy(object.quaternion); const lookTarget = getWorldPosition(target); const lookFrom = getWorldPosition(object); if (copyTargetRotation) { setWorldQuaternion(object, getWorldQuaternion(target)); // look at forward again so we don't get any roll if (keepUpDirection) { const ypos = lookFrom.y; const forwardPoint = lookFrom.sub(getWorldDirection(object)); forwardPoint.y = ypos; object.lookAt(forwardPoint); object.quaternion.multiply(flipYQuat); } // sanitize – three.js sometimes returns NaN values when parents are non-uniformly scaled if (Number.isNaN(object.quaternion.x)) { object.quaternion.copy(_tempQuat); } return; } if (keepUpDirection) { lookTarget.y = lookFrom.y; } object.lookAt(lookTarget); // sanitize – three.js sometimes returns NaN values when parents are non-uniformly scaled if (Number.isNaN(object.quaternion.x)) { object.quaternion.copy(_tempQuat); } } /** * Look at a 2D point in screen space * @param object the object to look at the point * @param target the target point in 2D screen space XY e.g. from a mouse event * @param camera the camera to use for the lookAt * @param factor the factor to multiply the distance from the camera to the object. Default is 1 * @returns the target point in world space * * @example Needle Engine Component * ```ts * export class MyLookAtComponent extends Behaviour { * update() { * lookAtScreenPoint(this.gameObject, this.context.input.mousePosition, this.context.mainCamera); * } * } * ``` * * @example Look at from browser mouse move event * ```ts * window.addEventListener("mousemove", (e) => { * lookAtScreenPoint(object, new Vector2(e.clientX, e.clientY), camera); * }); * ``` */ export function lookAtScreenPoint(object, target, camera, factor = 1) { if (camera) { const pos = getTempVector(0, 0, 0); const ndcx = (target.x / window.innerWidth) * 2 - 1; const ndcy = -(target.y / window.innerHeight) * 2 + 1; pos.set(ndcx, ndcy, 0); pos.unproject(camera); // get distance from object to camera const camPos = camera.worldPosition; const dist = object.worldPosition.distanceTo(camPos); // Create direction from camera through cursor point const dir = pos.sub(camPos); dir.multiplyScalar(factor * 3.6 * dist); const targetPoint = camera.worldPosition.add(dir); object.lookAt(targetPoint); return targetPoint; } return null; } const _tempVecs = new CircularBuffer(() => new Vector3(), 100); export function getTempVector(vecOrX, y, z) { const vec = _tempVecs.get(); vec.set(0, 0, 0); // initialize with default values if (vecOrX instanceof Vector3) vec.copy(vecOrX); else if (Array.isArray(vecOrX)) vec.set(vecOrX[0], vecOrX[1], vecOrX[2]); else if (vecOrX instanceof DOMPointReadOnly) vec.set(vecOrX.x, vecOrX.y, vecOrX.z); else { if (typeof vecOrX === "number") { vec.x = vecOrX; vec.y = y !== undefined ? y : vec.x; vec.z = z !== undefined ? z : vec.x; } else if (typeof vecOrX === "object") { vec.x = vecOrX.x; vec.y = vecOrX.y; vec.z = vecOrX.z; } } return vec; } const _tempColors = new CircularBuffer(() => new Color(), 30); export function getTempColor(color) { const col = _tempColors.get(); if (color) col.copy(color); else { col.set(0, 0, 0); } return col; } const _tempQuats = new CircularBuffer(() => new Quaternion(), 100); /** * Gets a temporary quaternion. If a quaternion is passed in it will be copied to the temporary quaternion * Temporary quaternions are cached and reused internally. Don't store them! * @param value the quaternion to copy * @returns a temporary quaternion */ export function getTempQuaternion(value) { const val = _tempQuats.get(); val.identity(); if (value instanceof Quaternion) val.copy(value); else if (value instanceof DOMPointReadOnly) val.set(value.x, value.y, value.z, value.w); return val; } const _worldPositions = new CircularBuffer(() => new Vector3(), 100); const _lastMatrixWorldUpdateKey = Symbol("lastMatrixWorldUpdateKey"); /** * Get the world position of an object * @param obj the object to get the world position from * @param vec a vector to store the result in. If not passed in a temporary vector will be used * @param updateParents if true the parents will be updated before getting the world position * @returns the world position */ export function getWorldPosition(obj, vec = null, updateParents = true) { const wp = vec ?? _worldPositions.get(); if (!obj) return wp.set(0, 0, 0); if (!obj.parent) return wp.copy(obj.position); if (updateParents) obj.updateWorldMatrix(true, false); if (obj.matrixWorldNeedsUpdate && obj[_lastMatrixWorldUpdateKey] !== Date.now()) { obj[_lastMatrixWorldUpdateKey] = Date.now(); obj.updateMatrixWorld(); } wp.setFromMatrixPosition(obj.matrixWorld); return wp; } /** * Set the world position of an object * @param obj the object to set the world position of * @param val the world position to set */ export function setWorldPosition(obj, val) { if (!obj) return obj; const wp = _worldPositions.get(); if (val !== wp) wp.copy(val); const obj2 = obj?.parent ?? obj; obj2.worldToLocal(wp); obj.position.set(wp.x, wp.y, wp.z); return obj; } /** * Set the world position of an object * @param obj the object to set the world position of * @param x the x position * @param y the y position * @param z the z position */ export function setWorldPositionXYZ(obj, x, y, z) { const wp = _worldPositions.get(); wp.set(x, y, z); setWorldPosition(obj, wp); return obj; } const _worldQuaternions = new CircularBuffer(() => new Quaternion(), 100); const _worldQuaternionBuffer = new Quaternion(); const _tempQuaternionBuffer2 = new Quaternion(); export function getWorldQuaternion(obj, target = null) { if (!obj) return _worldQuaternions.get().identity(); const quat = target ?? _worldQuaternions.get(); if (!obj.parent) return quat.copy(obj.quaternion); obj.getWorldQuaternion(quat); return quat; } export function setWorldQuaternion(obj, val) { if (!obj) return; if (val !== _worldQuaternionBuffer) _worldQuaternionBuffer.copy(val); const tempVec = _worldQuaternionBuffer; const parent = obj?.parent; parent?.getWorldQuaternion(_tempQuaternionBuffer2); _tempQuaternionBuffer2.invert(); const q = _tempQuaternionBuffer2.multiply(tempVec); // console.log(tempVec); obj.quaternion.set(q.x, q.y, q.z, q.w); // console.error("quaternion world to local is not yet implemented"); } export function setWorldQuaternionXYZW(obj, x, y, z, w) { _worldQuaternionBuffer.set(x, y, z, w); setWorldQuaternion(obj, _worldQuaternionBuffer); } const _worldScaleBuffer = new CircularBuffer(() => new Vector3(), 100); const _worldScale = new Vector3(); export function getWorldScale(obj, vec = null) { if (!vec) vec = _worldScaleBuffer.get(); if (!obj) return vec.set(0, 0, 0); if (!obj.parent) return vec.copy(obj.scale); obj.getWorldScale(vec); return vec; } export function setWorldScale(obj, vec) { if (!obj) return; if (!obj.parent) { obj.scale.copy(vec); return; } const tempVec = _worldScale; const obj2 = obj.parent; obj2.getWorldScale(tempVec); obj.scale.copy(vec); obj.scale.divide(tempVec); } const _forward = new Vector3(); const _forwardQuat = new Quaternion(); export function forward(obj) { getWorldQuaternion(obj, _forwardQuat); return _forward.set(0, 0, 1).applyQuaternion(_forwardQuat); } const _worldDirectionBuffer = new CircularBuffer(() => new Vector3(), 100); const _worldDirectionQuat = new Quaternion(); /** Get the world direction. Returns world forward if nothing is passed in. * Pass in a relative direction to get it converted to world space (e.g. dir = new Vector3(0, 1, 1)) * The returned vector will not be normalized */ export function getWorldDirection(obj, dir) { // If no direction is passed in set the direction to the forward vector if (!dir) dir = _worldDirectionBuffer.get().set(0, 0, 1); getWorldQuaternion(obj, _worldDirectionQuat); return dir.applyQuaternion(_worldDirectionQuat); } const _worldEulerBuffer = new Euler(); const _worldEuler = new Euler(); const _worldRotation = new Vector3(); // world euler (in radians) export function getWorldEuler(obj) { const quat = _worldQuaternions.get(); obj.getWorldQuaternion(quat); _worldEuler.setFromQuaternion(quat); return _worldEuler; } // world euler (in radians) export function setWorldEuler(obj, val) { const quat = _worldQuaternions.get(); setWorldQuaternion(obj, quat.setFromEuler(val)); ; } // returns rotation in degrees export function getWorldRotation(obj) { const rot = getWorldEuler(obj); const wr = _worldRotation; wr.set(rot.x, rot.y, rot.z); wr.x = Mathf.toDegrees(wr.x); wr.y = Mathf.toDegrees(wr.y); wr.z = Mathf.toDegrees(wr.z); return wr; } export function setWorldRotation(obj, val) { setWorldRotationXYZ(obj, val.x, val.y, val.z, true); } export function setWorldRotationXYZ(obj, x, y, z, degrees = true) { if (degrees) { x = Mathf.toRadians(x); y = Mathf.toRadians(y); z = Mathf.toRadians(z); } _worldEulerBuffer.set(x, y, z); _worldQuaternionBuffer.setFromEuler(_worldEulerBuffer); setWorldQuaternion(obj, _worldQuaternionBuffer); } // from https://github.com/mrdoob/js/pull/10995#issuecomment-287614722 export function logHierarchy(root, collapsible = true) { if (!root) return; if (collapsible) { (function printGraph(obj) { console.groupCollapsed((obj.name ? obj.name : '(no name : ' + obj.type + ')') + ' %o', obj); obj.children.forEach(printGraph); console.groupEnd(); }(root)); } else { root.traverse(function (obj) { var s = '|___'; var obj2 = obj; while (obj2.parent !== null) { s = '\t' + s; obj2 = obj2.parent; } console.log(s + obj.name + ' <' + obj.type + '>'); }); } ; } export function getParentHierarchyPath(obj) { let path = obj?.name || ""; if (!obj) return path; let parent = obj.parent; while (parent) { path = parent.name + "/" + path; parent = parent.parent; } return path; } export function isAnimationAction(obj) { if (obj) { // this doesnt work :( // return obj instanceof AnimationAction; // instead we do this: const act = obj; return act.blendMode !== undefined && act.clampWhenFinished !== undefined && act.enabled !== undefined && act.fadeIn !== undefined && act.getClip !== undefined; } return false; } class BlitMaterial extends ShaderMaterial { static vertex = ` varying vec2 vUv; void main(){ vUv = uv; gl_Position = vec4(position.xy, 0., 1.0); }`; constructor() { super({ vertexShader: BlitMaterial.vertex, uniforms: { map: new Uniform(null), flipY: new Uniform(true), writeDepth: new Uniform(false), depthTexture: new Uniform(null) }, fragmentShader: ` uniform sampler2D map; uniform bool flipY; uniform bool writeDepth; uniform sampler2D depthTexture; varying vec2 vUv; void main(){ vec2 uv = vUv; if (flipY) uv.y = 1.0 - uv.y; gl_FragColor = texture2D(map, uv); if (writeDepth) { float depth = texture2D(depthTexture, uv).r; gl_FragDepth = depth; // float linearDepth = (depth - 0.99) * 100.0; // Enhance near 1.0 values // gl_FragColor = vec4(linearDepth, linearDepth, linearDepth, 1.0); } }` }); } reset() { this.uniforms.map.value = null; this.uniforms.flipY.value = true; this.uniforms.writeDepth.value = false; this.uniforms.depthTexture.value = null; this.needsUpdate = true; this.uniformsNeedUpdate = true; } } /** * Utility class to perform various graphics operations like copying textures to canvas */ export class Graphics { static planeGeometry = new PlaneGeometry(2, 2, 1, 1); static renderer = new WebGLRenderer({ antialias: false, alpha: true }); static perspectiveCam = new PerspectiveCamera(); static orthographicCam = new OrthographicCamera(); static scene = new Scene(); static blitMaterial = new BlitMaterial(); static mesh = new Mesh(Graphics.planeGeometry, Graphics.blitMaterial); /** * Copy a texture to a new texture * @param texture the texture to copy * @param blitMaterial the material to use for copying (optional) * @returns the newly created, copied texture */ static copyTexture(texture, blitMaterial) { // ensure that a blit material exists if (!blitMaterial) { blitMaterial = this.blitMaterial; } ; this.blitMaterial.reset(); const material = blitMaterial || this.blitMaterial; // TODO: reset the uniforms... material.uniforms.map.value = texture; material.needsUpdate = true; material.uniformsNeedUpdate = true; // ensure that the blit material has the correct vertex shader const origVertexShader = material.vertexShader; material.vertexShader = BlitMaterial.vertex; const mesh = this.mesh; mesh.material = material; mesh.frustumCulled = false; this.scene.children.length = 0; this.scene.add(mesh); this.renderer.setSize(texture.image.width, texture.image.height); this.renderer.clear(); this.renderer.render(this.scene, this.perspectiveCam); const tex = new Texture(this.renderer.domElement); tex.name = "Copy"; tex.needsUpdate = true; // < important! // reset vertex shader material.vertexShader = origVertexShader; return tex; } static blit(src, target, options) { const { renderer = this.renderer, blitMaterial = this.blitMaterial, flipY = false, depthTexture = null, depthTest = true, depthWrite = true, } = options || {}; this.blitMaterial.reset(); if (blitMaterial.uniforms.map) blitMaterial.uniforms.map.value = src; if (blitMaterial.uniforms.flipY) blitMaterial.uniforms.flipY.value = flipY; if (depthTexture) { blitMaterial.uniforms.writeDepth = new Uniform(true); blitMaterial.uniforms.depthTexture.value = depthTexture; } else { blitMaterial.uniforms.writeDepth = new Uniform(false); blitMaterial.uniforms.depthTexture.value = null; } blitMaterial.needsUpdate = true; blitMaterial.uniformsNeedUpdate = true; const mesh = this.mesh; mesh.material = blitMaterial; mesh.frustumCulled = false; this.scene.children.length = 0; this.scene.add(mesh); // Save state const renderTarget = renderer.getRenderTarget(); const gl = renderer.getContext(); const depthTestEnabled = gl.getParameter(gl.DEPTH_TEST); const depthWriteMask = gl.getParameter(gl.DEPTH_WRITEMASK); const depthFunc = gl.getParameter(gl.DEPTH_FUNC); // Set state if (depthTest) renderer.getContext().enable(renderer.getContext().DEPTH_TEST); else renderer.getContext().disable(renderer.getContext().DEPTH_TEST); renderer.state.buffers.depth.setMask(depthWrite); renderer.setClearColor(new Color(0, 0, 0), 0); // renderer.setSize(target.width, target.height); renderer.setPixelRatio(window.devicePixelRatio); renderer.setRenderTarget(target); renderer.clear(); renderer.render(this.scene, this.perspectiveCam); // Restore state renderer.setRenderTarget(renderTarget); // reset render target const depthBuffer = renderer.state.buffers.depth; depthBuffer.setTest(depthTestEnabled); depthBuffer.setMask(depthWriteMask); depthBuffer.setFunc(depthFunc); } /** * Copy a texture to a HTMLCanvasElement * @param texture the texture convert * @param force if true the texture will be copied to a new texture before converting * @returns the HTMLCanvasElement with the texture or null if the texture could not be copied */ static textureToCanvas(texture, force = false) { if (!texture) { return null; } if (force === true || texture["isCompressedTexture"] === true) { texture = copyTexture(texture); } const image = texture.image; if (isImageBitmap(image)) { const canvas = document.createElement('canvas'); canvas.width = image.width; canvas.height = image.height; const context = canvas.getContext('2d'); if (!context) { console.error("Failed getting canvas 2d context"); return null; } context.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height); return canvas; } return null; } } /**@obsolete use Graphics.copyTexture */ export function copyTexture(texture) { return Graphics.copyTexture(texture); } /**@obsolete use Graphics.textureToCanvas */ export function textureToCanvas(texture, force = false) { return Graphics.textureToCanvas(texture, force); } ; function isImageBitmap(image) { return (typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement) || (typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement) || (typeof OffscreenCanvas !== 'undefined' && image instanceof OffscreenCanvas) || (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap); } function isMesh(obj) { const type = obj.type; // Note: we don't use the instanceof check here because it's unreliable in certain minification scenarios where it returns false return type === "Mesh" || type === "SkinnedMesh"; } // for contact shadows export function setVisibleInCustomShadowRendering(obj, enabled) { if (enabled) obj["needle:rendercustomshadow"] = true; else { obj["needle:rendercustomshadow"] = false; } } export function getVisibleInCustomShadowRendering(obj) { if (obj) { if (obj["needle:rendercustomshadow"] === true) { return true; } else if (obj["needle:rendercustomshadow"] == undefined) { return true; } } return false; } /** * Get the axis-aligned bounding box of a list of objects. * @param objects The objects to get the bounding box from. * @param ignore Objects to ignore when calculating the bounding box. Objects that are invisible (gizmos, helpers, etc.) are excluded by default. * @param layers The layers to include. Typically the main camera's layers. * @param result The result box to store the bounding box in. Returns a new box if not passed in. */ export function getBoundingBox(objects, ignore = undefined, layers = undefined, result = undefined) { const box = result || new Box3(); box.makeEmpty(); const emptyChildren = []; function expandByObjectRecursive(obj) { let allowExpanding = true; // we dont want to check invisible objects if (!obj.visible) return; if (useForAutoFit(obj) === false) return; if (obj.type === "TransformControlsGizmo" || obj.type === "TransformControlsPlane") return; // ignore Box3Helpers if (obj instanceof Box3Helper) allowExpanding = false; if (obj instanceof GridHelper) allowExpanding = false; // ignore GroundProjectedEnv if (obj instanceof GroundedSkybox) allowExpanding = false; if (obj.isGizmo === true) allowExpanding = false; // // Ignore shadow catcher geometry if (obj.material instanceof ShadowMaterial) allowExpanding = false; // ONLY fit meshes if (!(isMesh(obj))) allowExpanding = false; // Layer test, typically with the main camera if (layers && obj.layers.test(layers) === false) allowExpanding = false; if (allowExpanding) { // Ignore things parented to the camera + ignore the camera if (ignore && Array.isArray(ignore) && ignore?.includes(obj)) return; else if (typeof ignore === "function") { if (ignore(obj) === true) return; } } // We don't want to fit UI objects if (obj["isUI"] === true) return; // If we encountered some geometry that should be ignored // Then we don't want to use that for expanding the view if (allowExpanding) { // Temporary override children const children = obj.children; obj.children = emptyChildren; // TODO: validate that object doesn't contain NaN values const pos = obj.position; const scale = obj.scale; if (Number.isNaN(pos.x) || Number.isNaN(pos.y) || Number.isNaN(pos.z)) { console.warn(`Object \"${obj.name}\" has NaN values in position or scale.... will ignore it`, pos, scale); return; } // Sanitize for the three.js method that only checks for undefined here // @ts-ignore if (obj.geometry === null) obj.geometry = undefined; box.expandByObject(obj, true); obj.children = children; } for (const child of obj.children) { expandByObjectRecursive(child); } } let hasAnyObject = false; if (!Array.isArray(objects)) objects = [objects]; for (const object of objects) { if (!object) continue; hasAnyObject = true; object.updateMatrixWorld(); expandByObjectRecursive(object); } if (!hasAnyObject) { console.warn("No objects to fit camera to..."); return box; } return box; } /** * Fits an object into a bounding volume. The volume is defined by a Box3 in world space. * @param obj the object to fit * @param volume the volume to fit the object into * @param opts options for fitting */ export function fitObjectIntoVolume(obj, volume, opts) { const box = getBoundingBox([obj], opts?.ignore); const boundsSize = new Vector3(); box.getSize(boundsSize); const boundsCenter = new Vector3(); box.getCenter(boundsCenter); const targetSize = new Vector3(); volume.getSize(targetSize); const targetCenter = new Vector3(); volume.getCenter(targetCenter); const scale = new Vector3(); scale.set(targetSize.x / boundsSize.x, targetSize.y / boundsSize.y, targetSize.z / boundsSize.z); const minScale = Math.min(scale.x, scale.y, scale.z); const useScale = opts?.scale !== false; if (useScale) { setWorldScale(obj, getWorldScale(obj).multiplyScalar(minScale)); } if (opts?.position !== false) { const boundsBottomPosition = new Vector3(); box.getCenter(boundsBottomPosition); boundsBottomPosition.y = box.min.y; const targetBottomPosition = new Vector3(); volume.getCenter(targetBottomPosition); targetBottomPosition.y = volume.min.y; const offset = targetBottomPosition.clone().sub(boundsBottomPosition); if (useScale) offset.multiplyScalar(minScale); setWorldPosition(obj, getWorldPosition(obj).add(offset)); } return { boundsBefore: box, scale, }; } /** * Place an object on a surface. This will calculate the object bounds which might be an expensive operation for complex objects. * The object will be visually placed on the surface (the object's pivot will be ignored). * @param obj the object to place on the surface * @param point the point to place the object on * @returns the offset from the object bounds to the pivot */ export function placeOnSurface(obj, point) { const bounds = getBoundingBox([obj]); const center = new Vector3(); bounds.getCenter(center); center.y = bounds.min.y; const offset = point.clone().sub(center); const worldPos = getWorldPosition(obj); setWorldPosition(obj, worldPos.add(offset)); return { offset, bounds, }; } /** * Postprocesses the material of an object loaded by {@link FBXLoader}. * It will apply necessary color conversions, remap shininess to roughness, and turn everything into {@link MeshStandardMaterial} on the object. * This ensures consistent lighting and shading, including environment effects. */ export function postprocessFBXMaterials(obj, material, index, array) { if (Array.isArray(material)) { let success = true; for (let i = 0; i < material.length; i++) { const res = postprocessFBXMaterials(obj, material[i], i, material); if (!res) success = false; } return success; } // ignore if the material is already a MeshStandardMaterial if (material.type === "MeshStandardMaterial" || material.type === "MeshBasicMaterial") { return false; } // check if the material was already processed else if (material["material:fbx"] != undefined) { return true; } const newMaterial = new MeshStandardMaterial(); newMaterial["material:fbx"] = material; const oldMaterial = material; if (oldMaterial) { // If a map is present then the FBX color should be ignored // Tested e.g. in Unity and Substance Stager // https://docs.unity3d.com/2020.1/Documentation/Manual/FBXImporter-Materials.html#:~:text=If%20a%20diffuse%20Texture%20is%20set%2C%20it%20ignores%20the%20diffuse%20color%20(this%20matches%20how%20it%20works%20in%20Autodesk®%20Maya®%20and%20Autodesk®%203ds%20Max®) if (!oldMaterial.map) newMaterial.color.copyLinearToSRGB(oldMaterial.color); else newMaterial.color.set(1, 1, 1); newMaterial.emissive.copyLinearToSRGB(oldMaterial.emissive); newMaterial.emissiveIntensity = oldMaterial.emissiveIntensity; newMaterial.opacity = oldMaterial.opacity; newMaterial.displacementScale = oldMaterial.displacementScale; newMaterial.transparent = oldMaterial.transparent; newMaterial.bumpMap = oldMaterial.bumpMap; newMaterial.aoMap = oldMaterial.aoMap; newMaterial.map = oldMaterial.map; newMaterial.displacementMap = oldMaterial.displacementMap; newMaterial.emissiveMap = oldMaterial.emissiveMap; newMaterial.normalMap = oldMaterial.normalMap; newMaterial.envMap = oldMaterial.envMap; newMaterial.alphaMap = oldMaterial.alphaMap; newMaterial.metalness = oldMaterial.reflectivity; newMaterial.vertexColors = oldMaterial.vertexColors; if (oldMaterial.shininess) { // from blender source code // https://github.com/blender/blender-addons/blob/5e66092bcbe0df6855b3fa814b4826add8b01360/io_scene_fbx/import_fbx.py#L1442 // https://github.com/blender/blender-addons/blob/main/io_scene_fbx/import_fbx.py#L2060 newMaterial.roughness = 1.0 - (Math.sqrt(oldMaterial.shininess) / 10.0); } newMaterial.needsUpdate = true; } if (index === undefined) { obj.material = newMaterial; } else { array[index] = newMaterial; } return true; } //# sourceMappingURL=engine_three_utils.js.map