Use normal JavaScript operators like +, -, *, /, %, **, +=, >, &&, and ! directly inside Three.js TSL Fn() blocks.
vite-plugin-tsl-operator is a plug-and-play Vite plugin for Three.js Shading Language (TSL), WebGPU, and shader node projects. It rewrites readable operator syntax to TSL node methods during Vite transforms, so you can write shader logic naturally without hand-chaining .add(), .mul(), .greaterThan(), and friends.
| Category | Operators |
|---|---|
| Arithmetic | +, -, *, /, %, ** |
| Assignment | +=, -=, *=, /=, %= |
| Comparison | >, <, >=, <=, ==, ===, !=, !== |
| Logical | &&, ||, ! |
Opt-in with //@tsl |
i++, --i, a ? b : c |
Instead of writing chained TSL methods:
Fn(() => {
let x = float(1).sub(alpha.mul(color.r))
x = x.mul(4)
return x
})you can write normal JavaScript-style math:
Fn(() => {
let x = 1 - alpha * color.r
x *= 4
return x
})The plugin transforms it for you.
import { Fn, float } from 'three/tsl'
const shader = Fn(() => {
const strength = float(0.8)
return 1 - strength * color.r
})becomes:
import { Fn, float } from 'three/tsl'
const shader = Fn(() => {
const strength = float(0.8)
return float(1).sub(strength.mul(color.r))
})pnpm add vite-plugin-tsl-operatornpm install vite-plugin-tsl-operatoryarn add vite-plugin-tsl-operatorAdd it to vite.config.js and start writing operators inside Fn() blocks. No Babel config, no runtime setup, and no code changes outside your Vite plugins array.
import { defineConfig } from 'vite'
import tslOperatorPlugin from 'vite-plugin-tsl-operator'
export default defineConfig({
plugins: [
tslOperatorPlugin(),
],
})| Category | Input | Output |
|---|---|---|
| Arithmetic | a + b |
a.add(b) |
| Arithmetic | a - b |
a.sub(b) |
| Arithmetic | a * b |
a.mul(b) |
| Arithmetic | a / b |
a.div(b) |
| Arithmetic | a % b |
a.mod(b) |
| Arithmetic | a ** b |
a.pow(b) |
| Assignment | a += b |
a.addAssign(b) |
| Assignment | a -= b |
a.subAssign(b) |
| Assignment | a *= b |
a.mulAssign(b) |
| Assignment | a /= b |
a.divAssign(b) |
| Assignment | a %= b |
a.modAssign(b) |
| Comparison | a > b |
a.greaterThan(b) |
| Comparison | a < b |
a.lessThan(b) |
| Comparison | a >= b |
a.greaterThanEqual(b) |
| Comparison | a <= b |
a.lessThanEqual(b) |
| Comparison | a == b, a === b |
a.equal(b) |
| Comparison | a != b, a !== b |
a.notEqual(b) |
| Logical | a && b |
a.and(b) |
| Logical | a || b |
a.or(b) |
| Logical | !a |
a.not() |
Opt-in with //@tsl |
a ? b : c |
select(a, b, c) |
Opt-in with //@tsl |
i++, --i |
i.addAssign(1), i.subAssign(1) |
Only code inside direct Fn(() => ...) calls is transformed. Regular application code, helper functions outside Fn(), files in node_modules, and non-JS/TS files are ignored.
const value = a + b // unchanged
Fn(() => {
return a + b // a.add(b)
})The plugin also exits early when a file does not contain Fn( or any candidate operator, so most unrelated files pay almost no transform cost.
The transform is intentionally conservative.
Pure numeric math stays JavaScript:
Fn(() => {
const radius = 0.08
const halfRadius = radius * 0.5 // unchanged
return smoothstep(radius, halfRadius, dist)
})Math.* expressions stay JavaScript:
Fn(() => {
return 1 - Math.PI / 2 // float(1).sub(Math.PI / 2)
})Plain JavaScript control flow stays JavaScript unless you opt in:
Fn(() => {
if (enabled && !debug) {
return color * opacity // color.mul(opacity)
}
})Use comments when the automatic context detection needs help.
//@tsl forces TSL conversion for the next statement, or for an entire Fn() when placed immediately before it:
//@tsl
Fn(() => {
if (x > y) {
return a
}
})//@js keeps the next statement as JavaScript:
Fn(() => {
//@js
const debugValue = count / 2
return color / 2 // color.div(2)
})By default, the plugin adds missing TSL imports when a transform introduces wrappers such as float, int, Loop, or select.
import { Fn, uv } from 'three/tsl'
Fn(() => 1 - uv())becomes:
import { Fn, uv, float } from 'three/tsl'
Fn(() => float(1).sub(uv()))The plugin adds to existing imports from three/tsl, three/webgpu, or three/src/nodes/.... If no TSL import exists, it creates one using importSource.
| Option | Default | Description |
|---|---|---|
logs |
false |
Log before/after transform snippets. Accepts true, false, a filename, an array of filenames, or a RegExp. |
autoImportMissingTSL |
true |
Add missing TSL imports required by generated code. |
importSource |
'three/tsl' |
Source used when creating a new import declaration. |
tslOperatorPlugin({ logs: /shader/i })
tslOperatorPlugin({ autoImportMissingTSL: false })
tslOperatorPlugin({ importSource: 'three/webgpu' })Annotate loops with //@tsl to turn them into Loop() calls.
Fn(() => {
//@tsl
for (let i = 0; i < 10; i++) {
sum += value
}
})becomes:
Fn(() => {
Loop({ start: int(0), end: int(10), type: 'int', condition: '<', name: 'i' }, ({ i }) => {
sum.addAssign(value)
})
})while and do...while are also supported with //@tsl. Loop bounds are wrapped with int() or float() based on the values used.
The repository includes two test layers:
- Unit transform tests for operator output, idempotency, TypeScript syntax, source maps, directives, and auto-import behavior.
- Browser runtime tests that compile transformed TSL shaders through Three.js WebGL/WebGPU paths.
pnpm test
pnpm test:unit
pnpm test:browserFor production builds, pair this with vite-plugin-tsl-optimizer. Run this plugin first, then the optimizer to simplify generated chains such as .add(0), .mul(1), and redundant wrappers.
plugins: [
tslOperatorPlugin(),
tslOptimizerPlugin(),
]TSL is Three.js Shading Language. Start with the official Three.js TSL documentation, and use the TSL reference page for available nodes, helpers, and properties.
MIT