import { getRaycastMesh } from "@needle-tools/gltf-progressive"; import { AxesHelper, Material, Mesh, Object3D, SkinnedMesh, Texture, Vector4 } from "three"; import { showBalloonWarning } from "../engine/debug/index.js"; import { getComponent, getOrAddComponent } from "../engine/engine_components.js"; import { Gizmos } from "../engine/engine_gizmos.js"; import { InstancingUtil, NEED_UPDATE_INSTANCE_KEY } from "../engine/engine_instancing.js"; import { isLocalNetwork } from "../engine/engine_networking_utils.js"; import { serializable } from "../engine/engine_serialization_decorator.js"; import { FrameEvent } from "../engine/engine_setup.js"; import { getTempVector } from "../engine/engine_three_utils.js"; import type { IRenderer, ISharedMaterials } from "../engine/engine_types.js"; import { getParam } from "../engine/engine_utils.js"; import { NEEDLE_render_objects } from "../engine/extensions/NEEDLE_render_objects.js"; import { setCustomVisibility } from "../engine/js-extensions/Layers.js"; import { Behaviour, GameObject } from "./Component.js"; import { ReflectionProbe } from "./ReflectionProbe.js"; import { InstanceHandle, InstancingHandler } from "./RendererInstancing.js" // import { RendererCustomShader } from "./RendererCustomShader.js"; import { RendererLightmap } from "./RendererLightmap.js"; // for staying compatible with old code export { InstancingUtil } from "../engine/engine_instancing.js"; const debugRenderer = getParam("debugrenderer"); const debugskinnedmesh = getParam("debugskinnedmesh"); const suppressInstancing = getParam("noinstancing"); const showWireframe = getParam("wireframe"); export enum ReflectionProbeUsage { Off = 0, BlendProbes = 1, BlendProbesAndSkybox = 2, Simple = 3, } export class FieldWithDefault { public path: string | null = null; public asset: object | null = null; public default: any; } export enum RenderState { Both = 0, Back = 1, Front = 2, } // support sharedMaterials[index] assigning materials directly to the objects class SharedMaterialArray implements ISharedMaterials { [num: number]: Material; private _renderer: Renderer; private _targets: Object3D[] = []; private _indexMapMaxIndex?: number; private _indexMap?: Map; private _changed: boolean = false; get changed(): boolean { return this._changed; } set changed(value: boolean) { if (value === true) { if (debugRenderer) console.warn("SharedMaterials have changed: " + this._renderer.name, this); } this._changed = value; } is(renderer: Renderer) { return this._renderer === renderer; } constructor(renderer: Renderer, originalMaterials: Material[]) { this._renderer = renderer; const setMaterial = this.setMaterial.bind(this); const getMaterial = this.getMaterial.bind(this); const go = renderer.gameObject; this._targets = []; if (go) { switch (go.type) { case "Group": this._targets = [...go.children]; break; case "SkinnedMesh": case "Mesh": this._targets.push(go); break; } } // this is useful to have an index map when e.g. materials are trying to be assigned by index let hasMissingMaterials = false; let indexMap: Map | undefined = undefined; let maxIndex: number = 0; for (let i = 0; i < this._targets.length; i++) { const target = this._targets[i] as Mesh; if (!target) continue; const mat = target.material as Material; if (!mat) continue; // set the shadow side to the same as the side of the material, three flips this for some reason mat.shadowSide = mat.side; for (let k = 0; k < originalMaterials.length; k++) { const orig = originalMaterials[k]; if (!orig) { hasMissingMaterials = true; continue; } if (mat.name === orig.name) { if (indexMap === undefined) indexMap = new Map(); indexMap.set(k, i); maxIndex = Math.max(maxIndex, k); // console.log(`Material ${mat.name} at ${k} was found at index ${i} in renderer ${renderer.name}.`) break; } } } if (hasMissingMaterials) { this._indexMapMaxIndex = maxIndex; this._indexMap = indexMap; const warningMessage = `Renderer ${renderer.name} was initialized with missing materials - this may lead to unexpected behaviour when trying to access sharedMaterials by index.`; console.warn(warningMessage); if (isLocalNetwork()) showBalloonWarning("Found renderer with missing materials: please check the console for details."); } // this lets us override the javascript indexer, only works in ES6 tho // but like that we can use sharedMaterials[index] and it will be assigned to the object directly return new Proxy(this, { get(target, key) { if (typeof key === "string") { const index = parseInt(key); if (!isNaN(index)) { return getMaterial(index); } } return target[key]; }, set(target, key, value) { if (typeof key === "string") setMaterial(value, Number.parseInt(key)); // console.log(target, key, value); if (Reflect.set(target, key, value)) { if (value instanceof Material) target.changed = true; return true; } return false; } }); } get length(): number { if (this._indexMapMaxIndex !== undefined) return this._indexMapMaxIndex + 1; return this._targets.length; } // iterator to support: for(const mat of sharedMaterials) *[Symbol.iterator]() { for (let i = 0; i < this.length; i++) { yield this.getMaterial(i); } } private resolveIndex(index: number): number { const map = this._indexMap; // if we have a index map it means that some materials were missing if (map) { if (map.has(index)) return map.get(index) as number; // return -1; } return index; } private setMaterial(mat: Material, index: number) { index = this.resolveIndex(index); if (index < 0 || index >= this._targets.length) return; const target = this._targets[index]; if (!target || target["material"] === undefined) return; target["material"] = mat; this.changed = true; } private getMaterial(index: number): Material | null { index = this.resolveIndex(index); if (index < 0) return null; const obj = this._targets; if (index >= obj.length) return null; const target = obj[index]; if (!target) return null; return target["material"]; } } /** * @category Rendering * @group Components */ export class Renderer extends Behaviour implements IRenderer { /** Enable or disable instancing for an object. This will create a Renderer component if it does not exist yet. * @returns the Renderer component that was created or already existed on the object */ static setInstanced(obj: Object3D, enableInstancing: boolean): Renderer { const renderer = getOrAddComponent(obj, Renderer); renderer.setInstancingEnabled(enableInstancing); return renderer; } /** Check if an object is currently rendered using instancing * @returns true if the object is rendered using instancing */ static isInstanced(obj: Object3D): boolean { const renderer = getComponent(obj, Renderer); if (renderer) return renderer.isInstancingActive; return InstancingUtil.isUsingInstancing(obj); } /** Set the rendering state only of an object (makes it visible or invisible) without affecting component state or child hierarchy visibility! You can also just enable/disable the Renderer component on that object for the same effect! * * If you want to activate or deactivate a complete object you can use obj.visible as usual (it acts the same as setActive in Unity) */ static setVisible(obj: Object3D, visible: boolean) { setCustomVisibility(obj, visible); } @serializable() receiveShadows: boolean = false; @serializable() shadowCastingMode: ShadowCastingMode = ShadowCastingMode.Off; @serializable() lightmapIndex: number = -1; @serializable(Vector4) lightmapScaleOffset: Vector4 = new Vector4(1, 1, 0, 0); /** If the renderer should use instancing * If this is a boolean (true) all materials will be instanced or (false) none of them. * If this is an array of booleans the materials will be instanced based on the index of the material. */ @serializable() enableInstancing: boolean | boolean[] | undefined = undefined; @serializable() renderOrder: number[] | undefined = undefined; @serializable() allowOcclusionWhenDynamic: boolean = true; @serializable(Object3D) probeAnchor?: Object3D; @serializable() reflectionProbeUsage: ReflectionProbeUsage = ReflectionProbeUsage.Off; // custom shader // get materialProperties(): Array | undefined { // return this._materialProperties; // } // set materialProperties(value: Array | undefined) { // this._materialProperties = value; // } // private customShaderHandler: RendererCustomShader | undefined = undefined; // private _materialProperties: Array | undefined = undefined; private _lightmaps?: RendererLightmap[]; /** Get the mesh Object3D for this renderer * Warn: if this is a multimaterial object it will return the first mesh only * @returns a mesh object3D. * */ get sharedMesh(): Mesh | SkinnedMesh | undefined { if (this.gameObject.type === "Mesh") { return this.gameObject as unknown as Mesh } else if (this.gameObject.type === "SkinnesMesh") { return this.gameObject as unknown as SkinnedMesh; } else if (this.gameObject.type === "Group") { return this.gameObject.children[0] as unknown as Mesh; } return undefined; } private readonly _sharedMeshes: Mesh[] = []; /** Get all the mesh Object3D for this renderer * @returns an array of mesh object3D. */ get sharedMeshes(): Mesh[] { if (this.destroyed || !this.gameObject) return this._sharedMeshes; this._sharedMeshes.length = 0; if (this.gameObject.type === "Group") { for (const ch of this.gameObject.children) { if (ch.type === "Mesh" || ch.type === "SkinnedMesh") { this._sharedMeshes.push(ch as Mesh); } } } else if (this.gameObject.type === "Mesh" || this.gameObject.type === "SkinnedMesh") { this._sharedMeshes.push(this.gameObject as unknown as Mesh); } return this._sharedMeshes; } get sharedMaterial(): Material { return this.sharedMaterials[0]; } set sharedMaterial(mat: Material) { const cur = this.sharedMaterials[0]; if (cur === mat) return; this.sharedMaterials[0] = mat; this.applyLightmapping(); } /**@deprecated please use sharedMaterial */ get material(): Material { return this.sharedMaterials[0]; } /**@deprecated please use sharedMaterial */ set material(mat: Material) { this.sharedMaterial = mat; } private _sharedMaterials!: SharedMaterialArray; private _originalMaterials?: Material[]; private _probeAnchorLastFrame?: Object3D; // this is just available during deserialization private set sharedMaterials(_val: Array) { // TODO: elements in the array might be missing at the moment which leads to problems if an index is serialized if (!this._originalMaterials) { this._originalMaterials = _val as Material[]; } else if (_val) { let didWarn = false; for (let i = 0; i < this._sharedMaterials.length; i++) { const mat = i < _val.length ? _val[i] : null; if (mat && mat instanceof Material) { this.sharedMaterials[i] = mat as Material; } else { if (!didWarn) { didWarn = true; console.warn("Can not assign null as material: " + this.name, mat); } } } } } //@ts-ignore get sharedMaterials(): SharedMaterialArray { if (!this._sharedMaterials || !this._sharedMaterials.is(this)) { if (!this._originalMaterials) this._originalMaterials = []; this._sharedMaterials = new SharedMaterialArray(this, this._originalMaterials); } return this._sharedMaterials!; } public static get shouldSuppressInstancing() { return suppressInstancing; } private _lightmapTextureOverride: Texture | null | undefined = undefined; public get lightmap(): Texture | null { if (this._lightmaps?.length) { return this._lightmaps[0].lightmap; } return null; } /** set undefined to return to default lightmap */ public set lightmap(tex: Texture | null | undefined) { this._lightmapTextureOverride = tex; if (tex === undefined) { tex = this.context.lightmaps.tryGetLightmap(this.sourceId, this.lightmapIndex); } if (this._lightmaps?.length) { for (const lm of this._lightmaps) { lm.lightmap = tex; } } } get hasLightmap(): boolean { const lm = this.lightmap; return lm !== null && lm !== undefined; } public allowProgressiveLoading: boolean = true; private _firstFrame: number = -1; registering() { if (!this.enabled) { this.setVisibility(false); } } awake() { this._firstFrame = this.context.time.frame; if (debugRenderer) console.log("Renderer ", this.name, this); this.clearInstancingState(); if (this.probeAnchor && debugRenderer) this.probeAnchor.add(new AxesHelper(.2)); this._reflectionProbe = null; if (this.isMultiMaterialObject(this.gameObject)) { for (const child of this.gameObject.children) { this.context.addBeforeRenderListener(child, this.onBeforeRenderThree); child.layers.mask = this.gameObject.layers.mask; } if (this.renderOrder !== undefined) { // Objects can have nested renderers (e.g. contain 2 meshes and then again another group) // or perhaps just regular child objects that have their own renderer component (?) let index = 0; for (let i = 0; i < this.gameObject.children.length; i++) { const ch = this.gameObject.children[i]; // ignore nested groups or objects that have their own renderer (aka their own render order settings) if (!this.isMeshOrSkinnedMesh(ch) || GameObject.getComponent(ch, Renderer)) continue; if (this.renderOrder.length <= index) { console.warn("Incorrect renderOrder element count", this, this.renderOrder.length + " but expected " + this.gameObject.children.length, "Index: " + index, "ChildElement:", ch); continue; } // if(debugRenderer) console.log("Setting render order", ch, this.renderOrder[index]) ch.renderOrder = this.renderOrder[index]; index += 1; } } } // TODO: custom shader with sub materials else if (this.isMeshOrSkinnedMesh(this.gameObject)) { this.context.addBeforeRenderListener(this.gameObject, this.onBeforeRenderThree); if (this.renderOrder !== undefined && this.renderOrder.length > 0) this.gameObject.renderOrder = this.renderOrder[0]; } else { this.context.addBeforeRenderListener(this.gameObject, this.onBeforeRenderThree); } this.applyLightmapping(); if (showWireframe) { for (let i = 0; i < this.sharedMaterials.length; i++) { const mat: any = this.sharedMaterials[i]; if (mat) { mat.wireframe = true; } } } } private applyLightmapping() { if (this.lightmapIndex >= 0) { const type = this.gameObject.type; // use the override lightmap if its not undefined const tex = this._lightmapTextureOverride !== undefined ? this._lightmapTextureOverride : this.context.lightmaps.tryGetLightmap(this.sourceId, this.lightmapIndex); if (tex) { if (!this._lightmaps) this._lightmaps = []; if (type === "Mesh") { const mat = this.gameObject["material"]; if (!mat?.isMeshBasicMaterial) { if (this._lightmaps.length <= 0) { const rm = new RendererLightmap(this.gameObject as any as Mesh, this.context); this._lightmaps.push(rm); } const rm = this._lightmaps[0]; rm.init(this.lightmapIndex, this.lightmapScaleOffset, tex); } else { if (mat) console.warn("Lightmapping is not supported on MeshBasicMaterial", mat.name) } } // for multi materials we need to loop through children // and then we add a lightmap renderer component to each of them else if (this.isMultiMaterialObject(this.gameObject) && this.sharedMaterials.length > 0) { for (let i = 0; i < this.gameObject.children.length; i++) { const child = this.gameObject.children[i]; if (!child["material"]?.isMeshBasicMaterial) { let rm: RendererLightmap | undefined = undefined; if (i >= this._lightmaps.length) { rm = new RendererLightmap(child as Mesh, this.context); this._lightmaps.push(rm); } else rm = this._lightmaps[i]; rm.init(this.lightmapIndex, this.lightmapScaleOffset, tex); } } } } else { if (debugRenderer) console.warn("Lightmap not found", this.sourceId, this.lightmapIndex); } } } private _isInstancingEnabled: boolean = false; private _handles: InstanceHandle[] | null | undefined = undefined; /** * @returns true if this renderer has instanced objects */ get isInstancingActive() { return this._handles != undefined && this._handles.length > 0 && this._isInstancingEnabled; } /** @returns the instancing handles */ get instances(): InstanceHandle[] | null { if (!this._handles || this._handles.length <= 0) { return null; } this._handlesTempArray.length = 0; if (this._handles) { for (const h of this._handles) { this._handlesTempArray.push(h); } } return this._handlesTempArray; } private _handlesTempArray: InstanceHandle[] = []; /** Enable or disable instancing for this renderer. * @param enabled true to enable instancing, false to disable it */ setInstancingEnabled(enabled: boolean): boolean { if (this._isInstancingEnabled === enabled) return enabled && (this._handles === undefined || this._handles != null && this._handles.length > 0); this._isInstancingEnabled = enabled; if (enabled) { if (this.enableInstancing === undefined) this.enableInstancing = true; if (this._handles === undefined) { this._handles = InstancingHandler.instance.setup(this, this.gameObject, this.context, null, { rend: this, foundMeshes: 0, useMatrixWorldAutoUpdate: this.useInstanceMatrixWorldAutoUpdate() }); if (this._handles) { GameObject.markAsInstancedRendered(this.gameObject, true); return true; } } else if (this._handles !== null) { for (const handler of this._handles) { handler.updateInstanceMatrix(true); handler.add(); } GameObject.markAsInstancedRendered(this.gameObject, true); return true; } } else { if (this._handles) { for (const handler of this._handles) { handler.remove(this.destroyed); } } return true; } return false; } private clearInstancingState() { this._isInstancingEnabled = false; this._handles = undefined; } /** Return true to wrap matrix update events for instanced rendering to update instance matrices automatically when matrixWorld changes * This is a separate method to be overrideable from user code */ useInstanceMatrixWorldAutoUpdate() { return true; } start() { if (this.enableInstancing && !suppressInstancing) { this.setInstancingEnabled(true); // make sure the instance is marked dirty once for cases where e.g. an animator animates the instanced object // in the first frame we want the updated matrix then to be applied immediately to the instancing InstancingUtil.markDirty(this.gameObject); } this.gameObject.frustumCulled = this.allowOcclusionWhenDynamic; if (this.isMultiMaterialObject(this.gameObject)) { for (let i = 0; i < this.gameObject.children.length; i++) { const ch = this.gameObject.children[i]; ch.frustumCulled = this.allowOcclusionWhenDynamic; } } } onEnable() { // ensure shared meshes are initialized const _ = this.sharedMeshes; this.setVisibility(true); // Check if the renderer is using instancing (or any child object is supposed to use instancing) const isUsingInstancing = this._isInstancingEnabled || (this.enableInstancing == true || (Array.isArray(this.enableInstancing) && this.enableInstancing.some(x => x))); if (isUsingInstancing) { if (this.__internalDidAwakeAndStart) this.setInstancingEnabled(true); } // if no insancing is used we can apply the stencil settings // but instancing and stencil at the same time is not supported else if (this.enabled) { this.applyStencil(); } this.updateReflectionProbe(); // this.testIfLODLevelsAreAvailable(); } onDisable() { this.setVisibility(false); if (this._handles && this._handles.length > 0) { this.setInstancingEnabled(false); } } onDestroy(): void { this._handles = null; if (this.isMultiMaterialObject(this.gameObject)) { for (const child of this.gameObject.children) { this.context.removeBeforeRenderListener(child, this.onBeforeRenderThree); } } else { this.context.removeBeforeRenderListener(this.gameObject, this.onBeforeRenderThree); } } onBeforeRender() { if (!this.gameObject) { return; } if (this._probeAnchorLastFrame !== this.probeAnchor) { this._reflectionProbe?.onUnset(this); this.updateReflectionProbe(); } if (debugRenderer == this.name && this.gameObject instanceof Mesh) { this.gameObject.geometry.computeBoundingSphere(); const tempCenter = getTempVector(this.gameObject.geometry.boundingSphere.center).applyMatrix4(this.gameObject.matrixWorld); Gizmos.DrawWireSphere(tempCenter, this.gameObject.geometry.boundingSphere.radius, 0x00ddff); } if (this.isMultiMaterialObject(this.gameObject) && this.gameObject.children?.length > 0) { for (const ch of this.gameObject.children) { this.applySettings(ch); } } else { this.applySettings(this.gameObject); } if (this.sharedMaterials.changed) { this.sharedMaterials.changed = false; this.applyLightmapping(); } if (this._handles?.length) { // if (this.name === "Darbouka") // console.log(this.name, this.gameObject.matrixWorldNeedsUpdate); const needsUpdate: boolean = this.gameObject[NEED_UPDATE_INSTANCE_KEY] === true;// || this.gameObject.matrixWorldNeedsUpdate; if (needsUpdate) { // if (debugInstancing) console.log("UPDATE INSTANCED MATRICES at frame #" + this.context.time.frame); this.gameObject[NEED_UPDATE_INSTANCE_KEY] = false; const remove = false;// Math.random() < .01; for (let i = this._handles.length - 1; i >= 0; i--) { const h = this._handles[i]; if (remove) { h.remove(this.destroyed); this._handles.splice(i, 1); } else h.updateInstanceMatrix(); } this.gameObject.matrixWorldNeedsUpdate = false; } } if (this._handles && this._handles.length <= 0) { GameObject.markAsInstancedRendered(this.gameObject, false); } if (this._isInstancingEnabled && this._handles) { for (let i = 0; i < this._handles.length; i++) { const handle = this._handles[i]; setCustomVisibility(handle.object, false); } } if (this.reflectionProbeUsage !== ReflectionProbeUsage.Off && this._reflectionProbe) { this._reflectionProbe.onSet(this); } } private onBeforeRenderThree = (_renderer, _scene, _camera, _geometry, material, _group) => { if (material.envMapIntensity !== undefined) { const factor = this.hasLightmap ? Math.PI : 1; const environmentIntensity = this.context.mainCameraComponent?.environmentIntensity ?? 1; material.envMapIntensity = Math.max(0, environmentIntensity * this.context.sceneLighting.environmentIntensity / factor); // since three 163 we need to set the envMap to the scene envMap if it is not set // otherwise the envmapIntensity has no effect: https://github.com/mrdoob/three.js/pull/27903 // internal issue: https://linear.app/needle/issue/NE-6363 if (!material.envMap) material.envMap = this.context.scene.environment; } if (this._lightmaps) { for (const lm of this._lightmaps) { lm.updateLightmapUniforms(material); lm.applyLightmap(); } } } onAfterRender() { if (this._isInstancingEnabled && this._handles) { for (let i = 0; i < this._handles.length; i++) { const handle = this._handles[i]; setCustomVisibility(handle.object, true); } } if (this.reflectionProbeUsage !== ReflectionProbeUsage.Off && this._reflectionProbe) { this._reflectionProbe.onUnset(this); } if (this.static && this.gameObject.matrixAutoUpdate) { this.gameObject.matrixAutoUpdate = false; } } /** Applies stencil settings for this renderer's objects (if stencil settings are available) */ applyStencil() { NEEDLE_render_objects.applyStencil(this); } /** Apply the settings of this renderer to the given object * Settings include shadow casting and receiving (e.g. this.receiveShadows, this.shadowCastingMode) */ applySettings(go: Object3D) { go.receiveShadow = this.receiveShadows; if (this.shadowCastingMode == ShadowCastingMode.On) { go.castShadow = true; } else go.castShadow = false; } private _reflectionProbe: ReflectionProbe | null = null; private updateReflectionProbe() { // handle reflection probe this._reflectionProbe = null; if (this.reflectionProbeUsage !== ReflectionProbeUsage.Off) { // update the reflection probe right before rendering // if we do it immediately the reflection probe might not be enabled yet // (since this method is called from onEnable) this.startCoroutine(this._updateReflectionProbe(), FrameEvent.LateUpdate); this._probeAnchorLastFrame = this.probeAnchor; } } private *_updateReflectionProbe() { const obj = this.probeAnchor || this.gameObject; const isAnchor = this.probeAnchor ? true : false; this._reflectionProbe = ReflectionProbe.get(obj, this.context, isAnchor, this.probeAnchor); } private setVisibility(visible: boolean) { if (!this.isMultiMaterialObject(this.gameObject)) { setCustomVisibility(this.gameObject, visible); } else { for (const ch of this.gameObject.children) { if (this.isMeshOrSkinnedMesh(ch)) { setCustomVisibility(ch, visible); } } } } private isMultiMaterialObject(obj: Object3D) { return obj.type === "Group"; } private isMeshOrSkinnedMesh(obj: Object3D): obj is Mesh | SkinnedMesh { return obj.type === "Mesh" || obj.type === "SkinnedMesh"; } } export class MeshRenderer extends Renderer { } export class SkinnedMeshRenderer extends MeshRenderer { private _needUpdateBoundingSphere = false; // private _lastWorldPosition = new Vector3(); awake() { super.awake(); if (debugskinnedmesh) console.log("SkinnedMeshRenderer for \"" + this.name + "\"", this); // disable skinned mesh occlusion because of https://github.com/mrdoob/js/issues/14499 this.allowOcclusionWhenDynamic = false; for (const mesh of this.sharedMeshes) { // If we don't do that here the bounding sphere matrix used for raycasts will be wrong. Not sure *why* this is necessary mesh.parent?.updateWorldMatrix(false, true); this.markBoundsDirty(); } } onAfterRender(): void { super.onAfterRender(); // this.gameObject.parent.position.x += Math.sin(this.context.time.time) * .01; // if (this.gameObject instanceof SkinnedMesh && this.gameObject.geometry.boundingSphere) { // const bounds = this.gameObject.geometry.boundingSphere; // const worldpos = getTempVector().setFromMatrixPosition(this.gameObject.matrixWorld); // if (worldpos.distanceTo(this._lastWorldPosition) > bounds.radius) { // this._lastWorldPosition.copy(worldpos); // this.markBoundsDirty(); // }; // } if (this._needUpdateBoundingSphere) { for (const mesh of this.sharedMeshes) { if (mesh instanceof SkinnedMesh) { this._needUpdateBoundingSphere = false; try { const geometry = mesh.geometry; const raycastmesh = getRaycastMesh(mesh); if (raycastmesh) { mesh.geometry = raycastmesh; } mesh.computeBoundingSphere(); mesh.geometry = geometry; } catch(err) { console.error(`Error updating bounding sphere for ${mesh.name}`, err); } } } } // if (this.context.time.frame % 30 === 0) this.markBoundsDirty(); if (debugskinnedmesh) { for (const mesh of this.sharedMeshes) { if (mesh instanceof SkinnedMesh && mesh.boundingSphere) { const tempCenter = getTempVector(mesh.boundingSphere.center).applyMatrix4(mesh.matrixWorld); Gizmos.DrawWireSphere(tempCenter, mesh.boundingSphere.radius, "red"); } } } } markBoundsDirty() { this._needUpdateBoundingSphere = true; } } export enum ShadowCastingMode { /// /// No shadows are cast from this object. /// Off, /// /// Shadows are cast from this object. /// On, /// /// Shadows are cast from this object, treating it as two-sided. /// TwoSided, /// /// Object casts shadows, but is otherwise invisible in the Scene. /// ShadowsOnly, }