onejs-sl 0.2.1 → 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 +26 -0
- package/README.md +61 -53
- package/goldens/goldens.json +98 -63
- package/lib/common.hlsl +5 -6
- package/package.json +2 -4
- package/src/body.ts +35 -12
- package/src/compile.ts +3 -4
- package/src/core.ts +1 -1
- package/src/hlsl.ts +17 -21
- package/src/index.ts +2 -4
- package/src/ir.ts +138 -46
- package/src/lang/README.md +11 -15
- package/src/lang/builtins.ts +10 -23
- package/src/lang/check.ts +24 -11
- package/src/lang/glsl.ts +1 -2
- package/src/lang/index.ts +1 -1
- package/src/lang/lower.ts +31 -37
- package/src/lang/parser.ts +2 -2
- package/src/lang/prelude.ts +2 -2
- package/src/ops.ts +15 -44
- package/src/params.ts +2 -0
- package/src/serial.ts +66 -14
- package/src/sl.ts +113 -84
- package/src/web.ts +50 -18
- package/src/encode.ts +0 -376
- package/src/limits.ts +0 -50
- package/src/vm.ts +0 -11
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"
|
|
@@ -207,7 +207,7 @@ function align2(a: Val, o: Num): [NodeRef, NodeRef] {
|
|
|
207
207
|
* right for two operands because `bin` takes the max itself; the three and four
|
|
208
208
|
* operand ops used to take the width from one chosen operand, so
|
|
209
209
|
* `mix(0, aVec3, t)` produced a node typed float that held three components.
|
|
210
|
-
* The VM
|
|
210
|
+
* The VM, which kept every value in a float4 register, never noticed, while the
|
|
211
211
|
* HLSL emitter declared `float` and truncated, so the two backends rendered
|
|
212
212
|
* different pictures from one program. Anything component wise with more than
|
|
213
213
|
* two operands goes through here.
|
|
@@ -288,13 +288,12 @@ 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(),
|
|
295
295
|
textures: b.textures.slice(),
|
|
296
296
|
hash: hashProgram(nodes, out.ref, b.uniforms, b.textures),
|
|
297
|
-
loops: b.loops.slice(),
|
|
298
297
|
}
|
|
299
298
|
} finally {
|
|
300
299
|
current = null
|
|
@@ -312,23 +311,14 @@ function compose(width: SLType, parts: Num[]): Val {
|
|
|
312
311
|
// Wide parts are SPLIT into their components here, so COMPOSE only ever
|
|
313
312
|
// sees scalars.
|
|
314
313
|
//
|
|
315
|
-
// The VM
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
// that has no room for them, and would put the complexity in the half that
|
|
320
|
-
// is hardest to test. Splitting at record time costs a few extra swizzle
|
|
321
|
-
// nodes, which the register allocator reclaims immediately, and leaves the
|
|
322
|
-
// VM's COMPOSE trivially correct.
|
|
314
|
+
// The VM (removed in 0.3.0) took the x of each operand, which dropped
|
|
315
|
+
// everything after the first component: vec4(uv, 0, 1) rendered
|
|
316
|
+
// (uv.x, 0, 1, 0). Splitting at record time costs a few extra swizzle
|
|
317
|
+
// nodes and keeps every emitter's COMPOSE trivial.
|
|
323
318
|
// A constructor of literals is ONE constant, not a COMPOSE of four.
|
|
324
319
|
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
// all live. The IR has always been able to say it in one node: a const
|
|
328
|
-
// carries up to four components. A two stop `ramp` used five of the eight
|
|
329
|
-
// registers on nothing but its own stop colours, which put the language's
|
|
330
|
-
// headline colour tool within one or two registers of unusable in any
|
|
331
|
-
// program that also did something.
|
|
320
|
+
// Every part as its own CONST, joined by a COMPOSE, is five nodes where the
|
|
321
|
+
// IR can say it in one: a const carries up to four components.
|
|
332
322
|
if (parts.length === width && parts.every((p) => typeof p === "number")) {
|
|
333
323
|
return mk(b, b.constant(parts as number[]), width)
|
|
334
324
|
}
|
|
@@ -425,9 +415,8 @@ export function hsv2rgb(c: Vec3): Vec3 {
|
|
|
425
415
|
/**
|
|
426
416
|
* Cross and reflect, both vec3 only.
|
|
427
417
|
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
* other widths here is what keeps the two honest.
|
|
418
|
+
* Refusing the other widths here keeps every backend drawing the same thing,
|
|
419
|
+
* rather than each deciding what a vec2 or vec4 cross product means.
|
|
431
420
|
*/
|
|
432
421
|
export function cross(a: Vec3, b: Vec3): Vec3 {
|
|
433
422
|
if (a.width !== TYPE.VEC3 || b.width !== TYPE.VEC3) {
|
|
@@ -452,7 +441,7 @@ export function reflect(incident: Vec3, normal: Vec3): Vec3 {
|
|
|
452
441
|
*
|
|
453
442
|
* A MACRO, like `ramp`, and for the same reason: it expands into arithmetic
|
|
454
443
|
* both backends already have, so it needs no opcode, no second implementation
|
|
455
|
-
* in the
|
|
444
|
+
* in the emitters. `SLOP.REMAP` stays a reserved number.
|
|
456
445
|
*
|
|
457
446
|
* Unclamped on purpose, which is what HLSL authors expect of the one liner they
|
|
458
447
|
* would otherwise write; wrap it in `saturate` when the ends matter.
|
|
@@ -495,11 +484,10 @@ export function select(cond: Num, whenTrue: Num, whenFalse: Num): Val {
|
|
|
495
484
|
const t = typeof whenTrue === "number" ? float(whenTrue) : whenTrue
|
|
496
485
|
const f = typeof whenFalse === "number" ? float(whenFalse) : whenFalse
|
|
497
486
|
// The condition is widened with the branches, not left at its own width.
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
// failure this design cannot tolerate.
|
|
487
|
+
// The VM (removed in 0.3.0) held a scalar condition as (c, 0, 0, 0) and
|
|
488
|
+
// answered 0 for components y, z and w, while HLSL broadcasts a scalar
|
|
489
|
+
// itself; the two disagreed on every vector valued select. Widening here
|
|
490
|
+
// keeps every emitter's SELECT the same simple form.
|
|
503
491
|
const { refs, width } = alignN([c, t, f])
|
|
504
492
|
return mk(b, b.call(SLOP.SELECT, width, refs), width)
|
|
505
493
|
}
|
|
@@ -507,8 +495,8 @@ export function select(cond: Num, whenTrue: Num, whenFalse: Num): Val {
|
|
|
507
495
|
/**
|
|
508
496
|
* The interpolant is broadcast to the operand width before it crosses.
|
|
509
497
|
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
498
|
+
* The VM (removed in 0.3.0) held a scalar `t` as (t, 0, 0, 0) and its lerp
|
|
499
|
+
* ran per component against those zeros:
|
|
512
500
|
* a black to white ramp at its midpoint rendered (0.5, 0, 0, 1) instead of
|
|
513
501
|
* grey. Broadcasting here rather than in the shader keeps `t` free to be a
|
|
514
502
|
* genuine per component vector when an author wants one.
|
|
@@ -545,6 +533,10 @@ export function uniformDefaults(p: Program): number[] {
|
|
|
545
533
|
* fields `fx.noise` draws: a value or simplex base, layered as fBm, or the
|
|
546
534
|
* turbulence and ridged variants built on simplex. A program has no seed;
|
|
547
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.
|
|
548
540
|
*/
|
|
549
541
|
export function noise(p: Vec2): Float {
|
|
550
542
|
return mk(p.owner, p.owner.call(SLOP.NOISE, TYPE.FLOAT, [p.ref]), TYPE.FLOAT)
|
|
@@ -552,24 +544,31 @@ export function noise(p: Vec2): Float {
|
|
|
552
544
|
export function simplex(p: Vec2): Float {
|
|
553
545
|
return mk(p.owner, p.owner.call(SLOP.SIMPLEX, TYPE.FLOAT, [p.ref]), TYPE.FLOAT)
|
|
554
546
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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}`)
|
|
558
557
|
}
|
|
559
|
-
|
|
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)
|
|
560
560
|
}
|
|
561
561
|
/** Layered noise. `base` picks the grid ("value", the default) or triangles ("simplex"). */
|
|
562
|
-
export function fbm(p: Vec2,
|
|
563
|
-
|
|
564
|
-
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])
|
|
565
564
|
}
|
|
566
565
|
/** Sum of |simplex| octaves: creases that stack into veins and licks. Fire, smoke, marble. */
|
|
567
|
-
export function turbulence(p: Vec2,
|
|
568
|
-
return
|
|
566
|
+
export function turbulence(p: Vec2, octaveCount: Num = 3): Float {
|
|
567
|
+
return octaves(SLOP.TURBULENCE, "turbulence", p, octaveCount, [])
|
|
569
568
|
}
|
|
570
569
|
/** The turbulence crease made bright and squared: ridges, lightning, cracks. */
|
|
571
|
-
export function ridged(p: Vec2,
|
|
572
|
-
return
|
|
570
|
+
export function ridged(p: Vec2, octaveCount: Num = 3): Float {
|
|
571
|
+
return octaves(SLOP.RIDGED, "ridged", p, octaveCount, [])
|
|
573
572
|
}
|
|
574
573
|
|
|
575
574
|
/** "#rgb", "#rrggbb" or "#rrggbbaa" to 0..1 components. Shared with fx. */
|
|
@@ -593,18 +592,23 @@ export function parseColor(hex: string): [number, number, number, number] {
|
|
|
593
592
|
*
|
|
594
593
|
* This is the argument for the EDSL in miniature. A library function that
|
|
595
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.
|
|
596
601
|
*/
|
|
597
|
-
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 {
|
|
598
603
|
if (stops.length < 2) throw new SLError(`a ramp needs at least 2 stops, got ${stops.length}`)
|
|
599
604
|
const tv = (typeof t === "number" ? float(t) : t).saturate()
|
|
600
|
-
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
// live until the last mix, and a four stop ramp reserved four of the VM's
|
|
605
|
-
// eight registers before the program did anything. That put the language's
|
|
606
|
-
// headline colour tool a couple of registers from unusable.
|
|
605
|
+
const written = stops.map((s) => (s instanceof Val ? asWritten(s) : null))
|
|
606
|
+
// Each stop is built immediately before the mix that consumes it. The graph
|
|
607
|
+
// and the hash are the same either way (the hash is over the shape, not the
|
|
608
|
+
// storage order); the order only mattered to the VM's register allocator.
|
|
607
609
|
const colourAt = (i: number) => {
|
|
610
|
+
const w = written[i]
|
|
611
|
+
if (w !== null && w !== undefined) return w
|
|
608
612
|
const v = typeof stops[i] === "string" ? parseColor(stops[i] as string) : (stops[i] as number[])
|
|
609
613
|
return vec4(v[0], v[1], v[2], v[3])
|
|
610
614
|
}
|
|
@@ -622,6 +626,18 @@ export function ramp(t: Num, stops: Array<string | [number, number, number, numb
|
|
|
622
626
|
return toLinear(out)
|
|
623
627
|
}
|
|
624
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
|
+
|
|
625
641
|
/**
|
|
626
642
|
* Signed distance to a shape, positive outside and negative inside.
|
|
627
643
|
*
|
|
@@ -637,27 +653,45 @@ export function ramp(t: Num, stops: Array<string | [number, number, number, numb
|
|
|
637
653
|
*
|
|
638
654
|
* A shape takes up to `SL_SDF_PARAMS[kind]` parameters, six at most. Fewer
|
|
639
655
|
* fills zeros, which is all a program could pass before six shapes grew their
|
|
640
|
-
* 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.
|
|
641
658
|
*
|
|
642
659
|
* Which parameters a shape takes is the shape's own business; `circle` wants a
|
|
643
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.
|
|
644
665
|
*/
|
|
645
|
-
export function sdf(kind: SlSdfKind, p: Vec2, params:
|
|
666
|
+
export function sdf(kind: SlSdfKind, p: Vec2, params: Num[] = []): Float {
|
|
646
667
|
const id = SL_SDF_SHAPES[kind]
|
|
647
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))
|
|
648
670
|
// At least four, so a program that passed a shape more than it reads, which
|
|
649
671
|
// was harmless when every shape took four, still builds.
|
|
650
672
|
const most = Math.max(4, SL_SDF_PARAMS[kind])
|
|
651
|
-
if (
|
|
652
|
-
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}`)
|
|
653
675
|
}
|
|
654
|
-
for (const v of
|
|
655
|
-
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}`)
|
|
656
678
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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"))
|
|
661
695
|
}
|
|
662
696
|
|
|
663
697
|
/** Distance to the nearest point of a jittered lattice. Cells, cracks, scales. */
|
|
@@ -712,7 +746,14 @@ export const uniform = {
|
|
|
712
746
|
}
|
|
713
747
|
|
|
714
748
|
export interface Texture {
|
|
749
|
+
/** The colour at `uv`, filtered as the texture's own settings say. */
|
|
715
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
|
|
716
757
|
}
|
|
717
758
|
|
|
718
759
|
export function texture(name: string): Texture {
|
|
@@ -723,6 +764,15 @@ export function texture(name: string): Texture {
|
|
|
723
764
|
if (uv.owner !== b) throw new SLError("a value from another program cannot be used in this one")
|
|
724
765
|
return mk(b, b.call(SLOP.SAMPLE, TYPE.VEC4, [uv.ref], [slot]), TYPE.VEC4)
|
|
725
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
|
+
},
|
|
726
776
|
}
|
|
727
777
|
}
|
|
728
778
|
|
|
@@ -733,34 +783,13 @@ export function texture(name: string): Texture {
|
|
|
733
783
|
*
|
|
734
784
|
* Honest about what it is: a macro, not a loop. It covers fbm, layered noise and
|
|
735
785
|
* small iterated distance fields, which is most of what 2D shaders loop for. A
|
|
736
|
-
*
|
|
737
|
-
* bounded loop with a dynamic trip count while codegen would handle it fine, and
|
|
738
|
-
* the two backends agreeing is the property this whole design protects.
|
|
786
|
+
* loop with a runtime count is `Specs/SL_NEXT.md` proposal 3b.
|
|
739
787
|
*/
|
|
740
788
|
export function repeat<T extends Val>(n: number, body: (i: number, acc: T) => T, seed: T): T {
|
|
741
789
|
if (!Number.isInteger(n) || n < 0 || n > 64) {
|
|
742
790
|
throw new SLError(`repeat count must be a whole number from 0 to 64, got ${n}`)
|
|
743
791
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
return acc
|
|
748
|
-
})
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
/**
|
|
752
|
-
* Records the nodes a body produced as one unrolled span.
|
|
753
|
-
*
|
|
754
|
-
* Diagnostic only: the instruction ceiling error uses the span to say which
|
|
755
|
-
* loop spent the budget, so the fix reads as "lower this count" rather than
|
|
756
|
-
* "fewer instructions". `repeat` is the EDSL's form of it and the text
|
|
757
|
-
* language's `for` is the other, and neither should be reaching into the
|
|
758
|
-
* Builder to push a span itself.
|
|
759
|
-
*/
|
|
760
|
-
export function unrolled<T>(count: number, body: () => T): T {
|
|
761
|
-
const b = ctx()
|
|
762
|
-
const start = b.nodes.length
|
|
763
|
-
const out = body()
|
|
764
|
-
b.loops.push({ count, start, end: b.nodes.length })
|
|
765
|
-
return out
|
|
792
|
+
let acc = seed
|
|
793
|
+
for (let i = 0; i < n; i++) acc = body(i, acc)
|
|
794
|
+
return acc
|
|
766
795
|
}
|
package/src/web.ts
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
* The web backends: prints a program as GLSL ES 3.00 or WGSL, for a browser to
|
|
3
3
|
* compile at runtime on Unity's own graphics device.
|
|
4
4
|
*
|
|
5
|
-
* Unity cannot compile a shader in a built game,
|
|
6
|
-
* A browser can, and the Play container
|
|
7
|
-
* so a program becomes WGSL on one and
|
|
8
|
-
*
|
|
9
|
-
* 600 times cheaper per pixel. The host that compiles and draws these lives in
|
|
5
|
+
* Unity cannot compile a shader in a built game, so a native build ships the
|
|
6
|
+
* shaders the editor generated. A browser can compile, and the Play container
|
|
7
|
+
* runs on WebGPU with WebGL2 behind it, so a program becomes WGSL on one and
|
|
8
|
+
* GLSL ES on the other. The host that compiles and draws these lives in
|
|
10
9
|
* OneJS (`Plugins/WebGL/OneJSSLWeb.jslib`); this file only prints them.
|
|
11
10
|
*
|
|
12
11
|
* Structurally the HLSL emitter again: one local per reachable node, in order,
|
|
@@ -53,6 +52,11 @@ function emitWeb(p: Program, lang: WebLanguage): string {
|
|
|
53
52
|
const emitted = new Set<number>()
|
|
54
53
|
const T = (t: SLType) => (W ? (t === 1 ? "f32" : `vec${t}f`) : t === 1 ? "float" : `vec${t}`)
|
|
55
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
|
+
}
|
|
56
60
|
|
|
57
61
|
/** A node's value widened to `t` when it is a scalar and `t` is not. */
|
|
58
62
|
const splat = (ref: number, t: SLType) => (typeOf(ref) === 1 && t > 1 ? `${T(t)}(n${ref})` : `n${ref}`)
|
|
@@ -159,7 +163,7 @@ function emitWeb(p: Program, lang: WebLanguage): string {
|
|
|
159
163
|
case SLOP.MIX: return `mix(${s[0]}, ${s[1]}, ${s[2]})`
|
|
160
164
|
case SLOP.STEP: return `step(${s[0]}, ${s[1]})`
|
|
161
165
|
case SLOP.SMOOTHSTEP: smoothsteps.add(t); return `sl_smoothstep${t}(${s[0]}, ${s[1]}, ${s[2]})`
|
|
162
|
-
// As the
|
|
166
|
+
// As the HLSL emitter does it: branchless, so every
|
|
163
167
|
// backend agrees on the edge value.
|
|
164
168
|
case SLOP.SELECT: {
|
|
165
169
|
const c = typeOf(n.args[0])
|
|
@@ -169,44 +173,72 @@ function emitWeb(p: Program, lang: WebLanguage): string {
|
|
|
169
173
|
case SLOP.HSV2RGB: return `${lib("sl_hsv2rgb")}(${a[0]})`
|
|
170
174
|
case SLOP.NOISE: return `${lib("sl_valueNoise")}(${a[0]})`
|
|
171
175
|
case SLOP.SIMPLEX: return `${lib("sl_simplex")}(${a[0]})`
|
|
172
|
-
case SLOP.FBM: return octaveCall(Math.round(imm[1] ?? 0), a[0]
|
|
173
|
-
case SLOP.TURBULENCE: return octaveCall(2, a[0]
|
|
174
|
-
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])
|
|
175
179
|
case SLOP.VORONOI: return `${lib("sl_voronoi")}(${a[0]})`
|
|
176
|
-
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())
|
|
177
181
|
case SLOP.SAMPLE: {
|
|
178
182
|
const slot = Math.round(imm[0] ?? 0)
|
|
179
183
|
sampled.add(slot)
|
|
180
184
|
return W ? `textureSample(sl_tex${slot}, sl_samp${slot}, ${a[0]})` : `texture(sl_Tex${slot}, ${a[0]})`
|
|
181
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
|
+
}
|
|
182
193
|
default:
|
|
183
194
|
throw new SLError(
|
|
184
195
|
`the ${lang.toUpperCase()} emitter has no case for opcode ${n.op}. A program using it would ` +
|
|
185
|
-
`differ between
|
|
196
|
+
`differ between a browser and a Unity build, which is the one failure this design cannot tolerate.`,
|
|
186
197
|
)
|
|
187
198
|
}
|
|
188
199
|
|
|
189
200
|
/**
|
|
190
201
|
* The library's onejsFbmKind, resolved here since the kind is a
|
|
191
202
|
* constant: calling the dispatcher would print a WGSL `select` that
|
|
192
|
-
* 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.
|
|
193
206
|
*/
|
|
194
|
-
function octaveCall(kind: number, pt: string
|
|
195
|
-
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)`
|
|
196
212
|
const fn = kind === 2 ? "onejsTurbulence" : kind === 3 ? "onejsRidged" : kind === 1 ? "onejsFbmSimplex" : "onejsFbm"
|
|
197
213
|
return `${lib(fn, ["float2", "float", "int", "float", "float"])}(${pt}, 0.0, ${o}, 2.0, 0.5)`
|
|
198
214
|
}
|
|
199
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
|
+
|
|
200
229
|
/** The shape is a constant, so this calls it directly instead of sl_sdfDistance's switch. */
|
|
201
|
-
function sdfCall(id: number, pt: string,
|
|
230
|
+
function sdfCall(id: number, pt: string, v: Array<{ text: string; value?: number }>): string {
|
|
202
231
|
const shape = SDF_CALLS[id]
|
|
203
232
|
// sl_sdfDistance returns 1e6 for an id it does not know.
|
|
204
233
|
if (shape === undefined) return lit(1e6)
|
|
205
234
|
const fn = lib(shape.fn)
|
|
206
|
-
const v = [im[1] ?? 0, im[2] ?? 0, im[3] ?? 0, im[4] ?? 0, im[5] ?? 0, im[6] ?? 0]
|
|
207
235
|
const args = shape.args.map((arg) => {
|
|
208
|
-
|
|
209
|
-
|
|
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)
|
|
210
242
|
return parts.length === 1 ? parts[0] : `${T(parts.length as SLType)}(${parts.join(", ")})`
|
|
211
243
|
})
|
|
212
244
|
return `${fn}(${[pt, ...args].join(", ")})`
|