@es3d/es3d 1.0.0 → 1.0.2
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/LICENSE +201 -201
- package/README.md +95 -86
- package/docs/SPEC.md +426 -417
- package/package.json +1 -1
- package/scripts/es3d-cli.mjs +390 -377
- package/src/cache/geometry-store.js +392 -392
- package/src/cache/mesh-bundle.js +123 -123
- package/src/cache/sha256.js +73 -73
- package/src/format/ast.js +238 -238
- package/src/format/colors.js +93 -93
- package/src/format/compact-parser.js +171 -171
- package/src/format/compact-tokenizer.js +165 -165
- package/src/format/errors.js +247 -247
- package/src/format/json.js +55 -55
- package/src/format/limits.js +14 -14
- package/src/format/parse.js +132 -92
- package/src/format/schema.js +617 -617
- package/src/format/serializer.js +333 -333
- package/src/format/validator.js +330 -330
- package/src/format/value-error.js +12 -12
- package/src/format/value-format.js +314 -314
- package/src/format/values.js +459 -459
- package/src/format/xml-parser.js +136 -136
- package/src/format/xml-tokenizer.js +237 -237
- package/src/geometry/contour.js +86 -86
- package/src/geometry/extrude.js +175 -175
- package/src/geometry/gear.js +85 -85
- package/src/geometry/geometry-cache.js +159 -159
- package/src/geometry/lod-error.js +178 -178
- package/src/geometry/mesh.js +458 -458
- package/src/geometry/primitives.js +299 -299
- package/src/geometry/registry.js +645 -645
- package/src/geometry/revolve.js +136 -136
- package/src/geometry/sweep.js +441 -441
- package/src/index.js +47 -47
- package/src/io/compression.js +67 -67
- package/src/io/convert.js +236 -236
- package/src/io/download.js +13 -13
- package/src/io/external-scene.js +127 -127
- package/src/io/file-loader.js +28 -28
- package/src/io/gltf-pack.js +1461 -0
- package/src/io/gltf.js +630 -630
- package/src/io/mesh-import.js +104 -104
- package/src/io/obj.js +352 -352
- package/src/io/step-brep.js +439 -439
- package/src/io/step-exact.js +1149 -1149
- package/src/io/step.js +1418 -1418
- package/src/io/stl.js +193 -193
- package/src/io/svg-import.js +787 -787
- package/src/material/materials.js +154 -154
- package/src/math/mat4.js +475 -475
- package/src/math/random.js +30 -30
- package/src/math/trig.js +65 -65
- package/src/math/vec3.js +81 -81
- package/src/optimize/optimizer.js +604 -579
- package/src/path/path-flatten.js +322 -322
- package/src/path/path-parser.js +332 -332
- package/src/path/shapes.js +129 -129
- package/src/path/triangulate.js +517 -517
- package/src/pipeline.js +74 -74
- package/src/render/camera.js +168 -168
- package/src/render/frame-builder.js +362 -362
- package/src/render/geometry-pool.js +149 -149
- package/src/render/lighting.js +149 -149
- package/src/render/orbit-controls.js +237 -237
- package/src/render/renderer.js +258 -249
- package/src/render/scroll-timeline.js +104 -104
- package/src/render/shaders/glsl.js +285 -285
- package/src/render/shaders/wgsl.js +252 -252
- package/src/render/stage.js +365 -352
- package/src/render/texture-cache.js +40 -40
- package/src/render/textures.js +223 -223
- package/src/render/webgl2/webgl2-backend.js +586 -532
- package/src/render/webgpu/webgpu-backend.js +482 -480
- package/src/scene/flatten.js +754 -754
- package/src/scene/picking.js +161 -161
- package/src/scene/scene-graph.js +320 -320
- package/src/transform/transform.js +211 -211
- package/src/wasm/kernels-binary.js +3 -3
- package/src/wasm/kernels.js +385 -385
package/src/render/renderer.js
CHANGED
|
@@ -1,249 +1,258 @@
|
|
|
1
|
-
// Renderer facade: picks a GPU backend (WebGPU preferred, WebGL2 fallback)
|
|
2
|
-
// and runs the backend-independent parts of a frame (camera, culling, LOD,
|
|
3
|
-
// batching, uniform packing, incremental geometry upload).
|
|
4
|
-
//
|
|
5
|
-
// The facade is the only renderer API used by the application and the
|
|
6
|
-
// benchmark; backends never see the ES3D document, only a RenderScene.
|
|
7
|
-
|
|
8
|
-
import { GeometryPool } from './geometry-pool.js';
|
|
9
|
-
import { FrameBuilder } from './frame-builder.js';
|
|
10
|
-
import { TextureCache } from './texture-cache.js';
|
|
11
|
-
import { FRAME_FLOATS, packFrameUniforms } from './lighting.js';
|
|
12
|
-
import { Mesh } from '../geometry/mesh.js';
|
|
13
|
-
import { WebGL2Backend } from './webgl2/webgl2-backend.js';
|
|
14
|
-
import { WebGPUBackend } from './webgpu/webgpu-backend.js';
|
|
15
|
-
|
|
16
|
-
const now = () => performance.now();
|
|
17
|
-
|
|
18
|
-
function cubeEdges() {
|
|
19
|
-
const p = [];
|
|
20
|
-
for (let i = 0; i < 8; i++) p.push(i & 1 ? 0.5 : -0.5, i & 2 ? 0.5 : -0.5, i & 4 ? 0.5 : -0.5);
|
|
21
|
-
const e = [0, 1, 2, 3, 4, 5, 6, 7, 0, 2, 1, 3, 4, 6, 5, 7, 0, 4, 1, 5, 2, 6, 3, 7];
|
|
22
|
-
return new Mesh({ positions: Float32Array.from(p), indices: Uint32Array.from(e), kind: 'lines' });
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export const OVERLAY_COLORS = {
|
|
26
|
-
wireframe: [0.02, 0.03, 0.05, 0.55],
|
|
27
|
-
normals: [1.0, 0.82, 0.15, 0.95],
|
|
28
|
-
bbox: [0.35, 0.78, 1.0, 0.75],
|
|
29
|
-
selection: [1.0, 0.55, 0.12, 1.0],
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// setScene keeps the packed geometry, so a new document that shares meshes
|
|
33
|
-
// with the last one (a colour change, an edit elsewhere in the file) uploads
|
|
34
|
-
// only what is new. Meshes no longer used stay packed until the pool passes
|
|
35
|
-
// this size; then it is repacked from what the scene draws.
|
|
36
|
-
const POOL_REPACK_BYTES = 32 << 20;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Tessellation budget (ms per frame) for render loops that draw again while
|
|
40
|
-
* stats.refining is non-zero: the first frame comes at once with coarser
|
|
41
|
-
* stand-ins, full detail a few frames later.
|
|
42
|
-
*/
|
|
43
|
-
export const PROGRESSIVE_BUDGET_MS = 4;
|
|
44
|
-
|
|
45
|
-
export class Renderer {
|
|
46
|
-
constructor(backend) {
|
|
47
|
-
this.backend = backend;
|
|
48
|
-
this.kind = backend.kind;
|
|
49
|
-
this.pool = new GeometryPool();
|
|
50
|
-
this.frames = new FrameBuilder();
|
|
51
|
-
this.textures = new TextureCache();
|
|
52
|
-
this.frameData = new Float32Array(FRAME_FLOATS);
|
|
53
|
-
this.options = {
|
|
54
|
-
wireframe: false,
|
|
55
|
-
normals: false,
|
|
56
|
-
bbox: false,
|
|
57
|
-
autoLod: true,
|
|
58
|
-
fixedLod: 0,
|
|
59
|
-
culling: true,
|
|
60
|
-
lodBias: 1,
|
|
61
|
-
// Same picture on every screen: the canvas resolution is capped at this
|
|
62
|
-
// many device pixels per CSS pixel, and automatic LOD budgets its error in
|
|
63
|
-
// CSS pixels, so a phone and a desktop choose the same meshes for the
|
|
64
|
-
// same view ('device' spends more triangles on high-density screens).
|
|
65
|
-
maxPixelRatio: 2,
|
|
66
|
-
lodPixels: 'css',
|
|
67
|
-
// Milliseconds of mesh generation per frame. Past it, levels that do not
|
|
68
|
-
// exist yet are drawn with a coarser one, and stats.refining counts them:
|
|
69
|
-
// render again while it is non-zero and full detail follows within a few
|
|
70
|
-
// frames. Infinity (the default) generates everything the frame needs.
|
|
71
|
-
tessellationBudget: Infinity,
|
|
72
|
-
};
|
|
73
|
-
this.selection = new Set();
|
|
74
|
-
this.scene = null;
|
|
75
|
-
this.meshOf = null;
|
|
76
|
-
this.cube = cubeEdges();
|
|
77
|
-
this.cubeRange = this.pool.meshRange(this.cube);
|
|
78
|
-
this.lastDraw = null;
|
|
79
|
-
this.lightInfo = { truncated: 0, usedDefaults: false };
|
|
80
|
-
this.stats = { cpuMs: 0, uploadBytes: 0, totalUploadBytes: 0, drawCalls: 0, triangles: 0, visible: 0, instances: 0 };
|
|
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
|
-
this.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
this.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
this.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
*
|
|
119
|
-
* @param {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
this.backend.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
this.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
s
|
|
164
|
-
s.
|
|
165
|
-
s.
|
|
166
|
-
s.
|
|
167
|
-
s.
|
|
168
|
-
s.
|
|
169
|
-
s.
|
|
170
|
-
s.
|
|
171
|
-
s.
|
|
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
|
-
this.
|
|
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
|
-
|
|
1
|
+
// Renderer facade: picks a GPU backend (WebGPU preferred, WebGL2 fallback)
|
|
2
|
+
// and runs the backend-independent parts of a frame (camera, culling, LOD,
|
|
3
|
+
// batching, uniform packing, incremental geometry upload).
|
|
4
|
+
//
|
|
5
|
+
// The facade is the only renderer API used by the application and the
|
|
6
|
+
// benchmark; backends never see the ES3D document, only a RenderScene.
|
|
7
|
+
|
|
8
|
+
import { GeometryPool } from './geometry-pool.js';
|
|
9
|
+
import { FrameBuilder } from './frame-builder.js';
|
|
10
|
+
import { TextureCache } from './texture-cache.js';
|
|
11
|
+
import { FRAME_FLOATS, packFrameUniforms } from './lighting.js';
|
|
12
|
+
import { Mesh } from '../geometry/mesh.js';
|
|
13
|
+
import { WebGL2Backend } from './webgl2/webgl2-backend.js';
|
|
14
|
+
import { WebGPUBackend } from './webgpu/webgpu-backend.js';
|
|
15
|
+
|
|
16
|
+
const now = () => performance.now();
|
|
17
|
+
|
|
18
|
+
function cubeEdges() {
|
|
19
|
+
const p = [];
|
|
20
|
+
for (let i = 0; i < 8; i++) p.push(i & 1 ? 0.5 : -0.5, i & 2 ? 0.5 : -0.5, i & 4 ? 0.5 : -0.5);
|
|
21
|
+
const e = [0, 1, 2, 3, 4, 5, 6, 7, 0, 2, 1, 3, 4, 6, 5, 7, 0, 4, 1, 5, 2, 6, 3, 7];
|
|
22
|
+
return new Mesh({ positions: Float32Array.from(p), indices: Uint32Array.from(e), kind: 'lines' });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const OVERLAY_COLORS = {
|
|
26
|
+
wireframe: [0.02, 0.03, 0.05, 0.55],
|
|
27
|
+
normals: [1.0, 0.82, 0.15, 0.95],
|
|
28
|
+
bbox: [0.35, 0.78, 1.0, 0.75],
|
|
29
|
+
selection: [1.0, 0.55, 0.12, 1.0],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// setScene keeps the packed geometry, so a new document that shares meshes
|
|
33
|
+
// with the last one (a colour change, an edit elsewhere in the file) uploads
|
|
34
|
+
// only what is new. Meshes no longer used stay packed until the pool passes
|
|
35
|
+
// this size; then it is repacked from what the scene draws.
|
|
36
|
+
const POOL_REPACK_BYTES = 32 << 20;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Tessellation budget (ms per frame) for render loops that draw again while
|
|
40
|
+
* stats.refining is non-zero: the first frame comes at once with coarser
|
|
41
|
+
* stand-ins, full detail a few frames later.
|
|
42
|
+
*/
|
|
43
|
+
export const PROGRESSIVE_BUDGET_MS = 4;
|
|
44
|
+
|
|
45
|
+
export class Renderer {
|
|
46
|
+
constructor(backend) {
|
|
47
|
+
this.backend = backend;
|
|
48
|
+
this.kind = backend.kind;
|
|
49
|
+
this.pool = new GeometryPool();
|
|
50
|
+
this.frames = new FrameBuilder();
|
|
51
|
+
this.textures = new TextureCache();
|
|
52
|
+
this.frameData = new Float32Array(FRAME_FLOATS);
|
|
53
|
+
this.options = {
|
|
54
|
+
wireframe: false,
|
|
55
|
+
normals: false,
|
|
56
|
+
bbox: false,
|
|
57
|
+
autoLod: true,
|
|
58
|
+
fixedLod: 0,
|
|
59
|
+
culling: true,
|
|
60
|
+
lodBias: 1,
|
|
61
|
+
// Same picture on every screen: the canvas resolution is capped at this
|
|
62
|
+
// many device pixels per CSS pixel, and automatic LOD budgets its error in
|
|
63
|
+
// CSS pixels, so a phone and a desktop choose the same meshes for the
|
|
64
|
+
// same view ('device' spends more triangles on high-density screens).
|
|
65
|
+
maxPixelRatio: 2,
|
|
66
|
+
lodPixels: 'css',
|
|
67
|
+
// Milliseconds of mesh generation per frame. Past it, levels that do not
|
|
68
|
+
// exist yet are drawn with a coarser one, and stats.refining counts them:
|
|
69
|
+
// render again while it is non-zero and full detail follows within a few
|
|
70
|
+
// frames. Infinity (the default) generates everything the frame needs.
|
|
71
|
+
tessellationBudget: Infinity,
|
|
72
|
+
};
|
|
73
|
+
this.selection = new Set();
|
|
74
|
+
this.scene = null;
|
|
75
|
+
this.meshOf = null;
|
|
76
|
+
this.cube = cubeEdges();
|
|
77
|
+
this.cubeRange = this.pool.meshRange(this.cube);
|
|
78
|
+
this.lastDraw = null;
|
|
79
|
+
this.lightInfo = { truncated: 0, usedDefaults: false };
|
|
80
|
+
this.stats = { cpuMs: 0, uploadBytes: 0, totalUploadBytes: 0, drawCalls: 0, triangles: 0, visible: 0, instances: 0 };
|
|
81
|
+
this.gpuEpoch = 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get info() {
|
|
85
|
+
return this.backend.info;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* What this device actually renders with, for checking that two devices
|
|
90
|
+
* show the same thing: backend, MSAA samples, texture filtering, limits,
|
|
91
|
+
* and whether the scene has more lights than are drawn.
|
|
92
|
+
*/
|
|
93
|
+
get caps() {
|
|
94
|
+
return { backend: this.kind, ...this.backend.caps, maxPixelRatio: this.options.maxPixelRatio, lodPixels: this.options.lodPixels, lightsDropped: this.lightInfo.truncated || 0 };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Device pixels per CSS pixel actually used for a requested ratio. */
|
|
98
|
+
pixelRatio(dpr) {
|
|
99
|
+
return Math.min(dpr || 1, this.options.maxPixelRatio || Infinity);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Replace the scene. Geometry is packed lazily into the shared buffers.
|
|
104
|
+
* Meshes already packed are kept (pass keepGeometry: false for a cold start).
|
|
105
|
+
*/
|
|
106
|
+
setScene(scene, meshOf, { keepGeometry = true } = {}) {
|
|
107
|
+
this.scene = scene;
|
|
108
|
+
this.meshOf = meshOf;
|
|
109
|
+
if (!scene || !keepGeometry || this.pool.byteSize > POOL_REPACK_BYTES) {
|
|
110
|
+
this.pool.reset();
|
|
111
|
+
this.cubeRange = this.pool.meshRange(this.cube);
|
|
112
|
+
}
|
|
113
|
+
this.backend.releaseMaterials(new Set(scene ? scene.materials : []));
|
|
114
|
+
this.stats.totalUploadBytes = 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Render one frame.
|
|
119
|
+
* @param {import('./camera.js').Camera} camera
|
|
120
|
+
* @param {{width:number, height:number, dpr?:number, time?:number}} view CSS size of the canvas
|
|
121
|
+
*/
|
|
122
|
+
render(camera, view) {
|
|
123
|
+
const t0 = now();
|
|
124
|
+
const dpr = this.pixelRatio(view.dpr);
|
|
125
|
+
const w = Math.max(1, Math.round(view.width * dpr));
|
|
126
|
+
const h = Math.max(1, Math.round(view.height * dpr));
|
|
127
|
+
this.backend.resize(w, h);
|
|
128
|
+
const scene = this.scene;
|
|
129
|
+
const bounds = scene ? scene.bounds : { min: [-1, -1, -1], max: [1, 1, 1] };
|
|
130
|
+
camera.update(w / h, this.backend.zeroToOne, bounds);
|
|
131
|
+
const env = scene && scene.environment ? scene.environment : { background: [0.09, 0.1, 0.12, 1], sky: [0.8, 0.86, 0.95, 1], ground: [0.25, 0.22, 0.2, 1], intensity: 0.5, exposure: 1 };
|
|
132
|
+
let draw;
|
|
133
|
+
if (scene) {
|
|
134
|
+
draw = this.buildFrame(camera, view, h);
|
|
135
|
+
} else {
|
|
136
|
+
draw = { batches: [], overlays: { wireframe: [], normals: [], bbox: null, selection: null }, instanceData: new Float32Array(16), instanceCount: 0, stats: { instances: 0, visible: 0, culled: 0, batches: 0, drawCalls: 0, triangles: 0, lodHistogram: [0, 0, 0, 0], refining: 0 } };
|
|
137
|
+
}
|
|
138
|
+
const radius = Math.hypot(bounds.max[0] - bounds.min[0], bounds.max[1] - bounds.min[1], bounds.max[2] - bounds.min[2]) / 2 || 1;
|
|
139
|
+
this.lightInfo = packFrameUniforms(this.frameData, {
|
|
140
|
+
viewProj: camera.viewProj,
|
|
141
|
+
view: camera.view,
|
|
142
|
+
cameraPos: camera.position,
|
|
143
|
+
time: view.time || 0,
|
|
144
|
+
environment: env,
|
|
145
|
+
lights: scene ? scene.lights : [],
|
|
146
|
+
normalLength: radius * 0.025,
|
|
147
|
+
lineBias: 0.0015,
|
|
148
|
+
viewportHeight: h,
|
|
149
|
+
});
|
|
150
|
+
const uploaded = this.backend.syncPool(this.pool);
|
|
151
|
+
const epoch = this.backend.resourceEpoch || 0;
|
|
152
|
+
if (epoch !== this.gpuEpoch) {
|
|
153
|
+
this.gpuEpoch = epoch;
|
|
154
|
+
for (const entry of this.textures.entries.values()) entry.gpu = null;
|
|
155
|
+
}
|
|
156
|
+
this.backend.render(draw, this.frameData, {
|
|
157
|
+
background: env.background,
|
|
158
|
+
textures: this.textures,
|
|
159
|
+
overlayColors: OVERLAY_COLORS,
|
|
160
|
+
cubeRange: this.cubeRange,
|
|
161
|
+
});
|
|
162
|
+
this.lastDraw = draw;
|
|
163
|
+
const s = this.stats;
|
|
164
|
+
s.cpuMs = now() - t0;
|
|
165
|
+
s.uploadBytes = uploaded + draw.instanceCount * 64;
|
|
166
|
+
s.totalUploadBytes += uploaded;
|
|
167
|
+
s.drawCalls = draw.stats.drawCalls;
|
|
168
|
+
s.triangles = draw.stats.triangles;
|
|
169
|
+
s.visible = draw.stats.visible;
|
|
170
|
+
s.instances = draw.stats.instances;
|
|
171
|
+
s.culled = draw.stats.culled;
|
|
172
|
+
s.batches = draw.stats.batches;
|
|
173
|
+
s.lodHistogram = draw.stats.lodHistogram;
|
|
174
|
+
s.refining = draw.stats.refining || 0;
|
|
175
|
+
s.geometryBytes = this.pool.byteSize;
|
|
176
|
+
s.instanceBytes = draw.instanceCount * 64;
|
|
177
|
+
s.textureBytes = this.backend.gpuBytes;
|
|
178
|
+
return draw;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
buildFrame(camera, view, deviceHeight, tessellationBudget = this.options.tessellationBudget) {
|
|
182
|
+
const lodHeight = this.options.lodPixels === 'device' ? deviceHeight : Math.max(1, Math.round(view.height));
|
|
183
|
+
return this.frames.build({
|
|
184
|
+
scene: this.scene,
|
|
185
|
+
camera,
|
|
186
|
+
pool: this.pool,
|
|
187
|
+
meshOf: this.meshOf,
|
|
188
|
+
viewportHeight: lodHeight,
|
|
189
|
+
options: { ...this.options, selection: this.selection, tessellationBudget },
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Generate and pack (but do not draw) every mesh a view needs, so a later
|
|
195
|
+
* frame from that view does not stall on tessellation. The upload happens
|
|
196
|
+
* with the next render. Used to warm up a scroll-driven camera path.
|
|
197
|
+
* @param {import('./camera.js').Camera} camera not modified
|
|
198
|
+
* @param {{width:number, height:number, dpr?:number}} view
|
|
199
|
+
*/
|
|
200
|
+
prepare(camera, view) {
|
|
201
|
+
if (!this.scene) return;
|
|
202
|
+
const dpr = this.pixelRatio(view.dpr);
|
|
203
|
+
const w = Math.max(1, Math.round(view.width * dpr));
|
|
204
|
+
const h = Math.max(1, Math.round(view.height * dpr));
|
|
205
|
+
const cam = camera.clone();
|
|
206
|
+
cam.update(w / h, this.backend.zeroToOne, this.scene.bounds);
|
|
207
|
+
const keep = this.frames.last;
|
|
208
|
+
this.buildFrame(cam, view, h, Infinity);
|
|
209
|
+
this.frames.last = keep;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Wait for the GPU to finish all submitted work (benchmarks). */
|
|
213
|
+
finish() {
|
|
214
|
+
return this.backend.finish();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
dispose() {
|
|
218
|
+
this.textures.clear((g) => this.backend.destroyTexture(g));
|
|
219
|
+
this.backend.dispose();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Create a renderer for a canvas.
|
|
225
|
+
* @param {HTMLCanvasElement} canvas
|
|
226
|
+
* @param {{backend?: 'auto'|'webgpu'|'webgl2', present?: 'canvas'|'readback'}} [opts]
|
|
227
|
+
*/
|
|
228
|
+
export async function createRenderer(canvas, opts = {}) {
|
|
229
|
+
const want = opts.backend || 'auto';
|
|
230
|
+
const errors = [];
|
|
231
|
+
let canvasUsed = false;
|
|
232
|
+
if ((want === 'auto' || want === 'webgpu') && typeof navigator !== 'undefined' && navigator.gpu) {
|
|
233
|
+
const b = new WebGPUBackend(canvas, { present: opts.present });
|
|
234
|
+
try {
|
|
235
|
+
await b.init();
|
|
236
|
+
return new Renderer(b);
|
|
237
|
+
} catch (e) {
|
|
238
|
+
errors.push(e.message);
|
|
239
|
+
canvasUsed = !!b.canvasUsed;
|
|
240
|
+
try { b.dispose(); } catch {}
|
|
241
|
+
if (want === 'webgpu') throw e;
|
|
242
|
+
}
|
|
243
|
+
} else if (want === 'webgpu') {
|
|
244
|
+
throw new Error('WebGPU is not available in this browser.');
|
|
245
|
+
}
|
|
246
|
+
// A canvas that already has a WebGPU context cannot create a WebGL2 context.
|
|
247
|
+
let target = canvas;
|
|
248
|
+
if (canvasUsed && canvas.getContext && canvas.parentNode) {
|
|
249
|
+
target = canvas.cloneNode(false);
|
|
250
|
+
canvas.parentNode.replaceChild(target, canvas);
|
|
251
|
+
}
|
|
252
|
+
const b = new WebGL2Backend(target);
|
|
253
|
+
await b.init();
|
|
254
|
+
const r = new Renderer(b);
|
|
255
|
+
r.fallbackReason = errors.join('; ');
|
|
256
|
+
r.canvas = target;
|
|
257
|
+
return r;
|
|
258
|
+
}
|