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/CHANGELOG.md +15 -0
- package/README.md +38 -13
- package/goldens/goldens.json +98 -63
- package/package.json +1 -1
- package/src/body.ts +29 -6
- package/src/core.ts +1 -1
- package/src/hlsl.ts +6 -3
- package/src/ir.ts +130 -14
- package/src/lang/README.md +2 -2
- package/src/lang/builtins.ts +5 -17
- package/src/lang/check.ts +24 -11
- package/src/lang/glsl.ts +1 -2
- package/src/lang/lower.ts +23 -27
- package/src/ops.ts +4 -3
- package/src/params.ts +2 -0
- package/src/serial.ts +66 -14
- package/src/sl.ts +90 -24
- package/src/web.ts +44 -11
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 {
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
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
|
|
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,
|
|
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:
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
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,
|
|
551
|
-
|
|
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,
|
|
556
|
-
return
|
|
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,
|
|
560
|
-
return
|
|
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:
|
|
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 (
|
|
636
|
-
throw new SLError(`sl.sdf("${kind}") takes at most ${most} parameters and was given ${
|
|
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
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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]
|
|
172
|
-
case SLOP.TURBULENCE: return octaveCall(2, a[0]
|
|
173
|
-
case SLOP.RIDGED: return octaveCall(3, a[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],
|
|
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
|
|
194
|
-
const
|
|
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,
|
|
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
|
-
|
|
208
|
-
|
|
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(", ")})`
|