onejs-sl 0.1.1 → 0.1.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 +18 -0
- package/README.md +63 -13
- 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/names.ts +77 -0
- package/src/lib/table.ts +125 -0
- package/src/lib/types.ts +23 -0
- package/src/lib/wgsl.ts +994 -0
- package/src/params.ts +113 -0
- package/src/shapes.ts +4 -4
- package/src/sl.ts +9 -1
- package/src/tables.ts +4 -1
- package/src/web.ts +53 -28
- package/src/weblib.ts +0 -1303
package/src/params.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parameter names, for an editor's completions and signature help: so it can
|
|
3
|
+
* offer `lerp(x, y, s)` rather than "lerp takes 3 arguments".
|
|
4
|
+
*
|
|
5
|
+
* Metadata only. Nothing that checks or emits a program reads this file, and
|
|
6
|
+
* only `onejs-sl/tables` imports it, so it cannot change what any program
|
|
7
|
+
* compiles to. Each table is built by a call marked pure, so a bundle that
|
|
8
|
+
* imports `onejs-sl/tables` for BUILTINS alone (the Play editor's parser) drops
|
|
9
|
+
* all of it.
|
|
10
|
+
*
|
|
11
|
+
* The intrinsics carry the names the HLSL documentation gives them. Everything
|
|
12
|
+
* the library implements is named as `lib/*.hlsl` writes it, read by the
|
|
13
|
+
* translator into `src/lib/names.ts` when `npm run lib` runs, so there is no
|
|
14
|
+
* second list of those to drift: a builtin that lowers to a library helper takes
|
|
15
|
+
* that helper's names, and a shape's parameters are named after its function's
|
|
16
|
+
* parameters, in the slots `sl_sdfDistance` passes them in.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { BUILTINS } from "./lang/builtins"
|
|
20
|
+
import { LIB_FUNCTIONS, SDF_CALLS } from "./lib/table"
|
|
21
|
+
import { LIB_PARAM_NAMES } from "./lib/names"
|
|
22
|
+
import { SL_SDF_PARAMS, SL_SDF_SHAPES, type SlSdfKind } from "./shapes"
|
|
23
|
+
|
|
24
|
+
/** One function of the helper library, as its source declares it. */
|
|
25
|
+
export interface LibSignature {
|
|
26
|
+
name: string
|
|
27
|
+
params: ReadonlyArray<{ name: string; type: string }>
|
|
28
|
+
ret: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Every function in the helper library, by `LIB_FUNCTIONS` index, overloads included. */
|
|
32
|
+
export const LIB_SIGNATURES: readonly LibSignature[] = /* @__PURE__ */ LIB_FUNCTIONS.map((f, i) => ({
|
|
33
|
+
name: f.name,
|
|
34
|
+
params: f.params.map((type, j) => ({ name: LIB_PARAM_NAMES[i]![j]!, type })),
|
|
35
|
+
ret: f.ret,
|
|
36
|
+
}))
|
|
37
|
+
|
|
38
|
+
/** A library function's parameter names, the same for every overload of it. */
|
|
39
|
+
function namesOf(fn: string): readonly string[] {
|
|
40
|
+
const found = LIB_SIGNATURES.filter((s) => s.name === fn).map((s) => s.params.map((p) => p.name).join(", "))
|
|
41
|
+
if (found.length === 0) throw new Error(`the library has no ${fn}`)
|
|
42
|
+
if (new Set(found).size > 1) throw new Error(`${fn}'s overloads name their parameters differently: ${found.join(" / ")}`)
|
|
43
|
+
return found[0]!.split(", ")
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The HLSL documentation's names. */
|
|
47
|
+
const INTRINSIC: Record<string, readonly string[]> = {
|
|
48
|
+
sin: ["x"], cos: ["x"], tan: ["x"], asin: ["x"], acos: ["x"], exp: ["x"], log: ["x"], sqrt: ["x"],
|
|
49
|
+
sign: ["x"], ceil: ["x"], round: ["x"], abs: ["x"], floor: ["x"], frac: ["x"], saturate: ["x"],
|
|
50
|
+
rcp: ["x"], normalize: ["x"], length: ["x"],
|
|
51
|
+
pow: ["x", "y"], min: ["x", "y"], max: ["x", "y"], clamp: ["x", "min", "max"], atan2: ["y", "x"],
|
|
52
|
+
distance: ["x", "y"], dot: ["x", "y"], cross: ["x", "y"], reflect: ["i", "n"],
|
|
53
|
+
lerp: ["x", "y", "s"], step: ["y", "x"], smoothstep: ["min", "max", "x"],
|
|
54
|
+
tex2D: ["s", "t"],
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The library helper each builtin lowers to (`emitBody` calls it, and a test
|
|
59
|
+
* checks that it does), and how many of its parameters an author writes: fbm's
|
|
60
|
+
* third, the kind, is the opcode's own.
|
|
61
|
+
*/
|
|
62
|
+
export const LIBRARY: Readonly<Record<string, [string, number]>> = {
|
|
63
|
+
toLinear: ["sl_toLinear", 1], hsv2rgb: ["sl_hsv2rgb", 1], luminance: ["sl_luminance", 1],
|
|
64
|
+
noise: ["sl_valueNoise", 1], simplex: ["sl_simplex", 1], voronoi: ["sl_voronoi", 1],
|
|
65
|
+
fbm: ["sl_fbm", 2], turbulence: ["sl_fbm", 2], ridged: ["sl_fbm", 2],
|
|
66
|
+
// The point; the shape's own parameters are SL_SDF_PARAM_NAMES.
|
|
67
|
+
sdf: ["sl_sdfDistance", 0],
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The two the language defines itself, named as the EDSL's `sl.remap` and `sl.ramp` name theirs. */
|
|
71
|
+
const MACRO: Record<string, readonly string[]> = {
|
|
72
|
+
remap: ["v", "fromMin", "fromMax", "toMin", "toMax"],
|
|
73
|
+
// At least two stops, and as many more as `max` allows.
|
|
74
|
+
ramp: ["t", "stop", "stop..."],
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Each builtin's parameter names, one per argument up to its `max`; those past
|
|
79
|
+
* its `min` are optional. A name ending in `...` repeats up to `max`. `sdf` is
|
|
80
|
+
* written `sdf.circle(p, r)`: its entry names the point, and the shape's own
|
|
81
|
+
* parameters are `SL_SDF_PARAM_NAMES[shape]`.
|
|
82
|
+
*/
|
|
83
|
+
export const BUILTIN_PARAMS: Readonly<Record<string, readonly string[]>> = /* @__PURE__ */ Object.fromEntries(
|
|
84
|
+
Object.keys(BUILTINS).map((name) => {
|
|
85
|
+
const lib = LIBRARY[name]
|
|
86
|
+
if (name === "sdf") return [name, [namesOf(lib![0])[1]!]]
|
|
87
|
+
const names = INTRINSIC[name] ?? MACRO[name] ?? (lib && namesOf(lib[0]).slice(0, lib[1]))
|
|
88
|
+
if (names === undefined) throw new Error(`builtin ${name} has no parameter names in params.ts`)
|
|
89
|
+
return [name, names]
|
|
90
|
+
}),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Each shape's parameters, in the order `sdf.<shape>(p, ...)` takes them, named
|
|
95
|
+
* after its function's: a vector parameter's components as `b.x`, `b.y`.
|
|
96
|
+
*/
|
|
97
|
+
export const SL_SDF_PARAM_NAMES: Readonly<Record<SlSdfKind, readonly string[]>> = /* @__PURE__ */ shapeNames()
|
|
98
|
+
|
|
99
|
+
function shapeNames(): Record<SlSdfKind, readonly string[]> {
|
|
100
|
+
const out = {} as Record<SlSdfKind, readonly string[]>
|
|
101
|
+
for (const shape of Object.keys(SL_SDF_SHAPES) as SlSdfKind[]) {
|
|
102
|
+
const call = SDF_CALLS[SL_SDF_SHAPES[shape]]!
|
|
103
|
+
// The function's first parameter is the point, which `sl_sdfDistance` passes itself.
|
|
104
|
+
const fn = namesOf(call.fn).slice(1)
|
|
105
|
+
const slots = new Array<string>(SL_SDF_PARAMS[shape])
|
|
106
|
+
call.args.forEach((arg, i) => {
|
|
107
|
+
const at = "int" in arg ? [arg.int] : arg
|
|
108
|
+
at.forEach((slot, j) => { slots[slot] = at.length === 1 ? fn[i]! : `${fn[i]}.${"xyzw"[j]}` })
|
|
109
|
+
})
|
|
110
|
+
out[shape] = slots
|
|
111
|
+
}
|
|
112
|
+
return out
|
|
113
|
+
}
|
package/src/shapes.ts
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Duplicating and pinning is the trade this repository already makes for the
|
|
14
14
|
* painter and particle opcode tables, and for the SDF dispatcher that exists in
|
|
15
|
-
* both FxSources and SLCommon. The ids are a contract with
|
|
16
|
-
* way; what matters is that a test fails when they drift, not that there is
|
|
15
|
+
* both FxSources and SLCommon. The ids are a contract with `lib/sdf2d.hlsl`
|
|
16
|
+
* (SDF2D.cginc in OneJS) either way; what matters is that a test fails when they drift, not that there is
|
|
17
17
|
* only one copy.
|
|
18
18
|
*/
|
|
19
19
|
export const SL_SDF_SHAPES = {
|
|
@@ -30,8 +30,8 @@ export const SL_SDF_SHAPES = {
|
|
|
30
30
|
export type SlSdfKind = keyof typeof SL_SDF_SHAPES
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
* How many parameters each shape reads, in the order
|
|
34
|
-
* (its `a` then `b`). Six shapes take more than four: roundedBox's last two
|
|
33
|
+
* How many parameters each shape reads, in the order `sl_sdfDistance` in
|
|
34
|
+
* `lib/common.hlsl` takes them (its `a` then `b`). Six shapes take more than four: roundedBox's last two
|
|
35
35
|
* corner radii, orientedBox's thickness, triangle's third vertex, horseshoe's
|
|
36
36
|
* second width, orientedVesica's width and bezier's end point. `sl.sdf` takes
|
|
37
37
|
* up to this many, and a test derives the same counts from the web emitters'
|
package/src/sl.ts
CHANGED
|
@@ -461,6 +461,14 @@ function difference(a: Num, b: Num): Num {
|
|
|
461
461
|
|
|
462
462
|
export function atan2(y: Num, x: Num): Float {
|
|
463
463
|
const b = ctx()
|
|
464
|
+
// An angle is one number. Given vectors, the node would still be a float
|
|
465
|
+
// and every backend would disagree about which lane it kept: HLSL takes
|
|
466
|
+
// .x silently, GLSL and WGSL refuse to compile.
|
|
467
|
+
for (const v of [y, x]) {
|
|
468
|
+
if (typeof v !== "number" && v.width !== 1) {
|
|
469
|
+
throw new SLError(`atan2 takes two floats, and this is a ${v.width === 2 ? "float2" : v.width === 3 ? "float3" : "float4"}: call it per component`)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
464
472
|
const yy = typeof y === "number" ? float(y) : y
|
|
465
473
|
const [a, c] = align2(yy as Val, x)
|
|
466
474
|
return mk(b, b.call(SLOP.ATAN2, TYPE.FLOAT, [a, c]), TYPE.FLOAT)
|
|
@@ -622,7 +630,7 @@ export function ramp(t: Num, stops: Array<string | [number, number, number, numb
|
|
|
622
630
|
* fifth and sixth.
|
|
623
631
|
*
|
|
624
632
|
* Which parameters a shape takes is the shape's own business; `circle` wants a
|
|
625
|
-
* radius, `roundedBox` wants half extents and a corner. See
|
|
633
|
+
* radius, `roundedBox` wants half extents and a corner. See `lib/sdf2d.hlsl`.
|
|
626
634
|
*/
|
|
627
635
|
export function sdf(kind: SlSdfKind, p: Vec2, params: number[] = []): Float {
|
|
628
636
|
const id = SL_SDF_SHAPES[kind]
|
package/src/tables.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* `onejs-sl/tables`: what completion and highlighting read.
|
|
3
3
|
*
|
|
4
4
|
* The same tables the checker and the emitters use, so an editor offers
|
|
5
|
-
* exactly the builtins that compile, with the arities they take
|
|
5
|
+
* exactly the builtins that compile, with the arities they take and the names
|
|
6
|
+
* of their parameters (`params.ts`), and the helper library's signatures.
|
|
6
7
|
*/
|
|
7
8
|
export { BUILTINS, NOT_YET } from "./lang/builtins"
|
|
8
9
|
export type { Builtin } from "./lang/builtins"
|
|
@@ -12,4 +13,6 @@ export {
|
|
|
12
13
|
export type { SLSurface } from "./ops"
|
|
13
14
|
export { INPUTS } from "./ir"
|
|
14
15
|
export { SL_SDF_SHAPES, SL_SDF_PARAMS } from "./shapes"
|
|
16
|
+
export { BUILTIN_PARAMS, LIB_SIGNATURES, SL_SDF_PARAM_NAMES } from "./params"
|
|
17
|
+
export type { LibSignature } from "./params"
|
|
15
18
|
export type { SlSdfKind } from "./shapes"
|
package/src/web.ts
CHANGED
|
@@ -26,8 +26,10 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import { SLError, TYPE, type Program, type SLNode, type SLType } from "./ir"
|
|
29
|
+
import { libClosure, libIndex, LIB_FUNCTIONS, SDF_CALLS } from "./lib"
|
|
30
|
+
import { LIB_GLSL } from "./lib/glsl"
|
|
31
|
+
import { LIB_WGSL } from "./lib/wgsl"
|
|
29
32
|
import { INPUT_ID, SLOP, VM_UNIFORMS } from "./ops"
|
|
30
|
-
import { SDF_CALLS, WEB_LIB } from "./weblib"
|
|
31
33
|
|
|
32
34
|
export type WebLanguage = "glsl" | "wgsl"
|
|
33
35
|
|
|
@@ -44,7 +46,8 @@ export function emitWGSL(p: Program): string {
|
|
|
44
46
|
|
|
45
47
|
function emitWeb(p: Program, lang: WebLanguage): string {
|
|
46
48
|
const W = lang === "wgsl"
|
|
47
|
-
const need = new Set<
|
|
49
|
+
const need = new Set<number>()
|
|
50
|
+
const smoothsteps = new Set<SLType>()
|
|
48
51
|
const sampled = new Set<number>()
|
|
49
52
|
const lines: string[] = []
|
|
50
53
|
const emitted = new Set<number>()
|
|
@@ -100,7 +103,13 @@ function emitWeb(p: Program, lang: WebLanguage): string {
|
|
|
100
103
|
const s = n.args.map((r) => splat(r, t))
|
|
101
104
|
const w0 = n.args.length > 0 ? typeOf(n.args[0]) : 1
|
|
102
105
|
const imm = n.imm ?? []
|
|
103
|
-
|
|
106
|
+
/** A library function by name, and by HLSL parameter types where it is overloaded. */
|
|
107
|
+
const lib = (name: string, params?: string[]) => {
|
|
108
|
+
const i = libIndex(name, params)
|
|
109
|
+
need.add(i)
|
|
110
|
+
return W ? LIB_FUNCTIONS[i]!.wgsl : name
|
|
111
|
+
}
|
|
112
|
+
const hlslType = (w: SLType) => (w === 1 ? "float" : `float${w}`)
|
|
104
113
|
switch (n.op) {
|
|
105
114
|
case SLOP.COMPOSE: return t === 1 ? a[0] : `${T(t)}(${a.join(", ")})`
|
|
106
115
|
|
|
@@ -145,11 +154,11 @@ function emitWeb(p: Program, lang: WebLanguage): string {
|
|
|
145
154
|
? `(${a[0]} - 2.0 * ${a[1]} * ${a[0]} * ${a[1]})`
|
|
146
155
|
: `reflect(${a[0]}, ${a[1]})`
|
|
147
156
|
case SLOP.LUMINANCE: return `dot(${a[0]}.rgb, ${T(3)}(0.2126, 0.7152, 0.0722))`
|
|
148
|
-
case SLOP.TO_LINEAR: return `${lib(
|
|
157
|
+
case SLOP.TO_LINEAR: return `${lib("sl_toLinear", [hlslType(t)])}(${a[0]})`
|
|
149
158
|
|
|
150
159
|
case SLOP.MIX: return `mix(${s[0]}, ${s[1]}, ${s[2]})`
|
|
151
160
|
case SLOP.STEP: return `step(${s[0]}, ${s[1]})`
|
|
152
|
-
case SLOP.SMOOTHSTEP: return
|
|
161
|
+
case SLOP.SMOOTHSTEP: smoothsteps.add(t); return `sl_smoothstep${t}(${s[0]}, ${s[1]}, ${s[2]})`
|
|
153
162
|
// As the VM and the HLSL emitter do it: branchless, so every
|
|
154
163
|
// backend agrees on the edge value.
|
|
155
164
|
case SLOP.SELECT: {
|
|
@@ -158,8 +167,8 @@ function emitWeb(p: Program, lang: WebLanguage): string {
|
|
|
158
167
|
}
|
|
159
168
|
|
|
160
169
|
case SLOP.HSV2RGB: return `${lib("sl_hsv2rgb")}(${a[0]})`
|
|
161
|
-
case SLOP.NOISE: return `${lib("
|
|
162
|
-
case SLOP.SIMPLEX: return `${lib("
|
|
170
|
+
case SLOP.NOISE: return `${lib("sl_valueNoise")}(${a[0]})`
|
|
171
|
+
case SLOP.SIMPLEX: return `${lib("sl_simplex")}(${a[0]})`
|
|
163
172
|
case SLOP.FBM: return octaveCall(Math.round(imm[1] ?? 0), a[0], imm[0])
|
|
164
173
|
case SLOP.TURBULENCE: return octaveCall(2, a[0], imm[0])
|
|
165
174
|
case SLOP.RIDGED: return octaveCall(3, a[0], imm[0])
|
|
@@ -177,32 +186,36 @@ function emitWeb(p: Program, lang: WebLanguage): string {
|
|
|
177
186
|
)
|
|
178
187
|
}
|
|
179
188
|
|
|
180
|
-
/**
|
|
189
|
+
/**
|
|
190
|
+
* The library's onejsFbmKind, resolved here since the kind is a
|
|
191
|
+
* constant: calling the dispatcher would print a WGSL `select` that
|
|
192
|
+
* runs two of the four fields to keep one.
|
|
193
|
+
*/
|
|
181
194
|
function octaveCall(kind: number, pt: string, octaves: number | undefined): string {
|
|
182
195
|
const o = Math.min(4, Math.max(1, Math.round(octaves ?? 3)))
|
|
183
|
-
const fn = kind === 2 ? "
|
|
184
|
-
return `${lib(fn)}(${pt}, 0.0, ${o})`
|
|
196
|
+
const fn = kind === 2 ? "onejsTurbulence" : kind === 3 ? "onejsRidged" : kind === 1 ? "onejsFbmSimplex" : "onejsFbm"
|
|
197
|
+
return `${lib(fn, ["float2", "float", "int", "float", "float"])}(${pt}, 0.0, ${o}, 2.0, 0.5)`
|
|
185
198
|
}
|
|
186
199
|
|
|
187
|
-
/** The shape is a constant, so this calls it directly instead of
|
|
200
|
+
/** The shape is a constant, so this calls it directly instead of sl_sdfDistance's switch. */
|
|
188
201
|
function sdfCall(id: number, pt: string, im: number[]): string {
|
|
189
202
|
const shape = SDF_CALLS[id]
|
|
190
|
-
//
|
|
203
|
+
// sl_sdfDistance returns 1e6 for an id it does not know.
|
|
191
204
|
if (shape === undefined) return lit(1e6)
|
|
192
|
-
lib(shape.fn)
|
|
205
|
+
const fn = lib(shape.fn)
|
|
193
206
|
const v = [im[1] ?? 0, im[2] ?? 0, im[3] ?? 0, im[4] ?? 0, im[5] ?? 0, im[6] ?? 0]
|
|
194
207
|
const args = shape.args.map((arg) => {
|
|
195
|
-
if (
|
|
208
|
+
if ("int" in arg) return String(Math.trunc(v[arg.int]))
|
|
196
209
|
const parts = arg.map((i) => lit(v[i]))
|
|
197
210
|
return parts.length === 1 ? parts[0] : `${T(parts.length as SLType)}(${parts.join(", ")})`
|
|
198
211
|
})
|
|
199
|
-
return `${
|
|
212
|
+
return `${fn}(${[pt, ...args].join(", ")})`
|
|
200
213
|
}
|
|
201
214
|
}
|
|
202
215
|
|
|
203
216
|
walk(p.result)
|
|
204
217
|
const body = lines.join("\n")
|
|
205
|
-
const library = librarySource(need, lang)
|
|
218
|
+
const library = [...[...smoothsteps].sort().map((w) => smoothstep(w, lang)), ...librarySource(need, lang)].join("\n")
|
|
206
219
|
const header = `// GENERATED from a shader language program (${p.hash}). Do not edit.`
|
|
207
220
|
// Only the textures the program samples. WebGPU's automatic layout leaves
|
|
208
221
|
// out a binding the shader never reads, and a bind group that supplies one
|
|
@@ -251,19 +264,31 @@ ${body}
|
|
|
251
264
|
`
|
|
252
265
|
}
|
|
253
266
|
|
|
254
|
-
/** The needed
|
|
255
|
-
function librarySource(need: Set<
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
267
|
+
/** The needed library functions and everything they call, in library order. */
|
|
268
|
+
function librarySource(need: Set<number>, lang: WebLanguage): string[] {
|
|
269
|
+
const texts = lang === "wgsl" ? LIB_WGSL : LIB_GLSL
|
|
270
|
+
return libClosure(need).map((i) => texts[i]!.trim())
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* HLSL's smoothstep is the formula whatever the edge order; GLSL leaves
|
|
275
|
+
* edge0 >= edge1 undefined, so the web writes the formula out. This belongs
|
|
276
|
+
* to the web frame rather than the library: the HLSL backends call the
|
|
277
|
+
* intrinsic, which already means this.
|
|
278
|
+
*/
|
|
279
|
+
function smoothstep(w: SLType, lang: WebLanguage): string {
|
|
280
|
+
if (lang === "glsl") {
|
|
281
|
+
const g = w === 1 ? "float" : `vec${w}`
|
|
282
|
+
return `${g} sl_smoothstep${w}(${g} a, ${g} b, ${g} x) {
|
|
283
|
+
${g} t = clamp((x - a) / (b - a), 0.0, 1.0);
|
|
284
|
+
return t * t * (3.0 - 2.0 * t);
|
|
285
|
+
}`
|
|
264
286
|
}
|
|
265
|
-
|
|
266
|
-
return
|
|
287
|
+
const t = w === 1 ? "f32" : `vec${w}f`
|
|
288
|
+
return `fn sl_smoothstep${w}(a: ${t}, b: ${t}, x: ${t}) -> ${t} {
|
|
289
|
+
let t = saturate((x - a) / (b - a));
|
|
290
|
+
return t * t * (3.0 - 2.0 * t);
|
|
291
|
+
}`
|
|
267
292
|
}
|
|
268
293
|
|
|
269
294
|
/** A literal that survives a float32 round trip and never reads as an int. */
|