@needle-tools/engine 4.13.1-next.9fc3e64 → 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/dist/generateMeshBVH.worker-DFcS3P04.js +21 -0
- package/dist/{gltf-progressive-Dbi_Tfhb.js → gltf-progressive-8voIgNp_.js} +4 -4
- package/dist/{gltf-progressive-CaUGGjVL.umd.cjs → gltf-progressive-BRRBj-nY.umd.cjs} +1 -1
- package/dist/{gltf-progressive-DuAR0MQR.min.js → gltf-progressive-Dkh3tG4-.min.js} +1 -1
- 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-Dqrh7aWw.umd.cjs → needle-engine.bundle-BQXG5qbQ.umd.cjs} +136 -143
- package/dist/{needle-engine.bundle-BZRE5G6O.js → needle-engine.bundle-Byl5i6zJ.js} +6120 -5904
- package/dist/{needle-engine.bundle-DwybonUg.min.js → needle-engine.bundle-D7w0XD7M.min.js} +147 -154
- package/dist/needle-engine.d.ts +61 -9
- 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_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-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/package.json +2 -2
- package/src/engine/api.ts +1 -0
- package/src/engine/engine_lightdata.ts +8 -6
- package/src/engine/engine_materialpropertyblock.ts +500 -0
- package/src/engine-components/ReflectionProbe.ts +17 -89
- package/src/engine-components/RendererLightmap.ts +76 -87
- package/dist/generateMeshBVH.worker-iyfPIK6R.js +0 -21
- package/dist/loader.worker-C1GG9A7C.js +0 -23
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CubeReflectionMapping, CubeTexture, EquirectangularReflectionMapping, LinearSRGBColorSpace, Material, MeshBasicMaterial, Object3D, SRGBColorSpace, Texture, Vector3 } from "three";
|
|
1
|
+
import { Color, CubeReflectionMapping, CubeTexture, EquirectangularReflectionMapping, LinearSRGBColorSpace, Material, MeshBasicMaterial, MeshStandardMaterial, Object3D, SRGBColorSpace, Texture, Vector3 } from "three";
|
|
2
2
|
|
|
3
3
|
import { isDevEnvironment, showBalloonWarning } from "../engine/debug/index.js";
|
|
4
4
|
import { serializable } from "../engine/engine_serialization.js";
|
|
@@ -7,13 +7,12 @@ import type { IRenderer } from "../engine/engine_types.js";
|
|
|
7
7
|
import { getParam } from "../engine/engine_utils.js";
|
|
8
8
|
import { BoxHelperComponent } from "./BoxHelperComponent.js";
|
|
9
9
|
import { Behaviour } from "./Component.js";
|
|
10
|
+
import { MaterialPropertyBlock } from "../engine/engine_materialpropertyblock.js";
|
|
10
11
|
|
|
11
12
|
export const debug = getParam("debugreflectionprobe");
|
|
12
13
|
const disable = getParam("noreflectionprobe");
|
|
13
14
|
|
|
14
15
|
const $reflectionProbeKey = Symbol("reflectionProbeKey");
|
|
15
|
-
const $originalMaterial = Symbol("original material");
|
|
16
|
-
|
|
17
16
|
/**
|
|
18
17
|
* The [ReflectionProbe](https://engine.needle.tools/docs/api/ReflectionProbe) provides environment reflection data to materials within its defined area.
|
|
19
18
|
* Use for chrome-like materials that need accurate environment reflections.
|
|
@@ -38,12 +37,13 @@ const $originalMaterial = Symbol("original material");
|
|
|
38
37
|
* @see {@link Renderer} for material and rendering control
|
|
39
38
|
* @see {@link Light} for scene lighting
|
|
40
39
|
*/
|
|
40
|
+
|
|
41
41
|
export class ReflectionProbe extends Behaviour {
|
|
42
42
|
|
|
43
43
|
private static _probes: Map<Context, ReflectionProbe[]> = new Map();
|
|
44
44
|
|
|
45
45
|
static isUsingReflectionProbe(material: Material) {
|
|
46
|
-
return !!(material
|
|
46
|
+
return !!(material as any)[$reflectionProbeKey];
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
public static get(object: Object3D | null | undefined, context: Context, isAnchor: boolean, anchor?: Object3D): ReflectionProbe | null {
|
|
@@ -55,7 +55,6 @@ export class ReflectionProbe extends Behaviour {
|
|
|
55
55
|
if (!probe.__didAwake) probe.__internalAwake();
|
|
56
56
|
if (probe.activeAndEnabled) {
|
|
57
57
|
if (anchor) {
|
|
58
|
-
// test if anchor is reflection probe object
|
|
59
58
|
if (probe.gameObject === anchor) {
|
|
60
59
|
return probe;
|
|
61
60
|
}
|
|
@@ -72,13 +71,9 @@ export class ReflectionProbe extends Behaviour {
|
|
|
72
71
|
return null;
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
|
|
76
|
-
|
|
77
74
|
private _texture!: Texture;
|
|
78
75
|
|
|
79
|
-
// @serializable(Texture)
|
|
80
76
|
set texture(tex: Texture) {
|
|
81
|
-
|
|
82
77
|
if (this._texture === tex) return;
|
|
83
78
|
this._texture = tex;
|
|
84
79
|
|
|
@@ -86,7 +81,6 @@ export class ReflectionProbe extends Behaviour {
|
|
|
86
81
|
|
|
87
82
|
if (tex) {
|
|
88
83
|
if (tex instanceof CubeTexture) {
|
|
89
|
-
// cube textures use CubeReflectionMapping by default
|
|
90
84
|
}
|
|
91
85
|
else if (tex.mapping !== EquirectangularReflectionMapping) {
|
|
92
86
|
tex.mapping = EquirectangularReflectionMapping;
|
|
@@ -146,29 +140,14 @@ export class ReflectionProbe extends Behaviour {
|
|
|
146
140
|
}
|
|
147
141
|
}
|
|
148
142
|
|
|
149
|
-
|
|
150
|
-
// when objects are rendered and they share material
|
|
151
|
-
// and some need reflection probe and some don't
|
|
152
|
-
// we need to make sure we don't override the material but use a copy
|
|
153
|
-
|
|
154
|
-
private static _rendererMaterialsCache: Map<IRenderer, Array<{ material: Material, copy: Material, originalVersion: number }>> = new Map();
|
|
155
|
-
|
|
156
143
|
onSet(_rend: IRenderer) {
|
|
157
144
|
if (disable) return;
|
|
158
145
|
if (!this.enabled) return;
|
|
159
146
|
if (_rend.sharedMaterials?.length <= 0) return;
|
|
160
147
|
if (!this.texture) return;
|
|
161
148
|
|
|
162
|
-
|
|
163
|
-
if (!rendererCache) {
|
|
164
|
-
rendererCache = [];
|
|
165
|
-
ReflectionProbe._rendererMaterialsCache.set(_rend, rendererCache);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// 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
|
|
149
|
+
const object = _rend.gameObject as unknown as Object3D;
|
|
169
150
|
|
|
170
|
-
// need to make sure materials are not shared when using reflection probes
|
|
171
|
-
// otherwise some renderers outside of the probe will be affected or vice versa
|
|
172
151
|
for (let i = 0; i < _rend.sharedMaterials.length; i++) {
|
|
173
152
|
const material = _rend.sharedMaterials[i];
|
|
174
153
|
if (!material) {
|
|
@@ -178,75 +157,24 @@ export class ReflectionProbe extends Behaviour {
|
|
|
178
157
|
continue;
|
|
179
158
|
}
|
|
180
159
|
|
|
181
|
-
if (material
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
let cached = rendererCache[i];
|
|
186
|
-
|
|
187
|
-
// make sure we have the currently assigned material cached (and an up to date clone of that)
|
|
188
|
-
// Compare against the stored original version, not the clone's version (which gets modified by needsUpdate)
|
|
189
|
-
// This prevents cloning materials every frame when onUnset restores the original material
|
|
190
|
-
const isCachedInstance = material === cached?.copy;
|
|
191
|
-
const hasChanged = !cached || cached.material.uuid !== material.uuid || cached.originalVersion !== material.version;
|
|
192
|
-
if (!isCachedInstance && hasChanged) {
|
|
193
|
-
if (debug) {
|
|
194
|
-
let reason = "";
|
|
195
|
-
if (!cached) reason = "not cached";
|
|
196
|
-
else if (cached.material !== material) reason = "reference changed; cached instance?: " + isCachedInstance;
|
|
197
|
-
else if (cached.originalVersion !== material.version) reason = "version changed";
|
|
198
|
-
console.warn("Cloning material", material.name, material.version, "Reason:", reason, "\n", material.uuid, "\n", cached?.copy.uuid, "\n", _rend.name);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const clone = material.clone();
|
|
202
|
-
clone.version = material.version;
|
|
203
|
-
|
|
204
|
-
if (cached) {
|
|
205
|
-
cached.copy = clone;
|
|
206
|
-
cached.material = material;
|
|
207
|
-
cached.originalVersion = material.version;
|
|
208
|
-
}
|
|
209
|
-
else {
|
|
210
|
-
cached = {
|
|
211
|
-
material: material,
|
|
212
|
-
copy: clone,
|
|
213
|
-
originalVersion: material.version
|
|
214
|
-
};
|
|
215
|
-
rendererCache.push(cached);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
clone[$reflectionProbeKey] = this;
|
|
219
|
-
clone[$originalMaterial] = material;
|
|
220
|
-
|
|
221
|
-
if (debug) console.log("Set reflection", _rend.name, _rend.guid);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// See NE-4771 and NE-4856
|
|
225
|
-
if (cached && cached.copy) {
|
|
226
|
-
cached.copy.onBeforeCompile = material.onBeforeCompile;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/** this is the material that we copied and that has the reflection probe */
|
|
230
|
-
const copy = cached?.copy;
|
|
160
|
+
if (debug) console.log("Setting reflection probe on material", material.name, "for renderer", _rend.name);
|
|
231
161
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
copy.needsUpdate = true;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
162
|
+
const propertyBlock = MaterialPropertyBlock.get(object);
|
|
163
|
+
propertyBlock.setOverride("envMap", this.texture);
|
|
164
|
+
// propertyBlock.setOverride("color", new Color(0xff0000));
|
|
238
165
|
|
|
239
|
-
|
|
166
|
+
(material as any)[$reflectionProbeKey] = true;
|
|
240
167
|
}
|
|
241
168
|
}
|
|
242
169
|
|
|
243
170
|
onUnset(_rend: IRenderer) {
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
171
|
+
// const object = _rend.gameObject as unknown as Object3D;
|
|
172
|
+
|
|
173
|
+
for (let i = 0; i < _rend.sharedMaterials.length; i++) {
|
|
174
|
+
const material = _rend.sharedMaterials[i];
|
|
175
|
+
if (material) {
|
|
176
|
+
delete (material as any)[$reflectionProbeKey];
|
|
249
177
|
}
|
|
250
178
|
}
|
|
251
179
|
}
|
|
252
|
-
}
|
|
180
|
+
}
|