Skip to main content

Creating a scene

Building 3D worlds with shaders.

In this chapter, we'll create a rotating cube using ray marching with glre. You'll learn shader programming fundamentals from basic concepts to implementation, step by step.

Prerequisites

Environment Setup

Before using glre, you need a canvas to display your graphics.

<!DOCTYPE html>
<html>
<head>
<title>glre Tutorial</title>
</head>
<body>
<canvas id="myCanvas"></canvas>
<script type="module">
import createGL from 'https://esm.sh/glre'

// Shader and JavaScript code goes here
const fragmentShader = `
// GLSL code will be written here
`

function App() {
// Application logic will be written here
}

document.addEventListener('DOMContentLoaded', App)
</script>
</body>
</html>

Understanding Ray Marching

What is Ray Marching?

Ray marching is a technique for rendering 3D objects using mathematical distance functions. Unlike traditional polygon-based rendering, it casts rays from each pixel to find intersections with objects.

Distance Functions (SDF)

A Signed Distance Function (SDF) returns the shortest distance from any point in space to an object's surface.

// Box distance function
float boxSDF(vec3 p, float side) {
vec3 d = abs(p) - side;
return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d, 0.0));
}

How this function works:

  • abs(p) - side: Distance from cube boundary on each axis
  • Returns negative values inside, positive values outside
  • length(max(d, 0.0)): Outside distance calculation
  • min(max(d.x, max(d.y, d.z)), 0.0): Inside distance calculation

Building the Fragment Shader

Defining Uniform Variables

Define parameters to pass to the shader.

#version 300 es
precision highp float;

// Canvas information
uniform vec2 iResolution; // Canvas size

// Camera parameters
uniform vec3 up; // Camera up direction
uniform vec3 eye; // Camera position
uniform vec3 focus; // Camera focal point
uniform float focal; // Focal length

// Object parameters
uniform float size; // Cube size

out vec4 fragColor;

Role of each parameter:

  • iResolution: Screen resolution for UV coordinate normalization
  • eye, focus, up: Used to build camera matrix
  • focal: Controls field of view
  • size: Cube dimensions

Implementing Camera System

void main() {
// Ray marching camera setup
vec3 look = normalize(focus - eye); // View direction
vec3 right = normalize(cross(look, up)); // Right direction

// Convert screen coordinates to center origin
vec2 scr = gl_FragCoord.xy - iResolution * 0.5;

// Calculate ray direction
vec3 dir = normalize(focal * look + scr.x * right + scr.y * up);
}

Calculation details:

  1. look: Direction vector from camera to focal point
  2. right: Right direction calculated from cross product of look and up
  3. scr: Convert pixel coordinates to screen-centered coordinate system
  4. dir: Calculate actual ray direction from focal length and screen coordinates

Ray Marching Algorithm

void main() {
// ... Camera setup ...

// Ray marching initialization
vec3 p = eye; // Current position
vec3 epsilon = vec3(0.0005, 0.0, 0.0); // Precision control

// Ray marching loop
for (int i = 0; i < 50; i++) {
float d = boxSDF(p, size); // Distance from current position to cube

if (d <= epsilon.x) {
// Normal calculation when surface is reached
float dx = boxSDF(p + epsilon.xyy, size) - d;
float dy = boxSDF(p + epsilon.yxy, size) - d;
float dz = boxSDF(p + epsilon.yyx, size) - d;

vec3 normal = normalize(vec3(dx, dy, dz));

// Output normal as color
fragColor = vec4(normal * 0.5 + 0.5, 1.0);
return;
}

// Advance ray by distance
p = p + d * dir;
}

// Nothing hit - return background
fragColor = vec4(0.0, 0.0, 0.0, 1.0);
}

Algorithm mechanics:

  1. Start ray from camera position
  2. Evaluate distance function at each step
  3. If distance is small enough, surface is reached
  4. Otherwise, advance ray by that distance
  5. Return background color if maximum steps reached

JavaScript Implementation

Initializing glre

function App() {
const gl = createGL({
el: document.getElementById('myCanvas'),
frag: fragmentShader,
isWebGL: true, // Force WebGL (omit if WebGPU available)

// Animation loop
render() {
const time = performance.now() / 1000
const radius = 200

// Rotate camera in circular orbit
const x = radius * Math.cos(time)
const z = radius * Math.sin(time)

gl.uniform({ eye: [x, 0, z] })
},
})

// Set initial uniform values
gl.uniform({
up: [0, 1, 0], // Y-axis is up
focus: [0, 0, 0], // Look at origin
focal: 500, // Focal length
size: 50, // Cube size
})

gl.mount()
}