onejs-sl 0.3.0 → 0.4.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.
package/src/serial.ts CHANGED
@@ -9,14 +9,16 @@
9
9
  * older one is migrated to this one and rehashed under it.
10
10
  */
11
11
  import {
12
- INPUTS, SLError, SL_IR_VERSION, TYPE, checkCaps, controlOf, controlProblem, hashProgram,
13
- type NodeRef, type Program, type SLNode, type SLType, type TextureDecl, type UniformDecl,
12
+ INPUTS, SLError, SL_HASH_VERSION, SL_IR_VERSION, TYPE, checkCaps, controlOf, controlProblem, formProblem, hashProgram,
13
+ programVersion, type NodeRef, type Program, type SLNode, type SLType, type TextureDecl, type UniformDecl,
14
14
  } from "./ir"
15
- import { SL_ARITY, SL_NAME } from "./ops"
15
+ import { SLOP, SL_ARITY, SL_NAME } from "./ops"
16
16
 
17
17
  export interface ProgramJSON {
18
18
  /** `SL_IR_VERSION` of the writer. Absent means 1. */
19
19
  v: number
20
+ /** `SL_HASH_VERSION` of the writer, which `hash` was computed under. Absent means 1. */
21
+ h?: number
20
22
  nodes: SLNode[]
21
23
  result: NodeRef
22
24
  uniforms: UniformDecl[]
@@ -25,7 +27,10 @@ export interface ProgramJSON {
25
27
  }
26
28
 
27
29
  export function toJSON(p: Program): ProgramJSON {
28
- return { v: p.version, nodes: p.nodes, result: p.result, uniforms: p.uniforms, textures: p.textures, hash: p.hash }
30
+ return {
31
+ v: p.version, h: SL_HASH_VERSION, nodes: p.nodes, result: p.result, uniforms: p.uniforms, textures: p.textures,
32
+ hash: p.hash,
33
+ }
29
34
  }
30
35
 
31
36
  export function fromJSON(json: unknown): Program {
@@ -38,24 +43,71 @@ export function fromJSON(json: unknown): Program {
38
43
  `Update onejs-sl to read it, or rebuild it from its .sl source.`)
39
44
  }
40
45
  if (!Array.isArray(j.nodes) || j.nodes.length === 0) fail("it has no nodes")
41
- const nodes = j.nodes.map((n, i) => node(n, i))
42
- const result = j.result
43
- if (!Number.isInteger(result) || result! < 0 || result! >= nodes.length) fail(`its result ${String(result)} is not a node`)
44
- if (nodes[result!]!.type !== TYPE.VEC4) fail("its result is not a float4")
46
+ const written = j.nodes.map((n, i) => node(n, i))
47
+ if (!Number.isInteger(j.result) || j.result! < 0 || j.result! >= written.length) {
48
+ fail(`its result ${String(j.result)} is not a node`)
49
+ }
50
+ // The lowest version that has these nodes, which is what the writer
51
+ // recorded unless the file predates version 2 (whose nodes version 2 reads
52
+ // as they are) or was edited. One that claims less than its nodes need
53
+ // holds something its version never had.
54
+ const needs = programVersion(written)
55
+ if (v < needs && v >= 2) fail(`it says IR version ${v} and holds nodes version ${needs} added; it was changed after it was written`)
56
+ const { nodes, result } = v < 3 ? toOperands(written, j.result!) : { nodes: written, result: j.result! }
57
+ nodes.forEach((n, i) => {
58
+ const problem = n.k === "call" ? formProblem(n, nodes) : null
59
+ if (problem !== null) fail(`node ${i} is not a form its op has: ${problem}`)
60
+ })
61
+ if (nodes[result]!.type !== TYPE.VEC4) fail("its result is not a float4")
45
62
  const uniforms = (j.uniforms ?? []).map(uniform)
46
63
  const textures = (j.textures ?? []).map(texture)
47
64
  checkCaps({ uniforms, textures })
48
65
  for (const n of nodes) {
49
66
  if (n.k === "uniform" && n.slot >= uniforms.length) fail(`a node reads uniform slot ${n.slot}, which is not declared`)
50
67
  }
51
- const hash = hashProgram(nodes, result!, uniforms, textures)
52
- // Same version, so the same maths and the same hash; a different one means
53
- // the file was edited or damaged, and its cached shader belongs to
54
- // something else. An older version is rehashed under this one on purpose.
55
- if (v === SL_IR_VERSION && j.hash !== undefined && j.hash !== hash) {
68
+ const version = programVersion(nodes)
69
+ const hash = hashProgram(nodes, result, uniforms, textures)
70
+ // The same IR version and hash scheme, so the same hash; a different one
71
+ // means the file was edited or damaged, and its cached shader belongs to
72
+ // something else. A file an older version or scheme wrote is rehashed under
73
+ // this one on purpose.
74
+ if (v === version && (j.h ?? 1) === SL_HASH_VERSION && j.hash !== undefined && j.hash !== hash) {
56
75
  fail(`its hash ${j.hash} does not match its nodes (${hash}); it was changed after it was written`)
57
76
  }
58
- return { version: SL_IR_VERSION, nodes, result: result!, uniforms, textures, hash }
77
+ return { version, nodes, result, uniforms, textures, hash }
78
+ }
79
+
80
+ /**
81
+ * A graph written before IR 3, with the forms it had then moved to the one each
82
+ * op has now: an sdf's shape parameters after its id and an octave op's count
83
+ * were immediates, and are operands. Each constant goes in just before the
84
+ * call that reads it, and every later reference moves along.
85
+ */
86
+ function toOperands(written: readonly SLNode[], writtenResult: NodeRef): { nodes: SLNode[]; result: NodeRef } {
87
+ const nodes: SLNode[] = []
88
+ const at: NodeRef[] = []
89
+ const constant = (type: SLType, v: number[]): NodeRef => nodes.push({ k: "const", type, v }) - 1
90
+ written.forEach((n, i) => {
91
+ const imm = n.k === "call" ? n.imm ?? [] : []
92
+ if (n.k === "call" && n.op === SLOP.SDF && n.args.length === 1) {
93
+ if (imm.length < 5) fail(`node ${i} is an sdf with ${imm.length} immediates; one written before IR 3 has at least 5`)
94
+ const q = constant(TYPE.VEC4, [imm[1]!, imm[2]!, imm[3]!, imm[4]!])
95
+ const r = constant(TYPE.VEC2, [imm[5] ?? 0, imm[6] ?? 0])
96
+ at[i] = nodes.push({ ...n, args: [at[n.args[0]!]!, q, r], imm: [imm[0]!] }) - 1
97
+ return
98
+ }
99
+ const octaves = n.k === "call" && (n.op === SLOP.FBM || n.op === SLOP.TURBULENCE || n.op === SLOP.RIDGED)
100
+ if (octaves && n.args.length === 1) {
101
+ if (imm.length < 1) fail(`node ${i} is ${SL_NAME[n.op]} with no octave count`)
102
+ const count = constant(TYPE.FLOAT, [imm[0]!])
103
+ at[i] = nodes.push({ ...n, args: [at[n.args[0]!]!, count], imm: imm.slice(1) }) - 1
104
+ return
105
+ }
106
+ const moved: SLNode = n.k === "call" ? { ...n, args: n.args.map((a) => at[a]!) }
107
+ : n.k === "swizzle" ? { ...n, src: at[n.src]! } : n
108
+ at[i] = nodes.push(moved) - 1
109
+ })
110
+ return { nodes, result: at[writtenResult]! }
59
111
  }
60
112
 
61
113
  function node(n: unknown, i: number): SLNode {
package/src/sl.ts CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  import { parseColor as parseHex } from "./color"
26
26
  import {
27
- Builder, INPUTS, SLError, SL_IR_VERSION, TYPE, hashProgram, widthName,
27
+ Builder, INPUTS, RAMP_STOP_COMPUTED, SLError, TYPE, hashProgram, programVersion, widthName, writtenColour,
28
28
  type InputName, type NodeRef, type Program, type SLNode, type SLType, type UniformControl,
29
29
  } from "./ir"
30
30
  import { SLOP, type SLOpCode } from "./ops"
@@ -288,7 +288,7 @@ export function program(fn: (inputs: ProgramInputs) => Vec4): Program {
288
288
  }
289
289
  const nodes: SLNode[] = b.nodes.slice()
290
290
  return {
291
- version: SL_IR_VERSION,
291
+ version: programVersion(nodes),
292
292
  nodes,
293
293
  result: out.ref,
294
294
  uniforms: b.uniforms.slice(),
@@ -533,6 +533,10 @@ export function uniformDefaults(p: Program): number[] {
533
533
  * fields `fx.noise` draws: a value or simplex base, layered as fBm, or the
534
534
  * turbulence and ridged variants built on simplex. A program has no seed;
535
535
  * offset the input for a different field. Octaves are 1 to 4, as in fx.
536
+ *
537
+ * The octave count may be a value, a uniform say, rounded to the nearest whole
538
+ * number and held to 1 to 4 where it is read. It is an operand either way
539
+ * (IR 3); a constant one is checked here, and prints as the number it is.
536
540
  */
537
541
  export function noise(p: Vec2): Float {
538
542
  return mk(p.owner, p.owner.call(SLOP.NOISE, TYPE.FLOAT, [p.ref]), TYPE.FLOAT)
@@ -540,24 +544,31 @@ export function noise(p: Vec2): Float {
540
544
  export function simplex(p: Vec2): Float {
541
545
  return mk(p.owner, p.owner.call(SLOP.SIMPLEX, TYPE.FLOAT, [p.ref]), TYPE.FLOAT)
542
546
  }
543
- function octaveCount(name: string, octaves: number): number {
544
- if (!Number.isInteger(octaves) || octaves < 1 || octaves > 4) {
545
- throw new SLError(`${name} octaves must be a whole number from 1 to 4, got ${octaves}`)
547
+ /**
548
+ * An octave op: the point and the count as operands, with `rest` (fbm's kind)
549
+ * the immediates. A constant count is checked here.
550
+ */
551
+ function octaves(op: SLOpCode, name: string, p: Vec2, count: Num, rest: number[]): Float {
552
+ const c = components(p.owner, count)
553
+ if (c.length !== 1) throw new SLError(`${name}'s octave count is one number, and this is a ${widthName(c.length as SLType)}`)
554
+ const n = c[0]!
555
+ if (typeof n === "number" && (!Number.isInteger(n) || n < 1 || n > 4)) {
556
+ throw new SLError(`${name} octaves must be a whole number from 1 to 4, got ${n}`)
546
557
  }
547
- return octaves
558
+ const v = typeof n === "number" ? float(n) : n
559
+ return mk(p.owner, p.owner.call(op, TYPE.FLOAT, [p.ref, v.ref], rest), TYPE.FLOAT)
548
560
  }
549
561
  /** Layered noise. `base` picks the grid ("value", the default) or triangles ("simplex"). */
550
- export function fbm(p: Vec2, octaves = 3, base: "value" | "simplex" = "value"): Float {
551
- const kind = base === "simplex" ? 1 : 0
552
- return mk(p.owner, p.owner.call(SLOP.FBM, TYPE.FLOAT, [p.ref], [octaveCount("fbm", octaves), kind]), TYPE.FLOAT)
562
+ export function fbm(p: Vec2, octaveCount: Num = 3, base: "value" | "simplex" = "value"): Float {
563
+ return octaves(SLOP.FBM, "fbm", p, octaveCount, [base === "simplex" ? 1 : 0])
553
564
  }
554
565
  /** Sum of |simplex| octaves: creases that stack into veins and licks. Fire, smoke, marble. */
555
- export function turbulence(p: Vec2, octaves = 3): Float {
556
- return mk(p.owner, p.owner.call(SLOP.TURBULENCE, TYPE.FLOAT, [p.ref], [octaveCount("turbulence", octaves)]), TYPE.FLOAT)
566
+ export function turbulence(p: Vec2, octaveCount: Num = 3): Float {
567
+ return octaves(SLOP.TURBULENCE, "turbulence", p, octaveCount, [])
557
568
  }
558
569
  /** The turbulence crease made bright and squared: ridges, lightning, cracks. */
559
- export function ridged(p: Vec2, octaves = 3): Float {
560
- return mk(p.owner, p.owner.call(SLOP.RIDGED, TYPE.FLOAT, [p.ref], [octaveCount("ridged", octaves)]), TYPE.FLOAT)
570
+ export function ridged(p: Vec2, octaveCount: Num = 3): Float {
571
+ return octaves(SLOP.RIDGED, "ridged", p, octaveCount, [])
561
572
  }
562
573
 
563
574
  /** "#rgb", "#rrggbb" or "#rrggbbaa" to 0..1 components. Shared with fx. */
@@ -581,14 +592,23 @@ export function parseColor(hex: string): [number, number, number, number] {
581
592
  *
582
593
  * This is the argument for the EDSL in miniature. A library function that
583
594
  * composes from primitives costs one function here and nothing anywhere else.
595
+ *
596
+ * A stop is a colour as written: a hex, four numbers, or a value that is a
597
+ * colour as written, which is one `color`, `sl.uniform.colour` or a hex
598
+ * literal made (a colour uniform, say). The ramp blends the colours as
599
+ * written, so a value computed any other way, already in the working space,
600
+ * has nothing it could blend, and is refused.
584
601
  */
585
- export function ramp(t: Num, stops: Array<string | [number, number, number, number]>): Vec4 {
602
+ export function ramp(t: Num, stops: Array<string | [number, number, number, number] | Vec3 | Vec4>): Vec4 {
586
603
  if (stops.length < 2) throw new SLError(`a ramp needs at least 2 stops, got ${stops.length}`)
587
604
  const tv = (typeof t === "number" ? float(t) : t).saturate()
605
+ const written = stops.map((s) => (s instanceof Val ? asWritten(s) : null))
588
606
  // Each stop is built immediately before the mix that consumes it. The graph
589
607
  // and the hash are the same either way (the hash is over the shape, not the
590
608
  // storage order); the order only mattered to the VM's register allocator.
591
609
  const colourAt = (i: number) => {
610
+ const w = written[i]
611
+ if (w !== null && w !== undefined) return w
592
612
  const v = typeof stops[i] === "string" ? parseColor(stops[i] as string) : (stops[i] as number[])
593
613
  return vec4(v[0], v[1], v[2], v[3])
594
614
  }
@@ -606,6 +626,18 @@ export function ramp(t: Num, stops: Array<string | [number, number, number, numb
606
626
  return toLinear(out)
607
627
  }
608
628
 
629
+ /**
630
+ * A colour value as it was written, a float4: the operand of the `toLinear`
631
+ * that reading it as a colour applied, with alpha 1 when it has three
632
+ * components. Anything else was computed in the working space.
633
+ */
634
+ function asWritten(v: Vec3 | Vec4): Vec4 {
635
+ const ref = writtenColour(v.owner.nodes, v.ref)
636
+ if (ref === null) throw new SLError(RAMP_STOP_COMPUTED)
637
+ const raw = mk(v.owner, ref, v.width) as Val
638
+ return v.width === TYPE.VEC4 ? (raw as Vec4) : vec4(raw, 1)
639
+ }
640
+
609
641
  /**
610
642
  * Signed distance to a shape, positive outside and negative inside.
611
643
  *
@@ -621,27 +653,45 @@ export function ramp(t: Num, stops: Array<string | [number, number, number, numb
621
653
  *
622
654
  * A shape takes up to `SL_SDF_PARAMS[kind]` parameters, six at most. Fewer
623
655
  * fills zeros, which is all a program could pass before six shapes grew their
624
- * fifth and sixth.
656
+ * fifth and sixth. A vector parameter counts as its components, so a box's
657
+ * half extents can be one float2.
625
658
  *
626
659
  * Which parameters a shape takes is the shape's own business; `circle` wants a
627
660
  * radius, `roundedBox` wants half extents and a corner. See `lib/sdf2d.hlsl`.
661
+ *
662
+ * A parameter may be any value, a uniform or one computed per pixel. The six
663
+ * are operands either way (IR 3), a float4 and a float2, and constant ones
664
+ * print as the numbers they are.
628
665
  */
629
- export function sdf(kind: SlSdfKind, p: Vec2, params: number[] = []): Float {
666
+ export function sdf(kind: SlSdfKind, p: Vec2, params: Num[] = []): Float {
630
667
  const id = SL_SDF_SHAPES[kind]
631
668
  if (id === undefined) throw new SLError(`"${kind}" is not a shape; see SL_SDF_SHAPES for the 42 names`)
669
+ const c = params.flatMap((v) => components(p.owner, v))
632
670
  // At least four, so a program that passed a shape more than it reads, which
633
671
  // was harmless when every shape took four, still builds.
634
672
  const most = Math.max(4, SL_SDF_PARAMS[kind])
635
- if (params.length > most) {
636
- throw new SLError(`sl.sdf("${kind}") takes at most ${most} parameters and was given ${params.length}`)
673
+ if (c.length > most) {
674
+ throw new SLError(`sl.sdf("${kind}") takes at most ${most} parameters and was given ${c.length}`)
637
675
  }
638
- for (const v of params) {
639
- if (!Number.isFinite(v)) throw new SLError(`sl.sdf parameters must be finite, got ${v}`)
676
+ for (const v of c) {
677
+ if (typeof v === "number" && !Number.isFinite(v)) throw new SLError(`sl.sdf parameters must be finite, got ${v}`)
640
678
  }
641
- // Four always, as before, and a fifth and sixth only when given: a program
642
- // using four or fewer is the same node, and hashes the same, as it was.
643
- const imm = [params[0] ?? 0, params[1] ?? 0, params[2] ?? 0, params[3] ?? 0, ...params.slice(4)]
644
- return mk(p.owner, p.owner.call(SLOP.SDF, TYPE.FLOAT, [p.ref], [id, ...imm]), TYPE.FLOAT)
679
+ const q = vec4(c[0] ?? 0, c[1] ?? 0, c[2] ?? 0, c[3] ?? 0)
680
+ const r = vec2(c[4] ?? 0, c[5] ?? 0)
681
+ return mk(p.owner, p.owner.call(SLOP.SDF, TYPE.FLOAT, [p.ref, q.ref, r.ref], [id]), TYPE.FLOAT)
682
+ }
683
+
684
+ /**
685
+ * A value as its components: numbers for a constant, which is what it would
686
+ * have been written as, and a float per component otherwise.
687
+ */
688
+ function components(b: Builder, v: Num): Num[] {
689
+ if (typeof v === "number") return [v]
690
+ if (v.owner !== b) throw new SLError("a value from another program cannot be used in this one")
691
+ const n = b.nodes[v.ref]!
692
+ if (n.k === "const") return n.v.slice()
693
+ if (v.width === 1) return [v]
694
+ return Array.from({ length: v.width }, (_, i) => v.swz("xyzw"[i] as "x"))
645
695
  }
646
696
 
647
697
  /** Distance to the nearest point of a jittered lattice. Cells, cracks, scales. */
@@ -696,7 +746,14 @@ export const uniform = {
696
746
  }
697
747
 
698
748
  export interface Texture {
749
+ /** The colour at `uv`, filtered as the texture's own settings say. */
699
750
  sample(uv: Vec2): Vec4
751
+ /**
752
+ * The colour at `uv` from mip level `lod`: 0 is the full size texture, 1
753
+ * half, and a fraction blends two levels where the texture's filter does.
754
+ * A texture with no mips has only level 0.
755
+ */
756
+ sampleLevel(uv: Vec2, lod: Num): Vec4
700
757
  }
701
758
 
702
759
  export function texture(name: string): Texture {
@@ -707,6 +764,15 @@ export function texture(name: string): Texture {
707
764
  if (uv.owner !== b) throw new SLError("a value from another program cannot be used in this one")
708
765
  return mk(b, b.call(SLOP.SAMPLE, TYPE.VEC4, [uv.ref], [slot]), TYPE.VEC4)
709
766
  },
767
+ sampleLevel(uv: Vec2, lod: Num): Vec4 {
768
+ if (uv.owner !== b) throw new SLError("a value from another program cannot be used in this one")
769
+ const level = typeof lod === "number" ? b.constant([lod]) : lod
770
+ if (typeof level !== "number") {
771
+ if (level.owner !== b) throw new SLError("a value from another program cannot be used in this one")
772
+ if (level.width !== 1) throw new SLError(`a mip level is one number, and this is a ${widthName(level.width)}`)
773
+ }
774
+ return mk(b, b.call(SLOP.SAMPLE_LOD, TYPE.VEC4, [uv.ref, typeof level === "number" ? level : level.ref], [slot]), TYPE.VEC4)
775
+ },
710
776
  }
711
777
  }
712
778
 
package/src/web.ts CHANGED
@@ -52,6 +52,11 @@ function emitWeb(p: Program, lang: WebLanguage): string {
52
52
  const emitted = new Set<number>()
53
53
  const T = (t: SLType) => (W ? (t === 1 ? "f32" : `vec${t}f`) : t === 1 ? "float" : `vec${t}`)
54
54
  const typeOf = (ref: number) => p.nodes[ref].type
55
+ /** A constant node's value, or null for anything computed. */
56
+ const constantOf = (ref: number): number[] | null => {
57
+ const c = p.nodes[ref]!
58
+ return c.k === "const" ? c.v : null
59
+ }
55
60
 
56
61
  /** A node's value widened to `t` when it is a scalar and `t` is not. */
57
62
  const splat = (ref: number, t: SLType) => (typeOf(ref) === 1 && t > 1 ? `${T(t)}(n${ref})` : `n${ref}`)
@@ -168,16 +173,23 @@ function emitWeb(p: Program, lang: WebLanguage): string {
168
173
  case SLOP.HSV2RGB: return `${lib("sl_hsv2rgb")}(${a[0]})`
169
174
  case SLOP.NOISE: return `${lib("sl_valueNoise")}(${a[0]})`
170
175
  case SLOP.SIMPLEX: return `${lib("sl_simplex")}(${a[0]})`
171
- case SLOP.FBM: return octaveCall(Math.round(imm[1] ?? 0), a[0], imm[0])
172
- case SLOP.TURBULENCE: return octaveCall(2, a[0], imm[0])
173
- case SLOP.RIDGED: return octaveCall(3, a[0], imm[0])
176
+ case SLOP.FBM: return octaveCall(Math.round((n.args.length > 1 ? imm[0] : imm[1]) ?? 0), a[0])
177
+ case SLOP.TURBULENCE: return octaveCall(2, a[0])
178
+ case SLOP.RIDGED: return octaveCall(3, a[0])
174
179
  case SLOP.VORONOI: return `${lib("sl_voronoi")}(${a[0]})`
175
- case SLOP.SDF: return sdfCall(Math.round(imm[0] ?? 0), a[0], imm)
180
+ case SLOP.SDF: return sdfCall(Math.round(imm[0] ?? 0), a[0], sdfParams())
176
181
  case SLOP.SAMPLE: {
177
182
  const slot = Math.round(imm[0] ?? 0)
178
183
  sampled.add(slot)
179
184
  return W ? `textureSample(sl_tex${slot}, sl_samp${slot}, ${a[0]})` : `texture(sl_Tex${slot}, ${a[0]})`
180
185
  }
186
+ case SLOP.SAMPLE_LOD: {
187
+ const slot = Math.round(imm[0] ?? 0)
188
+ sampled.add(slot)
189
+ return W
190
+ ? `textureSampleLevel(sl_tex${slot}, sl_samp${slot}, ${a[0]}, ${a[1]})`
191
+ : `textureLod(sl_Tex${slot}, ${a[0]}, ${a[1]})`
192
+ }
181
193
  default:
182
194
  throw new SLError(
183
195
  `the ${lang.toUpperCase()} emitter has no case for opcode ${n.op}. A program using it would ` +
@@ -188,24 +200,45 @@ function emitWeb(p: Program, lang: WebLanguage): string {
188
200
  /**
189
201
  * The library's onejsFbmKind, resolved here since the kind is a
190
202
  * constant: calling the dispatcher would print a WGSL `select` that
191
- * runs two of the four fields to keep one.
203
+ * runs two of the four fields to keep one. The count is a literal when
204
+ * it is a constant, and otherwise rounded and held to 1 to 4 as
205
+ * `sl_fbm` holds it.
192
206
  */
193
- function octaveCall(kind: number, pt: string, octaves: number | undefined): string {
194
- const o = Math.min(4, Math.max(1, Math.round(octaves ?? 3)))
207
+ function octaveCall(kind: number, pt: string): string {
208
+ const c = constantOf(n.args[1]!)
209
+ const o = c !== null
210
+ ? String(Math.min(4, Math.max(1, Math.round(c[0]!))))
211
+ : `clamp(${W ? "i32" : "int"}(floor(${a[1]} + 0.5)), 1, 4)`
195
212
  const fn = kind === 2 ? "onejsTurbulence" : kind === 3 ? "onejsRidged" : kind === 1 ? "onejsFbmSimplex" : "onejsFbm"
196
213
  return `${lib(fn, ["float2", "float", "int", "float", "float"])}(${pt}, 0.0, ${o}, 2.0, 0.5)`
197
214
  }
198
215
 
216
+ /**
217
+ * The six shape parameters, each as a float expression and, when it
218
+ * is a constant, its number: literals from a constant float4 or
219
+ * float2, and their components otherwise.
220
+ */
221
+ function sdfParams(): Array<{ text: string; value?: number }> {
222
+ const part = (ref: number, chans: string[]) => {
223
+ const c = constantOf(ref)
224
+ return chans.map((ch, i) => c !== null ? { text: lit(c[i]!), value: c[i]! } : { text: `${a[n.args.indexOf(ref)]}.${ch}` })
225
+ }
226
+ return [...part(n.args[1]!, ["x", "y", "z", "w"]), ...part(n.args[2]!, ["x", "y"])]
227
+ }
228
+
199
229
  /** The shape is a constant, so this calls it directly instead of sl_sdfDistance's switch. */
200
- function sdfCall(id: number, pt: string, im: number[]): string {
230
+ function sdfCall(id: number, pt: string, v: Array<{ text: string; value?: number }>): string {
201
231
  const shape = SDF_CALLS[id]
202
232
  // sl_sdfDistance returns 1e6 for an id it does not know.
203
233
  if (shape === undefined) return lit(1e6)
204
234
  const fn = lib(shape.fn)
205
- const v = [im[1] ?? 0, im[2] ?? 0, im[3] ?? 0, im[4] ?? 0, im[5] ?? 0, im[6] ?? 0]
206
235
  const args = shape.args.map((arg) => {
207
- if ("int" in arg) return String(Math.trunc(v[arg.int]))
208
- const parts = arg.map((i) => lit(v[i]))
236
+ // Truncated toward zero, as HLSL's int() of the float4 does.
237
+ if ("int" in arg) {
238
+ const p = v[arg.int]!
239
+ return p.value !== undefined ? String(Math.trunc(p.value)) : W ? `i32(${p.text})` : `int(${p.text})`
240
+ }
241
+ const parts = arg.map((i) => v[i]!.text)
209
242
  return parts.length === 1 ? parts[0] : `${T(parts.length as SLType)}(${parts.join(", ")})`
210
243
  })
211
244
  return `${fn}(${[pt, ...args].join(", ")})`