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.
- package/README.md +207 -531
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,556 +1,232 @@
|
|
|
1
|
-
# micro-gl
|
|
2
|
-
|
|
3
|
-
A
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
25
|
+
npm install micro-gl
|
|
15
26
|
```
|
|
16
27
|
|
|
17
|
-
|
|
18
|
-
from your game project:
|
|
28
|
+
The package is ESM-only:
|
|
19
29
|
|
|
20
|
-
```
|
|
21
|
-
|
|
30
|
+
```js
|
|
31
|
+
import { Renderer, Scene, Mesh } from 'micro-gl';
|
|
22
32
|
```
|
|
23
33
|
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
60
|
+
Create a scene in `src/main.js`:
|
|
31
61
|
|
|
32
62
|
```js
|
|
33
63
|
import {
|
|
34
|
-
|
|
35
|
-
|
|
64
|
+
AmbientLight,
|
|
65
|
+
BoxGeometry,
|
|
66
|
+
DirectionalLight,
|
|
67
|
+
LambertMaterial,
|
|
36
68
|
Mesh,
|
|
69
|
+
OrbitControls,
|
|
37
70
|
PerspectiveCamera,
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
127
|
+
const canvas = document.querySelector('#canvas');
|
|
128
|
+
const renderer = new Renderer2d(canvas, { autoResize: true });
|
|
129
|
+
await renderer.init();
|
|
49
130
|
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
57
|
-
npm publish
|
|
227
|
+
npm run test:webgpu
|
|
58
228
|
```
|
|
59
229
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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.
|