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.

201 lines (170 loc) • 7.79 kB
import { NEEDLE_progressive } from "@needle-tools/gltf-progressive"; import { Group, Mesh, Object3D, ShaderMaterial, Texture, Vector2, Vector4 } from "three"; import { MaterialPropertyBlock } from "../engine/engine_materialpropertyblock.js"; import type { Context } from "../engine/engine_setup.js"; import { getParam } from "../engine/engine_utils.js"; import { type Renderer } from "./Renderer.js"; const debug = getParam("debuglightmaps"); const $lightmapKey = Symbol("lightmapKey"); /** * This component is automatically added by the {@link Renderer} component if the object has lightmap uvs AND we have a lightmap. * * @category Rendering * @group Components */ export class RendererLightmap { get lightmap(): Texture | null { return this.lightmapTexture; } set lightmap(tex: Texture | null) { if (tex !== this.lightmapTexture) { this.lightmapTexture = tex; this.applyLightmap(); this.updatePropertyBlockTexture(); if (this.lightmapTexture) { NEEDLE_progressive.assignTextureLOD(this.lightmapTexture, 0).then((res: unknown) => { if ((res as Texture)?.isTexture) { this.lightmapTexture = res as Texture; this.updatePropertyBlockTexture(); } }) } } } private lightmapIndex: number = -1; private lightmapScaleOffset: Vector4 = new Vector4(1, 1, 0, 0); private readonly renderer: Renderer; private _isApplied: boolean = false; private get context(): Context { return this.renderer.context; } private get gameObject() { return this.renderer.gameObject; } private lightmapTexture: Texture | null = null; constructor(renderer: Renderer) { this.renderer = renderer; } init(lightmapIndex: number, lightmapScaleOffset: Vector4, lightmapTexture: Texture) { console.assert(this.gameObject !== undefined && this.gameObject !== null, "Missing gameobject", this); this.lightmapIndex = lightmapIndex; if (this.lightmapIndex < 0) return; this.lightmapScaleOffset = lightmapScaleOffset; this.lightmapTexture = lightmapTexture; NEEDLE_progressive.assignTextureLOD(lightmapTexture, 0).then((res: unknown) => { if ((res as Texture)?.isTexture) { this.lightmapTexture = res as Texture; this.updatePropertyBlockTexture(); } }) if (debug == "show") { console.log("Lightmap:", this.gameObject.name, lightmapIndex, "\nScaleOffset:", lightmapScaleOffset, "\nTexture:", lightmapTexture) this.setLightmapDebugMaterial(); } else if (debug) console.log("Use debuglightmaps=show to render lightmaps only in the scene.") this.applyLightmap(); } updateLightmapUniforms(_material: any) { } /** * Apply the lightmap to the object using MaterialPropertyBlock instead of cloning materials. * The lightmap texture and its per-object UV transform are set as overrides via PropertyBlock. * Three.js reads material.lightMap to determine shader defines and upload uniforms, * and uses texture.offset/repeat to compute lightMapTransform in the vertex shader. */ applyLightmap() { if (this._isApplied) return; if (this.gameObject.type === "Object3D") { if (debug) console.warn("Can not add lightmap. Is this object missing a renderer?", this.gameObject.name); return; } const mesh = this.gameObject as unknown as (Mesh | Group); this.ensureLightmapUvs(mesh); if (this.lightmapIndex >= 0 && this.lightmapTexture) { this.lightmapTexture.channel = 1; const so = this.lightmapScaleOffset; for (let i = 0; i < this.renderer.sharedMaterials.length; i++) { const mat = this.renderer.sharedMaterials[i]; if (!mat) continue; // If the material doesn't support lightmaps, skip it if (mat["lightMap"] === undefined) continue; if (debug) console.log("Setting lightmap on material", mat.name, "for renderer", this.renderer.name); // Use property block to set lightMap with per-object UV transform. // The texture transform differentiates cache keys per object and // PropertyBlock handles save/restore of the shared texture's offset/repeat. const propertyBlock = MaterialPropertyBlock.get(this.gameObject); propertyBlock.setOverride("lightMap", this.lightmapTexture, { offset: new Vector2(so.z, 1 - so.y - so.w), repeat: new Vector2(so.x, so.y) }); (mat as any)[$lightmapKey] = true; } this._isApplied = true; } } /** Update the lightMap override on all property blocks (e.g. after LOD swap) */ private updatePropertyBlockTexture() { if (!this._isApplied || !this.lightmapTexture) return; this.lightmapTexture.channel = 1; const so = this.lightmapScaleOffset; const propertyBlock = MaterialPropertyBlock.get(this.gameObject); propertyBlock.setOverride("lightMap", this.lightmapTexture, { offset: new Vector2(so.z, 1 - so.y - so.w), repeat: new Vector2(so.x, so.y) }); } /** * Remove the lightmap from the object */ onUnset() { for (let i = 0; i < this.renderer.sharedMaterials.length; i++) { const mat = this.renderer.sharedMaterials[i]; if (mat) { delete (mat as any)[$lightmapKey]; } } const block = MaterialPropertyBlock.get(this.gameObject); if (block) { block.removeOveride("lightMap"); } } private ensureLightmapUvs(object: Object3D | Group | Mesh) { if (object instanceof Mesh) { if (!object.geometry.getAttribute("uv1")) { object.geometry.setAttribute("uv1", object.geometry.getAttribute("uv")); } } else if (object instanceof Group) { for (const child of object.children) { this.ensureLightmapUvs(child); } } } private setLightmapDebugMaterial() { const so = this.lightmapScaleOffset; // debug lightmaps this.gameObject["material"] = new ShaderMaterial({ vertexShader: ` varying vec2 vUv1; void main() { vUv1 = uv1; gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); } `, fragmentShader: ` uniform sampler2D lightMap; uniform float lightMapIntensity; varying vec2 vUv1; // took from threejs 05fc79cd52b79e8c3e8dec1e7dca72c5c39983a4 vec4 conv_sRGBToLinear( in vec4 value ) { return vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.a ); } void main() { vec2 lUv = vUv1.xy * vec2(${so.x.toFixed(6)}, ${so.y.toFixed(6)}) + vec2(${so.z.toFixed(6)}, ${(1 - so.y - so.w).toFixed(6)}); vec4 lightMapTexel = texture2D( lightMap, lUv); gl_FragColor = lightMapTexel; gl_FragColor.a = 1.; } `, defines: { USE_LIGHTMAP: '' } }); } }