Last active
August 3, 2022 20:03
-
-
Save sporsh/95f6eacb6d0a6be59a92046319a22d49 to your computer and use it in GitHub Desktop.
WebAssembly SDF rendering
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#ifndef BASIS_H | |
#define BASIS_H | |
#include "vector3.h" | |
typedef struct { | |
v3 normal; | |
v3 tangent; | |
v3 bitangent; | |
} basis; | |
static inline v3 v3_from_local_basis(const basis* b, const v3* v) | |
{ | |
// Transform a vector from a local basis to wold | |
return (v3) { | |
.x = v3_dot(v, &b->tangent), | |
.y = v3_dot(v, &b->bitangent), | |
.z = v3_dot(v, &b->normal) | |
}; | |
} | |
static inline v3 v3_to_local_basis(const basis* b, const v3* v) | |
{ | |
// Transform a vector from world coordinates to a local basis | |
return (v3) { | |
.x = b->tangent.x * v->x + b->bitangent.x * v->y + b->normal.x * v->z, | |
.y = b->tangent.y * v->x + b->bitangent.y * v->y + b->normal.y * v->z, | |
.z = b->tangent.z * v->x + b->bitangent.z * v->y + b->normal.z * v->z, | |
}; | |
} | |
static inline v3 orthoginal_unit_vector(const v3* v) | |
{ | |
double f = sqrt(v->x * v->x + v->z * v->z); | |
return v3_normalize(&(v3) { | |
.x = v->z * f, | |
.y = 0, | |
.z = -v->x * f }); | |
} | |
static inline basis arbitrary_basis_from_normal(const v3 normal) | |
{ | |
v3 tangent = orthoginal_unit_vector(&normal); | |
return (basis) { | |
.normal = normal, | |
.tangent = tangent, | |
.bitangent = v3_cross(&tangent, &normal) | |
}; | |
} | |
#endif // BASIS_H |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/sh | |
clang \ | |
--pedantic \ | |
--std=c11 \ | |
--target=wasm32 \ | |
-O3 \ | |
-flto \ | |
-nostdlib \ | |
-Wl,--no-entry \ | |
-Wl,--export-all \ | |
-Wl,--lto-O3 \ | |
-Wl,--allow-undefined-file=wasm.syms \ | |
-Wl,--import-memory \ | |
-o dist/render.wasm \ | |
render.c | |
# -Wl,-z,stack-size=8388608 \ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#ifndef CAMERA_H | |
#define CAMERA_H | |
#include "basis.h" | |
#include "math.h" | |
#include "vector3.h" | |
typedef struct { | |
v3 position; | |
double aperture; | |
double field_of_view; | |
double focal_length; | |
basis b; | |
} camera; | |
typedef struct { | |
v3 origin; | |
v3 direction; | |
double t_min; | |
double t_max; | |
} ray; | |
ray get_ray_through(const camera* c, double u, double v) | |
{ | |
v3 origin = random_origin_within_aperture(c->aperture, &c->position, &c->b); | |
v3 target = v3_add( | |
&c->position, | |
&v3_to_local_basis(&c->b, | |
&(v3) { | |
.x = u * c->field_of_view * c->focal_length, | |
.y = v * c->field_of_view * c->focal_length, | |
.z = c->focal_length })); | |
v3 direction = v3_normalize(&v3_sub(&target, &origin)); | |
return (ray) | |
{ | |
.direction = direction, | |
.origin = origin, | |
t_min = EPSILON, | |
t_max = | |
} | |
} | |
v3 random_origin_within_aperture(double aperture, const v3* position, const basis* b) | |
{ | |
const v2 point_on_disc = random_point_on_disc(aperture); | |
return v3_add( | |
position, | |
&v3_from_local_basis( | |
b, | |
&(v3) { | |
.x = point_on_disc.x, | |
.y = point_on_disc.y, | |
.z = 0 })); | |
} | |
typedef struct { | |
double x; | |
double y; | |
} v2; | |
static inline double random() | |
{ | |
return rand() / RAND_MAX; | |
} | |
v2 random_point_on_disc(double radius) | |
{ | |
double random_angle = random() * 2 * M_PI; | |
double r = sqrt(random()) * radius; | |
return polar_to_cartesian(r, random_angle); | |
} | |
v2 polar_to_cartesian(double r, double phi) | |
{ | |
return (v2) { | |
.x = r * cos(phi), | |
.y = r * sin(phi) | |
}; | |
} | |
#endif // CAMERA_H |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#ifndef COLOR_H | |
#define COLOR_H | |
#include <stdint.h> | |
typedef union { | |
struct { | |
unsigned char r; | |
unsigned char g; | |
unsigned char b; | |
unsigned char a; | |
}; | |
uint32_t value; | |
} color; | |
#endif // COLOR_H |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
FROM silkeh/clang:14 as build | |
WORKDIR /app | |
COPY . . | |
RUN mkdir dist | |
RUN ./build.sh | |
FROM nginx:1.23.1 | |
COPY --from=build /app /usr/share/nginx/html |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en" dir="ltr"> | |
<head> | |
<meta charset="utf-8" /> | |
<title></title> | |
<script type="module"> | |
const width = 400; | |
const height = 400; | |
const canvas = document.createElement("canvas"); | |
canvas.width = width; | |
canvas.height = height; | |
document.body.appendChild(canvas); | |
// Memory sizes are in # pages (each 64KiB) | |
const bufferSize = Math.ceil((4 * width * height) / 64 / 1000); // Make sure it's enough to contain our buffer | |
const memory = new WebAssembly.Memory({ initial: 1 + bufferSize }); | |
async function init() { | |
var importObject = { | |
env: { | |
sin: Math.sin, | |
cos: Math.cos, | |
pow: Math.pow, | |
memory, | |
}, | |
}; | |
const { instance } = await WebAssembly.instantiateStreaming( | |
fetch("./dist/render.wasm"), | |
importObject | |
); | |
console.log("instance", instance); | |
// instance.memory.grow(10); | |
// const buffer_address = instance.exports.data.value; | |
const buffer_address = instance.exports.__heap_base.value; | |
console.log("buffer_address", buffer_address); | |
const data = new Uint8ClampedArray( | |
memory.buffer, | |
buffer_address, | |
// 4 * width * 31 // * height | |
4 * width * height | |
); | |
const image = new ImageData(data, width, height); | |
const ctx = canvas.getContext("2d"); | |
const render = (time) => { | |
instance.exports.render(buffer_address, width, height, time); | |
ctx.putImageData(image, 0, 0); | |
window.requestAnimationFrame(render); | |
}; | |
render(performance.now()); | |
} | |
init(); | |
</script> | |
</head> | |
<body></body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "color.h" | |
#include "vector3.h" | |
// #define WIDTH 512 | |
// #define HEIGHT 512 | |
// #define DATA_SIZE (WIDTH * HEIGHT) | |
// unsigned int data[DATA_SIZE]; | |
// WASM imports | |
double sin(double); | |
double cos(double); | |
double pow(double, double); | |
// #define pow __builtin_pow | |
const double t_min = 0.0001; | |
const double t_max = 20.0; | |
const int MAX_STEPS = 512; | |
typedef struct { | |
double radius; | |
v3 center; | |
} sphere; | |
static inline double min(double a, double b) | |
{ | |
return a < b ? a : b; | |
} | |
static inline double max(double a, double b) | |
{ | |
return a > b ? a : b; | |
} | |
static inline double op_union(double d1, double d2) | |
{ | |
return min(d1, d2); | |
} | |
static inline double op_subtract(double d1, double d2) | |
{ | |
return max(-d1, d2); | |
} | |
static inline double sphere_distance(const sphere* sphere, const v3* point) | |
{ | |
const v3 sp = v3_sub(point, &sphere->center); | |
return v3_length(&sp) - sphere->radius; | |
} | |
double distance_at_point(const v3* point, double time) | |
{ | |
double d1 = sphere_distance( | |
&(sphere) { | |
.radius = 1, | |
.center = (v3) { | |
// .x = cos(time / 1000) * 1.5, | |
.x = 1.5, | |
// .y = sin(time / 1000) * 1.5, | |
.y = 0, | |
.z = 6 } }, | |
point); | |
double d2 = sphere_distance( | |
&(sphere) { | |
.radius = 0.8, | |
.center = (v3) { | |
// .x = 1.5, | |
.x = 1.5 + cos(-time / 1000) * 0.5, | |
.y = 0, | |
// .z = 6.5 } }, | |
.z = 6 + sin(-time / 1000) * 0.5 } }, | |
point); | |
double d_spheres = op_subtract(d2, d1); | |
// double d_spheres = op_subtract(d1, d2); | |
// double d_spheres = op_union(d1, d2); | |
// return d_spheres; | |
// double d_ground = point->y + 2; | |
double d_ground = sphere_distance( | |
&(sphere) { | |
.radius = 1000, | |
.center = (v3) { | |
.x = 0, | |
.y = -1000 - 1, | |
.z = 0 } }, | |
point); | |
// return d_ground; | |
return op_union(d_spheres, d_ground); | |
} | |
v3 normal_at_point(const v3* point, double time) | |
{ | |
// Approximate the normal for the SDF using central difference | |
const double d = distance_at_point(point, time); | |
const double x = point->x, y = point->y, z = point->z; | |
return v3_normalize(&(v3) { | |
.x = d - distance_at_point(&(v3) { x - t_min, y, z }, time), | |
.y = d - distance_at_point(&(v3) { x, y - t_min, z }, time), | |
.z = d - distance_at_point(&(v3) { x, y, z - t_min }, time) }); | |
} | |
static inline double clamp(double value) | |
{ | |
return value < 0 ? 0 : (value > 1 ? 1 : value); | |
} | |
static inline double gamma(double color) | |
{ | |
return pow(color, 1 / 2.2); | |
} | |
unsigned int get_color_at_point(const v3* point, double time) | |
{ | |
// const v3 key_light_position = (v3) { | |
// .x = -0.5703, | |
// // .x = cos(time / 1000) * 1.5, | |
// .y = 0.5703, | |
// // .y = cos(time / 2000) * 1.5, | |
// .z = -0.5703 | |
// // .z = sin(time / 1000) * 1.5, | |
// }; | |
const v3 key_light_position = (v3) { -1, 1, -1 }; | |
// const v3 key_light_color = (v3) { 0.7, 0.6, 0.3 }; | |
const v3 key_light_color = (v3) { 0.4, 0.4, 0.4 }; | |
// const v3 ambient_light_color = (v3) { 0.2, 0.3, 0.4 }; | |
const v3 ambient_light_color = (v3) { 0, 0, 0 }; | |
const v3 normal = normal_at_point(point, time); | |
const v3 key_light = v3_scale(&key_light_color, clamp(v3_dot(&normal, &key_light_position))); | |
const v3 rim_light_color = (v3) { 0.4, 0.6, 0.8 }; | |
const v3 rim_light_position = (v3) { 2.5, 2.5, 5 }; | |
const v3 rim_light = v3_scale(&rim_light_color, clamp(v3_dot(&normal, &rim_light_position))); | |
const v3 fill_light_color = (v3) { 0.1, 0.1, 0.1 }; | |
const v3 fill_light_position = (v3) { 1, -1, -1 }; | |
const v3 fill_light = v3_scale(&fill_light_color, clamp(v3_dot(&normal, &fill_light_position))); | |
const v3 ambient = v3_scale(&ambient_light_color, clamp(0.5 + 0.5 * normal.y)); | |
// const v3 radiance = v3_add(&key_light, &ambient); | |
const v3 radiance = (v3) { | |
.x = key_light.x + fill_light.x + rim_light.x + ambient.x, | |
.y = key_light.y + fill_light.y + rim_light.y + ambient.y, | |
.z = key_light.z + fill_light.z + rim_light.z + ambient.z | |
}; | |
return (color) { | |
.r = (int)(clamp(gamma(radiance.x)) * 0xff), | |
.g = (int)(clamp(gamma(radiance.y)) * 0xff), | |
.b = (int)(clamp(gamma(radiance.z)) * 0xff), | |
.a = 0xff | |
} | |
.value; | |
} | |
unsigned int sample(double u, double v, double time) | |
{ | |
const v3 direction = v3_normalize(&(v3) { .x = u, .y = -v, .z = 1 }); | |
// March along ray | |
double t = 0.5; | |
for (int i = 0; i < MAX_STEPS && t < t_max; i++) { | |
v3 intersection_point = v3_scale(&direction, t); | |
const double d = distance_at_point(&intersection_point, time); | |
if (d < t_min) { | |
return get_color_at_point(&intersection_point, time); | |
} | |
t += d; | |
} | |
return (color) { | |
.r = (unsigned char)(0.2 * 0xff), | |
.g = (unsigned char)(0.3 * 0xff), | |
.b = (unsigned char)(0.4 * 0xff), | |
.a = 0xff | |
} | |
.value; | |
} | |
void render(unsigned int data[], unsigned int width, unsigned int height, double time) | |
{ | |
for (int y = 0; y < height; y++) { | |
for (int x = 0; x < width; x++) { | |
unsigned int i = x + y * width; | |
double u = (double)x / width - 0.5; | |
double v = (double)y / height - 0.5; | |
data[i] = sample(u, v, time); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/sh | |
docker run --rm -it -p 8080:80 $(docker build -q .) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#ifndef VECTOR3_H | |
#define VECTOR3_H | |
// TODO: Stop relying on clang builting sqrt for WASM. | |
#define sqrt __builtin_sqrt | |
typedef struct { | |
const double | |
x, | |
y, | |
z; | |
} v3; | |
static inline v3 v3_add(const v3* a, const v3* b) | |
{ | |
return (v3) { | |
.x = a->x + b->x, | |
.y = a->y + b->y, | |
.z = a->z + b->z | |
}; | |
} | |
static inline v3 v3_sub(const v3* a, const v3* b) | |
{ | |
return (v3) { | |
.x = a->x - b->x, | |
.y = a->y - b->y, | |
.z = a->z - b->z | |
}; | |
} | |
static inline double v3_length2(const v3* v) | |
{ | |
return (v->x * v->x) + (v->y * v->y) + (v->z * v->z); | |
} | |
static inline double v3_length(const v3* v) | |
{ | |
return sqrt(v3_length2(v)); | |
} | |
static inline double v3_dot(const v3* a, const v3* b) | |
{ | |
return a->x * b->x + a->y * b->y + a->z * b->z; | |
} | |
static inline v3 v3_cross(const v3* a, const v3* b) | |
{ | |
return (v3) { | |
.x = a->y * b->z - a->z * b->y, | |
.y = a->z * b->x - a->x * b->z, | |
.z = a->x * b->y - a->y * b->x | |
}; | |
} | |
static inline v3 v3_scale(const v3* v, const double s) | |
{ | |
return (v3) { | |
.x = v->x * s, | |
.y = v->y * s, | |
.z = v->z * s | |
}; | |
} | |
static inline v3 v3_normalize(const v3* v) | |
{ | |
return v3_scale(v, 1 / v3_length(v)); | |
} | |
#endif // VECTOR3_H |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
sin | |
cos | |
pow |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment