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,353 @@
|
|
|
1
|
+
import { Shape2d } from './Shape2d.js';
|
|
2
|
+
import { GpuResources2d } from './GpuResources2d.js';
|
|
3
|
+
import {
|
|
4
|
+
FRAME_UNIFORM_SIZE_2D,
|
|
5
|
+
OBJECT_UNIFORM_OFFSET_2D,
|
|
6
|
+
} from './Uniforms2d.js';
|
|
7
|
+
import { initWebGpu } from '../../core/initWebGpu.js';
|
|
8
|
+
import { srgbToLinear } from '../../math/color.js';
|
|
9
|
+
import {
|
|
10
|
+
ANTIALIAS_SAMPLE_COUNT,
|
|
11
|
+
DEFAULT_CANVAS_HEIGHT,
|
|
12
|
+
DEFAULT_CANVAS_WIDTH,
|
|
13
|
+
SINGLE_SAMPLE_COUNT,
|
|
14
|
+
colorAttachment,
|
|
15
|
+
drawingBufferSize,
|
|
16
|
+
linearClearColor,
|
|
17
|
+
} from '../../core/rendererConfig.js';
|
|
18
|
+
import {
|
|
19
|
+
acquireDeviceLease,
|
|
20
|
+
releaseDeviceLease,
|
|
21
|
+
} from '../../core/deviceLease.js';
|
|
22
|
+
import {
|
|
23
|
+
INDEX_FORMAT,
|
|
24
|
+
SHADER_BIND_GROUP,
|
|
25
|
+
SHADER_BINDING,
|
|
26
|
+
VERTEX_BUFFER_SLOT,
|
|
27
|
+
} from '../../core/pipelineConstants.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The 2D renderer. Same shape as the 3D Renderer — one render pass,
|
|
31
|
+
* lazy GPU resources — but with everything a flat scene doesn't need
|
|
32
|
+
* removed: there is no depth buffer. Draw order comes from sorting
|
|
33
|
+
* shapes by `zIndex` (painter's algorithm) and alpha blending is on,
|
|
34
|
+
* so transparent shapes work out of the box.
|
|
35
|
+
*
|
|
36
|
+
* Usage:
|
|
37
|
+
* const renderer2d = new Renderer2d(canvas);
|
|
38
|
+
* await renderer2d.init();
|
|
39
|
+
* renderer2d.render(scene2d, camera2d);
|
|
40
|
+
*
|
|
41
|
+
* To share a canvas with a 3D Renderer, initialize one of them first
|
|
42
|
+
* and pass it to the other's `init` so they use the same GPU device:
|
|
43
|
+
* await renderer2d.init(renderer3d);
|
|
44
|
+
*/
|
|
45
|
+
export class Renderer2d {
|
|
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
|
+
/** How many shapes the last render() call drew. */
|
|
65
|
+
this.drawCount = 0;
|
|
66
|
+
|
|
67
|
+
this._autoResize = autoResize;
|
|
68
|
+
this._sampleCount = antialias
|
|
69
|
+
? ANTIALIAS_SAMPLE_COUNT
|
|
70
|
+
: SINGLE_SAMPLE_COUNT;
|
|
71
|
+
this._resizeObserver = null;
|
|
72
|
+
this._ownsDevice = false;
|
|
73
|
+
this._deviceLease = null;
|
|
74
|
+
this._initPromise = null;
|
|
75
|
+
this._initVersion = 0;
|
|
76
|
+
this._resources = null;
|
|
77
|
+
this._msaaTexture = null;
|
|
78
|
+
this._msaaView = null;
|
|
79
|
+
this._targetWidth = 0;
|
|
80
|
+
this._targetHeight = 0;
|
|
81
|
+
this._drawList = [];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Requests the adapter/device and configures the canvas. Must be
|
|
86
|
+
* awaited before rendering. Pass another already-initialized renderer
|
|
87
|
+
* on the same canvas as `shared` to reuse its GPU device.
|
|
88
|
+
*
|
|
89
|
+
* @param {{device, context, format, colorFormat}} [shared] another renderer,
|
|
90
|
+
* or an initWebGpu() result whose canvas viewFormats remain configured
|
|
91
|
+
*/
|
|
92
|
+
async init(shared) {
|
|
93
|
+
// Coalesce overlapping calls so one canvas is never configured with two
|
|
94
|
+
// newly requested devices. Both callers observe the same result.
|
|
95
|
+
if (this._initPromise) return this._initPromise;
|
|
96
|
+
if (this.device) throw new Error('Renderer2d is already initialized');
|
|
97
|
+
|
|
98
|
+
const version = ++this._initVersion;
|
|
99
|
+
const initialization = this._completeInitialization(shared, version);
|
|
100
|
+
this._initPromise = initialization;
|
|
101
|
+
try {
|
|
102
|
+
return await initialization;
|
|
103
|
+
} finally {
|
|
104
|
+
if (this._initPromise === initialization) {
|
|
105
|
+
this._initPromise = null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async _completeInitialization(shared, version) {
|
|
111
|
+
const renderer = await this._initialize(shared, version);
|
|
112
|
+
if (version !== this._initVersion) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
'Renderer2d initialization was cancelled by dispose()',
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return renderer;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async _initialize(shared, version) {
|
|
121
|
+
const gpu = shared || (await initWebGpu(this.canvas));
|
|
122
|
+
if (version !== this._initVersion) {
|
|
123
|
+
if (!shared) {
|
|
124
|
+
gpu.context.unconfigure();
|
|
125
|
+
gpu.device.destroy();
|
|
126
|
+
}
|
|
127
|
+
throw new Error(
|
|
128
|
+
'Renderer2d initialization was cancelled by dispose()',
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let leaseAcquired = false;
|
|
133
|
+
try {
|
|
134
|
+
const lease = acquireDeviceLease(this, gpu, shared);
|
|
135
|
+
leaseAcquired = true;
|
|
136
|
+
this.device = lease.device;
|
|
137
|
+
this.context = lease.context;
|
|
138
|
+
this.format = lease.format;
|
|
139
|
+
this.colorFormat = lease.colorFormat;
|
|
140
|
+
|
|
141
|
+
this._resources = new GpuResources2d(
|
|
142
|
+
this.device,
|
|
143
|
+
this.colorFormat,
|
|
144
|
+
this._sampleCount,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
this._frameUniformBuffer = this.device.createBuffer({
|
|
148
|
+
size: FRAME_UNIFORM_SIZE_2D,
|
|
149
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
150
|
+
});
|
|
151
|
+
this._frameBindGroup = this.device.createBindGroup({
|
|
152
|
+
layout: this._resources.frameBindGroupLayout,
|
|
153
|
+
entries: [
|
|
154
|
+
{
|
|
155
|
+
binding: SHADER_BINDING.uniforms,
|
|
156
|
+
resource: { buffer: this._frameUniformBuffer },
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
this.setSize(
|
|
162
|
+
this.canvas.clientWidth || DEFAULT_CANVAS_WIDTH,
|
|
163
|
+
this.canvas.clientHeight || DEFAULT_CANVAS_HEIGHT,
|
|
164
|
+
);
|
|
165
|
+
if (this._autoResize && typeof ResizeObserver !== 'undefined') {
|
|
166
|
+
this._resizeObserver = new ResizeObserver((entries) => {
|
|
167
|
+
const { width, height } = entries[0].contentRect;
|
|
168
|
+
if (width > 0 && height > 0) this.setSize(width, height);
|
|
169
|
+
});
|
|
170
|
+
this._resizeObserver.observe(this.canvas);
|
|
171
|
+
}
|
|
172
|
+
return this;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (leaseAcquired) {
|
|
175
|
+
this.dispose();
|
|
176
|
+
} else if (!shared) {
|
|
177
|
+
gpu.context.unconfigure();
|
|
178
|
+
gpu.device.destroy();
|
|
179
|
+
}
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Resizes the drawing buffer. Pass CSS pixel dimensions. */
|
|
185
|
+
setSize(width, height) {
|
|
186
|
+
const size = drawingBufferSize(width, height);
|
|
187
|
+
this.canvas.width = size.width;
|
|
188
|
+
this.canvas.height = size.height;
|
|
189
|
+
|
|
190
|
+
if (!this.device) return;
|
|
191
|
+
this._recreateRenderTargets();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Keeps MSAA valid when another renderer resized a shared canvas. */
|
|
195
|
+
_ensureRenderTargets() {
|
|
196
|
+
const sizeChanged =
|
|
197
|
+
this._targetWidth !== this.canvas.width ||
|
|
198
|
+
this._targetHeight !== this.canvas.height;
|
|
199
|
+
const attachmentMissing = this._sampleCount > 1 && !this._msaaTexture;
|
|
200
|
+
if (sizeChanged || attachmentMissing) this._recreateRenderTargets();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
_recreateRenderTargets() {
|
|
204
|
+
if (this._msaaTexture) this._msaaTexture.destroy();
|
|
205
|
+
this._msaaTexture = null;
|
|
206
|
+
this._msaaView = null;
|
|
207
|
+
if (this._sampleCount > 1) {
|
|
208
|
+
// The multisampled color target the pass draws into; it resolves
|
|
209
|
+
// to the swap chain texture at the end of every render pass.
|
|
210
|
+
this._msaaTexture = this.device.createTexture({
|
|
211
|
+
size: [this.canvas.width, this.canvas.height],
|
|
212
|
+
format: this.colorFormat,
|
|
213
|
+
sampleCount: this._sampleCount,
|
|
214
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
215
|
+
});
|
|
216
|
+
this._msaaView = this._msaaTexture.createView();
|
|
217
|
+
}
|
|
218
|
+
this._targetWidth = this.canvas.width;
|
|
219
|
+
this._targetHeight = this.canvas.height;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Draws one frame of `scene` as seen from `camera`. */
|
|
223
|
+
render(scene, camera) {
|
|
224
|
+
if (!this.device) {
|
|
225
|
+
throw new Error('Renderer2d.init() must complete before render()');
|
|
226
|
+
}
|
|
227
|
+
this._ensureRenderTargets();
|
|
228
|
+
camera.aspect = this.canvas.width / this.canvas.height;
|
|
229
|
+
|
|
230
|
+
scene.updateWorldMatrix();
|
|
231
|
+
camera.updateMatrices();
|
|
232
|
+
|
|
233
|
+
this.device.queue.writeBuffer(
|
|
234
|
+
this._frameUniformBuffer,
|
|
235
|
+
0,
|
|
236
|
+
camera.viewProjectionMatrix.elements,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const drawList = this._collectShapes(scene);
|
|
240
|
+
|
|
241
|
+
const encoder = this.device.createCommandEncoder();
|
|
242
|
+
const pass = this._beginRenderPass(encoder, scene.background);
|
|
243
|
+
|
|
244
|
+
pass.setBindGroup(SHADER_BIND_GROUP.frame, this._frameBindGroup);
|
|
245
|
+
for (const shape of drawList) {
|
|
246
|
+
this._drawShape(pass, shape);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
pass.end();
|
|
250
|
+
this.device.queue.submit([encoder.finish()]);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Collects visible shapes in painter's-algorithm order. */
|
|
254
|
+
_collectShapes(scene) {
|
|
255
|
+
const drawList = this._drawList;
|
|
256
|
+
drawList.length = 0;
|
|
257
|
+
scene.traverseVisible((object) => {
|
|
258
|
+
if (object instanceof Shape2d) drawList.push(object);
|
|
259
|
+
});
|
|
260
|
+
// sort() is stable, so equal zIndex values preserve scene order.
|
|
261
|
+
drawList.sort((a, b) => a.zIndex - b.zIndex);
|
|
262
|
+
this.drawCount = drawList.reduce(
|
|
263
|
+
(count, shape) => count + (shape.isInstanced ? shape.count : 1),
|
|
264
|
+
0,
|
|
265
|
+
);
|
|
266
|
+
return drawList;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
_beginRenderPass(encoder, background) {
|
|
270
|
+
const swapView = this.context
|
|
271
|
+
.getCurrentTexture()
|
|
272
|
+
.createView({ format: this.colorFormat });
|
|
273
|
+
return encoder.beginRenderPass({
|
|
274
|
+
colorAttachments: [
|
|
275
|
+
colorAttachment(
|
|
276
|
+
this._msaaView,
|
|
277
|
+
swapView,
|
|
278
|
+
linearClearColor(background),
|
|
279
|
+
),
|
|
280
|
+
],
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Releases the resize observer, frame buffer, render targets and this
|
|
286
|
+
* renderer's per-object buffers. A
|
|
287
|
+
* managed shared device stays alive until the last renderer using it
|
|
288
|
+
* is disposed. Geometry and texture caches may be shared; release those
|
|
289
|
+
* separately with geometry.dispose() and texture.dispose(). Call init()
|
|
290
|
+
* again before reusing this renderer.
|
|
291
|
+
*/
|
|
292
|
+
dispose() {
|
|
293
|
+
// Invalidates an adapter/device request that has not completed yet.
|
|
294
|
+
this._initVersion++;
|
|
295
|
+
if (this._resizeObserver) {
|
|
296
|
+
this._resizeObserver.disconnect();
|
|
297
|
+
this._resizeObserver = null;
|
|
298
|
+
}
|
|
299
|
+
if (this._frameUniformBuffer) this._frameUniformBuffer.destroy();
|
|
300
|
+
if (this._msaaTexture) this._msaaTexture.destroy();
|
|
301
|
+
if (this._resources) this._resources.dispose();
|
|
302
|
+
this._frameUniformBuffer = null;
|
|
303
|
+
this._msaaTexture = null;
|
|
304
|
+
this._msaaView = null;
|
|
305
|
+
this._targetWidth = 0;
|
|
306
|
+
this._targetHeight = 0;
|
|
307
|
+
const shouldDestroyDevice = releaseDeviceLease(this);
|
|
308
|
+
if (shouldDestroyDevice && this.device) {
|
|
309
|
+
this.context.unconfigure();
|
|
310
|
+
this.device.destroy();
|
|
311
|
+
}
|
|
312
|
+
this.device = null;
|
|
313
|
+
this.context = null;
|
|
314
|
+
this.format = null;
|
|
315
|
+
this.colorFormat = null;
|
|
316
|
+
this._frameBindGroup = null;
|
|
317
|
+
this._resources = null;
|
|
318
|
+
this._drawList.length = 0;
|
|
319
|
+
this._ownsDevice = false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
_drawShape(pass, shape) {
|
|
323
|
+
const instanced = !!shape.isInstanced;
|
|
324
|
+
const geometryGPU = this._resources.geometryFor(shape.geometry);
|
|
325
|
+
const shapeGPU = this._resources.shapeFor(shape);
|
|
326
|
+
const pipeline = this._resources.pipelineFor(shape.material, instanced);
|
|
327
|
+
|
|
328
|
+
// Per-object uniforms: world transform, color.
|
|
329
|
+
const data = shapeGPU.data;
|
|
330
|
+
data.set(shape.worldMatrix.elements, OBJECT_UNIFORM_OFFSET_2D.transform);
|
|
331
|
+
const color = shape.material.color;
|
|
332
|
+
// Colors are authored in sRGB; shaders and attachment blending use linear
|
|
333
|
+
// values, then the sRGB color target encodes the stored canvas pixels.
|
|
334
|
+
const colorOffset = OBJECT_UNIFORM_OFFSET_2D.color;
|
|
335
|
+
data[colorOffset] = srgbToLinear(color[0]);
|
|
336
|
+
data[colorOffset + 1] = srgbToLinear(color[1]);
|
|
337
|
+
data[colorOffset + 2] = srgbToLinear(color[2]);
|
|
338
|
+
data[colorOffset + 3] = color.length > 3 ? color[3] : 1;
|
|
339
|
+
this.device.queue.writeBuffer(shapeGPU.uniformBuffer, 0, data);
|
|
340
|
+
|
|
341
|
+
pass.setPipeline(pipeline);
|
|
342
|
+
pass.setBindGroup(SHADER_BIND_GROUP.object, shapeGPU.bindGroup);
|
|
343
|
+
pass.setVertexBuffer(
|
|
344
|
+
VERTEX_BUFFER_SLOT.geometry,
|
|
345
|
+
geometryGPU.vertexBuffer,
|
|
346
|
+
);
|
|
347
|
+
if (instanced) {
|
|
348
|
+
pass.setVertexBuffer(VERTEX_BUFFER_SLOT.instance, shapeGPU.instanceBuffer);
|
|
349
|
+
}
|
|
350
|
+
pass.setIndexBuffer(geometryGPU.indexBuffer, INDEX_FORMAT);
|
|
351
|
+
pass.drawIndexed(shape.geometry.indexCount, instanced ? shape.count : 1);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Object2d } from './Object2d.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Root of the 2D scene graph. Add shapes to it, then pass it to
|
|
5
|
+
* `renderer2d.render(scene, camera)`.
|
|
6
|
+
*/
|
|
7
|
+
export class Scene2d extends Object2d {
|
|
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,13 @@
|
|
|
1
|
+
import { Object2d } from './Object2d.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A renderable 2D object: a Geometry2d (the outline) combined with a
|
|
5
|
+
* Material2d (how it is filled) — the 2D counterpart of Mesh.
|
|
6
|
+
*/
|
|
7
|
+
export class Shape2d extends Object2d {
|
|
8
|
+
constructor(geometry, material) {
|
|
9
|
+
super();
|
|
10
|
+
this.geometry = geometry;
|
|
11
|
+
this.material = material;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const FLOAT_BYTES = Float32Array.BYTES_PER_ELEMENT;
|
|
2
|
+
const VEC4_FLOATS = 4;
|
|
3
|
+
const MAT3_COLUMNS = 3;
|
|
4
|
+
const PADDED_MAT3_FLOATS = MAT3_COLUMNS * VEC4_FLOATS;
|
|
5
|
+
|
|
6
|
+
export const FRAME_UNIFORM_SIZE_2D = PADDED_MAT3_FLOATS * FLOAT_BYTES;
|
|
7
|
+
export const OBJECT_UNIFORM_SIZE_2D =
|
|
8
|
+
(PADDED_MAT3_FLOATS + VEC4_FLOATS) * FLOAT_BYTES;
|
|
9
|
+
|
|
10
|
+
export const OBJECT_UNIFORM_OFFSET_2D = Object.freeze({
|
|
11
|
+
transform: 0,
|
|
12
|
+
color: PADDED_MAT3_FLOATS,
|
|
13
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Geometry2d } from './Geometry2d.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A circle centered on the origin, built as a triangle fan.
|
|
5
|
+
*/
|
|
6
|
+
export class CircleGeometry extends Geometry2d {
|
|
7
|
+
constructor(radius = 0.5, segments = 32) {
|
|
8
|
+
// Center vertex, then one vertex per rim segment.
|
|
9
|
+
const vertices = [0, 0, 0.5, 0.5];
|
|
10
|
+
const indices = [];
|
|
11
|
+
for (let i = 0; i < segments; i++) {
|
|
12
|
+
const angle = (i / segments) * Math.PI * 2;
|
|
13
|
+
const c = Math.cos(angle),
|
|
14
|
+
s = Math.sin(angle);
|
|
15
|
+
vertices.push(radius * c, radius * s, 0.5 + c * 0.5, 0.5 + s * 0.5);
|
|
16
|
+
indices.push(0, 1 + i, 1 + ((i + 1) % segments));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
super(vertices, indices);
|
|
20
|
+
this.radius = radius;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Exact circle test — tighter than the bounding-box default. */
|
|
24
|
+
containsPoint(x, y) {
|
|
25
|
+
return x * x + y * y <= this.radius * this.radius;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Holds the raw 2D shape data on the CPU side.
|
|
3
|
+
*
|
|
4
|
+
* Vertices are interleaved as 4 floats each:
|
|
5
|
+
* position (x, y), uv (u, v)
|
|
6
|
+
* so one GPU vertex buffer with a 16-byte stride serves both attributes
|
|
7
|
+
* (half the data of a 3D vertex — no z, no normal).
|
|
8
|
+
*
|
|
9
|
+
* Renderers lazily create GPU buffers the first time the geometry is
|
|
10
|
+
* drawn. `_gpu` holds one cache entry per GPUDevice, so the same CPU
|
|
11
|
+
* geometry can safely be shared by independently initialized renderers.
|
|
12
|
+
*/
|
|
13
|
+
export class Geometry2d {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Float32Array|number[]} vertices interleaved vertex data
|
|
16
|
+
* @param {Uint32Array|number[]} indices triangle indices
|
|
17
|
+
*/
|
|
18
|
+
constructor(vertices, indices) {
|
|
19
|
+
this.vertices =
|
|
20
|
+
vertices instanceof Float32Array ? vertices : new Float32Array(vertices);
|
|
21
|
+
this.indices =
|
|
22
|
+
indices instanceof Uint32Array ? indices : new Uint32Array(indices);
|
|
23
|
+
this._gpu = null;
|
|
24
|
+
this._bounds = null;
|
|
25
|
+
this._needsUpdate = false;
|
|
26
|
+
// Each cache entry records the revision it last uploaded. A revision
|
|
27
|
+
// cannot be replaced by one boolean when several devices share this data.
|
|
28
|
+
this._gpuRevision = 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Set to true after editing `vertices` or `indices` in place: the
|
|
33
|
+
* renderer re-uploads the GPU buffers on the next draw and the cached
|
|
34
|
+
* bounds are recomputed. The arrays must keep their length — to
|
|
35
|
+
* change the vertex or index count, make a new geometry.
|
|
36
|
+
*/
|
|
37
|
+
get needsUpdate() {
|
|
38
|
+
return this._needsUpdate;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
set needsUpdate(value) {
|
|
42
|
+
this._needsUpdate = Boolean(value);
|
|
43
|
+
if (this._needsUpdate) {
|
|
44
|
+
this._gpuRevision++;
|
|
45
|
+
this._bounds = null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Local-space axis-aligned bounding box `{ min, max }`, computed from
|
|
51
|
+
* the vertex positions on first access. Used for picking.
|
|
52
|
+
*/
|
|
53
|
+
get bounds() {
|
|
54
|
+
if (!this._bounds) {
|
|
55
|
+
const min = [Infinity, Infinity];
|
|
56
|
+
const max = [-Infinity, -Infinity];
|
|
57
|
+
for (let i = 0; i < this.vertices.length; i += VERTEX_SIZE_2D) {
|
|
58
|
+
for (let a = 0; a < 2; a++) {
|
|
59
|
+
const v = this.vertices[i + a];
|
|
60
|
+
if (v < min[a]) min[a] = v;
|
|
61
|
+
if (v > max[a]) max[a] = v;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
this._bounds = { min, max };
|
|
65
|
+
}
|
|
66
|
+
return this._bounds;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Whether a local-space point lies inside the shape. The base class
|
|
71
|
+
* tests the bounding box; subclasses can be exact (see CircleGeometry).
|
|
72
|
+
* This is all 2D picking needs — no raycaster required.
|
|
73
|
+
*/
|
|
74
|
+
containsPoint(x, y) {
|
|
75
|
+
const { min, max } = this.bounds;
|
|
76
|
+
return x >= min[0] && x <= max[0] && y >= min[1] && y <= max[1];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
get vertexCount() {
|
|
80
|
+
return this.vertices.length / VERTEX_SIZE_2D;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get indexCount() {
|
|
84
|
+
return this.indices.length;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Destroys the GPU vertex/index buffers (if any), releasing the
|
|
89
|
+
* memory right away instead of waiting for GC. Call it when nothing
|
|
90
|
+
* draws this geometry anymore; drawing it again re-uploads.
|
|
91
|
+
*/
|
|
92
|
+
dispose() {
|
|
93
|
+
if (this._gpu) {
|
|
94
|
+
for (const { vertexBuffer, indexBuffer } of this._gpu.values()) {
|
|
95
|
+
vertexBuffer.destroy();
|
|
96
|
+
indexBuffer.destroy();
|
|
97
|
+
}
|
|
98
|
+
this._gpu = null;
|
|
99
|
+
}
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Number of floats per vertex (2 position + 2 uv). */
|
|
105
|
+
export const VERTEX_SIZE_2D = 4;
|
|
106
|
+
/** Byte stride of one vertex in the GPU buffer. */
|
|
107
|
+
export const VERTEX_STRIDE_2D =
|
|
108
|
+
VERTEX_SIZE_2D * Float32Array.BYTES_PER_ELEMENT;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Geometry2d } from './Geometry2d.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A rectangle centered on the origin.
|
|
5
|
+
*/
|
|
6
|
+
export class RectGeometry extends Geometry2d {
|
|
7
|
+
constructor(width = 1, height = 1) {
|
|
8
|
+
const w = width / 2;
|
|
9
|
+
const h = height / 2;
|
|
10
|
+
|
|
11
|
+
// prettier-ignore
|
|
12
|
+
const vertices = [
|
|
13
|
+
// position uv
|
|
14
|
+
-w, -h, 0, 0,
|
|
15
|
+
w, -h, 1, 0,
|
|
16
|
+
w, h, 1, 1,
|
|
17
|
+
-w, h, 0, 1,
|
|
18
|
+
];
|
|
19
|
+
const indices = [0, 1, 2, 0, 2, 3];
|
|
20
|
+
|
|
21
|
+
super(vertices, indices);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Material2d } from './Material2d.js';
|
|
2
|
+
import { BASIC_FRAGMENT_SHADER_2D } from '../shaders/fragments.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fills the shape with a flat color. Alpha below 1 shows through to
|
|
6
|
+
* whatever was drawn underneath.
|
|
7
|
+
*/
|
|
8
|
+
export class BasicMaterial2d extends Material2d {
|
|
9
|
+
get fragmentShader() {
|
|
10
|
+
return BASIC_FRAGMENT_SHADER_2D;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_CULL_MODE_2D,
|
|
3
|
+
DEFAULT_FRONT_FACE,
|
|
4
|
+
DEFAULT_PRIMITIVE_TOPOLOGY,
|
|
5
|
+
} from '../../core/pipelineConstants.js';
|
|
6
|
+
import {
|
|
7
|
+
INSTANCED_SHAPE_SHADER_PREFIX,
|
|
8
|
+
SHAPE_SHADER_PREFIX,
|
|
9
|
+
} from '../shaders/vertexStages.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Base class for 2D materials. It owns per-object parameters and pipeline
|
|
13
|
+
* state; reusable WGSL stages live in `2d/shaders`.
|
|
14
|
+
*
|
|
15
|
+
* Every 2D shader shares the same uniform interface:
|
|
16
|
+
* @group(0) — per-frame data (the camera's view-projection Mat3)
|
|
17
|
+
* @group(1) — per-object data (world transform + color)
|
|
18
|
+
* There are no lights or normals. Mat3 uniforms use WGSL's padded
|
|
19
|
+
* 16-byte column layout, which is the layout `Mat3.elements` stores.
|
|
20
|
+
*/
|
|
21
|
+
export class Material2d {
|
|
22
|
+
/**
|
|
23
|
+
* @param {object} [options]
|
|
24
|
+
* @param {number[]} [options.color] [r, g, b] or [r, g, b, a] in 0..1 —
|
|
25
|
+
* alpha below 1 blends with what is behind
|
|
26
|
+
* @param {Texture} [options.map] texture the shader can sample at the
|
|
27
|
+
* shape's uvs — only used when `usesMap` is true
|
|
28
|
+
* @param {boolean} [options.usesMap] whether the fragment shader declares
|
|
29
|
+
* the map and sampler bindings. It defaults to whether `map` was supplied
|
|
30
|
+
* for compatibility; custom material subclasses should set it explicitly.
|
|
31
|
+
* It is fixed for the material's lifetime so replacing or clearing `map`
|
|
32
|
+
* cannot silently change its pipeline layout
|
|
33
|
+
* @param {GPUPrimitiveTopology} [options.topology]
|
|
34
|
+
* 'triangle-list' (default), 'triangle-strip', 'line-list',
|
|
35
|
+
* 'line-strip' or 'point-list'
|
|
36
|
+
* @param {GPUCullMode} [options.cullMode] 'none' (default — a negative
|
|
37
|
+
* scale flips a shape's winding and it should stay visible), 'back'
|
|
38
|
+
* or 'front'; only applies to triangle topologies
|
|
39
|
+
* @param {GPUFrontFace} [options.frontFace] 'ccw' (default) or 'cw'
|
|
40
|
+
*/
|
|
41
|
+
constructor({
|
|
42
|
+
color = [1, 1, 1],
|
|
43
|
+
map = null,
|
|
44
|
+
usesMap = map !== null,
|
|
45
|
+
topology = DEFAULT_PRIMITIVE_TOPOLOGY,
|
|
46
|
+
cullMode = DEFAULT_CULL_MODE_2D,
|
|
47
|
+
frontFace = DEFAULT_FRONT_FACE,
|
|
48
|
+
} = {}) {
|
|
49
|
+
this.color = color;
|
|
50
|
+
this.map = map;
|
|
51
|
+
Object.defineProperty(this, 'usesMap', {
|
|
52
|
+
value: Boolean(usesMap),
|
|
53
|
+
enumerable: true,
|
|
54
|
+
});
|
|
55
|
+
this.topology = topology;
|
|
56
|
+
this.cullMode = cullMode;
|
|
57
|
+
this.frontFace = frontFace;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Full WGSL source with `vs` and `fs` entry points. */
|
|
61
|
+
get shaderCode() {
|
|
62
|
+
return Material2d.SHARED_WGSL + this.fragmentShader;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Full WGSL source whose vertex stage reads per-instance data. */
|
|
66
|
+
get instancedShaderCode() {
|
|
67
|
+
return Material2d.INSTANCED_WGSL + this.fragmentShader;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Fragment-stage WGSL supplied by a concrete material. */
|
|
71
|
+
get fragmentShader() {
|
|
72
|
+
throw new Error('Material2d subclasses must implement fragmentShader');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Mutable for compatibility with code that customized these fields directly.
|
|
76
|
+
static SHARED_WGSL = SHAPE_SHADER_PREFIX;
|
|
77
|
+
static INSTANCED_WGSL = INSTANCED_SHAPE_SHADER_PREFIX;
|
|
78
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Material2d } from './Material2d.js';
|
|
2
|
+
import { SPRITE_FRAGMENT_SHADER_2D } from '../shaders/fragments.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fills the shape with a texture sampled at its uvs, tinted by `color` —
|
|
6
|
+
* the 2D sprite workhorse. The texture's alpha blends like the color
|
|
7
|
+
* alpha in BasicMaterial2d. Requires the map: the shader's texture
|
|
8
|
+
* bindings must exist when the pipeline is created.
|
|
9
|
+
*
|
|
10
|
+
* RectGeometry's v axis points up while images store their top row
|
|
11
|
+
* first, so image sprites usually want the texture created with
|
|
12
|
+
* `flipY: true`.
|
|
13
|
+
*/
|
|
14
|
+
export class SpriteMaterial2d extends Material2d {
|
|
15
|
+
/** @param {{map: Texture, color?: number[]}} options see Material2d for the rest */
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
super({ ...options, usesMap: true });
|
|
18
|
+
// Compatibility alias retained for existing material introspection.
|
|
19
|
+
this.requiresMap = true;
|
|
20
|
+
if (!this.map) {
|
|
21
|
+
throw new Error('SpriteMaterial2d requires a `map` texture');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get fragmentShader() {
|
|
26
|
+
return SPRITE_FRAGMENT_SHADER_2D;
|
|
27
|
+
}
|
|
28
|
+
}
|