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.

297 lines (260 loc) • 13.5 kB
import { Box3, CubeTexture, CubeUVReflectionMapping, EquirectangularReflectionMapping, LinearSRGBColorSpace, LineSegments, Material, Object3D, Quaternion, Texture, Vector3 } from "three"; import { Gizmos } from "../engine/engine_gizmos.js"; import { MaterialPropertyBlock } from "../engine/engine_materialpropertyblock.js"; import { loadPMREM } from "../engine/engine_pmrem.js"; import { serializable } from "../engine/engine_serialization.js"; import { Context } from "../engine/engine_setup.js"; import { getBoundingBox, getWorldPosition } from "../engine/engine_three_utils.js"; import { getParam, resolveUrl } from "../engine/engine_utils.js"; import { Behaviour } from "./Component.js"; import { EventList } from "./EventList.js"; export const debug = getParam("debugreflectionprobe"); const disable = getParam("noreflectionprobe"); let box: Box3 | null = null; const _identityRotation = new Quaternion(); const $reflectionProbeKey = Symbol("reflectionProbeKey"); /** * The [ReflectionProbe](https://engine.needle.tools/docs/api/ReflectionProbe) provides environment reflection data to materials within its defined area. * Use for chrome-like materials that need accurate environment reflections. * * **Setup:** * 1. Add ReflectionProbe component to an object * 2. Assign a cubemap or HDR texture * 3. In Renderer components, assign the probe as anchor override * * **Note:** Volume-based automatic assignment is not fully supported yet. * Objects (Renderer components) can explicitly reference their reflection probe. * * **Debug options:** * - `?debugreflectionprobe` - Log probe info * - `?noreflectionprobe` - Disable all reflection probes * * - Example: https://engine.needle.tools/samples/reflection-probes * * @summary Provides reflection data to materials * @category Rendering * @group Components * @see {@link Renderer} for material and rendering control * @see {@link Light} for scene lighting */ export class ReflectionProbe extends Behaviour { private static _probes: Map<Context, ReflectionProbe[]> = new Map(); private static testBox: Box3 = new Box3(); /** * Checks if the given material is currently using a reflection probe. * This is determined by checking for an override on the material's "envMap" property, which is set by the Renderer component when applying a reflection probe. */ static isUsingReflectionProbe(material: Material) { return !!(material as any)[$reflectionProbeKey]; } /** * Event invoked when a reflection probe is enabled. Used internally by Renderer components to update probes when they become active. Not recommended to call this directly in most cases. * @see {@link onDisabled} for the corresponding disable event. */ static readonly onEnabled: EventList<ReflectionProbe> = new EventList(); /** * Event invoked when a reflection probe is disabled. Used internally by Renderer components to update probes when they become inactive. Not recommended to call this directly in most cases. * @see {@link onEnabled} for the corresponding enable event. */ static readonly onDisabled: EventList<ReflectionProbe> = new EventList(); /** * Gets the active reflection probe for the given object and context. If `isAnchor` is true, it will only return a probe if the object is the anchor of that probe. Otherwise, it checks if the object is within the probe's influence area. * * Note: This method is used internally by the Renderer component to determine which reflection probe to apply. It is not recommended to call this method directly in most cases. Instead, assign probes to renderers using the "anchor" property or rely on automatic assignment when supported. * Note: Volume-based automatic assignment is not fully supported yet, so explicit assignment is recommended for now. * * @param object The object to find a reflection probe for * @param context The context to search within * @param isAnchor If true, only return a probe if the object is the anchor of that probe * @param anchor Optional anchor object to match against probes */ 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.activeAndEnabled) { if (anchor) { if (probe.gameObject === anchor) { return probe; } } 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 | null; private _textureUrlInFlight?: string; /** * The cubemap or HDR texture used for reflections. Can be assigned directly or via a URL string. * When assigning via URL, the texture will be loaded asynchronously and applied once ready. * @param tex The texture or URL to assign to this reflection probe * @default null */ @serializable([Texture, String]) set texture(tex: Texture | null) { if (this._texture === tex) return; if (typeof tex === "string") { if (debug) console.debug(`[ReflectionProbe] Loading reflection probe texture from URL: ${tex}`); this._textureUrlInFlight = tex; const textureUrl = resolveUrl(this.sourceId, tex); loadPMREM(textureUrl, this.context.renderer).then(loaded => { if (this._textureUrlInFlight === tex && loaded) { this._textureUrlInFlight = undefined; if (debug) console.debug(`[ReflectionProbe] Successfully loaded reflection probe texture: ${tex}`); this.texture = loaded; } }); return; } // During deserialization / loading of the GLB the deserializer sets an empty DataTexture // But if the texture is serialized as a string (via Blender Reflection Probes) then the async loading above // Will abort IF textureInFlight is reset (here). That's why we do NOT reset textureInFlight during initialization (when __didAwake is false). Only after awake, when we are sure that the async loading is done, we reset the in-flight URL to allow new assignments. if (this.__didAwake) this._textureUrlInFlight = undefined; this._texture = tex; if (debug) console.debug("[ReflectionProbe] Set reflection probe texture " + (tex?.name || "(removed)")); if (tex) { if (tex instanceof CubeTexture) { } else if (tex.mapping === CubeUVReflectionMapping) { // Already in correct format, do nothing } else if (tex.mapping !== EquirectangularReflectionMapping) { tex.mapping = EquirectangularReflectionMapping; } tex.colorSpace = LinearSRGBColorSpace; tex.needsUpdate = true; } } get texture(): Texture | null { return this._texture; } /** * The intensity of the reflection probe's influence. * Higher values will make reflections brighter, while lower values will make them dimmer. * The default value is 1, which means the reflections will be applied at their original brightness. Adjust this value to achieve the desired look for your scene. * @default 1 */ @serializable() intensity: number = 1; /** * Defines the center of the reflection probe's influence area relative to the GameObject's position. * The probe will affect objects within a box defined by this center and the `size` property. * Note: The actual influence area is determined by both the `center` and `size` properties. The `center` defines the offset from the GameObject's position, while the `size` defines the dimensions of the box around that center. Objects within this box will be influenced by the reflection probe. */ @serializable(Vector3) center: Vector3 = new Vector3(); /** * Defines the size of the reflection probe's influence area. Objects within this box will be affected by the probe's reflections. * Note: The actual influence area is determined by both the `center` and `size` properties. The `center` defines the offset from the GameObject's position, while the `size` defines the dimensions of the box around that center. Objects within this box will be influenced by the reflection probe. */ @serializable(Vector3) size: Vector3 = new Vector3(1, 1, 1); /** * Workaround for lightmap. Compensates for the fact that lightmaps are pre-multiplied by intensity, while reflection probes are not. This means that if you use both lightmaps and reflection probes, you may need to adjust this value to get the correct balance between them. The default value of `Math.PI` is a good starting point for most cases, but you may need to tweak it based on your specific lighting setup and artistic needs. */ __lightmapIntensityScale: boolean = true; private isInBox(obj: Object3D) { box ??= new Box3(); box!.setFromCenterAndSize(this.gameObject.worldPosition.add(this.center), this.size); getBoundingBox([obj], undefined, undefined, ReflectionProbe.testBox); if (ReflectionProbe.testBox.isEmpty()) { return box.containsPoint(obj.worldPosition); } else { return box?.intersectsBox(ReflectionProbe.testBox); } } constructor() { super(); if (!ReflectionProbe._probes.has(this.context)) { ReflectionProbe._probes.set(this.context, []); } ReflectionProbe._probes.get(this.context)?.push(this); } /** @internal */ awake() { if (this._texture) { if (this._texture.mapping !== CubeUVReflectionMapping) this._texture.mapping = EquirectangularReflectionMapping; this._texture.colorSpace = LinearSRGBColorSpace; this._texture.needsUpdate = true; } } /** @internal */ update(): void { if (debug) { box ??= new Box3(); box!.setFromCenterAndSize(this.gameObject.worldPosition.add(this.center), this.size); Gizmos.DrawWireBox3(box, 0x555500); } } /** @internal */ onEnable(): void { ReflectionProbe.onEnabled?.invoke(this); } /** @internal */ onDisable(): void { ReflectionProbe.onDisabled?.invoke(this); } /** @internal */ start(): void { if (!this._texture) { console.warn(`[ReflectionProbe] Missing texture. Please assign a custom cubemap texture. To use reflection probes assign them to your renderer's "anchor" property.`); } } /** @internal */ onDestroy() { const probes = ReflectionProbe._probes.get(this.context); if (probes) { const index = probes.indexOf(this); if (index >= 0) { probes.splice(index, 1); } } } /** * Applies this reflection probe to the given object by setting material property overrides for "envMap", "envMapRotation", and "envMapIntensity". * This is typically called by the Renderer component when determining which reflection probe to use for a given object. * @param object The object to apply the reflection probe to * @see {@link unapply} for the corresponding method to remove the probe's influence from an object. */ apply(object: Object3D) { if (disable) return; if (!this.enabled) return; if (!this.texture) return; const propertyBlock = MaterialPropertyBlock.get(object); propertyBlock.setOverride("envMap", this.texture); propertyBlock.setOverride("envMapRotation", this.gameObject.rotation); let intensity = this.intensity; if (this.__lightmapIntensityScale && propertyBlock.getOverride("lightMap")) { // @TODO: Remove this here and in Renderer https://linear.app/needle/issue/NE-6922 intensity /= Math.PI; } propertyBlock.setOverride("envMapIntensity", intensity); } /** * Removes the reflection probe overrides from the given object. * This is typically called by the Renderer component when an object is no longer influenced by this probe or when the probe is disabled. * @param object The object to remove the reflection probe overrides from * @see {@link apply} for the corresponding method to apply the probe's influence to an object. */ unapply(obj: Object3D) { const block = MaterialPropertyBlock.get(obj); if (block) { const current = block.getOverride("envMap")?.value; if (current === this.texture) { block.removeOveride("envMap"); } } } }