onejs-sl 0.3.0 → 0.5.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 +29 -0
- package/README.md +70 -28
- package/goldens/goldens.json +147 -63
- package/package.json +1 -1
- package/src/body.ts +132 -26
- package/src/core.ts +1 -1
- package/src/hlsl.ts +7 -3
- package/src/ir.ts +293 -33
- package/src/lang/README.md +2 -2
- package/src/lang/ast.ts +37 -7
- package/src/lang/builtins.ts +5 -17
- package/src/lang/check.ts +179 -70
- package/src/lang/glsl.ts +6 -10
- package/src/lang/lexer.ts +30 -6
- package/src/lang/lower.ts +1034 -289
- package/src/lang/parser.ts +146 -68
- package/src/lang/words.ts +5 -6
- package/src/ops.ts +48 -4
- package/src/params.ts +2 -0
- package/src/serial.ts +100 -17
- package/src/sl.ts +343 -33
- package/src/structure.ts +285 -0
- package/src/web.ts +152 -33
package/src/sl.ts
CHANGED
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
|
|
25
25
|
import { parseColor as parseHex } from "./color"
|
|
26
26
|
import {
|
|
27
|
-
Builder, INPUTS,
|
|
28
|
-
type InputName, type NodeRef, type Program, type SLNode, type SLType, type UniformControl,
|
|
27
|
+
Builder, INPUTS, RAMP_STOP_COMPUTED, SLError, TYPE, hashProgram, programVersion, truncateHeld, widthName, writtenColour,
|
|
28
|
+
type InputName, type NodeRef, type Program, type SLKind, type SLNode, type SLType, type UniformControl,
|
|
29
29
|
} from "./ir"
|
|
30
|
-
import { SLOP, type SLOpCode } from "./ops"
|
|
30
|
+
import { SLOP, SL_HLSL, SL_NAME, type SLOpCode } from "./ops"
|
|
31
31
|
import { SL_SDF_PARAMS, SL_SDF_SHAPES, type SlSdfKind } from "./shapes"
|
|
32
32
|
|
|
33
33
|
// MARK: values
|
|
@@ -55,7 +55,8 @@ export type Num = Val | number
|
|
|
55
55
|
// point rather than a hazard; see the comment above that loop.
|
|
56
56
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
|
57
57
|
export class Val {
|
|
58
|
-
|
|
58
|
+
/** `kind` is what the value holds beside its width: an int, a uint or a bool. None means floats. */
|
|
59
|
+
constructor(readonly owner: Builder, readonly ref: NodeRef, readonly width: SLType, readonly kind?: SLKind) {}
|
|
59
60
|
|
|
60
61
|
// Arithmetic. Mixing a vector with a Float broadcasts, as in HLSL.
|
|
61
62
|
add(o: Num): this { return bin(SLOP.ADD, this, o) as this }
|
|
@@ -73,6 +74,11 @@ export class Val {
|
|
|
73
74
|
min(o: Num): this { return bin(SLOP.MIN, this, o) as this }
|
|
74
75
|
max(o: Num): this { return bin(SLOP.MAX, this, o) as this }
|
|
75
76
|
clamp(lo: Num, hi: Num): this {
|
|
77
|
+
if ([this, lo, hi].some((v) => typeof v !== "number" && v.kind !== undefined)) {
|
|
78
|
+
const u = unify([this, lo, hi])
|
|
79
|
+
if (u.kind !== undefined) return mk(u.owner, u.owner.call(SLOP.CLAMP, u.width, u.refs, undefined, u.kind), u.width, u.kind) as this
|
|
80
|
+
return (mk(u.owner, u.refs[0]!, u.width) as Val).clamp(mk(u.owner, u.refs[1]!, u.width) as Val, mk(u.owner, u.refs[2]!, u.width) as Val) as this
|
|
81
|
+
}
|
|
76
82
|
const { refs, width } = alignN([this, lo, hi])
|
|
77
83
|
return mk(this.owner, this.owner.call(SLOP.CLAMP, width, refs), width) as this
|
|
78
84
|
}
|
|
@@ -89,6 +95,27 @@ export class Val {
|
|
|
89
95
|
}
|
|
90
96
|
normalize(): this { return un(SLOP.NORMALIZE, this) as this }
|
|
91
97
|
|
|
98
|
+
// Comparisons give a bool per component; an int meeting a float is compared as a float.
|
|
99
|
+
lt(o: Num): Bool { return compare(SLOP.LT, this, o) }
|
|
100
|
+
le(o: Num): Bool { return compare(SLOP.LE, this, o) }
|
|
101
|
+
gt(o: Num): Bool { return compare(SLOP.GT, this, o) }
|
|
102
|
+
ge(o: Num): Bool { return compare(SLOP.GE, this, o) }
|
|
103
|
+
eq(o: Num): Bool { return compare(SLOP.EQ, this, o) }
|
|
104
|
+
ne(o: Num): Bool { return compare(SLOP.NE, this, o) }
|
|
105
|
+
|
|
106
|
+
// Logic, on bools.
|
|
107
|
+
and(o: Val | boolean): Bool { return logic(SLOP.AND, this, o) }
|
|
108
|
+
or(o: Val | boolean): Bool { return logic(SLOP.OR, this, o) }
|
|
109
|
+
not(): Bool { return logic(SLOP.NOT, this) }
|
|
110
|
+
|
|
111
|
+
// Bits, on ints and uints. A shift takes its count modulo 32 on every backend.
|
|
112
|
+
band(o: Num): this { return bits(SLOP.BIT_AND, this, o) as this }
|
|
113
|
+
bor(o: Num): this { return bits(SLOP.BIT_OR, this, o) as this }
|
|
114
|
+
bxor(o: Num): this { return bits(SLOP.BIT_XOR, this, o) as this }
|
|
115
|
+
shl(o: Num): this { return bits(SLOP.SHL, this, o) as this }
|
|
116
|
+
shr(o: Num): this { return bits(SLOP.SHR, this, o) as this }
|
|
117
|
+
bnot(): this { return bits(SLOP.BIT_NOT, this) as this }
|
|
118
|
+
|
|
92
119
|
/**
|
|
93
120
|
* Arbitrary swizzle. The common single and full width ones are also getters
|
|
94
121
|
* (`p.x`, `c.rgb`); this covers everything else and is typed by its argument,
|
|
@@ -106,7 +133,7 @@ export class Val {
|
|
|
106
133
|
chans.push(c)
|
|
107
134
|
}
|
|
108
135
|
const t = chans.length as SLType
|
|
109
|
-
return mk(this.owner, this.owner.add({ k: "swizzle", type: t, src: this.ref, chans }), t) as Widen<S>
|
|
136
|
+
return mk(this.owner, this.owner.add(withKind({ k: "swizzle", type: t, src: this.ref, chans }, this.kind)), t, this.kind) as Widen<S>
|
|
110
137
|
}
|
|
111
138
|
|
|
112
139
|
get x(): Float { return this.swz("x") }
|
|
@@ -160,9 +187,18 @@ export interface Float extends Val { readonly width: 1 }
|
|
|
160
187
|
export interface Vec2 extends Val { readonly width: 2 }
|
|
161
188
|
export interface Vec3 extends Val { readonly width: 3 }
|
|
162
189
|
export interface Vec4 extends Val { readonly width: 4 }
|
|
190
|
+
/** A value holding a bool, an int or a uint rather than floats. */
|
|
191
|
+
export interface Bool extends Val { readonly width: 1; readonly kind: "bool" }
|
|
192
|
+
export interface Int extends Val { readonly width: 1; readonly kind: "int" }
|
|
193
|
+
export interface UInt extends Val { readonly width: 1; readonly kind: "uint" }
|
|
194
|
+
|
|
195
|
+
function mk(b: Builder, ref: NodeRef, width: SLType, kind?: SLKind): any {
|
|
196
|
+
return new Val(b, ref, width, kind)
|
|
197
|
+
}
|
|
163
198
|
|
|
164
|
-
|
|
165
|
-
|
|
199
|
+
/** A node with its kind, which a float node leaves out so it keys and hashes as it always did. */
|
|
200
|
+
function withKind<T extends SLNode>(n: T, kind: SLKind | undefined): T {
|
|
201
|
+
return kind === undefined ? n : { ...n, kind }
|
|
166
202
|
}
|
|
167
203
|
|
|
168
204
|
// MARK: recording context
|
|
@@ -233,19 +269,222 @@ function alignN(vs: Num[]): { refs: NodeRef[]; width: SLType } {
|
|
|
233
269
|
|
|
234
270
|
/** float -> vecN by repeating the component. */
|
|
235
271
|
function broadcast(v: Val, width: SLType): NodeRef {
|
|
236
|
-
return v.owner.add({ k: "swizzle", type: width, src: v.ref, chans: new Array(width).fill(0) })
|
|
272
|
+
return v.owner.add(withKind({ k: "swizzle", type: width, src: v.ref, chans: new Array(width).fill(0) }, v.kind))
|
|
237
273
|
}
|
|
238
274
|
|
|
239
275
|
function bin(op: SLOpCode, a: Val, o: Num): Val {
|
|
276
|
+
if (a.kind !== undefined || (typeof o !== "number" && o.kind !== undefined)) return kindedBin(op, a, o)
|
|
240
277
|
const [x, y] = align2(a, o)
|
|
241
278
|
const width = typeof o === "number" ? a.width : (Math.max(a.width, o.width) as SLType)
|
|
242
279
|
return mk(a.owner, a.owner.call(op, width, [x, y]), width)
|
|
243
280
|
}
|
|
244
281
|
|
|
245
282
|
function un(op: SLOpCode, a: Val): Val {
|
|
283
|
+
if (a.kind !== undefined) {
|
|
284
|
+
if ((op === SLOP.NEG || op === SLOP.ABS) && a.kind === "int") return mk(a.owner, a.owner.call(op, a.width, [a.ref], undefined, a.kind), a.width, a.kind)
|
|
285
|
+
a = toFloat(a)
|
|
286
|
+
}
|
|
246
287
|
return mk(a.owner, a.owner.call(op, a.width, [a.ref]), a.width)
|
|
247
288
|
}
|
|
248
289
|
|
|
290
|
+
// MARK: kinds (IR 4)
|
|
291
|
+
|
|
292
|
+
/** The ops an int or a uint keeps its kind through. Anything else takes it as a float. */
|
|
293
|
+
const INT_OPS = new Set<number>([SLOP.ADD, SLOP.SUB, SLOP.MUL, SLOP.DIV, SLOP.MOD, SLOP.MIN, SLOP.MAX, SLOP.CLAMP])
|
|
294
|
+
|
|
295
|
+
/** A value as floats: itself when it already is, a conversion otherwise (a bool is 0 or 1). */
|
|
296
|
+
export function toFloat(v: Val): Val {
|
|
297
|
+
if (v.kind === undefined) return v
|
|
298
|
+
return mk(v.owner, v.owner.call(SLOP.CAST, v.width, [v.ref]), v.width)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* The operands of an op brought to one kind and one width.
|
|
303
|
+
*
|
|
304
|
+
* Two ints stay ints, and a whole number beside one is an int too. Anything
|
|
305
|
+
* else meeting a float, a bool among them, becomes a float, as HLSL converts
|
|
306
|
+
* it. An int and a uint are refused, since which one wins is a guess.
|
|
307
|
+
*/
|
|
308
|
+
function unify(vs: Num[]): { refs: NodeRef[]; width: SLType; kind: SLKind | undefined; owner: Builder } {
|
|
309
|
+
let owner: Builder | null = null
|
|
310
|
+
const kinds = new Set<SLKind | undefined>()
|
|
311
|
+
let width: SLType = 1
|
|
312
|
+
for (const v of vs) {
|
|
313
|
+
if (typeof v === "number") continue
|
|
314
|
+
if (owner === null) owner = v.owner
|
|
315
|
+
else if (v.owner !== owner) throw new SLError("a value from another program cannot be used in this one")
|
|
316
|
+
kinds.add(v.kind)
|
|
317
|
+
if (v.width > width) width = v.width
|
|
318
|
+
}
|
|
319
|
+
const b = owner ?? ctx()
|
|
320
|
+
if (kinds.has("int") && kinds.has("uint")) {
|
|
321
|
+
throw new SLError("cannot combine an int with a uint; convert one with int(...) or uint(...)")
|
|
322
|
+
}
|
|
323
|
+
let kind = kinds.size === 1 ? [...kinds][0] : undefined
|
|
324
|
+
if (kind === "bool") kind = undefined
|
|
325
|
+
if (kind !== undefined && vs.some((v) => typeof v === "number" && !Number.isInteger(v))) kind = undefined
|
|
326
|
+
const refs = vs.map((v) => {
|
|
327
|
+
if (typeof v === "number") return b.constant(new Array(width).fill(v), kind)
|
|
328
|
+
const x = kind === undefined ? toFloat(v) : v
|
|
329
|
+
if (x.width === width) return x.ref
|
|
330
|
+
if (x.width === 1) return broadcast(x, width)
|
|
331
|
+
throw new SLError(`cannot combine a ${widthName(x.width)} with a ${widthName(width)}`)
|
|
332
|
+
})
|
|
333
|
+
return { refs, width, kind, owner: b }
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function kindedBin(op: SLOpCode, a: Val, o: Num): Val {
|
|
337
|
+
const { refs, width, kind, owner } = unify([a, o])
|
|
338
|
+
if (kind === undefined || !INT_OPS.has(op)) {
|
|
339
|
+
const x = mk(owner, refs[0]!, width) as Val
|
|
340
|
+
return bin(op, x, mk(owner, refs[1]!, width) as Val)
|
|
341
|
+
}
|
|
342
|
+
return mk(owner, owner.call(op, width, refs, undefined, kind), width, kind)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function compare(op: SLOpCode, a: Val, o: Num): Bool {
|
|
346
|
+
const { refs, width, kind, owner } = unify([a, o])
|
|
347
|
+
void kind
|
|
348
|
+
return mk(owner, owner.call(op, width, refs, undefined, "bool"), width, "bool")
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** A bool operand, a JS boolean made a constant. */
|
|
352
|
+
function asBool(b: Builder, v: Val | boolean, what: string): Val {
|
|
353
|
+
if (typeof v === "boolean") return mk(b, b.constant([v ? 1 : 0], "bool"), 1, "bool")
|
|
354
|
+
if (v.kind !== "bool") throw new SLError(`${what} takes bools, and this is ${v.kind === undefined ? `a ${widthName(v.width)}` : `an ${v.kind}`}; compare it, as in x > 0`)
|
|
355
|
+
return v
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function logic(op: SLOpCode, a: Val, o?: Val | boolean): Bool {
|
|
359
|
+
const name = op === SLOP.AND ? "&&" : op === SLOP.OR ? "||" : "!"
|
|
360
|
+
const x = asBool(a.owner, a, name)
|
|
361
|
+
if (o === undefined) return mk(a.owner, a.owner.call(op, x.width, [x.ref], undefined, "bool"), x.width, "bool")
|
|
362
|
+
const y = asBool(a.owner, o, name)
|
|
363
|
+
if (x.width !== y.width) throw new SLError(`cannot combine a bool${x.width} with a bool${y.width}`)
|
|
364
|
+
return mk(a.owner, a.owner.call(op, x.width, [x.ref, y.ref], undefined, "bool"), x.width, "bool")
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function bits(op: SLOpCode, a: Val, o?: Num): Val {
|
|
368
|
+
const word = SL_HLSL[op]?.syntax ?? SL_NAME[op]
|
|
369
|
+
const intOnly = (v: Num) => {
|
|
370
|
+
if (typeof v === "number" ? !Number.isInteger(v) : v.kind !== "int" && v.kind !== "uint") {
|
|
371
|
+
throw new SLError(`${word} works on the bits of an int or a uint, and this is ${typeof v === "number" ? "a fraction" : v.kind === "bool" ? "a bool; use && or ||" : `a ${widthName(v.width)}`}`)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
intOnly(a)
|
|
375
|
+
if (o === undefined) return mk(a.owner, a.owner.call(op, a.width, [a.ref], undefined, a.kind), a.width, a.kind)
|
|
376
|
+
intOnly(o)
|
|
377
|
+
// A shift's count is its own kind; the value shifted keeps its own.
|
|
378
|
+
if (op === SLOP.SHL || op === SLOP.SHR) {
|
|
379
|
+
const count = typeof o === "number" ? a.owner.constant([o], a.kind) : o.ref
|
|
380
|
+
return mk(a.owner, a.owner.call(op, a.width, [a.ref, count], undefined, a.kind), a.width, a.kind)
|
|
381
|
+
}
|
|
382
|
+
const { refs, width, kind, owner } = unify([a, o])
|
|
383
|
+
return mk(owner, owner.call(op, width, refs, undefined, kind), width, kind)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** A conversion to `kind`, or to floats when it is undefined. A float to an int truncates toward zero, held to the int's range. */
|
|
387
|
+
function convert(v: Num | boolean, kind: SLKind | undefined): Val {
|
|
388
|
+
const b = typeof v === "object" ? v.owner : ctx()
|
|
389
|
+
if (typeof v === "boolean") v = mk(b, b.constant([v ? 1 : 0], "bool"), 1, "bool") as Val
|
|
390
|
+
if (typeof v === "number") {
|
|
391
|
+
if (kind === undefined) return mk(b, b.constant([v]), 1)
|
|
392
|
+
if (kind === "bool") return mk(b, b.constant([v !== 0 ? 1 : 0], "bool"), 1, "bool")
|
|
393
|
+
// A whole number the kind holds is that number exactly (`747796405u`);
|
|
394
|
+
// anything else is a float converted, as the GPU would.
|
|
395
|
+
const exact = Number.isInteger(v) && (kind === "int" ? v >= -2147483648 && v <= 2147483647 : v >= 0 && v <= 4294967295)
|
|
396
|
+
return mk(b, b.constant([exact ? v : truncateHeld(v, kind)], kind), 1, kind)
|
|
397
|
+
}
|
|
398
|
+
if (v.kind === kind) return v
|
|
399
|
+
return mk(b, b.call(SLOP.CAST, v.width, [v.ref], undefined, kind), v.width, kind)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** An int: a whole number, or a value converted, a float truncating toward zero. */
|
|
403
|
+
export function int(v: Num | boolean): Int { return convert(v, "int") as Int }
|
|
404
|
+
export function uint(v: Num | boolean): UInt { return convert(v, "uint") as UInt }
|
|
405
|
+
/** A bool: true or false, or a value converted, where anything but zero is true. */
|
|
406
|
+
export function bool(v: Num | boolean): Bool { return convert(v, "bool") as Bool }
|
|
407
|
+
|
|
408
|
+
function choose(b: Builder, cond: Num | boolean, whenTrue: Num, whenFalse: Num): Val {
|
|
409
|
+
if (typeof cond === "boolean") return convertTo(cond ? whenTrue : whenFalse, whenTrue, whenFalse)
|
|
410
|
+
// What SELECT reads a float condition as, so the two agree.
|
|
411
|
+
const c = typeof cond === "number" ? asBool(b, cond >= 0.5, "a choice")
|
|
412
|
+
: cond.kind === "bool" ? cond : cond.kind === undefined ? cond.ge(0.5) : cond.ne(0)
|
|
413
|
+
if (c.width !== 1) throw new SLError("a choice takes one bool, and this is several; combine them with && or ||")
|
|
414
|
+
const { refs, width, kind, owner } = unify([whenTrue, whenFalse])
|
|
415
|
+
return mk(owner, owner.call(SLOP.CHOOSE, width, [c.ref, ...refs], undefined, kind), width, kind)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** `v` as the type the two would unify to, for a choice whose condition is known. */
|
|
419
|
+
function convertTo(v: Num, a: Num, c: Num): Val {
|
|
420
|
+
const { refs, width, kind, owner } = unify([a, c])
|
|
421
|
+
return mk(owner, v === a ? refs[0]! : refs[1]!, width, kind)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* A real branch: only the side `cond` picks runs, and its results come back.
|
|
426
|
+
* Both sides give the same number of results, pairwise of one type; a plain
|
|
427
|
+
* number takes its partner's. A value only one side needs is computed only when
|
|
428
|
+
* that side runs.
|
|
429
|
+
*/
|
|
430
|
+
export function branch(cond: Val | boolean, whenTrue: () => Num[], whenFalse: () => Num[]): Val[] {
|
|
431
|
+
const b = ctx()
|
|
432
|
+
if (typeof cond === "boolean") return (cond ? whenTrue() : whenFalse()).map((v) => (typeof v === "number" ? float(v) : v))
|
|
433
|
+
const c = asBool(b, cond, "a branch")
|
|
434
|
+
if (c.width !== 1) throw new SLError("a branch takes one bool, and this is several; combine them with && or ||")
|
|
435
|
+
const t = whenTrue()
|
|
436
|
+
const f = whenFalse()
|
|
437
|
+
if (t.length !== f.length) throw new SLError(`a branch's sides give ${t.length} and ${f.length} results; they must give the same`)
|
|
438
|
+
const types = t.map((x, i) => pairType(x, f[i]!, i))
|
|
439
|
+
if (types.length === 0) return []
|
|
440
|
+
const node = b.add({ k: "if", cond: c.ref, then: t.map((v, i) => typed(b, v, types[i]!)), else: f.map((v, i) => typed(b, v, types[i]!)) })
|
|
441
|
+
return types.map((ty, i) => mk(b, b.add(withKind({ k: "proj", type: ty.width, src: node, index: i }, ty.kind)), ty.width, ty.kind))
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* A real loop. The values start as `init`; while `cond` of them holds, and
|
|
446
|
+
* fewer than `max` turns have run, they become `body` of them. What comes back
|
|
447
|
+
* is the values when it stops. `max` is what guarantees it stops: a loop that
|
|
448
|
+
* reaches it leaves as if its condition had failed.
|
|
449
|
+
*/
|
|
450
|
+
export function loop(init: Num[], cond: (v: Val[]) => Val | boolean, body: (v: Val[]) => Num[], max = 1024): Val[] {
|
|
451
|
+
const b = ctx()
|
|
452
|
+
if (!Number.isInteger(max) || max < 1) throw new SLError(`a loop's turn limit is a whole number from 1, got ${max}`)
|
|
453
|
+
const start = init.map((v) => (typeof v === "number" ? (float(v) as Val) : v))
|
|
454
|
+
const id = b.newLoop()
|
|
455
|
+
const params = start.map((v, i) => mk(b, b.add(withKind({ k: "param", type: v.width, loop: id, index: i }, v.kind)), v.width, v.kind) as Val)
|
|
456
|
+
const c = asBool(b, cond(params), "a loop's condition")
|
|
457
|
+
if (c.width !== 1) throw new SLError("a loop's condition is one bool, and this is several; combine them with && or ||")
|
|
458
|
+
const next = body(params)
|
|
459
|
+
if (next.length !== params.length) throw new SLError(`a loop carries ${params.length} values and its body gave ${next.length}`)
|
|
460
|
+
const ty = params.map((p) => ({ width: p.width, kind: p.kind }))
|
|
461
|
+
const node = b.add({
|
|
462
|
+
k: "loop", id, init: start.map((v) => v.ref), cond: c.ref,
|
|
463
|
+
next: next.map((v, i) => { pairType(params[i]!, v, i); return typed(b, v, ty[i]!) }), max,
|
|
464
|
+
})
|
|
465
|
+
return ty.map((t, i) => mk(b, b.add(withKind({ k: "proj", type: t.width, src: node, index: i }, t.kind)), t.width, t.kind))
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
interface ValType { width: SLType; kind: SLKind | undefined }
|
|
469
|
+
|
|
470
|
+
/** The one type two results share, or why they do not. */
|
|
471
|
+
function pairType(a: Num, c: Num, i: number): ValType {
|
|
472
|
+
if (typeof a === "number" && typeof c === "number") return { width: 1, kind: undefined }
|
|
473
|
+
if (typeof a === "number") return { width: (c as Val).width, kind: (c as Val).kind }
|
|
474
|
+
if (typeof c === "number") return { width: a.width, kind: a.kind }
|
|
475
|
+
if (a.width !== c.width || a.kind !== c.kind) {
|
|
476
|
+
const name = (v: Val) => (v.kind === undefined ? widthName(v.width) : v.width === 1 ? v.kind : `${v.kind}${v.width}`)
|
|
477
|
+
throw new SLError(`result ${i} is a ${name(a)} one way and a ${name(c)} the other`)
|
|
478
|
+
}
|
|
479
|
+
return { width: a.width, kind: a.kind }
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** A result as a node of its type, a plain number becoming a constant. */
|
|
483
|
+
function typed(b: Builder, v: Num, t: ValType): NodeRef {
|
|
484
|
+
if (typeof v === "number") return b.constant(new Array(t.width).fill(v), t.kind)
|
|
485
|
+
return v.ref
|
|
486
|
+
}
|
|
487
|
+
|
|
249
488
|
// MARK: the public surface
|
|
250
489
|
|
|
251
490
|
export interface ProgramInputs {
|
|
@@ -288,7 +527,7 @@ export function program(fn: (inputs: ProgramInputs) => Vec4): Program {
|
|
|
288
527
|
}
|
|
289
528
|
const nodes: SLNode[] = b.nodes.slice()
|
|
290
529
|
return {
|
|
291
|
-
version:
|
|
530
|
+
version: programVersion(nodes, out.ref),
|
|
292
531
|
nodes,
|
|
293
532
|
result: out.ref,
|
|
294
533
|
uniforms: b.uniforms.slice(),
|
|
@@ -302,7 +541,8 @@ export function program(fn: (inputs: ProgramInputs) => Vec4): Program {
|
|
|
302
541
|
|
|
303
542
|
export function float(v: Num): Float {
|
|
304
543
|
const b = ctx()
|
|
305
|
-
|
|
544
|
+
if (typeof v !== "number") return (v.kind === undefined ? v : toFloat(v)) as Float
|
|
545
|
+
return mk(b, b.constant([v]), TYPE.FLOAT)
|
|
306
546
|
}
|
|
307
547
|
|
|
308
548
|
/** Builds a wider value from narrower parts, which must add up exactly. */
|
|
@@ -478,8 +718,12 @@ export function atan2(y: Num, x: Num): Float {
|
|
|
478
718
|
* rather than an `if`: a real branch would have to survive both backends
|
|
479
719
|
* identically, and this does not.
|
|
480
720
|
*/
|
|
481
|
-
export function select(cond: Num, whenTrue: Num, whenFalse: Num): Val {
|
|
721
|
+
export function select(cond: Num | boolean, whenTrue: Num, whenFalse: Num): Val {
|
|
482
722
|
const b = ctx()
|
|
723
|
+
const kinded = (v: Num | boolean) => typeof v === "object" && v.kind !== undefined
|
|
724
|
+
// A bool condition, or results that are not floats: a choice, which carries
|
|
725
|
+
// any kind, where SELECT's arithmetic carries only floats (IR 4).
|
|
726
|
+
if (typeof cond === "boolean" || kinded(cond) || kinded(whenTrue) || kinded(whenFalse)) return choose(b, cond, whenTrue, whenFalse)
|
|
483
727
|
const c = typeof cond === "number" ? float(cond) : cond
|
|
484
728
|
const t = typeof whenTrue === "number" ? float(whenTrue) : whenTrue
|
|
485
729
|
const f = typeof whenFalse === "number" ? float(whenFalse) : whenFalse
|
|
@@ -533,6 +777,10 @@ export function uniformDefaults(p: Program): number[] {
|
|
|
533
777
|
* fields `fx.noise` draws: a value or simplex base, layered as fBm, or the
|
|
534
778
|
* turbulence and ridged variants built on simplex. A program has no seed;
|
|
535
779
|
* offset the input for a different field. Octaves are 1 to 4, as in fx.
|
|
780
|
+
*
|
|
781
|
+
* The octave count may be a value, a uniform say, rounded to the nearest whole
|
|
782
|
+
* number and held to 1 to 4 where it is read. It is an operand either way
|
|
783
|
+
* (IR 3); a constant one is checked here, and prints as the number it is.
|
|
536
784
|
*/
|
|
537
785
|
export function noise(p: Vec2): Float {
|
|
538
786
|
return mk(p.owner, p.owner.call(SLOP.NOISE, TYPE.FLOAT, [p.ref]), TYPE.FLOAT)
|
|
@@ -540,24 +788,31 @@ export function noise(p: Vec2): Float {
|
|
|
540
788
|
export function simplex(p: Vec2): Float {
|
|
541
789
|
return mk(p.owner, p.owner.call(SLOP.SIMPLEX, TYPE.FLOAT, [p.ref]), TYPE.FLOAT)
|
|
542
790
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
791
|
+
/**
|
|
792
|
+
* An octave op: the point and the count as operands, with `rest` (fbm's kind)
|
|
793
|
+
* the immediates. A constant count is checked here.
|
|
794
|
+
*/
|
|
795
|
+
function octaves(op: SLOpCode, name: string, p: Vec2, count: Num, rest: number[]): Float {
|
|
796
|
+
const c = components(p.owner, count)
|
|
797
|
+
if (c.length !== 1) throw new SLError(`${name}'s octave count is one number, and this is a ${widthName(c.length as SLType)}`)
|
|
798
|
+
const n = c[0]!
|
|
799
|
+
if (typeof n === "number" && (!Number.isInteger(n) || n < 1 || n > 4)) {
|
|
800
|
+
throw new SLError(`${name} octaves must be a whole number from 1 to 4, got ${n}`)
|
|
546
801
|
}
|
|
547
|
-
|
|
802
|
+
const v = typeof n === "number" ? float(n) : n
|
|
803
|
+
return mk(p.owner, p.owner.call(op, TYPE.FLOAT, [p.ref, v.ref], rest), TYPE.FLOAT)
|
|
548
804
|
}
|
|
549
805
|
/** 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)
|
|
806
|
+
export function fbm(p: Vec2, octaveCount: Num = 3, base: "value" | "simplex" = "value"): Float {
|
|
807
|
+
return octaves(SLOP.FBM, "fbm", p, octaveCount, [base === "simplex" ? 1 : 0])
|
|
553
808
|
}
|
|
554
809
|
/** Sum of |simplex| octaves: creases that stack into veins and licks. Fire, smoke, marble. */
|
|
555
|
-
export function turbulence(p: Vec2,
|
|
556
|
-
return
|
|
810
|
+
export function turbulence(p: Vec2, octaveCount: Num = 3): Float {
|
|
811
|
+
return octaves(SLOP.TURBULENCE, "turbulence", p, octaveCount, [])
|
|
557
812
|
}
|
|
558
813
|
/** The turbulence crease made bright and squared: ridges, lightning, cracks. */
|
|
559
|
-
export function ridged(p: Vec2,
|
|
560
|
-
return
|
|
814
|
+
export function ridged(p: Vec2, octaveCount: Num = 3): Float {
|
|
815
|
+
return octaves(SLOP.RIDGED, "ridged", p, octaveCount, [])
|
|
561
816
|
}
|
|
562
817
|
|
|
563
818
|
/** "#rgb", "#rrggbb" or "#rrggbbaa" to 0..1 components. Shared with fx. */
|
|
@@ -581,14 +836,23 @@ export function parseColor(hex: string): [number, number, number, number] {
|
|
|
581
836
|
*
|
|
582
837
|
* This is the argument for the EDSL in miniature. A library function that
|
|
583
838
|
* composes from primitives costs one function here and nothing anywhere else.
|
|
839
|
+
*
|
|
840
|
+
* A stop is a colour as written: a hex, four numbers, or a value that is a
|
|
841
|
+
* colour as written, which is one `color`, `sl.uniform.colour` or a hex
|
|
842
|
+
* literal made (a colour uniform, say). The ramp blends the colours as
|
|
843
|
+
* written, so a value computed any other way, already in the working space,
|
|
844
|
+
* has nothing it could blend, and is refused.
|
|
584
845
|
*/
|
|
585
|
-
export function ramp(t: Num, stops: Array<string | [number, number, number, number]>): Vec4 {
|
|
846
|
+
export function ramp(t: Num, stops: Array<string | [number, number, number, number] | Vec3 | Vec4>): Vec4 {
|
|
586
847
|
if (stops.length < 2) throw new SLError(`a ramp needs at least 2 stops, got ${stops.length}`)
|
|
587
848
|
const tv = (typeof t === "number" ? float(t) : t).saturate()
|
|
849
|
+
const written = stops.map((s) => (s instanceof Val ? asWritten(s) : null))
|
|
588
850
|
// Each stop is built immediately before the mix that consumes it. The graph
|
|
589
851
|
// and the hash are the same either way (the hash is over the shape, not the
|
|
590
852
|
// storage order); the order only mattered to the VM's register allocator.
|
|
591
853
|
const colourAt = (i: number) => {
|
|
854
|
+
const w = written[i]
|
|
855
|
+
if (w !== null && w !== undefined) return w
|
|
592
856
|
const v = typeof stops[i] === "string" ? parseColor(stops[i] as string) : (stops[i] as number[])
|
|
593
857
|
return vec4(v[0], v[1], v[2], v[3])
|
|
594
858
|
}
|
|
@@ -606,6 +870,18 @@ export function ramp(t: Num, stops: Array<string | [number, number, number, numb
|
|
|
606
870
|
return toLinear(out)
|
|
607
871
|
}
|
|
608
872
|
|
|
873
|
+
/**
|
|
874
|
+
* A colour value as it was written, a float4: the operand of the `toLinear`
|
|
875
|
+
* that reading it as a colour applied, with alpha 1 when it has three
|
|
876
|
+
* components. Anything else was computed in the working space.
|
|
877
|
+
*/
|
|
878
|
+
function asWritten(v: Vec3 | Vec4): Vec4 {
|
|
879
|
+
const ref = writtenColour(v.owner.nodes, v.ref)
|
|
880
|
+
if (ref === null) throw new SLError(RAMP_STOP_COMPUTED)
|
|
881
|
+
const raw = mk(v.owner, ref, v.width) as Val
|
|
882
|
+
return v.width === TYPE.VEC4 ? (raw as Vec4) : vec4(raw, 1)
|
|
883
|
+
}
|
|
884
|
+
|
|
609
885
|
/**
|
|
610
886
|
* Signed distance to a shape, positive outside and negative inside.
|
|
611
887
|
*
|
|
@@ -621,27 +897,45 @@ export function ramp(t: Num, stops: Array<string | [number, number, number, numb
|
|
|
621
897
|
*
|
|
622
898
|
* A shape takes up to `SL_SDF_PARAMS[kind]` parameters, six at most. Fewer
|
|
623
899
|
* fills zeros, which is all a program could pass before six shapes grew their
|
|
624
|
-
* fifth and sixth.
|
|
900
|
+
* fifth and sixth. A vector parameter counts as its components, so a box's
|
|
901
|
+
* half extents can be one float2.
|
|
625
902
|
*
|
|
626
903
|
* Which parameters a shape takes is the shape's own business; `circle` wants a
|
|
627
904
|
* radius, `roundedBox` wants half extents and a corner. See `lib/sdf2d.hlsl`.
|
|
905
|
+
*
|
|
906
|
+
* A parameter may be any value, a uniform or one computed per pixel. The six
|
|
907
|
+
* are operands either way (IR 3), a float4 and a float2, and constant ones
|
|
908
|
+
* print as the numbers they are.
|
|
628
909
|
*/
|
|
629
|
-
export function sdf(kind: SlSdfKind, p: Vec2, params:
|
|
910
|
+
export function sdf(kind: SlSdfKind, p: Vec2, params: Num[] = []): Float {
|
|
630
911
|
const id = SL_SDF_SHAPES[kind]
|
|
631
912
|
if (id === undefined) throw new SLError(`"${kind}" is not a shape; see SL_SDF_SHAPES for the 42 names`)
|
|
913
|
+
const c = params.flatMap((v) => components(p.owner, v))
|
|
632
914
|
// At least four, so a program that passed a shape more than it reads, which
|
|
633
915
|
// was harmless when every shape took four, still builds.
|
|
634
916
|
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 ${
|
|
917
|
+
if (c.length > most) {
|
|
918
|
+
throw new SLError(`sl.sdf("${kind}") takes at most ${most} parameters and was given ${c.length}`)
|
|
637
919
|
}
|
|
638
|
-
for (const v of
|
|
639
|
-
if (!Number.isFinite(v)) throw new SLError(`sl.sdf parameters must be finite, got ${v}`)
|
|
920
|
+
for (const v of c) {
|
|
921
|
+
if (typeof v === "number" && !Number.isFinite(v)) throw new SLError(`sl.sdf parameters must be finite, got ${v}`)
|
|
640
922
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
923
|
+
const q = vec4(c[0] ?? 0, c[1] ?? 0, c[2] ?? 0, c[3] ?? 0)
|
|
924
|
+
const r = vec2(c[4] ?? 0, c[5] ?? 0)
|
|
925
|
+
return mk(p.owner, p.owner.call(SLOP.SDF, TYPE.FLOAT, [p.ref, q.ref, r.ref], [id]), TYPE.FLOAT)
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* A value as its components: numbers for a constant, which is what it would
|
|
930
|
+
* have been written as, and a float per component otherwise.
|
|
931
|
+
*/
|
|
932
|
+
function components(b: Builder, v: Num): Num[] {
|
|
933
|
+
if (typeof v === "number") return [v]
|
|
934
|
+
if (v.owner !== b) throw new SLError("a value from another program cannot be used in this one")
|
|
935
|
+
const n = b.nodes[v.ref]!
|
|
936
|
+
if (n.k === "const") return n.v.slice()
|
|
937
|
+
if (v.width === 1) return [v]
|
|
938
|
+
return Array.from({ length: v.width }, (_, i) => v.swz("xyzw"[i] as "x"))
|
|
645
939
|
}
|
|
646
940
|
|
|
647
941
|
/** Distance to the nearest point of a jittered lattice. Cells, cracks, scales. */
|
|
@@ -696,7 +990,14 @@ export const uniform = {
|
|
|
696
990
|
}
|
|
697
991
|
|
|
698
992
|
export interface Texture {
|
|
993
|
+
/** The colour at `uv`, filtered as the texture's own settings say. */
|
|
699
994
|
sample(uv: Vec2): Vec4
|
|
995
|
+
/**
|
|
996
|
+
* The colour at `uv` from mip level `lod`: 0 is the full size texture, 1
|
|
997
|
+
* half, and a fraction blends two levels where the texture's filter does.
|
|
998
|
+
* A texture with no mips has only level 0.
|
|
999
|
+
*/
|
|
1000
|
+
sampleLevel(uv: Vec2, lod: Num): Vec4
|
|
700
1001
|
}
|
|
701
1002
|
|
|
702
1003
|
export function texture(name: string): Texture {
|
|
@@ -707,6 +1008,15 @@ export function texture(name: string): Texture {
|
|
|
707
1008
|
if (uv.owner !== b) throw new SLError("a value from another program cannot be used in this one")
|
|
708
1009
|
return mk(b, b.call(SLOP.SAMPLE, TYPE.VEC4, [uv.ref], [slot]), TYPE.VEC4)
|
|
709
1010
|
},
|
|
1011
|
+
sampleLevel(uv: Vec2, lod: Num): Vec4 {
|
|
1012
|
+
if (uv.owner !== b) throw new SLError("a value from another program cannot be used in this one")
|
|
1013
|
+
const level = typeof lod === "number" ? b.constant([lod]) : lod
|
|
1014
|
+
if (typeof level !== "number") {
|
|
1015
|
+
if (level.owner !== b) throw new SLError("a value from another program cannot be used in this one")
|
|
1016
|
+
if (level.width !== 1) throw new SLError(`a mip level is one number, and this is a ${widthName(level.width)}`)
|
|
1017
|
+
}
|
|
1018
|
+
return mk(b, b.call(SLOP.SAMPLE_LOD, TYPE.VEC4, [uv.ref, typeof level === "number" ? level : level.ref], [slot]), TYPE.VEC4)
|
|
1019
|
+
},
|
|
710
1020
|
}
|
|
711
1021
|
}
|
|
712
1022
|
|