@needle-tools/engine 4.13.1 → 4.14.0-next.31f837e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components.needle.json +1 -1
- package/dist/generateMeshBVH.worker-DFcS3P04.js +21 -0
- package/dist/gltf-progressive-8voIgNp_.js +1528 -0
- package/dist/gltf-progressive-BRRBj-nY.umd.cjs +10 -0
- package/dist/gltf-progressive-Dkh3tG4-.min.js +10 -0
- package/dist/loader.worker-C6cXDgR1.js +23 -0
- package/dist/{materialx-BF23AVE8.umd.cjs → materialx-CxlgposR.umd.cjs} +1 -1
- package/dist/{materialx-fkoFuRh3.js → materialx-D66rYPqe.js} +2 -2
- package/dist/{materialx-B9ddsHcF.min.js → materialx-Dx8st96L.min.js} +1 -1
- package/dist/{needle-engine.bundle-tjI5Fq2c.umd.cjs → needle-engine.bundle-BQXG5qbQ.umd.cjs} +138 -145
- package/dist/{needle-engine.bundle-DauZUYl7.js → needle-engine.bundle-Byl5i6zJ.js} +6403 -6164
- package/dist/needle-engine.bundle-D7w0XD7M.min.js +1646 -0
- package/dist/needle-engine.d.ts +251 -23
- package/dist/needle-engine.js +416 -415
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/dist/{postprocessing-DdM-tz1j.js → postprocessing-BkSpxpYB.js} +2 -2
- package/dist/{postprocessing-BVNrgYZK.min.js → postprocessing-Ce5-UWiA.min.js} +1 -1
- package/dist/{postprocessing-CI2TjWpu.umd.cjs → postprocessing-DFVElmAh.umd.cjs} +1 -1
- package/dist/{three-BW2s1Yl-.umd.cjs → three-Bad8p1pf.umd.cjs} +46 -46
- package/dist/{three-I__hSXzr.min.js → three-CWn13_u1.min.js} +33 -33
- package/dist/{three-VvRoMeIN.js → three-DFV1-P9z.js} +4209 -4209
- package/dist/{three-examples-BhfOE7NG.js → three-examples-43yqn3mL.js} +1 -1
- package/dist/{three-examples-Bpfu6ke_.umd.cjs → three-examples-CO-tx3Sp.umd.cjs} +1 -1
- package/dist/{three-examples-D8zAE_7t.min.js → three-examples-DKuJVGT4.min.js} +1 -1
- package/dist/{three-mesh-ui-BU55xDxJ.umd.cjs → three-mesh-ui-ChzVOraf.umd.cjs} +1 -1
- package/dist/{three-mesh-ui-C3QbemOV.min.js → three-mesh-ui-DyEA5HQF.min.js} +1 -1
- package/dist/{three-mesh-ui-CcMp-FQm.js → three-mesh-ui-fSAQJxdI.js} +1 -1
- package/dist/{vendor-COVQl0b8.umd.cjs → vendor-B51YffMU.umd.cjs} +1 -1
- package/dist/{vendor-BiyIZ61v.js → vendor-CgpZ5ivC.js} +1 -1
- package/dist/{vendor-DW7zqjuT.min.js → vendor-pe19S9r5.min.js} +1 -1
- package/lib/engine/api.d.ts +1 -0
- package/lib/engine/api.js +1 -0
- package/lib/engine/api.js.map +1 -1
- package/lib/engine/engine_context.js +12 -2
- package/lib/engine/engine_context.js.map +1 -1
- package/lib/engine/engine_lightdata.js +8 -6
- package/lib/engine/engine_lightdata.js.map +1 -1
- package/lib/engine/engine_materialpropertyblock.d.ts +47 -0
- package/lib/engine/engine_materialpropertyblock.js +412 -0
- package/lib/engine/engine_materialpropertyblock.js.map +1 -0
- package/lib/engine/engine_utils.d.ts +1 -0
- package/lib/engine/engine_utils.js +1 -0
- package/lib/engine/engine_utils.js.map +1 -1
- package/lib/engine/engine_utils_screenshot.d.ts +171 -14
- package/lib/engine/engine_utils_screenshot.js +65 -0
- package/lib/engine/engine_utils_screenshot.js.map +1 -1
- package/lib/engine/engine_utils_screenshot.xr.d.ts +1 -1
- package/lib/engine/engine_utils_screenshot.xr.js +1 -1
- package/lib/engine/extensions/NEEDLE_techniques_webgl.js +3 -0
- package/lib/engine/extensions/NEEDLE_techniques_webgl.js.map +1 -1
- package/lib/engine/xr/NeedleXRSession.d.ts +5 -0
- package/lib/engine/xr/NeedleXRSession.js +5 -0
- package/lib/engine/xr/NeedleXRSession.js.map +1 -1
- package/lib/engine-components/ReflectionProbe.d.ts +0 -1
- package/lib/engine-components/ReflectionProbe.js +15 -76
- package/lib/engine-components/ReflectionProbe.js.map +1 -1
- package/lib/engine-components/RendererLightmap.d.ts +13 -9
- package/lib/engine-components/RendererLightmap.js +68 -81
- package/lib/engine-components/RendererLightmap.js.map +1 -1
- package/lib/engine-components/SeeThrough.js.map +1 -1
- package/lib/engine-components/VideoPlayer.js +6 -0
- package/lib/engine-components/VideoPlayer.js.map +1 -1
- package/lib/engine-components/utils/OpenURL.d.ts +1 -0
- package/lib/engine-components/utils/OpenURL.js +1 -0
- package/lib/engine-components/utils/OpenURL.js.map +1 -1
- package/lib/engine-components/web/CursorFollow.d.ts +1 -0
- package/lib/engine-components/web/CursorFollow.js +1 -0
- package/lib/engine-components/web/CursorFollow.js.map +1 -1
- package/lib/engine-components/web/ScrollFollow.d.ts +1 -0
- package/lib/engine-components/web/ScrollFollow.js +1 -0
- package/lib/engine-components/web/ScrollFollow.js.map +1 -1
- package/lib/engine-components/webxr/WebARCameraBackground.d.ts +9 -0
- package/lib/engine-components/webxr/WebARCameraBackground.js +9 -0
- package/lib/engine-components/webxr/WebARCameraBackground.js.map +1 -1
- package/lib/engine-components/webxr/WebXR.d.ts +1 -0
- package/lib/engine-components/webxr/WebXR.js +1 -0
- package/lib/engine-components/webxr/WebXR.js.map +1 -1
- package/package.json +4 -4
- package/plugins/vite/build-pipeline.js +16 -2
- package/src/engine/api.ts +1 -0
- package/src/engine/engine_context.ts +17 -3
- package/src/engine/engine_lightdata.ts +8 -6
- package/src/engine/engine_materialpropertyblock.ts +500 -0
- package/src/engine/engine_utils.ts +1 -0
- package/src/engine/engine_utils_screenshot.ts +241 -17
- package/src/engine/engine_utils_screenshot.xr.ts +1 -1
- package/src/engine/extensions/NEEDLE_techniques_webgl.ts +3 -0
- package/src/engine/xr/NeedleXRSession.ts +5 -0
- package/src/engine-components/ContactShadows.ts +1 -1
- package/src/engine-components/ReflectionProbe.ts +17 -89
- package/src/engine-components/RendererLightmap.ts +76 -87
- package/src/engine-components/SeeThrough.ts +2 -2
- package/src/engine-components/VideoPlayer.ts +6 -0
- package/src/engine-components/utils/OpenURL.ts +1 -0
- package/src/engine-components/web/CursorFollow.ts +1 -0
- package/src/engine-components/web/ScrollFollow.ts +1 -0
- package/src/engine-components/webxr/WebARCameraBackground.ts +12 -3
- package/src/engine-components/webxr/WebXR.ts +1 -0
- package/dist/generateMeshBVH.worker-iyfPIK6R.js +0 -21
- package/dist/gltf-progressive-BURrJW0U.umd.cjs +0 -8
- package/dist/gltf-progressive-DHLDFNvQ.min.js +0 -8
- package/dist/gltf-progressive-eiJCrjLb.js +0 -1400
- package/dist/loader.worker-C1GG9A7C.js +0 -23
- package/dist/needle-engine.bundle-BNIUpreS.min.js +0 -1653
- package/src/include/three/DragControls.js +0 -232
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import { Material, Object3D, Color, Texture, Vector2, Vector3, Vector4, WebGLRenderer, Scene, Camera, BufferGeometry, Group } from "three";
|
|
2
|
+
|
|
3
|
+
type MaterialPropertyType = number | number[] | Color | Texture | Vector2 | Vector3 | Vector4 | null;
|
|
4
|
+
|
|
5
|
+
export interface TextureTransform {
|
|
6
|
+
offset?: Vector2;
|
|
7
|
+
repeat?: Vector2;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PropertyBlockOverride {
|
|
11
|
+
name: string;
|
|
12
|
+
value: MaterialPropertyType;
|
|
13
|
+
textureTransform?: TextureTransform;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type NonFunctionPropertyNames<T> = {
|
|
17
|
+
[K in keyof T]: T[K] extends Function ? never : K
|
|
18
|
+
}[keyof T];
|
|
19
|
+
|
|
20
|
+
// Centralized registry for all property block related data
|
|
21
|
+
class PropertyBlockRegistry {
|
|
22
|
+
// Map from object to its property block
|
|
23
|
+
private objectToBlock = new WeakMap<Object3D, MaterialPropertyBlock>();
|
|
24
|
+
|
|
25
|
+
// Track which materials belong to which property block (to prevent applying to wrong materials)
|
|
26
|
+
// Use WeakSet for automatic cleanup when materials are garbage collected
|
|
27
|
+
private objectToMaterials = new WeakMap<Object3D, WeakSet<Material>>();
|
|
28
|
+
|
|
29
|
+
// Track which meshes have callbacks for which property block owners
|
|
30
|
+
private meshToOwners = new WeakMap<Object3D, Set<Object3D>>();
|
|
31
|
+
|
|
32
|
+
// Track original callback functions for cleanup (reserved for future use)
|
|
33
|
+
private meshToOriginalCallbacks = new WeakMap<Object3D, {
|
|
34
|
+
onBeforeRender?: ObjectRenderCallback;
|
|
35
|
+
onAfterRender?: ObjectRenderCallback;
|
|
36
|
+
}>();
|
|
37
|
+
|
|
38
|
+
getBlock(object: Object3D): MaterialPropertyBlock | undefined {
|
|
39
|
+
return this.objectToBlock.get(object);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setBlock(object: Object3D, block: MaterialPropertyBlock): void {
|
|
43
|
+
this.objectToBlock.set(object, block);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
deleteBlock(object: Object3D): void {
|
|
47
|
+
this.objectToBlock.delete(object);
|
|
48
|
+
this.objectToMaterials.delete(object);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
addMaterial(object: Object3D, material: Material): void {
|
|
52
|
+
let materials = this.objectToMaterials.get(object);
|
|
53
|
+
if (!materials) {
|
|
54
|
+
materials = new WeakSet();
|
|
55
|
+
this.objectToMaterials.set(object, materials);
|
|
56
|
+
}
|
|
57
|
+
materials.add(material);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
hasMaterial(object: Object3D, material: Material): boolean {
|
|
61
|
+
return this.objectToMaterials.get(object)?.has(material) ?? false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
isHooked(mesh: Object3D, owner: Object3D): boolean {
|
|
65
|
+
return this.meshToOwners.get(mesh)?.has(owner) ?? false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
addHook(mesh: Object3D, owner: Object3D): void {
|
|
69
|
+
let owners = this.meshToOwners.get(mesh);
|
|
70
|
+
if (!owners) {
|
|
71
|
+
owners = new Set();
|
|
72
|
+
this.meshToOwners.set(mesh, owners);
|
|
73
|
+
}
|
|
74
|
+
owners.add(owner);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
removeHook(mesh: Object3D, owner: Object3D): void {
|
|
78
|
+
const owners = this.meshToOwners.get(mesh);
|
|
79
|
+
if (owners) {
|
|
80
|
+
owners.delete(owner);
|
|
81
|
+
if (owners.size === 0) {
|
|
82
|
+
this.meshToOwners.delete(mesh);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getOriginalCallbacks(mesh: Object3D): { onBeforeRender?: ObjectRenderCallback; onAfterRender?: ObjectRenderCallback } | undefined {
|
|
88
|
+
return this.meshToOriginalCallbacks.get(mesh);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
setOriginalCallbacks(mesh: Object3D, callbacks: { onBeforeRender?: ObjectRenderCallback; onAfterRender?: ObjectRenderCallback }): void {
|
|
92
|
+
this.meshToOriginalCallbacks.set(mesh, callbacks);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const registry = new PropertyBlockRegistry();
|
|
97
|
+
|
|
98
|
+
export class MaterialPropertyBlock<T extends Material = Material> {
|
|
99
|
+
private _overrides: PropertyBlockOverride[] = [];
|
|
100
|
+
private _defines: Record<string, string | number | boolean> = {};
|
|
101
|
+
private _object: Object3D | null = null;
|
|
102
|
+
|
|
103
|
+
get object(): Object3D | null { return this._object; }
|
|
104
|
+
|
|
105
|
+
constructor(object: Object3D | null = null) {
|
|
106
|
+
this._object = object;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
static get<T extends Material = Material>(object: Object3D): MaterialPropertyBlock<T> {
|
|
110
|
+
let block = registry.getBlock(object);
|
|
111
|
+
if (!block) {
|
|
112
|
+
block = new MaterialPropertyBlock(object);
|
|
113
|
+
registry.setBlock(object, block);
|
|
114
|
+
attachPropertyBlockToObject(object, block);
|
|
115
|
+
}
|
|
116
|
+
return block as MaterialPropertyBlock<T>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
dispose() {
|
|
120
|
+
if (this._object) {
|
|
121
|
+
registry.deleteBlock(this._object);
|
|
122
|
+
// TODO: Add cleanup for hooked meshes
|
|
123
|
+
}
|
|
124
|
+
this._overrides = [];
|
|
125
|
+
this._object = null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
setOverride<K extends NonFunctionPropertyNames<T>>(name: K, value: T[K], textureTransform?: TextureTransform): void;
|
|
129
|
+
setOverride(name: string, value: MaterialPropertyType, textureTransform?: TextureTransform): void;
|
|
130
|
+
setOverride(name: string, value: MaterialPropertyType, textureTransform?: TextureTransform): void {
|
|
131
|
+
const existing = this._overrides.find(o => o.name === name);
|
|
132
|
+
if (existing) {
|
|
133
|
+
existing.value = value;
|
|
134
|
+
existing.textureTransform = textureTransform;
|
|
135
|
+
} else {
|
|
136
|
+
this._overrides.push({ name, value, textureTransform });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
getOverride(name: string): PropertyBlockOverride | undefined {
|
|
141
|
+
return this._overrides.find(o => o.name === name);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
clearOverride(name: string): void {
|
|
145
|
+
const index = this._overrides.findIndex(o => o.name === name);
|
|
146
|
+
if (index >= 0) {
|
|
147
|
+
this._overrides.splice(index, 1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
clearAllOverrides(): void {
|
|
152
|
+
this._overrides = [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
removeOverride(name: string): void {
|
|
156
|
+
this.clearOverride(name);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
get overrides(): PropertyBlockOverride[] {
|
|
160
|
+
return this._overrides;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
hasOverrides(): boolean {
|
|
164
|
+
return this._overrides.length > 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Set a shader define that will be included in the program cache key
|
|
169
|
+
* This allows different objects sharing the same material to have different shader programs
|
|
170
|
+
*/
|
|
171
|
+
setDefine(name: string, value: string | number | boolean): void {
|
|
172
|
+
this._defines[name] = value;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Remove a shader define
|
|
177
|
+
*/
|
|
178
|
+
clearDefine(name: string): void {
|
|
179
|
+
this._defines[name] = undefined as any;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get all defines set on this property block
|
|
184
|
+
*/
|
|
185
|
+
getDefines(): Readonly<Record<string, string | number | boolean>> {
|
|
186
|
+
return this._defines;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
getCacheKey(): string {
|
|
190
|
+
const parts: string[] = [];
|
|
191
|
+
|
|
192
|
+
// Add defines to cache key
|
|
193
|
+
const defineKeys = Object.keys(this._defines).sort();
|
|
194
|
+
for (const key of defineKeys) {
|
|
195
|
+
const value = this._defines[key];
|
|
196
|
+
if (value !== undefined) {
|
|
197
|
+
parts.push(`d:${key}=${value}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Add overrides to cache key
|
|
202
|
+
for (const o of this._overrides) {
|
|
203
|
+
if (o.value === null) continue;
|
|
204
|
+
let val = "";
|
|
205
|
+
if (o.value instanceof Texture) {
|
|
206
|
+
val = (o.value as any).uuid || "texture";
|
|
207
|
+
if (o.textureTransform) {
|
|
208
|
+
const t = o.textureTransform;
|
|
209
|
+
if (t.offset) val += `;to:${t.offset.x},${t.offset.y}`;
|
|
210
|
+
if (t.repeat) val += `;tr:${t.repeat.x},${t.repeat.y}`;
|
|
211
|
+
}
|
|
212
|
+
} else if (Array.isArray(o.value)) {
|
|
213
|
+
val = o.value.join(",");
|
|
214
|
+
} else if (o.value && typeof o.value === "object" && "r" in o.value) {
|
|
215
|
+
const c = o.value as any;
|
|
216
|
+
val = `${c.r},${c.g},${c.b},${c.a !== undefined ? c.a : ""}`;
|
|
217
|
+
} else if (o.value && typeof o.value === "object" && "x" in o.value) {
|
|
218
|
+
// Vector2, Vector3, Vector4
|
|
219
|
+
const v = o.value as any;
|
|
220
|
+
val = `${v.x},${v.y}${v.z !== undefined ? `,${v.z}` : ""}${v.w !== undefined ? `,${v.w}` : ""}`;
|
|
221
|
+
} else {
|
|
222
|
+
val = String(o.value);
|
|
223
|
+
}
|
|
224
|
+
parts.push(`${o.name}=${val}`);
|
|
225
|
+
}
|
|
226
|
+
return parts.join(";");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const $originalValues = Symbol("originalValues");
|
|
231
|
+
|
|
232
|
+
interface OriginalValue {
|
|
233
|
+
name: string;
|
|
234
|
+
value: unknown;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
interface SavedTextureTransform {
|
|
238
|
+
name: string;
|
|
239
|
+
offsetX: number;
|
|
240
|
+
offsetY: number;
|
|
241
|
+
repeatX: number;
|
|
242
|
+
repeatY: number;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const $savedTextureTransforms = Symbol("savedTextureTransforms");
|
|
246
|
+
|
|
247
|
+
type ObjectRenderCallback = (this: Object3D, renderer: WebGLRenderer, scene: Scene, camera: Camera, geometry: BufferGeometry, material: Material, group: Group) => void;
|
|
248
|
+
|
|
249
|
+
// Collect all materials from an object and its children
|
|
250
|
+
function collectMaterials(object: Object3D, materials: Set<Material>): void {
|
|
251
|
+
const obj = object as Object3D & { material?: Material | Material[] };
|
|
252
|
+
if (obj.material) {
|
|
253
|
+
if (Array.isArray(obj.material)) {
|
|
254
|
+
obj.material.forEach(mat => materials.add(mat));
|
|
255
|
+
} else {
|
|
256
|
+
materials.add(obj.material);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// For Groups, collect materials from children too
|
|
261
|
+
if (object.type === "Group") {
|
|
262
|
+
object.children.forEach(child => collectMaterials(child, materials));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Find property block by checking this object and parent (if parent is a Group)
|
|
267
|
+
// Returns both the block and the owner object
|
|
268
|
+
function findPropertyBlockAndOwner(obj: Object3D): { block: MaterialPropertyBlock; owner: Object3D } | undefined {
|
|
269
|
+
// First check if this object itself has a property block
|
|
270
|
+
let block = registry.getBlock(obj);
|
|
271
|
+
if (block) return { block, owner: obj };
|
|
272
|
+
|
|
273
|
+
// If not, check if parent is a Group and has a property block
|
|
274
|
+
if (obj.parent && obj.parent.type === "Group") {
|
|
275
|
+
block = registry.getBlock(obj.parent);
|
|
276
|
+
if (block) return { block, owner: obj.parent };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// #region OnBeforeRender
|
|
283
|
+
const onBeforeRender_MaterialBlock: ObjectRenderCallback = function (this: Object3D, _renderer: WebGLRenderer, _scene: Scene, _camera: Camera, _geometry: BufferGeometry, material: Material, _group: Group) {
|
|
284
|
+
const result = findPropertyBlockAndOwner(this);
|
|
285
|
+
if (!result) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const { block: propertyBlock, owner } = result;
|
|
290
|
+
|
|
291
|
+
// Only apply if this material was registered with this property block
|
|
292
|
+
if (!registry.hasMaterial(owner, material)) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const overrides = propertyBlock.overrides;
|
|
297
|
+
const mat = material as any;
|
|
298
|
+
|
|
299
|
+
// Apply defines to material - this affects shader compilation
|
|
300
|
+
const defines = propertyBlock.getDefines();
|
|
301
|
+
const defineKeys = Object.keys(defines);
|
|
302
|
+
if (defineKeys.length > 0) {
|
|
303
|
+
if (!mat.defines) mat.defines = {};
|
|
304
|
+
for (const key of defineKeys) {
|
|
305
|
+
const value = defines[key];
|
|
306
|
+
if (value !== undefined) {
|
|
307
|
+
mat.defines[key] = value;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Still set up cache key even if no overrides (defines affect it)
|
|
313
|
+
if (overrides.length === 0 && defineKeys.length === 0) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Defines always affect shader compilation → need program change
|
|
318
|
+
let needsProgramChange = defineKeys.length > 0;
|
|
319
|
+
|
|
320
|
+
if (!mat[$originalValues]) {
|
|
321
|
+
mat[$originalValues] = [];
|
|
322
|
+
}
|
|
323
|
+
const originalValues = mat[$originalValues] as OriginalValue[];
|
|
324
|
+
|
|
325
|
+
for (const override of overrides) {
|
|
326
|
+
if (override.value === null) continue;
|
|
327
|
+
|
|
328
|
+
const currentValue = mat[override.name];
|
|
329
|
+
|
|
330
|
+
const existingOriginal = originalValues.find((o: OriginalValue) => o.name === override.name);
|
|
331
|
+
if (existingOriginal) {
|
|
332
|
+
// Update to current value each frame so animations/external changes are preserved
|
|
333
|
+
existingOriginal.value = currentValue;
|
|
334
|
+
} else {
|
|
335
|
+
originalValues.push({ name: override.name, value: currentValue });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Check if this override changes shader features (truthiness change).
|
|
339
|
+
// E.g. null → Texture enables USE_LIGHTMAP, Texture → null disables it.
|
|
340
|
+
// Pure uniform changes (red → blue, textureA → textureB) don't need program switch.
|
|
341
|
+
if (!needsProgramChange && !!currentValue !== !!override.value) {
|
|
342
|
+
needsProgramChange = true;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Set all material properties including lightMap -
|
|
346
|
+
// three.js reads material.lightMap to determine shader parameters and upload uniforms
|
|
347
|
+
mat[override.name] = override.value;
|
|
348
|
+
|
|
349
|
+
// Apply per-object texture transform (offset/repeat) if specified
|
|
350
|
+
if (override.textureTransform && override.value instanceof Texture) {
|
|
351
|
+
const tex = override.value;
|
|
352
|
+
if (!mat[$savedTextureTransforms]) mat[$savedTextureTransforms] = [];
|
|
353
|
+
(mat[$savedTextureTransforms] as SavedTextureTransform[]).push({
|
|
354
|
+
name: override.name,
|
|
355
|
+
offsetX: tex.offset.x, offsetY: tex.offset.y,
|
|
356
|
+
repeatX: tex.repeat.x, repeatY: tex.repeat.y
|
|
357
|
+
});
|
|
358
|
+
const t = override.textureTransform;
|
|
359
|
+
if (t.offset) tex.offset.copy(t.offset);
|
|
360
|
+
if (t.repeat) tex.repeat.copy(t.repeat);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Only set needsUpdate when overrides change shader features (truthiness changes
|
|
365
|
+
// like null↔texture, or defines added). This triggers getProgram() for program switches.
|
|
366
|
+
// Pure uniform overrides (color, roughness) skip this — no version increment needed.
|
|
367
|
+
if (needsProgramChange) {
|
|
368
|
+
mat.needsUpdate = true;
|
|
369
|
+
}
|
|
370
|
+
// _forceRefresh triggers uniform re-upload for consecutive objects sharing
|
|
371
|
+
// the same program and material (without it three.js skips the upload).
|
|
372
|
+
mat._forceRefresh = true;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// #region OnAfterRender
|
|
376
|
+
const onAfterRender_MaterialBlock: ObjectRenderCallback = function (this: Object3D, _renderer: WebGLRenderer, _scene: Scene, _camera: Camera, _geometry: BufferGeometry, material: Material, _group: Group) {
|
|
377
|
+
const result = findPropertyBlockAndOwner(this);
|
|
378
|
+
if (!result) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const { block: propertyBlock, owner } = result;
|
|
383
|
+
|
|
384
|
+
// Only restore if this material was registered with this property block
|
|
385
|
+
if (!registry.hasMaterial(owner, material)) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const overrides = propertyBlock.overrides;
|
|
390
|
+
|
|
391
|
+
const mat = material as any;
|
|
392
|
+
const originalValues = mat[$originalValues] as OriginalValue[] | undefined;
|
|
393
|
+
|
|
394
|
+
// Clean up defines — this affects shader compilation
|
|
395
|
+
const defines = propertyBlock.getDefines();
|
|
396
|
+
const defineKeys = Object.keys(defines);
|
|
397
|
+
let needsProgramChange = false;
|
|
398
|
+
if (defineKeys.length > 0 && mat.defines) {
|
|
399
|
+
for (const key of defineKeys) {
|
|
400
|
+
delete mat.defines[key];
|
|
401
|
+
}
|
|
402
|
+
needsProgramChange = true;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (overrides.length === 0) {
|
|
406
|
+
if (needsProgramChange) {
|
|
407
|
+
mat.needsUpdate = true;
|
|
408
|
+
mat._forceRefresh = true;
|
|
409
|
+
}
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (!originalValues) return;
|
|
414
|
+
|
|
415
|
+
// Restore texture transforms before restoring material properties
|
|
416
|
+
const savedTransforms = mat[$savedTextureTransforms] as SavedTextureTransform[] | undefined;
|
|
417
|
+
if (savedTransforms && savedTransforms.length > 0) {
|
|
418
|
+
for (const saved of savedTransforms) {
|
|
419
|
+
const override = overrides.find(o => o.name === saved.name);
|
|
420
|
+
if (override?.value instanceof Texture) {
|
|
421
|
+
override.value.offset.set(saved.offsetX, saved.offsetY);
|
|
422
|
+
override.value.repeat.set(saved.repeatX, saved.repeatY);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
savedTransforms.length = 0;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
for (const override of overrides) {
|
|
429
|
+
const original = originalValues.find(o => o.name === override.name);
|
|
430
|
+
if (original) {
|
|
431
|
+
// Check if restoring changes shader features (truthiness change)
|
|
432
|
+
if (!needsProgramChange && !!override.value !== !!original.value) {
|
|
433
|
+
needsProgramChange = true;
|
|
434
|
+
}
|
|
435
|
+
mat[override.name] = original.value;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Only set needsUpdate when restoring affects shader features
|
|
440
|
+
if (needsProgramChange) {
|
|
441
|
+
mat.needsUpdate = true;
|
|
442
|
+
}
|
|
443
|
+
// Always force uniform refresh so the next object gets correct values
|
|
444
|
+
mat._forceRefresh = true;
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
// #region Attach Callbacks
|
|
449
|
+
function attachPropertyBlockToObject(object: Object3D, _propertyBlock: MaterialPropertyBlock): void {
|
|
450
|
+
// Collect and register all materials that belong to this property block
|
|
451
|
+
const materials = new Set<Material>();
|
|
452
|
+
collectMaterials(object, materials);
|
|
453
|
+
materials.forEach(mat => registry.addMaterial(object, mat));
|
|
454
|
+
|
|
455
|
+
// Attach callbacks to renderable objects (Mesh, SkinnedMesh)
|
|
456
|
+
// Groups don't render themselves but we still need to handle child meshes
|
|
457
|
+
if (object.type === "Group") {
|
|
458
|
+
object.children.forEach(child => {
|
|
459
|
+
if (child.type === "Mesh" || child.type === "SkinnedMesh") {
|
|
460
|
+
attachCallbacksToMesh(child, object);
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
} else if (object.type === "Mesh" || object.type === "SkinnedMesh") {
|
|
464
|
+
attachCallbacksToMesh(object, object);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function attachCallbacksToMesh(mesh: Object3D, propertyBlockOwner: Object3D): void {
|
|
468
|
+
// Check if this specific mesh already has our callbacks attached for this property block owner
|
|
469
|
+
if (registry.isHooked(mesh, propertyBlockOwner)) {
|
|
470
|
+
// Already hooked for this property block owner
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
registry.addHook(mesh, propertyBlockOwner);
|
|
475
|
+
|
|
476
|
+
if (!mesh.onBeforeRender) {
|
|
477
|
+
mesh.onBeforeRender = onBeforeRender_MaterialBlock;
|
|
478
|
+
} else {
|
|
479
|
+
const original = mesh.onBeforeRender;
|
|
480
|
+
mesh.onBeforeRender = function (renderer, scene, camera, geometry, material, group) {
|
|
481
|
+
original.call(this, renderer, scene, camera, geometry, material, group);
|
|
482
|
+
onBeforeRender_MaterialBlock.call(this, renderer, scene, camera, geometry, material, group);
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (!mesh.onAfterRender) {
|
|
487
|
+
mesh.onAfterRender = onAfterRender_MaterialBlock;
|
|
488
|
+
} else {
|
|
489
|
+
const original = mesh.onAfterRender;
|
|
490
|
+
mesh.onAfterRender = function (renderer, scene, camera, geometry, material, group) {
|
|
491
|
+
onAfterRender_MaterialBlock.call(this, renderer, scene, camera, geometry, material, group);
|
|
492
|
+
original.call(this, renderer, scene, camera, geometry, material, group);
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
//#endregion
|
|
497
|
+
|
|
498
|
+
export function objectHasPropertyBlock(object: Object3D): boolean {
|
|
499
|
+
return registry.getBlock(object) !== undefined;
|
|
500
|
+
}
|