import { EquirectangularReflectionMapping, LinearSRGBColorSpace, Material, MeshBasicMaterial, Object3D, SRGBColorSpace, Texture, Vector3 } from "three"; import { isDevEnvironment, showBalloonWarning } from "../engine/debug/index.js"; import { serializable } from "../engine/engine_serialization.js"; import { Context } from "../engine/engine_setup.js"; import type { IRenderer } from "../engine/engine_types.js"; import { getParam } from "../engine/engine_utils.js"; import { BoxHelperComponent } from "./BoxHelperComponent.js"; import { Behaviour } from "./Component.js"; export const debug = getParam("debugreflectionprobe"); const disable = getParam("noreflectionprobe"); const $reflectionProbeKey = Symbol("reflectionProbeKey"); const $originalMaterial = Symbol("original material"); /** * @category Rendering * @group Components */ export class ReflectionProbe extends Behaviour { private static _probes: Map = new Map(); public static get(object: Object3D | null | undefined, context: Context, isAnchor: boolean, anchor?: Object3D): ReflectionProbe | null { if (!object || object.isObject3D !== true) return null; if (disable) return null; const probes = ReflectionProbe._probes.get(context); if (probes) { for (const probe of probes) { if (!probe.__didAwake) probe.__internalAwake(); if (probe.enabled) { if (anchor) { // test if anchor is reflection probe object if (probe.gameObject === anchor) { return probe; } } // TODO not supported right now, as we'd have to pass the ReflectionProbe scale through as well. else if (probe.isInBox(object)) { if (debug) console.log("Found reflection probe", object.name, probe.name); return probe; } } } } if (debug) console.debug("Did not find reflection probe", object.name, isAnchor, object); return null; } private _texture!: Texture; // @serializable(Texture) set texture(tex: Texture) { if (tex && !(tex instanceof Texture)) { console.error("ReflectionProbe.texture must be a Texture", tex); return; } this._texture = tex; if (tex) { tex.mapping = EquirectangularReflectionMapping; tex.colorSpace = LinearSRGBColorSpace; tex.needsUpdate = true; } } get texture(): Texture { return this._texture; } @serializable(Vector3) center?: Vector3; @serializable(Vector3) size?: Vector3; private _boxHelper?: BoxHelperComponent; private isInBox(obj: Object3D) { return this._boxHelper?.isInBox(obj); } constructor() { super(); if (!ReflectionProbe._probes.has(this.context)) { ReflectionProbe._probes.set(this.context, []); } ReflectionProbe._probes.get(this.context)?.push(this); } awake() { this._boxHelper = this.gameObject.addComponent(BoxHelperComponent) as BoxHelperComponent; this._boxHelper.updateBox(true); if (debug) this._boxHelper.showHelper(0x555500, true); if (this._texture) { this._texture.mapping = EquirectangularReflectionMapping; this._texture.colorSpace = LinearSRGBColorSpace; this._texture.needsUpdate = true; } } start(): void { if (!this._texture && isDevEnvironment()) { console.warn(`[ReflectionProbe] Missing texture. Please assign a custom cubemap texture. To use reflection probes assign them to your renderer's "anchor" property.`); showBalloonWarning("ReflectionProbe configuration hint: See browser console for details") } } onDestroy() { const probes = ReflectionProbe._probes.get(this.context); if (probes) { const index = probes.indexOf(this); if (index >= 0) { probes.splice(index, 1); } } } // when objects are rendered and they share material // and some need reflection probe and some don't // we need to make sure we don't override the material but use a copy private static _rendererMaterialsCache: Map> = new Map(); onSet(_rend: IRenderer) { if (disable) return; if (!this.enabled) return; if (_rend.sharedMaterials?.length <= 0) return; if (!this.texture) return; let rendererCache = ReflectionProbe._rendererMaterialsCache.get(_rend); if (!rendererCache) { rendererCache = []; ReflectionProbe._rendererMaterialsCache.set(_rend, rendererCache); } // TODO: dont clone material for every renderer that uses reflection probes, we can do it once per material when they use the same reflection texture // need to make sure materials are not shared when using reflection probes // otherwise some renderers outside of the probe will be affected or vice versa for (let i = 0; i < _rend.sharedMaterials.length; i++) { const material = _rend.sharedMaterials[i]; if (!material) { continue; } if (material["envMap"] === undefined) { continue; } if (material instanceof MeshBasicMaterial) { continue; } let cached = rendererCache[i]; // make sure we have the currently assigned material cached (and an up to date clone of that) // TODO: this is causing problems with progressive textures sometimes (depending on the order) when the version changes and we re-create materials over and over. We might want to just set the material envmap instead of making a clone const isCachedInstance = material === cached?.copy; const hasChanged = !cached || cached.material.uuid !== material.uuid || cached.copy.version !== material.version; if (!isCachedInstance && hasChanged) { if (debug) { let reason = ""; if (!cached) reason = "not cached"; else if (cached.material !== material) reason = "reference changed; cached instance?: " + isCachedInstance; else if (cached.copy.version !== material.version) reason = "version changed"; console.warn("Cloning material", material.name, material.version, "Reason:", reason, "\n", material.uuid, "\n", cached?.copy.uuid, "\n", _rend.name); } const clone = material.clone(); clone.version = material.version; if (cached) { cached.copy = clone; cached.material = material; } else { cached = { material: material, copy: clone }; rendererCache.push(cached); } clone[$reflectionProbeKey] = this; clone[$originalMaterial] = material; if (debug) console.log("Set reflection", _rend.name, _rend.guid); } // See NE-4771 and NE-4856 if (cached && cached.copy) { cached.copy.onBeforeCompile = material.onBeforeCompile; } /** this is the material that we copied and that has the reflection probe */ const copy = cached?.copy; // make sure the reflection probe is assigned copy["envMap"] = this.texture; _rend.sharedMaterials[i] = copy; } } onUnset(_rend: IRenderer) { const rendererCache = ReflectionProbe._rendererMaterialsCache.get(_rend); if (rendererCache) { for (let i = 0; i < rendererCache.length; i++) { const cached = rendererCache[i]; _rend.sharedMaterials[i] = cached.material; } } } }