Soyuz is a procedural 3D asset generation framework. You write scripts that describe shapes mathematically, and Soyuz renders them in real-time and exports them as 3D meshes.
// This is a complete Soyuz script - it creates a donut
torus(0.5, 0.15)
Soyuz uses Signed Distance Fields (SDFs) to represent 3D shapes. Understanding this mental model is the key to using Soyuz effectively.
An SDF is a function that takes any point in 3D space and returns the distance to the nearest surface:
- Positive values = outside the shape
- Negative values = inside the shape
- Zero = exactly on the surface
You never work with vertices or polygons directly. Instead, you:
- Create primitives - Basic shapes like spheres, cubes, cylinders
- Combine them - Union (add), subtract (cut), intersect (overlap)
- Transform them - Translate, rotate, scale, mirror
- Modify them - Round edges, make hollow, twist, bend
The SDF approach means:
- Smooth blending between shapes is trivial
- Boolean operations (cutting holes, combining parts) just work
- Complex organic shapes emerge from simple operations
- Real-time preview via GPU raymarching
- Rust toolchain (1.75+)
- Linux with X11 or Wayland (primary target)
git clone https://github.com/noahsabaj/soyuz
cd soyuz
# Build all binaries
cargo build --release
# The binaries are:
# ./target/release/soyuz-studio (desktop IDE)
# ./target/release/soyuz-preview (preview window)cargo run --release -p appThis opens the full IDE with:
- Code editor with syntax highlighting
- Real-time 3D preview
- File browser
- Export controls
Your first shape:
- The editor starts with an empty script
- Type:
sphere(0.5) - Press
Ctrl+Enterto preview - A window opens showing your sphere rendered in real-time
Scripts are written in Rhai, a JavaScript-like language. Here's the essential pattern:
// Your script must RETURN an SDF (no semicolon on the last line)
sphere(0.5)
All primitives are centered at the origin. Dimensions are in world units.
sphere(radius) // Ball
cube(size) // Box with equal sides
box3(width, height, depth) // Rectangular box
cylinder(radius, height) // Cylinder along Y axis
capsule(radius, height) // Pill shape
torus(major_radius, minor_radius) // Donut
cone(radius, height) // Cone pointing up
Use method chaining to combine shapes:
// Add shapes together
sphere(0.5).union(cube(0.8))
// Cut one shape from another
cube(1.0).subtract(sphere(0.7)) // Cube with spherical hole
// Keep only the overlap
sphere(0.6).intersect(cube(0.8)) // Rounded cube
Smooth versions blend shapes organically:
// k controls the blend radius (0.05-0.2 are common values)
sphere(0.4).smooth_union(sphere(0.4).translate_x(0.6), 0.15)
shape.translate(x, y, z) // Move
shape.translate_x(x) // Move along one axis
shape.rotate_x(angle) // Rotate (radians!)
shape.rotate_y(deg(45.0)) // Use deg() for degrees
shape.scale(factor) // Uniform scale
shape.mirror_x() // Mirror across YZ plane
shape.symmetry_x() // Fold space (instant symmetry)
shape.shell(thickness) // Make hollow
shape.round(radius) // Round all edges
shape.twist(amount) // Twist around Y axis
shape.bend(amount) // Bend around Y axis
shape.repeat_polar(count) // Repeat in a circle around Y
shape.repeat_limited( // Finite 3D grid
sx, sy, sz, // Spacing
cx, cy, cz // Count per axis
)
Here's a gear - it demonstrates the typical workflow:
let teeth_count = 12;
let outer_radius = 1.0;
let inner_radius = 0.3;
let thickness = 0.2;
let tooth_size = 0.15;
// Main body - a cylinder
let body = cylinder(outer_radius - tooth_size, thickness);
// Center hole - subtract this later
let hole = cylinder(inner_radius, thickness + 0.1);
// Single tooth, positioned at the edge
let tooth = box3(tooth_size * 2.0, thickness, tooth_size * 1.5)
.translate(outer_radius - tooth_size * 0.3, 0.0, 0.0);
// Repeat the tooth around the gear
let teeth = tooth.repeat_polar(teeth_count);
// Spoke holes for visual interest
let spoke = cylinder(0.08, thickness + 0.1)
.translate((outer_radius + inner_radius) / 2.0, 0.0, 0.0);
let spokes = spoke.repeat_polar(6);
// Final assembly: body + teeth - hole - spokes
body.union(teeth).subtract(hole).subtract(spokes)
| Shortcut | Action |
|---|---|
Ctrl+Enter |
Run preview |
Ctrl+N |
New tab |
Ctrl+W |
Close tab |
Ctrl+S |
Save file |
Ctrl+O |
Open file |
Ctrl+Z |
Undo |
Ctrl+Shift+Z |
Redo |
Ctrl+\ |
Split pane vertically |
Ctrl+Shift+\ |
Split pane horizontally |
- Format: GLB (binary, recommended), GLTF (JSON + binary), OBJ (legacy)
- Resolution: Controls mesh density (32=fast preview, 128=high quality)
- Optimize: Reduces vertex count while preserving shape
Configure the rendering environment in your script:
// Use a preset
env_sunset(); // Warm lighting
env_studio(); // Neutral (default)
env_clay(); // Soft AO look
// Or customize
set_sun_direction(1.0, 1.0, 0.5);
set_sun_color(1.0, 0.95, 0.9);
set_material_color_hex("#ff5500");
set_ao_enabled(true);
// Then define your shape
sphere(0.5)
soyuz/
app/ # Desktop IDE (Dioxus)
crates/
soyuz-math/ # Mathematical formulas (generates Rust + WGSL)
soyuz-core/ # SDF engine, mesh generation, export
soyuz-render/ # GPU raymarching renderer
soyuz-script/ # Rhai scripting integration
soyuz-engine/ # High-level orchestration (render + script)
examples/ # Sample scripts
SOYUZ_COOKBOOK.md # Complete scripting reference
// shell() creates a hollow version with wall thickness
sphere(0.5).shell(0.05)
Make the cutting shape slightly larger to ensure clean geometry:
let body = cylinder(0.5, 1.0);
let hole = cylinder(0.2, 1.1); // Taller than body
body.subtract(hole)
Primitives are centered at origin. Translate to position:
let base = cylinder(0.5, 0.2).translate_y(-0.1);
let top = sphere(0.4).translate_y(0.3);
base.union(top)
Position a shape away from center, then repeat around Y:
let spoke = box3(0.1, 0.5, 0.05).translate_x(0.5);
spoke.repeat_polar(8) // 8 spokes in a circle
Higher k = more blending:
// k=0.05: subtle fillet
// k=0.15: noticeable blend
// k=0.3+: blobby, organic
sphere(0.3).smooth_union(sphere(0.3).translate_x(0.4), 0.15)
Instead of building both sides, use symmetry:
// Build one side, mirror automatically
let half = sphere(0.3).translate_x(0.5);
half.symmetry_x() // Creates both sides
Preview window is black
- Check the status bar for script errors
- Ensure your script returns an SDF (no trailing semicolon)
Mesh has holes or artifacts
- Increase export resolution
- Check for self-intersecting geometry
- Ensure cutting shapes fully penetrate
Script doesn't update
- Press
Ctrl+Enterto refresh preview - Check for syntax errors in status bar
Performance is slow
- Reduce preview resolution
- Avoid infinite repetition (
repeat()) - userepeat_limited()instead - Simplify complex smooth blending chains
- SOYUZ_COOKBOOK.md - Complete API reference with all primitives, operations, and recipes
- examples/ - Working script examples
MIT OR Apache-2.0