onejs-sl 0.9.2 → 0.9.3

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.3
4
+
5
+ A hex colour is a float3 wherever a float3 is wanted, and `lookRay(eye, target, lens)` gives the ray through this pixel from a camera. Every program that compiled before keeps its hash.
6
+
7
+ - `lookRay(eye, target, lens)` returns the ray through this pixel from a y up camera at `eye` looking at `target`, as Unity's camera casts it; `lens` is `0.5 / tan(fov / 2)`
8
+ - A hex colour is a float3 wherever a float3 is wanted: returned, passed, in `float3(...)`, or beside a float3 in an op, a choice or a builtin
9
+ - A program that compiled before keeps its hash
10
+
3
11
  ## 0.9.2
4
12
 
5
13
  A fractional `tex2Dlod` level now weighs each level's colour by its alpha, so a transparent level no longer darkens the other. Every program keeps its hash; a host's generated shaders take the new text on their next build.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "generatedBy": "onejs-sl goldens/run.mjs",
3
- "package": "0.9.2",
3
+ "package": "0.9.3",
4
4
  "ir": 6,
5
5
  "target": "rgba8unorm-srgb: straight alpha, 8 bit, sRGB encoded, the stored bytes of a Linear OneJS game's element",
6
6
  "size": [
package/lib/noise2d.hlsl CHANGED
@@ -9,7 +9,7 @@
9
9
  // is the kind of thing nobody notices until two effects that should match do
10
10
  // not.
11
11
 
12
- // MARK: hash
12
+ // hash
13
13
  //
14
14
  // The lattice hash is integer arithmetic, and has to stay integer arithmetic.
15
15
  // A float hash, frac of a large product as most shader noise is written, is
@@ -108,7 +108,7 @@ float onejsFbm(float2 p, float seed, int octaves)
108
108
  return onejsFbm(p, seed, octaves, 2.0, 0.5);
109
109
  }
110
110
 
111
- // MARK: simplex
111
+ // simplex
112
112
  //
113
113
  // Ashima/McEwan 2D simplex. Worth carrying next to the value noise because the
114
114
  // two fail differently: value noise interpolates a square grid, so at high
@@ -167,7 +167,7 @@ float onejsFbmSimplex(float2 p, float seed, int octaves, float lacunarity, float
167
167
  return sum / max(norm, 1e-4);
168
168
  }
169
169
 
170
- // MARK: turbulence and ridged
170
+ // turbulence and ridged
171
171
  //
172
172
  // fBm above sums signed octaves, so octaves cancel as often as they add and the
173
173
  // field reads as cloud. Turbulence sums the ABSOLUTE value of each octave: the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onejs-sl",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "The OneJS shader language: a small typed language for per pixel programs, compiled to a portable IR and emitted as HLSL, WGSL or GLSL ES",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -125,6 +125,14 @@ constant 7 before the IR sees it. `sin(0.5)` is two instructions, because
125
125
  folding it would mean a second implementation of `sin` in JavaScript, and a
126
126
  second implementation is somewhere for the two backends to disagree.
127
127
 
128
+ **A hex colour takes the width its place wants.** Alone, `#ff8040` is a
129
+ float4. Where a float3 is wanted (a float3's declaration, a float3 function's
130
+ return, a float3 parameter, `float3(#ff8040)`, the other side of an op or a
131
+ choice, or a builtin's other arguments) it is that float3, its alpha dropped.
132
+ A choice between two hexes counts as one. A computed float4 does not fit a
133
+ float3 this way: that is still an error, since its alpha may be meant
134
+ (`writtenHex` and `asWidth` in `lower.ts`; `hex-width.test.ts`).
135
+
128
136
  **A hex default makes a uniform a colour.** `uniform float4 tint = #ff8040;`
129
137
  stores the sRGB components as its default, which is what the host sets and what
130
138
  the generated shader's Properties block shows, and every read of `tint` goes
package/src/lang/check.ts CHANGED
@@ -243,7 +243,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
243
243
  }
244
244
  })
245
245
 
246
- // MARK: bodies
246
+ // bodies
247
247
 
248
248
  // The prelude's own bodies are checked once, where it is built, against a
249
249
  // unit that has no uniforms of its own. Re-checking them here would let a
@@ -492,7 +492,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
492
492
  return `this can never run: every way through the ${above} above ends in a return, break or continue`
493
493
  }
494
494
 
495
- // MARK: expressions
495
+ // expressions
496
496
 
