@multiplekex/shallot 0.1.9 → 0.1.11

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.
@@ -6,6 +6,10 @@ import { createBox } from "./box";
6
6
  import { createSphere } from "./sphere";
7
7
  import { createPlane } from "./plane";
8
8
 
9
+ export const MAX_SURFACES = 16;
10
+ export const MAX_BATCH_SLOTS = 64;
11
+ export const INVALID_SHAPE = 0xffffffff;
12
+
9
13
  export interface MeshData {
10
14
  vertices: Float32Array<ArrayBuffer>;
11
15
  indices: Uint16Array<ArrayBuffer>;
@@ -46,6 +50,10 @@ export function clearMeshes(): void {
46
50
  initBuiltIns();
47
51
  }
48
52
 
53
+ export const MeshShapes = {
54
+ data: new Uint32Array(MAX_ENTITIES).fill(INVALID_SHAPE),
55
+ };
56
+
49
57
  export const MeshColors = {
50
58
  data: new Float32Array(MAX_ENTITIES * 4),
51
59
  };
@@ -54,6 +62,21 @@ export const MeshSizes = {
54
62
  data: new Float32Array(MAX_ENTITIES * 4),
55
63
  };
56
64
 
65
+ export const MeshPBR = {
66
+ data: new Float32Array(MAX_ENTITIES * 4),
67
+ };
68
+
69
+ export const MeshEmission = {
70
+ data: new Float32Array(MAX_ENTITIES * 4),
71
+ };
72
+
73
+ function initPBRDefaults(): void {
74
+ for (let i = 0; i < MAX_ENTITIES; i++) {
75
+ MeshPBR.data[i * 4] = 0.9;
76
+ MeshPBR.data[i * 4 + 1] = 0.0;
77
+ }
78
+ }
79
+
57
80
  interface ColorProxy extends Array<number>, FieldAccessor {}
58
81
 
59
82
  function colorProxy(): ColorProxy {
@@ -152,8 +175,104 @@ function sizeProxy(component: number): SizeProxy {
152
175
  });
153
176
  }
154
177
 
