micro-gl 0.1.0 → 0.1.1

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 (2) hide show
  1. package/README.md +207 -531
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,556 +1,232 @@
1
- # micro-gl
2
-
3
- A tiny three.js-style 3D engine written from scratch for **WebGPU only**, in plain
4
- JavaScript ES modules — no build step, no dependencies. It also ships a real 2D
5
- engine alongside the 3D one: a parallel set of classes built on 3x3 matrices
6
- (`Vec2` positions, one rotation angle) instead of a 3D scene viewed top-down.
7
-
8
- ## Installation
9
-
10
- `micro-gl` is an ES module package for browser projects. Install it straight
11
- from GitHub:
1
+ # micro-gl
2
+
3
+ A small, dependency-free WebGPU engine for 2D and 3D browser graphics.
4
+
5
+ **WebGPU only · ES modules · zero runtime dependencies · 2D and 3D**
6
+
7
+ `micro-gl` provides a scene graph, cameras, geometry, materials, lighting,
8
+ textures, controls, picking, shadows, and instanced rendering without bringing
9
+ in a large framework.
10
+
11
+ ## Features
12
+
13
+ - Separate 2D and 3D renderers with a familiar scene/object model
14
+ - Perspective, orthographic, and 2D cameras
15
+ - Built-in geometry, materials, textures, transparency, and MSAA
16
+ - Directional, ambient, and point lights with directional shadows
17
+ - Scene hierarchies, frustum culling, raycasting, and drag controls
18
+ - Instanced 2D and 3D drawing for large object counts
19
+ - Mouse, touch, and trackpad camera controls
20
+ - Correct sRGB texture and color handling
21
+
22
+ ## Install
12
23
 
13
24
  ```sh
14
- npm install github:karkad96/micro-gl
25
+ npm install micro-gl
15
26
  ```
16
27
 
17
- Or, after downloading or cloning this repository, install that local folder
18
- from your game project:
28
+ The package is ESM-only:
19
29
 
20
- ```sh
21
- npm install ../micro-gl
30
+ ```js
31
+ import { Renderer, Scene, Mesh } from 'micro-gl';
22
32
  ```
23
33
 
24
- Once the package has been published to npm, the equivalent command is:
34
+ Use a browser development server or bundler such as Vite, Rollup, webpack, or
35
+ Parcel so the `micro-gl` import can be resolved from `node_modules`.
25
36
 
26
- ```sh
27
- npm install micro-gl
37
+ ## 3D quick start
38
+
39
+ Add a full-page canvas:
40
+
41
+ ```html
42
+ <style>
43
+ html,
44
+ body,
45
+ #canvas {
46
+ width: 100%;
47
+ height: 100%;
48
+ margin: 0;
49
+ }
50
+
51
+ #canvas {
52
+ display: block;
53
+ }
54
+ </style>
55
+
56
+ <canvas id="canvas"></canvas>
57
+ <script type="module" src="/src/main.js"></script>
28
58
  ```
29
59
 
30
- Then import the engine through its public package entry point:
60
+ Create a scene in `src/main.js`:
31
61
 
32
62
  ```js
33
63
  import {
34
- Renderer,
35
- Scene,
64
+ AmbientLight,
65
+ BoxGeometry,
66
+ DirectionalLight,
67
+ LambertMaterial,
36
68
  Mesh,
69
+ OrbitControls,
37
70
  PerspectiveCamera,
38
- BoxGeometry,
39
- BasicMaterial,
71
+ Renderer,
72
+ Scene,
40
73
  } from 'micro-gl';
74
+
75
+ const canvas = document.querySelector('#canvas');
76
+ const renderer = new Renderer(canvas, { autoResize: true });
77
+ await renderer.init();
78
+
79
+ const scene = new Scene();
80
+ scene.background = [0.04, 0.05, 0.08, 1];
81
+
82
+ const cube = new Mesh(
83
+ new BoxGeometry(),
84
+ new LambertMaterial({ color: [1, 0.35, 0.1] }),
85
+ );
86
+ scene.add(cube);
87
+
88
+ const sun = new DirectionalLight([1, 1, 1], 1);
89
+ sun.direction.set(-1, -2, -1);
90
+ scene.add(sun);
91
+ scene.add(new AmbientLight([1, 1, 1], 0.25));
92
+
93
+ const camera = new PerspectiveCamera(60);
94
+ camera.position.set(3, 2, 4);
95
+ camera.lookAt(0, 0, 0);
96
+
97
+ const controls = new OrbitControls(camera, canvas);
98
+
99
+ function frame(time) {
100
+ cube.rotation.y = time * 0.001;
101
+ controls.update();
102
+ renderer.render(scene, camera);
103
+ requestAnimationFrame(frame);
104
+ }
105
+
106
+ requestAnimationFrame(frame);
41
107
  ```
