@multiplekex/shallot 0.2.5 → 0.3.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.
Files changed (41) hide show
  1. package/package.json +1 -1
  2. package/src/core/component.ts +1 -1
  3. package/src/core/index.ts +1 -13
  4. package/src/core/math.ts +186 -0
  5. package/src/core/state.ts +1 -1
  6. package/src/core/xml.ts +56 -41
  7. package/src/extras/orbit/index.ts +1 -1
  8. package/src/extras/text/index.ts +10 -65
  9. package/src/extras/{water.ts → water/index.ts} +59 -4
  10. package/src/standard/raster/batch.ts +149 -0
  11. package/src/standard/raster/forward.ts +832 -0
  12. package/src/standard/raster/index.ts +146 -472
  13. package/src/standard/raster/shadow.ts +408 -0
  14. package/src/standard/raytracing/bvh/blas.ts +335 -87
  15. package/src/standard/raytracing/bvh/radix.ts +225 -228
  16. package/src/standard/raytracing/bvh/refit.ts +711 -0
  17. package/src/standard/raytracing/bvh/structs.ts +0 -55
  18. package/src/standard/raytracing/bvh/tlas.ts +153 -141
  19. package/src/standard/raytracing/bvh/traverse.ts +72 -64
  20. package/src/standard/raytracing/index.ts +233 -204
  21. package/src/standard/raytracing/instance.ts +30 -18
  22. package/src/standard/raytracing/ray.ts +1 -1
  23. package/src/standard/raytracing/shaders.ts +23 -40
  24. package/src/standard/render/camera.ts +10 -28
  25. package/src/standard/render/data.ts +1 -1
  26. package/src/standard/render/index.ts +68 -12
  27. package/src/standard/render/light.ts +2 -2
  28. package/src/standard/render/mesh.ts +404 -0
  29. package/src/standard/render/overlay.ts +5 -2
  30. package/src/standard/render/postprocess.ts +263 -267
  31. package/src/standard/render/surface/index.ts +81 -12
  32. package/src/standard/render/surface/shaders.ts +265 -11
  33. package/src/standard/render/surface/structs.ts +10 -0
  34. package/src/standard/tween/tween.ts +44 -115
  35. package/src/standard/render/mesh/box.ts +0 -20
  36. package/src/standard/render/mesh/index.ts +0 -315
  37. package/src/standard/render/mesh/plane.ts +0 -11
  38. package/src/standard/render/mesh/sphere.ts +0 -40
  39. package/src/standard/render/mesh/unified.ts +0 -96
  40. package/src/standard/render/surface/compile.ts +0 -65
  41. package/src/standard/render/surface/noise.ts +0 -58
@@ -241,22 +241,267 @@ export interface PostProcessConfig {
241
241
  getRenderSize?: () => { width: number; height: number };
242
242
  }
243
243
 
