@multiplekex/shallot 0.2.4 → 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 (62) 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/arrows/index.ts +3 -3
  8. package/src/extras/caustic.ts +37 -0
  9. package/src/extras/gradient/index.ts +63 -69
  10. package/src/extras/index.ts +3 -0
  11. package/src/extras/lines/index.ts +3 -3
  12. package/src/extras/orbit/index.ts +1 -1
  13. package/src/extras/skylab/index.ts +314 -0
  14. package/src/extras/text/font.ts +69 -14
  15. package/src/extras/text/index.ts +17 -69
  16. package/src/extras/text/sdf.ts +13 -2
  17. package/src/extras/water/index.ts +119 -0
  18. package/src/standard/defaults.ts +2 -0
  19. package/src/standard/index.ts +2 -0
  20. package/src/standard/raster/batch.ts +149 -0
  21. package/src/standard/raster/forward.ts +832 -0
  22. package/src/standard/raster/index.ts +191 -0
  23. package/src/standard/raster/shadow.ts +408 -0
  24. package/src/standard/{render → raytracing}/bvh/blas.ts +336 -88
  25. package/src/standard/raytracing/bvh/radix.ts +473 -0
  26. package/src/standard/raytracing/bvh/refit.ts +711 -0
  27. package/src/standard/{render → raytracing}/bvh/structs.ts +0 -55
  28. package/src/standard/{render → raytracing}/bvh/tlas.ts +155 -140
  29. package/src/standard/{render → raytracing}/bvh/traverse.ts +72 -64
  30. package/src/standard/{render → raytracing}/depth.ts +9 -9
  31. package/src/standard/raytracing/index.ts +409 -0
  32. package/src/standard/{render → raytracing}/instance.ts +31 -16
  33. package/src/standard/{render → raytracing}/ray.ts +1 -1
  34. package/src/standard/raytracing/shaders.ts +798 -0
  35. package/src/standard/{render → raytracing}/triangle.ts +1 -1
  36. package/src/standard/render/camera.ts +96 -106
  37. package/src/standard/render/data.ts +1 -1
  38. package/src/standard/render/index.ts +136 -220
  39. package/src/standard/render/indirect.ts +9 -10
  40. package/src/standard/render/light.ts +2 -2
  41. package/src/standard/render/mesh.ts +404 -0
  42. package/src/standard/render/overlay.ts +8 -5
  43. package/src/standard/render/pass.ts +1 -1
  44. package/src/standard/render/postprocess.ts +263 -242
  45. package/src/standard/render/scene.ts +28 -16
  46. package/src/standard/render/surface/index.ts +81 -12
  47. package/src/standard/render/surface/shaders.ts +511 -0
  48. package/src/standard/render/surface/structs.ts +23 -6
  49. package/src/standard/tween/tween.ts +44 -115
  50. package/src/standard/render/bvh/radix.ts +0 -476
  51. package/src/standard/render/forward/index.ts +0 -259
  52. package/src/standard/render/forward/raster.ts +0 -228
  53. package/src/standard/render/mesh/box.ts +0 -20
  54. package/src/standard/render/mesh/index.ts +0 -446
  55. package/src/standard/render/mesh/plane.ts +0 -11
  56. package/src/standard/render/mesh/sphere.ts +0 -40
  57. package/src/standard/render/mesh/unified.ts +0 -96
  58. package/src/standard/render/shaders.ts +0 -484
  59. package/src/standard/render/surface/compile.ts +0 -67
  60. package/src/standard/render/surface/noise.ts +0 -45
  61. package/src/standard/render/surface/wgsl.ts +0 -573
  62. /package/src/standard/{render → raytracing}/intersection.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiplekex/shallot",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -11,7 +11,7 @@ export interface FieldAccessor {
11
11
 
12
12
  export interface ComponentTraits {
13
13
  defaults?: () => Record<string, number>;
14
- adapter?: (attrs: Record<string, string>, eid: number) => Record<string, number>;
14
+ parse?: Record<string, (value: string) => number | undefined>;
15
15
  }
16
16
 
17
17
  const traitsMap = new WeakMap<ComponentLike, ComponentTraits>();
