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,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
|
+
}
|