micro-gl 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +556 -0
  2. package/package.json +37 -0
  3. package/src/2d/cameras/Camera2d.js +46 -0
  4. package/src/2d/constants.js +3 -0
  5. package/src/2d/controls/DragControls2d.js +174 -0
  6. package/src/2d/controls/PanZoomControls.js +64 -0
  7. package/src/2d/core/GpuResources2d.js +163 -0
  8. package/src/2d/core/InstancedShape2d.js +74 -0
  9. package/src/2d/core/Object2d.js +107 -0
  10. package/src/2d/core/Pipelines2d.js +110 -0
  11. package/src/2d/core/Renderer2d.js +353 -0
  12. package/src/2d/core/Scene2d.js +13 -0
  13. package/src/2d/core/Shape2d.js +13 -0
  14. package/src/2d/core/Uniforms2d.js +13 -0
  15. package/src/2d/geometries/CircleGeometry.js +27 -0
  16. package/src/2d/geometries/Geometry2d.js +108 -0
  17. package/src/2d/geometries/RectGeometry.js +23 -0
  18. package/src/2d/materials/BasicMaterial2d.js +12 -0
  19. package/src/2d/materials/Material2d.js +78 -0
  20. package/src/2d/materials/SpriteMaterial2d.js +28 -0
  21. package/src/2d/shaders/fragments.js +23 -0
  22. package/src/2d/shaders/shared.js +28 -0
  23. package/src/2d/shaders/vertexLayout.js +74 -0
  24. package/src/2d/shaders/vertexStages.js +59 -0
  25. package/src/3d/cameras/Camera.js +66 -0
  26. package/src/3d/cameras/OrthographicCamera.js +35 -0
  27. package/src/3d/cameras/PerspectiveCamera.js +25 -0
  28. package/src/3d/constants.js +18 -0
  29. package/src/3d/controls/DragControls.js +168 -0
  30. package/src/3d/controls/OrbitControls.js +125 -0
  31. package/src/3d/core/DirectionalShadowMap.js +283 -0
  32. package/src/3d/core/GpuResources.js +174 -0
  33. package/src/3d/core/InstanceMatrix.js +55 -0
  34. package/src/3d/core/InstancedBounds.js +114 -0
  35. package/src/3d/core/InstancedMesh.js +124 -0
  36. package/src/3d/core/Mesh.js +26 -0
  37. package/src/3d/core/Object3d.js +101 -0
  38. package/src/3d/core/Pipelines.js +125 -0
  39. package/src/3d/core/RayIntersection.js +184 -0
  40. package/src/3d/core/Raycaster.js +246 -0
  41. package/src/3d/core/Renderer.js +492 -0
  42. package/src/3d/core/Scene.js +13 -0
  43. package/src/3d/core/ShadowPipelines.js +64 -0
  44. package/src/3d/core/Uniforms.js +132 -0
  45. package/src/3d/geometries/BoxGeometry.js +56 -0
  46. package/src/3d/geometries/Geometry.js +97 -0
  47. package/src/3d/geometries/PlaneGeometry.js +24 -0
  48. package/src/3d/geometries/SphereGeometry.js +46 -0
  49. package/src/3d/geometries/WireframeGeometry.js +39 -0
  50. package/src/3d/helpers/GridHelper.js +43 -0
  51. package/src/3d/lights/AmbientLight.js +18 -0
  52. package/src/3d/lights/DirectionalLight.js +26 -0
  53. package/src/3d/lights/DirectionalShadow.js +29 -0
  54. package/src/3d/lights/PointLight.js +23 -0
  55. package/src/3d/materials/BasicMaterial.js +11 -0
  56. package/src/3d/materials/LambertMaterial.js +13 -0
  57. package/src/3d/materials/Material.js +87 -0
  58. package/src/3d/materials/TextureMaterial.js +24 -0
  59. package/src/3d/shaders/fragments.js +34 -0
  60. package/src/3d/shaders/shadows.js +52 -0
  61. package/src/3d/shaders/shared.js +129 -0
  62. package/src/3d/shaders/vertexLayout.js +85 -0
  63. package/src/3d/shaders/vertexStages.js +70 -0
  64. package/src/core/PointerControls.js +258 -0
  65. package/src/core/Texture.js +87 -0
  66. package/src/core/createMaterialPipelineLayouts.js +114 -0
  67. package/src/core/deviceLease.js +67 -0
  68. package/src/core/generateMipmaps.js +115 -0
  69. package/src/core/indexedTriangles.js +53 -0
  70. package/src/core/initWebGpu.js +65 -0
  71. package/src/core/materialResources.js +18 -0
  72. package/src/core/objectGpuResources.js +68 -0
  73. package/src/core/pipelineConstants.js +68 -0
  74. package/src/core/rendererConfig.js +66 -0
  75. package/src/core/uploadTexture.js +57 -0
  76. package/src/index.js +68 -0
  77. package/src/math/Frustum.js +143 -0
  78. package/src/math/Mat3.js +165 -0
  79. package/src/math/Mat4.js +359 -0
  80. package/src/math/Vec2.js +72 -0
  81. package/src/math/Vec3.js +98 -0
  82. package/src/math/color.js +20 -0
