ShaderToy clone in Rust, currently supporting MacOS.
cargo run <rust project dir>
displays a single macOS window filled with a Metal framework fragment shader.- You can edit and save the Rust project source code (in VS code or any other editor) to change the fragment shader output and the window will update in real time.
- You write the shader in Rust but it is compiled to Metal Shading Language (a variation of C++)
- In the shader source you can reference the
const
INPUT
struct which provides inputs for each frame, similarly to Input Uniforms in ShaderToy. You don't need to thread these values through your functions as arguments, despite Metal having no concept of global uniforms like WebGL does. - You can split the shader across multiple files using
mod <name>
anduse <name>::*
. - You can pause, restart and even run another shader file from the command line while the window is open.
- clone this repo
- run
cargo run raymarching_eyes
- edit
examples/raymarching_eyes.rs
Why Rust for the shader source? Better syntax, better editor integration and because it's a fun hack. It should feel exactly like writing Rust (which feels awesome!). Unlike in ShaderToy the Rust typechecker warns immediately about most errors one might make.
In general you will write Rust that closely resembles the C++ Metal Shading Language API, except for a few differences.
The INPUT
struct is documented in shader_roy_metal_sl_interface.
API | MSL (C++) | Rust |
Scalar Types |
float foo() {
return 3.0;
} |
Use standard Rust types that correspond to the Metal types. fn foo() -> f32 {
3.0
} |
Vector Types |
void foo(float3 a, uint2 b) {} |
Use the generic fn foo(a: Vec3, b: Vec2<u32>) {} |
Constructors |
vector constructors ( auto x = float2(1.0);
float4(x, x);
uint2(0, 0); |
In Rust, these follow the type names. You need to call these as methods. let x = 1.0.vec2();
(x, x).vec4();
vec2u32(0, 0); |
Access Constructors |
vector component selection constructors ( auto pos = float2(1.0, 2.0);
auto foo = pos.yyxy;
// => float4(2.0, 2.0, 1.0, 2.0) |
In Rust you need to call these as methods: let pos = (1.0, 2.0).vec2();
let foo = pos.yyxy();
// => (2.0, 2.0, 1.0, 2.0).vec4() |
Functions |
min(x, y); |
Math, geometric and common functions need to be called as methods x.min(y) |
Renamed functions |
clamp(x, 0.3, 0.4);
length(x);
length_squared(x);
normalize(x);
faceforward(x, incident, surface);
reflect(x, normal);
refract(x, normal, eta);
fmin(x, y);
fmax(x, y); |
Names follow vek x.clamped(0.3, 0.4);
x.magnitude();
x.magnitude_squared();
x.normalized();
x.face_forward(incident, surface);
x.reflected(normal);
x.refracted(normal, eta);
x.min(y);
x.max(y); |
Methods with different argument order |
mix(0.3, 0.4, a);
smoothstep(0.3, 0.4, x);
step(0.3, x); |
When one argument is special from the others it is used as the receiver of the method call. a.mix(0.3, 0.4);
x.smoothstep(0.3, 0.4);
x.step(0.3); |
Right now only modules in the same directory as the main file will be watched.
Only files in the same directory are supported for mod <name>
s, <name>/mod.rs
is not supported.
There is no support for path
attribute on mod
s.
Variables cannot be redeclared. (for now)
In Rust we could use a single generic vec2
constructor for all Vec2
s. But this would require actually using rustc
to compile the constructors to the concrete Metal C++ constructors. To keep things simpler, the Rust bindings here require specifying the constructors directly (vec2bool
-> bool2
).
Print the compiled shader without opening the window:
cargo test -- --nocapture