497
497
  function checkExpr(fn: FuncDecl, e: Expr, scope: Set<string>): void {
498
498
  switch (e.k) {
package/src/lang/lower.ts CHANGED
@@ -317,7 +317,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
317
317
  })
318
318
  }
319
319
 
320
- // MARK: declarations
320
+ // declarations
321
321
 
322
322
  function declareUniform(name: string, type: TypeName, c: number[], control: UniformControl): Val {
323
323
  switch (type) {
@@ -429,7 +429,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
429
429
  }
430
430
  }
431
431
 
432
- // MARK: functions
432
+ // functions
433
433
 
434
434
  /** A function's body run to its return, in `scope`, which holds its parameters. */
435
435
  function callBody(fn: FuncDecl, scope: Scope): LV {
@@ -442,7 +442,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
442
442
  return frame.retval
443
443
  }
444
444
 
445
- // MARK: statements
445
+ // statements
446
446
 
447
447
  /**
448
448
  * Runs a body. What it does to the locals is in their bindings and what it
@@ -570,7 +570,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
570
570
  case "switch": exec([switchAsIf(s, scope)], scope, frame); break
571
571
  case "block": exec(s.body, new Scope(scope), frame); break
572
572
  case "return": {
573
- const v = lowerExpr(s.value, scope, want(frame.ret))
573
+ const v = asWidth(lowerExpr(s.value, scope, want(frame.ret)), s.value, frame.ret.width, s.pos)
574
574
  frame.retval = returned(v, frame, s.pos)
575
575
  frame.returned = true
576
576
  break
@@ -606,6 +606,25 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
606
606
  b.value = fit(v, b, name, s.pos)
607
607
  }
608
608
 
609
+ /**
610
+ * A colour written as a hex, `#ff8000`, or a choice between two. It has
611
+ * no width of its own: alone it is a float4, and where a float3 is wanted
612
+ * (a float3's declaration, return or argument, the other side of an op or
613
+ * a choice, a builtin's other arguments) it is that float3, its alpha
614
+ * dropped. Before, only a declaration dropped it, and returning one from a
615
+ * float3 function, passing one, or mixing one with a float3 was an error
616
+ * (learn Mac, 6 Nov 2026, lessons 7 and 8).
617
+ */
618
+ function writtenHex(e: Expr): boolean {
619
+ return e.k === "hex" || (e.k === "cond" && writtenHex(e.then) && writtenHex(e.else))
620
+ }
621
+
622
+ /** `v`, lowered from `e`, as the float3 a place wants when `e` is a written hex; otherwise as it is. */
623
+ function asWidth(v: LV, e: Expr, width: number, pos: Pos): LV {
624
+ if (width !== 3 || widthOf(v) !== 4 || !writtenHex(e)) return v
625
+ return at(pos, () => asVal(v).swz("xyz") as unknown as Val)
626
+ }
627
+
609
628
  /** A return's value as the function's type. Only the kind converts; a width has to match. */
610
629
  function returned(v: LV, frame: Frame, pos: Pos): LV {
611
630
  const fn = frame.fn
@@ -621,7 +640,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
621
640
  return fit(v, t, `${fn.name}'s result`, pos)
622
641
  }
623
642
 
624
- // MARK: loops
643
+ // loops
625
644
 
626
645
  function runLoop(s: Extract<Stmt, { k: "for" | "while" }>, scope: Scope, frame: Frame): void {
627
646
  const loopScope = new Scope(scope)
@@ -765,7 +784,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
765
784
  return r === undefined ? null : { min: r.min, max: r.max }
766
785
  }
767
786
 
768
- // MARK: switch
787
+ // switch
769
788
 
770
789
  /**
771
790
  * A switch as the if chain it means: `x == 1 || x == 2` per case, in order,
@@ -801,7 +820,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
801
820
  return out[0] ?? { k: "if", cond: { k: "bool", value: true, pos: s.pos }, then: [], else: [], pos: s.pos }
802
821
  }
803
822
 
804
- // MARK: conversions
823
+ // conversions
805
824
 
806
825
  /**
807
826
  * A value as a declared type, as a declaration, an assignment and a
@@ -919,7 +938,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
919
938
  return b.width === 1 ? parts[0]! : at(pos, () => compose(b.width, parts))
920
939
  }
921
940
 
922
- // MARK: expressions
941
+ // expressions
923
942
 
924
943
  /**
925
944
  * `want` is the kind the value is going into, an int or a uint, which makes
@@ -993,6 +1012,8 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
993
1012
 
994
1013
  /** A binary op on operands already lowered from `ea` and `eb`. */
995
1014
  function combine(op: BinaryOp, a: LV, b: LV, ea: Expr, eb: Expr, scope: Scope, pos: Pos): LV {
1015
+ a = asWidth(a, ea, widthOf(b), pos)
1016
+ b = asWidth(b, eb, widthOf(a), pos)
996
1017
  // A whole number beside an int is an int: `i < 10`, `i % 3`.
997
1018
  if (isIntLike(a) && typeof b === "number" && wholeExpr(eb)) b = lowerExpr(eb, scope, kindWant(a))
998
1019
  if (isIntLike(b) && typeof a === "number" && wholeExpr(ea)) a = lowerExpr(ea, scope, kindWant(b))
@@ -1134,6 +1155,8 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
1134
1155
  if (cond instanceof Test) void cond.float
1135
1156
  let t = lowerExpr(e.then, scope, want)
1136
1157
  let f = lowerExpr(e.else, scope, want)
1158
+ t = asWidth(t, e.then, widthOf(f), e.pos)
1159
+ f = asWidth(f, e.else, widthOf(t), e.pos)
1137
1160
  if (isIntLike(t) && typeof f === "number" && wholeExpr(e.else)) f = lowerExpr(e.else, scope, kindWant(t))
1138
1161
  if (isIntLike(f) && typeof t === "number" && wholeExpr(e.then)) t = lowerExpr(e.then, scope, kindWant(f))
1139
1162
  const kinded = [t, f].some((v) => typeof v === "bigint" || v instanceof Uint || typeof v === "boolean" || (v instanceof Val && v.kind !== undefined))
@@ -1163,7 +1186,11 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
1163
1186
  const n = (callee as Extract<Expr, { k: "ident" }>).name
1164
1187
 
1165
1188
  if (n === "int" || n === "uint" || n === "bool") return convert(n, e, scope)
1166
- if (n in TYPE_WIDTH) return construct(n as TypeName, floats(e.args), e.pos)
1189
+ if (n in TYPE_WIDTH) {
1190
+ // float3(#ff8000) is the colour's rgb, as a float3 declared from it is.
1191
+ const one = e.args.length === 1 ? asWidth(toFloat(lowerExpr(e.args[0]!, scope), e.args[0]!.pos), e.args[0]!, TYPE_WIDTH[n as TypeName], e.pos) : null
1192
+ return construct(n as TypeName, one !== null ? [one as number | Val] : floats(e.args), e.pos)
1193
+ }
1167
1194
 
1168
1195
  if (n === "textureSize" || n === "tex2Dfetch") {
1169
1196
  const tex = samplers.get((e.args[0] as Extract<Expr, { k: "ident" }>).name)!
@@ -1210,6 +1237,9 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
1210
1237
  const builtin = BUILTINS[n]
1211
1238
  if (builtin !== undefined) {
1212
1239
  let args = e.args.map((a) => lowerExpr(a, scope))
1240
+ // A hex among float3s is a float3: lerp(#ff8000, sky, t).
1241
+ const others = Math.max(0, ...args.filter((_, i) => !writtenHex(e.args[i]!)).map(widthOf))
1242
+ args = args.map((v, i) => asWidth(v, e.args[i]!, others, e.args[i]!.pos))
1213
1243
  // min, max, abs and clamp keep ints ints; everything else takes floats.
1214
1244
  if (INT_BUILTINS.has(n) && args.some(isIntLike) && args.every((v, i) => isIntLike(v) || (typeof v === "number" && wholeExpr(e.args[i]!)))) {
1215
1245
  const k = kindWant(args.find(isIntLike)!)
@@ -1264,7 +1294,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
1264
1294
  const arg = e.args[i]!
1265
1295
  // In the caller's scope, where the argument is written; as the
1266
1296
  // parameter's kind, so a whole number passed to an int is one.
1267
- const v = lowerExpr(arg, caller, want(t))
1297
+ const v = asWidth(lowerExpr(arg, caller, want(t)), arg, t.width, arg.pos)
1268
1298
  const got = widthOf(v)
1269
1299
  if (got !== t.width) {
1270
1300
  fail(
@@ -1307,7 +1337,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
1307
1337
  }
1308
1338
  }
1309
1339
 
1310
- // MARK: tables and small helpers
1340
+ // tables and small helpers
1311
1341
 
1312
1342
  type ArithOp = "+" | "-" | "*" | "/" | "%"
1313
1343
  type BitOp = "&" | "|" | "^" | "<<" | ">>"
@@ -82,7 +82,7 @@ class Parser {
82
82
  private readonly errors?: SLParseError[],
83
83
  ) {}
84
84
 
85
- // MARK: token plumbing
85
+ // token plumbing
86
86
 
87
87
  private peek(n = 0): Token { return this.tokens[Math.min(this.i + n, this.tokens.length - 1)]! }
88
88
  private next(): Token { return this.tokens[this.i++]! }
@@ -107,7 +107,7 @@ class Parser {
107
107
  this.errors.push(e)
108
108
  }
109
109
 
110
- // MARK: recovery
110
+ // recovery
111
111
 
112
112
  /**
113
113
  * Runs one statement's or one declaration's parse. Collecting, an error is
@@ -184,7 +184,7 @@ class Parser {
184
184
  this.fail(`"${t.text}" is not a type; the types are float, float2, float3, float4, int, uint and bool`, t)
185
185
  }
186
186
 
187
- // MARK: the file
187
+ // the file
188
188
 
189
189
  parseUnit(): Unit {
190
190
  const unit: Unit = {
@@ -328,7 +328,7 @@ class Parser {
328
328
  return { name: name.text, ret, params, body, pos: start, prelude: false }
329
329
  }
330
330
 
331
- // MARK: statements
331
+ // statements
332
332
 
333
333
  private parseBlock(): Stmt[] {
334
334
  this.expect("{", "to open a body")
@@ -538,7 +538,7 @@ class Parser {
538
538
  return { k: "switch", value, cases, pos: kw }
539
539
  }
540
540
 
541
- // MARK: expressions
541
+ // expressions
542
542
 
543
543
  parseExpr(): Expr { return this.parseBinary(0) }
544
544
 
@@ -40,6 +40,14 @@ float2 tile(float2 p, float n) {
40
40
  float3 palette(float t, float3 a, float3 b, float3 c, float3 d) {
41
41
  return a + b * cos(6.28318530718 * (c * t + d));
42
42
  }
43
+
44
+ // This pixel's ray from eye toward target, as Unity's camera casts it. lens is 0.5 / tan(fov / 2).
45
+ float3 lookRay(float3 eye, float3 target, float lens) {
46
+ float3 forward = normalize(target - eye);
47
+ float3 right = normalize(cross(float3(0, 1, 0), forward));
48
+ float3 up = cross(forward, right);
49
+ return normalize(forward * lens + right * centered.x + up * centered.y);
50
+ }
43
51
  `
44
52
 
45
53
  /** Each prelude function's name, in the order the source declares them. */
package/src/sl.ts CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  import { SLOP, SL_HLSL, SL_NAME, type SLOpCode } from "./ops"
32
32
  import { SL_SDF_PARAMS, SL_SDF_SHAPES, type SlSdfKind } from "./shapes"
33
33
 
34
- // MARK: values
34
+ // values
35
35
 
36
36
  type Swz = "x" | "y" | "z" | "w" | "r" | "g" | "b" | "a"
37
37
  const CHAN: Record<string, number> = { x: 0, y: 1, z: 2, w: 3, r: 0, g: 1, b: 2, a: 3 }
@@ -202,7 +202,7 @@ function withKind<T extends SLNode>(n: T, kind: SLKind | undefined): T {
202
202
  return kind === undefined ? n : { ...n, kind }
203
203
  }
204
204
 
205
- // MARK: recording context
205
+ // recording context
206
206
 
207
207
  let current: Builder | null = null
208
208
 
@@ -288,7 +288,7 @@ function un(op: SLOpCode, a: Val): Val {
288
288
  return mk(a.owner, a.owner.call(op, a.width, [a.ref]), a.width)
289
289
  }
290
290
 
291
- // MARK: kinds (IR 4)
291
+ // kinds (IR 4)
292
292
 
293
293
  /** The ops an int or a uint keeps its kind through. Anything else takes it as a float. */
294
294
  const INT_OPS = new Set<number>([SLOP.ADD, SLOP.SUB, SLOP.MUL, SLOP.DIV, SLOP.MOD, SLOP.MIN, SLOP.MAX, SLOP.CLAMP])
@@ -486,7 +486,7 @@ function typed(b: Builder, v: Num, t: ValType): NodeRef {
486
486
  return v.ref
487
487
  }
488
488
 
489
- // MARK: the public surface
489
+ // the public surface
490
490
 
491
491
  export interface ProgramInputs {
492
492
  uv: Vec2