@multiplekex/shallot 0.1.7 → 0.1.9
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/package.json +2 -2
- package/src/core/component.ts +7 -17
- package/src/core/index.ts +2 -1
- package/src/core/math.ts +30 -5
- package/src/core/state.ts +2 -2
- package/src/core/types.ts +2 -2
- package/src/core/xml.ts +83 -33
- package/src/extras/arrows/index.ts +73 -95
- package/src/extras/lines/index.ts +97 -56
- package/src/extras/orbit/index.ts +1 -1
- package/src/extras/text/index.ts +245 -82
- package/src/standard/compute/index.ts +26 -2
- package/src/standard/compute/inspect.ts +15 -1
- package/src/standard/render/camera.ts +8 -1
- package/src/standard/render/forward.ts +54 -3
- package/src/standard/render/index.ts +52 -3
- package/src/standard/render/material/index.ts +92 -0
- package/src/standard/render/mesh/index.ts +66 -10
- package/src/standard/render/opaque.ts +44 -0
- package/src/standard/render/postprocess.ts +10 -2
- package/src/standard/render/scene.ts +12 -1
- package/src/standard/render/transparent.ts +94 -0
- package/src/standard/tween/sequence.ts +2 -2
- package/src/standard/tween/tween.ts +31 -13
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ComputeNode, ExecutionContext } from "../compute";
|
|
2
2
|
import type { ShapeBatch } from "./mesh";
|
|
3
3
|
import { DEPTH_FORMAT } from "./scene";
|
|
4
|
+
import type { OpaqueDrawCallback, OpaqueDrawContext } from "./opaque";
|
|
4
5
|
|
|
5
6
|
export const INDIRECT_SIZE = 20;
|
|
6
7
|
|
|
@@ -54,6 +55,7 @@ struct VertexOutput {
|
|
|
54
55
|
@builtin(position) position: vec4<f32>,
|
|
55
56
|
@location(0) color: vec4<f32>,
|
|
56
57
|
@location(1) worldNormal: vec3<f32>,
|
|
58
|
+
@location(2) @interpolate(flat) materialId: u32,
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
struct Scene {
|
|
@@ -62,6 +64,18 @@ struct Scene {
|
|
|
62
64
|
ambientColor: vec4<f32>,
|
|
63
65
|
sunDirection: vec4<f32>,
|
|
64
66
|
sunColor: vec4<f32>,
|
|
67
|
+
cameraMode: f32,
|
|
68
|
+
cameraSize: f32,
|
|
69
|
+
viewport: vec2<f32>,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
struct MaterialData {
|
|
73
|
+
emission: vec3<f32>,
|
|
74
|
+
roughness: f32,
|
|
75
|
+
metallic: f32,
|
|
76
|
+
_pad0: f32,
|
|
77
|
+
_pad1: f32,
|
|
78
|
+
_pad2: f32,
|
|
65
79
|
}
|
|
66
80
|
|
|
67
81
|
@group(0) @binding(0) var<uniform> scene: Scene;
|
|
@@ -69,6 +83,8 @@ struct Scene {
|
|
|
69
83
|
@group(0) @binding(2) var<storage, read> matrices: array<mat4x4<f32>>;
|
|
70
84
|
@group(0) @binding(3) var<storage, read> colors: array<vec4<f32>>;
|
|
71
85
|
@group(0) @binding(4) var<storage, read> sizes: array<vec4<f32>>;
|
|
86
|
+
@group(0) @binding(5) var<storage, read> materials: array<MaterialData>;
|
|
87
|
+
@group(0) @binding(6) var<storage, read> matIds: array<u32>;
|
|
72
88
|
|
|
73
89
|
@vertex
|
|
74
90
|
fn vs(input: VertexInput) -> VertexOutput {
|
|
@@ -82,19 +98,22 @@ fn vs(input: VertexInput) -> VertexOutput {
|
|
|
82
98
|
output.position = scene.viewProj * worldPos;
|
|
83
99
|
output.color = colors[eid];
|
|
84
100
|
output.worldNormal = worldNormal;
|
|
101
|
+
output.materialId = matIds[eid];
|
|
85
102
|
return output;
|
|
86
103
|
}
|
|
87
104
|
|
|
88
105
|
@fragment
|
|
89
106
|
fn fs(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
107
|
+
let mat = materials[input.materialId];
|
|
90
108
|
let normal = normalize(input.worldNormal);
|
|
91
109
|
let NdotL = max(dot(normal, -scene.sunDirection.xyz), 0.0);
|
|
92
110
|
|
|
93
111
|
let ambient = scene.ambientColor.rgb * scene.ambientColor.a;
|
|
94
112
|
let diffuse = scene.sunColor.rgb * NdotL;
|
|
95
|
-
let
|
|
113
|
+
let diffuseFactor = 1.0 - mat.metallic * 0.9;
|
|
114
|
+
let lighting = ambient + diffuse * diffuseFactor;
|
|
96
115
|
|
|
97
|
-
return vec4<f32>(input.color.rgb * lighting, input.color.a);
|
|
116
|
+
return vec4<f32>(input.color.rgb * lighting + mat.emission, input.color.a);
|
|
98
117
|
}
|
|
99
118
|
`;
|
|
100
119
|
|
|
@@ -143,8 +162,11 @@ export interface ForwardConfig {
|
|
|
143
162
|
matrices: GPUBuffer;
|
|
144
163
|
colors: GPUBuffer;
|
|
145
164
|
sizes: GPUBuffer;
|
|
165
|
+
materials: GPUBuffer;
|
|
166
|
+
materialIds: GPUBuffer;
|
|
146
167
|
indirect: GPUBuffer;
|
|
147
168
|
batches: Map<number, ShapeBatch>;
|
|
169
|
+
getCallbacks: () => OpaqueDrawCallback[];
|
|
148
170
|
}
|
|
149
171
|
|
|
150
172
|
export function createForwardNode(config: ForwardConfig): ComputeNode {
|
|
@@ -154,7 +176,10 @@ export function createForwardNode(config: ForwardConfig): ComputeNode {
|
|
|
154
176
|
return {
|
|
155
177
|
id: "forward",
|
|
156
178
|
inputs: [],
|
|
157
|
-
outputs: [
|
|
179
|
+
outputs: [
|
|
180
|
+
{ id: "scene", access: "write" },
|
|
181
|
+
{ id: "depth", access: "write" },
|
|
182
|
+
],
|
|
158
183
|
|
|
159
184
|
execute(ctx: ExecutionContext) {
|
|
160
185
|
const { device, encoder, format } = ctx;
|
|
@@ -182,6 +207,26 @@ export function createForwardNode(config: ForwardConfig): ComputeNode {
|
|
|
182
207
|
},
|
|
183
208
|
});
|
|
184
209
|
|
|
210
|
+
const callbacks = config.getCallbacks();
|
|
211
|
+
const preCallbacks = callbacks
|
|
212
|
+
.filter((c) => c.order < 0)
|
|
213
|
+
.sort((a, b) => a.order - b.order);
|
|
214
|
+
const postCallbacks = callbacks
|
|
215
|
+
.filter((c) => c.order >= 0)
|
|
216
|
+
.sort((a, b) => a.order - b.order);
|
|
217
|
+
|
|
218
|
+
const drawCtx: OpaqueDrawContext = {
|
|
219
|
+
device,
|
|
220
|
+
format,
|
|
221
|
+
depthFormat: DEPTH_FORMAT,
|
|
222
|
+
scene: config.scene,
|
|
223
|
+
matrices: config.matrices,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
for (const callback of preCallbacks) {
|
|
227
|
+
callback.draw(pass, drawCtx);
|
|
228
|
+
}
|
|
229
|
+
|
|
185
230
|
pass.setPipeline(pipeline);
|
|
186
231
|
|
|
187
232
|
for (const batch of config.batches.values()) {
|
|
@@ -195,6 +240,8 @@ export function createForwardNode(config: ForwardConfig): ComputeNode {
|
|
|
195
240
|
{ binding: 2, resource: { buffer: config.matrices } },
|
|
196
241
|
{ binding: 3, resource: { buffer: config.colors } },
|
|
197
242
|
{ binding: 4, resource: { buffer: config.sizes } },
|
|
243
|
+
{ binding: 5, resource: { buffer: config.materials } },
|
|
244
|
+
{ binding: 6, resource: { buffer: config.materialIds } },
|
|
198
245
|
],
|
|
199
246
|
});
|
|
200
247
|
bindGroups.set(batch.index, bindGroup);
|
|
@@ -206,6 +253,10 @@ export function createForwardNode(config: ForwardConfig): ComputeNode {
|
|
|
206
253
|
pass.drawIndexedIndirect(config.indirect, batch.index * INDIRECT_SIZE);
|
|
207
254
|
}
|
|
208
255
|
|
|
256
|
+
for (const callback of postCallbacks) {
|
|
257
|
+
callback.draw(pass, drawCtx);
|
|
258
|
+
}
|
|
259
|
+
|
|
209
260
|
pass.end();
|
|
210
261
|
},
|
|
211
262
|
};
|
|
@@ -13,22 +13,30 @@ import {
|
|
|
13
13
|
type ShapeBatch,
|
|
14
14
|
type MeshBuffers,
|
|
15
15
|
} from "./mesh";
|
|
16
|
+
import { Material, MaterialIds, packMaterials } from "./material";
|
|
16
17
|
import { createSceneBuffer, ensureRenderTextures } from "./scene";
|
|
17
18
|
import { createForwardNode, createIndirectBuffer } from "./forward";
|
|
18
19
|
import { createPostProcessNode, type PostProcessUniforms } from "./postprocess";
|
|
20
|
+
import { TransparentPass, createTransparentNode, type TransparentPassState } from "./transparent";
|
|
21
|
+
import { OpaquePass, getOpaqueCallbacks, type OpaquePassState } from "./opaque";
|
|
19
22
|
|
|
20
23
|
export * from "./camera";
|
|
21
24
|
export * from "./light";
|
|
22
25
|
export * from "./mesh";
|
|
26
|
+
export * from "./material";
|
|
23
27
|
export * from "./scene";
|
|
24
28
|
export * from "./forward";
|
|
25
29
|
export * from "./postprocess";
|
|
30
|
+
export * from "./transparent";
|
|
31
|
+
export * from "./opaque";
|
|
26
32
|
|
|
27
33
|
export interface RenderState {
|
|
28
34
|
scene: GPUBuffer;
|
|
29
35
|
matrices: GPUBuffer;
|
|
30
36
|
colors: GPUBuffer;
|
|
31
37
|
sizes: GPUBuffer;
|
|
38
|
+
materials: GPUBuffer;
|
|
39
|
+
materialIds: GPUBuffer;
|
|
32
40
|
indirect: GPUBuffer;
|
|
33
41
|
batches: Map<number, ShapeBatch>;
|
|
34
42
|
buffers: Map<number, MeshBuffers>;
|
|
@@ -60,7 +68,7 @@ const RenderSystem: System = {
|
|
|
60
68
|
|
|
61
69
|
for (const eid of state.query([Camera])) {
|
|
62
70
|
if (Camera.active[eid]) {
|
|
63
|
-
uploadCamera(device, render.scene, eid, width
|
|
71
|
+
uploadCamera(device, render.scene, eid, width, height);
|
|
64
72
|
|
|
65
73
|
render.postProcess.tonemap = state.hasComponent(eid, Tonemap);
|
|
66
74
|
if (render.postProcess.tonemap) {
|
|
@@ -71,6 +79,8 @@ const RenderSystem: System = {
|
|
|
71
79
|
render.postProcess.vignetteStrength = Vignette.strength[eid];
|
|
72
80
|
render.postProcess.vignetteInner = Vignette.inner[eid];
|
|
73
81
|
render.postProcess.vignetteOuter = Vignette.outer[eid];
|
|
82
|
+
} else {
|
|
83
|
+
render.postProcess.vignetteStrength = 0;
|
|
74
84
|
}
|
|
75
85
|
break;
|
|
76
86
|
}
|
|
@@ -117,13 +127,31 @@ const RenderSystem: System = {
|
|
|
117
127
|
const byShape = collectByShape(meshEntities);
|
|
118
128
|
device.queue.writeBuffer(render.colors, 0, MeshColors.data);
|
|
119
129
|
device.queue.writeBuffer(render.sizes, 0, MeshSizes.data);
|
|
130
|
+
|
|
131
|
+
for (const eid of state.query([Material])) {
|
|
132
|
+
MaterialIds.data[eid] = Material.type[eid];
|
|
133
|
+
}
|
|
134
|
+
device.queue.writeBuffer(render.materials, 0, packMaterials() as Float32Array<ArrayBuffer>);
|
|
135
|
+
device.queue.writeBuffer(render.materialIds, 0, MaterialIds.data);
|
|
136
|
+
|
|
120
137
|
updateBatches(device, byShape, render, render.indirect);
|
|
121
138
|
},
|
|
122
139
|
};
|
|
123
140
|
|
|
141
|
+
const DefaultMaterialSystem: System = {
|
|
142
|
+
group: "setup",
|
|
143
|
+
update(state: State) {
|
|
144
|
+
for (const eid of state.query([Mesh])) {
|
|
145
|
+
if (!state.hasComponent(eid, Material)) {
|
|
146
|
+
state.addComponent(eid, Material);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
124
152
|
export const RenderPlugin: Plugin = {
|
|
125
|
-
systems: [RenderSystem],
|
|
126
|
-
components: { Camera, Mesh, AmbientLight, DirectionalLight, Tonemap, FXAA, Vignette },
|
|
153
|
+
systems: [DefaultMaterialSystem, RenderSystem],
|
|
154
|
+
components: { Camera, Mesh, Material, AmbientLight, DirectionalLight, Tonemap, FXAA, Vignette },
|
|
127
155
|
dependencies: [ComputePlugin],
|
|
128
156
|
|
|
129
157
|
initialize(state: State) {
|
|
@@ -144,6 +172,8 @@ export const RenderPlugin: Plugin = {
|
|
|
144
172
|
matrices: createPropertyBuffer(MAX_ENTITIES * 64),
|
|
145
173
|
colors: createPropertyBuffer(MAX_ENTITIES * 16),
|
|
146
174
|
sizes: createPropertyBuffer(MAX_ENTITIES * 16),
|
|
175
|
+
materials: createPropertyBuffer(256 * 32),
|
|
176
|
+
materialIds: createPropertyBuffer(MAX_ENTITIES * 4),
|
|
147
177
|
indirect: createIndirectBuffer(device, 16),
|
|
148
178
|
batches: new Map(),
|
|
149
179
|
buffers: new Map(),
|
|
@@ -159,14 +189,33 @@ export const RenderPlugin: Plugin = {
|
|
|
159
189
|
|
|
160
190
|
state.setResource(Render, renderState);
|
|
161
191
|
|
|
192
|
+
const opaqueState: OpaquePassState = {
|
|
193
|
+
callbacks: new Map(),
|
|
194
|
+
};
|
|
195
|
+
state.setResource(OpaquePass, opaqueState);
|
|
196
|
+
|
|
197
|
+
const transparentState: TransparentPassState = {
|
|
198
|
+
contributors: new Map(),
|
|
199
|
+
};
|
|
200
|
+
state.setResource(TransparentPass, transparentState);
|
|
201
|
+
|
|
162
202
|
compute.graph.add(
|
|
163
203
|
createForwardNode({
|
|
164
204
|
scene: renderState.scene,
|
|
165
205
|
matrices: renderState.matrices,
|
|
166
206
|
colors: renderState.colors,
|
|
167
207
|
sizes: renderState.sizes,
|
|
208
|
+
materials: renderState.materials,
|
|
209
|
+
materialIds: renderState.materialIds,
|
|
168
210
|
indirect: renderState.indirect,
|
|
169
211
|
batches: renderState.batches,
|
|
212
|
+
getCallbacks: () => getOpaqueCallbacks(state),
|
|
213
|
+
})
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
compute.graph.add(
|
|
217
|
+
createTransparentNode({
|
|
218
|
+
getContributors: () => Array.from(transparentState.contributors.values()),
|
|
170
219
|
})
|
|
171
220
|
);
|
|
172
221
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { MAX_ENTITIES } from "../../../core";
|
|
2
|
+
import { setTraits } from "../../../core/component";
|
|
3
|
+
|
|
4
|
+
export interface MaterialData {
|
|
5
|
+
roughness?: number;
|
|
6
|
+
metallic?: number;
|
|
7
|
+
emissionColor?: number;
|
|
8
|
+
emissionIntensity?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const materials: MaterialData[] = [];
|
|
12
|
+
|
|
13
|
+
function initBuiltIns(): void {
|
|
14
|
+
if (materials.length === 0) {
|
|
15
|
+
materials.push({
|
|
16
|
+
roughness: 0.9,
|
|
17
|
+
metallic: 0.0,
|
|
18
|
+
emissionColor: 0x000000,
|
|
19
|
+
emissionIntensity: 0.0,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
initBuiltIns();
|
|
25
|
+
|
|
26
|
+
export const MaterialType = {
|
|
27
|
+
Default: 0,
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
export function material(data: MaterialData): number {
|
|
31
|
+
const id = materials.length;
|
|
32
|
+
materials.push(data);
|
|
33
|
+
return id;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getMaterial(id: number): MaterialData | undefined {
|
|
37
|
+
return materials[id];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function clearMaterials(): void {
|
|
41
|
+
materials.length = 0;
|
|
42
|
+
initBuiltIns();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const MaterialIds = {
|
|
46
|
+
data: new Uint32Array(MAX_ENTITIES),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const Material: {
|
|
50
|
+
type: number[];
|
|
51
|
+
} = {
|
|
52
|
+
type: [],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
setTraits(Material, {
|
|
56
|
+
defaults: () => ({
|
|
57
|
+
type: MaterialType.Default,
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
function hexToRgb(hex: number): { r: number; g: number; b: number } {
|
|
62
|
+
return {
|
|
63
|
+
r: ((hex >> 16) & 0xff) / 255,
|
|
64
|
+
g: ((hex >> 8) & 0xff) / 255,
|
|
65
|
+
b: (hex & 0xff) / 255,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function packMaterials(): Float32Array {
|
|
70
|
+
const floatsPerMaterial = 8;
|
|
71
|
+
const buffer = new Float32Array(materials.length * floatsPerMaterial);
|
|
72
|
+
|
|
73
|
+
for (let i = 0; i < materials.length; i++) {
|
|
74
|
+
const mat = materials[i];
|
|
75
|
+
const offset = i * floatsPerMaterial;
|
|
76
|
+
|
|
77
|
+
const emissionColor = mat.emissionColor ?? 0x000000;
|
|
78
|
+
const emissionIntensity = mat.emissionIntensity ?? 0.0;
|
|
79
|
+
const rgb = hexToRgb(emissionColor);
|
|
80
|
+
|
|
81
|
+
buffer[offset + 0] = rgb.r * emissionIntensity;
|
|
82
|
+
buffer[offset + 1] = rgb.g * emissionIntensity;
|
|
83
|
+
buffer[offset + 2] = rgb.b * emissionIntensity;
|
|
84
|
+
buffer[offset + 3] = mat.roughness ?? 0.9;
|
|
85
|
+
buffer[offset + 4] = mat.metallic ?? 0.0;
|
|
86
|
+
buffer[offset + 5] = 0;
|
|
87
|
+
buffer[offset + 6] = 0;
|
|
88
|
+
buffer[offset + 7] = 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return buffer;
|
|
92
|
+
}
|
|
@@ -13,21 +13,37 @@ export interface MeshData {
|
|
|
13
13
|
indexCount: number;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
const meshes: MeshData[] = [];
|
|
17
|
+
|
|
18
|
+
function initBuiltIns(): void {
|
|
19
|
+
if (meshes.length === 0) {
|
|
20
|
+
meshes.push(createBox());
|
|
21
|
+
meshes.push(createSphere());
|
|
22
|
+
meshes.push(createPlane());
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
initBuiltIns();
|
|
27
|
+
|
|
16
28
|
export const MeshShape = {
|
|
17
29
|
Box: 0,
|
|
18
30
|
Sphere: 1,
|
|
19
31
|
Plane: 2,
|
|
20
32
|
} as const;
|
|
21
33
|
|
|
22
|
-
export function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
export function mesh(data: MeshData): number {
|
|
35
|
+
const id = meshes.length;
|
|
36
|
+
meshes.push(data);
|
|
37
|
+
return id;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getMesh(id: number): MeshData | undefined {
|
|
41
|
+
return meshes[id];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function clearMeshes(): void {
|
|
45
|
+
meshes.length = 0;
|
|
46
|
+
initBuiltIns();
|
|
31
47
|
}
|
|
32
48
|
|
|
33
49
|
export const MeshColors = {
|
|
@@ -76,6 +92,36 @@ function colorProxy(): ColorProxy {
|
|
|
76
92
|
});
|
|
77
93
|
}
|
|
78
94
|
|
|
95
|
+
interface ColorChannelProxy extends Array<number>, FieldAccessor {}
|
|
96
|
+
|
|
97
|
+
function colorChannelProxy(channelIndex: number): ColorChannelProxy {
|
|
98
|
+
const data = MeshColors.data;
|
|
99
|
+
|
|
100
|
+
function getValue(eid: number): number {
|
|
101
|
+
return data[eid * 4 + channelIndex];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function setValue(eid: number, value: number): void {
|
|
105
|
+
data[eid * 4 + channelIndex] = value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return new Proxy([] as unknown as ColorChannelProxy, {
|
|
109
|
+
get(_, prop) {
|
|
110
|
+
if (prop === "get") return getValue;
|
|
111
|
+
if (prop === "set") return setValue;
|
|
112
|
+
const eid = Number(prop);
|
|
113
|
+
if (Number.isNaN(eid)) return undefined;
|
|
114
|
+
return getValue(eid);
|
|
115
|
+
},
|
|
116
|
+
set(_, prop, value) {
|
|
117
|
+
const eid = Number(prop);
|
|
118
|
+
if (Number.isNaN(eid)) return false;
|
|
119
|
+
setValue(eid, value);
|
|
120
|
+
return true;
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
79
125
|
interface SizeProxy extends Array<number>, FieldAccessor {}
|
|
80
126
|
|
|
81
127
|
function sizeProxy(component: number): SizeProxy {
|
|
@@ -109,12 +155,18 @@ function sizeProxy(component: number): SizeProxy {
|
|
|
109
155
|
export const Mesh: {
|
|
110
156
|
shape: number[];
|
|
111
157
|
color: ColorProxy;
|
|
158
|
+
colorR: ColorChannelProxy;
|
|
159
|
+
colorG: ColorChannelProxy;
|
|
160
|
+
colorB: ColorChannelProxy;
|
|
112
161
|
sizeX: SizeProxy;
|
|
113
162
|
sizeY: SizeProxy;
|
|
114
163
|
sizeZ: SizeProxy;
|
|
115
164
|
} = {
|
|
116
165
|
shape: [],
|
|
117
166
|
color: colorProxy(),
|
|
167
|
+
colorR: colorChannelProxy(0),
|
|
168
|
+
colorG: colorChannelProxy(1),
|
|
169
|
+
colorB: colorChannelProxy(2),
|
|
118
170
|
sizeX: sizeProxy(0),
|
|
119
171
|
sizeY: sizeProxy(1),
|
|
120
172
|
sizeZ: sizeProxy(2),
|
|
@@ -130,6 +182,9 @@ setTraits(Mesh, {
|
|
|
130
182
|
}),
|
|
131
183
|
accessors: {
|
|
132
184
|
color: Mesh.color,
|
|
185
|
+
colorR: Mesh.colorR,
|
|
186
|
+
colorG: Mesh.colorG,
|
|
187
|
+
colorB: Mesh.colorB,
|
|
133
188
|
sizeX: Mesh.sizeX,
|
|
134
189
|
sizeY: Mesh.sizeY,
|
|
135
190
|
sizeZ: Mesh.sizeZ,
|
|
@@ -197,7 +252,8 @@ export function updateBatches(
|
|
|
197
252
|
if (!batch) {
|
|
198
253
|
let buffers = state.buffers.get(shape);
|
|
199
254
|
if (!buffers) {
|
|
200
|
-
|
|
255
|
+
const data = getMesh(shape) ?? getMesh(MeshShape.Box)!;
|
|
256
|
+
buffers = createMeshBuffers(device, data);
|
|
201
257
|
state.buffers.set(shape, buffers);
|
|
202
258
|
}
|
|
203
259
|
batch = {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { resource, type State } from "../../core";
|
|
2
|
+
import { DEPTH_FORMAT } from "./scene";
|
|
3
|
+
|
|
4
|
+
export interface OpaqueDrawContext {
|
|
5
|
+
readonly device: GPUDevice;
|
|
6
|
+
readonly format: GPUTextureFormat;
|
|
7
|
+
readonly depthFormat: GPUTextureFormat;
|
|
8
|
+
readonly scene: GPUBuffer;
|
|
9
|
+
readonly matrices: GPUBuffer;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface OpaqueDrawCallback {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly order: number;
|
|
15
|
+
draw(pass: GPURenderPassEncoder, ctx: OpaqueDrawContext): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface OpaquePassState {
|
|
19
|
+
callbacks: Map<string, OpaqueDrawCallback>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const OpaquePass = resource<OpaquePassState>("opaque-pass");
|
|
23
|
+
|
|
24
|
+
export function registerOpaqueCallback(state: State, callback: OpaqueDrawCallback): void {
|
|
25
|
+
const pass = OpaquePass.from(state);
|
|
26
|
+
if (pass) {
|
|
27
|
+
pass.callbacks.set(callback.id, callback);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function unregisterOpaqueCallback(state: State, id: string): void {
|
|
32
|
+
const pass = OpaquePass.from(state);
|
|
33
|
+
if (pass) {
|
|
34
|
+
pass.callbacks.delete(id);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getOpaqueCallbacks(state: State): OpaqueDrawCallback[] {
|
|
39
|
+
const pass = OpaquePass.from(state);
|
|
40
|
+
if (!pass) return [];
|
|
41
|
+
return Array.from(pass.callbacks.values());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { DEPTH_FORMAT };
|
|
@@ -20,6 +20,7 @@ struct Uniforms {
|
|
|
20
20
|
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
21
21
|
@group(0) @binding(1) var inputSampler: sampler;
|
|
22
22
|
@group(0) @binding(2) var<uniform> uniforms: Uniforms;
|
|
23
|
+
@group(0) @binding(3) var maskTexture: texture_2d<f32>;
|
|
23
24
|
|
|
24
25
|
const FLAG_TONEMAP: u32 = 1u;
|
|
25
26
|
const FLAG_FXAA: u32 = 2u;
|
|
@@ -112,9 +113,11 @@ fn applyVignette(color: vec3f, uv: vec2f) -> vec3f {
|
|
|
112
113
|
@fragment
|
|
113
114
|
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
|
|
114
115
|
var color = textureSample(inputTexture, inputSampler, input.uv).rgb;
|
|
116
|
+
let maskValue = textureSample(maskTexture, inputSampler, input.uv).r;
|
|
115
117
|
|
|
116
118
|
if (uniforms.flags & FLAG_FXAA) != 0u {
|
|
117
|
-
|
|
119
|
+
let fxaaColor = applyFXAA(input.uv, color);
|
|
120
|
+
color = select(fxaaColor, color, maskValue >= 0.5);
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
if (uniforms.flags & FLAG_TONEMAP) != 0u {
|
|
@@ -150,7 +153,10 @@ export function createPostProcessNode(uniforms: PostProcessUniforms): ComputeNod
|
|
|
150
153
|
return {
|
|
151
154
|
id: "postprocess",
|
|
152
155
|
phase: "postprocess",
|
|
153
|
-
inputs: [
|
|
156
|
+
inputs: [
|
|
157
|
+
{ id: "scene", access: "read" },
|
|
158
|
+
{ id: "mask", access: "read" },
|
|
159
|
+
],
|
|
154
160
|
outputs: [{ id: "framebuffer", access: "write" }],
|
|
155
161
|
|
|
156
162
|
execute(ctx: ExecutionContext) {
|
|
@@ -158,6 +164,7 @@ export function createPostProcessNode(uniforms: PostProcessUniforms): ComputeNod
|
|
|
158
164
|
const width = context.canvas.width;
|
|
159
165
|
const height = context.canvas.height;
|
|
160
166
|
const sceneView = ctx.getTextureView("scene")!;
|
|
167
|
+
const maskView = ctx.getTextureView("mask")!;
|
|
161
168
|
|
|
162
169
|
if (!pipeline) {
|
|
163
170
|
const module = device.createShaderModule({ code: shader });
|
|
@@ -211,6 +218,7 @@ export function createPostProcessNode(uniforms: PostProcessUniforms): ComputeNod
|
|
|
211
218
|
{ binding: 0, resource: sceneView },
|
|
212
219
|
{ binding: 1, resource: sampler },
|
|
213
220
|
{ binding: 2, resource: { buffer: uniformBuffer } },
|
|
221
|
+
{ binding: 3, resource: maskView },
|
|
214
222
|
],
|
|
215
223
|
});
|
|
216
224
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export const SCENE_UNIFORM_SIZE =
|
|
1
|
+
export const SCENE_UNIFORM_SIZE = 192; // viewProj (64) + cameraWorld (64) + ambient (16) + sunDir (16) + sunColor (16) + camera params (16)
|
|
2
2
|
export const DEPTH_FORMAT: GPUTextureFormat = "depth24plus";
|
|
3
|
+
export const MASK_FORMAT: GPUTextureFormat = "r8unorm";
|
|
3
4
|
|
|
4
5
|
export function createSceneBuffer(device: GPUDevice): GPUBuffer {
|
|
5
6
|
return device.createBuffer({
|
|
@@ -22,6 +23,7 @@ export function ensureRenderTextures(
|
|
|
22
23
|
|
|
23
24
|
existing?.destroy();
|
|
24
25
|
textures.get("depth")?.destroy();
|
|
26
|
+
textures.get("mask")?.destroy();
|
|
25
27
|
|
|
26
28
|
const scene = device.createTexture({
|
|
27
29
|
label: "scene",
|
|
@@ -37,10 +39,19 @@ export function ensureRenderTextures(
|
|
|
37
39
|
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
38
40
|
});
|
|
39
41
|
|
|
42
|
+
const mask = device.createTexture({
|
|
43
|
+
label: "mask",
|
|
44
|
+
size: { width, height },
|
|
45
|
+
format: MASK_FORMAT,
|
|
46
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
47
|
+
});
|
|
48
|
+
|
|
40
49
|
textures.set("scene", scene);
|
|
41
50
|
textureViews.set("scene", scene.createView());
|
|
42
51
|
textures.set("depth", depth);
|
|
43
52
|
textureViews.set("depth", depth.createView());
|
|
53
|
+
textures.set("mask", mask);
|
|
54
|
+
textureViews.set("mask", mask.createView());
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
export function perspective(fov: number, aspect: number, near: number, far: number): Float32Array {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { resource, type State } from "../../core";
|
|
2
|
+
import type { ComputeNode, ExecutionContext } from "../compute";
|
|
3
|
+
import { MASK_FORMAT } from "./scene";
|
|
4
|
+
|
|
5
|
+
export interface DrawContext {
|
|
6
|
+
readonly device: GPUDevice;
|
|
7
|
+
readonly format: GPUTextureFormat;
|
|
8
|
+
readonly maskFormat: GPUTextureFormat;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DrawContributor {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly order: number;
|
|
14
|
+
draw(pass: GPURenderPassEncoder, ctx: DrawContext): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface TransparentPassState {
|
|
18
|
+
contributors: Map<string, DrawContributor>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const TransparentPass = resource<TransparentPassState>("transparent-pass");
|
|
22
|
+
|
|
23
|
+
export function registerDrawContributor(state: State, contributor: DrawContributor): void {
|
|
24
|
+
const pass = TransparentPass.from(state);
|
|
25
|
+
if (pass) {
|
|
26
|
+
pass.contributors.set(contributor.id, contributor);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function unregisterDrawContributor(state: State, id: string): void {
|
|
31
|
+
const pass = TransparentPass.from(state);
|
|
32
|
+
if (pass) {
|
|
33
|
+
pass.contributors.delete(id);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TransparentNodeConfig {
|
|
38
|
+
getContributors: () => DrawContributor[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createTransparentNode(config: TransparentNodeConfig): ComputeNode {
|
|
42
|
+
return {
|
|
43
|
+
id: "transparent",
|
|
44
|
+
phase: "transparent",
|
|
45
|
+
inputs: [{ id: "depth", access: "read" }],
|
|
46
|
+
outputs: [
|
|
47
|
+
{ id: "scene", access: "write" },
|
|
48
|
+
{ id: "mask", access: "write" },
|
|
49
|
+
],
|
|
50
|
+
|
|
51
|
+
execute(ctx: ExecutionContext) {
|
|
52
|
+
const contributors = config.getContributors();
|
|
53
|
+
if (contributors.length === 0) return;
|
|
54
|
+
|
|
55
|
+
const targetView = ctx.getTextureView("scene") ?? ctx.canvasView;
|
|
56
|
+
const depthView = ctx.getTextureView("depth")!;
|
|
57
|
+
const maskView = ctx.getTextureView("mask")!;
|
|
58
|
+
|
|
59
|
+
const pass = ctx.encoder.beginRenderPass({
|
|
60
|
+
colorAttachments: [
|
|
61
|
+
{
|
|
62
|
+
view: targetView,
|
|
63
|
+
loadOp: "load" as const,
|
|
64
|
+
storeOp: "store" as const,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
view: maskView,
|
|
68
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
69
|
+
loadOp: "clear" as const,
|
|
70
|
+
storeOp: "store" as const,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
depthStencilAttachment: {
|
|
74
|
+
view: depthView,
|
|
75
|
+
depthLoadOp: "load" as const,
|
|
76
|
+
depthStoreOp: "store" as const,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const drawCtx: DrawContext = {
|
|
81
|
+
device: ctx.device,
|
|
82
|
+
format: ctx.format,
|
|
83
|
+
maskFormat: MASK_FORMAT,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const sorted = [...contributors].sort((a, b) => a.order - b.order);
|
|
87
|
+
for (const contributor of sorted) {
|
|
88
|
+
contributor.draw(pass, drawCtx);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
pass.end();
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|