42
108
 
43
- Use it with a browser development server or bundler such as Vite, Rollup,
44
- webpack, or Parcel so the bare `micro-gl` import is resolved from
45
- `node_modules`. `require('micro-gl')` is intentionally not supported: the
46
- engine ships as native ES modules and uses browser WebGPU APIs.
109
+ Use <kbd>Alt</kbd> + left-drag to orbit, right-drag to pan, and the mouse
110
+ wheel or a pinch gesture to zoom.
111
+
112
+ ## 2D quick start
113
+
114
+ The 2D API uses the same scene/renderer pattern with flat transforms and
115
+ `zIndex` ordering:
116
+
117
+ ```js
118
+ import {
119
+ BasicMaterial2d,
120
+ Camera2d,
121
+ RectGeometry,
122
+ Renderer2d,
123
+ Scene2d,
124
+ Shape2d,
125
+ } from 'micro-gl';
47
126
 
48
- ## Publishing to npm
127
+ const canvas = document.querySelector('#canvas');
128
+ const renderer = new Renderer2d(canvas, { autoResize: true });
129
+ await renderer.init();
49
130
 
50
- The repository is ready to pack or publish without a build step. Before the
51
- first release, sign in and inspect exactly what the package will contain:
131
+ const scene = new Scene2d();
132
+ scene.background = [0.04, 0.05, 0.08, 1];
133
+
134
+ const rectangle = new Shape2d(
135
+ new RectGeometry(2, 1),
136
+ new BasicMaterial2d({ color: [0.2, 0.7, 1] }),
137
+ );
138
+ scene.add(rectangle);
139
+
140
+ const camera = new Camera2d(4);
141
+
142
+ function frame(time) {
143
+ rectangle.rotation = time * 0.001;
144
+ renderer.render(scene, camera);
145
+ requestAnimationFrame(frame);
146
+ }
147
+
148
+ requestAnimationFrame(frame);
149
+ ```
150
+
151
+ ## API overview
152
+
153
+ ### 3D
154
+
155
+ - **Scene and objects:** `Scene`, `Object3d`, `Mesh`, `InstancedMesh`
156
+ - **Cameras:** `PerspectiveCamera`, `OrthographicCamera`
157
+ - **Geometry:** `BoxGeometry`, `SphereGeometry`, `PlaneGeometry`,
158
+ `WireframeGeometry`, `Geometry`
159
+ - **Materials:** `BasicMaterial`, `LambertMaterial`, `TextureMaterial`
160
+ - **Lighting:** `DirectionalLight`, `AmbientLight`, `PointLight`
161
+ - **Interaction:** `OrbitControls`, `DragControls`, `Raycaster`
162
+ - **Helpers:** `GridHelper`
163
+
164
+ ### 2D
165
+
166
+ - **Scene and objects:** `Scene2d`, `Object2d`, `Shape2d`, `InstancedShape2d`
167
+ - **Camera:** `Camera2d`
168
+ - **Geometry:** `RectGeometry`, `CircleGeometry`, `Geometry2d`
169
+ - **Materials:** `BasicMaterial2d`, `SpriteMaterial2d`
170
+ - **Interaction:** `PanZoomControls`, `DragControls2d`
171
+
172
+ ### Shared
173
+
174
+ - `Texture` loads image assets for both engines
175
+ - `Vec2`, `Vec3`, `Mat3`, and `Mat4` provide transform math
176
+ - Colors use arrays of normalized sRGB values: `[r, g, b]` or
177
+ `[r, g, b, alpha]`
178
+
179
+ Load a texture with:
180
+
181
+ ```js
182
+ import { Texture, TextureMaterial } from 'micro-gl';
183
+
184
+ const texture = await Texture.load('/assets/texture.png');
185
+ const material = new TextureMaterial({ map: texture });
186
+ ```
187
+
188
+ For a 2D sprite, use `SpriteMaterial2d` and usually pass `{ flipY: true }`
189
+ when loading the texture.
190
+
191
+ ## Browser requirements
192
+
193
+ - A browser with WebGPU support
194
+ - HTTPS or `localhost`; WebGPU is unavailable in an insecure context
195
+ - A development server; opening the HTML file directly is not supported
196
+
197
+ There is no WebGL fallback. You can check support before starting:
198
+
199
+ ```js
200
+ if (!navigator.gpu) {
201
+ throw new Error('This browser does not support WebGPU.');
202
+ }
203
+ ```
204
+
205
+ ## Current scope
206
+
207
+ `micro-gl` is intentionally small. It currently supports one directional
208
+ light with one directional shadow map, up to four point lights, ambient light,
209
+ and batch-level culling for instanced objects. Physics, audio, animation
210
+ systems, model loaders, and a WebGL fallback are outside the engine's current
211
+ scope.
212
+
213
+ ## Run the repository demo
214
+
215
+ ```sh
216
+ npm install
217
+ npx serve .
218
+ ```
219
+
220
+ Open `http://localhost:3000`. The demo can switch between the 2D and 3D
221
+ renderers and includes interaction and stress tests.
222
+
223
+ ## Tests
52
224
 
