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.
- package/README.md +556 -0
- package/package.json +37 -0
- package/src/2d/cameras/Camera2d.js +46 -0
- package/src/2d/constants.js +3 -0
- package/src/2d/controls/DragControls2d.js +174 -0
- package/src/2d/controls/PanZoomControls.js +64 -0
- package/src/2d/core/GpuResources2d.js +163 -0
- package/src/2d/core/InstancedShape2d.js +74 -0
- package/src/2d/core/Object2d.js +107 -0
- package/src/2d/core/Pipelines2d.js +110 -0
- package/src/2d/core/Renderer2d.js +353 -0
- package/src/2d/core/Scene2d.js +13 -0
- package/src/2d/core/Shape2d.js +13 -0
- package/src/2d/core/Uniforms2d.js +13 -0
- package/src/2d/geometries/CircleGeometry.js +27 -0
- package/src/2d/geometries/Geometry2d.js +108 -0
- package/src/2d/geometries/RectGeometry.js +23 -0
- package/src/2d/materials/BasicMaterial2d.js +12 -0
- package/src/2d/materials/Material2d.js +78 -0
- package/src/2d/materials/SpriteMaterial2d.js +28 -0
- package/src/2d/shaders/fragments.js +23 -0
- package/src/2d/shaders/shared.js +28 -0
- package/src/2d/shaders/vertexLayout.js +74 -0
- package/src/2d/shaders/vertexStages.js +59 -0
- package/src/3d/cameras/Camera.js +66 -0
- package/src/3d/cameras/OrthographicCamera.js +35 -0
- package/src/3d/cameras/PerspectiveCamera.js +25 -0
- package/src/3d/constants.js +18 -0
- package/src/3d/controls/DragControls.js +168 -0
- package/src/3d/controls/OrbitControls.js +125 -0
- package/src/3d/core/DirectionalShadowMap.js +283 -0
- package/src/3d/core/GpuResources.js +174 -0
- package/src/3d/core/InstanceMatrix.js +55 -0
- package/src/3d/core/InstancedBounds.js +114 -0
- package/src/3d/core/InstancedMesh.js +124 -0
- package/src/3d/core/Mesh.js +26 -0
- package/src/3d/core/Object3d.js +101 -0
- package/src/3d/core/Pipelines.js +125 -0
- package/src/3d/core/RayIntersection.js +184 -0
- package/src/3d/core/Raycaster.js +246 -0
- package/src/3d/core/Renderer.js +492 -0
- package/src/3d/core/Scene.js +13 -0
- package/src/3d/core/ShadowPipelines.js +64 -0
- package/src/3d/core/Uniforms.js +132 -0
- package/src/3d/geometries/BoxGeometry.js +56 -0
- package/src/3d/geometries/Geometry.js +97 -0
- package/src/3d/geometries/PlaneGeometry.js +24 -0
- package/src/3d/geometries/SphereGeometry.js +46 -0
- package/src/3d/geometries/WireframeGeometry.js +39 -0
- package/src/3d/helpers/GridHelper.js +43 -0
- package/src/3d/lights/AmbientLight.js +18 -0
- package/src/3d/lights/DirectionalLight.js +26 -0
- package/src/3d/lights/DirectionalShadow.js +29 -0
- package/src/3d/lights/PointLight.js +23 -0
- package/src/3d/materials/BasicMaterial.js +11 -0
- package/src/3d/materials/LambertMaterial.js +13 -0
- package/src/3d/materials/Material.js +87 -0
- package/src/3d/materials/TextureMaterial.js +24 -0
- package/src/3d/shaders/fragments.js +34 -0
- package/src/3d/shaders/shadows.js +52 -0
- package/src/3d/shaders/shared.js +129 -0
- package/src/3d/shaders/vertexLayout.js +85 -0
- package/src/3d/shaders/vertexStages.js +70 -0
- package/src/core/PointerControls.js +258 -0
- package/src/core/Texture.js +87 -0
- package/src/core/createMaterialPipelineLayouts.js +114 -0
- package/src/core/deviceLease.js +67 -0
- package/src/core/generateMipmaps.js +115 -0
- package/src/core/indexedTriangles.js +53 -0
- package/src/core/initWebGpu.js +65 -0
- package/src/core/materialResources.js +18 -0
- package/src/core/objectGpuResources.js +68 -0
- package/src/core/pipelineConstants.js +68 -0
- package/src/core/rendererConfig.js +66 -0
- package/src/core/uploadTexture.js +57 -0
- package/src/index.js +68 -0
- package/src/math/Frustum.js +143 -0
- package/src/math/Mat3.js +165 -0
- package/src/math/Mat4.js +359 -0
- package/src/math/Vec2.js +72 -0
- package/src/math/Vec3.js +98 -0
- 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
|
+
}
|