244
+ interface PostProcessGPU {
245
+ pipeline: GPURenderPipeline;
246
+ blitPipeline: GPURenderPipeline;
247
+ uniformBuffer: GPUBuffer;
248
+ uniformData: ArrayBuffer;
249
+ uniformFloats: Float32Array;
250
+ uniformUints: Uint32Array;
251
+ linearSampler: GPUSampler;
252
+ nearestSampler: GPUSampler;
253
+ }
254
+
255
+ interface PostProcessCache {
256
+ mainBindGroup: GPUBindGroup | null;
257
+ blitBindGroup: GPUBindGroup | null;
258
+ inputView: GPUTextureView | null;
259
+ sampler: GPUSampler | null;
260
+ maskView: GPUTextureView | null;
261
+ }
262
+
263
+ async function preparePostProcess(device: GPUDevice): Promise<PostProcessGPU> {
264
+ const format: GPUTextureFormat = "bgra8unorm";
265
+
266
+ const [mainModule, blitModule] = await Promise.all([
267
+ device.createShaderModule({ code: shader }),
268
+ device.createShaderModule({ code: blitShader }),
269
+ ]);
270
+
271
+ const [pipeline, blitPipeline] = await Promise.all([
272
+ device.createRenderPipelineAsync({
273
+ layout: "auto",
274
+ vertex: { module: mainModule, entryPoint: "vertexMain" },
275
+ fragment: {
276
+ module: mainModule,
277
+ entryPoint: "fragmentMain",
278
+ targets: [{ format }],
279
+ },
280
+ primitive: { topology: "triangle-list" },
281
+ }),
282
+ device.createRenderPipelineAsync({
283
+ layout: "auto",
284
+ vertex: { module: blitModule, entryPoint: "vertexMain" },
285
+ fragment: {
286
+ module: blitModule,
287
+ entryPoint: "fragmentMain",
288
+ targets: [{ format }],
289
+ },
290
+ primitive: { topology: "triangle-list" },
291
+ }),
292
+ ]);
293
+
294
+ const uniformData = new ArrayBuffer(48);
295
+
296
+ return {
297
+ pipeline,
298
+ blitPipeline,
299
+ uniformBuffer: device.createBuffer({
300
+ size: 48,
301
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
302
+ }),
303
+ uniformData,
304
+ uniformFloats: new Float32Array(uniformData),
305
+ uniformUints: new Uint32Array(uniformData),
306
+ linearSampler: device.createSampler({ magFilter: "linear", minFilter: "linear" }),
307
+ nearestSampler: device.createSampler({ magFilter: "nearest", minFilter: "nearest" }),
308
+ };
309
+ }
310
+
311
+ function blitToCanvas(
312
+ encoder: GPUCommandEncoder,
313
+ pipeline: GPURenderPipeline,
314
+ bindGroup: GPUBindGroup,
315
+ canvasView: GPUTextureView
316
+ ): void {
317
+ const pass = encoder.beginRenderPass({
318
+ colorAttachments: [
319
+ {
320
+ view: canvasView,
321
+ loadOp: "clear" as const,
322
+ storeOp: "store" as const,
323
+ clearValue: { r: 0, g: 0, b: 0, a: 1 },
324
+ },
325
+ ],
326
+ });
327
+ pass.setPipeline(pipeline);
328
+ pass.setBindGroup(0, bindGroup);
329
+ pass.draw(3);
330
+ pass.end();
331
+ }
332
+
333
+ function executeContributors(
334
+ contributors: { execute: (ctx: DrawContext) => void }[],
335
+ baseCtx: Omit<DrawContext, "inputView" | "outputView">,
336
+ pingAView: GPUTextureView,
337
+ pingBView: GPUTextureView,
338
+ currentInput: GPUTextureView,
339
+ pingPong: boolean
340
+ ): { currentInput: GPUTextureView; pingPong: boolean } {
341
+ for (const contributor of contributors) {
342
+ const currentOutput = pingPong ? pingAView : pingBView;
343
+ contributor.execute({
344
+ ...baseCtx,
345
+ inputView: currentInput,
346
+ outputView: currentOutput,
347
+ });
348
+ currentInput = currentOutput;
349
+ pingPong = !pingPong;
350
+ }
351
+ return { currentInput, pingPong };
352
+ }
353
+
354
+ function ensureBlitBindGroup(
355
+ device: GPUDevice,
356
+ gpu: PostProcessGPU,
357
+ cache: PostProcessCache,
358
+ inputView: GPUTextureView,
359
+ sampler: GPUSampler
360
+ ): GPUBindGroup {
361
+ if (inputView !== cache.inputView || sampler !== cache.sampler) {
362
+ cache.blitBindGroup = device.createBindGroup({
363
+ layout: gpu.blitPipeline.getBindGroupLayout(0),
364
+ entries: [
365
+ { binding: 0, resource: inputView },
366
+ { binding: 1, resource: sampler },
367
+ ],
368
+ });
369
+ cache.inputView = inputView;
370
+ cache.sampler = sampler;
371
+ }
372
+ return cache.blitBindGroup!;
373
+ }
374
+
375
+ function executePostProcess(
376
+ gpu: PostProcessGPU,
377
+ cache: PostProcessCache,
378
+ ctx: ExecutionContext,
379
+ config: PostProcessConfig
380
+ ): void {
381
+ const { device, encoder, canvasView, format, context } = ctx;
382
+ const width = context.canvas.width;
383
+ const height = context.canvas.height;
384
+ const colorView = ctx.getTextureView("color")!;
385
+ const maskView = ctx.getTextureView("mask")!;
386
+ const zView = ctx.getTextureView("z")!;
387
+ const eidView = ctx.getTextureView("eid")!;
388
+ const pingAView = ctx.getTextureView("pingA")!;
389
+ const pingBView = ctx.getTextureView("pingB")!;
390
+
391
+ const renderSize = config.getRenderSize?.();
392
+ const isUpscaling = renderSize && (renderSize.width !== width || renderSize.height !== height);
393
+ const sampler = isUpscaling ? gpu.nearestSampler : gpu.linearSampler;
394
+
395
+ let currentInput = colorView;
396
+ let pingPong = false;
397
+
398
+ const baseCtx = {
399
+ device,
400
+ encoder,
401
+ format,
402
+ width,
403
+ height,
404
+ sceneView: colorView,
405
+ zView,
406
+ entityIdView: eidView,
407
+ maskView,
408
+ canvasView,
409
+ };
410
+
411
+ const beforePostProcess = getDrawsByPass(config.state, Pass.BeforePost);
412
+ ({ currentInput, pingPong } = executeContributors(
413
+ beforePostProcess,
414
+ baseCtx,
415
+ pingAView,
416
+ pingBView,
417
+ currentInput,
418
+ pingPong
419
+ ));
420
+
421
+ const postProcessContributors = getDrawsByPass(config.state, Pass.Post);
422
+ const hasBuiltinEffects =
423
+ config.uniforms.tonemap ||
424
+ config.uniforms.fxaa ||
425
+ config.uniforms.vignetteStrength > 0 ||
426
+ config.uniforms.bloomIntensity > 0 ||
427
+ config.uniforms.quantize > 0;
428
+
429
+ if (hasBuiltinEffects || postProcessContributors.length > 0) {
430
+ ({ currentInput, pingPong } = executeContributors(
431
+ postProcessContributors,
432
+ baseCtx,
433
+ pingAView,
434
+ pingBView,
435
+ currentInput,
436
+ pingPong
437
+ ));
438
+
439
+ if (hasBuiltinEffects) {
440
+ let flags = 0;
441
+ if (config.uniforms.tonemap) flags |= FLAG_TONEMAP;
442
+ if (config.uniforms.fxaa) flags |= FLAG_FXAA;
443
+ if (config.uniforms.vignetteStrength > 0) flags |= FLAG_VIGNETTE;
444
+ if (config.uniforms.bloomIntensity > 0) flags |= FLAG_BLOOM;
445
+ if (config.uniforms.quantize > 0) flags |= FLAG_QUANTIZE;
446
+
447
+ gpu.uniformFloats[0] = config.uniforms.exposure;
448
+ gpu.uniformFloats[1] = config.uniforms.vignetteStrength;
449
+ gpu.uniformFloats[2] = config.uniforms.vignetteInner;
450
+ gpu.uniformFloats[3] = config.uniforms.vignetteOuter;
451
+ gpu.uniformFloats[4] = 1.0 / width;
452
+ gpu.uniformFloats[5] = 1.0 / height;
453
+ gpu.uniformUints[6] = flags;
454
+ gpu.uniformFloats[7] = config.uniforms.bloomIntensity;
455
+ gpu.uniformFloats[8] = config.uniforms.bloomThreshold;
456
+ gpu.uniformFloats[9] = config.uniforms.bloomRadius;
457
+ gpu.uniformFloats[10] = config.uniforms.quantize;
458
+
459
+ device.queue.writeBuffer(gpu.uniformBuffer, 0, gpu.uniformData);
460
+
461
+ if (
462
+ currentInput !== cache.inputView ||
463
+ sampler !== cache.sampler ||
464
+ maskView !== cache.maskView
465
+ ) {
466
+ cache.mainBindGroup = device.createBindGroup({
467
+ layout: gpu.pipeline.getBindGroupLayout(0),
468
+ entries: [
469
+ { binding: 0, resource: currentInput },
470
+ { binding: 1, resource: sampler },
471
+ { binding: 2, resource: { buffer: gpu.uniformBuffer } },
472
+ { binding: 3, resource: maskView },
473
+ ],
474
+ });
475
+ cache.inputView = currentInput;
476
+ cache.sampler = sampler;
477
+ cache.maskView = maskView;
478
+ }
479
+
480
+ blitToCanvas(encoder, gpu.pipeline, cache.mainBindGroup!, canvasView);
481
+ } else {
482
+ const bindGroup = ensureBlitBindGroup(device, gpu, cache, currentInput, sampler);
483
+ blitToCanvas(encoder, gpu.blitPipeline, bindGroup, canvasView);
484
+ }
485
+ } else {
486
+ const bindGroup = ensureBlitBindGroup(device, gpu, cache, currentInput, sampler);
487
+ blitToCanvas(encoder, gpu.blitPipeline, bindGroup, canvasView);
488
+ }
489
+
490
+ const afterPostProcess = getDrawsByPass(config.state, Pass.AfterPost);
491
+ for (const contributor of afterPostProcess) {
492
+ contributor.execute(baseCtx);
493
+ }
494
+ }
495
+
244
496
  export function createPostProcessNode(config: PostProcessConfig): ComputeNode {
245
- let pipeline: GPURenderPipeline | null = null;
246
- let blitPipeline: GPURenderPipeline | null = null;
247
- let uniformBuffer: GPUBuffer | null = null;
248
- let linearSampler: GPUSampler | null = null;
249
- let nearestSampler: GPUSampler | null = null;
250
-
251
- let uniformData: ArrayBuffer;
252
- let uniformFloats: Float32Array;
253
- let uniformUints: Uint32Array;
254
-
255
- let mainBindGroup: GPUBindGroup | null = null;
256
- let blitBindGroup: GPUBindGroup | null = null;
257
- let cachedInputView: GPUTextureView | null = null;
258
- let cachedSampler: GPUSampler | null = null;
259
- let cachedMaskView: GPUTextureView | null = null;
497
+ let gpu: PostProcessGPU | null = null;
498
+ const cache: PostProcessCache = {
499
+ mainBindGroup: null,
500
+ blitBindGroup: null,
501
+ inputView: null,
502
+ sampler: null,
503
+ maskView: null,
504
+ };
260
505
 
261
506
  return {
262
507
  id: "postprocess",
@@ -269,261 +514,12 @@ export function createPostProcessNode(config: PostProcessConfig): ComputeNode {
269
514
  outputs: [{ id: "framebuffer", access: "write" }],
270
515
 
271
516
  async prepare(device: GPUDevice) {
272
- const format: GPUTextureFormat = "bgra8unorm";
273
-
274
- const [mainModule, blitModule] = await Promise.all([
275
- device.createShaderModule({ code: shader }),
276
- device.createShaderModule({ code: blitShader }),
277
- ]);
278
-
279
- [pipeline, blitPipeline] = await Promise.all([
280
- device.createRenderPipelineAsync({
281
- layout: "auto",
282
- vertex: { module: mainModule, entryPoint: "vertexMain" },
283
- fragment: {
284
- module: mainModule,
285
- entryPoint: "fragmentMain",
286
- targets: [{ format }],
287
- },
288
- primitive: { topology: "triangle-list" },
289
- }),
290
- device.createRenderPipelineAsync({
291
- layout: "auto",
292
- vertex: { module: blitModule, entryPoint: "vertexMain" },
293
- fragment: {
294
- module: blitModule,
295
- entryPoint: "fragmentMain",
296
- targets: [{ format }],
297
- },
298
- primitive: { topology: "triangle-list" },
299
- }),
300
- ]);
301
-
302
- uniformBuffer = device.createBuffer({
303
- size: 48,
304
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
305
- });
306
-
307
- linearSampler = device.createSampler({
308
- magFilter: "linear",
309
- minFilter: "linear",
310
- });
311
-
312
- nearestSampler = device.createSampler({
313
- magFilter: "nearest",
314
- minFilter: "nearest",
315
- });
316
-
317
- uniformData = new ArrayBuffer(48);
318
- uniformFloats = new Float32Array(uniformData);
319
- uniformUints = new Uint32Array(uniformData);
517
+ gpu = await preparePostProcess(device);
320
518
  },
321
519
 
322
520
  execute(ctx: ExecutionContext) {
323
- const { device, encoder, canvasView, format, context } = ctx;
324
- const width = context.canvas.width;
325
- const height = context.canvas.height;
326
- const colorView = ctx.getTextureView("color")!;
327
- const maskView = ctx.getTextureView("mask")!;
328
- const zView = ctx.getTextureView("z")!;
329
- const eidView = ctx.getTextureView("eid")!;
330
- const pingAView = ctx.getTextureView("pingA")!;
331
- const pingBView = ctx.getTextureView("pingB")!;
332
-
333
- const renderSize = config.getRenderSize?.();
334
- const isUpscaling =
335
- renderSize && (renderSize.width !== width || renderSize.height !== height);
336
- const sampler = isUpscaling ? nearestSampler! : linearSampler!;
337
-
338
- let currentInput = colorView;
339
- let currentOutput = pingAView;
340
- let pingPong = false;
341
-
342
- const beforePostProcess = getDrawsByPass(config.state, Pass.BeforePost);
343
- for (const contributor of beforePostProcess) {
344
- const passCtx: DrawContext = {
345
- device,
346
- encoder,
347
- format,
348
- width,
349
- height,
350
- sceneView: colorView,
351
- zView,
352
- entityIdView: eidView,
353
- maskView,
354
- canvasView,
355
- inputView: currentInput,
356
- outputView: currentOutput,
357
- };
358
- contributor.execute(passCtx);
359
-
360
- currentInput = currentOutput;
361
- currentOutput = pingPong ? pingAView : pingBView;
362
- pingPong = !pingPong;
363
- }
364
-
365
- const postProcessContributors = getDrawsByPass(config.state, Pass.Post);
366
- const hasBuiltinEffects =
367
- config.uniforms.tonemap ||
368
- config.uniforms.fxaa ||
369
- config.uniforms.vignetteStrength > 0 ||
370
- config.uniforms.bloomIntensity > 0 ||
371
- config.uniforms.quantize > 0;
372
-
373
- if (hasBuiltinEffects || postProcessContributors.length > 0) {
374
- for (const contributor of postProcessContributors) {
375
- const passCtx: DrawContext = {
376
- device,
377
- encoder,
378
- format,
379
- width,
380
- height,
381
- sceneView: colorView,
382
- zView,
383
- entityIdView: eidView,
384
- maskView,
385
- canvasView,
386
- inputView: currentInput,
387
- outputView: currentOutput,
388
- };
389
- contributor.execute(passCtx);
390
-
391
- currentInput = currentOutput;
392
- currentOutput = pingPong ? pingAView : pingBView;
393
- pingPong = !pingPong;
394
- }
395
-
396
- if (hasBuiltinEffects) {
397
- let flags = 0;
398
- if (config.uniforms.tonemap) flags |= FLAG_TONEMAP;
399
- if (config.uniforms.fxaa) flags |= FLAG_FXAA;
400
- if (config.uniforms.vignetteStrength > 0) flags |= FLAG_VIGNETTE;
401
- if (config.uniforms.bloomIntensity > 0) flags |= FLAG_BLOOM;
402
- if (config.uniforms.quantize > 0) flags |= FLAG_QUANTIZE;
403
-
404
- uniformFloats[0] = config.uniforms.exposure;
405
- uniformFloats[1] = config.uniforms.vignetteStrength;
406
- uniformFloats[2] = config.uniforms.vignetteInner;
407
- uniformFloats[3] = config.uniforms.vignetteOuter;
408
- uniformFloats[4] = 1.0 / width;
409
- uniformFloats[5] = 1.0 / height;
410
- uniformUints[6] = flags;
411
- uniformFloats[7] = config.uniforms.bloomIntensity;
412
- uniformFloats[8] = config.uniforms.bloomThreshold;
413
- uniformFloats[9] = config.uniforms.bloomRadius;
414
- uniformFloats[10] = config.uniforms.quantize;
415
-
416
- device.queue.writeBuffer(uniformBuffer!, 0, uniformData);
417
-
418
- if (
419
- currentInput !== cachedInputView ||
420
- sampler !== cachedSampler ||
421
- maskView !== cachedMaskView
422
- ) {
423
- mainBindGroup = device.createBindGroup({
424
- layout: pipeline!.getBindGroupLayout(0),
425
- entries: [
426
- { binding: 0, resource: currentInput },
427
- { binding: 1, resource: sampler },
428
- { binding: 2, resource: { buffer: uniformBuffer! } },
429
- { binding: 3, resource: maskView },
430
- ],
431
- });
432
- cachedInputView = currentInput;
433
- cachedSampler = sampler;
434
- cachedMaskView = maskView;
435
- }
436
-
437
- const pass = encoder.beginRenderPass({
438
- colorAttachments: [
439
- {
440
- view: canvasView,
441
- loadOp: "clear" as const,
442
- storeOp: "store" as const,
443
- clearValue: { r: 0, g: 0, b: 0, a: 1 },
444
- },
445
- ],
446
- });
447
-
448
- pass.setPipeline(pipeline!);
449
- pass.setBindGroup(0, mainBindGroup!);
450
- pass.draw(3);
451
- pass.end();
452
- } else {
453
- if (currentInput !== cachedInputView || sampler !== cachedSampler) {
454
- blitBindGroup = device.createBindGroup({
455
- layout: blitPipeline!.getBindGroupLayout(0),
456
- entries: [
457
- { binding: 0, resource: currentInput },
458
- { binding: 1, resource: sampler },
459
- ],
460
- });
461
- cachedInputView = currentInput;
462
- cachedSampler = sampler;
463
- }
464
-
465
- const pass = encoder.beginRenderPass({
466
- colorAttachments: [
467
- {
468
- view: canvasView,
469
- loadOp: "clear" as const,
470
- storeOp: "store" as const,
471
- clearValue: { r: 0, g: 0, b: 0, a: 1 },
472
- },
473
- ],
474
- });
475
-
476
- pass.setPipeline(blitPipeline!);
477
- pass.setBindGroup(0, blitBindGroup!);
478
- pass.draw(3);
479
- pass.end();
480
- }
481
- } else {
482
- if (currentInput !== cachedInputView || sampler !== cachedSampler) {
483
- blitBindGroup = device.createBindGroup({
484
- layout: blitPipeline!.getBindGroupLayout(0),
485
- entries: [
486
- { binding: 0, resource: currentInput },
487
- { binding: 1, resource: sampler },
488
- ],
489
- });
490
- cachedInputView = currentInput;
491
- cachedSampler = sampler;
492
- }
493
-
494
- const pass = encoder.beginRenderPass({
495
- colorAttachments: [
496
- {
497
- view: canvasView,
498
- loadOp: "clear" as const,
499
- storeOp: "store" as const,
500
- clearValue: { r: 0, g: 0, b: 0, a: 1 },
501
- },
502
- ],
503
- });
504
-
505
- pass.setPipeline(blitPipeline!);
506
- pass.setBindGroup(0, blitBindGroup!);
507
- pass.draw(3);
508
- pass.end();
509
- }
510
-
511
- const afterPostProcess = getDrawsByPass(config.state, Pass.AfterPost);
512
- for (const contributor of afterPostProcess) {
513
- const passCtx: DrawContext = {
514
- device,
515
- encoder,
516
- format,
517
- width,
518
- height,
519
- sceneView: colorView,
520
- zView,
521
- entityIdView: eidView,
522
- maskView,
523
- canvasView,
524
- };
525
- contributor.execute(passCtx);
526
- }
521
+ if (!gpu) return;
522
+ executePostProcess(gpu, cache, ctx, config);
527
523
  },
528
524
  };
529
525
  }
@@ -1,10 +1,11 @@
1
1
  import { MAX_ENTITIES } from "../../../core";
2
2
  import { setTraits } from "../../../core/component";
3
+ import { WGSL_STRUCTS } from "./structs";
4
+ import { compileVertexBody, WGSL_LIGHTING_CALC, SPECULAR_WGSL } from "./shaders";
3
5
 
4
6
  export interface SurfaceData {
5
7
  vertex?: string;
6
8
  fragment?: string;
7
- lit?: boolean;
8
9
  }
9
10
 
10
11
  interface ComposedSurface extends SurfaceData {
@@ -21,23 +22,28 @@ export function createSurfaceRegistry(): SurfaceRegistry {
21
22
  return registry;
22
23
  }
23
24
 
25
+ const surfaceNames = new Map<string, number>();
26
+
24
27
  function initBuiltIns(registry: SurfaceRegistry): void {
25
- registry.surfaces.push({ lit: true });
28
+ registry.surfaces.push({});
26
29
 
27
30
  registry.surfaces.push({
28
- lit: false,
29
31
  fragment: `(*surface).baseColor = (*surface).normal * 0.5 + 0.5;`,
30
32
  });
31
33
 
32
34
  registry.surfaces.push({
33
- lit: false,
34
35
  fragment: `
35
36
  let depth = position.z;
36
37
  let remapped = pow(1.0 - depth, 0.1);
37
38
  (*surface).baseColor = vec3(remapped);`,
38
39
  });
39
40
 
40
- registry.surfaces.push({ lit: false });
41
+ registry.surfaces.push({});
42
+
43
+ surfaceNames.set("default", 0);
44
+ surfaceNames.set("normals", 1);
45
+ surfaceNames.set("depth", 2);
46
+ surfaceNames.set("albedo", 3);
41
47
  }
42
48
 
43
49
  export const SurfaceType = {
@@ -73,14 +79,11 @@ export function composeSurfaces(registry: SurfaceRegistry, ...ids: number[]): nu
73
79
 
74
80
  const vertexParts: string[] = [];
75
81
  const fragmentParts: string[] = [];
76
- let lit = true;
77
82
 
78
83
  for (const id of validIds) {
79
84
  const s = registry.surfaces[id];
80
85
  if (!s) continue;
81
86
 
82
- if (s.lit === false) lit = false;
83
-
84
87
  if ("composed" in s) {
85
88
  for (const composedId of s.composed) {
86
89
  const inner = registry.surfaces[composedId];
@@ -96,7 +99,6 @@ export function composeSurfaces(registry: SurfaceRegistry, ...ids: number[]): nu
96
99
  const composedData: ComposedSurface = {
97
100
  vertex: vertexParts.length > 0 ? vertexParts.join("\n ") : undefined,
98
101
  fragment: fragmentParts.length > 0 ? fragmentParts.join("\n ") : undefined,
99
- lit,
100
102
  composed: validIds,
101
103
  };
102
104
 
@@ -107,8 +109,14 @@ export function composeSurfaces(registry: SurfaceRegistry, ...ids: number[]): nu
107
109
 
108
110
  const defaultRegistry = createSurfaceRegistry();
109
111
 
110
- export function surface(data: SurfaceData): number {
111
- return registerSurface(defaultRegistry, data);
112
+ export function surface(data: SurfaceData, name?: string): number {
113
+ const id = registerSurface(defaultRegistry, data);
114
+ if (name) surfaceNames.set(name, id);
115
+ return id;
116
+ }
117
+
118
+ export function getSurfaceByName(name: string): number | undefined {
119
+ return surfaceNames.get(name);
112
120
  }
113
121
 
114
122
  export function getDefaultSurface(id: number): SurfaceData | undefined {
@@ -120,6 +128,7 @@ export function getDefaultAllSurfaces(): SurfaceData[] {
120
128
  }
121
129
 
122
130
  export function clearDefaultSurfaces(): void {
131
+ surfaceNames.clear();
123
132
  clearSurfaces(defaultRegistry);
124
133
  }
125
134
 
@@ -141,6 +150,66 @@ setTraits(Surface, {
141
150
  defaults: () => ({
142
151
  type: SurfaceType.Default,
143
152
  }),
153
+ parse: { type: getSurfaceByName },
144
154
  });
145
155
 
146
- export { compileSurface } from "./compile";
156
+ export function compileSurface(data: SurfaceData): string {
157
+ const vertexTransform = compileVertexBody(data.vertex);
158
+
159
+ const fragmentBody = data.fragment ?? "";
160
+
161
+ return /* wgsl */ `
162
+ ${WGSL_STRUCTS}
163
+ ${SPECULAR_WGSL}
164
+
165
+ fn userVertexTransform(worldPos: vec3<f32>, normal: vec3<f32>, eid: u32) -> vec3<f32> {
166
+ ${vertexTransform}
167
+ }
168
+
169
+ fn userFragment(surface: ptr<function, SurfaceData>, position: vec4<f32>) {
170
+ ${fragmentBody}
171
+ }
172
+
173
+ @vertex
174
+ fn vs(input: VertexInput) -> VertexOutput {
175
+ let eid = entityIds[input.instance];
176
+ let world = matrices[eid];
177
+ let scaledPos = input.position * sizes[eid].xyz;
178
+ let baseWorldPos = (world * vec4<f32>(scaledPos, 1.0)).xyz;
179
+ let worldNormal = normalize((world * vec4<f32>(input.normal, 0.0)).xyz);
180
+ let finalWorldPos = userVertexTransform(baseWorldPos, worldNormal, eid);
181
+
182
+ var output: VertexOutput;
183
+ output.position = scene.viewProj * vec4<f32>(finalWorldPos, 1.0);
184
+ output.color = data[eid].baseColor;
185
+ output.worldNormal = worldNormal;
186
+ output.entityId = eid;
187
+ output.worldPos = finalWorldPos;
188
+ return output;
189
+ }
190
+
191
+ @fragment
192
+ fn fs(input: VertexOutput) -> FragmentOutput {
193
+ let eid = input.entityId;
194
+ let d = data[eid];
195
+
196
+ var surface: SurfaceData;
197
+ surface.baseColor = input.color.rgb;
198
+ surface.roughness = d.pbr.x;
199
+ surface.metallic = d.pbr.y;
200
+ surface.emission = d.emission.rgb * d.emission.a;
201
+ surface.normal = normalize(input.worldNormal);
202
+ surface.worldPos = input.worldPos;
203
+
204
+ userFragment(&surface, input.position);
205
+
206
+ let shadowFactor = 1.0;
207
+ ${WGSL_LIGHTING_CALC}
208
+
209
+ var output: FragmentOutput;
210
+ output.color = vec4<f32>(litColor, input.color.a);
211
+ output.entityId = input.entityId;
212
+ return output;
213
+ }
214
+ `;
215
+ }