178
+ interface PBRProxy extends Array<number>, FieldAccessor {}
179
+
180
+ function pbrProxy(component: number, defaultValue: number): PBRProxy {
181
+ const data = MeshPBR.data;
182
+
183
+ function getValue(eid: number): number {
184
+ const val = data[eid * 4 + component];
185
+ return val === 0 && component === 0 ? defaultValue : val;
186
+ }
187
+
188
+ function setValue(eid: number, value: number): void {
189
+ data[eid * 4 + component] = value;
190
+ }
191
+
192
+ return new Proxy([] as unknown as PBRProxy, {
193
+ get(_, prop) {
194
+ if (prop === "get") return getValue;
195
+ if (prop === "set") return setValue;
196
+ const eid = Number(prop);
197
+ if (Number.isNaN(eid)) return undefined;
198
+ return getValue(eid);
199
+ },
200
+ set(_, prop, value) {
201
+ const eid = Number(prop);
202
+ if (Number.isNaN(eid)) return false;
203
+ setValue(eid, value);
204
+ return true;
205
+ },
206
+ });
207
+ }
208
+
209
+ interface EmissionProxy extends Array<number>, FieldAccessor {}
210
+
211
+ function emissionProxy(): EmissionProxy {
212
+ const data = MeshEmission.data;
213
+
214
+ function getValue(eid: number): number {
215
+ const offset = eid * 4;
216
+ const r = Math.round(data[offset] * 255);
217
+ const g = Math.round(data[offset + 1] * 255);
218
+ const b = Math.round(data[offset + 2] * 255);
219
+ return (r << 16) | (g << 8) | b;
220
+ }
221
+
222
+ function setValue(eid: number, value: number): void {
223
+ const offset = eid * 4;
224
+ data[offset] = ((value >> 16) & 0xff) / 255;
225
+ data[offset + 1] = ((value >> 8) & 0xff) / 255;
226
+ data[offset + 2] = (value & 0xff) / 255;
227
+ }
228
+
229
+ return new Proxy([] as unknown as EmissionProxy, {
230
+ get(_, prop) {
231
+ if (prop === "get") return getValue;
232
+ if (prop === "set") return setValue;
233
+ const eid = Number(prop);
234
+ if (Number.isNaN(eid)) return undefined;
235
+ return getValue(eid);
236
+ },
237
+ set(_, prop, value) {
238
+ const eid = Number(prop);
239
+ if (Number.isNaN(eid)) return false;
240
+ setValue(eid, value);
241
+ return true;
242
+ },
243
+ });
244
+ }
245
+
246
+ function emissionIntensityProxy(): PBRProxy {
247
+ const data = MeshEmission.data;
248
+
249
+ function getValue(eid: number): number {
250
+ return data[eid * 4 + 3];
251
+ }
252
+
253
+ function setValue(eid: number, value: number): void {
254
+ data[eid * 4 + 3] = value;
255
+ }
256
+
257
+ return new Proxy([] as unknown as PBRProxy, {
258
+ get(_, prop) {
259
+ if (prop === "get") return getValue;
260
+ if (prop === "set") return setValue;
261
+ const eid = Number(prop);
262
+ if (Number.isNaN(eid)) return undefined;
263
+ return getValue(eid);
264
+ },
265
+ set(_, prop, value) {
266
+ const eid = Number(prop);
267
+ if (Number.isNaN(eid)) return false;
268
+ setValue(eid, value);
269
+ return true;
270
+ },
271
+ });
272
+ }
273
+
155
274
  export const Mesh: {
156
- shape: number[];
275
+ shape: Uint32Array;
157
276
  color: ColorProxy;
158
277
  colorR: ColorChannelProxy;
159
278
  colorG: ColorChannelProxy;
@@ -161,8 +280,12 @@ export const Mesh: {
161
280
  sizeX: SizeProxy;
162
281
  sizeY: SizeProxy;
163
282
  sizeZ: SizeProxy;
283
+ roughness: PBRProxy;
284
+ metallic: PBRProxy;
285
+ emission: EmissionProxy;
286
+ emissionIntensity: PBRProxy;
164
287
  } = {
165
- shape: [],
288
+ shape: MeshShapes.data,
166
289
  color: colorProxy(),
167
290
  colorR: colorChannelProxy(0),
168
291
  colorG: colorChannelProxy(1),
@@ -170,6 +293,10 @@ export const Mesh: {
170
293
  sizeX: sizeProxy(0),
171
294
  sizeY: sizeProxy(1),
172
295
  sizeZ: sizeProxy(2),
296
+ roughness: pbrProxy(0, 0.9),
297
+ metallic: pbrProxy(1, 0.0),
298
+ emission: emissionProxy(),
299
+ emissionIntensity: emissionIntensityProxy(),
173
300
  };
174
301
 
175
302
  setTraits(Mesh, {
@@ -179,6 +306,10 @@ setTraits(Mesh, {
179
306
  sizeX: 1,
180
307
  sizeY: 1,
181
308
  sizeZ: 1,
309
+ roughness: 0.9,
310
+ metallic: 0.0,
311
+ emission: 0x000000,
312
+ emissionIntensity: 0.0,
182
313
  }),
183
314
  accessors: {
184
315
  color: Mesh.color,
@@ -188,6 +319,10 @@ setTraits(Mesh, {
188
319
  sizeX: Mesh.sizeX,
189
320
  sizeY: Mesh.sizeY,
190
321
  sizeZ: Mesh.sizeZ,
322
+ roughness: Mesh.roughness,
323
+ metallic: Mesh.metallic,
324
+ emission: Mesh.emission,
325
+ emissionIntensity: Mesh.emissionIntensity,
191
326
  },
192
327
  });
193
328
 
@@ -215,54 +350,84 @@ export function createMeshBuffers(device: GPUDevice, mesh: MeshData): MeshBuffer
215
350
  return { vertex, index, indexCount: mesh.indexCount };
216
351
  }
217
352
 
218
- export function collectByShape(entities: Iterable<number>): Map<number, number[]> {
219
- const byShape = new Map<number, number[]>();
353
+ export interface BatchKey {
354
+ shape: number;
355
+ surface: number;
356
+ }
357
+
358
+ export function batchKeyString(key: BatchKey): string {
359
+ return `${key.shape}:${key.surface}`;
360
+ }
361
+
362
+ export function collectByShapeAndSurface(
363
+ entities: Iterable<number>,
364
+ getSurface: (eid: number) => number
365
+ ): Map<string, { key: BatchKey; entities: number[] }> {
366
+ const batches = new Map<string, { key: BatchKey; entities: number[] }>();
220
367
  for (const eid of entities) {
221
368
  const shape = Mesh.shape[eid];
222
- let list = byShape.get(shape);
223
- if (!list) {
224
- list = [];
225
- byShape.set(shape, list);
369
+ const surface = getSurface(eid);
370
+ const key: BatchKey = { shape, surface };
371
+ const keyStr = batchKeyString(key);
372
+ let entry = batches.get(keyStr);
373
+ if (!entry) {
374
+ entry = { key, entities: [] };
375
+ batches.set(keyStr, entry);
226
376
  }
227
- list.push(eid);
377
+ entry.entities.push(eid);
228
378
  }
229
- return byShape;
379
+ return batches;
230
380
  }
231
381
 
232
382
  export interface ShapeBatch {
233
383
  index: number;
384
+ shape: number;
385
+ surface: number;
234
386
  buffers: MeshBuffers;
235
387
  entityIds: GPUBuffer;
236
388
  count: number;
237
389
  }
238
390
 
239
391
  export interface BatchState {
240
- batches: Map<number, ShapeBatch>;
392
+ batches: Map<string, ShapeBatch>;
241
393
  buffers: Map<number, MeshBuffers>;
242
394
  }
243
395
 
244
396
  export function updateBatches(
245
397
  device: GPUDevice,
246
- byShape: Map<number, number[]>,
398
+ byShapeAndSurface: Map<string, { key: BatchKey; entities: number[] }>,
247
399
  state: BatchState,
248
400
  indirect: GPUBuffer
249
401
  ): void {
250
- for (const [shape, entities] of byShape) {
251
- let batch = state.batches.get(shape);
402
+ const activeBatches = new Set<string>();
403
+
404
+ for (const [keyStr, { key, entities }] of byShapeAndSurface) {
405
+ activeBatches.add(keyStr);
406
+
407
+ let batch = state.batches.get(keyStr);
252
408
  if (!batch) {
253
- let buffers = state.buffers.get(shape);
409
+ const batchIndex = key.shape * MAX_SURFACES + key.surface;
410
+ if (batchIndex >= MAX_BATCH_SLOTS) {
411
+ console.warn(
412
+ `Batch index ${batchIndex} exceeds limit (${MAX_BATCH_SLOTS}), skipping batch`
413
+ );
414
+ continue;
415
+ }
416
+ let buffers = state.buffers.get(key.shape);
254
417
  if (!buffers) {
255
- const data = getMesh(shape) ?? getMesh(MeshShape.Box)!;
418
+ const data = getMesh(key.shape) ?? getMesh(MeshShape.Box)!;
256
419
  buffers = createMeshBuffers(device, data);
257
- state.buffers.set(shape, buffers);
420
+ state.buffers.set(key.shape, buffers);
258
421
  }
259
422
  batch = {
260
- index: state.batches.size,
423
+ index: batchIndex,
424
+ shape: key.shape,
425
+ surface: key.surface,
261
426
  buffers,
262
427
  entityIds: createEntityIdBuffer(device, MAX_ENTITIES),
263
428
  count: 0,
264
429
  };
265
- state.batches.set(shape, batch);
430
+ state.batches.set(keyStr, batch);
266
431
  }
267
432
 
268
433
  device.queue.writeBuffer(batch.entityIds, 0, new Uint32Array(entities));
@@ -276,6 +441,12 @@ export function updateBatches(
276
441
  firstInstance: 0,
277
442
  });
278
443
  }
444
+
445
+ for (const keyStr of state.batches.keys()) {
446
+ if (!activeBatches.has(keyStr)) {
447
+ state.batches.delete(keyStr);
448
+ }
449
+ }
279
450
  }
280
451
 
281
452
  export { createBox } from "./box";
@@ -0,0 +1,63 @@
1
+ import { resource, type State } from "../../core";
2
+ import { Pass } from "../compute/pass";
3
+
4
+ export { Pass };
5
+
6
+ export interface DrawContext {
7
+ readonly device: GPUDevice;
8
+ readonly encoder: GPUCommandEncoder;
9
+ readonly format: GPUTextureFormat;
10
+ readonly width: number;
11
+ readonly height: number;
12
+
13
+ readonly sceneView: GPUTextureView;
14
+ readonly depthView: GPUTextureView;
15
+ readonly entityIdView: GPUTextureView;
16
+ readonly maskView: GPUTextureView;
17
+ readonly canvasView: GPUTextureView;
18
+
19
+ readonly inputView?: GPUTextureView;
20
+ readonly outputView?: GPUTextureView;
21
+ }
22
+
23
+ export interface SharedPassContext {
24
+ readonly device: GPUDevice;
25
+ readonly format: GPUTextureFormat;
26
+ readonly maskFormat: GPUTextureFormat;
27
+ }
28
+
29
+ export interface Draw {
30
+ readonly id: string;
31
+ readonly pass: Pass;
32
+ readonly order: number;
33
+ execute(ctx: DrawContext): void;
34
+ draw?(pass: GPURenderPassEncoder, ctx: SharedPassContext): void;
35
+ }
36
+
37
+ export interface DrawState {
38
+ draws: Map<string, Draw>;
39
+ }
40
+
41
+ export const Draws = resource<DrawState>("draws");
42
+
43
+ export function registerDraw(state: State, draw: Draw): void {
44
+ const draws = Draws.from(state);
45
+ if (draws) {
46
+ draws.draws.set(draw.id, draw);
47
+ }
48
+ }
49
+
50
+ export function unregisterDraw(state: State, id: string): void {
51
+ const draws = Draws.from(state);
52
+ if (draws) {
53
+ draws.draws.delete(id);
54
+ }
55
+ }
56
+
57
+ export function getDrawsByPass(state: State, pass: Pass): Draw[] {
58
+ const draws = Draws.from(state);
59
+ if (!draws) return [];
60
+ return Array.from(draws.draws.values())
61
+ .filter((d) => d.pass === pass)
62
+ .sort((a, b) => a.order - b.order);
63
+ }