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,283 @@
1
+ import { Mat4 } from '../../math/Mat4.js';
2
+ import { Vec3 } from '../../math/Vec3.js';
3
+ import {
4
+ INDEX_FORMAT,
5
+ SHADER_BIND_GROUP,
6
+ SHADER_BINDING,
7
+ SHADOW_BINDING,
8
+ VERTEX_BUFFER_SLOT,
9
+ } from '../../core/pipelineConstants.js';
10
+ import {
11
+ DIRECTIONAL_SHADOW_DEPTH_FORMAT,
12
+ SHADOW_SAMPLE_COUNT,
13
+ } from '../constants.js';
14
+ import { ShadowPipelines } from './ShadowPipelines.js';
15
+
16
+ const FLOAT_BYTES = Float32Array.BYTES_PER_ELEMENT;
17
+ const MAT4_FLOATS = 16;
18
+ const PARAM_FLOATS = 4;
19
+ const SHADOW_UNIFORM_FLOATS = MAT4_FLOATS + PARAM_FLOATS;
20
+ const SHADOW_UNIFORM_SIZE = SHADOW_UNIFORM_FLOATS * FLOAT_BYTES;
21
+ const SHADOW_MAP_FALLBACK_SIZE = 1;
22
+ const SHADOW_DEPTH_CLEAR_VALUE = 1;
23
+ const DIRECTION_EPSILON = 1e-12;
24
+
25
+ export const SHADOW_UNIFORM_OFFSET = Object.freeze({
26
+ viewProjection: 0,
27
+ enabled: MAT4_FLOATS,
28
+ bias: MAT4_FLOATS + 1,
29
+ normalBias: MAT4_FLOATS + 2,
30
+ texelSize: MAT4_FLOATS + 3,
31
+ });
32
+
33
+ /**
34
+ * Renderer-local depth texture, uniforms and render pass for the first
35
+ * directional light in a scene. Lights only retain CPU configuration, so the
36
+ * same scene can be rendered safely by different GPU devices.
37
+ */
38
+ export class DirectionalShadowMap {
39
+ constructor(device, samplingLayout, objectLayout) {
40
+ this.device = device;
41
+ this.enabled = false;
42
+ this._mapSize = 0;
43
+ this._data = new Float32Array(SHADOW_UNIFORM_FLOATS);
44
+ this._identity = new Mat4();
45
+ this._direction = new Vec3();
46
+ this._uniformBuffer = null;
47
+ this._texture = null;
48
+
49
+ try {
50
+ this._uniformBuffer = device.createBuffer({
51
+ label: 'Directional shadow uniforms',
52
+ size: SHADOW_UNIFORM_SIZE,
53
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
54
+ });
55
+ this._shadowUniformLayout = device.createBindGroupLayout({
56
+ label: 'Directional shadow pass uniforms',
57
+ entries: [
58
+ {
59
+ binding: SHADER_BINDING.uniforms,
60
+ visibility: GPUShaderStage.VERTEX,
61
+ buffer: {},
62
+ },
63
+ ],
64
+ });
65
+ this._shadowUniformBindGroup = device.createBindGroup({
66
+ label: 'Directional shadow pass uniforms',
67
+ layout: this._shadowUniformLayout,
68
+ entries: [
69
+ {
70
+ binding: SHADER_BINDING.uniforms,
71
+ resource: { buffer: this._uniformBuffer },
72
+ },
73
+ ],
74
+ });
75
+ this._sampler = device.createSampler({
76
+ label: 'Directional shadow comparison sampler',
77
+ addressModeU: 'clamp-to-edge',
78
+ addressModeV: 'clamp-to-edge',
79
+ minFilter: 'linear',
80
+ magFilter: 'linear',
81
+ compare: 'less-equal',
82
+ });
83
+ this._samplingLayout = samplingLayout;
84
+ this._pipelines = new ShadowPipelines(
85
+ device,
86
+ this._shadowUniformLayout,
87
+ objectLayout,
88
+ );
89
+ this._replaceTexture(SHADOW_MAP_FALLBACK_SIZE);
90
+ this._writeDisabledUniforms();
91
+ } catch (error) {
92
+ this.dispose();
93
+ throw error;
94
+ }
95
+ }
96
+
97
+ /** Sampling bind group bound by the renderer's color pass. */
98
+ get bindGroup() {
99
+ return this._samplingBindGroup;
100
+ }
101
+
102
+ /**
103
+ * Validates the light's configuration, resizes the map if necessary and
104
+ * uploads the light-camera transform. Returns whether a depth pass is needed.
105
+ */
106
+ update(light) {
107
+ if (!light || !light.castShadow) {
108
+ const wasEnabled = this.enabled;
109
+ this.enabled = false;
110
+ if (wasEnabled) this._writeDisabledUniforms();
111
+ return false;
112
+ }
113
+
114
+ const shadow = light.shadow;
115
+ validateShadow(shadow, this.device.limits?.maxTextureDimension2D);
116
+ if (shadow.mapSize !== this._mapSize) {
117
+ this._replaceTexture(shadow.mapSize);
118
+ }
119
+
120
+ const camera = shadow.camera;
121
+ const directionLength = light.direction.length();
122
+ if (Number.isFinite(directionLength) && directionLength > DIRECTION_EPSILON) {
123
+ this._direction.copy(light.direction).multiplyScalar(1 / directionLength);
124
+ } else {
125
+ this._direction.set(0, -1, 0);
126
+ }
127
+
128
+ const distance = (camera.near + camera.far) / 2;
129
+ camera.position.copy(camera.target);
130
+ camera.position.x -= this._direction.x * distance;
131
+ camera.position.y -= this._direction.y * distance;
132
+ camera.position.z -= this._direction.z * distance;
133
+ camera.aspect = 1;
134
+ camera.updateMatrices();
135
+
136
+ this._data.set(
137
+ camera.viewProjectionMatrix.elements,
138
+ SHADOW_UNIFORM_OFFSET.viewProjection,
139
+ );
140
+ this._data[SHADOW_UNIFORM_OFFSET.enabled] = 1;
141
+ this._data[SHADOW_UNIFORM_OFFSET.bias] = shadow.bias;
142
+ this._data[SHADOW_UNIFORM_OFFSET.normalBias] = shadow.normalBias;
143
+ this._data[SHADOW_UNIFORM_OFFSET.texelSize] = 1 / shadow.mapSize;
144
+ this.device.queue.writeBuffer(this._uniformBuffer, 0, this._data);
145
+ this.enabled = true;
146
+ return true;
147
+ }
148
+
149
+ /** Encodes and returns the number of caster instances drawn. */
150
+ render(encoder, casters) {
151
+ if (!this.enabled) return 0;
152
+
153
+ const pass = encoder.beginRenderPass({
154
+ label: 'Directional shadow pass',
155
+ colorAttachments: [],
156
+ depthStencilAttachment: {
157
+ view: this._textureView,
158
+ depthClearValue: SHADOW_DEPTH_CLEAR_VALUE,
159
+ depthLoadOp: 'clear',
160
+ depthStoreOp: 'store',
161
+ },
162
+ });
163
+ pass.setBindGroup(SHADER_BIND_GROUP.frame, this._shadowUniformBindGroup);
164
+
165
+ let drawCount = 0;
166
+ for (const { mesh, geometryGPU, meshGPU } of casters) {
167
+ const instanced = !!mesh.isInstanced;
168
+ pass.setPipeline(this._pipelines.pipelineFor(mesh.material, instanced));
169
+ pass.setBindGroup(SHADER_BIND_GROUP.object, meshGPU.shadowBindGroup);
170
+ pass.setVertexBuffer(
171
+ VERTEX_BUFFER_SLOT.geometry,
172
+ geometryGPU.vertexBuffer,
173
+ );
174
+ if (instanced) {
175
+ pass.setVertexBuffer(
176
+ VERTEX_BUFFER_SLOT.instance,
177
+ meshGPU.instanceBuffer,
178
+ );
179
+ }
180
+ pass.setIndexBuffer(geometryGPU.indexBuffer, INDEX_FORMAT);
181
+ pass.drawIndexed(mesh.geometry.indexCount, instanced ? mesh.count : 1);
182
+ drawCount += instanced ? mesh.count : 1;
183
+ }
184
+ pass.end();
185
+ return drawCount;
186
+ }
187
+
188
+ dispose() {
189
+ if (this._texture) this._texture.destroy();
190
+ if (this._uniformBuffer) this._uniformBuffer.destroy();
191
+ this._texture = null;
192
+ this._textureView = null;
193
+ this._uniformBuffer = null;
194
+ this._samplingBindGroup = null;
195
+ this.enabled = false;
196
+ }
197
+
198
+ _writeDisabledUniforms() {
199
+ this._data.set(
200
+ this._identity.elements,
201
+ SHADOW_UNIFORM_OFFSET.viewProjection,
202
+ );
203
+ this._data.fill(0, SHADOW_UNIFORM_OFFSET.enabled);
204
+ this._data[SHADOW_UNIFORM_OFFSET.texelSize] = 1 / this._mapSize;
205
+ this.device.queue.writeBuffer(this._uniformBuffer, 0, this._data);
206
+ }
207
+
208
+ _replaceTexture(mapSize) {
209
+ const texture = this.device.createTexture({
210
+ label: 'Directional shadow map',
211
+ size: [mapSize, mapSize],
212
+ format: DIRECTIONAL_SHADOW_DEPTH_FORMAT,
213
+ sampleCount: SHADOW_SAMPLE_COUNT,
214
+ usage:
215
+ GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
216
+ });
217
+ let textureView;
218
+ let samplingBindGroup;
219
+ try {
220
+ textureView = texture.createView();
221
+ samplingBindGroup = this.device.createBindGroup({
222
+ label: 'Directional shadow sampling',
223
+ layout: this._samplingLayout,
224
+ entries: [
225
+ {
226
+ binding: SHADOW_BINDING.uniforms,
227
+ resource: { buffer: this._uniformBuffer },
228
+ },
229
+ { binding: SHADOW_BINDING.map, resource: textureView },
230
+ { binding: SHADOW_BINDING.sampler, resource: this._sampler },
231
+ ],
232
+ });
233
+ } catch (error) {
234
+ texture.destroy();
235
+ throw error;
236
+ }
237
+
238
+ const previousTexture = this._texture;
239
+ this._texture = texture;
240
+ this._textureView = textureView;
241
+ this._samplingBindGroup = samplingBindGroup;
242
+ this._mapSize = mapSize;
243
+ if (previousTexture) previousTexture.destroy();
244
+ }
245
+ }
246
+
247
+ function validateShadow(shadow, maxTextureDimension) {
248
+ if (!shadow || !shadow.camera) {
249
+ throw new TypeError('DirectionalLight.shadow must be a DirectionalShadow');
250
+ }
251
+ if (!Number.isInteger(shadow.mapSize) || shadow.mapSize < 1) {
252
+ throw new RangeError('DirectionalShadow.mapSize must be a positive integer');
253
+ }
254
+ if (
255
+ Number.isFinite(maxTextureDimension) &&
256
+ shadow.mapSize > maxTextureDimension
257
+ ) {
258
+ throw new RangeError(
259
+ `DirectionalShadow.mapSize cannot exceed ${maxTextureDimension}`,
260
+ );
261
+ }
262
+ if (!Number.isFinite(shadow.bias) || !Number.isFinite(shadow.normalBias)) {
263
+ throw new RangeError('Directional shadow biases must be finite numbers');
264
+ }
265
+
266
+ const { camera } = shadow;
267
+ if (!Number.isFinite(camera.size) || camera.size <= 0) {
268
+ throw new RangeError('Directional shadow camera size must be positive');
269
+ }
270
+ if (!Number.isFinite(camera.zoom) || camera.zoom <= 0) {
271
+ throw new RangeError('Directional shadow camera zoom must be positive');
272
+ }
273
+ if (
274
+ !Number.isFinite(camera.near) ||
275
+ !Number.isFinite(camera.far) ||
276
+ camera.near < 0 ||
277
+ camera.far <= camera.near
278
+ ) {
279
+ throw new RangeError(
280
+ 'Directional shadow camera requires 0 <= near < far',
281
+ );
282
+ }
283
+ }
@@ -0,0 +1,174 @@
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 { Pipelines } from './Pipelines.js';
10
+ import { OBJECT_UNIFORM_SIZE } from './Uniforms.js';
11
+
12
+ export { OBJECT_UNIFORM_SIZE } from './Uniforms.js';
13
+
14
+ /**
15
+ * Owns the GPU resources the renderer creates on behalf of geometries,
16
+ * materials and meshes, so scene objects stay plain data:
17
+ *
18
+ * - vertex + index buffers per geometry
19
+ * - a GPU texture + sampler per Texture
20
+ * - a small uniform buffer + bind group per mesh
21
+ *
22
+ * Everything is created lazily on first use. Geometry and texture
23
+ * resources are cached per GPUDevice; mesh resources are cached per
24
+ * GpuResources instance because their bind groups use this instance's
25
+ * layouts. Render pipelines and their shared layouts live in Pipelines.
26
+ */
27
+ export class GpuResources {
28
+ constructor(device, format, sampleCount = SINGLE_SAMPLE_COUNT) {
29
+ this.device = device;
30
+ this.pipelines = new Pipelines(device, format, sampleCount);
31
+ this._objectGpuResourceRefs = new Set();
32
+ // The renderer builds its per-frame bind group against this layout.
33
+ this.frameBindGroupLayout = this.pipelines.frameBindGroupLayout;
34
+ this.objectBindGroupLayout = this.pipelines.objectBindGroupLayout;
35
+ this.shadowBindGroupLayout = this.pipelines.shadowBindGroupLayout;
36
+ }
37
+
38
+ /** The render pipeline for a material + pipeline state — see Pipelines. */
39
+ pipelineFor(material, instanced = false) {
40
+ return this.pipelines.pipelineFor(material, instanced);
41
+ }
42
+
43
+ /** GPU texture, view and sampler for a Texture, uploaded on first use. */
44
+ textureFor(texture) {
45
+ return uploadTexture(this.device, texture);
46
+ }
47
+
48
+ /**
49
+ * Vertex and index buffers for a geometry, re-uploaded when its
50
+ * `needsUpdate` flag is set.
51
+ */
52
+ geometryFor(geometry) {
53
+ const cache = geometry._gpu || (geometry._gpu = new Map());
54
+ let gpu = cache.get(this.device);
55
+
56
+ if (!gpu) {
57
+ const vertexBuffer = this.device.createBuffer({
58
+ size: geometry.vertices.byteLength,
59
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
60
+ });
61
+ this.device.queue.writeBuffer(vertexBuffer, 0, geometry.vertices);
62
+
63
+ const indexBuffer = this.device.createBuffer({
64
+ size: geometry.indices.byteLength,
65
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
66
+ });
67
+ this.device.queue.writeBuffer(indexBuffer, 0, geometry.indices);
68
+
69
+ gpu = {
70
+ vertexBuffer,
71
+ indexBuffer,
72
+ revision: geometry._gpuRevision,
73
+ };
74
+ cache.set(this.device, gpu);
75
+ } else if (
76
+ geometry.needsUpdate ||
77
+ gpu.revision !== geometry._gpuRevision
78
+ ) {
79
+ this.device.queue.writeBuffer(gpu.vertexBuffer, 0, geometry.vertices);
80
+ this.device.queue.writeBuffer(gpu.indexBuffer, 0, geometry.indices);
81
+ gpu.revision = geometry._gpuRevision;
82
+ }
83
+
84
+ // Revisions keep the other devices' caches stale until each is drawn,
85
+ // so clearing this public hint here cannot make them miss an update.
86
+ geometry.needsUpdate = false;
87
+ return gpu;
88
+ }
89
+
90
+ /**
91
+ * Uniform buffer, bind group and staging array for a mesh. The bind
92
+ * group is rebuilt automatically whenever the material's `map`
93
+ * changes (a swapped material, a swapped texture, or a Texture
94
+ * re-uploaded after `dispose()`).
95
+ */
96
+ meshFor(mesh) {
97
+ const cache = mesh._gpu || (mesh._gpu = new Map());
98
+ let gpu = cache.get(this);
99
+ if (!gpu) {
100
+ gpu = {
101
+ uniformBuffer: this.device.createBuffer({
102
+ size: OBJECT_UNIFORM_SIZE,
103
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
104
+ }),
105
+ bindGroup: null,
106
+ shadowBindGroup: null,
107
+ mapGpu: null, // the uploaded texture the bind group samples
108
+ data: new Float32Array(
109
+ OBJECT_UNIFORM_SIZE / Float32Array.BYTES_PER_ELEMENT,
110
+ ),
111
+ };
112
+ gpu.shadowBindGroup = this.device.createBindGroup({
113
+ layout: this.pipelines.objectBindGroupLayout,
114
+ entries: [
115
+ {
116
+ binding: SHADER_BINDING.uniforms,
117
+ resource: { buffer: gpu.uniformBuffer },
118
+ },
119
+ ],
120
+ });
121
+ if (mesh.isInstanced) {
122
+ gpu.instanceBuffer = this.device.createBuffer({
123
+ size: mesh.instanceData.byteLength,
124
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
125
+ });
126
+ gpu.instanceRevision = null; // this fresh buffer has no data yet
127
+ }
128
+ cache.set(this, gpu);
129
+ trackObjectGpuResource(this, mesh, gpu);
130
+ }
131
+
132
+ if (
133
+ mesh.isInstanced &&
134
+ (mesh.needsUpdate ||
135
+ gpu.instanceRevision !== mesh._instanceRevision)
136
+ ) {
137
+ this.device.queue.writeBuffer(
138
+ gpu.instanceBuffer,
139
+ 0,
140
+ mesh.instanceData,
141
+ );
142
+ gpu.instanceRevision = mesh._instanceRevision;
143
+ mesh.needsUpdate = false;
144
+ }
145
+
146
+ const mapGpu = materialUsesMap(mesh.material)
147
+ ? this.textureFor(mesh.material.map)
148
+ : null;
149
+ if (!gpu.bindGroup || gpu.mapGpu !== mapGpu) {
150
+ let layout = this.pipelines.objectBindGroupLayout;
151
+ const entries = [
152
+ {
153
+ binding: SHADER_BINDING.uniforms,
154
+ resource: { buffer: gpu.uniformBuffer },
155
+ },
156
+ ];
157
+ if (mapGpu) {
158
+ layout = this.pipelines.texturedObjectBindGroupLayout;
159
+ entries.push(
160
+ { binding: SHADER_BINDING.map, resource: mapGpu.view },
161
+ { binding: SHADER_BINDING.sampler, resource: mapGpu.sampler },
162
+ );
163
+ }
164
+ gpu.bindGroup = this.device.createBindGroup({ layout, entries });
165
+ gpu.mapGpu = mapGpu;
166
+ }
167
+ return gpu;
168
+ }
169
+
170
+ /** Releases this manager's per-object buffers and cache entries. */
171
+ dispose() {
172
+ disposeOwnedObjectGpuResources(this);
173
+ }
174
+ }
@@ -0,0 +1,55 @@
1
+ import {
2
+ INSTANCE_MATRIX_COMPONENTS,
3
+ INSTANCE_SIZE,
4
+ } from '../constants.js';
5
+
6
+ /** Whether one packed instance contains a finite affine mat4 transform. */
7
+ export function isFiniteAffineInstanceMatrix(data, offset) {
8
+ if (!hasMatrixComponents(data, offset)) return false;
9
+ for (
10
+ let component = 0;
11
+ component < INSTANCE_MATRIX_COMPONENTS;
12
+ component++
13
+ ) {
14
+ if (!Number.isFinite(data[offset + component])) return false;
15
+ }
16
+ return hasAffineRow(data, offset);
17
+ }
18
+
19
+ /** Copies one packed affine instance transform into a Mat4-like target. */
20
+ export function copyAffineInstanceMatrix(data, instanceId, target) {
21
+ const offset = instanceId * INSTANCE_SIZE;
22
+ if (!hasMatrixComponents(data, offset)) return false;
23
+
24
+ const elements = target.elements;
25
+ for (
26
+ let component = 0;
27
+ component < INSTANCE_MATRIX_COMPONENTS;
28
+ component++
29
+ ) {
30
+ const value = data[offset + component];
31
+ if (!Number.isFinite(value)) return false;
32
+ elements[component] = value;
33
+ }
34
+ return hasAffineRow(elements, 0);
35
+ }
36
+
37
+ function hasMatrixComponents(data, offset) {
38
+ return (
39
+ data &&
40
+ Number.isInteger(offset) &&
41
+ offset >= 0 &&
42
+ offset + INSTANCE_MATRIX_COMPONENTS <= data.length
43
+ );
44
+ }
45
+
46
+ function hasAffineRow(elements, offset) {
47
+ // These values are exact for every affine Mat4 produced by the library.
48
+ // Even a tiny projective term can materially affect homogeneous coordinates.
49
+ return (
50
+ elements[offset + 3] === 0 &&
51
+ elements[offset + 7] === 0 &&
52
+ elements[offset + 11] === 0 &&
53
+ elements[offset + 15] === 1
54
+ );
55
+ }
@@ -0,0 +1,114 @@
1
+ import { INSTANCE_SIZE } from '../constants.js';
2
+ import { isFiniteAffineInstanceMatrix } from './InstanceMatrix.js';
3
+
4
+ /**
5
+ * Builds the mesh-local AABB containing every `instanceMatrix * bounds`.
6
+ * Returns `null` when custom data cannot be represented safely, which tells
7
+ * the renderer to keep the batch visible rather than risk a false cull.
8
+ */
9
+ export function computeInstancedBounds(bounds, instanceData, count) {
10
+ const min = bounds?.min;
11
+ const max = bounds?.max;
12
+ if (!hasFiniteBounds(min, max)) return null;
13
+ if (!Number.isInteger(count) || count < 0) return null;
14
+
15
+ const centerX = (min[0] + max[0]) / 2;
16
+ const centerY = (min[1] + max[1]) / 2;
17
+ const centerZ = (min[2] + max[2]) / 2;
18
+ const extentX = (max[0] - min[0]) / 2;
19
+ const extentY = (max[1] - min[1]) / 2;
20
+ const extentZ = (max[2] - min[2]) / 2;
21
+ const combinedMin = [Infinity, Infinity, Infinity];
22
+ const combinedMax = [-Infinity, -Infinity, -Infinity];
23
+
24
+ for (let instance = 0; instance < count; instance++) {
25
+ const offset = instance * INSTANCE_SIZE;
26
+ if (!isFiniteAffineInstanceMatrix(instanceData, offset)) return null;
27
+
28
+ const m00 = instanceData[offset];
29
+ const m10 = instanceData[offset + 1];
30
+ const m20 = instanceData[offset + 2];
31
+ const m01 = instanceData[offset + 4];
32
+ const m11 = instanceData[offset + 5];
33
+ const m21 = instanceData[offset + 6];
34
+ const m02 = instanceData[offset + 8];
35
+ const m12 = instanceData[offset + 9];
36
+ const m22 = instanceData[offset + 10];
37
+ const m03 = instanceData[offset + 12];
38
+ const m13 = instanceData[offset + 13];
39
+ const m23 = instanceData[offset + 14];
40
+
41
+ const transformedCenterX =
42
+ m00 * centerX + m01 * centerY + m02 * centerZ + m03;
43
+ const transformedCenterY =
44
+ m10 * centerX + m11 * centerY + m12 * centerZ + m13;
45
+ const transformedCenterZ =
46
+ m20 * centerX + m21 * centerY + m22 * centerZ + m23;
47
+ const transformedExtentX =
48
+ Math.abs(m00) * extentX +
49
+ Math.abs(m01) * extentY +
50
+ Math.abs(m02) * extentZ;
51
+ const transformedExtentY =
52
+ Math.abs(m10) * extentX +
53
+ Math.abs(m11) * extentY +
54
+ Math.abs(m12) * extentZ;
55
+ const transformedExtentZ =
56
+ Math.abs(m20) * extentX +
57
+ Math.abs(m21) * extentY +
58
+ Math.abs(m22) * extentZ;
59
+ if (
60
+ !Number.isFinite(transformedCenterX) ||
61
+ !Number.isFinite(transformedCenterY) ||
62
+ !Number.isFinite(transformedCenterZ) ||
63
+ !Number.isFinite(transformedExtentX) ||
64
+ !Number.isFinite(transformedExtentY) ||
65
+ !Number.isFinite(transformedExtentZ)
66
+ ) {
67
+ return null;
68
+ }
69
+
70
+ expandBounds(
71
+ combinedMin,
72
+ combinedMax,
73
+ transformedCenterX,
74
+ transformedCenterY,
75
+ transformedCenterZ,
76
+ transformedExtentX,
77
+ transformedExtentY,
78
+ transformedExtentZ,
79
+ );
80
+ }
81
+ return { min: combinedMin, max: combinedMax };
82
+ }
83
+
84
+ function hasFiniteBounds(min, max) {
85
+ if (!min || !max || min.length < 3 || max.length < 3) return false;
86
+ for (let axis = 0; axis < 3; axis++) {
87
+ if (
88
+ !Number.isFinite(min[axis]) ||
89
+ !Number.isFinite(max[axis]) ||
90
+ min[axis] > max[axis]
91
+ ) {
92
+ return false;
93
+ }
94
+ }
95
+ return true;
96
+ }
97
+
98
+ function expandBounds(
99
+ min,
100
+ max,
101
+ centerX,
102
+ centerY,
103
+ centerZ,
104
+ extentX,
105
+ extentY,
106
+ extentZ,
107
+ ) {
108
+ min[0] = Math.min(min[0], centerX - extentX);
109
+ min[1] = Math.min(min[1], centerY - extentY);
110
+ min[2] = Math.min(min[2], centerZ - extentZ);
111
+ max[0] = Math.max(max[0], centerX + extentX);
112
+ max[1] = Math.max(max[1], centerY + extentY);
113
+ max[2] = Math.max(max[2], centerZ + extentZ);
114
+ }