micro-gl 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +556 -0
- package/package.json +37 -0
- package/src/2d/cameras/Camera2d.js +46 -0
- package/src/2d/constants.js +3 -0
- package/src/2d/controls/DragControls2d.js +174 -0
- package/src/2d/controls/PanZoomControls.js +64 -0
- package/src/2d/core/GpuResources2d.js +163 -0
- package/src/2d/core/InstancedShape2d.js +74 -0
- package/src/2d/core/Object2d.js +107 -0
- package/src/2d/core/Pipelines2d.js +110 -0
- package/src/2d/core/Renderer2d.js +353 -0
- package/src/2d/core/Scene2d.js +13 -0
- package/src/2d/core/Shape2d.js +13 -0
- package/src/2d/core/Uniforms2d.js +13 -0
- package/src/2d/geometries/CircleGeometry.js +27 -0
- package/src/2d/geometries/Geometry2d.js +108 -0
- package/src/2d/geometries/RectGeometry.js +23 -0
- package/src/2d/materials/BasicMaterial2d.js +12 -0
- package/src/2d/materials/Material2d.js +78 -0
- package/src/2d/materials/SpriteMaterial2d.js +28 -0
- package/src/2d/shaders/fragments.js +23 -0
- package/src/2d/shaders/shared.js +28 -0
- package/src/2d/shaders/vertexLayout.js +74 -0
- package/src/2d/shaders/vertexStages.js +59 -0
- package/src/3d/cameras/Camera.js +66 -0
- package/src/3d/cameras/OrthographicCamera.js +35 -0
- package/src/3d/cameras/PerspectiveCamera.js +25 -0
- package/src/3d/constants.js +18 -0
- package/src/3d/controls/DragControls.js +168 -0
- package/src/3d/controls/OrbitControls.js +125 -0
- package/src/3d/core/DirectionalShadowMap.js +283 -0
- package/src/3d/core/GpuResources.js +174 -0
- package/src/3d/core/InstanceMatrix.js +55 -0
- package/src/3d/core/InstancedBounds.js +114 -0
- package/src/3d/core/InstancedMesh.js +124 -0
- package/src/3d/core/Mesh.js +26 -0
- package/src/3d/core/Object3d.js +101 -0
- package/src/3d/core/Pipelines.js +125 -0
- package/src/3d/core/RayIntersection.js +184 -0
- package/src/3d/core/Raycaster.js +246 -0
- package/src/3d/core/Renderer.js +492 -0
- package/src/3d/core/Scene.js +13 -0
- package/src/3d/core/ShadowPipelines.js +64 -0
- package/src/3d/core/Uniforms.js +132 -0
- package/src/3d/geometries/BoxGeometry.js +56 -0
- package/src/3d/geometries/Geometry.js +97 -0
- package/src/3d/geometries/PlaneGeometry.js +24 -0
- package/src/3d/geometries/SphereGeometry.js +46 -0
- package/src/3d/geometries/WireframeGeometry.js +39 -0
- package/src/3d/helpers/GridHelper.js +43 -0
- package/src/3d/lights/AmbientLight.js +18 -0
- package/src/3d/lights/DirectionalLight.js +26 -0
- package/src/3d/lights/DirectionalShadow.js +29 -0
- package/src/3d/lights/PointLight.js +23 -0
- package/src/3d/materials/BasicMaterial.js +11 -0
- package/src/3d/materials/LambertMaterial.js +13 -0
- package/src/3d/materials/Material.js +87 -0
- package/src/3d/materials/TextureMaterial.js +24 -0
- package/src/3d/shaders/fragments.js +34 -0
- package/src/3d/shaders/shadows.js +52 -0
- package/src/3d/shaders/shared.js +129 -0
- package/src/3d/shaders/vertexLayout.js +85 -0
- package/src/3d/shaders/vertexStages.js +70 -0
- package/src/core/PointerControls.js +258 -0
- package/src/core/Texture.js +87 -0
- package/src/core/createMaterialPipelineLayouts.js +114 -0
- package/src/core/deviceLease.js +67 -0
- package/src/core/generateMipmaps.js +115 -0
- package/src/core/indexedTriangles.js +53 -0
- package/src/core/initWebGpu.js +65 -0
- package/src/core/materialResources.js +18 -0
- package/src/core/objectGpuResources.js +68 -0
- package/src/core/pipelineConstants.js +68 -0
- package/src/core/rendererConfig.js +66 -0
- package/src/core/uploadTexture.js +57 -0
- package/src/index.js +68 -0
- package/src/math/Frustum.js +143 -0
- package/src/math/Mat3.js +165 -0
- package/src/math/Mat4.js +359 -0
- package/src/math/Vec2.js +72 -0
- package/src/math/Vec3.js +98 -0
- package/src/math/color.js +20 -0
|
@@ -0,0 +1,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
|
+
}
|