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,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SHADER_BIND_GROUP,
|
|
3
|
+
SHADER_BINDING,
|
|
4
|
+
SHADOW_BINDING,
|
|
5
|
+
} from './pipelineConstants.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates the bind-group and pipeline layouts shared by the material system.
|
|
9
|
+
* The 2D and 3D engines differ only in which stages can read frame uniforms.
|
|
10
|
+
*/
|
|
11
|
+
export function createMaterialPipelineLayouts(
|
|
12
|
+
device,
|
|
13
|
+
frameVisibility,
|
|
14
|
+
{ shadows = false } = {},
|
|
15
|
+
) {
|
|
16
|
+
const objectVisibility =
|
|
17
|
+
GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT;
|
|
18
|
+
|
|
19
|
+
const frameBindGroupLayout = device.createBindGroupLayout({
|
|
20
|
+
entries: [
|
|
21
|
+
{
|
|
22
|
+
binding: SHADER_BINDING.uniforms,
|
|
23
|
+
visibility: frameVisibility,
|
|
24
|
+
buffer: {},
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const objectBindGroupLayout = device.createBindGroupLayout({
|
|
30
|
+
entries: [
|
|
31
|
+
{
|
|
32
|
+
binding: SHADER_BINDING.uniforms,
|
|
33
|
+
visibility: objectVisibility,
|
|
34
|
+
buffer: {},
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const texturedObjectBindGroupLayout = device.createBindGroupLayout({
|
|
40
|
+
entries: [
|
|
41
|
+
{
|
|
42
|
+
binding: SHADER_BINDING.uniforms,
|
|
43
|
+
visibility: objectVisibility,
|
|
44
|
+
buffer: {},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
binding: SHADER_BINDING.map,
|
|
48
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
49
|
+
texture: {},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
binding: SHADER_BINDING.sampler,
|
|
53
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
54
|
+
sampler: {},
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const shadowBindGroupLayout = shadows
|
|
60
|
+
? device.createBindGroupLayout({
|
|
61
|
+
entries: [
|
|
62
|
+
{
|
|
63
|
+
binding: SHADOW_BINDING.uniforms,
|
|
64
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
65
|
+
buffer: {},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
binding: SHADOW_BINDING.map,
|
|
69
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
70
|
+
texture: { sampleType: 'depth' },
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
binding: SHADOW_BINDING.sampler,
|
|
74
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
75
|
+
sampler: { type: 'comparison' },
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
})
|
|
79
|
+
: null;
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
frameBindGroupLayout,
|
|
83
|
+
objectBindGroupLayout,
|
|
84
|
+
texturedObjectBindGroupLayout,
|
|
85
|
+
shadowBindGroupLayout,
|
|
86
|
+
pipelineLayout: createPipelineLayout(
|
|
87
|
+
device,
|
|
88
|
+
frameBindGroupLayout,
|
|
89
|
+
objectBindGroupLayout,
|
|
90
|
+
shadowBindGroupLayout,
|
|
91
|
+
),
|
|
92
|
+
texturedPipelineLayout: createPipelineLayout(
|
|
93
|
+
device,
|
|
94
|
+
frameBindGroupLayout,
|
|
95
|
+
texturedObjectBindGroupLayout,
|
|
96
|
+
shadowBindGroupLayout,
|
|
97
|
+
),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function createPipelineLayout(
|
|
102
|
+
device,
|
|
103
|
+
frameLayout,
|
|
104
|
+
objectLayout,
|
|
105
|
+
shadowLayout,
|
|
106
|
+
) {
|
|
107
|
+
const bindGroupLayouts = [];
|
|
108
|
+
bindGroupLayouts[SHADER_BIND_GROUP.frame] = frameLayout;
|
|
109
|
+
bindGroupLayouts[SHADER_BIND_GROUP.object] = objectLayout;
|
|
110
|
+
if (shadowLayout) {
|
|
111
|
+
bindGroupLayouts[SHADER_BIND_GROUP.shadow] = shadowLayout;
|
|
112
|
+
}
|
|
113
|
+
return device.createPipelineLayout({ bindGroupLayouts });
|
|
114
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coordinates renderers that deliberately share one canvas configuration and
|
|
3
|
+
* GPU device. Ownership moves to another live renderer when the original
|
|
4
|
+
* owner is disposed, and the device is destroyed only after the last member.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export function acquireDeviceLease(renderer, gpu, shared = null) {
|
|
8
|
+
const sourceCanvas =
|
|
9
|
+
shared?.canvas || gpu?.canvas || gpu?.context?.canvas || null;
|
|
10
|
+
if (sourceCanvas && sourceCanvas !== renderer.canvas) {
|
|
11
|
+
throw new Error('Shared renderers must use the same canvas');
|
|
12
|
+
}
|
|
13
|
+
if (!gpu?.device || !gpu.context || !gpu.format) {
|
|
14
|
+
throw new Error('The shared renderer must be initialized before it is used');
|
|
15
|
+
}
|
|
16
|
+
if (!gpu.colorFormat) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
'Shared WebGPU setups must provide an sRGB colorFormat configured in ' +
|
|
19
|
+
'the canvas context viewFormats',
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (shared?._deviceLease) {
|
|
24
|
+
const lease = shared._deviceLease;
|
|
25
|
+
lease.members.add(renderer);
|
|
26
|
+
renderer._deviceLease = lease;
|
|
27
|
+
renderer._ownsDevice = false;
|
|
28
|
+
return lease;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const managed = !shared;
|
|
32
|
+
const lease = {
|
|
33
|
+
device: gpu.device,
|
|
34
|
+
context: gpu.context,
|
|
35
|
+
format: gpu.format,
|
|
36
|
+
colorFormat: gpu.colorFormat,
|
|
37
|
+
canvas: renderer.canvas,
|
|
38
|
+
managed,
|
|
39
|
+
members: new Set([renderer]),
|
|
40
|
+
};
|
|
41
|
+
renderer._deviceLease = lease;
|
|
42
|
+
renderer._ownsDevice = managed;
|
|
43
|
+
return lease;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Releases one renderer's membership.
|
|
48
|
+
* @returns {boolean} whether this renderer must destroy/unconfigure the device
|
|
49
|
+
*/
|
|
50
|
+
export function releaseDeviceLease(renderer) {
|
|
51
|
+
const lease = renderer._deviceLease;
|
|
52
|
+
renderer._deviceLease = null;
|
|
53
|
+
|
|
54
|
+
// Supports renderers constructed manually in tests or older integrations.
|
|
55
|
+
if (!lease) return !!renderer._ownsDevice;
|
|
56
|
+
|
|
57
|
+
lease.members.delete(renderer);
|
|
58
|
+
if (!lease.managed || !renderer._ownsDevice) return false;
|
|
59
|
+
|
|
60
|
+
const successor = lease.members.values().next().value;
|
|
61
|
+
if (successor) {
|
|
62
|
+
successor._ownsDevice = true;
|
|
63
|
+
renderer._ownsDevice = false;
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// WebGPU has no built-in mipmap generation, so we do the standard
|
|
2
|
+
// trick: for each mip level, run a tiny render pass that draws a
|
|
3
|
+
// fullscreen triangle sampling the previous level with linear
|
|
4
|
+
// filtering. The pipeline is created once per device and cached.
|
|
5
|
+
|
|
6
|
+
import { SHADER_ENTRY_POINT } from './pipelineConstants.js';
|
|
7
|
+
|
|
8
|
+
const MIPMAP_BIND_GROUP = 0;
|
|
9
|
+
const MIPMAP_BINDING = Object.freeze({
|
|
10
|
+
source: 0,
|
|
11
|
+
sampler: 1,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const MIPMAP_WGSL = /* wgsl */ `
|
|
15
|
+
struct VertexOut {
|
|
16
|
+
@builtin(position) position: vec4f,
|
|
17
|
+
@location(0) uv: vec2f,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// One triangle that covers the whole target: uvs (0,0) (2,0) (0,2)
|
|
21
|
+
// mapped so uv (0,0) lands on the destination's top-left texel.
|
|
22
|
+
@vertex
|
|
23
|
+
fn vs(@builtin(vertex_index) index: u32) -> VertexOut {
|
|
24
|
+
var out: VertexOut;
|
|
25
|
+
let uv = vec2f(f32((index << 1u) & 2u), f32(index & 2u));
|
|
26
|
+
out.position = vec4f(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, 0.0, 1.0);
|
|
27
|
+
out.uv = uv;
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@group(${MIPMAP_BIND_GROUP}) @binding(${MIPMAP_BINDING.source})
|
|
32
|
+
var src: texture_2d<f32>;
|
|
33
|
+
@group(${MIPMAP_BIND_GROUP}) @binding(${MIPMAP_BINDING.sampler})
|
|
34
|
+
var srcSampler: sampler;
|
|
35
|
+
|
|
36
|
+
@fragment
|
|
37
|
+
fn fs(input: VertexOut) -> @location(0) vec4f {
|
|
38
|
+
return textureSample(src, srcSampler, input.uv);
|
|
39
|
+
}
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
// device -> { module, sampler, pipelines: Map(texture format -> pipeline) }
|
|
43
|
+
const generators = new WeakMap();
|
|
44
|
+
|
|
45
|
+
/** Mip levels needed to take a width x height image down to 1x1. */
|
|
46
|
+
export function mipLevelCount(width, height) {
|
|
47
|
+
return Math.floor(Math.log2(Math.max(width, height))) + 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Fills mip levels 1..levels-1 of `texture` (a GPUTexture of the given
|
|
52
|
+
* format whose level 0 is already written) by downsampling level by
|
|
53
|
+
* level. The texture must have TEXTURE_BINDING and RENDER_ATTACHMENT
|
|
54
|
+
* usage — the ones uploadTexture creates already do. For an '-srgb'
|
|
55
|
+
* format the samples decode to linear and the render target re-encodes,
|
|
56
|
+
* so the filtering itself happens in linear space.
|
|
57
|
+
*/
|
|
58
|
+
export function generateMipmaps(device, texture, levels, format) {
|
|
59
|
+
let generator = generators.get(device);
|
|
60
|
+
if (!generator) {
|
|
61
|
+
generator = {
|
|
62
|
+
module: device.createShaderModule({ code: MIPMAP_WGSL }),
|
|
63
|
+
sampler: device.createSampler({ minFilter: 'linear' }),
|
|
64
|
+
pipelines: new Map(),
|
|
65
|
+
};
|
|
66
|
+
generators.set(device, generator);
|
|
67
|
+
}
|
|
68
|
+
let pipeline = generator.pipelines.get(format);
|
|
69
|
+
if (!pipeline) {
|
|
70
|
+
pipeline = device.createRenderPipeline({
|
|
71
|
+
layout: 'auto',
|
|
72
|
+
vertex: {
|
|
73
|
+
module: generator.module,
|
|
74
|
+
entryPoint: SHADER_ENTRY_POINT.vertex,
|
|
75
|
+
},
|
|
76
|
+
fragment: {
|
|
77
|
+
module: generator.module,
|
|
78
|
+
entryPoint: SHADER_ENTRY_POINT.fragment,
|
|
79
|
+
targets: [{ format }],
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
generator.pipelines.set(format, pipeline);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const encoder = device.createCommandEncoder();
|
|
86
|
+
for (let level = 1; level < levels; level++) {
|
|
87
|
+
const bindGroup = device.createBindGroup({
|
|
88
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
89
|
+
entries: [
|
|
90
|
+
{
|
|
91
|
+
binding: MIPMAP_BINDING.source,
|
|
92
|
+
resource: texture.createView({
|
|
93
|
+
baseMipLevel: level - 1,
|
|
94
|
+
mipLevelCount: 1,
|
|
95
|
+
}),
|
|
96
|
+
},
|
|
97
|
+
{ binding: MIPMAP_BINDING.sampler, resource: generator.sampler },
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
const pass = encoder.beginRenderPass({
|
|
101
|
+
colorAttachments: [
|
|
102
|
+
{
|
|
103
|
+
view: texture.createView({ baseMipLevel: level, mipLevelCount: 1 }),
|
|
104
|
+
loadOp: 'clear',
|
|
105
|
+
storeOp: 'store',
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
pass.setPipeline(pipeline);
|
|
110
|
+
pass.setBindGroup(MIPMAP_BIND_GROUP, bindGroup);
|
|
111
|
+
pass.draw(3);
|
|
112
|
+
pass.end();
|
|
113
|
+
}
|
|
114
|
+
device.queue.submit([encoder.finish()]);
|
|
115
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_PRIMITIVE_TOPOLOGY,
|
|
3
|
+
isTriangleTopology,
|
|
4
|
+
} from './pipelineConstants.js';
|
|
5
|
+
|
|
6
|
+
const UINT32_PRIMITIVE_RESTART = 0xffffffff;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Visits indexed triangles in the same order and winding as WebGPU.
|
|
10
|
+
* Returning true from `visit` stops traversal early.
|
|
11
|
+
*/
|
|
12
|
+
export function forEachIndexedTriangle(indices, topology, visit) {
|
|
13
|
+
if (topology === DEFAULT_PRIMITIVE_TOPOLOGY) {
|
|
14
|
+
for (let offset = 0; offset + 2 < indices.length; offset += 3) {
|
|
15
|
+
if (visit(indices[offset], indices[offset + 1], indices[offset + 2])) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!isTriangleTopology(topology)) return false;
|
|
23
|
+
|
|
24
|
+
let first = null;
|
|
25
|
+
let second = null;
|
|
26
|
+
let triangleIndex = 0;
|
|
27
|
+
for (const index of indices) {
|
|
28
|
+
if (index === UINT32_PRIMITIVE_RESTART) {
|
|
29
|
+
first = null;
|
|
30
|
+
second = null;
|
|
31
|
+
triangleIndex = 0;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (first === null) {
|
|
35
|
+
first = index;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (second === null) {
|
|
39
|
+
second = index;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const stopped =
|
|
44
|
+
triangleIndex % 2 === 0
|
|
45
|
+
? visit(first, second, index)
|
|
46
|
+
: visit(second, first, index);
|
|
47
|
+
if (stopped) return true;
|
|
48
|
+
first = second;
|
|
49
|
+
second = index;
|
|
50
|
+
triangleIndex++;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { srgbColorFormat } from './rendererConfig.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Requests the GPU adapter/device and configures the canvas for WebGPU.
|
|
5
|
+
* Shared by Renderer (3D) and Renderer2d so both engines can drive the
|
|
6
|
+
* same canvas: a canvas context can only be configured for one device,
|
|
7
|
+
* so two renderers on one canvas must share the result of a single
|
|
8
|
+
* initWebGpu call (see `renderer.init(shared)`).
|
|
9
|
+
*
|
|
10
|
+
* `format` is the canvas's base format; `colorFormat` is the compatible sRGB
|
|
11
|
+
* view used by render pipelines so blending, clears and MSAA resolve in linear
|
|
12
|
+
* space before the attachment encodes the stored canvas pixels.
|
|
13
|
+
*
|
|
14
|
+
* @returns {Promise<{device: GPUDevice, context: GPUCanvasContext,
|
|
15
|
+
* format: GPUTextureFormat, colorFormat: GPUTextureFormat,
|
|
16
|
+
* canvas: HTMLCanvasElement}>}
|
|
17
|
+
*/
|
|
18
|
+
export async function initWebGpu(canvas) {
|
|
19
|
+
if (!navigator.gpu) {
|
|
20
|
+
throw new Error('WebGPU is not supported in this browser.');
|
|
21
|
+
}
|
|
22
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
23
|
+
if (!adapter) {
|
|
24
|
+
throw new Error('No suitable GPU adapter found.');
|
|
25
|
+
}
|
|
26
|
+
const device = await adapter.requestDevice();
|
|
27
|
+
let context = null;
|
|
28
|
+
try {
|
|
29
|
+
context = canvas.getContext('webgpu');
|
|
30
|
+
if (!context) throw new Error('Could not create a WebGPU canvas context.');
|
|
31
|
+
const format = navigator.gpu.getPreferredCanvasFormat();
|
|
32
|
+
const colorFormat = srgbColorFormat(format);
|
|
33
|
+
context.configure({
|
|
34
|
+
device,
|
|
35
|
+
format,
|
|
36
|
+
viewFormats: [colorFormat],
|
|
37
|
+
alphaMode: 'opaque',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Driver resets and GPU-process crashes otherwise freeze rendering
|
|
41
|
+
// silently; say what happened and how to recover. 'destroyed' is the
|
|
42
|
+
// deliberate renderer.dispose() path, not a failure.
|
|
43
|
+
device.lost.then((info) => {
|
|
44
|
+
if (info.reason !== 'destroyed') {
|
|
45
|
+
console.error(
|
|
46
|
+
`micro-gl: GPU device lost (${info.reason || 'unknown'}): ` +
|
|
47
|
+
`${info.message} Rendering has stopped. To recover, create and ` +
|
|
48
|
+
'initialize a new Renderer; scene resources upload ' +
|
|
49
|
+
'automatically when first drawn on the replacement device.',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return { device, context, format, colorFormat, canvas };
|
|
55
|
+
} catch (error) {
|
|
56
|
+
// A device was already allocated, so canvas-setup failure must not leak it.
|
|
57
|
+
try {
|
|
58
|
+
if (context) context.unconfigure();
|
|
59
|
+
} catch {
|
|
60
|
+
// Preserve the original setup error if the context also rejects cleanup.
|
|
61
|
+
}
|
|
62
|
+
device.destroy();
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns whether a material's shader declares texture-map bindings.
|
|
3
|
+
*
|
|
4
|
+
* `usesMap` describes the shader interface and deliberately does not depend
|
|
5
|
+
* on the current value of `map`: changing a resource must not silently change
|
|
6
|
+
* the pipeline layout. A textured material without its resource is therefore
|
|
7
|
+
* reported as a clear application error before WebGPU validation runs.
|
|
8
|
+
*/
|
|
9
|
+
export function materialUsesMap(material) {
|
|
10
|
+
if (!material.usesMap && !material.requiresMap) return false;
|
|
11
|
+
if (material.map) return true;
|
|
12
|
+
|
|
13
|
+
throw new Error(
|
|
14
|
+
`${material.constructor.name}: \`map\` was cleared, but this material ` +
|
|
15
|
+
'always samples its texture — assign a Texture or switch to a ' +
|
|
16
|
+
'material without a map',
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const orphanedResourceRegistry = new FinalizationRegistry(
|
|
2
|
+
({ owner, resourceRef }) => {
|
|
3
|
+
owner._objectGpuResourceRefs.delete(resourceRef);
|
|
4
|
+
},
|
|
5
|
+
);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Registers one manager-owned GPU record without strongly retaining the
|
|
9
|
+
* record or its scene object. Their normal garbage-collection fallback stays
|
|
10
|
+
* intact when an application drops a transient object without dispose().
|
|
11
|
+
*/
|
|
12
|
+
export function trackObjectGpuResource(owner, object, gpu) {
|
|
13
|
+
gpu.owner = owner;
|
|
14
|
+
gpu.objectRef = new WeakRef(object);
|
|
15
|
+
gpu.resourceRef = new WeakRef(gpu);
|
|
16
|
+
gpu.disposed = false;
|
|
17
|
+
owner._objectGpuResourceRefs.add(gpu.resourceRef);
|
|
18
|
+
orphanedResourceRegistry.register(
|
|
19
|
+
gpu,
|
|
20
|
+
{ owner, resourceRef: gpu.resourceRef },
|
|
21
|
+
gpu.resourceRef,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Destroys one mesh/shape record and unregisters it from its owner. */
|
|
26
|
+
export function destroyObjectGpuResource(gpu) {
|
|
27
|
+
if (!gpu || gpu.disposed) return;
|
|
28
|
+
gpu.uniformBuffer.destroy();
|
|
29
|
+
if (gpu.instanceBuffer) gpu.instanceBuffer.destroy();
|
|
30
|
+
gpu.disposed = true;
|
|
31
|
+
if (gpu.resourceRef) {
|
|
32
|
+
orphanedResourceRegistry.unregister(gpu.resourceRef);
|
|
33
|
+
if (gpu.owner) {
|
|
34
|
+
gpu.owner._objectGpuResourceRefs.delete(gpu.resourceRef);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
gpu.owner = null;
|
|
38
|
+
gpu.resourceRef = null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Releases all renderer-manager records attached to one scene object. */
|
|
42
|
+
export function disposeObjectGpuResources(object) {
|
|
43
|
+
if (!object._gpu) return;
|
|
44
|
+
for (const gpu of object._gpu.values()) destroyObjectGpuResource(gpu);
|
|
45
|
+
object._gpu = null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Releases only the records owned by one resource manager. Other renderers'
|
|
50
|
+
* cache entries on the same scene objects remain valid.
|
|
51
|
+
*/
|
|
52
|
+
export function disposeOwnedObjectGpuResources(owner) {
|
|
53
|
+
for (const resourceRef of [...owner._objectGpuResourceRefs]) {
|
|
54
|
+
const gpu = resourceRef.deref();
|
|
55
|
+
if (!gpu) {
|
|
56
|
+
orphanedResourceRegistry.unregister(resourceRef);
|
|
57
|
+
owner._objectGpuResourceRefs.delete(resourceRef);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const object = gpu.objectRef.deref();
|
|
61
|
+
destroyObjectGpuResource(gpu);
|
|
62
|
+
if (object?._gpu?.get(owner) === gpu) {
|
|
63
|
+
object._gpu.delete(owner);
|
|
64
|
+
if (object._gpu.size === 0) object._gpu = null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
owner._objectGpuResourceRefs.clear();
|
|
68
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Names shared by the JavaScript pipeline descriptors and WGSL modules.
|
|
3
|
+
* Keeping the interface in one place prevents a shader binding or entry
|
|
4
|
+
* point from silently drifting away from its matching WebGPU descriptor.
|
|
5
|
+
*/
|
|
6
|
+
export const SHADER_BIND_GROUP = Object.freeze({
|
|
7
|
+
frame: 0,
|
|
8
|
+
object: 1,
|
|
9
|
+
shadow: 2,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const SHADER_BINDING = Object.freeze({
|
|
13
|
+
uniforms: 0,
|
|
14
|
+
map: 1,
|
|
15
|
+
sampler: 2,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/** Bindings in the directional-shadow sampling group. */
|
|
19
|
+
export const SHADOW_BINDING = Object.freeze({
|
|
20
|
+
uniforms: 0,
|
|
21
|
+
map: 1,
|
|
22
|
+
sampler: 2,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const SHADER_ENTRY_POINT = Object.freeze({
|
|
26
|
+
vertex: 'vs',
|
|
27
|
+
fragment: 'fs',
|
|
28
|
+
shadowVertex: 'vsShadow',
|
|
29
|
+
shadowInstancedVertex: 'vsShadowInstanced',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const VERTEX_BUFFER_SLOT = Object.freeze({
|
|
33
|
+
geometry: 0,
|
|
34
|
+
instance: 1,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const DEFAULT_PRIMITIVE_TOPOLOGY = 'triangle-list';
|
|
38
|
+
export const DEFAULT_FRONT_FACE = 'ccw';
|
|
39
|
+
export const DEFAULT_CULL_MODE_3D = 'back';
|
|
40
|
+
export const DEFAULT_CULL_MODE_2D = 'none';
|
|
41
|
+
export const DEFAULT_DEPTH_COMPARE = 'less';
|
|
42
|
+
export const INDEX_FORMAT = 'uint32';
|
|
43
|
+
export const INSTANCE_STEP_MODE = 'instance';
|
|
44
|
+
|
|
45
|
+
const STRIP_TOPOLOGIES = new Set(['triangle-strip', 'line-strip']);
|
|
46
|
+
const TRIANGLE_TOPOLOGIES = new Set(['triangle-list', 'triangle-strip']);
|
|
47
|
+
|
|
48
|
+
/** Whether an indexed primitive topology needs `stripIndexFormat`. */
|
|
49
|
+
export function isStripTopology(topology) {
|
|
50
|
+
return STRIP_TOPOLOGIES.has(topology);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Whether a primitive can contribute polygonal depth to a shadow map. */
|
|
54
|
+
export function isTriangleTopology(topology) {
|
|
55
|
+
return TRIANGLE_TOPOLOGIES.has(topology);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Straight-alpha blending shared by transparent 3D and all 2D pipelines. */
|
|
59
|
+
export const STRAIGHT_ALPHA_BLEND = Object.freeze({
|
|
60
|
+
color: Object.freeze({
|
|
61
|
+
srcFactor: 'src-alpha',
|
|
62
|
+
dstFactor: 'one-minus-src-alpha',
|
|
63
|
+
}),
|
|
64
|
+
alpha: Object.freeze({
|
|
65
|
+
srcFactor: 'one',
|
|
66
|
+
dstFactor: 'one-minus-src-alpha',
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { srgbToLinear } from '../math/color.js';
|
|
2
|
+
|
|
3
|
+
/** Shared renderer defaults and WebGPU attachment formats. */
|
|
4
|
+
export const DEFAULT_CANVAS_WIDTH = 300;
|
|
5
|
+
export const DEFAULT_CANVAS_HEIGHT = 150;
|
|
6
|
+
export const MAX_PIXEL_RATIO = 2;
|
|
7
|
+
export const ANTIALIAS_SAMPLE_COUNT = 4;
|
|
8
|
+
export const SINGLE_SAMPLE_COUNT = 1;
|
|
9
|
+
export const DEPTH_FORMAT = 'depth24plus';
|
|
10
|
+
export const DEPTH_CLEAR_VALUE = 1;
|
|
11
|
+
|
|
12
|
+
/** Returns the sRGB view format compatible with a preferred canvas format. */
|
|
13
|
+
export function srgbColorFormat(canvasFormat) {
|
|
14
|
+
switch (canvasFormat) {
|
|
15
|
+
case 'bgra8unorm':
|
|
16
|
+
return 'bgra8unorm-srgb';
|
|
17
|
+
case 'rgba8unorm':
|
|
18
|
+
return 'rgba8unorm-srgb';
|
|
19
|
+
default:
|
|
20
|
+
throw new Error(`Unsupported canvas format: ${canvasFormat}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Decodes an authored sRGB clear color for an sRGB render attachment. */
|
|
25
|
+
export function linearClearColor(color) {
|
|
26
|
+
return [
|
|
27
|
+
srgbToLinear(color[0]),
|
|
28
|
+
srgbToLinear(color[1]),
|
|
29
|
+
srgbToLinear(color[2]),
|
|
30
|
+
color[3],
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Returns a bounded device-pixel ratio that is also safe in test environments. */
|
|
35
|
+
export function getPixelRatio() {
|
|
36
|
+
return Math.min(globalThis.devicePixelRatio || 1, MAX_PIXEL_RATIO);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Converts CSS dimensions into a valid WebGPU drawing-buffer extent. */
|
|
40
|
+
export function drawingBufferSize(width, height) {
|
|
41
|
+
const pixelRatio = getPixelRatio();
|
|
42
|
+
return {
|
|
43
|
+
width: Math.max(1, Math.floor(width * pixelRatio)),
|
|
44
|
+
height: Math.max(1, Math.floor(height * pixelRatio)),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Builds the color attachment shared by both renderers. */
|
|
49
|
+
export function colorAttachment(msaaView, swapView, clearValue) {
|
|
50
|
+
if (!msaaView) {
|
|
51
|
+
return {
|
|
52
|
+
view: swapView,
|
|
53
|
+
clearValue,
|
|
54
|
+
loadOp: 'clear',
|
|
55
|
+
storeOp: 'store',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
view: msaaView,
|
|
60
|
+
resolveTarget: swapView,
|
|
61
|
+
clearValue,
|
|
62
|
+
loadOp: 'clear',
|
|
63
|
+
// Only the resolved image reaches the canvas.
|
|
64
|
+
storeOp: 'discard',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { generateMipmaps, mipLevelCount } from './generateMipmaps.js';
|
|
2
|
+
|
|
3
|
+
const SRGB_TEXTURE_FORMAT = 'rgba8unorm-srgb';
|
|
4
|
+
const LINEAR_TEXTURE_FORMAT = 'rgba8unorm';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* GPU texture, view and sampler for a Texture, uploaded on first use
|
|
8
|
+
* and cached per GPUDevice on `texture._gpu`. Shared by GpuResources and
|
|
9
|
+
* GpuResources2d — texture upload is device-level plumbing, identical
|
|
10
|
+
* for both engines. A Texture can therefore be used by renderers on the
|
|
11
|
+
* same device or on independent devices.
|
|
12
|
+
*/
|
|
13
|
+
export function uploadTexture(device, texture) {
|
|
14
|
+
const cache = texture._gpu || (texture._gpu = new Map());
|
|
15
|
+
let gpu = cache.get(device);
|
|
16
|
+
|
|
17
|
+
if (!gpu) {
|
|
18
|
+
const size = [texture.width, texture.height];
|
|
19
|
+
const levels = texture.mipmaps
|
|
20
|
+
? mipLevelCount(texture.width, texture.height)
|
|
21
|
+
: 1;
|
|
22
|
+
// An sRGB format makes the GPU decode samples to linear (and
|
|
23
|
+
// re-encode when the mipmap pass renders into a level), so shading
|
|
24
|
+
// and mip filtering happen in linear space.
|
|
25
|
+
const format = texture.srgb ? SRGB_TEXTURE_FORMAT : LINEAR_TEXTURE_FORMAT;
|
|
26
|
+
const gpuTexture = device.createTexture({
|
|
27
|
+
size,
|
|
28
|
+
mipLevelCount: levels,
|
|
29
|
+
format,
|
|
30
|
+
// copyExternalImageToTexture requires RENDER_ATTACHMENT usage.
|
|
31
|
+
usage:
|
|
32
|
+
GPUTextureUsage.TEXTURE_BINDING |
|
|
33
|
+
GPUTextureUsage.COPY_DST |
|
|
34
|
+
GPUTextureUsage.RENDER_ATTACHMENT,
|
|
35
|
+
});
|
|
36
|
+
device.queue.copyExternalImageToTexture(
|
|
37
|
+
{ source: texture.source, flipY: texture.flipY },
|
|
38
|
+
{ texture: gpuTexture },
|
|
39
|
+
size,
|
|
40
|
+
);
|
|
41
|
+
if (levels > 1) generateMipmaps(device, gpuTexture, levels, format);
|
|
42
|
+
const sampler = device.createSampler({
|
|
43
|
+
magFilter: texture.magFilter,
|
|
44
|
+
minFilter: texture.minFilter,
|
|
45
|
+
mipmapFilter: texture.mipmaps ? 'linear' : 'nearest',
|
|
46
|
+
addressModeU: texture.addressModeU,
|
|
47
|
+
addressModeV: texture.addressModeV,
|
|
48
|
+
});
|
|
49
|
+
gpu = {
|
|
50
|
+
texture: gpuTexture,
|
|
51
|
+
view: gpuTexture.createView(),
|
|
52
|
+
sampler,
|
|
53
|
+
};
|
|
54
|
+
cache.set(device, gpu);
|
|
55
|
+
}
|
|
56
|
+
return gpu;
|
|
57
|
+
}
|