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.
- HTML/Vanilla JS
- React
<!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>
import { useGL } from 'glre/react'
const TutorialComponent = () => {
const gl = useGL({
frag: fragmentShader,
// Configuration goes here
})
return <canvas ref={gl.ref} />
}
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 calculationmin(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 normalizationeye
,focus
,up
: Used to build camera matrixfocal
: Controls field of viewsize
: 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:
look
: Direction vector from camera to focal pointright
: Right direction calculated from cross product oflook
andup
scr
: Convert pixel coordinates to screen-centered coordinate systemdir
: 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:
- Start ray from camera position
- Evaluate distance function at each step
- If distance is small enough, surface is reached
- Otherwise, advance ray by that distance
- Return background color if maximum steps reached
JavaScript Implementation
Initializing glre
- Vanilla JS
- React
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()
}
import { useGL } from 'glre/react'
import { useEffect } from 'react'
const RotatingCube = () => {
const gl = useGL({
frag: fragmentShader,
init() {
// Initial setup
this.uniform({
up: [0, 1, 0],
focus: [0, 0, 0],
focal: 500,
size: 50,
})
},
loop() {
// Animation loop
const time = performance.now() / 1000
const radius = 200
const x = radius * Math.cos(time)
const z = radius * Math.sin(time)
this.uniform({ eye: [x, 0, z] })
},
})
return <canvas ref={gl.ref} />
}