onejs-sl 0.1.1 → 0.1.2
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 +60 -12
- package/goldens/goldens.json +1 -1
- package/lib/common.hlsl +155 -0
- package/lib/noise2d.hlsl +167 -0
- package/lib/sdf2d.hlsl +510 -0
- package/package.json +6 -3
- package/src/body.ts +62 -21
- package/src/emit/hlsl-body.ts +3 -2
- package/src/lib/glsl.ts +899 -0
- package/src/lib/hlsl.ts +891 -0
- package/src/lib/index.ts +47 -0
- package/src/lib/table.ts +125 -0
- package/src/lib/types.ts +23 -0
- package/src/lib/wgsl.ts +994 -0
- package/src/shapes.ts +4 -4
- package/src/sl.ts +9 -1
- package/src/web.ts +53 -28
- package/src/weblib.ts +0 -1303
package/src/body.ts
CHANGED
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* The subset is HLSL's spelling (`float2`, `lerp`, `frac`, `fmod`, `atan2`,
|
|
12
12
|
* `saturate`), which Metal takes through a host's compatibility defines. `fmod`
|
|
13
13
|
* is the same truncating remainder in both. Nothing here uses `mul`, `static`,
|
|
14
|
-
* derivatives, or an overload the library does not resolve by width.
|
|
14
|
+
* derivatives, or an overload the library does not resolve by width. The
|
|
15
|
+
* library text `emitLibrary` prints is the same subset, translated from
|
|
16
|
+
* `lib/*.hlsl` by `lib/translate.ts`.
|
|
15
17
|
*
|
|
16
18
|
* One local per reachable node, in order, named `n<index>` and unreadable on
|
|
17
19
|
* purpose: generated code that looks hand written invites hand editing, and a
|
|
@@ -20,6 +22,8 @@
|
|
|
20
22
|
*/
|
|
21
23
|
|
|
22
24
|
import { SLError, TYPE, type InputName, type Program, type SLNode, type SLType } from "./ir"
|
|
25
|
+
import { libClosure, LIB_FUNCTIONS } from "./lib"
|
|
26
|
+
import { LIB_HLSL } from "./lib/hlsl"
|
|
23
27
|
import { SLOP } from "./ops"
|
|
24
28
|
|
|
25
29
|
export interface BodyTarget {
|
|
@@ -48,7 +52,7 @@ export interface Body {
|
|
|
48
52
|
uniforms: number[]
|
|
49
53
|
/** Texture slots the body samples, ascending. */
|
|
50
54
|
textures: number[]
|
|
51
|
-
/** Library functions the body calls, sorted: `sl_fbm`, `sl_sdfDistance`, ... */
|
|
55
|
+
/** Library functions the body calls, sorted: `sl_fbm`, `sl_sdfDistance`, ... `emitLibrary` prints them. */
|
|
52
56
|
helpers: string[]
|
|
53
57
|
}
|
|
54
58
|
}
|
|
@@ -77,6 +81,8 @@ export function emitBody(p: Program, target: BodyTarget): Body {
|
|
|
77
81
|
const textures = new Set<number>()
|
|
78
82
|
const helpers = new Set<string>()
|
|
79
83
|
const helper = (fn: string) => { helpers.add(fn); return fn }
|
|
84
|
+
/** A node at width `w`: a scalar repeated into a constructor, since HLSL refuses float3(x) and Metal x.xxx. */
|
|
85
|
+
const splat = (ref: number, w: SLType) => (p.nodes[ref]!.type === TYPE.FLOAT && w > 1 ? ctor(w, Array(w).fill(name(ref))) : name(ref))
|
|
80
86
|
|
|
81
87
|
const expr = (n: SLNode): string => {
|
|
82
88
|
switch (n.k) {
|
|
@@ -92,6 +98,8 @@ export function emitBody(p: Program, target: BodyTarget): Body {
|
|
|
92
98
|
return n.type === TYPE.VEC4 ? v : `${v}.${SWZ.slice(0, n.type)}`
|
|
93
99
|
}
|
|
94
100
|
case "swizzle":
|
|
101
|
+
// Metal has no swizzle of a scalar, so a scalar widens by constructor.
|
|
102
|
+
if (p.nodes[n.src]!.type === TYPE.FLOAT) return ctor(n.type, n.chans.map(() => name(n.src)))
|
|
95
103
|
return `${name(n.src)}.${n.chans.map((c) => SWZ[c]).join("")}`
|
|
96
104
|
case "call":
|
|
97
105
|
return call(n)
|
|
@@ -100,6 +108,14 @@ export function emitBody(p: Program, target: BodyTarget): Body {
|
|
|
100
108
|
|
|
101
109
|
const call = (n: Extract<SLNode, { k: "call" }>): string => {
|
|
102
110
|
const a = n.args.map(name)
|
|
111
|
+
const t = n.type
|
|
112
|
+
// Every argument of an element wise intrinsic at the result's width,
|
|
113
|
+
// and every literal at its exact type. HLSL broadcasts a scalar and
|
|
114
|
+
// converts an int; Metal finds max(float2, float) has no overload and
|
|
115
|
+
// max(float, 0) is ambiguous, so the shared subset spells both out.
|
|
116
|
+
const s = n.args.map((r) => splat(r, t))
|
|
117
|
+
const k = (v: number, w: SLType) => ctor(w, Array(w).fill(lit(v)))
|
|
118
|
+
const w0 = n.args.length > 0 ? p.nodes[n.args[0]!]!.type : TYPE.FLOAT
|
|
103
119
|
const imm = n.imm ?? []
|
|
104
120
|
switch (n.op) {
|
|
105
121
|
case SLOP.COMPOSE: return ctor(n.type, a)
|
|
@@ -108,50 +124,52 @@ export function emitBody(p: Program, target: BodyTarget): Body {
|
|
|
108
124
|
case SLOP.SUB: return `(${a[0]} - ${a[1]})`
|
|
109
125
|
case SLOP.MUL: return `(${a[0]} * ${a[1]})`
|
|
110
126
|
case SLOP.DIV: return `(${a[0]} / ${a[1]})`
|
|
111
|
-
case SLOP.MOD: return `fmod(${
|
|
127
|
+
case SLOP.MOD: return `fmod(${s[0]}, ${s[1]})`
|
|
112
128
|
// abs on the base, matching the VM. pow of a negative base is
|
|
113
129
|
// undefined in HLSL and the backends must be undefined in the same
|
|
114
130
|
// direction.
|
|
115
|
-
case SLOP.POW: return `pow(abs(${
|
|
131
|
+
case SLOP.POW: return `pow(abs(${s[0]}), ${s[1]})`
|
|
116
132
|
case SLOP.NEG: return `(-${a[0]})`
|
|
117
133
|
case SLOP.RECIP: return `(1.0 / ${a[0]})`
|
|
118
134
|
|
|
119
135
|
case SLOP.SIN: return `sin(${a[0]})`
|
|
120
136
|
case SLOP.COS: return `cos(${a[0]})`
|
|
121
137
|
case SLOP.TAN: return `tan(${a[0]})`
|
|
122
|
-
case SLOP.ASIN: return `asin(clamp(${a[0]}, -1, 1))`
|
|
123
|
-
case SLOP.ACOS: return `acos(clamp(${a[0]}, -1, 1))`
|
|
124
|
-
case SLOP.ATAN2: return `atan2(${
|
|
138
|
+
case SLOP.ASIN: return `asin(clamp(${a[0]}, ${k(-1, t)}, ${k(1, t)}))`
|
|
139
|
+
case SLOP.ACOS: return `acos(clamp(${a[0]}, ${k(-1, t)}, ${k(1, t)}))`
|
|
140
|
+
case SLOP.ATAN2: return `atan2(${s[0]}, ${s[1]})`
|
|
125
141
|
case SLOP.EXP: return `exp(${a[0]})`
|
|
126
|
-
case SLOP.LOG: return `log(max(${a[0]}, 1e-8))`
|
|
127
|
-
case SLOP.SQRT: return `sqrt(max(${a[0]}, 0))`
|
|
142
|
+
case SLOP.LOG: return `log(max(${a[0]}, ${k(1e-8, t)}))`
|
|
143
|
+
case SLOP.SQRT: return `sqrt(max(${a[0]}, ${k(0, t)}))`
|
|
128
144
|
case SLOP.ABS: return `abs(${a[0]})`
|
|
129
145
|
case SLOP.SIGN: return `sign(${a[0]})`
|
|
130
146
|
case SLOP.FLOOR: return `floor(${a[0]})`
|
|
131
147
|
case SLOP.CEIL: return `ceil(${a[0]})`
|
|
132
148
|
case SLOP.ROUND: return `round(${a[0]})`
|
|
133
149
|
case SLOP.FRACT: return `frac(${a[0]})`
|
|
134
|
-
case SLOP.MIN: return `min(${
|
|
135
|
-
case SLOP.MAX: return `max(${
|
|
136
|
-
case SLOP.CLAMP: return `clamp(${
|
|
150
|
+
case SLOP.MIN: return `min(${s[0]}, ${s[1]})`
|
|
151
|
+
case SLOP.MAX: return `max(${s[0]}, ${s[1]})`
|
|
152
|
+
case SLOP.CLAMP: return `clamp(${s[0]}, ${s[1]}, ${s[2]})`
|
|
137
153
|
case SLOP.SATURATE: return `saturate(${a[0]})`
|
|
138
154
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
case SLOP.
|
|
155
|
+
// Of a scalar, Metal's are ambiguous, so what the VM computes for one
|
|
156
|
+
// (its lanes past the width hold 0) is written out.
|
|
157
|
+
case SLOP.LENGTH: return w0 === 1 ? `abs(${a[0]})` : `length(${a[0]})`
|
|
158
|
+
case SLOP.DISTANCE: return w0 === 1 ? `abs(${a[0]} - ${a[1]})` : `distance(${a[0]}, ${a[1]})`
|
|
159
|
+
case SLOP.DOT: return w0 === 1 ? `(${a[0]} * ${a[1]})` : `dot(${a[0]}, ${a[1]})`
|
|
142
160
|
case SLOP.CROSS: return `cross(${a[0]}, ${a[1]})`
|
|
143
|
-
case SLOP.NORMALIZE: return `normalize(${a[0]})`
|
|
144
|
-
case SLOP.REFLECT: return `reflect(${a[0]}, ${a[1]})`
|
|
161
|
+
case SLOP.NORMALIZE: return w0 === 1 ? `(${a[0]} / abs(${a[0]}))` : `normalize(${a[0]})`
|
|
162
|
+
case SLOP.REFLECT: return w0 === 1 ? `(${a[0]} - 2.0 * ${a[1]} * ${a[0]} * ${a[1]})` : `reflect(${a[0]}, ${a[1]})`
|
|
145
163
|
case SLOP.LUMINANCE: return `${helper("sl_luminance")}(${a[0]}.rgb)`
|
|
146
164
|
case SLOP.TO_LINEAR: return target.colour === "linear" ? `${helper("sl_toLinear")}(${a[0]})` : a[0]!
|
|
147
165
|
|
|
148
|
-
case SLOP.MIX: return `lerp(${
|
|
149
|
-
case SLOP.STEP: return `step(${
|
|
150
|
-
case SLOP.SMOOTHSTEP: return `smoothstep(${
|
|
166
|
+
case SLOP.MIX: return `lerp(${s[0]}, ${s[1]}, ${s[2]})`
|
|
167
|
+
case SLOP.STEP: return `step(${s[0]}, ${s[1]})`
|
|
168
|
+
case SLOP.SMOOTHSTEP: return `smoothstep(${s[0]}, ${s[1]}, ${s[2]})`
|
|
151
169
|
// Matches the VM exactly, branchlessly, rather than using an if.
|
|
152
170
|
// Two backends that pick differently here disagree on every edge
|
|
153
171
|
// value.
|
|
154
|
-
case SLOP.SELECT: return `lerp(${
|
|
172
|
+
case SLOP.SELECT: return `lerp(${s[2]}, ${s[1]}, step(${k(0.5, t)}, ${s[0]}))`
|
|
155
173
|
|
|
156
174
|
case SLOP.HSV2RGB: return `${helper("sl_hsv2rgb")}(${a[0]})`
|
|
157
175
|
case SLOP.NOISE: return `${helper("sl_valueNoise")}(${a[0]})`
|
|
@@ -200,3 +218,26 @@ export function emitBody(p: Program, target: BodyTarget): Body {
|
|
|
200
218
|
uses: { uniforms: sorted(uniforms), textures: sorted(textures), helpers: [...helpers].sort() },
|
|
201
219
|
}
|
|
202
220
|
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The library functions a body calls (`Body.uses.helpers`) and everything they
|
|
224
|
+
* call, in the same subset and in dependency order, with the colour branch the
|
|
225
|
+
* target asked for: the text a host puts ahead of the function it wraps the
|
|
226
|
+
* body in.
|
|
227
|
+
*
|
|
228
|
+
* Separate from `emitBody` because OneJS's own frame never needs it: the Unity
|
|
229
|
+
* shader includes `SLCommon.cginc`, the library's source, and a bundle that
|
|
230
|
+
* prints only Unity shaders should not carry the library's text as well.
|
|
231
|
+
*/
|
|
232
|
+
export function emitLibrary(helpers: readonly string[], colour: "gamma" | "linear"): string {
|
|
233
|
+
const wanted = new Set(helpers)
|
|
234
|
+
const named = LIB_FUNCTIONS.flatMap((f, i) => (wanted.has(f.name) ? [i] : []))
|
|
235
|
+
if (named.length < wanted.size) {
|
|
236
|
+
const known = new Set(LIB_FUNCTIONS.map((f) => f.name))
|
|
237
|
+
throw new SLError(`the library has no ${[...wanted].filter((h) => !known.has(h)).join(", ")}`)
|
|
238
|
+
}
|
|
239
|
+
return libClosure(named).map((i) => {
|
|
240
|
+
const text = LIB_HLSL[i]!
|
|
241
|
+
return (typeof text === "string" ? text : text[colour]).trim()
|
|
242
|
+
}).join("\n")
|
|
243
|
+
}
|
package/src/emit/hlsl-body.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `onejs-sl/emit/hlsl-body`: a program as a function body in the HLSL and Metal
|
|
3
|
-
* shared subset, for a host that supplies its own frame
|
|
3
|
+
* shared subset, for a host that supplies its own frame (see `BodyTarget`), and
|
|
4
|
+
* the library functions that body calls, in the same subset.
|
|
4
5
|
*/
|
|
5
|
-
export { emitBody } from "../body"
|
|
6
|
+
export { emitBody, emitLibrary } from "../body"
|
|
6
7
|
export type { Body, BodyTarget } from "../body"
|