package/src/core/index.ts CHANGED
@@ -79,16 +79,4 @@ export { toKebabCase, toCamelCase } from "./strings";
79
79
 
80
80
  export { initRuntime, getRuntime, resetRuntime, type Runtime, type RuntimeTarget } from "./runtime";
81
81
 
82
- export {
83
- parse,
84
- serialize,
85
- load,
86
- registerPostLoadHook,
87
- unregisterPostLoadHook,
88
- type Node,
89
- type Attr,
90
- type Ref,
91
- type ParseError,
92
- type PostLoadHook,
93
- type PostLoadContext,
94
- } from "./xml";
82
+ export { parse, serialize, load, type Node, type Attr, type Ref, type ParseError } from "./xml";
package/src/core/math.ts CHANGED
@@ -284,6 +284,192 @@ export function extractFrustumPlanes(viewProj: Float32Array): Float32Array {
284
284
  return planes;
285
285
  }
286
286
 
287
+ export function lookAtMatrix(
288
+ eyeX: number,
289
+ eyeY: number,
290
+ eyeZ: number,
291
+ targetX: number,
292
+ targetY: number,
293
+ targetZ: number,
294
+ upX = 0,
295
+ upY = 1,
296
+ upZ = 0
297
+ ): Float32Array {
298
+ let zx = eyeX - targetX;
299
+ let zy = eyeY - targetY;
300
+ let zz = eyeZ - targetZ;
301
+ let zLen = Math.sqrt(zx * zx + zy * zy + zz * zz);
302
+
303
+ if (zLen < 1e-6) {
304
+ zx = 0;
305
+ zy = 0;
306
+ zz = 1;
307
+ } else {
308
+ zLen = 1 / zLen;
309
+ zx *= zLen;
310
+ zy *= zLen;
311
+ zz *= zLen;
312
+ }
313
+
314
+ let xx = upY * zz - upZ * zy;
315
+ let xy = upZ * zx - upX * zz;
316
+ let xz = upX * zy - upY * zx;
317
+ let xLen = Math.sqrt(xx * xx + xy * xy + xz * xz);
318
+
319
+ if (xLen < 1e-6) {
320
+ if (Math.abs(zy) > 0.9) {
321
+ xx = 1;
322
+ xy = 0;
323
+ xz = 0;
324
+ } else {
325
+ xx = -zz;
326
+ xy = 0;
327
+ xz = zx;
328
+ }
329
+ xLen = Math.sqrt(xx * xx + xy * xy + xz * xz);
330
+ }
331
+
332
+ xLen = 1 / xLen;
333
+ xx *= xLen;
334
+ xy *= xLen;
335
+ xz *= xLen;
336
+
337
+ const yx = zy * xz - zz * xy;
338
+ const yy = zz * xx - zx * xz;
339
+ const yz = zx * xy - zy * xx;
340
+
341
+ const tx = -(xx * eyeX + xy * eyeY + xz * eyeZ);
342
+ const ty = -(yx * eyeX + yy * eyeY + yz * eyeZ);
343
+ const tz = -(zx * eyeX + zy * eyeY + zz * eyeZ);
344
+
345
+ return new Float32Array([xx, yx, zx, 0, xy, yy, zy, 0, xz, yz, zz, 0, tx, ty, tz, 1]);
346
+ }
347
+
348
+ export function orthographicBounds(
349
+ left: number,
350
+ right: number,
351
+ bottom: number,
352
+ top: number,
353
+ near: number,
354
+ far: number
355
+ ): Float32Array {
356
+ const lr = 1 / (right - left);
357
+ const bt = 1 / (top - bottom);
358
+ const nf = 1 / (near - far);
359
+
360
+ return new Float32Array([
361
+ 2 * lr,
362
+ 0,
363
+ 0,
364
+ 0,
365
+ 0,
366
+ 2 * bt,
367
+ 0,
368
+ 0,
369
+ 0,
370
+ 0,
371
+ nf,
372
+ 0,
373
+ -(right + left) * lr,
374
+ -(top + bottom) * bt,
375
+ near * nf,
376
+ 1,
377
+ ]);
378
+ }
379
+
380
+ export function extractFrustumCorners(
381
+ invViewProj: Float32Array,
382
+ nearZ: number,
383
+ farZ: number
384
+ ): Float32Array {
385
+ const corners = new Float32Array(24);
386
+ const ndcCorners = [
387
+ [-1, -1, nearZ],
388
+ [1, -1, nearZ],
389
+ [-1, 1, nearZ],
390
+ [1, 1, nearZ],
391
+ [-1, -1, farZ],
392
+ [1, -1, farZ],
393
+ [-1, 1, farZ],
394
+ [1, 1, farZ],
395
+ ];
396
+
397
+ for (let i = 0; i < 8; i++) {
398
+ const [nx, ny, nz] = ndcCorners[i];
399
+ const m = invViewProj;
400
+
401
+ const wx = m[0] * nx + m[4] * ny + m[8] * nz + m[12];
402
+ const wy = m[1] * nx + m[5] * ny + m[9] * nz + m[13];
403
+ const wz = m[2] * nx + m[6] * ny + m[10] * nz + m[14];
404
+ const ww = m[3] * nx + m[7] * ny + m[11] * nz + m[15];
405
+
406
+ corners[i * 3] = wx / ww;
407
+ corners[i * 3 + 1] = wy / ww;
408
+ corners[i * 3 + 2] = wz / ww;
409
+ }
410
+
411
+ return corners;
412
+ }
413
+
414
+ export function invertMatrix(m: Float32Array): Float32Array {
415
+ const out = new Float32Array(16);
416
+
417
+ const a00 = m[0],
418
+ a01 = m[1],
419
+ a02 = m[2],
420
+ a03 = m[3];
421
+ const a10 = m[4],
422
+ a11 = m[5],
423
+ a12 = m[6],
424
+ a13 = m[7];
425
+ const a20 = m[8],
426
+ a21 = m[9],
427
+ a22 = m[10],
428
+ a23 = m[11];
429
+ const a30 = m[12],
430
+ a31 = m[13],
431
+ a32 = m[14],
432
+ a33 = m[15];
433
+
434
+ const b00 = a00 * a11 - a01 * a10;
435
+ const b01 = a00 * a12 - a02 * a10;
436
+ const b02 = a00 * a13 - a03 * a10;
437
+ const b03 = a01 * a12 - a02 * a11;
438
+ const b04 = a01 * a13 - a03 * a11;
439
+ const b05 = a02 * a13 - a03 * a12;
440
+ const b06 = a20 * a31 - a21 * a30;
441
+ const b07 = a20 * a32 - a22 * a30;
442
+ const b08 = a20 * a33 - a23 * a30;
443
+ const b09 = a21 * a32 - a22 * a31;
444
+ const b10 = a21 * a33 - a23 * a31;
445
+ const b11 = a22 * a33 - a23 * a32;
446
+
447
+ let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
448
+ if (Math.abs(det) < 1e-10) {
449
+ return out;
450
+ }
451
+ det = 1 / det;
452
+
453
+ out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det;
454
+ out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det;
455
+ out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det;
456
+ out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det;
457
+ out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det;
458
+ out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det;
459
+ out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det;
460
+ out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det;
461
+ out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det;
462
+ out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det;
463
+ out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det;
464
+ out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det;
465
+ out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det;
466
+ out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det;
467
+ out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det;
468
+ out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det;
469
+
470
+ return out;
471
+ }
472
+
287
473
  export function lookAt(
288
474
  eyeX: number,
289
475
  eyeY: number,
package/src/core/state.ts CHANGED
@@ -20,7 +20,7 @@ import type { RelationDef } from "./relation";
20
20
  import type { Plugin, StateBuilder } from "./builder";
21
21
  import { initRuntime, type Runtime } from "./runtime";
22
22
  import { registerComponent, getTraits, type ComponentData } from "./component";
23
- import { type ResourceKey } from "./resource";
23
+ import type { ResourceKey } from "./resource";
24
24
 
25
25
  export const MAX_ENTITIES = 65536;
26
26
 
package/src/core/xml.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  import { addComponent, Pair } from "bitecs";
2
2
  import type { State } from "./state";
3
- import { getRegisteredComponent, type ComponentLike, type RegisteredComponent } from "./component";
3
+ import {
4
+ getRegisteredComponent,
5
+ getTraits,
6
+ type ComponentLike,
7
+ type RegisteredComponent,
8
+ } from "./component";
4
9
  import { getRelationDef, ChildOf } from "./relation";
5
10
  import { toKebabCase, toCamelCase } from "./strings";
6
11
 
@@ -136,7 +141,7 @@ function tokenize(xml: string): Token[] {
136
141
 
137
142
  function parseTagAttrs(tag: string): Record<string, string> {
138
143
  const attrs: Record<string, string> = {};
139
- const attrRegex = /([^\s=<>\/]+)(?:\s*=\s*"([^"]*)")?/g;
144
+ const attrRegex = /([^\s=<>/]+)(?:\s*=\s*"([^"]*)")?/g;
140
145
  const inner = tag.replace(/^<\s*\w+/, "").replace(/\/?>$/, "");
141
146
  let match: RegExpExecArray | null;
142
147
 
@@ -312,23 +317,6 @@ function parseNodeFromTokens(
312
317
  };
313
318
  }
