onejs-sl 0.1.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 +10 -0
- package/README.md +266 -0
- package/package.json +60 -0
- package/src/color.ts +16 -0
- package/src/core.ts +24 -0
- package/src/emit/unity.ts +3 -0
- package/src/emit/web.ts +3 -0
- package/src/encode.ts +462 -0
- package/src/hlsl.ts +275 -0
- package/src/index.ts +24 -0
- package/src/ir.ts +301 -0
- package/src/lang/README.md +167 -0
- package/src/lang/ast.ts +87 -0
- package/src/lang/builtins.ts +168 -0
- package/src/lang/check.ts +541 -0
- package/src/lang/index.ts +55 -0
- package/src/lang/lexer.ts +176 -0
- package/src/lang/lower.ts +628 -0
- package/src/lang/parser.ts +460 -0
- package/src/lang/prelude.ts +75 -0
- package/src/limits.ts +50 -0
- package/src/ops.ts +346 -0
- package/src/serial.ts +137 -0
- package/src/shapes.ts +49 -0
- package/src/sl.ts +721 -0
- package/src/tables.ts +15 -0
- package/src/vm.ts +11 -0
- package/src/web.ts +273 -0
- package/src/weblib.ts +1303 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# `.sl`: the shader language as a file
|
|
2
|
+
|
|
3
|
+
Phase A of `Specs/SL_TEXT.md`. Source text in, the same `Program` the EDSL
|
|
4
|
+
records out.
|
|
5
|
+
|
|
6
|
+
```hlsl
|
|
7
|
+
// plasma.sl
|
|
8
|
+
uniform float warp = 0.5;
|
|
9
|
+
uniform float hue = 0.5;
|
|
10
|
+
|
|
11
|
+
float4 main() {
|
|
12
|
+
float2 p = (uv - 0.5) * (warp * 14 + 2);
|
|
13
|
+
float v = sin(p.x + time) + sin(p.y - time * 0.8);
|
|
14
|
+
float n = saturate(v * 0.22 + 0.5);
|
|
15
|
+
return float4(hsv2rgb(float3(frac(hue + n * 0.18), 0.75, n)), 1);
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { parse } from "onejs-sl"
|
|
21
|
+
const plasma = parse(source, { file: "plasma.sl" })
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## The one idea
|
|
25
|
+
|
|
26
|
+
**It is not a second language.** Every construct lowers through the EDSL, so
|
|
27
|
+
`sin(x)` in a file is the same `sl.sin(x)` call a TypeScript author would have
|
|
28
|
+
written, and the two produce the same graph. `parity.test.ts` asserts that as
|
|
29
|
+
hash equality for every GPU fixture and for the example on play.onejs.com: same
|
|
30
|
+
hash means same generated shader and same pixels, established without rendering
|
|
31
|
+
anything. It also means the existing GPU fixtures, the codegen goldens and the
|
|
32
|
+
C# VM tests cover the text form for free, because they run on the IR.
|
|
33
|
+
|
|
34
|
+
If that property ever breaks, the failure is silent in the worst way: a program
|
|
35
|
+
whose hash does not match its generated shader falls back to the VM and nobody
|
|
36
|
+
is told. So parity is the test to keep green, not the parser's unit tests.
|
|
37
|
+
|
|
38
|
+
| File | What it does |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `lexer.ts` | Characters to tokens, each carrying line and column |
|
|
41
|
+
| `ast.ts` | The node shapes, all of them, which is not many |
|
|
42
|
+
| `parser.ts` | Pratt parser: shape only, HLSL precedence |
|
|
43
|
+
| `check.ts` | Declarations, names, statement shape, budgets, recursion |
|
|
44
|
+
| `lower.ts` | AST to IR through the EDSL: SSA, unrolling, `select`, inlining |
|
|
45
|
+
| `builtins.ts` | What each name does, keyed by the spelling `ops.ts` gives it |
|
|
46
|
+
| `prelude.ts` | The standard library, written in the language |
|
|
47
|
+
| `onejs-unity`'s `src/esbuild/sl.mjs` | The loader: `import plasma from "./plasma.sl"` |
|
|
48
|
+
| `../index.ts` and `../core.ts` | The package with the parser, and without it. See below |
|
|
49
|
+
|
|
50
|
+
## The loader
|
|
51
|
+
|
|
52
|
+
`slPlugin()`, in onejs-unity's `onejs-unity/esbuild`, parses and encodes at BUILD TIME, so an import resolves to a small
|
|
53
|
+
object of numbers and the bundle carries neither the parser nor the source. A
|
|
54
|
+
parse error becomes an esbuild error with the file, line and column, which the
|
|
55
|
+
Play editor surfaces and every terminal editor links.
|
|
56
|
+
|
|
57
|
+
It writes two things beside the code:
|
|
58
|
+
|
|
59
|
+
- **`<name>.sl.d.ts`**, the way a USS module gets one, carrying the uniform
|
|
60
|
+
names in the type. `uniforms={{ wrap: 1 }}` is then a call site error rather
|
|
61
|
+
than a console warning on a frame nobody is looking at.
|
|
62
|
+
- **`app.sl.json`** beside the bundle, the manifest `SLShaderGenerator` already
|
|
63
|
+
watches for. This is the thing the file format makes possible and the EDSL
|
|
64
|
+
cannot: a program in a file is known statically, so **an ejected game is
|
|
65
|
+
compiled from its first frame** rather than from its second. Runtime
|
|
66
|
+
recording stays for EDSL programs and for a build older than the loader; the
|
|
67
|
+
editor reads every `*.sl.json` it can find.
|
|
68
|
+
|
|
69
|
+
An empty manifest is written only over one that is already there. Deleting the
|
|
70
|
+
last `.sl` file has to stop its shaders being generated, and a project that has
|
|
71
|
+
never had one should not find a new file beside its bundle.
|
|
72
|
+
|
|
73
|
+
### Why the parser is compiled rather than imported
|
|
74
|
+
|
|
75
|
+
The parser is TypeScript and the plugin runs under plain Node: an app's
|
|
76
|
+
`esbuild.config.mjs` is executed by `node`, which cannot load a `.ts` file.
|
|
77
|
+
Every plugin in that folder has the same constraint, which is why the Tailwind
|
|
78
|
+
generator beside them is `.mjs`; a parser with two hundred tests is not going
|
|
79
|
+
to be maintained twice.
|
|
80
|
+
|
|
81
|
+
So the plugin compiles `sl/compiler.ts` once per process, with the esbuild
|
|
82
|
+
already running the build, and imports the result: one source of truth, no
|
|
83
|
+
generated artifact to go stale, about thirty milliseconds once. A Cloudflare
|
|
84
|
+
Worker cannot evaluate code it builds, so PlaySite imports the parser
|
|
85
|
+
statically and hands it in as `compiler`, and that path never runs there.
|
|
86
|
+
|
|
87
|
+
### Why there are two barrels
|
|
88
|
+
|
|
89
|
+
`onejs-sl/core` is what a **game** imports (through `onejs-unity/sl`), and the
|
|
90
|
+
Play eject scaffold vendors every file it reaches into the downloaded project.
|
|
91
|
+
`onejs-sl`, which adds this folder, is what a **build** imports (through
|
|
92
|
+
`onejs-unity/sl/compiler`). Re-exporting the parser from the first put two thousand
|
|
93
|
+
lines a played game never executes into every ejected project's source tree,
|
|
94
|
+
where they could only read as clutter. The scaffold vendors what an entry actually
|
|
95
|
+
reaches rather than every file beside it, so the split is enforced by the
|
|
96
|
+
eject's own test.
|
|
97
|
+
|
|
98
|
+
## Where the types are checked
|
|
99
|
+
|
|
100
|
+
In `lower.ts`, once, by the EDSL, which computes a width as it records. A
|
|
101
|
+
declaration is then an **assertion** against that width rather than an input to
|
|
102
|
+
inference. `check.ts` deliberately does no type inference: two implementations
|
|
103
|
+
of a type system are two type systems, and the one that would have been written
|
|
104
|
+
here is the one nothing else uses.
|
|
105
|
+
|
|
106
|
+
## What surprises people
|
|
107
|
+
|
|
108
|
+
**A scalar on the left of an operator broadcasts through a swizzle.** `uv * 8`
|
|
109
|
+
is `uv.mul(8)`, one constant node of two components. `8 * uv` is
|
|
110
|
+
`sl.float(8).mul(uv)`, a one component constant and a broadcast. Same picture,
|
|
111
|
+
different graph, different hash. Write the vector on the left when it matters.
|
|
112
|
+
|
|
113
|
+
**Literal arithmetic folds; a builtin call never does.** `2 * 3 + 1` is the
|
|
114
|
+
constant 7 before the IR sees it. `sin(0.5)` is two instructions, because
|
|
115
|
+
folding it would mean a second implementation of `sin` in JavaScript, and a
|
|
116
|
+
second implementation is somewhere for the two backends to disagree.
|
|
117
|
+
|
|
118
|
+
**A hex default makes a uniform a colour.** `uniform float4 tint = #ff8040;`
|
|
119
|
+
stores the sRGB components as its default, which is what the host sets and what
|
|
120
|
+
the generated shader's Properties block shows, and every read of `tint` goes
|
|
121
|
+
through `toLinear`. Without that rule, `#ff8040` written as a literal and
|
|
122
|
+
`#ff8040` written as a default would be two different colours in one file.
|
|
123
|
+
A default built out of numbers (`float4(1, 0.5, 0.25, 1)`) is not a colour and
|
|
124
|
+
is read unconverted.
|
|
125
|
+
|
|
126
|
+
**A uniform's default has to be written out.** Numbers, constructors of numbers
|
|
127
|
+
and colours, and nothing else. It is baked into the program before anything
|
|
128
|
+
runs, so it cannot name a const, which could itself name a uniform.
|
|
129
|
+
|
|
130
|
+
**Shadowing is refused.** A local may not take the name of an input, a uniform,
|
|
131
|
+
a texture, a const or a builtin. Allowing it would make "is this a texture?"
|
|
132
|
+
depend on where the question is asked, for no gain in a language whose
|
|
133
|
+
functions are six lines long. A file function may shadow a *prelude* function,
|
|
134
|
+
which is the sanctioned way to replace one.
|
|
135
|
+
|
|
136
|
+
## Not yet spellable
|
|
137
|
+
|
|
138
|
+
Three things the EDSL or the opcode table has and a file cannot say. Each is a
|
|
139
|
+
recorded gap, not an oversight, and each has its own error message rather than
|
|
140
|
+
"unknown identifier".
|
|
141
|
+
|
|
142
|
+
| | Why |
|
|
143
|
+
|---|---|
|
|
144
|
+
| `rgb2hsv`, `tex2Dlod` | The opcodes are numbered and **neither backend implements them**. Writing one would render as whatever the VM's dispatch falls through to and fail outright in the HLSL emitter. |
|
|
145
|
+
| `fbm`'s simplex base | `sl.fbm(p, octaves, "simplex")` picks the base with a string, and the language has no strings. `fbm(p, octaves)` is the value base, and `turbulence` and `ridged` are the simplex family. |
|
|
146
|
+
|
|
147
|
+
Related, and worth knowing: **the EDSL lets a program declare 15 textures and
|
|
148
|
+
the VM has 4.** `ir.ts` picked its ceiling from the WebGL2 sampler count rather
|
|
149
|
+
than from `FxProgram.shader`, which declares `_Tex0` to `_Tex3` and samples
|
|
150
|
+
`_Tex3` for every slot past it, so slots 4 and up are silently wrong in the
|
|
151
|
+
browser and correct after an eject. A `.sl` file is held to the real number, 4,
|
|
152
|
+
reported at the declaration. The EDSL's ceiling is untouched here because
|
|
153
|
+
lowering it changes recorded behaviour rather than parser behaviour.
|
|
154
|
+
|
|
155
|
+
## Errors
|
|
156
|
+
|
|
157
|
+
Every one carries file, line and column, and an error that starts inside the
|
|
158
|
+
EDSL keeps the EDSL's wording. `"z" is component 3 of a vec2, which has 2` is
|
|
159
|
+
already the right sentence; what it lacked was a place. `errors.test.ts` checks
|
|
160
|
+
the positions as strictly as the words, because a marker under the wrong
|
|
161
|
+
character sends the reader to look at something that is fine.
|
|
162
|
+
|
|
163
|
+
## See also
|
|
164
|
+
|
|
165
|
+
- `Specs/SL_TEXT.md`, the whole design, including the four decisions taken
|
|
166
|
+
- `../README.md`, the IR, the EDSL and the hash this lowers to
|
|
167
|
+
- `Specs/SHADER_LANG.md`, the backends
|
package/src/lang/ast.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AST the parser builds and the checker and lowering walk.
|
|
3
|
+
*
|
|
4
|
+
* Phase A of `Specs/SL_TEXT.md`. Deliberately small: a `.sl` file is one
|
|
5
|
+
* fragment function and its declarations, so there is no module system, no
|
|
6
|
+
* struct, no array and no statement that is not one of six shapes.
|
|
7
|
+
*
|
|
8
|
+
* Every node carries the position of the token it started at. That is what lets
|
|
9
|
+
* an error from the middle of lowering say which character to underline, and it
|
|
10
|
+
* is why the position lives on the node rather than being recovered by a second
|
|
11
|
+
* pass over the source.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Pos } from "./lexer"
|
|
15
|
+
|
|
16
|
+
/** The four value types, under their HLSL names. `texture2D` is not one: it declares a slot. */
|
|
17
|
+
export type TypeName = "float" | "float2" | "float3" | "float4"
|
|
18
|
+
|
|
19
|
+
export const TYPE_WIDTH: Record<TypeName, 1 | 2 | 3 | 4> = {
|
|
20
|
+
float: 1, float2: 2, float3: 3, float4: 4,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type BinaryOp =
|
|
24
|
+
| "+" | "-" | "*" | "/" | "%"
|
|
25
|
+
| "<" | "<=" | ">" | ">=" | "==" | "!="
|
|
26
|
+
| "&&" | "||"
|
|
27
|
+
|
|
28
|
+
export type Expr =
|
|
29
|
+
| { k: "num"; value: number; pos: Pos }
|
|
30
|
+
| { k: "hex"; hex: string; pos: Pos }
|
|
31
|
+
| { k: "ident"; name: string; pos: Pos }
|
|
32
|
+
/** A swizzle, or the shape half of `sdf.circle`. Which one is decided later. */
|
|
33
|
+
| { k: "member"; obj: Expr; name: string; pos: Pos }
|
|
34
|
+
| { k: "call"; callee: Expr; args: Expr[]; pos: Pos }
|
|
35
|
+
| { k: "unary"; op: "-" | "+" | "!"; arg: Expr; pos: Pos }
|
|
36
|
+
| { k: "binary"; op: BinaryOp; a: Expr; b: Expr; pos: Pos }
|
|
37
|
+
| { k: "cond"; cond: Expr; then: Expr; else: Expr; pos: Pos }
|
|
38
|
+
|
|
39
|
+
export type AssignOp = "=" | "+=" | "-=" | "*=" | "/=" | "%="
|
|
40
|
+
|
|
41
|
+
export type Stmt =
|
|
42
|
+
| { k: "var"; type: TypeName; name: string; init: Expr; pos: Pos }
|
|
43
|
+
/** A compile time constant: usable as a value and as a `for` bound. */
|
|
44
|
+
| { k: "const"; type: TypeName; name: string; init: Expr; pos: Pos }
|
|
45
|
+
| { k: "assign"; target: Expr; op: AssignOp; value: Expr; pos: Pos }
|
|
46
|
+
| { k: "if"; cond: Expr; then: Stmt[]; else: Stmt[]; pos: Pos }
|
|
47
|
+
| { k: "for"; counter: string; from: Expr; to: Expr; inclusive: boolean; step: Expr; body: Stmt[]; pos: Pos }
|
|
48
|
+
| { k: "return"; value: Expr; pos: Pos }
|
|
49
|
+
|
|
50
|
+
export interface Param {
|
|
51
|
+
type: TypeName
|
|
52
|
+
name: string
|
|
53
|
+
pos: Pos
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface FuncDecl {
|
|
57
|
+
name: string
|
|
58
|
+
ret: TypeName
|
|
59
|
+
params: Param[]
|
|
60
|
+
body: Stmt[]
|
|
61
|
+
pos: Pos
|
|
62
|
+
/** Prelude functions are shadowed by a function of the same name in the file. */
|
|
63
|
+
prelude: boolean
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface UniformDecl {
|
|
67
|
+
name: string
|
|
68
|
+
type: TypeName
|
|
69
|
+
/** Null when the declaration gave no default; the slot then starts at zero. */
|
|
70
|
+
init: Expr | null
|
|
71
|
+
pos: Pos
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface TextureDecl {
|
|
75
|
+
name: string
|
|
76
|
+
pos: Pos
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface Unit {
|
|
80
|
+
file: string
|
|
81
|
+
uniforms: UniformDecl[]
|
|
82
|
+
textures: TextureDecl[]
|
|
83
|
+
consts: Extract<Stmt, { k: "const" }>[]
|
|
84
|
+
funcs: FuncDecl[]
|
|
85
|
+
/** `float4 main()`. Exactly one per file, and the parser refuses a file without it. */
|
|
86
|
+
main: FuncDecl | null
|
|
87
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each builtin name does, keyed by the name `ops.ts` gives it.
|
|
3
|
+
*
|
|
4
|
+
* Phase A of `Specs/SL_TEXT.md` 3.4. THE NAMES ARE NOT WRITTEN HERE. Every key
|
|
5
|
+
* comes from `SL_HLSL`, the one column the EDSL and the parser share, so a new
|
|
6
|
+
* op cannot arrive with a spelling in one surface and not the other. A contract
|
|
7
|
+
* test (`lang/surface.test.ts`) walks the same table and refuses an op that has
|
|
8
|
+
* neither a lowering nor a stated reason to lack one.
|
|
9
|
+
*
|
|
10
|
+
* Everything lowers through the EDSL rather than through the Builder. That is
|
|
11
|
+
* the property the parity test rests on: `sin(x)` in a file and `sl.sin(x)` in
|
|
12
|
+
* TypeScript are the same call, so they cannot produce different graphs.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { SLError } from "../ir"
|
|
16
|
+
import { SLOP, SL_HLSL, SL_UNIMPLEMENTED } from "../ops"
|
|
17
|
+
import * as sl from "../sl"
|
|
18
|
+
import type { Num, Val } from "../sl"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* How a builtin consumes its arguments.
|
|
22
|
+
*
|
|
23
|
+
* `value` is the ordinary case: every argument is a value and the lowering gets
|
|
24
|
+
* them as `Num`, so a plain literal stays a literal and broadcasts the way the
|
|
25
|
+
* EDSL broadcasts it. `special` names the three whose arguments are not values
|
|
26
|
+
* at all: a texture slot, a shape's constant parameters, a list of colour
|
|
27
|
+
* stops. Those are lowered in `lower.ts`, where the AST is still in reach.
|
|
28
|
+
*/
|
|
29
|
+
export interface Builtin {
|
|
30
|
+
op: number
|
|
31
|
+
kind: "value" | "special"
|
|
32
|
+
min: number
|
|
33
|
+
max: number
|
|
34
|
+
/** Present for `kind: "value"`. */
|
|
35
|
+
lower?: (args: Num[]) => Val
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const name = (op: number): string => {
|
|
39
|
+
const call = SL_HLSL[op]?.call
|
|
40
|
+
if (call === undefined) throw new Error(`opcode ${op} has no call spelling in SL_HLSL`)
|
|
41
|
+
return call
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A plain number where a value is needed, for the ops whose first operand must be one. */
|
|
45
|
+
const val = (v: Num): Val => (typeof v === "number" ? (sl.float(v) as unknown as Val) : v)
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Asserts an operand's width where the two backends would otherwise part ways.
|
|
49
|
+
*
|
|
50
|
+
* The VM keeps every value in a float4 register and writes `a.xy` or `a.rgb`
|
|
51
|
+
* whatever it was handed, so a noise call on a float3 quietly works there. The
|
|
52
|
+
* generated HLSL passes the real type to `sl_valueNoise(float2)` and either
|
|
53
|
+
* truncates with a warning or fails to compile. Neither outcome is one an
|
|
54
|
+
* author should discover after ejecting, so the narrow ops say so here.
|
|
55
|
+
*/
|
|
56
|
+
function widthMustBe(fn: string, v: Num, want: 1 | 2 | 3 | 4, why: string): Val {
|
|
57
|
+
const got = typeof v === "number" ? 1 : v.width
|
|
58
|
+
if (got !== want) {
|
|
59
|
+
throw new SLError(`${fn} takes ${why}, and this is a ${WIDTH_NAME[got]}`)
|
|
60
|
+
}
|
|
61
|
+
return val(v)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const WIDTH_NAME: Record<number, string> = { 1: "float", 2: "float2", 3: "float3", 4: "float4" }
|
|
65
|
+
|
|
66
|
+
/** A colour operand: three components, or four with alpha along for the ride. */
|
|
67
|
+
function colourOperand(fn: string, v: Num): Val {
|
|
68
|
+
const got = typeof v === "number" ? 1 : v.width
|
|
69
|
+
if (got < 3) throw new SLError(`${fn} takes a colour, so a float3 or a float4, and this is a ${WIDTH_NAME[got]}`)
|
|
70
|
+
return val(v)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const table: Record<string, Builtin> = {}
|
|
74
|
+
|
|
75
|
+
function value(op: number, min: number, max: number, lower: (args: Num[]) => Val): void {
|
|
76
|
+
table[name(op)] = { op, kind: "value", min, max, lower }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function special(op: number, min: number, max: number): void {
|
|
80
|
+
table[name(op)] = { op, kind: "special", min, max }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Width preserving one argument ops, which the EDSL exposes as free functions. */
|
|
84
|
+
const UNARY: Array<[number, (v: Num) => Val]> = [
|
|
85
|
+
[SLOP.SIN, (v) => sl.sin(val(v))],
|
|
86
|
+
[SLOP.COS, (v) => sl.cos(val(v))],
|
|
87
|
+
[SLOP.TAN, (v) => sl.tan(val(v))],
|
|
88
|
+
[SLOP.ASIN, (v) => sl.asin(val(v))],
|
|
89
|
+
[SLOP.ACOS, (v) => sl.acos(val(v))],
|
|
90
|
+
[SLOP.EXP, (v) => sl.exp(val(v))],
|
|
91
|
+
[SLOP.LOG, (v) => sl.log(val(v))],
|
|
92
|
+
[SLOP.SQRT, (v) => sl.sqrt(val(v))],
|
|
93
|
+
[SLOP.SIGN, (v) => sl.sign(val(v))],
|
|
94
|
+
[SLOP.CEIL, (v) => sl.ceil(val(v))],
|
|
95
|
+
[SLOP.ROUND, (v) => sl.round(val(v))],
|
|
96
|
+
[SLOP.TO_LINEAR, (v) => sl.toLinear(val(v))],
|
|
97
|
+
[SLOP.ABS, (v) => val(v).abs()],
|
|
98
|
+
[SLOP.FLOOR, (v) => val(v).floor()],
|
|
99
|
+
[SLOP.FRACT, (v) => val(v).fract()],
|
|
100
|
+
[SLOP.SATURATE, (v) => val(v).saturate()],
|
|
101
|
+
[SLOP.RECIP, (v) => val(v).recip()],
|
|
102
|
+
[SLOP.NORMALIZE, (v) => val(v).normalize()],
|
|
103
|
+
]
|
|
104
|
+
for (const [op, fn] of UNARY) value(op, 1, 1, (a) => fn(a[0]!))
|
|
105
|
+
|
|
106
|
+
value(SLOP.POW, 2, 2, (a) => val(a[0]!).pow(a[1]!))
|
|
107
|
+
value(SLOP.MIN, 2, 2, (a) => val(a[0]!).min(a[1]!))
|
|
108
|
+
value(SLOP.MAX, 2, 2, (a) => val(a[0]!).max(a[1]!))
|
|
109
|
+
value(SLOP.CLAMP, 3, 3, (a) => val(a[0]!).clamp(a[1]!, a[2]!))
|
|
110
|
+
value(SLOP.ATAN2, 2, 2, (a) => sl.atan2(a[0]!, a[1]!))
|
|
111
|
+
|
|
112
|
+
value(SLOP.LENGTH, 1, 1, (a) => val(a[0]!).length())
|
|
113
|
+
value(SLOP.DISTANCE, 2, 2, (a) => val(a[0]!).distance(a[1]!))
|
|
114
|
+
value(SLOP.DOT, 2, 2, (a) => val(a[0]!).dot(a[1]!))
|
|
115
|
+
value(SLOP.CROSS, 2, 2, (a) => sl.cross(val(a[0]!) as never, val(a[1]!) as never))
|
|
116
|
+
value(SLOP.REFLECT, 2, 2, (a) => sl.reflect(val(a[0]!) as never, val(a[1]!) as never))
|
|
117
|
+
|
|
118
|
+
value(SLOP.MIX, 3, 3, (a) => sl.mix(a[0]!, a[1]!, a[2]!))
|
|
119
|
+
value(SLOP.STEP, 2, 2, (a) => sl.step(a[0]!, a[1]!))
|
|
120
|
+
value(SLOP.SMOOTHSTEP, 3, 3, (a) => sl.smoothstep(a[0]!, a[1]!, a[2]!))
|
|
121
|
+
value(SLOP.REMAP, 5, 5, (a) => sl.remap(a[0]!, a[1]!, a[2]!, a[3]!, a[4]!))
|
|
122
|
+
|
|
123
|
+
value(SLOP.HSV2RGB, 1, 1, (a) => sl.hsv2rgb(widthMustBe("hsv2rgb", a[0]!, 3, "a float3 of hue, saturation and value") as never))
|
|
124
|
+
value(SLOP.LUMINANCE, 1, 1, (a) => sl.luminance(colourOperand("luminance", a[0]!)))
|
|
125
|
+
|
|
126
|
+
value(SLOP.NOISE, 1, 1, (a) => sl.noise(point("noise", a[0]!) as never))
|
|
127
|
+
value(SLOP.SIMPLEX, 1, 1, (a) => sl.simplex(point("simplex", a[0]!) as never))
|
|
128
|
+
value(SLOP.VORONOI, 1, 1, (a) => sl.voronoi(point("voronoi", a[0]!) as never))
|
|
129
|
+
// The octave count is an immediate, so it has to be a number at parse time.
|
|
130
|
+
// `lower.ts` folds constants before it gets here and rejects anything else with
|
|
131
|
+
// a message that says why, rather than letting this cast be the error.
|
|
132
|
+
value(SLOP.FBM, 1, 2, (a) => sl.fbm(point("fbm", a[0]!) as never, octaves("fbm", a[1])))
|
|
133
|
+
value(SLOP.TURBULENCE, 1, 2, (a) => sl.turbulence(point("turbulence", a[0]!) as never, octaves("turbulence", a[1])))
|
|
134
|
+
value(SLOP.RIDGED, 1, 2, (a) => sl.ridged(point("ridged", a[0]!) as never, octaves("ridged", a[1])))
|
|
135
|
+
|
|
136
|
+
/** Every field is sampled at a float2. */
|
|
137
|
+
function point(fn: string, v: Num): Val {
|
|
138
|
+
return widthMustBe(fn, v, 2, "a float2 to sample at")
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
special(SLOP.SAMPLE, 2, 2)
|
|
142
|
+
special(SLOP.SDF, 1, 7)
|
|
143
|
+
special(SLOP.RAMP, 3, 64)
|
|
144
|
+
|
|
145
|
+
function octaves(fn: string, v: Num | undefined): number {
|
|
146
|
+
if (v === undefined) return 3
|
|
147
|
+
if (typeof v !== "number") {
|
|
148
|
+
throw new SLError(
|
|
149
|
+
`${fn}'s octave count is baked into the instruction, so it has to be a constant. ` +
|
|
150
|
+
`A uniform or a computed value cannot change how many octaves the shader runs.`,
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
return v
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export const BUILTINS: Readonly<Record<string, Builtin>> = table
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Names that exist in the opcode table and cannot be written yet, with why.
|
|
160
|
+
*
|
|
161
|
+
* Separate from "unknown identifier" on purpose: an author who writes
|
|
162
|
+
* `rgb2hsv` has read the right documentation and deserves the real answer.
|
|
163
|
+
*/
|
|
164
|
+
export const NOT_YET: Readonly<Record<string, string>> = Object.fromEntries(
|
|
165
|
+
Object.entries(SL_UNIMPLEMENTED)
|
|
166
|
+
.filter(([op]) => SL_HLSL[Number(op)]?.call !== undefined && table[SL_HLSL[Number(op)]!.call!] === undefined)
|
|
167
|
+
.map(([op, why]) => [SL_HLSL[Number(op)]!.call!, why]),
|
|
168
|
+
)
|