Skip to content

Instantly share code, notes, and snippets.

@gabriel-fallen
Created June 15, 2020 17:22
Show Gist options
  • Save gabriel-fallen/a3724bae16fbcaca64ad518e60eee7bc to your computer and use it in GitHub Desktop.
Save gabriel-fallen/a3724bae16fbcaca64ad518e60eee7bc to your computer and use it in GitHub Desktop.
SDF-based Ray Marching with GPU.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>GPU.js ray marching</title>
<script src="gpu-browser.js"></script>
</head>
<body>
<button id="render">Render!</button>
<p>Render:</p>
<div id="out"></div>
<script>
const gpu = new GPU();
const MAX_MARCHING_STEPS = 255;
const MIN_DIST = 0.0;
const MAX_DIST = 100.0;
const EPSILON = 0.0001;
const width = 600, height = 600;
function marchAll() {
const viewSize = [this.constants.width, this.constants.height],
fragCoord = [this.thread.x, this.thread.y],
MIN_DIST = this.constants.MIN_DIST,
MAX_DIST = this.constants.MAX_DIST;
// Inlined functions start
/*vec3*/ function toWorldDir(/*vec3*/ eye, /*vec3*/ center, /*vec3*/ up, /*vec3*/ v) {
const /*vec3*/ f = vec3normalize(vec3sub(center, eye));
const /*vec3*/ f_m = vec3scale(f, -1);
const /*vec3*/ s = vec3normalize(vec3cross(f, up));
const /*vec3*/ u = vec3cross(s, f);
return [
vec3dot([s[0], s[1], s[2]], v),
vec3dot([u[0], u[1], u[2]], v),
vec3dot([f_m[0], f_m[1], f_m[2]], v)
];
}
// Inlined functions end
const /*vec3*/ viewDir = rayDirection(60.0, viewSize, fragCoord);
const /*vec3*/ eye = [8.0, 5.0, 7.0];
// const /*mat3*/ viewToWorld = viewMatrix(eye, [0.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
// const /*vec3*/ worldDir = mat3vec3mul(viewToWorld, viewDir);
const /*vec3*/ worldDir = toWorldDir(eye, [0.0, 0.0, 0.0], [0.0, 1.0, 0.0], viewDir);
const /*float*/ dist = shortestDistanceToSurface(eye, worldDir, MIN_DIST, MAX_DIST);
if (dist > MAX_DIST - this.constants.EPSILON) {
// Didn't hit anything
this.color(0.0, 0.0, 0.0);
// Can't return, it won't compile :(
} else {
// The closest point on the surface to the eyepoint along the view ray
const /*vec3*/ p = vec3add(eye, vec3scale(worldDir, dist));
const /*vec3*/ K_a = [0.2, 0.2, 0.2];
const /*vec3*/ K_d = [0.7, 0.2, 0.2];
const /*vec3*/ K_s = [1.0, 1.0, 1.0];
const shininess = 10.0;
const pixel = phongIllumination(K_a, K_d, K_s, shininess, p, eye);
this.color(pixel[0], pixel[1], pixel[2]);
}
}
const kernel = gpu.createKernel(marchAll)
.setConstants({ width, height, MIN_DIST, MAX_DIST, MAX_MARCHING_STEPS, EPSILON })
.setGraphical(true)
.setOutput([width, height]);
/**
* Standard vector math functions.
*/
function vec2add(a, b) {
return [a[0] + b[0], a[1] + b[1]];
}
kernel.addFunction(vec2add, { argumentTypes: { a: 'Array(2)', b: 'Array(2)'}, returnType: 'Array(2)' });
function vec3add(a, b) {
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
}
kernel.addFunction(vec3add, { argumentTypes: { a: 'Array(3)', b: 'Array(3)'}, returnType: 'Array(3)' });
function vec2sub(a, b) {
return [a[0] - b[0], a[1] - b[1]];
}
kernel.addFunction(vec2sub, { argumentTypes: { a: 'Array(2)', b: 'Array(2)'}, returnType: 'Array(2)' });
function vec3sub(a, b) {
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
}
kernel.addFunction(vec3sub, { argumentTypes: { a: 'Array(3)', b: 'Array(3)'}, returnType: 'Array(3)' });
function vec2mul(a, b) {
return [a[0] * b[0], a[1] * b[1]];
}
kernel.addFunction(vec2mul, { argumentTypes: { a: 'Array(2)', b: 'Array(2)'}, returnType: 'Array(2)' });
function vec3mul(a, b) {
return [a[0] * b[0], a[1] * b[1], a[2] * b[2]];
}
kernel.addFunction(vec3mul, { argumentTypes: { a: 'Array(3)', b: 'Array(3)'}, returnType: 'Array(3)' });
function vec2scale(a, sc) {
return [a[0] * sc, a[1] * sc];
}
kernel.addFunction(vec2scale, { argumentTypes: { a: 'Array(2)', b: 'Float'}, returnType: 'Array(2)' });
function vec3scale(a, sc) {
return [a[0] * sc, a[1] * sc, a[2] * sc];
}
kernel.addFunction(vec3scale, { argumentTypes: { a: 'Array(3)', b: 'Float'}, returnType: 'Array(3)' });
function vec2dot(a, b) {
return (a[0] * b[0] + a[1] * b[1]);
}
kernel.addFunction(vec2dot, { argumentTypes: { a: 'Array(2)', b: 'Array(2)'}, returnType: 'Float' });
function vec3dot(a, b) {
return (a[0] * b[0] + a[1] * b[1] + a[2] * b[2]);
}
kernel.addFunction(vec3dot, { argumentTypes: { a: 'Array(3)', b: 'Array(3)'}, returnType: 'Float' });
function vec3cross(a, b) {
return [
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0]
];
}
kernel.addFunction(vec3cross, { argumentTypes: { a: 'Array(3)', b: 'Array(3)'}, returnType: 'Array(3)' });
function vec2length(a) {
return Math.sqrt(vec2dot(a, a));
}
kernel.addFunction(vec2length, { argumentTypes: { a: 'Array(2)' }, returnType: 'Float' });
function vec3length(a) {
return Math.sqrt(vec3dot(a, a));
}
kernel.addFunction(vec3length, { argumentTypes: { a: 'Array(3)' }, returnType: 'Float' });
function vec2normalize(a) {
const mag = vec2length(a);
return vec2scale(a, 1 / mag);
}
kernel.addFunction(vec2normalize, { argumentTypes: { a: 'Array(2)' }, returnType: 'Array(2)' });
function vec3normalize(a) {
const mag = vec3length(a);
return vec3scale(a, 1 / mag);
}
kernel.addFunction(vec3normalize, { argumentTypes: { a: 'Array(3)' }, returnType: 'Array(3)' });
function vec3reflect(/*vec3*/ v, /*vec3*/ n) {
const vn = vec3dot(v, n);
return vec3sub(v, vec3scale(n, 2*vn));
}
kernel.addFunction(vec3reflect, { argumentTypes: { v: 'Array(3)', n: 'Array(3)'}, returnType: 'Array(3)' });
/**
* Constructive solid geometry intersection operation on SDF-calculated distances.
*/
function intersectSDF(/*float*/ distA, /*float*/ distB) {
return Math.max(distA, distB);
}
kernel.addFunction(intersectSDF, { argumentTypes: { distA: 'Float', distB: 'Float' }, returnType: 'Float' });
/**
* Constructive solid geometry union operation on SDF-calculated distances.
*/
function unionSDF(/*float*/ distA, /*float*/ distB) {
return Math.min(distA, distB);
}
kernel.addFunction(unionSDF, { argumentTypes: { distA: 'Float', distB: 'Float' }, returnType: 'Float' });
/**
* Constructive solid geometry difference operation on SDF-calculated distances.
*/
function differenceSDF(/*float*/ distA, /*float*/ distB) {
return Math.max(distA, -distB);
}
kernel.addFunction(differenceSDF, { argumentTypes: { distA: 'Float', distB: 'Float' }, returnType: 'Float' });
/**
* Signed distance function for a cube centered at the origin
* with dimensions specified by size.
*/
function boxSDF(/*vec3*/ p, /*vec3*/ size) {
const /*vec3*/ d = vec3sub([Math.abs(p[0]), Math.abs(p[1]), Math.abs(p[2])], vec3scale(size, 0.5));
// Assuming p is inside the cube, how far is it from the surface?
// Result will be negative or zero.
const /*float*/ insideDistance = Math.min(Math.max(d[0], Math.max(d[1], d[2])), 0.0);
// Assuming p is outside the cube, how far is it from the surface?
// Result will be positive or zero.
const /*float*/ outsideDistance = vec3length([Math.max(d[0], 0.0), Math.max(d[1], 0.0), Math.max(d[2], 0.0)]);
return insideDistance + outsideDistance;
}
kernel.addFunction(boxSDF, { argumentTypes: { p: 'Array(3)', size: 'Array(3)' }, returnType: 'Float' });
/**
* Signed distance function for a sphere centered at the origin with radius r.
*/
function sphereSDF(/*vec3*/ p, /*float*/ r) {
return vec3length(p) - r;
}
kernel.addFunction(sphereSDF, { argumentTypes: { p: 'Array(3)', r: 'Float' }, returnType: 'Float' });
/**
* Signed distance function describing the scene.
*
* Absolute value of the return value indicates the distance to the surface.
* Sign indicates whether the point is inside or outside the surface,
* negative indicating inside.
*/
function sceneSDF(/*vec3*/ samplePoint) {
return boxSDF(samplePoint, [1.0, 1.0, 1.0]);
}
kernel.addFunction(sceneSDF, { argumentTypes: { samplePoint: 'Array(3)' }, returnType: 'Float' });
/**
* Return the shortest distance from the eyepoint to the scene surface along
* the marching direction. If no part of the surface is found between start and end,
* return end.
*
* eye: the eye point, acting as the origin of the ray
* marchingDirection: the normalized direction to march in
* start: the starting distance away from the eye
* end: the max distance away from the ey to march before giving up
*/
/*float*/ function shortestDistanceToSurface(/*vec3*/ eye, /*vec3*/ marchingDirection, /*float*/ start, /*float*/ end) {
let depth = start;
for (let i = 0; i < this.constants.MAX_MARCHING_STEPS; i++) {
const dist = sceneSDF(vec3add(eye, vec3scale(marchingDirection, depth)));
if (dist < this.constants.EPSILON) {
return depth;
}
depth += dist;
if (depth >= end) {
return end;
}
}
return end;
}
kernel.addFunction(shortestDistanceToSurface, { argumentTypes: { eye: 'Array(3)', marchingDirection: 'Array(3)', start: 'Float', end: 'Float' }, returnType: 'Float', constants: { MAX_MARCHING_STEPS, EPSILON } });
/**
* Return the normalized direction to march in from the eye point for a single pixel.
*
* fieldOfView: vertical field of view in degrees
* size: resolution of the output image
* fragCoord: the x,y coordinate of the pixel in the output image
*/
/*vec3*/ function rayDirection(/*float*/ fieldOfView, /*vec2*/ size, /*vec2*/ fragCoord) {
const radians = Math.PI*fieldOfView/180.0;
const /*float*/ x = fragCoord[0] - size[0] / 2.0;
const /*float*/ y = fragCoord[1] - size[1] / 2.0;
const /*float*/ z = size[1] / Math.tan(radians / 2.0);
return vec3normalize([x, y, -z]);
}
kernel.addFunction(rayDirection, { argumentTypes: { size: 'Array(2)', fragCoord: 'Array(2)', fieldOfView: 'Float' }, returnType: 'Array(3)' });
/**
* Using the gradient of the SDF, estimate the normal on the surface at point p.
*/
/*vec3*/ function estimateNormal(/*vec3*/ p) {
const EPSILON = this.constants.EPSILON;
return vec3normalize([
sceneSDF([p[0] + EPSILON, p[1], p[2]]) - sceneSDF([p[0] - EPSILON, p[1], p[2]]),
sceneSDF([p[0], p[1] + EPSILON, p[2]]) - sceneSDF([p[0], p[1] - EPSILON, p[2]]),
sceneSDF([p[0], p[1], p[2] + EPSILON]) - sceneSDF([p[0], p[1], p[2] - EPSILON])
]);
}
kernel.addFunction(estimateNormal, { argumentTypes: { p: 'Array(3)' }, returnType: 'Array(3)', constants: { EPSILON } });
/**
* Lighting contribution of a single point light source via Phong illumination.
*
* The vec3 returned is the RGB color of the light's contribution.
*
* k_a: Ambient color
* k_d: Diffuse color
* k_s: Specular color
* alpha: Shininess coefficient
* p: position of point being lit
* eye: the position of the camera
* lightPos: the position of the light
* lightIntensity: color/intensity of the light
*
* See https://en.wikipedia.org/wiki/Phong_reflection_model#Description
*/
/*vec3*/ function phongContribForLight(/*vec3*/ k_d, /*vec3*/ k_s, /*float*/ alpha, /*vec3*/ p, /*vec3*/ eye,
/*vec3*/ lightPos, /*vec3*/ lightIntensity) {
const /*vec3*/ N = estimateNormal(p);
const /*vec3*/ L = vec3normalize(vec3sub(lightPos, p));
const /*vec3*/ V = vec3normalize(vec3sub(eye, p));
const /*vec3*/ R = vec3normalize(vec3reflect(vec3scale(L, -1), N));
const /*float*/ dotLN = vec3dot(L, N);
const /*float*/ dotRV = vec3dot(R, V);
if (dotLN < 0.0) {
// Light not visible from this point on the surface
return [0.0, 0.0, 0.0];
}
const k_d_scaled = vec3scale(k_d, dotLN);
if (dotRV < 0.0) {
// Light reflection in opposite direction as viewer, apply only diffuse
// component
return vec3mul(lightIntensity, k_d_scaled);
}
const k_s_scaled = vec3scale(k_s, Math.pow(dotRV, alpha));
return vec3mul(lightIntensity, vec3add(k_d_scaled, k_s_scaled));
}
kernel.addFunction(phongContribForLight);
/**
* Lighting via Phong illumination.
*
* The vec3 returned is the RGB color of that point after lighting is applied.
* k_a: Ambient color
* k_d: Diffuse color
* k_s: Specular color
* alpha: Shininess coefficient
* p: position of point being lit
* eye: the position of the camera
*
* See https://en.wikipedia.org/wiki/Phong_reflection_model#Description
*/
/*vec3*/ function phongIllumination(/*vec3*/ k_a, /*vec3*/ k_d, /*vec3*/ k_s, /*float*/ alpha, /*vec3*/ p, /*vec3*/ eye) {
const /*vec3*/ ambientLight = [0.5, 0.5, 0.5];
let /*vec3*/ color = vec3mul(ambientLight, k_a);
const /*vec3*/ light1Pos = [4.0/* * sin(iTime)*/,
2.0,
4.0/* * cos(iTime)*/];
const /*vec3*/ light1Intensity = [0.4, 0.4, 0.4];
color += phongContribForLight(k_d, k_s, alpha, p, eye,
light1Pos,
light1Intensity);
const /*vec3*/ light2Pos = [2.0/* * sin(0.37 * iTime)*/,
2.0/* * cos(0.37 * iTime)*/,
2.0];
const /*vec3*/ light2Intensity = [0.4, 0.4, 0.4];
color += phongContribForLight(k_d, k_s, alpha, p, eye,
light2Pos,
light2Intensity);
return color;
}
kernel.addFunction(phongIllumination);
// Uncompilable functions start
/*vec3*/ function mat3vec3mul(m, v) {
return [
vec3dot([m[0], m[1], m[2]], v),
vec3dot([m[3], m[4], m[5]], v),
vec3dot([m[6], m[7], m[8]], v)
];
}
/**
* Return a transform matrix that will transform a ray from view space
* to world coordinates, given the eye point, the camera target, and an up vector.
*
* This assumes that the center of the camera is aligned with the negative z axis in
* view space when calculating the ray marching direction. See rayDirection.
*/
/*mat3*/ function viewMatrix(/*vec3*/ eye, /*vec3*/ center, /*vec3*/ up) {
// Based on gluLookAt man page
const /*vec3*/ f = vec3normalize(vec3sub(center, eye));
const /*vec3*/ f_m = vec3scale(f, -1);
const /*vec3*/ s = vec3normalize(vec3cross(f, up));
const /*vec3*/ u = vec3cross(s, f);
return [
s[0], s[1], s[2],
u[0], u[1], u[2],
f_m[0], f_m[1], f_m[2]
];
}
// Uncompilable functions end
function render() {
kernel();
// Result: colorful image
const out = document.getElementById('out');
out.firstChild && out.removeChild(out.firstChild);
out.appendChild(kernel.canvas);
}
const button = document.getElementById('render');
button.addEventListener('click', render);
</script>
</body>
</html>
@gabriel-fallen
Copy link
Author

Revision 1 gets incorrectly compiled.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment