micro-gl 0.1.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.
Files changed (82) hide show
  1. package/README.md +556 -0
  2. package/package.json +37 -0
  3. package/src/2d/cameras/Camera2d.js +46 -0
  4. package/src/2d/constants.js +3 -0
  5. package/src/2d/controls/DragControls2d.js +174 -0
  6. package/src/2d/controls/PanZoomControls.js +64 -0
  7. package/src/2d/core/GpuResources2d.js +163 -0
  8. package/src/2d/core/InstancedShape2d.js +74 -0
  9. package/src/2d/core/Object2d.js +107 -0
  10. package/src/2d/core/Pipelines2d.js +110 -0
  11. package/src/2d/core/Renderer2d.js +353 -0
  12. package/src/2d/core/Scene2d.js +13 -0
  13. package/src/2d/core/Shape2d.js +13 -0
  14. package/src/2d/core/Uniforms2d.js +13 -0
  15. package/src/2d/geometries/CircleGeometry.js +27 -0
  16. package/src/2d/geometries/Geometry2d.js +108 -0
  17. package/src/2d/geometries/RectGeometry.js +23 -0
  18. package/src/2d/materials/BasicMaterial2d.js +12 -0
  19. package/src/2d/materials/Material2d.js +78 -0
  20. package/src/2d/materials/SpriteMaterial2d.js +28 -0
  21. package/src/2d/shaders/fragments.js +23 -0
  22. package/src/2d/shaders/shared.js +28 -0
  23. package/src/2d/shaders/vertexLayout.js +74 -0
  24. package/src/2d/shaders/vertexStages.js +59 -0
  25. package/src/3d/cameras/Camera.js +66 -0
  26. package/src/3d/cameras/OrthographicCamera.js +35 -0
  27. package/src/3d/cameras/PerspectiveCamera.js +25 -0
  28. package/src/3d/constants.js +18 -0
  29. package/src/3d/controls/DragControls.js +168 -0
  30. package/src/3d/controls/OrbitControls.js +125 -0
  31. package/src/3d/core/DirectionalShadowMap.js +283 -0
  32. package/src/3d/core/GpuResources.js +174 -0
  33. package/src/3d/core/InstanceMatrix.js +55 -0
  34. package/src/3d/core/InstancedBounds.js +114 -0
  35. package/src/3d/core/InstancedMesh.js +124 -0
  36. package/src/3d/core/Mesh.js +26 -0
  37. package/src/3d/core/Object3d.js +101 -0
  38. package/src/3d/core/Pipelines.js +125 -0
  39. package/src/3d/core/RayIntersection.js +184 -0
  40. package/src/3d/core/Raycaster.js +246 -0
  41. package/src/3d/core/Renderer.js +492 -0
  42. package/src/3d/core/Scene.js +13 -0
  43. package/src/3d/core/ShadowPipelines.js +64 -0
  44. package/src/3d/core/Uniforms.js +132 -0
  45. package/src/3d/geometries/BoxGeometry.js +56 -0
  46. package/src/3d/geometries/Geometry.js +97 -0
  47. package/src/3d/geometries/PlaneGeometry.js +24 -0
  48. package/src/3d/geometries/SphereGeometry.js +46 -0
  49. package/src/3d/geometries/WireframeGeometry.js +39 -0
  50. package/src/3d/helpers/GridHelper.js +43 -0
  51. package/src/3d/lights/AmbientLight.js +18 -0
  52. package/src/3d/lights/DirectionalLight.js +26 -0
  53. package/src/3d/lights/DirectionalShadow.js +29 -0
  54. package/src/3d/lights/PointLight.js +23 -0
  55. package/src/3d/materials/BasicMaterial.js +11 -0
  56. package/src/3d/materials/LambertMaterial.js +13 -0
  57. package/src/3d/materials/Material.js +87 -0
  58. package/src/3d/materials/TextureMaterial.js +24 -0
  59. package/src/3d/shaders/fragments.js +34 -0
  60. package/src/3d/shaders/shadows.js +52 -0
  61. package/src/3d/shaders/shared.js +129 -0
  62. package/src/3d/shaders/vertexLayout.js +85 -0
  63. package/src/3d/shaders/vertexStages.js +70 -0
  64. package/src/core/PointerControls.js +258 -0
  65. package/src/core/Texture.js +87 -0
  66. package/src/core/createMaterialPipelineLayouts.js +114 -0
  67. package/src/core/deviceLease.js +67 -0
  68. package/src/core/generateMipmaps.js +115 -0
  69. package/src/core/indexedTriangles.js +53 -0
  70. package/src/core/initWebGpu.js +65 -0
  71. package/src/core/materialResources.js +18 -0
  72. package/src/core/objectGpuResources.js +68 -0
  73. package/src/core/pipelineConstants.js +68 -0
  74. package/src/core/rendererConfig.js +66 -0
  75. package/src/core/uploadTexture.js +57 -0
  76. package/src/index.js +68 -0
  77. package/src/math/Frustum.js +143 -0
  78. package/src/math/Mat3.js +165 -0
  79. package/src/math/Mat4.js +359 -0
  80. package/src/math/Vec2.js +72 -0
  81. package/src/math/Vec3.js +98 -0
  82. package/src/math/color.js +20 -0
@@ -0,0 +1,492 @@
1
+ import { Mesh } from './Mesh.js';
2
+ import { Mat4 } from '../../math/Mat4.js';
3
+ import { Frustum } from '../../math/Frustum.js';
4
+ import { srgbToLinear } from '../../math/color.js';
5
+ import { GpuResources } from './GpuResources.js';
6
+ import { DirectionalShadowMap } from './DirectionalShadowMap.js';
7
+ import {
8
+ FRAME_UNIFORM_SIZE,
9
+ FrameUniformWriter,
10
+ OBJECT_UNIFORM_OFFSET,
11
+ } from './Uniforms.js';
12
+ import { initWebGpu } from '../../core/initWebGpu.js';
13
+ import {
14
+ ANTIALIAS_SAMPLE_COUNT,
15
+ DEFAULT_CANVAS_HEIGHT,
16
+ DEFAULT_CANVAS_WIDTH,
17
+ DEPTH_CLEAR_VALUE,
18
+ DEPTH_FORMAT,
19
+ SINGLE_SAMPLE_COUNT,
20
+ colorAttachment,
21
+ drawingBufferSize,
22
+ linearClearColor,
23
+ } from '../../core/rendererConfig.js';
24
+ import {
25
+ acquireDeviceLease,
26
+ releaseDeviceLease,
27
+ } from '../../core/deviceLease.js';
28
+ import {
29
+ INDEX_FORMAT,
30
+ SHADER_BIND_GROUP,
31
+ SHADER_BINDING,
32
+ VERTEX_BUFFER_SLOT,
33
+ isTriangleTopology,
34
+ } from '../../core/pipelineConstants.js';
35
+
36
+ /**
37
+ * The WebGPU renderer. Owns or shares a device lease and manages the canvas
38
+ * swap chain and depth buffer; per-object GPU resources live in GpuResources.
39
+ *
40
+ * Usage:
41
+ * const renderer = new Renderer(canvas);
42
+ * await renderer.init();
43
+ * renderer.render(scene, camera);
44
+ */
45
+ export class Renderer {
46
+ /**
47
+ * @param {HTMLCanvasElement} canvas
48
+ * @param {object} [options]
49
+ * @param {boolean} [options.autoResize] follow the canvas's CSS size
50
+ * with a ResizeObserver, calling setSize automatically (default
51
+ * false — call setSize yourself)
52
+ * @param {boolean} [options.antialias] draw into a 4x multisampled
53
+ * target that resolves to the canvas (default true), smoothing
54
+ * edges; set false to render aliased at slightly lower cost
55
+ */
56
+ constructor(canvas, { autoResize = false, antialias = true } = {}) {
57
+ this.canvas = canvas;
58
+ this.device = null;
59
+ this.context = null;
60
+ /** Preferred non-sRGB format used to configure the canvas context. */
61
+ this.format = null;
62
+ /** Compatible sRGB view format used by color render passes. */
63
+ this.colorFormat = null;
64
+ /** Color-pass instances submitted by the last render(), after culling. */
65
+ this.drawCount = 0;
66
+ /** Shadow-pass instances submitted by the last render(), after culling. */
67
+ this.shadowDrawCount = 0;
68
+
69
+ this._autoResize = autoResize;
70
+ this._sampleCount = antialias
71
+ ? ANTIALIAS_SAMPLE_COUNT
72
+ : SINGLE_SAMPLE_COUNT;
73
+ this._resizeObserver = null;
74
+ this._ownsDevice = false;
75
+ this._deviceLease = null;
76
+ this._initPromise = null;
77
+ this._initVersion = 0;
78
+ this._resources = null;
79
+ this._shadowMap = null;
80
+ this._depthTexture = null;
81
+ this._depthView = null;
82
+ this._msaaTexture = null;
83
+ this._msaaView = null;
84
+ this._targetWidth = 0;
85
+ this._targetHeight = 0;
86
+ this._opaqueList = [];
87
+ this._transparentList = [];
88
+ this._shadowMeshList = [];
89
+ this._shadowCasters = [];
90
+ this._preparedMeshes = new Map();
91
+ this._frameUniformWriter = new FrameUniformWriter();
92
+ this._normalMatrix = new Mat4();
93
+ this._viewFrustum = new Frustum();
94
+ this._shadowFrustum = new Frustum();
95
+ }
96
+
97
+ /**
98
+ * Requests the adapter/device and configures the canvas. Must be
99
+ * awaited before rendering. Pass another already-initialized renderer
100
+ * on the same canvas as `shared` to reuse its GPU device (a canvas
101
+ * can only be configured for one device, so e.g. a Renderer2d and a
102
+ * Renderer driving the same canvas must share one).
103
+ *
104
+ * @param {{device, context, format, colorFormat}} [shared] another renderer,
105
+ * or an initWebGpu() result whose canvas viewFormats remain configured
106
+ */
107
+ async init(shared) {
108
+ // Coalesce overlapping calls so one canvas is never configured with two
109
+ // newly requested devices. Both callers observe the same result.
110
+ if (this._initPromise) return this._initPromise;
111
+ if (this.device) throw new Error('Renderer is already initialized');
112
+
113
+ const version = ++this._initVersion;
114
+ const initialization = this._completeInitialization(shared, version);
115
+ this._initPromise = initialization;
116
+ try {
117
+ return await initialization;
118
+ } finally {
119
+ if (this._initPromise === initialization) {
120
+ this._initPromise = null;
121
+ }
122
+ }
123
+ }
124
+
125
+ async _completeInitialization(shared, version) {
126
+ const renderer = await this._initialize(shared, version);
127
+ if (version !== this._initVersion) {
128
+ throw new Error('Renderer initialization was cancelled by dispose()');
129
+ }
130
+ return renderer;
131
+ }
132
+
133
+ async _initialize(shared, version) {
134
+ const gpu = shared || (await initWebGpu(this.canvas));
135
+ if (version !== this._initVersion) {
136
+ // dispose() was called while the adapter/device request was pending.
137
+ // No newer init can start until this promise settles, so this canvas
138
+ // configuration is ours to release.
139
+ if (!shared) {
140
+ gpu.context.unconfigure();
141
+ gpu.device.destroy();
142
+ }
143
+ throw new Error('Renderer initialization was cancelled by dispose()');
144
+ }
145
+
146
+ let leaseAcquired = false;
147
+ try {
148
+ const lease = acquireDeviceLease(this, gpu, shared);
149
+ leaseAcquired = true;
150
+ this.device = lease.device;
151
+ this.context = lease.context;
152
+ this.format = lease.format;
153
+ this.colorFormat = lease.colorFormat;
154
+
155
+ this._resources = new GpuResources(
156
+ this.device,
157
+ this.colorFormat,
158
+ this._sampleCount,
159
+ );
160
+ this._shadowMap = new DirectionalShadowMap(
161
+ this.device,
162
+ this._resources.shadowBindGroupLayout,
163
+ this._resources.objectBindGroupLayout,
164
+ );
165
+
166
+ this._frameUniformBuffer = this.device.createBuffer({
167
+ size: FRAME_UNIFORM_SIZE,
168
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
169
+ });
170
+ this._frameBindGroup = this.device.createBindGroup({
171
+ layout: this._resources.frameBindGroupLayout,
172
+ entries: [
173
+ {
174
+ binding: SHADER_BINDING.uniforms,
175
+ resource: { buffer: this._frameUniformBuffer },
176
+ },
177
+ ],
178
+ });
179
+
180
+ this.setSize(
181
+ this.canvas.clientWidth || DEFAULT_CANVAS_WIDTH,
182
+ this.canvas.clientHeight || DEFAULT_CANVAS_HEIGHT,
183
+ );
184
+ if (this._autoResize && typeof ResizeObserver !== 'undefined') {
185
+ this._resizeObserver = new ResizeObserver((entries) => {
186
+ const { width, height } = entries[0].contentRect;
187
+ if (width > 0 && height > 0) this.setSize(width, height);
188
+ });
189
+ this._resizeObserver.observe(this.canvas);
190
+ }
191
+ return this;
192
+ } catch (error) {
193
+ if (leaseAcquired) {
194
+ // Roll back membership and every resource created before the error.
195
+ this.dispose();
196
+ } else if (!shared) {
197
+ gpu.context.unconfigure();
198
+ gpu.device.destroy();
199
+ }
200
+ throw error;
201
+ }
202
+ }
203
+
204
+ /** Resizes the drawing buffer and depth texture. Pass CSS pixel dimensions. */
205
+ setSize(width, height) {
206
+ const size = drawingBufferSize(width, height);
207
+ this.canvas.width = size.width;
208
+ this.canvas.height = size.height;
209
+
210
+ if (!this.device) return;
211
+ this._recreateRenderTargets();
212
+ }
213
+
214
+ /** Keeps attachments valid when another renderer resized a shared canvas. */
215
+ _ensureRenderTargets() {
216
+ const sizeChanged =
217
+ this._targetWidth !== this.canvas.width ||
218
+ this._targetHeight !== this.canvas.height;
219
+ const attachmentMissing =
220
+ !this._depthTexture || (this._sampleCount > 1 && !this._msaaTexture);
221
+ if (attachmentMissing || sizeChanged) this._recreateRenderTargets();
222
+ }
223
+
224
+ _recreateRenderTargets() {
225
+ const size = [this.canvas.width, this.canvas.height];
226
+ if (this._depthTexture) this._depthTexture.destroy();
227
+ this._depthTexture = this.device.createTexture({
228
+ size,
229
+ format: DEPTH_FORMAT,
230
+ sampleCount: this._sampleCount,
231
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
232
+ });
233
+ this._depthView = this._depthTexture.createView();
234
+ if (this._msaaTexture) this._msaaTexture.destroy();
235
+ this._msaaTexture = null;
236
+ this._msaaView = null;
237
+ if (this._sampleCount > 1) {
238
+ // The multisampled color target the pass draws into; it resolves
239
+ // to the swap chain texture at the end of every render pass.
240
+ this._msaaTexture = this.device.createTexture({
241
+ size,
242
+ format: this.colorFormat,
243
+ sampleCount: this._sampleCount,
244
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
245
+ });
246
+ this._msaaView = this._msaaTexture.createView();
247
+ }
248
+ this._targetWidth = this.canvas.width;
249
+ this._targetHeight = this.canvas.height;
250
+ }
251
+
252
+ /** Draws one frame of `scene` as seen from `camera`. */
253
+ render(scene, camera) {
254
+ if (!this.device) {
255
+ throw new Error('Renderer.init() must complete before render()');
256
+ }
257
+ this._ensureRenderTargets();
258
+ camera.aspect = this.canvas.width / this.canvas.height;
259
+
260
+ scene.updateWorldMatrix();
261
+ camera.updateMatrices();
262
+
263
+ const directionalLight = this._writeFrameUniforms(scene, camera);
264
+ this._shadowMap.update(directionalLight);
265
+ this._collectMeshes(
266
+ scene,
267
+ camera,
268
+ directionalLight?.shadow?.camera || null,
269
+ );
270
+ this._prepareMeshes();
271
+
272
+ const encoder = this.device.createCommandEncoder();
273
+ this.shadowDrawCount = this._shadowMap.render(
274
+ encoder,
275
+ this._shadowCasters,
276
+ );
277
+ const pass = this._beginRenderPass(encoder, scene.background);
278
+ pass.setBindGroup(SHADER_BIND_GROUP.frame, this._frameBindGroup);
279
+ pass.setBindGroup(SHADER_BIND_GROUP.shadow, this._shadowMap.bindGroup);
280
+ for (const mesh of this._opaqueList) this._drawMesh(pass, mesh);
281
+ for (const mesh of this._transparentList) this._drawMesh(pass, mesh);
282
+
283
+ pass.end();
284
+ this.device.queue.submit([encoder.finish()]);
285
+ }
286
+
287
+ /**
288
+ * Collects camera-visible color meshes and light-visible shadow casters,
289
+ * then orders retained transparent meshes back-to-front.
290
+ */
291
+ _collectMeshes(scene, camera, shadowCamera = null) {
292
+ const {
293
+ _opaqueList: opaque,
294
+ _transparentList: transparent,
295
+ _shadowMeshList: shadowMeshes,
296
+ } = this;
297
+ opaque.length = 0;
298
+ transparent.length = 0;
299
+ shadowMeshes.length = 0;
300
+ this._viewFrustum.setFromViewProjectionMatrix(
301
+ camera.viewProjectionMatrix,
302
+ );
303
+ const collectShadows = this._shadowMap?.enabled && shadowCamera;
304
+ if (collectShadows) {
305
+ this._shadowFrustum.setFromViewProjectionMatrix(
306
+ shadowCamera.viewProjectionMatrix,
307
+ );
308
+ }
309
+
310
+ let drawCount = 0;
311
+ scene.traverseVisible((object) => {
312
+ if (object instanceof Mesh) {
313
+ if (object.isInstanced && object.count === 0) return;
314
+ if (this._intersectsFrustum(object, this._viewFrustum)) {
315
+ (object.material.transparent ? transparent : opaque).push(object);
316
+ drawCount += object.isInstanced ? object.count : 1;
317
+ }
318
+ if (
319
+ collectShadows &&
320
+ object.castShadow &&
321
+ !object.material.transparent &&
322
+ isTriangleTopology(object.material.topology) &&
323
+ this._intersectsFrustum(object, this._shadowFrustum)
324
+ ) {
325
+ shadowMeshes.push(object);
326
+ }
327
+ }
328
+ });
329
+ this.drawCount = drawCount;
330
+ if (transparent.length > 0) {
331
+ const v = camera.viewMatrix.elements;
332
+ for (const mesh of transparent) {
333
+ const w = mesh.worldMatrix.elements;
334
+ // View-space z of the mesh origin; the camera looks down -z,
335
+ // so the most negative depth is the farthest away.
336
+ mesh._viewDepth = v[2] * w[12] + v[6] * w[13] + v[10] * w[14] + v[14];
337
+ }
338
+ transparent.sort((a, b) => a._viewDepth - b._viewDepth);
339
+ }
340
+ }
341
+
342
+ _intersectsFrustum(mesh, frustum) {
343
+ return (
344
+ !mesh.frustumCulled ||
345
+ frustum.intersectsBox(mesh.bounds, mesh.worldMatrix)
346
+ );
347
+ }
348
+
349
+ _beginRenderPass(encoder, background) {
350
+ const swapView = this.context
351
+ .getCurrentTexture()
352
+ .createView({ format: this.colorFormat });
353
+ return encoder.beginRenderPass({
354
+ colorAttachments: [
355
+ colorAttachment(
356
+ this._msaaView,
357
+ swapView,
358
+ linearClearColor(background),
359
+ ),
360
+ ],
361
+ depthStencilAttachment: {
362
+ view: this._depthView,
363
+ depthClearValue: DEPTH_CLEAR_VALUE,
364
+ depthLoadOp: 'clear',
365
+ depthStoreOp: 'store',
366
+ },
367
+ });
368
+ }
369
+
370
+ _writeFrameUniforms(scene, camera) {
371
+ return this._frameUniformWriter.write(
372
+ scene,
373
+ camera,
374
+ this.device,
375
+ this._frameUniformBuffer,
376
+ );
377
+ }
378
+
379
+ /**
380
+ * Releases the resize observer, frame buffer, render targets and this
381
+ * renderer's per-object buffers. A
382
+ * managed shared device stays alive until the last renderer using it
383
+ * is disposed. Geometry and texture caches may be shared; release those
384
+ * separately with geometry.dispose() and texture.dispose(). Call init()
385
+ * again before reusing this renderer.
386
+ */
387
+ dispose() {
388
+ // Invalidates an adapter/device request that has not completed yet.
389
+ this._initVersion++;
390
+ if (this._resizeObserver) {
391
+ this._resizeObserver.disconnect();
392
+ this._resizeObserver = null;
393
+ }
394
+ if (this._frameUniformBuffer) this._frameUniformBuffer.destroy();
395
+ if (this._depthTexture) this._depthTexture.destroy();
396
+ if (this._msaaTexture) this._msaaTexture.destroy();
397
+ if (this._shadowMap) this._shadowMap.dispose();
398
+ if (this._resources) this._resources.dispose();
399
+ this._frameUniformBuffer = null;
400
+ this._depthTexture = null;
401
+ this._depthView = null;
402
+ this._msaaTexture = null;
403
+ this._msaaView = null;
404
+ this._targetWidth = 0;
405
+ this._targetHeight = 0;
406
+ const shouldDestroyDevice = releaseDeviceLease(this);
407
+ if (shouldDestroyDevice && this.device) {
408
+ this.context.unconfigure();
409
+ this.device.destroy();
410
+ }
411
+ this.device = null;
412
+ this.context = null;
413
+ this.format = null;
414
+ this.colorFormat = null;
415
+ this._frameBindGroup = null;
416
+ this._resources = null;
417
+ this._shadowMap = null;
418
+ this._opaqueList.length = 0;
419
+ this._transparentList.length = 0;
420
+ this._shadowMeshList.length = 0;
421
+ this._shadowCasters.length = 0;
422
+ this._preparedMeshes.clear();
423
+ this.drawCount = 0;
424
+ this.shadowDrawCount = 0;
425
+ this._ownsDevice = false;
426
+ }
427
+
428
+ _drawMesh(pass, mesh) {
429
+ const { geometryGPU, meshGPU } = this._preparedMeshes.get(mesh);
430
+ const instanced = !!mesh.isInstanced;
431
+ const pipeline = this._resources.pipelineFor(mesh.material, instanced);
432
+
433
+ pass.setPipeline(pipeline);
434
+ pass.setBindGroup(SHADER_BIND_GROUP.object, meshGPU.bindGroup);
435
+ pass.setVertexBuffer(
436
+ VERTEX_BUFFER_SLOT.geometry,
437
+ geometryGPU.vertexBuffer,
438
+ );
439
+ if (instanced) {
440
+ pass.setVertexBuffer(VERTEX_BUFFER_SLOT.instance, meshGPU.instanceBuffer);
441
+ }
442
+ pass.setIndexBuffer(geometryGPU.indexBuffer, INDEX_FORMAT);
443
+ pass.drawIndexed(mesh.geometry.indexCount, instanced ? mesh.count : 1);
444
+ }
445
+
446
+ /** Uploads object/instance data before either render pass reads it. */
447
+ _prepareMeshes() {
448
+ this._preparedMeshes.clear();
449
+ this._shadowCasters.length = 0;
450
+ for (const mesh of this._opaqueList) this._prepareMesh(mesh);
451
+ for (const mesh of this._transparentList) this._prepareMesh(mesh);
452
+ for (const mesh of this._shadowMeshList) {
453
+ const prepared =
454
+ this._preparedMeshes.get(mesh) || this._prepareMesh(mesh);
455
+ this._shadowCasters.push(prepared);
456
+ }
457
+ }
458
+
459
+ _prepareMesh(mesh) {
460
+ const geometryGPU = this._resources.geometryFor(mesh.geometry);
461
+ const meshGPU = this._resources.meshFor(mesh);
462
+ const data = meshGPU.data;
463
+
464
+ data.set(mesh.worldMatrix.elements, OBJECT_UNIFORM_OFFSET.modelMatrix);
465
+ this._normalMatrix.copy(mesh.worldMatrix);
466
+ if (this._normalMatrix.tryInvert()) {
467
+ this._normalMatrix.transpose();
468
+ } else {
469
+ // A collapsed axis has no inverse normal transform. Keep rendering
470
+ // deterministic; raycasting/dragging skip the singular object.
471
+ this._normalMatrix.identity();
472
+ }
473
+ data.set(
474
+ this._normalMatrix.elements,
475
+ OBJECT_UNIFORM_OFFSET.normalMatrix,
476
+ );
477
+
478
+ const color = mesh.material.color;
479
+ const colorOffset = OBJECT_UNIFORM_OFFSET.color;
480
+ data[colorOffset] = srgbToLinear(color[0]);
481
+ data[colorOffset + 1] = srgbToLinear(color[1]);
482
+ data[colorOffset + 2] = srgbToLinear(color[2]);
483
+ data[colorOffset + 3] = color.length > 3 ? color[3] : 1;
484
+ data.fill(0, OBJECT_UNIFORM_OFFSET.shadowFlags);
485
+ data[OBJECT_UNIFORM_OFFSET.shadowFlags] = mesh.receiveShadow ? 1 : 0;
486
+ this.device.queue.writeBuffer(meshGPU.uniformBuffer, 0, data);
487
+
488
+ const prepared = { mesh, geometryGPU, meshGPU };
489
+ this._preparedMeshes.set(mesh, prepared);
490
+ return prepared;
491
+ }
492
+ }
@@ -0,0 +1,13 @@
1
+ import { Object3d } from './Object3d.js';
2
+
3
+ /**
4
+ * Root of the scene graph. Add meshes and lights to it, then pass it
5
+ * to `renderer.render(scene, camera)`.
6
+ */
7
+ export class Scene extends Object3d {
8
+ constructor() {
9
+ super();
10
+ /** sRGB clear color as [r, g, b, a] in the 0..1 range. */
11
+ this.background = [0.05, 0.06, 0.09, 1];
12
+ }
13
+ }
@@ -0,0 +1,64 @@
1
+ import {
2
+ DEFAULT_DEPTH_COMPARE,
3
+ INDEX_FORMAT,
4
+ SHADER_ENTRY_POINT,
5
+ isStripTopology,
6
+ } from '../../core/pipelineConstants.js';
7
+ import {
8
+ DIRECTIONAL_SHADOW_DEPTH_FORMAT,
9
+ SHADOW_SAMPLE_COUNT,
10
+ } from '../constants.js';
11
+ import { DIRECTIONAL_SHADOW_SHADER } from '../shaders/shadows.js';
12
+ import { vertexBufferLayouts } from '../shaders/vertexLayout.js';
13
+
14
+ /** Compiles and caches the vertex-only pipelines used by shadow casters. */
15
+ export class ShadowPipelines {
16
+ constructor(device, shadowUniformLayout, objectLayout) {
17
+ this.device = device;
18
+ this._cache = new Map();
19
+ this._module = null;
20
+ this._layout = device.createPipelineLayout({
21
+ bindGroupLayouts: [shadowUniformLayout, objectLayout],
22
+ });
23
+ }
24
+
25
+ pipelineFor(material, instanced = false) {
26
+ const { topology, cullMode, frontFace } = material;
27
+ const key = `${topology}|${cullMode}|${frontFace}|${instanced}`;
28
+ let pipeline = this._cache.get(key);
29
+ if (!pipeline) {
30
+ const primitive = { topology, cullMode, frontFace };
31
+ if (isStripTopology(topology)) {
32
+ primitive.stripIndexFormat = INDEX_FORMAT;
33
+ }
34
+ pipeline = this.device.createRenderPipeline({
35
+ layout: this._layout,
36
+ vertex: {
37
+ module: this._moduleForShadowPass(),
38
+ entryPoint: instanced
39
+ ? SHADER_ENTRY_POINT.shadowInstancedVertex
40
+ : SHADER_ENTRY_POINT.shadowVertex,
41
+ buffers: vertexBufferLayouts(instanced),
42
+ },
43
+ primitive,
44
+ multisample: { count: SHADOW_SAMPLE_COUNT },
45
+ depthStencil: {
46
+ format: DIRECTIONAL_SHADOW_DEPTH_FORMAT,
47
+ depthWriteEnabled: true,
48
+ depthCompare: DEFAULT_DEPTH_COMPARE,
49
+ },
50
+ });
51
+ this._cache.set(key, pipeline);
52
+ }
53
+ return pipeline;
54
+ }
55
+
56
+ _moduleForShadowPass() {
57
+ if (!this._module) {
58
+ this._module = this.device.createShaderModule({
59
+ code: DIRECTIONAL_SHADOW_SHADER,
60
+ });
61
+ }
62
+ return this._module;
63
+ }
64
+ }
@@ -0,0 +1,132 @@
1
+ import { srgbToLinear } from '../../math/color.js';
2
+ import { AmbientLight } from '../lights/AmbientLight.js';
3
+ import { DirectionalLight } from '../lights/DirectionalLight.js';
4
+ import { PointLight } from '../lights/PointLight.js';
5
+ import { MAX_POINT_LIGHTS } from '../constants.js';
6
+
7
+ const FLOAT_BYTES = Float32Array.BYTES_PER_ELEMENT;
8
+ const VEC4_FLOATS = 4;
9
+ const MAT4_FLOATS = 4 * VEC4_FLOATS;
10
+ const PADDED_VEC3_FLOATS = VEC4_FLOATS;
11
+ const FRAME_HEADER_FLOATS = MAT4_FLOATS + 3 * PADDED_VEC3_FLOATS;
12
+ const POINT_LIGHT_FLOATS = 2 * PADDED_VEC3_FLOATS;
13
+ const MATRIX_TRANSLATION_OFFSET = 3 * VEC4_FLOATS;
14
+ const POINT_LIGHT_COLOR_OFFSET = PADDED_VEC3_FLOATS;
15
+
16
+ export const FRAME_UNIFORM_SIZE =
17
+ (FRAME_HEADER_FLOATS + MAX_POINT_LIGHTS * POINT_LIGHT_FLOATS) * FLOAT_BYTES;
18
+
19
+ export const OBJECT_UNIFORM_SIZE =
20
+ (2 * MAT4_FLOATS + 2 * VEC4_FLOATS) * FLOAT_BYTES;
21
+
22
+ export const OBJECT_UNIFORM_OFFSET = Object.freeze({
23
+ modelMatrix: 0,
24
+ normalMatrix: MAT4_FLOATS,
25
+ color: 2 * MAT4_FLOATS,
26
+ shadowFlags: 2 * MAT4_FLOATS + VEC4_FLOATS,
27
+ });
28
+
29
+ const FRAME_OFFSET = Object.freeze({
30
+ viewProjection: 0,
31
+ lightDirection: MAT4_FLOATS,
32
+ lightColor: MAT4_FLOATS + PADDED_VEC3_FLOATS,
33
+ ambientColor: MAT4_FLOATS + 2 * PADDED_VEC3_FLOATS,
34
+ pointLightCount: FRAME_HEADER_FLOATS - 1,
35
+ pointLights: FRAME_HEADER_FLOATS,
36
+ });
37
+
38
+ /** Owns the reusable CPU staging array for one renderer's frame uniforms. */
39
+ export class FrameUniformWriter {
40
+ constructor() {
41
+ this.data = new Float32Array(FRAME_UNIFORM_SIZE / FLOAT_BYTES);
42
+ }
43
+
44
+ write(scene, camera, device, buffer) {
45
+ const data = this.data;
46
+ let directionalLight = null;
47
+ let pointLightCount = 0;
48
+ let ambientRed = 0;
49
+ let ambientGreen = 0;
50
+ let ambientBlue = 0;
51
+
52
+ scene.traverseVisible((object) => {
53
+ if (!directionalLight && object instanceof DirectionalLight) {
54
+ directionalLight = object;
55
+ }
56
+ if (object instanceof PointLight && pointLightCount < MAX_POINT_LIGHTS) {
57
+ this._writePointLight(pointLightCount++, object);
58
+ }
59
+ if (object instanceof AmbientLight) {
60
+ ambientRed += srgbToLinear(object.color[0]) * object.intensity;
61
+ ambientGreen += srgbToLinear(object.color[1]) * object.intensity;
62
+ ambientBlue += srgbToLinear(object.color[2]) * object.intensity;
63
+ }
64
+ });
65
+
66
+ data.set(camera.viewProjectionMatrix.elements, FRAME_OFFSET.viewProjection);
67
+ data[FRAME_OFFSET.pointLightCount] = pointLightCount;
68
+ writeVec3(
69
+ data,
70
+ FRAME_OFFSET.ambientColor,
71
+ ambientRed,
72
+ ambientGreen,
73
+ ambientBlue,
74
+ );
75
+ this._writeDirectionalLight(directionalLight);
76
+ device.queue.writeBuffer(buffer, 0, data);
77
+ return directionalLight;
78
+ }
79
+
80
+ _writePointLight(index, light) {
81
+ const base = FRAME_OFFSET.pointLights + index * POINT_LIGHT_FLOATS;
82
+ const world = light.worldMatrix.elements;
83
+ writeVec3(
84
+ this.data,
85
+ base,
86
+ world[MATRIX_TRANSLATION_OFFSET],
87
+ world[MATRIX_TRANSLATION_OFFSET + 1],
88
+ world[MATRIX_TRANSLATION_OFFSET + 2],
89
+ );
90
+ writeLinearColor(
91
+ this.data,
92
+ base + POINT_LIGHT_COLOR_OFFSET,
93
+ light.color,
94
+ light.intensity,
95
+ );
96
+ }
97
+
98
+ _writeDirectionalLight(light) {
99
+ if (!light) {
100
+ writeVec3(this.data, FRAME_OFFSET.lightDirection, 0, -1, 0);
101
+ writeVec3(this.data, FRAME_OFFSET.lightColor, 0, 0, 0);
102
+ return;
103
+ }
104
+
105
+ const length = light.direction.length() || 1;
106
+ writeVec3(
107
+ this.data,
108
+ FRAME_OFFSET.lightDirection,
109
+ light.direction.x / length,
110
+ light.direction.y / length,
111
+ light.direction.z / length,
112
+ );
113
+ writeLinearColor(
114
+ this.data,
115
+ FRAME_OFFSET.lightColor,
116
+ light.color,
117
+ light.intensity,
118
+ );
119
+ }
120
+ }
121
+
122
+ function writeVec3(target, offset, x, y, z) {
123
+ target[offset] = x;
124
+ target[offset + 1] = y;
125
+ target[offset + 2] = z;
126
+ }
127
+
128
+ function writeLinearColor(target, offset, color, intensity) {
129
+ target[offset] = srgbToLinear(color[0]) * intensity;
130
+ target[offset + 1] = srgbToLinear(color[1]) * intensity;
131
+ target[offset + 2] = srgbToLinear(color[2]) * intensity;
132
+ }