Created
June 15, 2020 17:22
-
-
Save gabriel-fallen/a3724bae16fbcaca64ad518e60eee7bc to your computer and use it in GitHub Desktop.
SDF-based Ray Marching with GPU.js
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"> | |
<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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Revision 1 gets incorrectly compiled.