Skip to content

Commit d4550cb

Browse files
committed
Add ray tracing project
1 parent 71fbc1f commit d4550cb

File tree

3 files changed

+213
-24
lines changed

3 files changed

+213
-24
lines changed

README.md

+10-3
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,18 @@ This project is a real-time ray tracer implemented as a GPU shader. Ray tracing
147147

148148
> `pbrt` is based on the _ray-tracing_ algorithm. Ray tracing is an elegant technique that has its origins in lens making; Carl Friedrich Gauß traced rays through lenses by hand in the 19th century. Ray-tracing algorithms on computers follow the path of infinitesimal rays of light through the scene until they intersect a surface. This approach gives a simple method for finding the first visible object as seen from any particular position and direction and is the basis for many rendering algorithms.
149149
150+
<p align="center">
151+
<a href="#ray-tracing">
152+
<img src="https://i.imgur.com/1xaJmxY.png" width="600">
153+
</a>
154+
</p>
155+
150156
You might find this [non-technical introductory video by Disney](https://youtu.be/frLwRLS_ZR0) to provide helpful context.
151157

152-
1. To parallelize our ray tracer and run it on a GPU shader, we will use a geometry modeling technique known as [_signed distance fields (SDFs)_](https://en.wikipedia.org/wiki/Signed_distance_function). The `map()` function returns a signed distance corresponding to the closest point on the surface of any object in the scene.
153-
2. **Task 1:** Fill in the code for the `march()` function, which takes a ray as input and returns the point of first intersection with any object in the scene. [Hint: You will need to call the `map()` function in a loop.]
154-
3. **Task 2:** Implement shadows. In the `illuminate()` function, before doing lighting calculations, add a conditional statement that first checks if the ray from the point to the light source is unobstructed.
158+
1. To parallelize our ray tracer and run it on a GPU shader, we revert back to running our fragment shader once for every pixel in the screen, like in the 2D graphics examples. However, this time, our shader will do some geometry calculations by shooting rays from the camera for each pixel. The `trace()` function models this behavior by returning the color corresponding to a given ray.
159+
2. To model geometry, the starter code has set up a simple scene involving three spheres and one circle forming the checkered floor. This geometry is located in the `intersect()` function, which computes the first intersection point (represented by a `Hit` struct) of any ray in the space. Following the pattern shown here, add a fourth sphere to the scene with a different position, color, and radius.
160+
3. The `calcLighting()` function uses `illuminate()`, which contains a simple implementation of a Phong shader, to compute the lighting at a given point by adding up the contributions of two point lights. Modify this code to add a third point light at a different location, with your choice of intensity. (For inspiration, see [three-point lighting](https://en.wikipedia.org/wiki/Three-point_lighting).)
161+
4. **Task:** Implement shadows. In the `illuminate()` function, before doing lighting calculations, add a conditional statement checks if the ray from the point to the light source is obstructed or not. If it is obstructed, then you should immediately return `vec3(0.0)` to simulate shadows. [Hint: You will need to use the `intersect()` function with a new `Ray` object that you construct, starting from the point `pos`.]
155162

156163
If you're interested in learning more about ray tracing, check out [rpt](https://github.com/ekzhang/rpt/), which is a physically-based path tracer written in Rust with a lot more features.
157164

shaders/raytracing.frag.glsl

+151-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,160 @@
11
precision mediump float;
22

3+
const float epsilon = 0.001;
4+
const float inf = 1e9;
5+
36
uniform vec2 resolution;
4-
uniform float time;
7+
uniform vec3 eye;
8+
uniform vec3 center;
9+
uniform vec3 background;
10+
uniform bool antialias;
11+
12+
struct Ray {
13+
vec3 origin;
14+
vec3 dir;
15+
};
16+
17+
struct Material {
18+
vec3 kd;
19+
vec3 ks;
20+
bool metal;
21+
bool checker;
22+
};
23+
24+
struct Hit {
25+
float time;
26+
vec3 normal;
27+
Material material;
28+
};
29+
30+
// Trace a ray to a sphere, using high school geometry
31+
void sphere(inout Hit h, Ray r, vec4 s, Material m) {
32+
// Rescale to unit sphere at the origin
33+
r.origin = (r.origin - s.xyz) / s.w;
34+
r.dir = r.dir / s.w;
35+
36+
// Quadratic formula
37+
float a = dot(r.dir, r.dir);
38+
float b = dot(r.dir, r.origin);
39+
float c = dot(r.origin, r.origin) - 1.0;
40+
41+
float d = b * b - a * c;
42+
if (d < 0.0) {
43+
return;
44+
}
45+
46+
d = sqrt(d);
47+
float t = (-b - d) / a;
48+
if (t < epsilon) {
49+
t = (-b + d) / a;
50+
}
51+
52+
if (t >= epsilon && t < h.time) {
53+
h.time = t;
54+
h.normal = normalize(r.origin + r.dir * t);
55+
h.material = m;
56+
}
57+
}
58+
59+
void circle(inout Hit h, Ray r, float y, float radius, Material m) {
60+
float t = (y - r.origin.y) / r.dir.y;
61+
if (t >= epsilon && t < h.time
62+
&& length(r.origin + t * r.dir) < radius) {
63+
h.time = t;
64+
h.normal = vec3(0.0, 1.0, 0.0);
65+
h.material = m;
66+
}
67+
}
68+
69+
// Intersect a ray with the scene
70+
Hit intersect(Ray r) {
71+
Hit h = Hit(inf, vec3(0.0), Material(vec3(0.0), vec3(0.0), false, false));
72+
sphere(h, r, vec4(0.8, -1.0, -10.0, 1.0),
73+
Material(vec3(0.4, 0.2, 0.8), vec3(0.8), false, false));
74+
sphere(h, r, vec4(-2.5, -0.2, -12.0, 1.8),
75+
Material(vec3(1.0, 0.4, 0.2), vec3(0.8), true, false));
76+
sphere(h, r, vec4(-3.5, -1.2, -6.0, 0.8),
77+
Material(vec3(0.2, 0.6, 0.3), vec3(0.8), false, false));
78+
circle(h, r, -2.0, 50.0,
79+
Material(vec3(0.8, 0.8, 0.8), vec3(0.0), false, true));
80+
return h;
81+
}
82+
83+
// Compute lighting from one light
84+
vec3 illuminate(vec3 lightPosition, vec3 pos, vec3 wo, Hit h) {
85+
vec3 wi = lightPosition - pos;
86+
vec3 kd = h.material.kd;
87+
if (h.material.checker) {
88+
// Checkerboard pattern for the floor
89+
vec2 coords = floor(pos.xz);
90+
kd = vec3(mod(coords.x + coords.y, 2.0) * 0.8 + 0.2);
91+
}
92+
float intensity = 1.0 / dot(wi, wi); // inverse-square law
93+
vec3 diffuse = kd * max(dot(normalize(wi), h.normal), 0.0);
94+
95+
// Non-dielectric materials have tinted reflections
96+
vec3 ks = h.material.metal ? h.material.kd : h.material.ks;
97+
vec3 r = -reflect(normalize(wi), h.normal);
98+
vec3 specular = ks * pow(max(dot(r, wo), 0.0), 10.0);
99+
100+
return intensity * (diffuse + specular);
101+
}
102+
103+
// Compute total lighting at a given point
104+
vec3 calcLighting(vec3 pos, vec3 wo, Hit h) {
105+
vec3 color = vec3(0.0);
106+
color += 100.0 * illuminate(vec3(-3.0, 10.0, 0.0), pos, wo, h);
107+
color += 200000.0 * illuminate(vec3(0.0, 1000.0, 0.0), pos, wo, h);
108+
return color;
109+
}
110+
111+
// Trace a ray, returning an RGB color based on its value
112+
vec3 trace(Ray r) {
113+
Hit h = intersect(r);
114+
if (h.time != inf) {
115+
vec3 pos = r.origin + h.time * r.dir;
116+
vec3 color = calcLighting(pos, -r.dir, h);
117+
if (h.material.metal) {
118+
vec3 dir = reflect(r.dir, h.normal);
119+
Hit h2 = intersect(Ray(pos, dir));
120+
if (h2.time < inf) {
121+
vec3 pos2 = pos + h2.time * dir;
122+
color += 0.2 * h.material.ks * calcLighting(pos2, -dir, h2);
123+
} else {
124+
color += 0.2 * h.material.ks * background;
125+
}
126+
}
127+
return color;
128+
}
129+
return background;
130+
}
131+
132+
vec3 tracePixel(vec2 coord) {
133+
// Pixel coordinates, normalized so that p.y in range [-1, 1]
134+
vec2 p = (2.0 * coord - resolution) / resolution.y;
135+
136+
// View ray from camera
137+
vec3 ww = normalize(center - eye);
138+
vec3 uu = normalize(cross(ww, vec3(0.0, 1.0, 0.0)));
139+
vec3 vv = normalize(cross(uu, ww));
140+
// (Note: cot(pi/12) = 2 + sqrt(3) = 3.73)
141+
vec3 dir = normalize(p.x * uu + p.y * vv + 3.73 * ww);
142+
143+
return trace(Ray(eye, dir));
144+
}
5145

6146
void main() {
7-
vec2 coord = gl_FragCoord.xy / resolution;
147+
vec3 color = vec3(0.0);
8148

9-
// Output RGB color in range from 0.0 to 1.0
10-
vec3 color = vec3(coord.x, coord.y, 0.0);
11-
color.z += abs(sin(time));
149+
if (antialias) {
150+
// Anti-aliasing by supersampling multiple rays
151+
color += 0.25 * tracePixel(gl_FragCoord.xy + vec2(-0.25, -0.25));
152+
color += 0.25 * tracePixel(gl_FragCoord.xy + vec2(-0.25, +0.25));
153+
color += 0.25 * tracePixel(gl_FragCoord.xy + vec2(+0.25, -0.25));
154+
color += 0.25 * tracePixel(gl_FragCoord.xy + vec2(+0.25, +0.25));
155+
} else {
156+
color += tracePixel(gl_FragCoord.xy);
157+
}
12158

13159
gl_FragColor = vec4(color, 1.0);
14160
}

src/index.js

+52-16
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ function initPane() {
3131
kd: { r: 95, g: 230, b: 213 },
3232
ks: { r: 240, g: 240, b: 240 },
3333
shininess: 5.0,
34+
background: { r: 120, g: 178, b: 255 },
35+
antialias: true,
3436
};
3537

3638
pane.addInput(params, "project", {
@@ -66,6 +68,8 @@ function initPane() {
6668
pane.addInput(params, "shininess", { min: 1, max: 9 }),
6769
["shading", "contours"],
6870
],
71+
[pane.addInput(params, "background"), ["raytracing"]],
72+
[pane.addInput(params, "antialias"), ["raytracing"]],
6973
];
7074

7175
pane.addMonitor(params, "fps");
@@ -105,6 +109,39 @@ async function updateMesh(path) {
105109
mesh = await resp.json();
106110
}
107111

112+
function toColor(color) {
113+
return [color.r / 255, color.g / 255, color.b / 255];
114+
}
115+
116+
function compileShaders() {
117+
try {
118+
return {
119+
quilt: regl({
120+
frag: shaders.quiltFrag,
121+
vert: shaders.quiltVert,
122+
}),
123+
landscape: regl({
124+
frag: shaders.landscapeFrag,
125+
vert: shaders.landscapeVert,
126+
}),
127+
shading: regl({
128+
frag: shaders.shadingFrag,
129+
vert: shaders.shadingVert,
130+
}),
131+
contours: regl({
132+
frag: shaders.contoursFrag,
133+
vert: shaders.contoursVert,
134+
}),
135+
raytracing: regl({
136+
frag: shaders.raytracingFrag,
137+
vert: shaders.raytracingVert,
138+
}),
139+
};
140+
} catch {
141+
return null;
142+
}
143+
}
144+
108145
const common = regl({
109146
attributes: {
110147
position: [
@@ -134,17 +171,13 @@ const common = regl({
134171
},
135172
});
136173

137-
const draw = {
174+
const setup = {
138175
quilt: regl({
139-
frag: () => shaders.quiltFrag,
140-
vert: () => shaders.quiltVert,
141176
uniforms: {
142177
seed: () => params.seed,
143178
},
144179
}),
145180
landscape: regl({
146-
frag: () => shaders.landscapeFrag,
147-
vert: () => shaders.landscapeVert,
148181
uniforms: {
149182
seed: () => params.seed,
150183
scale: () => params.scale,
@@ -156,34 +189,34 @@ const draw = {
156189
normal: () => mesh.normals,
157190
},
158191
uniforms: {
159-
kd: () => [params.kd.r / 255, params.kd.g / 255, params.kd.b / 255],
160-
ks: () => [params.ks.r / 255, params.ks.g / 255, params.ks.b / 255],
192+
kd: () => toColor(params.kd),
193+
ks: () => toColor(params.ks),
161194
shininess: () => params.shininess,
162195
},
163196
elements: () => mesh.elements,
164-
frag: () => shaders.shadingFrag,
165-
vert: () => shaders.shadingVert,
166197
}),
167198
contours: regl({
168199
attributes: {
169200
position: () => mesh.vertices,
170201
normal: () => mesh.normals,
171202
},
172203
uniforms: {
173-
kd: () => [params.kd.r / 255, params.kd.g / 255, params.kd.b / 255],
174-
ks: () => [params.ks.r / 255, params.ks.g / 255, params.ks.b / 255],
204+
kd: () => toColor(params.kd),
205+
ks: () => toColor(params.ks),
175206
shininess: () => params.shininess,
176207
},
177208
elements: () => mesh.elements,
178-
frag: () => shaders.contoursFrag,
179-
vert: () => shaders.contoursVert,
180209
}),
181210
raytracing: regl({
182-
frag: () => shaders.raytracingFrag,
183-
vert: () => shaders.raytracingVert,
211+
uniforms: {
212+
background: () => toColor(params.background),
213+
antialias: () => params.antialias,
214+
},
184215
}),
185216
};
186217

218+
let draw = compileShaders();
219+
187220
updateMesh(params.mesh).then(() => {
188221
const frameTimes = [...Array(60)].fill(0);
189222
regl.frame(() => {
@@ -196,7 +229,9 @@ updateMesh(params.mesh).then(() => {
196229
common(() => {
197230
if (params.project === "contours") regl.clear({ color: [1, 1, 1, 1] });
198231
else regl.clear({ color: [0, 0, 0, 1] });
199-
draw[params.project]();
232+
setup[params.project](() => {
233+
if (draw) draw[params.project]();
234+
});
200235
});
201236
});
202237
});
@@ -205,5 +240,6 @@ updateMesh(params.mesh).then(() => {
205240
if (import.meta.hot) {
206241
import.meta.hot.accept("./shaders.js", (module) => {
207242
Object.assign(shaders, module.default);
243+
draw = compileShaders();
208244
});
209245
}

0 commit comments

Comments
 (0)