@tinypoly/gpu-renderer 0.2.4 → 0.3.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/ARCHITECTURE.md +48 -10
- package/README.md +2 -0
- package/dist/accumulator.d.ts.map +1 -1
- package/dist/accumulator.js +1 -0
- package/dist/accumulator.js.map +1 -1
- package/dist/denoise.js +2 -2
- package/dist/denoise.js.map +1 -1
- package/dist/device.d.ts.map +1 -1
- package/dist/device.js +9 -0
- package/dist/device.js.map +1 -1
- package/dist/frameLayout.d.ts.map +1 -1
- package/dist/frameLayout.js +5 -0
- package/dist/frameLayout.js.map +1 -1
- package/dist/geometry.d.ts +48 -8
- package/dist/geometry.d.ts.map +1 -1
- package/dist/geometry.js +409 -134
- package/dist/geometry.js.map +1 -1
- package/dist/grass.d.ts.map +1 -1
- package/dist/grass.js +2 -1
- package/dist/grass.js.map +1 -1
- package/dist/imageResolve.js +2 -2
- package/dist/imageResolve.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/instancing.d.ts +100 -0
- package/dist/instancing.d.ts.map +1 -0
- package/dist/instancing.js +286 -0
- package/dist/instancing.js.map +1 -0
- package/dist/lensPass.d.ts +2 -0
- package/dist/lensPass.d.ts.map +1 -1
- package/dist/lensPass.js +7 -4
- package/dist/lensPass.js.map +1 -1
- package/dist/lights.d.ts.map +1 -1
- package/dist/lights.js +8 -5
- package/dist/lights.js.map +1 -1
- package/dist/memoryProbe.d.ts +76 -0
- package/dist/memoryProbe.d.ts.map +1 -0
- package/dist/memoryProbe.js +197 -0
- package/dist/memoryProbe.js.map +1 -0
- package/dist/oidnPass.d.ts +6 -0
- package/dist/oidnPass.d.ts.map +1 -1
- package/dist/oidnPass.js +18 -1
- package/dist/oidnPass.js.map +1 -1
- package/dist/pathTracePass.d.ts +15 -2
- package/dist/pathTracePass.d.ts.map +1 -1
- package/dist/pathTracePass.js +38 -16
- package/dist/pathTracePass.js.map +1 -1
- package/dist/renderer.d.ts +13 -0
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +248 -41
- package/dist/renderer.js.map +1 -1
- package/dist/sceneUpload.d.ts +26 -8
- package/dist/sceneUpload.d.ts.map +1 -1
- package/dist/sceneUpload.js +97 -27
- package/dist/sceneUpload.js.map +1 -1
- package/dist/shaderModule.d.ts.map +1 -1
- package/dist/shaderModule.js +2 -1
- package/dist/shaderModule.js.map +1 -1
- package/dist/skyBake.d.ts.map +1 -1
- package/dist/skyBake.js +5 -3
- package/dist/skyBake.js.map +1 -1
- package/dist/texturePack.d.ts +43 -3
- package/dist/texturePack.d.ts.map +1 -1
- package/dist/texturePack.js +209 -37
- package/dist/texturePack.js.map +1 -1
- package/dist/wgsl/bvh.wgsl.d.ts +5 -1
- package/dist/wgsl/bvh.wgsl.d.ts.map +1 -1
- package/dist/wgsl/bvh.wgsl.js +105 -15
- package/dist/wgsl/bvh.wgsl.js.map +1 -1
- package/dist/wgsl/common.wgsl.d.ts.map +1 -1
- package/dist/wgsl/common.wgsl.js +92 -11
- package/dist/wgsl/common.wgsl.js.map +1 -1
- package/dist/wgsl/lens.wgsl.d.ts +1 -1
- package/dist/wgsl/lens.wgsl.d.ts.map +1 -1
- package/dist/wgsl/lens.wgsl.js +14 -2
- package/dist/wgsl/lens.wgsl.js.map +1 -1
- package/dist/wgsl/surface.wgsl.d.ts +1 -1
- package/dist/wgsl/surface.wgsl.d.ts.map +1 -1
- package/dist/wgsl/surface.wgsl.js +24 -14
- package/dist/wgsl/surface.wgsl.js.map +1 -1
- package/dist/wgsl/texture.wgsl.d.ts +1 -1
- package/dist/wgsl/texture.wgsl.d.ts.map +1 -1
- package/dist/wgsl/texture.wgsl.js +11 -0
- package/dist/wgsl/texture.wgsl.js.map +1 -1
- package/package.json +1 -1
- package/src/accumulator.ts +1 -0
- package/src/denoise.ts +2 -2
- package/src/device.ts +12 -0
- package/src/frameLayout.ts +5 -0
- package/src/geometry.ts +527 -133
- package/src/grass.ts +2 -1
- package/src/imageResolve.ts +2 -2
- package/src/index.ts +1 -0
- package/src/instancing.ts +415 -0
- package/src/lensPass.ts +9 -4
- package/src/lights.ts +9 -5
- package/src/memoryProbe.ts +275 -0
- package/src/oidnPass.ts +21 -1
- package/src/pathTracePass.ts +42 -18
- package/src/renderer.ts +308 -47
- package/src/sceneUpload.ts +139 -36
- package/src/shaderModule.ts +2 -1
- package/src/skyBake.ts +5 -3
- package/src/texturePack.ts +274 -43
- package/src/wgsl/bvh.wgsl.ts +105 -15
- package/src/wgsl/common.wgsl.ts +92 -11
- package/src/wgsl/lens.wgsl.ts +14 -2
- package/src/wgsl/surface.wgsl.ts +24 -14
- package/src/wgsl/texture.wgsl.ts +11 -0
package/dist/geometry.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { clipToWorld, transform4, worldToClip } from "./camera.js";
|
|
2
|
-
import { isMaterialVisible, MATERIAL_KIND, MATERIAL_UNLIT_LIGHT, materialProperties, NO_PLACEMENT, NO_PROGRAM, } from "./materialLayout.js";
|
|
2
|
+
import { isMaterialVisible, MATERIAL_KIND, MATERIAL_UNLIT_LIGHT, MATERIAL_VOLUME, materialProperties, NO_PLACEMENT, NO_PROGRAM, } from "./materialLayout.js";
|
|
3
|
+
import { geometryKey, INSTANCE_MIN_TRIANGLES, instanceable, transformBox, trianglesBox, } from "./instancing.js";
|
|
3
4
|
import { normalMatrix, transformNormal, transformPoint } from "./math.js";
|
|
4
5
|
import { matrixValue, sampleTexture } from "./upstream.js";
|
|
5
6
|
/** Three's wireframe and line width: WebGL draws every line one pixel wide whatever the material asks. */
|
|
@@ -7,6 +8,27 @@ const LINE_WIDTH = 1;
|
|
|
7
8
|
/** A dashed line split finer than this is drawn solid rather than as millions of dashes. */
|
|
8
9
|
const MAX_DASHES = 4096;
|
|
9
10
|
/** Grows a float array as triangles are added, since how many a primitive makes is only known as it is built. */
|
|
11
|
+
class UintStream {
|
|
12
|
+
data;
|
|
13
|
+
length = 0;
|
|
14
|
+
constructor(capacity) {
|
|
15
|
+
this.data = new Uint32Array(Math.max(16, capacity));
|
|
16
|
+
}
|
|
17
|
+
reserve(count) {
|
|
18
|
+
if (this.length + count <= this.data.length)
|
|
19
|
+
return;
|
|
20
|
+
const next = new Uint32Array(Math.max(this.data.length * 2, this.length + count));
|
|
21
|
+
next.set(this.data.subarray(0, this.length));
|
|
22
|
+
this.data = next;
|
|
23
|
+
}
|
|
24
|
+
push(value) {
|
|
25
|
+
this.reserve(1);
|
|
26
|
+
this.data[this.length++] = value;
|
|
27
|
+
}
|
|
28
|
+
view() {
|
|
29
|
+
return this.data.subarray(0, this.length);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
10
32
|
class FloatStream {
|
|
11
33
|
data;
|
|
12
34
|
length = 0;
|
|
@@ -100,15 +122,53 @@ function flatten(value) {
|
|
|
100
122
|
return [...value.matrix];
|
|
101
123
|
return null;
|
|
102
124
|
}
|
|
125
|
+
/** Triangles one slice of packing covers before it may hand the thread back. */
|
|
126
|
+
const SLICE_TRIANGLES = 32768;
|
|
127
|
+
const IDENTITY = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
128
|
+
/** A mesh's groups, or the one group covering it when it declares none. */
|
|
129
|
+
function groupsOf(mesh) {
|
|
130
|
+
return mesh.groups.length
|
|
131
|
+
? mesh.groups
|
|
132
|
+
: [{
|
|
133
|
+
start: 0,
|
|
134
|
+
count: mesh.index ? mesh.index.length : mesh.attributes.position.data.length / 3,
|
|
135
|
+
materialIndex: 0,
|
|
136
|
+
}];
|
|
137
|
+
}
|
|
103
138
|
/**
|
|
104
139
|
* Flattens the scene into triangles, materials resolved and transforms applied. Every triangle carries its own three
|
|
105
|
-
*
|
|
106
|
-
*
|
|
140
|
+
* positions: the traversal reads them straight off the buffer, and sharing them would cost an indirection on the
|
|
141
|
+
* hottest path of the kernel. What shading reads (normals, UVs, colours) is kept per vertex instead, since it is read
|
|
142
|
+
* once a hit, and a mesh that repeats becomes instances of one prototype (see `instancing.ts`).
|
|
107
143
|
*
|
|
108
144
|
* Points, lines and sprites become triangles that face the camera, sized the way Three draws them, so a path has a
|
|
109
145
|
* surface to find; vertex shaders run here, on the CPU renderer's interpreter, and their varyings ride along per corner.
|
|
110
146
|
*/
|
|
111
147
|
export function packGeometry(scene, options = {}) {
|
|
148
|
+
const steps = packingSteps(scene, options);
|
|
149
|
+
let step = steps.next();
|
|
150
|
+
while (!step.done)
|
|
151
|
+
step = steps.next();
|
|
152
|
+
return step.value;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The same packing, handing the thread back between slices: `pause` is awaited whenever the work has held the thread
|
|
156
|
+
* for `sliceMs`, so a large scene does not freeze the page while it packs.
|
|
157
|
+
*/
|
|
158
|
+
export async function packGeometrySliced(scene, options, pause, sliceMs = 12) {
|
|
159
|
+
const steps = packingSteps(scene, options);
|
|
160
|
+
let started = performance.now();
|
|
161
|
+
let step = steps.next();
|
|
162
|
+
while (!step.done) {
|
|
163
|
+
if (performance.now() - started >= sliceMs) {
|
|
164
|
+
await pause();
|
|
165
|
+
started = performance.now();
|
|
166
|
+
}
|
|
167
|
+
step = steps.next();
|
|
168
|
+
}
|
|
169
|
+
return step.value;
|
|
170
|
+
}
|
|
171
|
+
function* packingSteps(scene, options) {
|
|
112
172
|
const settings = options instanceof Map ? { textures: options } : options;
|
|
113
173
|
const textures = settings.textures ?? new Map();
|
|
114
174
|
const textureData = settings.textureData ?? new Map();
|
|
@@ -118,18 +178,38 @@ export function packGeometry(scene, options = {}) {
|
|
|
118
178
|
const programs = settings.programs;
|
|
119
179
|
const extraBase = settings.extraBase ?? 0;
|
|
120
180
|
const meshes = scene.meshes.filter(mesh => mesh.attributes.position);
|
|
121
|
-
const
|
|
181
|
+
const repeated = settings.instancing === false ? new Map()
|
|
182
|
+
: repeatedGeometry(meshes, material => plainMaterial(material, textures, programs));
|
|
183
|
+
const estimate = meshes.reduce((count, mesh) => count + (mesh.primitive || repeated.has(mesh) ? 0 : triangleCountOf(mesh)), 0);
|
|
184
|
+
const vertexEstimate = meshes.reduce((count, mesh) => count + (mesh.primitive || repeated.has(mesh) ? 0 : mesh.attributes.position.data.length / 3), 0);
|
|
122
185
|
const anyVertexColors = meshes.some(mesh => mesh.attributes.color && mesh.materials.some(m => m?.values?.vertexColors === true));
|
|
123
186
|
const positions = new FloatStream(estimate * 9);
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
const
|
|
187
|
+
const corners = new UintStream(estimate * 3);
|
|
188
|
+
const normals = new FloatStream(vertexEstimate * 3);
|
|
189
|
+
const uvs = new FloatStream(vertexEstimate * 4);
|
|
190
|
+
const colors = new FloatStream(anyVertexColors ? vertexEstimate * 4 : 0);
|
|
191
|
+
let vertexCount = 0;
|
|
127
192
|
const varyings = new FloatStream(0);
|
|
128
|
-
const triangleMaterials =
|
|
193
|
+
const triangleMaterials = new UintStream(estimate);
|
|
129
194
|
const entries = [];
|
|
130
195
|
const shared = new Map();
|
|
131
196
|
const extra = [];
|
|
132
197
|
let cameraDependent = false;
|
|
198
|
+
// Repeated geometry: each prototype's triangles once, in object space, with the group each one came from.
|
|
199
|
+
const prototypePositions = new FloatStream(0);
|
|
200
|
+
const prototypeCorners = new UintStream(0);
|
|
201
|
+
const prototypeGroups = new UintStream(0);
|
|
202
|
+
const prototypes = [];
|
|
203
|
+
const prototypeOf = new Map();
|
|
204
|
+
const instances = [];
|
|
205
|
+
const instanceMaterials = new UintStream(0);
|
|
206
|
+
let instanced = 0;
|
|
207
|
+
const flatSink = { positions, corners, tags: triangleMaterials };
|
|
208
|
+
const prototypeSink = {
|
|
209
|
+
positions: prototypePositions,
|
|
210
|
+
corners: prototypeCorners,
|
|
211
|
+
tags: prototypeGroups,
|
|
212
|
+
};
|
|
133
213
|
/** A material's clipping planes, written once to the extra words. */
|
|
134
214
|
const clipPlanes = new Map();
|
|
135
215
|
const planesOf = (material) => {
|
|
@@ -199,21 +279,40 @@ export function packGeometry(scene, options = {}) {
|
|
|
199
279
|
shared.set(material, entry);
|
|
200
280
|
return entry;
|
|
201
281
|
};
|
|
202
|
-
|
|
282
|
+
/** A vertex of its own, for everything a hit reads per vertex; its number. */
|
|
283
|
+
const emitVertex = (normal, n, uv, u, color, c) => {
|
|
284
|
+
normals.reserve(3);
|
|
285
|
+
normals.data[normals.length++] = normal[n];
|
|
286
|
+
normals.data[normals.length++] = normal[n + 1];
|
|
287
|
+
normals.data[normals.length++] = normal[n + 2];
|
|
288
|
+
uvs.reserve(4);
|
|
289
|
+
uvs.data[uvs.length++] = uv[u];
|
|
290
|
+
uvs.data[uvs.length++] = uv[u + 1];
|
|
291
|
+
uvs.data[uvs.length++] = uv[u + 2];
|
|
292
|
+
uvs.data[uvs.length++] = uv[u + 3];
|
|
293
|
+
if (anyVertexColors) {
|
|
294
|
+
colors.reserve(4);
|
|
295
|
+
colors.data[colors.length++] = color[c];
|
|
296
|
+
colors.data[colors.length++] = color[c + 1];
|
|
297
|
+
colors.data[colors.length++] = color[c + 2];
|
|
298
|
+
colors.data[colors.length++] = color[c + 3];
|
|
299
|
+
}
|
|
300
|
+
return vertexCount++;
|
|
301
|
+
};
|
|
302
|
+
const writeTriangle = (triangle, entry) => {
|
|
203
303
|
// Geometry without normals is shaded flat: the three corners share the face normal.
|
|
204
|
-
const flat =
|
|
205
|
-
? faceNormal(
|
|
304
|
+
const flat = triangle.some(corner => corner.normal.length < 3)
|
|
305
|
+
? faceNormal(triangle[0].position, triangle[1].position, triangle[2].position)
|
|
206
306
|
: null;
|
|
207
|
-
for (const corner of
|
|
307
|
+
for (const corner of triangle) {
|
|
208
308
|
positions.push(corner.position, 3);
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
colors.push(corner.color, 4);
|
|
309
|
+
const color = corner.color;
|
|
310
|
+
// A missing component reads as zero, as the stream it replaces padded it.
|
|
311
|
+
corners.push(emitVertex(flat ?? corner.normal, 0, [corner.uv0[0], corner.uv0[1], corner.uv1[0], corner.uv1[1]], 0, [color[0] ?? 0, color[1] ?? 0, color[2] ?? 0, color[3] ?? 0], 0));
|
|
213
312
|
}
|
|
214
313
|
const stride = entry.placement.varyingStride;
|
|
215
314
|
if (stride > 0)
|
|
216
|
-
for (const corner of
|
|
315
|
+
for (const corner of triangle) {
|
|
217
316
|
if (corner.varyings)
|
|
218
317
|
varyings.push(corner.varyings, stride);
|
|
219
318
|
else
|
|
@@ -221,152 +320,211 @@ export function packGeometry(scene, options = {}) {
|
|
|
221
320
|
}
|
|
222
321
|
triangleMaterials.push(entry.index);
|
|
223
322
|
};
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
plain = plainVertices(mesh);
|
|
229
|
-
const { world, normal, hasNormal, uv, color, valid } = plain;
|
|
230
|
-
const index = mesh.index;
|
|
231
|
-
const stride = entry.placement.varyingStride;
|
|
232
|
-
const corners = [0, 0, 0];
|
|
323
|
+
const writePlainTriangles = (vertices, start, end, mirrored, stride, sink, tag) => {
|
|
324
|
+
const { world, normal, hasNormal, uv, color, valid, ids } = vertices;
|
|
325
|
+
const index = vertices.mesh.index;
|
|
326
|
+
const triangle = [0, 0, 0];
|
|
233
327
|
for (let v = start; v + 2 < end; v += 3) {
|
|
234
328
|
const a = index ? index[v] : v, b = index ? index[v + 1] : v + 1, c = index ? index[v + 2] : v + 2;
|
|
235
329
|
if (!valid(a) || !valid(b) || !valid(c))
|
|
236
330
|
continue;
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
// Geometry without normals is shaded flat: the three corners share the face normal.
|
|
331
|
+
triangle[0] = a;
|
|
332
|
+
triangle[1] = mirrored ? c : b;
|
|
333
|
+
triangle[2] = mirrored ? b : c;
|
|
334
|
+
// Geometry without normals is shaded flat: the three corners share the face normal, so a vertex of their own.
|
|
241
335
|
const flat = !hasNormal[a] || !hasNormal[b] || !hasNormal[c]
|
|
242
|
-
? faceNormal(Array.from(world.subarray(
|
|
336
|
+
? faceNormal(Array.from(world.subarray(triangle[0] * 3, triangle[0] * 3 + 3)), Array.from(world.subarray(triangle[1] * 3, triangle[1] * 3 + 3)), Array.from(world.subarray(triangle[2] * 3, triangle[2] * 3 + 3)))
|
|
243
337
|
: null;
|
|
244
|
-
positions.reserve(9);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
positions.data[positions.length++] = world[p + 1];
|
|
253
|
-
positions.data[positions.length++] = world[p + 2];
|
|
338
|
+
sink.positions.reserve(9);
|
|
339
|
+
sink.corners.reserve(3);
|
|
340
|
+
for (const corner of triangle) {
|
|
341
|
+
const p = corner * 3;
|
|
342
|
+
sink.positions.data[sink.positions.length++] = world[p];
|
|
343
|
+
sink.positions.data[sink.positions.length++] = world[p + 1];
|
|
344
|
+
sink.positions.data[sink.positions.length++] = world[p + 2];
|
|
345
|
+
let id;
|
|
254
346
|
if (flat) {
|
|
255
|
-
|
|
256
|
-
normals.data[normals.length++] = flat[1];
|
|
257
|
-
normals.data[normals.length++] = flat[2];
|
|
347
|
+
id = emitVertex(flat, 0, uv, corner * 4, color, corner * 4);
|
|
258
348
|
}
|
|
259
349
|
else {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
uvs.data[uvs.length++] = uv[q];
|
|
265
|
-
uvs.data[uvs.length++] = uv[q + 1];
|
|
266
|
-
uvs.data[uvs.length++] = uv[q + 2];
|
|
267
|
-
uvs.data[uvs.length++] = uv[q + 3];
|
|
268
|
-
if (anyVertexColors) {
|
|
269
|
-
colors.data[colors.length++] = color[q];
|
|
270
|
-
colors.data[colors.length++] = color[q + 1];
|
|
271
|
-
colors.data[colors.length++] = color[q + 2];
|
|
272
|
-
colors.data[colors.length++] = color[q + 3];
|
|
350
|
+
id = ids[corner];
|
|
351
|
+
if (id < 0)
|
|
352
|
+
id = ids[corner] = emitVertex(normal, p, uv, corner * 4, color, corner * 4);
|
|
273
353
|
}
|
|
354
|
+
sink.corners.data[sink.corners.length++] = id;
|
|
274
355
|
}
|
|
275
356
|
if (stride > 0)
|
|
276
357
|
varyings.pad(3 * stride);
|
|
277
|
-
|
|
358
|
+
sink.tags.push(tag);
|
|
278
359
|
}
|
|
279
360
|
};
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
for (
|
|
291
|
-
|
|
292
|
-
if (
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
361
|
+
// Time spent on the mesh at hand, without the pauses between slices.
|
|
362
|
+
let meshMs = 0;
|
|
363
|
+
let segment = performance.now();
|
|
364
|
+
const pause = function* () {
|
|
365
|
+
meshMs += performance.now() - segment;
|
|
366
|
+
yield;
|
|
367
|
+
segment = performance.now();
|
|
368
|
+
};
|
|
369
|
+
/** A plain run of triangles, sliced so the page is not held for the whole of a large mesh. */
|
|
370
|
+
const plainRun = function* (vertices, start, end, mirrored, stride, sink, tag) {
|
|
371
|
+
for (let from = start; from < end; from += SLICE_TRIANGLES * 3) {
|
|
372
|
+
writePlainTriangles(vertices, from, Math.min(end, from + SLICE_TRIANGLES * 3), mirrored, stride, sink, tag);
|
|
373
|
+
if (from + SLICE_TRIANGLES * 3 < end)
|
|
374
|
+
yield* pause();
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
for (let meshIndex = 0; meshIndex < meshes.length; meshIndex++) {
|
|
378
|
+
const mesh = meshes[meshIndex];
|
|
379
|
+
const groups = groupsOf(mesh);
|
|
380
|
+
const key = repeated.get(mesh);
|
|
381
|
+
const before = triangleMaterials.length + instanced;
|
|
382
|
+
let path = "plain";
|
|
383
|
+
meshMs = 0;
|
|
384
|
+
segment = performance.now();
|
|
385
|
+
if (key !== undefined) {
|
|
386
|
+
let prototype = prototypeOf.get(key);
|
|
387
|
+
// The first copy lays the prototype down, in object space; the next ones only point at it.
|
|
388
|
+
if (prototype === undefined) {
|
|
389
|
+
const first = prototypeGroups.length;
|
|
390
|
+
const vertices = plainVertices(mesh, IDENTITY);
|
|
391
|
+
for (let g = 0; g < groups.length; g++) {
|
|
392
|
+
const group = groups[g];
|
|
393
|
+
const material = mesh.materials[group.materialIndex] ?? mesh.materials[0];
|
|
394
|
+
if (!isMaterialVisible(material))
|
|
395
|
+
continue;
|
|
396
|
+
const count = mesh.index ? mesh.index.length : mesh.attributes.position.data.length / 3;
|
|
397
|
+
yield* plainRun(vertices, Math.max(0, group.start), Math.min(group.start + group.count, count), false, 0, prototypeSink, g);
|
|
398
|
+
}
|
|
399
|
+
prototype = prototypes.length;
|
|
400
|
+
prototypes.push({ first, count: prototypeGroups.length - first });
|
|
401
|
+
prototypeOf.set(key, prototype);
|
|
402
|
+
path = "prototype";
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
path = "instance";
|
|
406
|
+
}
|
|
407
|
+
const { first, count } = prototypes[prototype];
|
|
408
|
+
if (count > 0) {
|
|
409
|
+
const entryOfGroup = groups.map(group => {
|
|
410
|
+
const material = mesh.materials[group.materialIndex] ?? mesh.materials[0];
|
|
411
|
+
return isMaterialVisible(material) ? entryFor(material, mesh).index : 0;
|
|
412
|
+
});
|
|
413
|
+
instances.push({ prototype, matrix: mesh.matrixWorld, firstTriangle: instanced });
|
|
414
|
+
instanceMaterials.reserve(count);
|
|
415
|
+
for (let i = 0; i < count; i++)
|
|
416
|
+
instanceMaterials.data[instanceMaterials.length++] = entryOfGroup[prototypeGroups.data[first + i]];
|
|
417
|
+
instanced += count;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
if (mesh.primitive)
|
|
297
422
|
cameraDependent = true;
|
|
298
|
-
//
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
423
|
+
// Every vertex of a plain mesh, transformed once and shared by its groups.
|
|
424
|
+
let plain = null;
|
|
425
|
+
for (const group of groups) {
|
|
426
|
+
const material = mesh.materials[group.materialIndex] ?? mesh.materials[0];
|
|
427
|
+
if (!isMaterialVisible(material))
|
|
428
|
+
continue;
|
|
429
|
+
const entry = entryFor(material, mesh);
|
|
430
|
+
const vertexShader = material ? programs?.vertex(material) ?? null : null;
|
|
431
|
+
if (vertexShader)
|
|
432
|
+
cameraDependent = true;
|
|
433
|
+
// Primitives that face the camera need one to face; without it there is nothing sensible to build.
|
|
434
|
+
if (mesh.primitive && !camera)
|
|
435
|
+
continue;
|
|
436
|
+
const context = { camera, width, height, textureData, vertexShader, programs, fog: settings.fog };
|
|
437
|
+
const count = mesh.index ? mesh.index.length : mesh.attributes.position.data.length / 3;
|
|
438
|
+
const start = Math.max(0, group.start);
|
|
439
|
+
const end = Math.min(group.start + group.count, count);
|
|
440
|
+
const at = (offset) => (mesh.index ? mesh.index[offset] : offset);
|
|
441
|
+
if (!mesh.primitive) {
|
|
442
|
+
// A mirroring transform turns the winding around; Three flips its front face to match, and so does this.
|
|
443
|
+
const mirrored = determinant3(mesh.matrixWorld) < 0;
|
|
444
|
+
// The common case, a plain mesh, skips the general reader: each vertex is transformed once, straight into
|
|
445
|
+
// typed arrays, and each triangle is copied from them. What it writes is exactly what the reader would.
|
|
446
|
+
if (!vertexShader && !material?.maps?.displacementMap) {
|
|
447
|
+
plain ??= plainVertices(mesh);
|
|
448
|
+
yield* plainRun(plain, start, end, mirrored, entry.placement.varyingStride, flatSink, entry.index);
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
path = "general";
|
|
452
|
+
// Instanced geometry drawn by a shader (Line2, particles built in a vertex shader): the template runs once
|
|
453
|
+
// per instance, reading the per-instance attributes at that instance.
|
|
454
|
+
const instancing = vertexShader ? instancedAttributes(mesh) : null;
|
|
455
|
+
for (let instance = 0; instance < (instancing?.count ?? 1); instance++) {
|
|
456
|
+
const reader = new VertexReader(mesh, material, context, instancing?.names, instance);
|
|
457
|
+
for (let v = start; v + 2 < end; v += 3) {
|
|
458
|
+
const a = reader.corner(at(v)), b = reader.corner(at(v + 1)), c = reader.corner(at(v + 2));
|
|
459
|
+
if (a && b && c)
|
|
460
|
+
writeTriangle(mirrored ? [a, c, b] : [a, b, c], entry);
|
|
461
|
+
if ((v - start) % (SLICE_TRIANGLES * 3) === 0 && v > start)
|
|
462
|
+
yield* pause();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
317
465
|
continue;
|
|
318
466
|
}
|
|
319
|
-
|
|
320
|
-
|
|
467
|
+
path = "general";
|
|
468
|
+
const vertices = new VertexReader(mesh, material, context);
|
|
469
|
+
if (mesh.primitive === "points") {
|
|
470
|
+
for (let v = start; v < end; v++)
|
|
471
|
+
for (const triangle of vertices.point(at(v)))
|
|
472
|
+
writeTriangle(triangle, entry);
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (mesh.primitive === "sprite") {
|
|
321
476
|
for (let v = start; v + 2 < end; v += 3) {
|
|
322
|
-
const a =
|
|
323
|
-
if (
|
|
324
|
-
|
|
325
|
-
writeTriangle(mirrored ? [a, c, b] : [a, b, c], entry);
|
|
477
|
+
const a = vertices.corner(at(v)), b = vertices.corner(at(v + 1)), c = vertices.corner(at(v + 2));
|
|
478
|
+
if (a && b && c)
|
|
479
|
+
writeTriangle([a, b, c], entry);
|
|
326
480
|
}
|
|
481
|
+
continue;
|
|
327
482
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
writeTriangle(triangle, entry);
|
|
334
|
-
continue;
|
|
335
|
-
}
|
|
336
|
-
if (mesh.primitive === "sprite") {
|
|
337
|
-
for (let v = start; v + 2 < end; v += 3) {
|
|
338
|
-
const a = vertices.corner(at(v)), b = vertices.corner(at(v + 1)), c = vertices.corner(at(v + 2));
|
|
339
|
-
if (a && b && c)
|
|
340
|
-
writeTriangle([a, b, c], entry);
|
|
483
|
+
// Lines: segments, a strip, or a strip that closes on itself.
|
|
484
|
+
const segments = [];
|
|
485
|
+
if (mesh.primitive === "lineSegments") {
|
|
486
|
+
for (let v = start; v + 1 < end; v += 2)
|
|
487
|
+
segments.push([at(v), at(v + 1)]);
|
|
341
488
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
for (let v = start; v + 1 < end; v++)
|
|
352
|
-
segments.push([at(v), at(v + 1)]);
|
|
353
|
-
if (mesh.primitive === "lineLoop" && end - start > 2)
|
|
354
|
-
segments.push([at(end - 1), at(start)]);
|
|
489
|
+
else {
|
|
490
|
+
for (let v = start; v + 1 < end; v++)
|
|
491
|
+
segments.push([at(v), at(v + 1)]);
|
|
492
|
+
if (mesh.primitive === "lineLoop" && end - start > 2)
|
|
493
|
+
segments.push([at(end - 1), at(start)]);
|
|
494
|
+
}
|
|
495
|
+
for (const [a, b] of segments)
|
|
496
|
+
for (const triangle of vertices.segment(a, b))
|
|
497
|
+
writeTriangle(triangle, entry);
|
|
355
498
|
}
|
|
356
|
-
for (const [a, b] of segments)
|
|
357
|
-
for (const triangle of vertices.segment(a, b))
|
|
358
|
-
writeTriangle(triangle, entry);
|
|
359
499
|
}
|
|
500
|
+
meshMs += performance.now() - segment;
|
|
501
|
+
settings.onMesh?.({
|
|
502
|
+
mesh: meshIndex,
|
|
503
|
+
path,
|
|
504
|
+
triangles: triangleMaterials.length + instanced - before,
|
|
505
|
+
ms: meshMs,
|
|
506
|
+
});
|
|
507
|
+
yield* pause();
|
|
360
508
|
}
|
|
361
509
|
const properties = entries.map(entry => entry.properties);
|
|
510
|
+
const sceneTriangles = triangleMaterials.length;
|
|
511
|
+
const prototypeTriangles = prototypeGroups.length;
|
|
512
|
+
const allCorners = new Uint32Array((sceneTriangles + prototypeTriangles) * 3);
|
|
513
|
+
allCorners.set(corners.view(), 0);
|
|
514
|
+
allCorners.set(prototypeCorners.view(), sceneTriangles * 3);
|
|
362
515
|
return {
|
|
363
516
|
positions: positions.view(),
|
|
517
|
+
corners: allCorners,
|
|
364
518
|
normals: normals.view(),
|
|
365
519
|
uvs: uvs.view(),
|
|
366
520
|
colors: colors.view(),
|
|
521
|
+
vertexCount,
|
|
367
522
|
varyings: varyings.view(),
|
|
368
|
-
materials:
|
|
369
|
-
triangleCount:
|
|
523
|
+
materials: triangleMaterials.view(),
|
|
524
|
+
triangleCount: sceneTriangles,
|
|
525
|
+
instancing: instances.length
|
|
526
|
+
? packedInstancing(prototypePositions.view(), prototypes, instances, sceneTriangles, instanceMaterials.view())
|
|
527
|
+
: null,
|
|
370
528
|
materialColors: new Float32Array(properties.flatMap(m => [...m.color, m.opacity])),
|
|
371
529
|
// Only a lit or plainly emissive surface is a light: a shader's colour is not known until it runs. An unlit
|
|
372
530
|
// material of a fixed colour emits that colour, so it is one too.
|
|
@@ -380,7 +538,123 @@ export function packGeometry(scene, options = {}) {
|
|
|
380
538
|
cameraDependent,
|
|
381
539
|
};
|
|
382
540
|
}
|
|
383
|
-
|
|
541
|
+
/** The instances, numbered after the scene's own triangles, with their prototypes and the box around them all. */
|
|
542
|
+
function packedInstancing(positions, prototypes, instances, sceneTriangles, materials) {
|
|
543
|
+
const boxes = prototypes.map(prototype => trianglesBox(positions, prototype.first, prototype.count));
|
|
544
|
+
const low = [Infinity, Infinity, Infinity], high = [-Infinity, -Infinity, -Infinity];
|
|
545
|
+
for (const instance of instances) {
|
|
546
|
+
const box = transformBox(instance.matrix, boxes[instance.prototype].low, boxes[instance.prototype].high);
|
|
547
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
548
|
+
low[axis] = Math.min(low[axis], box.low[axis]);
|
|
549
|
+
high[axis] = Math.max(high[axis], box.high[axis]);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
positions,
|
|
554
|
+
triangleCount: positions.length / 9,
|
|
555
|
+
prototypes,
|
|
556
|
+
instances: instances.map(instance => ({ ...instance, firstTriangle: sceneTriangles + instance.firstTriangle })),
|
|
557
|
+
materials,
|
|
558
|
+
bounds: { low, high },
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Whether a material can be shared by instances: nothing about it is read triangle by triangle on the CPU (lights,
|
|
563
|
+
* windows, volumes, grass) or tied to one mesh (shaders, object space normal maps, displacement).
|
|
564
|
+
*/
|
|
565
|
+
function plainMaterial(material, textures, programs) {
|
|
566
|
+
if (!material)
|
|
567
|
+
return true;
|
|
568
|
+
if (programs?.fragment(material) || programs?.vertex(material))
|
|
569
|
+
return false;
|
|
570
|
+
if (material.values?.normalMapType === 1 || material.maps?.displacementMap)
|
|
571
|
+
return false;
|
|
572
|
+
const p = materialProperties(material, textures);
|
|
573
|
+
if (p.kind === MATERIAL_KIND.shader || (p.flags & (MATERIAL_UNLIT_LIGHT | MATERIAL_VOLUME)) !== 0)
|
|
574
|
+
return false;
|
|
575
|
+
if ((p.emissive[0] + p.emissive[1] + p.emissive[2]) * p.emissiveIntensity > 0)
|
|
576
|
+
return false;
|
|
577
|
+
if (p.transmission > 0 && p.thickness <= 0)
|
|
578
|
+
return false;
|
|
579
|
+
return !p.grass;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* The meshes whose geometry another mesh repeats, each with the key its copies share. Meshes are first set apart by
|
|
583
|
+
* their sizes, which is free, and only those that still have a match are hashed.
|
|
584
|
+
*/
|
|
585
|
+
function repeatedGeometry(meshes, plain) {
|
|
586
|
+
const plainOf = new Map();
|
|
587
|
+
const isPlain = (material) => {
|
|
588
|
+
let known = plainOf.get(material);
|
|
589
|
+
if (known === undefined)
|
|
590
|
+
plainOf.set(material, known = plain(material));
|
|
591
|
+
return known;
|
|
592
|
+
};
|
|
593
|
+
const bySize = new Map();
|
|
594
|
+
for (const mesh of meshes) {
|
|
595
|
+
if (mesh.primitive || !instanceable(mesh.matrixWorld) || triangleCountOf(mesh) < INSTANCE_MIN_TRIANGLES)
|
|
596
|
+
continue;
|
|
597
|
+
let visible = "";
|
|
598
|
+
let usable = true;
|
|
599
|
+
for (const group of groupsOf(mesh)) {
|
|
600
|
+
const material = mesh.materials[group.materialIndex] ?? mesh.materials[0];
|
|
601
|
+
const shown = isMaterialVisible(material);
|
|
602
|
+
visible += shown ? "1" : "0";
|
|
603
|
+
if (shown && !isPlain(material))
|
|
604
|
+
usable = false;
|
|
605
|
+
}
|
|
606
|
+
if (!usable)
|
|
607
|
+
continue;
|
|
608
|
+
const size = `${mesh.attributes.position.data.length}|${mesh.index?.length ?? -1}|${mesh.groups.length}|${visible}`;
|
|
609
|
+
const bucket = bySize.get(size);
|
|
610
|
+
if (bucket)
|
|
611
|
+
bucket.push({ mesh, visible });
|
|
612
|
+
else
|
|
613
|
+
bySize.set(size, [{ mesh, visible }]);
|
|
614
|
+
}
|
|
615
|
+
const byKey = new Map();
|
|
616
|
+
for (const bucket of bySize.values()) {
|
|
617
|
+
if (bucket.length < 2)
|
|
618
|
+
continue;
|
|
619
|
+
for (const { mesh, visible } of bucket) {
|
|
620
|
+
const key = geometryKey(mesh, visible);
|
|
621
|
+
const same = byKey.get(key);
|
|
622
|
+
if (same)
|
|
623
|
+
same.push(mesh);
|
|
624
|
+
else
|
|
625
|
+
byKey.set(key, [mesh]);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const repeated = new Map();
|
|
629
|
+
for (const [key, same] of byKey)
|
|
630
|
+
if (same.length > 1)
|
|
631
|
+
for (const mesh of same)
|
|
632
|
+
repeated.set(mesh, key);
|
|
633
|
+
return repeated;
|
|
634
|
+
}
|
|
635
|
+
/** The three vertex numbers of a stored triangle. */
|
|
636
|
+
export function triangleVertices(geometry, triangle) {
|
|
637
|
+
const at = triangle * 3;
|
|
638
|
+
return [geometry.corners[at], geometry.corners[at + 1], geometry.corners[at + 2]];
|
|
639
|
+
}
|
|
640
|
+
/** The normal at one corner of a stored triangle. */
|
|
641
|
+
export function cornerNormal(geometry, triangle, corner) {
|
|
642
|
+
const vertex = geometry.corners[triangle * 3 + corner];
|
|
643
|
+
return Array.from(geometry.normals.subarray(vertex * 3, vertex * 3 + 3));
|
|
644
|
+
}
|
|
645
|
+
/** The two UV sets at one corner of a stored triangle, four numbers. */
|
|
646
|
+
export function cornerUvs(geometry, triangle, corner) {
|
|
647
|
+
const vertex = geometry.corners[triangle * 3 + corner];
|
|
648
|
+
return Array.from(geometry.uvs.subarray(vertex * 4, vertex * 4 + 4));
|
|
649
|
+
}
|
|
650
|
+
/** The colour at one corner of a stored triangle, RGBA, or empty when the scene has no vertex colours. */
|
|
651
|
+
export function cornerColor(geometry, triangle, corner) {
|
|
652
|
+
if (!geometry.colors.length)
|
|
653
|
+
return [];
|
|
654
|
+
const vertex = geometry.corners[triangle * 3 + corner];
|
|
655
|
+
return Array.from(geometry.colors.subarray(vertex * 4, vertex * 4 + 4));
|
|
656
|
+
}
|
|
657
|
+
function plainVertices(mesh, matrixWorld = mesh.matrixWorld) {
|
|
384
658
|
const position = mesh.attributes.position;
|
|
385
659
|
const count = Math.floor(position.data.length / Math.max(1, position.itemSize));
|
|
386
660
|
const world = new Float32Array(count * 3);
|
|
@@ -390,8 +664,8 @@ function plainVertices(mesh) {
|
|
|
390
664
|
const color = new Float32Array(count * 4);
|
|
391
665
|
// 0 not read yet, 1 usable, 2 refused.
|
|
392
666
|
const state = new Uint8Array(count);
|
|
393
|
-
const rotation = normalMatrix(
|
|
394
|
-
const matrix =
|
|
667
|
+
const rotation = normalMatrix(matrixWorld);
|
|
668
|
+
const matrix = matrixWorld;
|
|
395
669
|
const { normal: normals, uv: uv0, uv1, color: colors } = mesh.attributes;
|
|
396
670
|
/** Component `c` of a vertex, as the reader's `Array.from(subarray(...))[c]` would see it. */
|
|
397
671
|
const component = (attribute, vertex, c) => {
|
|
@@ -437,6 +711,7 @@ function plainVertices(mesh) {
|
|
|
437
711
|
hasNormal,
|
|
438
712
|
uv,
|
|
439
713
|
color,
|
|
714
|
+
ids: new Int32Array(count).fill(-1),
|
|
440
715
|
valid(vertex) {
|
|
441
716
|
if (!(vertex >= 0 && vertex < count))
|
|
442
717
|
return false;
|