@@ -0,0 +1,174 @@
1
+ import { Vec2 } from '../../math/Vec2.js';
2
+ import { Mat3 } from '../../math/Mat3.js';
3
+
4
+ const _inverse = new Mat3();
5
+ const _pointer = new Vec2();
6
+ const _local = new Vec2();
7
+ const _worldPos = new Vec2();
8
+
9
+ function isVisibleInHierarchy(object) {
10
+ for (let current = object; current; current = current.parent) {
11
+ if (!current.visible) return false;
12
+ }
13
+ return true;
14
+ }
15
+
16
+ /**
17
+ * Lets the user pick shapes with the pointer and drag them around —
18
+ * the 2D counterpart of DragControls, without needing a Raycaster:
19
+ * the pointer is mapped into world space through the inverse camera
20
+ * matrix and tested against each shape's geometry directly.
21
+ *
22
+ * Assign the callbacks to react to interaction:
23
+ * onSelect(shape | null) selection changed (click on shape / empty space)
24
+ * onDragStart(shape), onDrag(shape), onDragEnd(shape)
25
+ *
26
+ * Disable your camera controls in onDragStart / re-enable in onDragEnd
27
+ * so panning doesn't fight the drag.
28
+ */
29
+ export class DragControls2d {
30
+ /**
31
+ * @param {Shape2d[]} objects shapes that can be selected and dragged
32
+ */
33
+ constructor(objects, camera, domElement) {
34
+ this.objects = objects;
35
+ this.camera = camera;
36
+ this.domElement = domElement;
37
+ /** Set to false to ignore input; it also pauses an in-progress drag. */
38
+ this.enabled = true;
39
+ this.selected = null;
40
+
41
+ this.onSelect = null;
42
+ this.onDragStart = null;
43
+ this.onDrag = null;
44
+ this.onDragEnd = null;
45
+
46
+ this._dragging = false;
47
+ this._activePointerId = null;
48
+ this._grabOffset = new Vec2(); // shape world position - grab point
49
+
50
+ this._onPointerDown = this._handlePointerDown.bind(this);
51
+ this._onPointerMove = this._handlePointerMove.bind(this);
52
+ this._onPointerUp = this._handlePointerUp.bind(this);
53
+
54
+ domElement.addEventListener('pointerdown', this._onPointerDown);
55
+ domElement.addEventListener('pointermove', this._onPointerMove);
56
+ domElement.addEventListener('pointerup', this._onPointerUp);
57
+ domElement.addEventListener('pointercancel', this._onPointerUp);
58
+ }
59
+
60
+ _handlePointerDown(event) {
61
+ const rotateViewGesture = event.button !== 0 || event.altKey;
62
+ if (
63
+ !this.enabled ||
64
+ rotateViewGesture ||
65
+ this._activePointerId !== null
66
+ ) {
67
+ return;
68
+ }
69
+
70
+ const point = this._pointerToWorld(event);
71
+ if (!point) return;
72
+ const hit = this._pick(point);
73
+ if (!hit) {
74
+ this._select(null);
75
+ return;
76
+ }
77
+
78
+ this._select(hit);
79
+ this._dragging = true;
80
+ this._activePointerId = event.pointerId;
81
+ this.domElement.setPointerCapture(event.pointerId);
82
+
83
+ const worldMatrix = hit.worldMatrix.elements;
84
+ this._grabOffset.set(
85
+ worldMatrix[8] - point.x,
86
+ worldMatrix[9] - point.y,
87
+ );
88
+ if (this.onDragStart) this.onDragStart(hit);
89
+ }
90
+
91
+ _handlePointerMove(event) {
92
+ if (
93
+ !this.enabled ||
94
+ !this._dragging ||
95
+ !this.selected ||
96
+ event.pointerId !== this._activePointerId
97
+ ) {
98
+ return;
99
+ }
100
+
101
+ const point = this._pointerToWorld(event);
102
+ if (!point) return;
103
+
104
+ // New world position, then into the parent's local space.
105
+ _worldPos.copy(point).add(this._grabOffset);
106
+ const parent = this.selected.parent;
107
+ if (parent) {
108
+ if (!_inverse.copy(parent.worldMatrix).tryInvert()) return;
109
+ _worldPos.applyMat3(_inverse);
110
+ }
111
+ this.selected.position.copy(_worldPos);
112
+ if (this.onDrag) this.onDrag(this.selected);
113
+ }
114
+
115
+ _handlePointerUp(event) {
116
+ if (!this._dragging || event.pointerId !== this._activePointerId) return;
117
+ this._finishDrag();
118
+ }
119
+
120
+ _finishDrag() {
121
+ const pointerId = this._activePointerId;
122
+ this._dragging = false;
123
+ this._activePointerId = null;
124
+ if (
125
+ pointerId !== null &&
126
+ this.domElement.hasPointerCapture(pointerId)
127
+ ) {
128
+ this.domElement.releasePointerCapture(pointerId);
129
+ }
130
+ if (this.onDragEnd) this.onDragEnd(this.selected);
131
+ }
132
+
133
+ _select(shape) {
134
+ if (shape === this.selected) return;
135
+ this.selected = shape;
136
+ if (this.onSelect) this.onSelect(shape);
137
+ }
138
+
139
+ /** The pointer position in world space (via the inverse view-projection). */
140
+ _pointerToWorld(e) {
141
+ const rect = this.domElement.getBoundingClientRect();
142
+ const ndcX = ((e.clientX - rect.left) / rect.width) * 2 - 1;
143
+ const ndcY = 1 - ((e.clientY - rect.top) / rect.height) * 2;
144
+ if (!_inverse.copy(this.camera.viewProjectionMatrix).tryInvert()) {
145
+ return null;
146
+ }
147
+ return _pointer.set(ndcX, ndcY).applyMat3(_inverse);
148
+ }
149
+
150
+ /**
151
+ * The topmost shape under the world-space point, or null. Higher
152
+ * zIndex wins; ties go to the later entry in `objects`, matching the
153
+ * renderer's draw order when the array follows scene order.
154
+ */
155
+ _pick(point) {
156
+ let hit = null;
157
+ for (const shape of this.objects) {
158
+ if (!isVisibleInHierarchy(shape)) continue;
159
+ if (!_inverse.copy(shape.worldMatrix).tryInvert()) continue;
160
+ _local.copy(point).applyMat3(_inverse);
161
+ if (!shape.geometry.containsPoint(_local.x, _local.y)) continue;
162
+ if (!hit || shape.zIndex >= hit.zIndex) hit = shape;
163
+ }
164
+ return hit;
165
+ }
166
+
167
+ dispose() {
168
+ if (this._dragging) this._finishDrag();
169
+ this.domElement.removeEventListener('pointerdown', this._onPointerDown);
170
+ this.domElement.removeEventListener('pointermove', this._onPointerMove);
171
+ this.domElement.removeEventListener('pointerup', this._onPointerUp);
172
+ this.domElement.removeEventListener('pointercancel', this._onPointerUp);
173
+ }
174
+ }
@@ -0,0 +1,64 @@
1
+ import { PointerControls } from '../../core/PointerControls.js';
2
+
3
+ /**
4
+ * Mouse / touch controls for a Camera2d: right-drag to pan, scroll to
5
+ * zoom (toward the cursor), Alt + left-drag to spin the view. A
6
+ * right-click without dragging still opens the browser context menu.
7
+ * On touch screens: drag with one or two fingers to pan, pinch to zoom.
8
+ *
9
+ * The same gestures as OrbitControls (both inherit the plumbing from
10
+ * PointerControls), minus the orbit — and because a 2D camera has no
11
+ * derived state, the handlers modify it directly, so there is no
12
+ * per-frame update() to call.
13
+ */
14
+ export class PanZoomControls extends PointerControls {
15
+ constructor(camera, domElement) {
16
+ super(domElement);
17
+ this.camera = camera;
18
+
19
+ this.minZoom = 0.1;
20
+ this.maxZoom = 20;
21
+ }
22
+
23
+ /** Half the height of the visible area, in world units. */
24
+ _visibleHalfHeight() {
25
+ return this.camera.size / this.camera.zoom;
26
+ }
27
+
28
+ _rotate(rx) {
29
+ this.camera.rotation += rx;
30
+ }
31
+
32
+ /**
33
+ * Moves the camera along its screen-space axes so the scene follows
34
+ * the cursor (1 pixel of drag = 1 pixel on screen).
35
+ */
36
+ _pan(dx, dy) {
37
+ const worldPerPixel =
38
+ (2 * this._visibleHalfHeight()) / this.domElement.clientHeight;
39
+ // The camera's right axis is (cos r, sin r), its up axis (-sin r, cos r).
40
+ const c = Math.cos(this.camera.rotation);
41
+ const s = Math.sin(this.camera.rotation);
42
+ this.camera.position.x -= (c * dx + s * dy) * worldPerPixel;
43
+ this.camera.position.y -= (s * dx - c * dy) * worldPerPixel;
44
+ }
45
+
46
+ _zoom(factor, ndcX, ndcY) {
47
+ const halfHBefore = this._visibleHalfHeight();
48
+ this.camera.zoom /= factor;
49
+ this.camera.zoom = Math.min(
50
+ Math.max(this.camera.zoom, this.minZoom),
51
+ this.maxZoom,
52
+ );
53
+
54
+ // Zoom toward the cursor: shift the camera so the world point
55
+ // under the pointer stays under it as the view scales.
56
+ const dh = halfHBefore - this._visibleHalfHeight();
57
+ const ox = ndcX * dh * this.camera.aspect;
58
+ const oy = ndcY * dh;
59
+ const c = Math.cos(this.camera.rotation);
60
+ const s = Math.sin(this.camera.rotation);
61
+ this.camera.position.x += c * ox - s * oy;
62
+ this.camera.position.y += s * ox + c * oy;
63
+ }
64
+ }
@@ -0,0 +1,163 @@
1
+ import { uploadTexture } from '../../core/uploadTexture.js';
2
+ import { SINGLE_SAMPLE_COUNT } from '../../core/rendererConfig.js';
3
+ import { SHADER_BINDING } from '../../core/pipelineConstants.js';
4
+ import { materialUsesMap } from '../../core/materialResources.js';
5
+ import {
6
+ disposeOwnedObjectGpuResources,
7
+ trackObjectGpuResource,
8
+ } from '../../core/objectGpuResources.js';
9
+ import { Pipelines2d } from './Pipelines2d.js';
10
+ import { OBJECT_UNIFORM_SIZE_2D } from './Uniforms2d.js';
11
+
12
+ export { OBJECT_UNIFORM_SIZE_2D } from './Uniforms2d.js';
13
+
14
+ /**
15
+ * Owns the GPU resources Renderer2d creates on behalf of geometries,
16
+ * materials and shapes — the 2D counterpart of GpuResources, with the
17
+ * same lazy caching scheme:
18
+ *
19
+ * - vertex + index buffers per geometry
20
+ * - a GPU texture + sampler per Texture
21
+ * - a small uniform buffer + bind group per shape
22
+ *
23
+ * Everything is created lazily on first use. Geometry and texture
24
+ * resources are cached per GPUDevice; shape resources are cached per
25
+ * GpuResources2d instance because their bind groups use this instance's
26
+ * layouts. Render pipelines and their shared layouts live in Pipelines2d.
27
+ */
28
+ export class GpuResources2d {
29
+ constructor(device, format, sampleCount = SINGLE_SAMPLE_COUNT) {
30
+ this.device = device;
31
+ this.pipelines = new Pipelines2d(device, format, sampleCount);
32
+ this._objectGpuResourceRefs = new Set();
33
+ // Renderer2d builds its per-frame bind group against this layout.
34
+ this.frameBindGroupLayout = this.pipelines.frameBindGroupLayout;
35
+ }
36
+
37
+ /** The render pipeline for a material + pipeline state — see Pipelines2d. */
38
+ pipelineFor(material, instanced = false) {
39
+ return this.pipelines.pipelineFor(material, instanced);
40
+ }
41
+
42
+ /** GPU texture, view and sampler for a Texture, uploaded on first use. */
43
+ textureFor(texture) {
44
+ return uploadTexture(this.device, texture);
45
+ }
46
+
47
+ /**
48
+ * Vertex and index buffers for a geometry, re-uploaded when its
49
+ * `needsUpdate` flag is set.
50
+ */
51
+ geometryFor(geometry) {
52
+ const cache = geometry._gpu || (geometry._gpu = new Map());
53
+ let gpu = cache.get(this.device);
54
+
55
+ if (!gpu) {
56
+ const vertexBuffer = this.device.createBuffer({
57
+ size: geometry.vertices.byteLength,
58
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
59
+ });
60
+ this.device.queue.writeBuffer(vertexBuffer, 0, geometry.vertices);
61
+
62
+ const indexBuffer = this.device.createBuffer({
63
+ size: geometry.indices.byteLength,
64
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
65
+ });
66
+ this.device.queue.writeBuffer(indexBuffer, 0, geometry.indices);
67
+
68
+ gpu = {
69
+ vertexBuffer,
70
+ indexBuffer,
71
+ revision: geometry._gpuRevision,
72
+ };
73
+ cache.set(this.device, gpu);
74
+ } else if (
75
+ geometry.needsUpdate ||
76
+ gpu.revision !== geometry._gpuRevision
77
+ ) {
78
+ this.device.queue.writeBuffer(gpu.vertexBuffer, 0, geometry.vertices);
79
+ this.device.queue.writeBuffer(gpu.indexBuffer, 0, geometry.indices);
80
+ gpu.revision = geometry._gpuRevision;
81
+ }
82
+
83
+ // Revisions keep the other devices' caches stale until each is drawn,
84
+ // so clearing this public hint here cannot make them miss an update.
85
+ geometry.needsUpdate = false;
86
+ return gpu;
87
+ }
88
+
89
+ /**
90
+ * Uniform buffer, bind group and staging array for a shape. The bind
91
+ * group is rebuilt automatically whenever the material's `map`
92
+ * changes (a swapped material, a swapped texture, or a Texture
93
+ * re-uploaded after `dispose()`).
94
+ */
95
+ shapeFor(shape) {
96
+ const cache = shape._gpu || (shape._gpu = new Map());
97
+ let gpu = cache.get(this);
98
+ if (!gpu) {
99
+ gpu = {
100
+ uniformBuffer: this.device.createBuffer({
101
+ size: OBJECT_UNIFORM_SIZE_2D,
102
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
103
+ }),
104
+ bindGroup: null,
105
+ mapGpu: null, // the uploaded texture the bind group samples
106
+ data: new Float32Array(
107
+ OBJECT_UNIFORM_SIZE_2D / Float32Array.BYTES_PER_ELEMENT,
108
+ ),
109
+ };
110
+ if (shape.isInstanced) {
111
+ gpu.instanceBuffer = this.device.createBuffer({
112
+ size: shape.instanceData.byteLength,
113
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
114
+ });
115
+ gpu.instanceRevision = null; // this fresh buffer has no data yet
116
+ }
117
+ cache.set(this, gpu);
118
+ trackObjectGpuResource(this, shape, gpu);
119
+ }
120
+
121
+ if (
122
+ shape.isInstanced &&
123
+ (shape.needsUpdate ||
124
+ gpu.instanceRevision !== shape._instanceRevision)
125
+ ) {
126
+ this.device.queue.writeBuffer(
127
+ gpu.instanceBuffer,
128
+ 0,
129
+ shape.instanceData,
130
+ );
131
+ gpu.instanceRevision = shape._instanceRevision;
132
+ shape.needsUpdate = false;
133
+ }
134
+
135
+ const mapGpu = materialUsesMap(shape.material)
136
+ ? this.textureFor(shape.material.map)
137
+ : null;
138
+ if (!gpu.bindGroup || gpu.mapGpu !== mapGpu) {
139
+ let layout = this.pipelines.objectBindGroupLayout;
140
+ const entries = [
141
+ {
142
+ binding: SHADER_BINDING.uniforms,
143
+ resource: { buffer: gpu.uniformBuffer },
144
+ },
145
+ ];
146
+ if (mapGpu) {
147
+ layout = this.pipelines.texturedObjectBindGroupLayout;
148
+ entries.push(
149
+ { binding: SHADER_BINDING.map, resource: mapGpu.view },
150
+ { binding: SHADER_BINDING.sampler, resource: mapGpu.sampler },
151
+ );
152
+ }
153
+ gpu.bindGroup = this.device.createBindGroup({ layout, entries });
154
+ gpu.mapGpu = mapGpu;
155
+ }
156
+ return gpu;
157
+ }
158
+
159
+ /** Releases this manager's per-object buffers and cache entries. */
160
+ dispose() {
161
+ disposeOwnedObjectGpuResources(this);
162
+ }
163
+ }
@@ -0,0 +1,74 @@
1
+ import { Shape2d } from './Shape2d.js';
2
+ import { srgbToLinear } from '../../math/color.js';
3
+ import { INSTANCE_SIZE_2D } from '../constants.js';
4
+
5
+ export { INSTANCE_SIZE_2D } from '../constants.js';
6
+
7
+ /**
8
+ * A Shape2d drawn `count` times in one draw call — the 2D counterpart
9
+ * of InstancedMesh. Each instance has its own transform and color;
10
+ * instance transforms are local to the shape and instance colors
11
+ * multiply with the material's color. The whole group sorts by the
12
+ * shape's single `zIndex`; instances draw in buffer order within it.
13
+ *
14
+ * Write instances with `setMatrixAt`/`setColorAt` — they mark the
15
+ * buffer for re-upload; set `needsUpdate = true` yourself only if you
16
+ * write `instanceData` directly. The instance count is fixed at
17
+ * construction.
18
+ *
19
+ * Note: picking (DragControls2d) sees only the base geometry at the
20
+ * shape's own transform, not the individual instances.
21
+ */
22
+ export class InstancedShape2d extends Shape2d {
23
+ constructor(geometry, material, count) {
24
+ super(geometry, material);
25
+ this.isInstanced = true;
26
+ this.count = count;
27
+ this.instanceData = new Float32Array(count * INSTANCE_SIZE_2D);
28
+ // Every instance starts as an identity matrix with a white color.
29
+ for (let i = 0; i < count; i++) {
30
+ const base = i * INSTANCE_SIZE_2D;
31
+ this.instanceData[base] = 1;
32
+ this.instanceData[base + 5] = 1;
33
+ this.instanceData[base + 10] = 1;
34
+ this.instanceData.fill(1, base + 12, base + 16);
35
+ }
36
+ // Resource caches compare revisions so every renderer/device receives
37
+ // an edit, even after another renderer has cleared needsUpdate.
38
+ this._instanceRevision = 0;
39
+ this._needsUpdate = true;
40
+ }
41
+
42
+ /** Upload hint; set true after changing `instanceData` directly. */
43
+ get needsUpdate() {
44
+ return this._needsUpdate;
45
+ }
46
+
47
+ set needsUpdate(value) {
48
+ this._needsUpdate = Boolean(value);
49
+ if (this._needsUpdate) this._instanceRevision++;
50
+ }
51
+
52
+ /** Copies a Mat3 into instance `index`'s transform. */
53
+ setMatrixAt(index, matrix) {
54
+ this.instanceData.set(matrix.elements, index * INSTANCE_SIZE_2D);
55
+ this.needsUpdate = true;
56
+ return this;
57
+ }
58
+
59
+ /**
60
+ * Sets instance `index`'s color from [r, g, b] or [r, g, b, a] —
61
+ * sRGB display values like material colors. They are stored
62
+ * linearized (shading happens in linear space), so write linear
63
+ * values if you fill `instanceData` directly instead.
64
+ */
65
+ setColorAt(index, color) {
66
+ const base = index * INSTANCE_SIZE_2D + 12;
67
+ this.instanceData[base] = srgbToLinear(color[0]);
68
+ this.instanceData[base + 1] = srgbToLinear(color[1]);
69
+ this.instanceData[base + 2] = srgbToLinear(color[2]);
70
+ this.instanceData[base + 3] = color.length > 3 ? color[3] : 1;
71
+ this.needsUpdate = true;
72
+ return this;
73
+ }
74
+ }
@@ -0,0 +1,107 @@
1
+ import { Vec2 } from '../../math/Vec2.js';
2
+ import { Mat3 } from '../../math/Mat3.js';
3
+ import { disposeObjectGpuResources } from '../../core/objectGpuResources.js';
4
+
5
+ /**
6
+ * Base class for everything that lives in the 2D scene graph — the flat
7
+ * counterpart of Object3d. The transform is genuinely 2D: a Vec2
8
+ * position, a single rotation angle and a Vec2 scale, composed into 3x3
9
+ * matrices instead of 4x4.
10
+ */
11
+ export class Object2d {
12
+ constructor() {
13
+ this.position = new Vec2(0, 0);
14
+ /** Rotation around the object's origin, in radians (counter-clockwise). */
15
+ this.rotation = 0;
16
+ this.scale = new Vec2(1, 1);
17
+ /**
18
+ * Draw order: higher values render on top. Objects with equal zIndex
19
+ * keep their scene order (later-added draws on top).
20
+ */
21
+ this.zIndex = 0;
22
+
23
+ this.parent = null;
24
+ this.children = [];
25
+ /** Invisible objects — and everything under them — are skipped by rendering and picking. */
26
+ this.visible = true;
27
+
28
+ this.localMatrix = new Mat3();
29
+ this.worldMatrix = new Mat3();
30
+ /** Renderer-local GPU resources, created lazily (see GpuResources2d). */
31
+ this._gpu = null;
32
+ }
33
+
34
+ add(child) {
35
+ // Adding an object to itself or to one of its own descendants would
36
+ // create a cycle and hang traverse/updateWorldMatrix — refuse it.
37
+ for (let node = this; node; node = node.parent) {
38
+ if (node === child) return this;
39
+ }
40
+ if (child.parent) child.parent.remove(child);
41
+ child.parent = this;
42
+ this.children.push(child);
43
+ return this;
44
+ }
45
+
46
+ remove(child) {
47
+ const index = this.children.indexOf(child);
48
+ if (index !== -1) {
49
+ child.parent = null;
50
+ this.children.splice(index, 1);
51
+ }
52
+ return this;
53
+ }
54
+
55
+ /** Recomputes localMatrix and worldMatrix for this object and all descendants. */
56
+ updateWorldMatrix() {
57
+ this.localMatrix.compose(this.position, this.rotation, this.scale);
58
+ if (this.parent) {
59
+ this.worldMatrix.multiplyMatrices(
60
+ this.parent.worldMatrix,
61
+ this.localMatrix,
62
+ );
63
+ } else {
64
+ this.worldMatrix.copy(this.localMatrix);
65
+ }
66
+ for (const child of this.children) {
67
+ child.updateWorldMatrix();
68
+ }
69
+ }
70
+
71
+ /** Calls `callback(object)` for this object and every descendant. */
72
+ traverse(callback) {
73
+ callback(this);
74
+ for (const child of this.children) {
75
+ child.traverse(callback);
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Like traverse, but skips objects with `visible = false` and their
81
+ * whole subtree — hiding a group hides everything inside it. The
82
+ * renderer and picking walk the scene with this.
83
+ */
84
+ traverseVisible(callback) {
85
+ if (!this.visible) return;
86
+ callback(this);
87
+ for (const child of this.children) {
88
+ child.traverseVisible(callback);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Destroys the per-object GPU resources created by every renderer for
94
+ * this object and its descendants (see GpuResources2d), releasing memory
95
+ * right away instead of waiting for GC. Geometry buffers are left alone:
96
+ * geometries may be shared between objects. Drawing a disposed object
97
+ * again re-creates its resources and re-uploads its instance data.
98
+ */
99
+ dispose() {
100
+ this.traverse((object) => {
101
+ disposeObjectGpuResources(object);
102
+ // Re-created instance buffers must receive the CPU-side data before use.
103
+ if (object.isInstanced) object.needsUpdate = true;
104
+ });
105
+ return this;
106
+ }
107
+ }
@@ -0,0 +1,110 @@
1
+ import {
2
+ INDEX_FORMAT,
3
+ SHADER_ENTRY_POINT,
4
+ STRAIGHT_ALPHA_BLEND,
5
+ isStripTopology,
6
+ } from '../../core/pipelineConstants.js';
7
+ import {
8
+ createMaterialPipelineLayouts,
9
+ } from '../../core/createMaterialPipelineLayouts.js';
10
+ import { materialUsesMap } from '../../core/materialResources.js';
11
+ import { SINGLE_SAMPLE_COUNT } from '../../core/rendererConfig.js';
12
+ import { vertexBufferLayouts2d } from '../shaders/vertexLayout.js';
13
+
14
+ /**
15
+ * Compiles and caches the render pipelines for 2D materials, and owns
16
+ * the bind group / pipeline layouts every 2D shader shares (the
17
+ * @group(0) / @group(1) uniform interface in Material2d).
18
+ *
19
+ * A pipeline is keyed by composed shader source + pipeline state (topology,
20
+ * cull mode, front face, textured or not, instanced or not): each
21
+ * combination compiles once and is shared by every material instance
22
+ * that matches.
23
+ *
24
+ * The pipelines differ from the 3D ones where 2D differs from 3D:
25
+ * - alpha blending is enabled (shapes draw back-to-front, so
26
+ * transparency just works — no depth buffer involved)
27
+ * - no depth/stencil state (the 2D render pass has no depth attachment)
28
+ * - culling defaults to 'none': a negative scale flips a shape's
29
+ * winding and it should still be visible
30
+ */
31
+ export class Pipelines2d {
32
+ constructor(device, format, sampleCount = SINGLE_SAMPLE_COUNT) {
33
+ this.device = device;
34
+ this.format = format;
35
+ /** MSAA sample count of the renderer's color target. */
36
+ this.sampleCount = sampleCount;
37
+ // composed WGSL -> Map of pipeline-state key -> GPURenderPipeline
38
+ this._cache = new Map();
39
+ // WGSL source -> GPUShaderModule, so pipeline variants that share a
40
+ // shader (e.g. the same material at another topology) compile once.
41
+ this._modules = new Map();
42
+
43
+ Object.assign(
44
+ this,
45
+ createMaterialPipelineLayouts(device, GPUShaderStage.VERTEX),
46
+ );
47
+ }
48
+
49
+ /** The render pipeline for a material's shader and fixed-function state. */
50
+ pipelineFor(material, instanced = false) {
51
+ const { topology, cullMode, frontFace } = material;
52
+ const shaderCode = instanced
53
+ ? material.instancedShaderCode
54
+ : material.shaderCode;
55
+ let variants = this._cache.get(shaderCode);
56
+ if (!variants) {
57
+ variants = new Map();
58
+ this._cache.set(shaderCode, variants);
59
+ }
60
+ const textured = materialUsesMap(material);
61
+ const stateKey = `${topology}|${cullMode}|${frontFace}|${textured}|${instanced}`;
62
+ let pipeline = variants.get(stateKey);
63
+ if (!pipeline) {
64
+ pipeline = this._build(material, shaderCode, { textured, instanced });
65
+ variants.set(stateKey, pipeline);
66
+ }
67
+ return pipeline;
68
+ }
69
+
70
+ _moduleFor(code) {
71
+ let module = this._modules.get(code);
72
+ if (!module) {
73
+ module = this.device.createShaderModule({ code });
74
+ this._modules.set(code, module);
75
+ }
76
+ return module;
77
+ }
78
+
79
+ _build(material, shaderCode, { textured, instanced }) {
80
+ const { topology, cullMode, frontFace } = material;
81
+ const primitive = { topology, cullMode, frontFace };
82
+ // Indexed draws on strip topologies must declare the index format
83
+ // up front; the renderer always uses uint32 indices.
84
+ if (isStripTopology(topology)) {
85
+ primitive.stripIndexFormat = INDEX_FORMAT;
86
+ }
87
+ const buffers = vertexBufferLayouts2d(instanced);
88
+ const module = this._moduleFor(shaderCode);
89
+ return this.device.createRenderPipeline({
90
+ layout: textured ? this.texturedPipelineLayout : this.pipelineLayout,
91
+ vertex: {
92
+ module,
93
+ entryPoint: SHADER_ENTRY_POINT.vertex,
94
+ buffers,
95
+ },
96
+ fragment: {
97
+ module,
98
+ entryPoint: SHADER_ENTRY_POINT.fragment,
99
+ targets: [
100
+ {
101
+ format: this.format,
102
+ blend: STRAIGHT_ALPHA_BLEND,
103
+ },
104
+ ],
105
+ },
106
+ primitive,
107
+ multisample: { count: this.sampleCount },
108
+ });
109
+ }
110
+ }