A local, developer-focused GLSL shader workbench: edit Shadertoy-style GLSL fragment shaders, render in real time, and inspect the result with camera tools, a pixel inspector, an interactive GLSL scratchpad, a debug console, and status monitoring.
Milestone 0 through 6 scaffolding is in place:
- Tauri 2 + React/TypeScript + Vite frontend.
- Rust workspace with
luxel-core,luxel-io,luxel-system,luxel-render. wgpu-based offscreen renderer withnagaGLSL-to-WGSL translation.- Shadertoy-compatible
mainImage(out vec4, in vec2)entry point withiResolution,iTime,iFrame,iMouseuniforms. - Scene file format (
.luxel.json) withschemaVersionand migration scaffolding. - Camera orbit/pan/dolly/reset, bookmarks, frustum overlay, aspect-ratio control.
- Auto/manual render toggle with
requestAnimationFramerender driver. - Pixel inspector (footer bar + dedicated Inspector panel) with a pinnable pixel and an on-canvas crosshair.
- Interactive GLSL Scratchpad: evaluate expressions at a chosen pixel, with reusable variables, a built-in catalog, and Tab autocomplete.
- Timeline playback with loop support.
- Unified per-view font zoom that targets the panel under the cursor.
- Cross-platform Python helper scripts.
- Python 3.11+ (for the helper scripts)
- Rust toolchain (stable, via rustup)
- Node.js 18+ with npm
- macOS Apple Silicon (Metal) or Windows with a DX12/Vulkan capable GPU
Run python scripts/doctor.py to verify your environment.
xcode-select --install
brew install node # if you don't already have it
curl https://sh.rustup.rs -sSf | sh
python scripts/doctor.py- Install Visual Studio Build Tools 2022 with the "Desktop development with C++" workload.
- Install Node.js LTS.
- Install Rust via rustup.
- Run
python scripts/doctor.py.
python scripts/launch.py
python scripts/launch.py --scene examples/default_scene.luxel.json
python scripts/launch.py --gpu-backend metal # macOS
python scripts/launch.py --gpu-backend dx12 # Windows
python scripts/launch.py --gpu-backend vulkanpython scripts/test.py
python scripts/test.py --rust-only
python scripts/test.py --frontend-only
python scripts/test.py --lintpython scripts/build.py --release
# or, identically:
python scripts/package.py --release--release runs tauri build, which produces shippable platform bundles:
| Platform | Output |
|---|---|
| macOS | target/release/bundle/macos/Luxel.app (the app itself) + target/release/bundle/dmg/Luxel_*.dmg (installer for sharing) |
| Windows | target/release/bundle/msi/Luxel_*.msi + target/release/bundle/nsis/Luxel_*-setup.exe |
A debug build (python scripts/build.py with no flags) just produces dist/ and target/debug/luxel-app - useful for ./target/debug/luxel-app development runs but not packaged for sharing.
Luxel's camera is a "look-at" rig: a position, target, and up vector with a vertical FOV. The Render view exposes it through these gestures (inside the render canvas):
| Input | What it does |
|---|---|
| Left-drag | Orbit around target, distance preserved |
| Shift-drag or middle-drag | Pan (both position and target slide together) |
| Scroll wheel | Dolly: move along the camera-to-target ray, can't cross the target |
F |
Reset to default (0, 0, 5) looking at the origin |
| Reset cam button | Same as F |
| Bookmarks | Save the current camera, restore a saved one, or delete |
| Camera position readout | Top-right of the Render header shows [x, y, z] |
The camera is exposed to GLSL shaders through these uniforms:
vec3 iCameraPosition // world-space position
float iCameraFov // vertical, radians
vec3 iCameraForward // unit basis vector pointing into the scene
vec3 iCameraRight // unit basis vector
vec3 iCameraUp // unit basis vectorThe canonical ray-direction formula for a raymarcher:
vec2 uv = (fragCoord * 2.0 - iResolution.xy) / iResolution.y;
float h = tan(iCameraFov * 0.5);
vec3 rd = normalize(iCameraForward + uv.x * h * iCameraRight + uv.y * h * iCameraUp);Press ? in the toolbar for an in-app quick reference covering keyboard shortcuts and the full uniform list.
The toolbar's FPS button toggles a heads-up overlay in the top-left of the render view that reports rolling FPS, last frame time (ms), and the current render resolution. The setting persists across app restarts via localStorage.
Luxel uses a single requestAnimationFrame loop (src/hooks/useRenderDriver.ts) - same model as a DCC viewport. Each animation frame:
- Checks whether the scene is "dirty" (camera, render size, iTime, iFrame, or render quality changed since the last render).
- If dirty AND no render is in flight, kicks off a GPU render at the current preview resolution.
- Serializes renders - at most one in flight at a time, so a slow shader naturally caps the loop's rate without manual throttling.
End-to-end latency from a drag event to a rendered frame is one animation frame (~16 ms at 60 Hz). The result: navigation feels native.
When the scene is idle (no inputs changing), the loop still ticks but does no GPU work.
The toolbar has an Auto/Manual dropdown next to the Render button. In Auto mode (the default), the render driver fires on every change. In Manual mode, rendering only happens when you click Render or press Cmd/Ctrl+Enter. Useful when working on a large shader and you want to avoid taxing the GPU until the code is ready to test.
iTime and iFrame are exposed as manual scrub controls in the playback bar at the bottom. The bar includes transport buttons (first, step back, play backward, play forward, step forward, last) and a loop toggle. When loop is enabled, playback wraps from last frame to first (or first to last when playing in reverse) instead of stopping at the timeline bounds.
Toggle Inspect in the Render view header to show a footer bar under the canvas with live readouts of the pixel under the cursor: pixel coordinates (bottom-left origin, matching gl_FragCoord), render resolution, UV, and RGB values with a color swatch. Inspect is off by default and is a per-session toggle.
The Inspector panel (available in the slot dropdown) shows the same pixel data along with uniforms, camera state, and render statistics.
The Inspector's Pin (x, y) field locks the readout to a specific pixel, so it stays populated even with interactive Inspect off, and even when the cursor leaves the canvas. This is handy for teaching ("we're looking at pixel 40, 100"). The Crosshair toggle in the Render header marks that pixel on the canvas. The pinned pixel is in bottom-left / gl_FragCoord space and is shared with the Scratchpad. If a resize puts the pixel outside the current render, the readout says so and the crosshair hides until it's back in range.
Editor, Inspector, Console, and Scratchpad text scale with one hotkey, Cmd/Ctrl + +/-/0, applied to whichever panel the cursor is over. The editor also has explicit zoom buttons in its header.
The Scratchpad view is a REPL for GLSL expressions: type an expression and see its value at the pinned pixel. It's the GLSL answer to print-debugging, useful for learning what built-ins and vector math actually do.
Every scene uniform (iResolution, iTime, the camera uniforms, ...) and gl_FragCoord are in scope, prefilled from the current scene; iTime and the pixel are overridable in the Scratchpad header. Expressions are evaluated through the real naga/wgpu path (rendered to a 1-pixel float target), so results match the live shader exactly.
> length(vec2(3.0, 4.0))
5
float
> (gl_FragCoord.xy - 0.5 * iResolution.xy) / iResolution.y
vec2(-0.5, -0.0833)
vec2| Action | What it does |
|---|---|
name = <expr> |
Snapshot a value and reuse it on later lines |
Tab |
Autocomplete built-ins, uniforms, variables, and swizzles (after a vector .) |
↑ / ↓ |
Move the suggestion list, or walk expression history |
:builtins |
List the available functions and uniforms |
:help <name> |
Show a built-in's signature (e.g. :help mix) |
:vars / :reset |
List or clear your variables |
:clear |
Clear the scrollback |
Color-like results (a vec3/vec4 in [0, 1]) show a swatch. A built-in called with the wrong arguments reports its real signature instead of a generic "unknown function" error.
Dev builds are noticeably slower than release because wgpu, naga, and the Tauri shell run unoptimized debug code on a hot path (every camera drag triggers a render). Two ways to speed them up:
- The
[profile.dev]tweak already applied toCargo.tomlkeeps Luxel's own crates fast to recompile but compiles every dependency atopt-level = 3. First build of new deps takes ~30s longer, then each subsequent dev run is much snappier. You don't need to do anything; just runpython scripts/launch.pyas usual. - Render-quality multiplier in the toolbar lets you scale the preview render down (1/4x to 2x). On a heavy raymarcher or a slow GPU, dropping to 1/2x makes camera drags feel native; you can bump it back to 1x before exporting.
For maximum performance, run the release build:
python scripts/build.py --release
# then open the bundled binary from src-tauri/target/release/The release build is typically 5-10x faster than the dev build for shader-heavy scenes.
- Use the Open... button in the toolbar to load a
.luxel.jsonfile. - Use Save to write the current scene; an unsaved scene prompts for a path.
- Scenes embed: GLSL source, render settings (resolution, aspect ratio, overlay), camera state, camera bookmarks, and panel layout.
Luxel remembers its window size, position, and maximized/fullscreen state between launches via tauri-plugin-window-state. State is written on exit and restored on startup; no per-scene configuration is involved. To reset to defaults, delete the plugin's state file (.window-state.json under the app's data directory).
- Single-frame rendering only (no animation loop).
- Fragment shader only; no compute, no multipass buffers, no texture channels.
- GLSL is translated to WGSL via
naga; some advanced GLSL features may not survive translation. Errors include line numbers mapped back to the user's source. - NVIDIA support means DX12 or Vulkan, not CUDA.
luxel/
├── Cargo.toml # Rust workspace
├── package.json # Frontend deps (Vite + React + TS)
├── src-tauri/ # Tauri shell (Rust process)
│ └── src/
│ ├── lib.rs # Entrypoint, plugin setup
│ ├── commands.rs # #[tauri::command] surface
│ ├── app_state.rs # AppState (renderer, sampler)
│ └── events.rs # Console event emission
├── crates/
│ ├── luxel-core/ # Scene model, camera math, validation
│ ├── luxel-io/ # Scene JSON load/save + migrations
│ ├── luxel-system/ # CPU/memory/GPU sampling
│ └── luxel-render/ # wgpu pipeline, GLSL prelude, naga compile, expression eval
├── src/ # React frontend
│ ├── components/ # LayoutRoot, RenderView, ShaderEditor, ConsolePanel, InspectorPanel, Scratchpad, ...
│ ├── glsl/ # Built-in catalog for Scratchpad help/autocomplete
│ ├── state/ # Zustand stores (scene, console, app)
│ └── tauri/ # invoke() and event subscription wrappers
├── scripts/ # Python build/test/launch helpers
└── examples/ # Default scene + GLSL fixtures
load_scene(path) -> SceneFilesave_scene(path, scene)validate_scene_cmd(scene)default_scene() -> SceneFilecompile_shader(shader) -> ShaderCompileResultrender_single_frame(scene) -> RenderResulteval_glsl(scene, expr, preamble, resolution, pixel, ...) -> EvalResultget_system_status() -> SystemStatusget_gpu_info() -> GpuInfoset_gpu_backend(backend)
Console messages are emitted on the luxel://console event channel.
MIT. See LICENSE for details.