314
319
 
315
- export interface PostLoadContext {
316
- getEntityByName(name: string): number | null;
317
- }
318
-
319
- export type PostLoadHook = (state: State, context: PostLoadContext) => void;
320
-
321
- const postLoadHooks: PostLoadHook[] = [];
322
-
323
- export function registerPostLoadHook(hook: PostLoadHook): void {
324
- postLoadHooks.push(hook);
325
- }
326
-
327
- export function unregisterPostLoadHook(hook: PostLoadHook): void {
328
- const idx = postLoadHooks.indexOf(hook);
329
- if (idx !== -1) postLoadHooks.splice(idx, 1);
330
- }
331
-
332
320
  interface QueuedEntity {
333
321
  node: Node;
334
322
  eid: number;
@@ -376,13 +364,6 @@ export function load(nodes: Node[], state: State): Map<Node, number> {
376
364
  setFieldValue(ref.component, ref.field, ref.eid, targetEid);
377
365
  }
378
366
 
379
- const context: PostLoadContext = {
380
- getEntityByName: (name) => nameToEntity.get(name) ?? null,
381
- };
382
- for (const hook of postLoadHooks) {
383
- hook(state, context);
384
- }
385
-
386
367
  if (errors.length > 0) {
387
368
  throw new Error(errors.map((e) => e.message).join("\n"));
388
369
  }
@@ -487,24 +468,22 @@ function applyComponent(
487
468
  props["_value"] = value;
488
469
  }
489
470
 
490
- let values: Record<string, number>;
491
- let entityRefs: { field: string; targetName: string }[] = [];
492
-
493
- if (traits?.adapter) {
494
- values = traits.adapter(props, eid);
495
- } else {
496
- const result = parseAttrs(def, props);
497
- values = result.values;
498
- entityRefs = result.entityRefs;
499
- for (const err of result.errors) {
500
- errors.push({ message: `<${name}> ${err}` });
501
- }
471
+ const result = parseAttrs(def, props);
472
+ const values = result.values;
473
+ const strings = result.strings;
474
+ const entityRefs = result.entityRefs;
475
+ for (const err of result.errors) {
476
+ errors.push({ message: `<${name}> ${err}` });
502
477
  }
503
478
 
504
479
  for (const [field, val] of Object.entries(values)) {
505
480
  setFieldValue(component, field, eid, val);
506
481
  }
507
482
 
483
+ for (const [field, val] of Object.entries(strings)) {
484
+ setString(component, field, eid, val);
485
+ }
486
+
508
487
  for (const ref of entityRefs) {
509
488
  pendingFieldRefs.push({
510
489
  eid,
@@ -520,10 +499,12 @@ function parseAttrs(
520
499
  props: Record<string, string>
521
500
  ): {
522
501
  values: Record<string, number>;
502
+ strings: Record<string, string>;
523
503
  entityRefs: { field: string; targetName: string }[];
524
504
  errors: string[];
525
505
  } {
526
506
  const allValues: Record<string, number> = {};
507
+ const allStrings: Record<string, string> = {};
527
508
  const allEntityRefs: { field: string; targetName: string }[] = [];
528
509
  const allErrors: string[] = [];
529
510
 
@@ -531,6 +512,7 @@ function parseAttrs(
531
512
  if (isCSSAttrSyntax(props._value)) {
532
513
  const result = parsePropertyString(def.name, props._value, def.component);
533
514
  Object.assign(allValues, result.values);
515
+ Object.assign(allStrings, result.strings);
534
516
  allEntityRefs.push(...result.entityRefs);
535
517
  allErrors.push(...result.errors);
536
518
  }
@@ -543,6 +525,7 @@ function parseAttrs(
543
525
  if (isCSSAttrSyntax(propValue)) {
544
526
  const result = parsePropertyString(def.name, propValue, def.component);
545
527
  Object.assign(allValues, result.values);
528
+ Object.assign(allStrings, result.strings);
546
529
  allEntityRefs.push(...result.entityRefs);
547
530
  allErrors.push(...result.errors);
548
531
  } else {
@@ -552,12 +535,13 @@ function parseAttrs(
552
535
  def.component
553
536
  );
554
537
  Object.assign(allValues, result.values);
538
+ Object.assign(allStrings, result.strings);
555
539
  allEntityRefs.push(...result.entityRefs);
556
540
  allErrors.push(...result.errors);
557
541
  }
558
542
  }
559
543
 
560
- return { values: allValues, entityRefs: allEntityRefs, errors: allErrors };
544
+ return { values: allValues, strings: allStrings, entityRefs: allEntityRefs, errors: allErrors };
561
545
  }
562
546
 
563
547
  function setFieldValue(component: ComponentLike, field: string, eid: number, value: number): void {
@@ -567,6 +551,17 @@ function setFieldValue(component: ComponentLike, field: string, eid: number, val
567
551
  }
568
552
  }
569
553
 
554
+ function isStringField(component: ComponentLike, field: string): boolean {
555
+ const val = component[field];
556
+ if (val == null) return false;
557
+ if (ArrayBuffer.isView(val) || Array.isArray(val)) return false;
558
+ return typeof val === "object";
559
+ }
560
+
561
+ function setString(component: ComponentLike, field: string, eid: number, value: string): void {
562
+ (component[field] as Record<number, string>)[eid] = value;
563
+ }
564
+
570
565
  function detectVec2(component: ComponentLike, base: string): boolean {
571
566
  return `${base}X` in component && `${base}Y` in component;
572
567
  }
@@ -594,7 +589,7 @@ function parseNumber(value: string): number | null {
594
589
  if (value === "false") return 0;
595
590
 
596
591
  const num = parseFloat(value);
597
- return isNaN(num) ? null : num;
592
+ return Number.isNaN(num) ? null : num;
598
593
  }
599
594
 
600
595
  function parseValues(valueStr: string): (number | null)[] {
@@ -633,10 +628,12 @@ function parsePropertyString(
633
628
  component: ComponentLike
634
629
  ): {
635
630
  values: Record<string, number>;
631
+ strings: Record<string, string>;
636
632
  entityRefs: { field: string; targetName: string }[];
637
633
  errors: string[];
638
634
  } {
639
635
  const values: Record<string, number> = {};
636
+ const strings: Record<string, string> = {};
640
637
  const entityRefs: { field: string; targetName: string }[] = [];
641
638
  const errors: string[] = [];
642
639
 
@@ -679,6 +676,24 @@ function parsePropertyString(
679
676
  const parsed = parseValues(valueStr);
680
677
 
681
678
  if (parsed.some((v) => v === null)) {
679
+ const rawValue = valueStr.trim();
680
+
681
+ if (name in component && isStringField(component, name)) {
682
+ strings[name] = rawValue;
683
+ continue;
684
+ }
685
+
686
+ if (parsed.length === 1) {
687
+ const traits = getTraits(component);
688
+ const parseFn = traits?.parse?.[name];
689
+ if (parseFn) {
690
+ const resolved = parseFn(rawValue);
691
+ if (resolved !== undefined) {
692
+ values[name] = resolved;
693
+ continue;
694
+ }
695
+ }
696
+ }
682
697
  errors.push(`Invalid number in "${prop}"`);
683
698
  continue;
684
699
  }
@@ -756,7 +771,7 @@ function parsePropertyString(
756
771
  }
757
772
  }
758
773
 
759
- return { values, entityRefs, errors };
774
+ return { values, strings, entityRefs, errors };
760
775
  }
761
776
 
762
777
  function isCSSAttrSyntax(value: string): boolean {
@@ -17,8 +17,8 @@ import {
17
17
  type Draw,
18
18
  type SharedPassContext,
19
19
  } from "../../standard/render";
20
- import { DEPTH_FORMAT } from "../../standard/render/scene";
21
- import { SCENE_STRUCT_WGSL } from "../../standard/render/shaders";
20
+ import { Z_FORMAT } from "../../standard/render/scene";
21
+ import { SCENE_STRUCT_WGSL } from "../../standard/render/surface/structs";
22
22
  import { Transform } from "../../standard/transforms";
23
23
  import { Line, Lines, LinesPlugin } from "../lines";
24
24
 
@@ -198,7 +198,7 @@ export function createArrowsPipeline(
198
198
  topology: "triangle-list",
199
199
  },
200
200
  depthStencil: {
201
- format: DEPTH_FORMAT,
201
+ format: Z_FORMAT,
202
202
  depthCompare: "less",
203
203
  depthWriteEnabled: false,
204
204
  },
@@ -0,0 +1,37 @@
1
+ import { surface } from "../standard/render/surface";
2
+
3
+ export const Caustic = surface({
4
+ fragment: `
5
+ let uv = (*surface).worldPos.xz;
6
+ let t = scene.time * 0.5;
7
+
8
+ // Fine caustic network
9
+ let f1 = sin(uv.x * 4.0 + t * 1.3 + sin(uv.y * 3.0 + t * 0.9) * 0.6);
10
+ let f2 = sin(uv.y * 5.0 - t * 1.1 + sin(uv.x * 3.5 - t * 0.7) * 0.5);
11
+ let f3 = sin((uv.x + uv.y) * 3.5 + t * 0.8);
12
+ let f4 = sin((uv.x - uv.y) * 4.0 - t * 1.2);
13
+ let f5 = sin(uv.x * 6.0 - uv.y * 2.5 + t * 1.5);
14
+ let f6 = sin(uv.y * 6.5 + uv.x * 2.0 - t * 1.0);
15
+
16
+ // Medium detail layer
17
+ let m1 = sin(uv.x * 2.5 + t * 0.7 + sin(uv.y * 1.5 + t * 0.5) * 0.4);
18
+ let m2 = sin(uv.y * 2.8 - t * 0.6 + sin(uv.x * 2.0 - t * 0.4) * 0.3);
19
+
20
+ // Sharp caustic lines using high power
21
+ let c1 = pow(max(0.0, 1.0 - abs(f1)), 12.0);
22
+ let c2 = pow(max(0.0, 1.0 - abs(f2)), 12.0);
23
+ let c3 = pow(max(0.0, 1.0 - abs(f3)), 10.0);
24
+ let c4 = pow(max(0.0, 1.0 - abs(f4)), 10.0);
25
+ let c5 = pow(max(0.0, 1.0 - abs(f5)), 14.0);
26
+ let c6 = pow(max(0.0, 1.0 - abs(f6)), 14.0);
27
+ let c7 = pow(max(0.0, 1.0 - abs(m1)), 8.0);
28
+ let c8 = pow(max(0.0, 1.0 - abs(m2)), 8.0);
29
+
30
+ // Combine layers with variation
31
+ let fine = (c1 + c2 + c3 + c4 + c5 + c6) * 0.12;
32
+ let medium = (c7 + c8) * 0.15;
33
+ let caustic = fine + medium;
34
+
35
+ let light = scene.sunColor.rgb * caustic * 0.3;
36
+ (*surface).baseColor += light;`,
37
+ });