three-realtime-rt 0.3.0 → 0.4.0
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/README.md +559 -394
- package/package.json +52 -52
- package/src/CompositePass.js +261 -214
- package/src/CopyPass.js +75 -0
- package/src/DenoisePass.js +221 -202
- package/src/GBufferPass.js +290 -228
- package/src/RTLightingPass.js +1124 -680
- package/src/RealtimeRaytracer.js +1106 -730
- package/src/RestirPass.js +31 -4
- package/src/SceneCompiler.js +542 -424
- package/src/TAAPass.js +271 -230
- package/src/VolumetricPass.js +398 -329
- package/src/blueNoise.js +17 -17
- package/src/index.d.ts +107 -6
package/src/SceneCompiler.js
CHANGED
|
@@ -1,424 +1,542 @@
|
|
|
1
|
-
import * as THREE from "three";
|
|
2
|
-
import { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js";
|
|
3
|
-
import {
|
|
4
|
-
MeshBVH,
|
|
5
|
-
MeshBVHUniformStruct,
|
|
6
|
-
FloatVertexAttributeTexture,
|
|
7
|
-
SAH,
|
|
8
|
-
CENTER,
|
|
9
|
-
} from "three-mesh-bvh";
|
|
10
|
-
import { decodeBlueNoise, BLUE_NOISE_SIZE } from "./blueNoise.js";
|
|
11
|
-
|
|
12
|
-
const MAX_LIGHTS =
|
|
13
|
-
|
|
14
|
-
// Emissive-mesh triangles sampled by next-event estimation. Beyond the cap the
|
|
15
|
-
// largest-area triangles win (they carry the most light); the rest are dropped
|
|
16
|
-
// from the light list with a warning.
|
|
17
|
-
const MAX_EMISSIVE_TRIS = 256;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* A two-level BVH scene. Static geometry lives in one BVH uploaded to the GPU
|
|
21
|
-
* ONCE; dynamic (moving) meshes live in a second, small BVH that is re-baked and
|
|
22
|
-
* re-uploaded every frame. The lighting shader traces both and takes the nearest
|
|
23
|
-
* hit, so the per-frame cost scales with the *moving* triangle count only — not
|
|
24
|
-
* the size of the static world. (This is the TLAS/BLAS idea, minus per-instance
|
|
25
|
-
* transforms: one merged dynamic mesh is plenty for typical rigid-body scenes.)
|
|
26
|
-
*/
|
|
27
|
-
export class CompiledScene {
|
|
28
|
-
constructor() {
|
|
29
|
-
// Static level (built once). Per level, one packed RGBA attribute texture
|
|
30
|
-
// (normal.xyz + materialIndex in .w): two BVH structs already cost 8 of the
|
|
31
|
-
// 16 guaranteed fragment samplers, so attributes must stay lean.
|
|
32
|
-
this.staticBvh = null;
|
|
33
|
-
this.staticBvhUniform = new MeshBVHUniformStruct();
|
|
34
|
-
this.staticAttrTex = new FloatVertexAttributeTexture();
|
|
35
|
-
|
|
36
|
-
// Dynamic level (re-baked/refit each frame).
|
|
37
|
-
this.dynamicBvh = null;
|
|
38
|
-
this.dynamicBvhUniform = new MeshBVHUniformStruct();
|
|
39
|
-
this.dynamicAttrTex = new FloatVertexAttributeTexture();
|
|
40
|
-
this.dynamicMerged = null;
|
|
41
|
-
this.dynamicPacked = null; // Float32Array + BufferAttribute for re-baking
|
|
42
|
-
this.dynamicPackedAttr = null;
|
|
43
|
-
this.dynamic = []; // [{ mesh, start, count, localPos, localNorm }]
|
|
44
|
-
this.hasDynamic = false;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
this.
|
|
48
|
-
|
|
49
|
-
this.
|
|
50
|
-
this.
|
|
51
|
-
this.
|
|
52
|
-
this.
|
|
53
|
-
|
|
54
|
-
this.
|
|
55
|
-
this.
|
|
56
|
-
this.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
compiled
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
obj.
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js";
|
|
3
|
+
import {
|
|
4
|
+
MeshBVH,
|
|
5
|
+
MeshBVHUniformStruct,
|
|
6
|
+
FloatVertexAttributeTexture,
|
|
7
|
+
SAH,
|
|
8
|
+
CENTER,
|
|
9
|
+
} from "three-mesh-bvh";
|
|
10
|
+
import { decodeBlueNoise, BLUE_NOISE_SIZE } from "./blueNoise.js";
|
|
11
|
+
|
|
12
|
+
const MAX_LIGHTS = 32; // stage-1 cap; a data-texture light list is future work
|
|
13
|
+
|
|
14
|
+
// Emissive-mesh triangles sampled by next-event estimation. Beyond the cap the
|
|
15
|
+
// largest-area triangles win (they carry the most light); the rest are dropped
|
|
16
|
+
// from the light list with a warning.
|
|
17
|
+
const MAX_EMISSIVE_TRIS = 256;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A two-level BVH scene. Static geometry lives in one BVH uploaded to the GPU
|
|
21
|
+
* ONCE; dynamic (moving) meshes live in a second, small BVH that is re-baked and
|
|
22
|
+
* re-uploaded every frame. The lighting shader traces both and takes the nearest
|
|
23
|
+
* hit, so the per-frame cost scales with the *moving* triangle count only — not
|
|
24
|
+
* the size of the static world. (This is the TLAS/BLAS idea, minus per-instance
|
|
25
|
+
* transforms: one merged dynamic mesh is plenty for typical rigid-body scenes.)
|
|
26
|
+
*/
|
|
27
|
+
export class CompiledScene {
|
|
28
|
+
constructor() {
|
|
29
|
+
// Static level (built once). Per level, one packed RGBA attribute texture
|
|
30
|
+
// (normal.xyz + materialIndex in .w): two BVH structs already cost 8 of the
|
|
31
|
+
// 16 guaranteed fragment samplers, so attributes must stay lean.
|
|
32
|
+
this.staticBvh = null;
|
|
33
|
+
this.staticBvhUniform = new MeshBVHUniformStruct();
|
|
34
|
+
this.staticAttrTex = new FloatVertexAttributeTexture();
|
|
35
|
+
|
|
36
|
+
// Dynamic level (re-baked/refit each frame).
|
|
37
|
+
this.dynamicBvh = null;
|
|
38
|
+
this.dynamicBvhUniform = new MeshBVHUniformStruct();
|
|
39
|
+
this.dynamicAttrTex = new FloatVertexAttributeTexture();
|
|
40
|
+
this.dynamicMerged = null;
|
|
41
|
+
this.dynamicPacked = null; // Float32Array + BufferAttribute for re-baking
|
|
42
|
+
this.dynamicPackedAttr = null;
|
|
43
|
+
this.dynamic = []; // [{ mesh, start, count, localPos, localNorm, deforming, ... }]
|
|
44
|
+
this.hasDynamic = false;
|
|
45
|
+
// True when any dynamic segment is CPU-deformed (rtDeforming) — such segments
|
|
46
|
+
// read their live geometry every frame and force a per-frame normal upload.
|
|
47
|
+
this.hasDeforming = false;
|
|
48
|
+
|
|
49
|
+
this.materialsTex = null;
|
|
50
|
+
this.materials = [];
|
|
51
|
+
this.lightPosType = [];
|
|
52
|
+
this.lightColorRadius = [];
|
|
53
|
+
this.lightDirCone = []; // spot direction.xyz + cos(outer angle)
|
|
54
|
+
this.lightCount = 0;
|
|
55
|
+
this.emissiveTriCount = 0;
|
|
56
|
+
this.triangleCount = 0;
|
|
57
|
+
|
|
58
|
+
this._m3 = new THREE.Matrix3();
|
|
59
|
+
this._normalFrame = 0;
|
|
60
|
+
this._dynBuildVolume = null; // world-volume of the dynamic set at build time
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Re-bake the dynamic meshes' current world transforms into the dynamic
|
|
65
|
+
* geometry, refit the dynamic BVH, and re-upload ONLY the (small) dynamic
|
|
66
|
+
* textures. The static BVH is never touched. Call once per frame after moving
|
|
67
|
+
* the meshes.
|
|
68
|
+
*/
|
|
69
|
+
updateDynamic() {
|
|
70
|
+
if (!this.hasDynamic || this.dynamic.length === 0) return;
|
|
71
|
+
const posAttr = this.dynamicMerged.getAttribute("position");
|
|
72
|
+
const pos = posAttr.array;
|
|
73
|
+
const packed = this.dynamicPacked; // normal.xyz + matIndex.w per vertex
|
|
74
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
75
|
+
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
76
|
+
|
|
77
|
+
for (const seg of this.dynamic) {
|
|
78
|
+
seg.mesh.updateWorldMatrix(true, false);
|
|
79
|
+
const m = seg.mesh.matrixWorld.elements;
|
|
80
|
+
const nm = this._m3.getNormalMatrix(seg.mesh.matrixWorld).elements;
|
|
81
|
+
let o = seg.start * 3;
|
|
82
|
+
let p = seg.start * 4;
|
|
83
|
+
|
|
84
|
+
if (seg.deforming) {
|
|
85
|
+
// CPU-deformed mesh (water/cloth): read the LIVE geometry every frame
|
|
86
|
+
// and expand it back to the merged de-indexed layout through the mapping
|
|
87
|
+
// snapshotted at compile time. `indexMap` (the source geometry's index
|
|
88
|
+
// buffer, or null for an already-non-indexed source) maps each merged
|
|
89
|
+
// triangle-soup vertex slot to its source-vertex index; the source
|
|
90
|
+
// attributes carry the shared, deformed values.
|
|
91
|
+
const livePosAttr = seg.liveGeometry.getAttribute("position");
|
|
92
|
+
if (livePosAttr.count !== seg.srcVertexCount) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
"three-realtime-rt: deforming mesh vertex count changed since " +
|
|
95
|
+
`compile (${seg.srcVertexCount} -> ${livePosAttr.count}); the merged ` +
|
|
96
|
+
"BVH layout is fixed at compile time — call compileScene() again."
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const sp = livePosAttr.array;
|
|
100
|
+
const snAttr = seg.liveGeometry.getAttribute("normal");
|
|
101
|
+
const sn = snAttr ? snAttr.array : null;
|
|
102
|
+
const map = seg.indexMap; // null = identity (non-indexed source)
|
|
103
|
+
const ln = seg.localNorm; // fallback if the app never recomputed normals
|
|
104
|
+
for (let i = 0; i < seg.count; i++) {
|
|
105
|
+
const sv = map ? map[i] : i;
|
|
106
|
+
const x = sp[sv * 3], y = sp[sv * 3 + 1], z = sp[sv * 3 + 2];
|
|
107
|
+
const wx = m[0] * x + m[4] * y + m[8] * z + m[12];
|
|
108
|
+
const wy = m[1] * x + m[5] * y + m[9] * z + m[13];
|
|
109
|
+
const wz = m[2] * x + m[6] * y + m[10] * z + m[14];
|
|
110
|
+
pos[o] = wx;
|
|
111
|
+
pos[o + 1] = wy;
|
|
112
|
+
pos[o + 2] = wz;
|
|
113
|
+
if (wx < minX) minX = wx; if (wx > maxX) maxX = wx;
|
|
114
|
+
if (wy < minY) minY = wy; if (wy > maxY) maxY = wy;
|
|
115
|
+
if (wz < minZ) minZ = wz; if (wz > maxZ) maxZ = wz;
|
|
116
|
+
let nx, ny, nz;
|
|
117
|
+
if (sn) { nx = sn[sv * 3]; ny = sn[sv * 3 + 1]; nz = sn[sv * 3 + 2]; }
|
|
118
|
+
else { nx = ln[i * 3]; ny = ln[i * 3 + 1]; nz = ln[i * 3 + 2]; }
|
|
119
|
+
const tx = nm[0] * nx + nm[3] * ny + nm[6] * nz;
|
|
120
|
+
const ty = nm[1] * nx + nm[4] * ny + nm[7] * nz;
|
|
121
|
+
const tz = nm[2] * nx + nm[5] * ny + nm[8] * nz;
|
|
122
|
+
const il = 1.0 / (Math.hypot(tx, ty, tz) || 1);
|
|
123
|
+
packed[p] = tx * il;
|
|
124
|
+
packed[p + 1] = ty * il;
|
|
125
|
+
packed[p + 2] = tz * il;
|
|
126
|
+
// packed[p + 3] (matIndex) never changes
|
|
127
|
+
o += 3;
|
|
128
|
+
p += 4;
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
// Rigid mover: transform the frozen local snapshot by the world matrix.
|
|
132
|
+
const lp = seg.localPos;
|
|
133
|
+
const ln = seg.localNorm;
|
|
134
|
+
for (let i = 0; i < seg.count; i++) {
|
|
135
|
+
const x = lp[i * 3], y = lp[i * 3 + 1], z = lp[i * 3 + 2];
|
|
136
|
+
const wx = m[0] * x + m[4] * y + m[8] * z + m[12];
|
|
137
|
+
const wy = m[1] * x + m[5] * y + m[9] * z + m[13];
|
|
138
|
+
const wz = m[2] * x + m[6] * y + m[10] * z + m[14];
|
|
139
|
+
pos[o] = wx;
|
|
140
|
+
pos[o + 1] = wy;
|
|
141
|
+
pos[o + 2] = wz;
|
|
142
|
+
if (wx < minX) minX = wx; if (wx > maxX) maxX = wx;
|
|
143
|
+
if (wy < minY) minY = wy; if (wy > maxY) maxY = wy;
|
|
144
|
+
if (wz < minZ) minZ = wz; if (wz > maxZ) maxZ = wz;
|
|
145
|
+
const nx = ln[i * 3], ny = ln[i * 3 + 1], nz = ln[i * 3 + 2];
|
|
146
|
+
const tx = nm[0] * nx + nm[3] * ny + nm[6] * nz;
|
|
147
|
+
const ty = nm[1] * nx + nm[4] * ny + nm[7] * nz;
|
|
148
|
+
const tz = nm[2] * nx + nm[5] * ny + nm[8] * nz;
|
|
149
|
+
const il = 1.0 / (Math.hypot(tx, ty, tz) || 1);
|
|
150
|
+
packed[p] = tx * il;
|
|
151
|
+
packed[p + 1] = ty * il;
|
|
152
|
+
packed[p + 2] = tz * il;
|
|
153
|
+
// packed[p + 3] (matIndex) never changes
|
|
154
|
+
o += 3;
|
|
155
|
+
p += 4;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
posAttr.needsUpdate = true;
|
|
160
|
+
|
|
161
|
+
// refit() keeps the tree TOPOLOGY from build time. While the props sit in
|
|
162
|
+
// a pile that's fine — but once they scatter (an explosion), triangles
|
|
163
|
+
// that are tree-neighbors end up across the room, refitted nodes balloon
|
|
164
|
+
// into huge overlapping boxes, and every ray wades through them (observed:
|
|
165
|
+
// 30 fps → 10 on mobile). When the set has spread well past its
|
|
166
|
+
// build-time volume, rebuild the tree from scratch instead — a few ms,
|
|
167
|
+
// paid only on large redistributions.
|
|
168
|
+
const vol =
|
|
169
|
+
Math.max(maxX - minX, 1e-6) *
|
|
170
|
+
Math.max(maxY - minY, 1e-6) *
|
|
171
|
+
Math.max(maxZ - minZ, 1e-6);
|
|
172
|
+
if (this._dynBuildVolume == null) this._dynBuildVolume = vol;
|
|
173
|
+
if (vol > this._dynBuildVolume * 3 || vol < this._dynBuildVolume / 3) {
|
|
174
|
+
this.dynamicBvh = new MeshBVH(this.dynamicMerged, { strategy: CENTER });
|
|
175
|
+
this._dynBuildVolume = vol;
|
|
176
|
+
} else {
|
|
177
|
+
this.dynamicBvh.refit();
|
|
178
|
+
}
|
|
179
|
+
this.dynamicBvhUniform.updateFrom(this.dynamicBvh);
|
|
180
|
+
// Normals only feed GI-bounce shading off movers — amortize their upload for
|
|
181
|
+
// rigid movers. Deforming meshes change shape (not just orientation) every
|
|
182
|
+
// frame, so their normals must go up every frame or the shading lags the
|
|
183
|
+
// silhouette; one deforming segment forces the whole (shared) upload.
|
|
184
|
+
if (this.hasDeforming || this._normalFrame++ % 8 === 0) {
|
|
185
|
+
this.dynamicAttrTex.updateFrom(this.dynamicPackedAttr);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
dispose() {
|
|
190
|
+
this.staticBvhUniform.dispose();
|
|
191
|
+
this.staticAttrTex.dispose();
|
|
192
|
+
this.dynamicBvhUniform.dispose();
|
|
193
|
+
this.dynamicAttrTex.dispose();
|
|
194
|
+
if (this.materialsTex) this.materialsTex.dispose();
|
|
195
|
+
if (this.staticBvh) this.staticBvh.geometry.dispose();
|
|
196
|
+
if (this.dynamicMerged) this.dynamicMerged.dispose();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function extractMeshGeometry(mesh, materialIndex) {
|
|
201
|
+
const indexed = mesh.geometry.index;
|
|
202
|
+
const src = indexed ? mesh.geometry.toNonIndexed() : mesh.geometry.clone();
|
|
203
|
+
|
|
204
|
+
if (!src.getAttribute("normal")) src.computeVertexNormals();
|
|
205
|
+
const localPos = src.getAttribute("position").array.slice();
|
|
206
|
+
const localNorm = src.getAttribute("normal").array.slice();
|
|
207
|
+
|
|
208
|
+
const geo = new THREE.BufferGeometry();
|
|
209
|
+
geo.setAttribute("position", src.getAttribute("position").clone());
|
|
210
|
+
geo.setAttribute("normal", src.getAttribute("normal").clone());
|
|
211
|
+
geo.applyMatrix4(mesh.matrixWorld); // bake world transform
|
|
212
|
+
|
|
213
|
+
const count = geo.getAttribute("position").count;
|
|
214
|
+
const mi = new Float32Array(count).fill(materialIndex);
|
|
215
|
+
geo.setAttribute("materialIndex", new THREE.BufferAttribute(mi, 1));
|
|
216
|
+
|
|
217
|
+
// For CPU-deformed (rtDeforming) meshes we re-read the LIVE geometry each
|
|
218
|
+
// frame. The merged BVH is de-indexed triangle soup, so record how to expand
|
|
219
|
+
// the live (source) vertices back into that layout: `indexMap[i]` is the
|
|
220
|
+
// source-vertex index feeding merged slot `i` (a snapshot of the index
|
|
221
|
+
// buffer), or null when the source was already non-indexed (identity map).
|
|
222
|
+
// `srcVertexCount` is the live position count at compile time — used to catch
|
|
223
|
+
// a topology change that would invalidate this mapping.
|
|
224
|
+
const indexMap = indexed ? mesh.geometry.index.array.slice() : null;
|
|
225
|
+
const srcVertexCount = mesh.geometry.getAttribute("position").count;
|
|
226
|
+
return { geo, localPos, localNorm, count, indexMap, srcVertexCount };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Effective emissive colour (already scaled by intensity), or null if the
|
|
230
|
+
// material doesn't emit. Matches the shading table's emissiveMap exclusion.
|
|
231
|
+
function emissiveColor(mat) {
|
|
232
|
+
if (mat.emissiveMap != null || !mat.emissive) return null;
|
|
233
|
+
const i = mat.emissiveIntensity ?? 1;
|
|
234
|
+
if (i <= 0 || mat.emissive.r + mat.emissive.g + mat.emissive.b <= 0) return null;
|
|
235
|
+
return [mat.emissive.r * i, mat.emissive.g * i, mat.emissive.b * i];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Row 0: materials, 2 texels each (albedo+rough, emissive+metal).
|
|
239
|
+
// Row 1: emissive triangles for NEE, 4 texels each:
|
|
240
|
+
// [v0.xyz | area] [e1.xyz | emit.r] [e2.xyz | emit.g] [n.xyz | emit.b]
|
|
241
|
+
// Rows 2..65: a 64x64 RGBA blue-noise tile for low-discrepancy sampling.
|
|
242
|
+
// Row 66 (2 + BLUE_NOISE_SIZE): the emissive power CDF, 1 texel per triangle:
|
|
243
|
+
// [cdf | prob | 0 | 0] — cumulative and individual pick probability, both
|
|
244
|
+
// proportional to area x emitted luminance. Lets NEE importance-sample WHICH
|
|
245
|
+
// triangle to shoot at instead of picking uniformly (a big/bright panel gets
|
|
246
|
+
// sampled proportionally more than a tiny dim strip), which is the main
|
|
247
|
+
// variance lever for emissive lighting outside of ReSTIR.
|
|
248
|
+
// All packed into ONE texture because the lighting pass already sits at the
|
|
249
|
+
// WebGL2-guaranteed 16-sampler limit — extra samplers are not available.
|
|
250
|
+
function buildSceneDataTexture(materials, emissiveTris) {
|
|
251
|
+
const bn = decodeBlueNoise();
|
|
252
|
+
const width = Math.max(materials.length * 2, emissiveTris.length * 4, BLUE_NOISE_SIZE);
|
|
253
|
+
const height = 2 + BLUE_NOISE_SIZE + 1;
|
|
254
|
+
const data = new Float32Array(width * height * 4);
|
|
255
|
+
materials.forEach((mat, i) => {
|
|
256
|
+
const o = i * 8;
|
|
257
|
+
const color = mat.color ?? new THREE.Color(1, 1, 1);
|
|
258
|
+
const emissive = emissiveColor(mat) ?? [0, 0, 0];
|
|
259
|
+
data[o + 0] = color.r;
|
|
260
|
+
data[o + 1] = color.g;
|
|
261
|
+
data[o + 2] = color.b;
|
|
262
|
+
data[o + 3] = mat.roughness ?? 1;
|
|
263
|
+
data[o + 4] = emissive[0];
|
|
264
|
+
data[o + 5] = emissive[1];
|
|
265
|
+
data[o + 6] = emissive[2];
|
|
266
|
+
data[o + 7] = mat.metalness ?? 0;
|
|
267
|
+
});
|
|
268
|
+
const row = width * 4;
|
|
269
|
+
emissiveTris.forEach((t, i) => {
|
|
270
|
+
const o = row + i * 16;
|
|
271
|
+
data[o + 0] = t.v0[0]; data[o + 1] = t.v0[1]; data[o + 2] = t.v0[2]; data[o + 3] = t.area;
|
|
272
|
+
data[o + 4] = t.e1[0]; data[o + 5] = t.e1[1]; data[o + 6] = t.e1[2]; data[o + 7] = t.emit[0];
|
|
273
|
+
data[o + 8] = t.e2[0]; data[o + 9] = t.e2[1]; data[o + 10] = t.e2[2]; data[o + 11] = t.emit[1];
|
|
274
|
+
data[o + 12] = t.n[0]; data[o + 13] = t.n[1]; data[o + 14] = t.n[2]; data[o + 15] = t.emit[2];
|
|
275
|
+
});
|
|
276
|
+
for (let y = 0; y < BLUE_NOISE_SIZE; y++) {
|
|
277
|
+
const o = (2 + y) * row;
|
|
278
|
+
const src = y * BLUE_NOISE_SIZE * 4;
|
|
279
|
+
for (let i = 0; i < BLUE_NOISE_SIZE * 4; i++) {
|
|
280
|
+
data[o + i] = (bn[src + i] + 0.5) / 256.0;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Emissive power CDF (see the layout comment above). Weight = area x
|
|
284
|
+
// emitted luminance; degenerate totals fall back to the uniform pick.
|
|
285
|
+
if (emissiveTris.length > 0) {
|
|
286
|
+
const w = emissiveTris.map(
|
|
287
|
+
(t) => t.area * (0.2126 * t.emit[0] + 0.7152 * t.emit[1] + 0.0722 * t.emit[2])
|
|
288
|
+
);
|
|
289
|
+
const total = w.reduce((a, b) => a + b, 0);
|
|
290
|
+
const cdfRow = (2 + BLUE_NOISE_SIZE) * row;
|
|
291
|
+
let cum = 0;
|
|
292
|
+
for (let i = 0; i < emissiveTris.length; i++) {
|
|
293
|
+
const p = total > 0 ? w[i] / total : 1 / emissiveTris.length;
|
|
294
|
+
cum += p;
|
|
295
|
+
data[cdfRow + i * 4 + 0] = i === emissiveTris.length - 1 ? 1.0 : cum;
|
|
296
|
+
data[cdfRow + i * 4 + 1] = p;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const tex = new THREE.DataTexture(data, width, height, THREE.RGBAFormat, THREE.FloatType);
|
|
300
|
+
tex.minFilter = THREE.NearestFilter;
|
|
301
|
+
tex.magFilter = THREE.NearestFilter;
|
|
302
|
+
tex.needsUpdate = true;
|
|
303
|
+
return tex;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Collect world-space triangles of an emissive mesh for the NEE light list.
|
|
307
|
+
// `geo` is already non-indexed and world-baked by extractMeshGeometry.
|
|
308
|
+
function collectEmissiveTriangles(geo, emit, out) {
|
|
309
|
+
const pos = geo.getAttribute("position").array;
|
|
310
|
+
for (let i = 0; i + 8 < pos.length; i += 9) {
|
|
311
|
+
const e1 = [pos[i + 3] - pos[i], pos[i + 4] - pos[i + 1], pos[i + 5] - pos[i + 2]];
|
|
312
|
+
const e2 = [pos[i + 6] - pos[i], pos[i + 7] - pos[i + 1], pos[i + 8] - pos[i + 2]];
|
|
313
|
+
const cx = e1[1] * e2[2] - e1[2] * e2[1];
|
|
314
|
+
const cy = e1[2] * e2[0] - e1[0] * e2[2];
|
|
315
|
+
const cz = e1[0] * e2[1] - e1[1] * e2[0];
|
|
316
|
+
const len = Math.hypot(cx, cy, cz);
|
|
317
|
+
if (len < 1e-10) continue; // degenerate
|
|
318
|
+
out.push({
|
|
319
|
+
v0: [pos[i], pos[i + 1], pos[i + 2]],
|
|
320
|
+
e1,
|
|
321
|
+
e2,
|
|
322
|
+
n: [cx / len, cy / len, cz / len],
|
|
323
|
+
area: len * 0.5,
|
|
324
|
+
emit,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// A single degenerate triangle so the dynamic BVH textures are always valid even
|
|
330
|
+
// when there are no dynamic meshes (tracing is gated by a uHasDynamic flag).
|
|
331
|
+
function degenerateGeometry() {
|
|
332
|
+
const geo = new THREE.BufferGeometry();
|
|
333
|
+
geo.setAttribute("position", new THREE.BufferAttribute(new Float32Array(9), 3));
|
|
334
|
+
geo.setAttribute("normal", new THREE.BufferAttribute(new Float32Array([0, 1, 0, 0, 1, 0, 0, 1, 0]), 3));
|
|
335
|
+
geo.setAttribute("materialIndex", new THREE.BufferAttribute(new Float32Array(3), 1));
|
|
336
|
+
return geo;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Pack normal.xyz + materialIndex.w into one itemSize-4 attribute so each BVH
|
|
340
|
+
// level costs a single sampler for its per-vertex data.
|
|
341
|
+
function packAttributes(merged) {
|
|
342
|
+
const norm = merged.getAttribute("normal");
|
|
343
|
+
const matIdx = merged.getAttribute("materialIndex");
|
|
344
|
+
const count = norm.count;
|
|
345
|
+
const packed = new Float32Array(count * 4);
|
|
346
|
+
for (let i = 0; i < count; i++) {
|
|
347
|
+
packed[i * 4] = norm.getX(i);
|
|
348
|
+
packed[i * 4 + 1] = norm.getY(i);
|
|
349
|
+
packed[i * 4 + 2] = norm.getZ(i);
|
|
350
|
+
packed[i * 4 + 3] = matIdx.getX(i);
|
|
351
|
+
}
|
|
352
|
+
return { packed, attr: new THREE.BufferAttribute(packed, 4) };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Build one BVH level (merge geometries, upload BVH + attribute textures).
|
|
356
|
+
function buildLevel(geometries, { dynamic }) {
|
|
357
|
+
const merged =
|
|
358
|
+
geometries.length > 0 ? mergeGeometries(geometries, false) : degenerateGeometry();
|
|
359
|
+
const bvh = new MeshBVH(merged, { strategy: dynamic ? CENTER : SAH });
|
|
360
|
+
return { merged, bvh, ...packAttributes(merged) };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function compileScene(scene, options = {}) {
|
|
364
|
+
scene.updateMatrixWorld(true);
|
|
365
|
+
const dynamicSet = options.dynamicMeshes ? new Set(options.dynamicMeshes) : null;
|
|
366
|
+
|
|
367
|
+
const compiled = new CompiledScene();
|
|
368
|
+
const materials = compiled.materials;
|
|
369
|
+
const staticGeoms = [];
|
|
370
|
+
const dynamicGeoms = [];
|
|
371
|
+
const emissiveTris = [];
|
|
372
|
+
let dynVertexOffset = 0;
|
|
373
|
+
const tmpGeoms = []; // to dispose after merge
|
|
374
|
+
|
|
375
|
+
scene.traverse((obj) => {
|
|
376
|
+
if (!obj.isMesh || !obj.geometry || !obj.visible || obj.userData.rtExclude) return;
|
|
377
|
+
const mat = Array.isArray(obj.material) ? obj.material[0] : obj.material;
|
|
378
|
+
// Transparent surfaces must not act as opaque occluders — e.g.
|
|
379
|
+
// LittlestTokyo's glass display case (texture-alpha, opacity 1) would put
|
|
380
|
+
// the whole model in shadow. Alpha-textured glass can't be cheaply tested,
|
|
381
|
+
// so ANY transparent material is skipped like rtExclude (still
|
|
382
|
+
// rasterized). alphaTest cut-outs (transparent: false) stay occluders.
|
|
383
|
+
if (mat.transparent) return;
|
|
384
|
+
let mi = materials.indexOf(mat);
|
|
385
|
+
if (mi < 0) { mi = materials.length; materials.push(mat); }
|
|
386
|
+
|
|
387
|
+
const extracted = extractMeshGeometry(obj, mi);
|
|
388
|
+
tmpGeoms.push(extracted.geo);
|
|
389
|
+
// Static emissive meshes become NEE area lights (sampled directly with
|
|
390
|
+
// shadow rays instead of waiting for a GI ray to stumble into them).
|
|
391
|
+
// Dynamic emitters are left out — their world-space triangles would go
|
|
392
|
+
// stale — so they keep lighting the old way, via GI-ray hits.
|
|
393
|
+
const isDynamic = dynamicSet && dynamicSet.has(obj);
|
|
394
|
+
if (!isDynamic) {
|
|
395
|
+
const emit = emissiveColor(mat);
|
|
396
|
+
if (emit) collectEmissiveTriangles(extracted.geo, emit, emissiveTris);
|
|
397
|
+
}
|
|
398
|
+
if (isDynamic) {
|
|
399
|
+
dynamicGeoms.push(extracted.geo);
|
|
400
|
+
// Opt-in CPU deformation: the mesh must be BOTH in dynamicMeshes AND carry
|
|
401
|
+
// userData.rtDeforming, and its live geometry is read every frame.
|
|
402
|
+
const deforming = obj.userData.rtDeforming === true;
|
|
403
|
+
if (deforming) compiled.hasDeforming = true;
|
|
404
|
+
compiled.dynamic.push({
|
|
405
|
+
mesh: obj,
|
|
406
|
+
start: dynVertexOffset,
|
|
407
|
+
count: extracted.count,
|
|
408
|
+
localPos: extracted.localPos,
|
|
409
|
+
localNorm: extracted.localNorm,
|
|
410
|
+
deforming,
|
|
411
|
+
liveGeometry: deforming ? obj.geometry : null,
|
|
412
|
+
indexMap: deforming ? extracted.indexMap : null,
|
|
413
|
+
srcVertexCount: deforming ? extracted.srcVertexCount : 0,
|
|
414
|
+
});
|
|
415
|
+
dynVertexOffset += extracted.count;
|
|
416
|
+
} else {
|
|
417
|
+
staticGeoms.push(extracted.geo);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
if (staticGeoms.length === 0 && dynamicGeoms.length === 0) {
|
|
422
|
+
throw new Error("three-realtime-rt: no meshes found in scene");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Static level.
|
|
426
|
+
const s = buildLevel(staticGeoms, { dynamic: false });
|
|
427
|
+
compiled.staticBvh = s.bvh;
|
|
428
|
+
compiled.staticBvhUniform.updateFrom(s.bvh);
|
|
429
|
+
compiled.staticAttrTex.updateFrom(s.attr);
|
|
430
|
+
|
|
431
|
+
// Dynamic level.
|
|
432
|
+
compiled.hasDynamic = dynamicGeoms.length > 0;
|
|
433
|
+
const d = buildLevel(dynamicGeoms, { dynamic: true });
|
|
434
|
+
compiled.dynamicMerged = d.merged;
|
|
435
|
+
compiled.dynamicBvh = d.bvh;
|
|
436
|
+
compiled.dynamicBvhUniform.updateFrom(d.bvh);
|
|
437
|
+
compiled.dynamicPacked = d.packed;
|
|
438
|
+
compiled.dynamicPackedAttr = d.attr;
|
|
439
|
+
compiled.dynamicAttrTex.updateFrom(d.attr);
|
|
440
|
+
|
|
441
|
+
compiled.triangleCount =
|
|
442
|
+
(s.merged.getAttribute("position").count +
|
|
443
|
+
(compiled.hasDynamic ? d.merged.getAttribute("position").count : 0)) / 3;
|
|
444
|
+
|
|
445
|
+
// World-space extent of the static level. Used to auto-scale the ray offset
|
|
446
|
+
// epsilon: dense/large scenes (e.g. a detailed diorama normalized to a few
|
|
447
|
+
// units) need a bigger offset or every shadow ray self-intersects nearby
|
|
448
|
+
// micro-geometry and the scene renders black.
|
|
449
|
+
s.merged.computeBoundingBox();
|
|
450
|
+
const bb = s.merged.boundingBox;
|
|
451
|
+
compiled.sceneDiagonal = bb.isEmpty() ? 1 : bb.min.distanceTo(bb.max);
|
|
452
|
+
|
|
453
|
+
if (emissiveTris.length > MAX_EMISSIVE_TRIS) {
|
|
454
|
+
console.warn(
|
|
455
|
+
`three-realtime-rt: ${emissiveTris.length} emissive triangles exceed the ` +
|
|
456
|
+
`NEE cap of ${MAX_EMISSIVE_TRIS}; keeping the largest by area. Dropped ` +
|
|
457
|
+
`triangles no longer act as lights — prefer low-poly emitter meshes.`
|
|
458
|
+
);
|
|
459
|
+
emissiveTris.sort((a, b) => b.area - a.area);
|
|
460
|
+
emissiveTris.length = MAX_EMISSIVE_TRIS;
|
|
461
|
+
}
|
|
462
|
+
compiled.emissiveTriCount = emissiveTris.length;
|
|
463
|
+
compiled.materialsTex = buildSceneDataTexture(materials, emissiveTris);
|
|
464
|
+
syncLights(scene, compiled);
|
|
465
|
+
|
|
466
|
+
// Static merged geometry is owned by its BVH (disposed with it); dynamic
|
|
467
|
+
// merged geometry is kept live for re-baking. Dispose the per-mesh temporaries
|
|
468
|
+
// that aren't the merged buffers.
|
|
469
|
+
for (const g of tmpGeoms) {
|
|
470
|
+
if (g !== s.merged && g !== d.merged) g.dispose();
|
|
471
|
+
}
|
|
472
|
+
return compiled;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** (Re)scan the scene's lights into the compiled light tables. Cheap; call anytime. */
|
|
476
|
+
export function syncLights(scene, compiled) {
|
|
477
|
+
const posType = compiled.lightPosType;
|
|
478
|
+
const colorRadius = compiled.lightColorRadius;
|
|
479
|
+
const dirCone = compiled.lightDirCone;
|
|
480
|
+
posType.length = 0;
|
|
481
|
+
colorRadius.length = 0;
|
|
482
|
+
dirCone.length = 0;
|
|
483
|
+
let count = 0;
|
|
484
|
+
const tmpP = new THREE.Vector3();
|
|
485
|
+
const tmpT = new THREE.Vector3();
|
|
486
|
+
|
|
487
|
+
scene.traverse((obj) => {
|
|
488
|
+
if (!obj.isLight || !obj.visible || obj.intensity <= 0) return;
|
|
489
|
+
if (count >= MAX_LIGHTS) return;
|
|
490
|
+
if (obj.isSpotLight) {
|
|
491
|
+
// posType.w encodes type AND the inner-cone cosine: w = 2 + cosInner
|
|
492
|
+
// (any w >= 1.5 is a spot). Direction + outer cosine live in dirCone.
|
|
493
|
+
obj.getWorldPosition(tmpP);
|
|
494
|
+
obj.target.getWorldPosition(tmpT);
|
|
495
|
+
const dir = tmpT.sub(tmpP).normalize();
|
|
496
|
+
const cosOuter = Math.cos(obj.angle);
|
|
497
|
+
const cosInner = Math.cos(obj.angle * (1 - (obj.penumbra ?? 0)));
|
|
498
|
+
posType.push(tmpP.x, tmpP.y, tmpP.z, 2 + cosInner);
|
|
499
|
+
colorRadius.push(
|
|
500
|
+
obj.color.r * obj.intensity,
|
|
501
|
+
obj.color.g * obj.intensity,
|
|
502
|
+
obj.color.b * obj.intensity,
|
|
503
|
+
obj.userData.rtRadius ?? 0.1
|
|
504
|
+
);
|
|
505
|
+
dirCone.push(dir.x, dir.y, dir.z, cosOuter);
|
|
506
|
+
count++;
|
|
507
|
+
} else if (obj.isPointLight) {
|
|
508
|
+
obj.getWorldPosition(tmpP);
|
|
509
|
+
posType.push(tmpP.x, tmpP.y, tmpP.z, 0);
|
|
510
|
+
colorRadius.push(
|
|
511
|
+
obj.color.r * obj.intensity,
|
|
512
|
+
obj.color.g * obj.intensity,
|
|
513
|
+
obj.color.b * obj.intensity,
|
|
514
|
+
obj.userData.rtRadius ?? 0.15
|
|
515
|
+
);
|
|
516
|
+
dirCone.push(0, 0, 0, 0);
|
|
517
|
+
count++;
|
|
518
|
+
} else if (obj.isDirectionalLight) {
|
|
519
|
+
obj.getWorldPosition(tmpP);
|
|
520
|
+
obj.target.getWorldPosition(tmpT);
|
|
521
|
+
const dir = tmpT.sub(tmpP).normalize();
|
|
522
|
+
posType.push(dir.x, dir.y, dir.z, 1);
|
|
523
|
+
colorRadius.push(
|
|
524
|
+
obj.color.r * obj.intensity,
|
|
525
|
+
obj.color.g * obj.intensity,
|
|
526
|
+
obj.color.b * obj.intensity,
|
|
527
|
+
obj.userData.rtRadius ?? 0.02
|
|
528
|
+
);
|
|
529
|
+
dirCone.push(0, 0, 0, 0);
|
|
530
|
+
count++;
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
compiled.lightCount = count;
|
|
535
|
+
while (posType.length < MAX_LIGHTS * 4) {
|
|
536
|
+
posType.push(0, 0, 0, 0);
|
|
537
|
+
colorRadius.push(0, 0, 0, 0);
|
|
538
|
+
dirCone.push(0, 0, 0, 0);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export { MAX_LIGHTS };
|