53
225
  ```sh
54
- npm login
55
226
  npm test
56
- npm pack --dry-run
57
- npm publish
227
+ npm run test:webgpu
58
228
  ```
59
229
 
60
- Publishing automatically runs the unit suite. If npm reports that the
61
- unscoped `micro-gl` name is unavailable, change `name` in `package.json` to a
62
- scope you own (for example `@your-name/micro-gl`), publish with `npm publish
63
- --access public`, and use that scoped name in imports.
64
-
65
- ## Running this repository
66
-
67
- This repo is the engine (`src/`) plus a demo page: `index.html` loads the
68
- included `main.js`, which exercises the 2D and 3D renderers.
69
-
70
- WebGPU requires the page to be served over http(s) (opening `index.html`
71
- directly from disk won't work because of ES modules). From this folder:
72
-
73
- ```
74
- npx serve .
75
- ```
76
-
77
- then open http://localhost:3000 in a WebGPU-capable browser
78
- (Chrome/Edge 113+, recent Firefox/Safari versions). Avoid Python's
79
- `http.server` — on some systems it serves `.js` with a MIME type that
80
- breaks ES module loading.
81
-
82
- ## Project structure
83
-
84
- The two engines mirror each other folder-for-folder, so if you know where
85
- something lives in one, you know where it lives in the other. Only `math/`
86
- and `core/` are shared.
87
-
88
- ```
89
- index.html demo page shell (canvas + HUD elements); loads main.js
90
- style.css demo page styles
91
- src/
92
- index.js single entry point that re-exports everything
93
-
94
- math/ shared by both engines
95
- Vec3.js 3-component vector
96
- Mat4.js column-major 4x4 matrix (perspective, invert, compose, ...)
97
- Frustum.js WebGPU clip planes + transformed bounding-box tests
98
- Vec2.js 2-component vector
99
- Mat3.js 3x3 affine transform, stored in the padded column
100
- layout WGSL uses for mat3x3f uniforms
101
- color.js sRGB <-> linear conversion helpers (colors are
102
- authored in sRGB, shaded in linear)
103
- core/ shared WebGPU plumbing
104
- initWebGpu.js adapter/device/canvas setup (one device per canvas —
105
- renderers on the same canvas share it)
106
- deviceLease.js shared-renderer lifetime/ownership coordination
107
- objectGpuResources.js manager-local object-buffer disposal
108
- rendererConfig.js drawing-buffer, MSAA and attachment constants
109
- pipelineConstants.js shader bindings and shared pipeline-state names
110
- materialResources.js validates each material's resource contract
111
- createMaterialPipelineLayouts.js shared 2D/3D bind-group layouts
112
- Texture.js an image + sampler settings; assign to a material's
113
- `map` (uploaded lazily, shareable across devices)
114
- uploadTexture.js caches one GPU texture/view/sampler per device
115
- generateMipmaps.js fills a texture's mip chain with a tiny render
116
- pass per level (WebGPU has no built-in way)
117
- indexedTriangles.js triangle-list/strip index traversal shared by CPU tools
118
- PointerControls.js the pointer/wheel/touch gesture plumbing shared by
119
- the camera controls (subclasses provide the camera
120
- math); touch: one-finger drag, two-finger pan,
121
- pinch zoom
122
-
123
- 3d/ the 3D engine
124
- constants.js limits shared by CPU layouts and WGSL
125
- core/
126
- Object3d.js scene-graph node: transform + children
127
- Scene.js scene root, holds the background color
128
- Mesh.js geometry + material
129
- InstancedMesh.js a mesh drawn N times in one call, each instance
130
- with its own transform + color
131
- InstanceMatrix.js validates packed affine instance transforms
132
- InstancedBounds.js cached union bounds for an instanced batch
133
- RayIntersection.js ray/AABB broad phase + indexed-triangle tests
134
- Raycaster.js exact triangle-surface and per-instance picking
135
- Renderer.js swap chain, depth buffer, render pass
136
- Pipelines.js bind group layouts + pipelines cached by composed
137
- shader source and fixed-function state
138
- DirectionalShadowMap.js renderer-owned shadow texture + depth pass
139
- ShadowPipelines.js vertex-only shadow pipeline cache
140
- GpuResources.js lazy caches: vertex/index/uniform buffers, bind groups
141
- Uniforms.js named CPU/WGSL layouts + 3D frame-uniform writer
142
- cameras/
143
- Camera.js shared base: lookAt target + view/projection matrices
144
- PerspectiveCamera.js
145
- OrthographicCamera.js
146
- controls/
147
- OrbitControls.js alt-drag-to-orbit / right-drag-to-pan /
148
- scroll-to-zoom; touch: one-finger orbit,
149
- two-finger pan, pinch zoom
150
- DragControls.js click-to-select and drag meshes around
151
- geometries/
152
- Geometry.js base class (interleaved position/normal/uv vertices)
153
- BoxGeometry.js
154
- SphereGeometry.js
155
- PlaneGeometry.js XZ ground plane facing +Y
156
- WireframeGeometry.js the unique edges of any triangle geometry,
157
- for drawing with a line-list material
158
- shaders/
159
- shared.js uniform structs and lighting helpers
160
- vertexStages.js regular + instanced vertex stages
161
- fragments.js reusable material fragment stages
162
- shadows.js regular + instanced directional depth shader
163
- vertexLayout.js matching GPU vertex attribute descriptors
164
- materials/
165
- Material.js material options + shader-stage composition
166
- BasicMaterial.js unlit flat color
167
- LambertMaterial.js diffuse shading from a DirectionalLight + ambient
168
- TextureMaterial.js LambertMaterial with a texture `map` for the
169
- surface color
170
- lights/
171
- DirectionalLight.js
172
- DirectionalShadow.js shadow-map and orthographic-camera settings
173
- AmbientLight.js
174
- PointLight.js a lamp: radiates from its scene-graph position,
175
- fading with distance (up to 4 per scene)
176
- helpers/
177
- GridHelper.js a line grid in the XZ plane — the usual ground
178
- reference while building a scene
179
-
180
- 2d/ the 2D engine — flat counterparts of the 3D classes
181
- constants.js shared instance-layout size
182
- core/
183
- Object2d.js scene-graph node: Vec2 position, one rotation angle,
184
- zIndex draw order
185
- Scene2d.js scene root, holds the background color
186
- Shape2d.js geometry + material (the 2D Mesh)
187
- InstancedShape2d.js the 2D InstancedMesh
188
- Renderer2d.js no depth buffer: zIndex sorting + alpha blending
189
- Pipelines2d.js the 2D pipeline cache (alpha blending, no depth)
190
- GpuResources2d.js lazy caches for the 2D buffers and bind groups
191
- Uniforms2d.js named padded Mat3/object uniform layouts
192
- cameras/
193
- Camera2d.js pan/zoom/rotation as a single Mat3, no perspective
194
- controls/
195
- PanZoomControls.js right-drag-to-pan / scroll-to-zoom /
196
- alt-drag-to-spin; touch: drag to pan, pinch to zoom
197
- DragControls2d.js click-to-select and drag shapes around
198
- geometries/
199
- Geometry2d.js base class (interleaved position/uv vertices) +
200
- containsPoint for raycaster-free picking
201
- RectGeometry.js
202
- CircleGeometry.js
203
- shaders/ 2D uniform chunks, vertex stages, fragments and
204
- matching GPU vertex layouts
205
- materials/
206
- Material2d.js material options + shader-stage composition
207
- BasicMaterial2d.js flat color, alpha-blended
208
- SpriteMaterial2d.js texture `map` times color — the 2D sprite
209
-
210
- test/ unit/regression tests plus small GPU-device fakes
211
- (the unit suite uses only Node built-ins)
212
- test-browser/ real WebGPU smoke fixture + installed-browser runner
213
- ```
214
-
215
- ## Minimal usage
216
-
217
- ```js
218
- import {
219
- Renderer,
220
- Scene,
221
- Mesh,
222
- PerspectiveCamera,
223
- BoxGeometry,
224
- LambertMaterial,
225
- DirectionalLight,
226
- AmbientLight,
227
- } from 'micro-gl';
228
-
229
- const renderer = new Renderer(document.querySelector('canvas'));
230
- await renderer.init();
231
-
232
- const scene = new Scene();
233
- scene.add(new DirectionalLight([1, 1, 1], 1));
234
- scene.add(new AmbientLight([1, 1, 1], 0.25));
235
-
236
- const cube = new Mesh(
237
- new BoxGeometry(),
238
- new LambertMaterial({ color: [1, 0.4, 0.1] }),
239
- );
240
- scene.add(cube);
241
-
242
- const camera = new PerspectiveCamera(60, innerWidth / innerHeight);
243
- camera.position.set(3, 2, 4);
244
- camera.lookAt(0, 0, 0);
245
-
246
- renderer.setSize(innerWidth, innerHeight);
247
- function frame() {
248
- cube.rotation.y += 0.01;
249
- renderer.render(scene, camera);
250
- requestAnimationFrame(frame);
251
- }
252
- requestAnimationFrame(frame);
253
- ```
254
-
255
- ## Directional shadows
256
-
257
- Shadow mapping is opt-in on both the light and each mesh, so adding a light
258
- does not silently add another render pass. Continuing the scene above (with
259
- `PlaneGeometry` added to its imports):
260
-
261
- ```js
262
- const sun = new DirectionalLight([1, 1, 1], 1);
263
- sun.direction.set(-1, -2, -1); // direction the light travels
264
- sun.castShadow = true;
265
- sun.shadow.mapSize = 1024;
266
- sun.shadow.bias = 0.001;
267
- sun.shadow.normalBias = 0.02;
268
- sun.shadow.camera.size = 8; // half-height of the square covered area
269
- sun.shadow.camera.near = 0.1;
270
- sun.shadow.camera.far = 30;
271
- sun.shadow.camera.lookAt(0, 0, 0); // center the covered area
272
- scene.add(sun);
273
-
274
- cube.castShadow = true;
275
- cube.receiveShadow = true;
276
-
277
- const ground = new Mesh(
278
- new PlaneGeometry(10, 10),
279
- new LambertMaterial({ color: [0.4, 0.4, 0.4] }),
280
- );
281
- ground.receiveShadow = true;
282
- scene.add(ground);
283
- ```
284
-
285
- The first visible `DirectionalLight` is the scene's sun and the only light that
286
- can own the directional shadow map. Its camera bounds are fixed in world space;
287
- move the camera target or increase `size`/`far` when casters fall outside them.
288
- Opaque regular and instanced triangle meshes can cast. Line, point and
289
- transparent materials are skipped by the shadow pass; lit transparent meshes
290
- may still receive shadows.
291
-
292
- ## Frustum culling
293
-
294
- The 3D renderer automatically skips meshes whose transformed geometry bounds
295
- are completely outside the active camera. Shadow casters are tested separately
296
- against the directional light's camera, so an off-screen object can still cast
297
- a visible shadow. `drawCount` and `shadowDrawCount` report only the instances
298
- actually submitted to their respective passes.
299
-
300
- Set `mesh.frustumCulled = false` when a custom vertex shader moves vertices
301
- beyond `geometry.bounds`, or when a mesh must render regardless of the camera:
302
-
303
- ```js
304
- mesh.frustumCulled = false;
305
- ```
306
-
307
- An `InstancedMesh` is culled conservatively as one batch. Its cached bound is
308
- the union of every instance transform: if any instance may be visible, the
309
- whole fixed-count batch draws. Split very large, spatially separate instance
310
- sets into several `InstancedMesh` objects for more precise culling. Direct
311
- matrix edits still require `mesh.needsUpdate = true`, which also refreshes the
312
- cached batch bound.
313
-
314
- ## Accurate 3D picking
315
-
316
- `Raycaster` intersects the indexed triangle surfaces that the geometry
317
- describes; a geometry's bounding box is only a fast broad phase. This avoids
318
- selecting empty corners of spheres, concave meshes, or gaps between disconnected
319
- triangles. Triangle lists and triangle strips (including primitive restarts)
320
- are supported and picking is deliberately double-sided.
321
-
322
- ```js
323
- const hits = new Raycaster()
324
- .setFromCamera(pointerNdcX, pointerNdcY, camera)
325
- .intersectObjects([scene]);
326
-
327
- const nearest = hits[0]; // { object, point, distance }
328
- ```
329
-
330
- The nearest surface hit per mesh or per instance is returned, globally sorted
331
- nearest-first; crossed exit faces are not separate results. Distances stay in
332
- world units under parenting, rotation, negative scale, and non-uniform scale.
333
- `InstancedMesh` tests every affine instance transform independently; its hit
334
- records also have `instanceId`, while `object` remains the batch mesh.
335
- Consequently `DragControls` moves the whole batch after an instanced hit.
336
-
337
- World matrices and camera matrices must be current before picking, as before.
338
- Line and point topologies do not claim a filled surface hit; selecting them
339
- accurately requires an application-specific screen-space tolerance. CPU
340
- picking follows indexed geometry, not texture alpha or custom vertex-shader
341
- displacement.
342
-
343
- ## Minimal 2D usage
344
-
345
- ```js
346
- import {
347
- Renderer2d,
348
- Scene2d,
349
- Shape2d,
350
- Camera2d,
351
- RectGeometry,
352
- BasicMaterial2d,
353
- } from 'micro-gl';
354
-
355
- const renderer = new Renderer2d(document.querySelector('canvas'));
356
- await renderer.init();
357
- // (to share a canvas with a 3D Renderer, init that first and pass it:
358
- // await renderer2d.init(renderer3d) — one GPU device per canvas)
359
-
360
- const scene = new Scene2d();
361
- const box = new Shape2d(
362
- new RectGeometry(2, 1),
363
- new BasicMaterial2d({ color: [1, 0.4, 0.1] }),
364
- );
365
- scene.add(box);
366
-
367
- const camera = new Camera2d(4, innerWidth / innerHeight);
368
-
369
- renderer.setSize(innerWidth, innerHeight);
370
- function frame() {
371
- box.rotation += 0.01;
372
- renderer.render(scene, camera);
373
- requestAnimationFrame(frame);
374
- }
375
- requestAnimationFrame(frame);
376
- ```
377
-
378
- ## How a frame happens
379
-
380
- One `renderer.render(scene, camera)` call, start to finish:
381
-
382
- 1. `scene.updateWorldMatrix()` recomposes every object's local matrix
383
- from its position/rotation/scale and multiplies down the tree, so
384
- each object ends up with an up-to-date `worldMatrix`.
385
- 2. `camera.updateMatrices()` rebuilds the projection and view matrices
386
- and their product, the view-projection matrix.
387
- 3. The frame uniforms — the view-projection matrix plus the lights found
388
- by a scene traversal — are written into the `@group(0)` uniform buffer.
389
- 4. The main camera's frustum and, when shadows are enabled, the directional
390
- light camera's frustum are refreshed. One scene traversal independently
391
- collects color-visible meshes and light-visible shadow casters. An object
392
- with `visible = false` is skipped along with its whole subtree.
393
- 5. The union of both lists has its geometry, instance data and object uniforms
394
- prepared. Camera-culled objects that do not cast a visible shadow therefore
395
- remain lazily unuploaded.
396
- 6. When directional shadows are enabled, a single-sample depth-only pass
397
- renders the opted-in opaque triangle casters into the light's shadow map.
398
- The map is still cleared when there are no visible casters, preventing
399
- stale shadows.
400
- 7. The color pass clears its color and depth targets. By default both are 4x
401
- multisampled and color resolves to the canvas when the pass ends
402
- (`antialias: false` renders straight to the canvas instead). Opaque meshes
403
- draw first in scene order, then transparent meshes draw back-to-front by
404
- view-space depth — the 3D counterpart of the 2D `zIndex` sort. For each:
405
- - `GpuResources` hands back the geometry's vertex/index buffers, the
406
- mesh's uniform buffer + bind group, and the material's pipeline
407
- (from `Pipelines`) — each created on first use and cached after;
408
- - the already-prepared model matrix, normal matrix, color and shadow
409
- receiver flag are read from the mesh's `@group(1)` uniform buffer;
410
- - the pass sets the pipeline, bind group and buffers, and issues one
411
- `drawIndexed` (with the instance count for an `InstancedMesh`).
412
- 8. The pass ends and both passes are submitted in one command buffer.
413
-
414
- `Renderer2d` follows the same shape minus the depth buffer: visible
415
- shapes are collected, sorted by `zIndex`, and drawn back-to-front with
416
- alpha blending on.
417
-
418
- ## How the WebGPU pieces fit together
419
-
420
- - **Renderer.init()** requests the GPU adapter and device, configures the canvas
421
- context, and creates the bind group layouts and the per-frame uniform buffer.
422
- If the device is ever lost (driver reset, GPU process crash), a console
423
- error explains how to recover. Construct with
424
- `new Renderer(canvas, { autoResize: true })` to have `setSize` follow the
425
- canvas's CSS size automatically, and call `renderer.dispose()` when done
426
- with it to release what it owns. Renderers initialized through
427
- `renderer2d.init(renderer3d)` share a managed device lease: disposing the
428
- original transfers ownership, and the last live renderer destroys the
429
- device. Each renderer also refreshes stale attachments if its shared canvas
430
- was resized by the other one. Sharing is intended for alternating 2D/3D
431
- scenes; each `render()` starts with a clear, so it is not an overlay/compositor
432
- API.
433
- - **3D uniforms and sampled shadows** use three bind groups, matching the
434
- declarations in `3d/shaders/shared.js` (the 2D counterpart uses the first
435
- two):
436
- - `@group(0)` _frame_ uniforms — view-projection matrix, light direction/color,
437
- ambient color, and up to `MAX_POINT_LIGHTS` (4) point lights (world
438
- position + color). Written once per frame.
439
- - `@group(1)` _object_ uniforms — model matrix, normal matrix, color and
440
- shadow receiver flag. Each mesh gets its own small uniform buffer and bind
441
- group (created lazily on first draw). Materials that declare
442
- `usesMap: true` use a second layout
443
- that adds the texture view and sampler to the same group. This declaration
444
- is independent of the current `map` value, so swapping a resource cannot
445
- accidentally change the shader's pipeline layout. Custom material classes
446
- should pass it explicitly; when omitted, it is inferred once from the
447
- initial `map` for compatibility.
448
- - `@group(2)` _directional shadow_ resources — light view-projection
449
- matrix, bias settings, sampled depth texture and comparison sampler.
450
- The color pass always binds it; when shadows are disabled a tiny fallback
451
- map and an `enabled = 0` flag keep every material on one stable layout.
452
- - **Pipelines** are compiled once per composed shader + pipeline state
453
- (primitive topology, culling, textured / instanced / transparent or
454
- not) and cached, so materials producing the same WGSL reuse the same
455
- `GPURenderPipeline`. Transparent variants alpha-blend and
456
- don't write the depth buffer.
457
- - **Shaders** are ordinary JavaScript ES modules under each
458
- engine's `shaders/` directory. Shared declarations, vertex stages, fragment
459
- stages and their matching vertex layouts are separate, while material
460
- classes only hold options and select a fragment stage. This keeps WGSL out
461
- of the public-facing classes without requiring raw-file imports or a build
462
- tool.
463
- - **Resources** are scoped to their real WebGPU owner. Geometry buffers and
464
- textures are cached per `GPUDevice`; object uniform buffers and bind groups
465
- are cached per renderer resource manager because their layouts belong to
466
- that manager. Disposing a renderer removes only its entries and buffers;
467
- other renderers' entries remain valid. The same scene data can therefore be
468
- rendered on independent canvases/devices.
469
- - **Geometries** upload one interleaved vertex buffer (position, normal, uv —
470
- 32-byte stride) and one 32-bit index buffer per device, lazily on first draw.
471
- Their cached local bounding boxes drive frustum culling and the raycasting
472
- broad phase; indexed triangle tests determine exact picking hits.
473
- Edit `vertices`/`indices` in place and set `geometry.needsUpdate = true`; a
474
- revision counter ensures every device receives the edit (the arrays must
475
- keep their length).
476
- - **Textures** upload per device with a full mip chain (generated level by level
477
- with a tiny render pass — `generateMipmaps.js`) and get a sampler from
478
- the Texture's settings. Image textures upload as `rgba8unorm-srgb`, so
479
- sampling and mip filtering happen in linear space.
480
- - **Color space**: the colors you author (material, light, instance and
481
- scene background colors) are sRGB display values. The renderer decodes them
482
- to linear (`srgbToLinear`), all shading happens in linear space, and
483
- fragment shaders write linear RGB to an sRGB canvas attachment. The
484
- attachment performs the final encoding after alpha blending and MSAA resolve,
485
- so unlit colors round-trip exactly while lighting, textures, transparency and
486
- antialiased edges are all combined in the correct color space. Custom
487
- fragment shaders must likewise return linear RGB.
488
- - **Instancing**: an `InstancedMesh` packs per-instance transform + color
489
- into a second, instance-stepped vertex buffer and draws all instances
490
- with one `drawIndexed` — thousands of objects for a couple of draw calls.
491
- Instance revisions keep every renderer synchronized and guarantee that a
492
- buffer recreated after `dispose()` is populated before drawing. A separate
493
- bounds revision avoids rebuilding the batch bound after color-only edits.
494
- Raycasting composes each instance transform with the mesh transform and
495
- reports the matching `instanceId`.
496
- - **Frustum culling** extracts the six WebGPU clip planes once per camera and
497
- tests local bounding boxes through their world transforms without allocating
498
- corner vectors or inverting matrices. Invalid/custom bounds fail open rather
499
- than risk hiding geometry. Color and directional-shadow passes use different
500
- frusta.
501
- - **render()** updates world matrices, writes the uniforms, optionally records
502
- a single-sample `depth32float` directional shadow pass, records the color
503
- pass with a `depth24plus` depth attachment, and submits them together. With
504
- the default `antialias: true`, the pass draws into 4x multisampled
505
- color/depth targets and resolves to the swap chain — the sample count is
506
- baked into every cached pipeline.
507
-
508
- ## Tests
509
-
510
- Math, scene graphs, controls, resource lifecycles, shader composition and
511
- pipeline descriptors are covered with Node's built-in test runner and small
512
- GPU fakes:
513
-
514
- ```
515
- npm test
516
- ```
517
-
518
- After `npm install`, the opt-in browser suite uses `playwright-core` to drive an
519
- installed Chrome or Edge browser without downloading another browser binary:
520
-
521
- ```
522
- npm run test:webgpu
523
- ```
524
-
525
- It compiles every stock regular/instanced WGSL module, renders the 2D and 3D
526
- material, texture, transparency, topology and directional-shadow variants
527
- through both 1x and 4x pipelines, and exercises frustum culling across camera
528
- resizes. It checks WebGPU error scopes and uncaptured errors, exercises shared
529
- renderer resizing/disposal, and reads known pixels back through a GPU buffer.
530
- The command reports a skip when no browser or WebGPU adapter is available; set
531
- `MICRO_GL_REQUIRE_WEBGPU=1` in CI to turn that into a failure.
532
-
533
- Set `MICRO_GL_BROWSER_PATH` to use a non-standard Chrome/Edge location,
534
- `MICRO_GL_HEADED=1` to watch the test, or `MICRO_GL_BROWSER_ARGS` to a JSON
535
- array of additional launch flags when a CI GPU setup needs them.
536
-
537
- ## Deliberate limitations (it's _micro_)
538
-
539
- - One directional light (and one fixed-bounds directional shadow map), up to
540
- four point lights, and ambient. Shadow casting is limited to opaque triangle
541
- meshes; there are no point-light, transparent or alpha-cutout shadows.
542
- - Cameras orient via `lookAt` target, not via their `rotation` Euler angles.
543
- - Transparent meshes sort back-to-front by their origin's depth, not per
544
- triangle, so intersecting or nested transparent meshes can blend in the
545
- wrong order.
546
- - Per-instance non-uniform scale skews instanced lighting normals (the
547
- shader uses the instance matrix directly instead of its inverse
548
- transpose).
549
- - Instanced frustum culling is all-or-none per batch; it does not compact
550
- individual visible instances into a temporary GPU buffer.
551
- - CPU picking covers indexed triangle geometry and affine instances. It does
552
- not evaluate texture alpha or custom vertex shaders, and line/point picking
553
- needs an application-defined screen-space tolerance.
554
-
555
- The natural next step if you want to grow it: spatial partitioning and broader
556
- material/shadow models.
230
+ `npm test` runs the Node unit and regression suite. `npm run test:webgpu`
231
+ uses an installed Chrome or Edge browser to compile and render the real WebGPU
232
+ pipelines.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "micro-gl",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "A tiny dependency-free 2D and 3D WebGPU engine",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",