@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.
@@ -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 lighting = ambient + diffuse;
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: [{ id: "scene", access: "write" }],
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 / height);
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 createGeometry(shape: number): MeshData {
23
- switch (shape) {
24
- case MeshShape.Sphere:
25
- return createSphere();
26
- case MeshShape.Plane:
27
- return createPlane();
28
- default:
29
- return createBox();
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
- buffers = createMeshBuffers(device, createGeometry(shape));
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
- color = applyFXAA(input.uv, color);
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: [{ id: "scene", access: "read" }],
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 = 176; // viewProj (64) + cameraWorld (64) + ambient (16) + sunDir (16) + sunColor (16)
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
+ }