gpu.js/examples/raytracer.html

513 lines
19 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>GPU.js Raytracer</title>
<meta name="author" content="Stacey Tay">
<meta name="description" content="A simple raytracer built with GPU.js.">
<link href='https://fonts.googleapis.com/css?family=Open+Sans' rel='stylesheet' type='text/css'>
<style>
html, body {
font-family: 'Open Sans', sans-serif;
}
canvas {
display: block;
margin: auto;
}
.center {
text-align: center;
}
#container {
margin: auto;
max-width: 600px;
}
</style>
</head>
<body>
<div id="container">
<div class="center">
<p>
From https://staceytay.com/raytracer/. A simple ray tracer with Lambertian and specular reflection,
built with <a href="http://gpu.rocks/">GPU.js</a>. Read more
about ray tracing and GPU.js in
my <a href="http://staceytay.com/2016/04/20/a-parallelized-ray-tracer-in-the-browser.html">blog
post</a>. Code available
on <a href="https://github.com/staceytay/raytracer/">GitHub</a>.
</p>
</div>
<div class="center">
<label for="cpu"><input type="radio" name="mode" value="cpu" id="cpu">CPU</label>
<label for="gpu"><input type="radio" name="mode" value="gpu" id="gpu">GPU</label>
</div>
<div class="center">
<label for="lambert"><input checked="checked" type="checkbox" id="lambert" value="lambert">Lambertian reflectance</label><br>
<label for="specular"><input checked="checked" type="checkbox" id="specular" value="specular">Specular reflection</label>
</div>
<div class="center">
<input type="button" value="Pause" onclick="togglePause ()" id="pause" />
</div>
<div class="center">
<span id="fps"></span><span> fps</span>
</div>
</div>
<script src="../bin/gpu-browser.min.js"></script>
<script>
var Vector = (function () {
function Vector(x, y, z) {
this.x = x;
this.y = y;
this.z = z;
}
Vector.prototype.toArray = function () {
return [this.x, this.y, this.z];
};
Vector.times = function (k, v) {
return new Vector(k * v.x, k * v.y, k * v.z);
};
Vector.minus = function (v1, v2) {
return new Vector(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z);
};
Vector.plus = function (v1, v2) {
return new Vector(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z);
};
Vector.dot = function (v1, v2) {
return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
};
Vector.magnitude = function (v) {
return Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
};
Vector.norm = function (v) {
var mag = Vector.magnitude(v);
var div = (mag === 0) ? Infinity : 1.0 / mag;
return Vector.times(div, v);
};
Vector.cross = function (v1, v2) {
return new Vector(v1.y * v2.z - v1.z * v2.y, v1.z * v2.x - v1.x * v2.z, v1.x * v2.y - v1.y * v2.x);
};
return Vector;
}());
var Mode;
(function (Mode) {
Mode[Mode["GPU"] = 0] = "GPU";
Mode[Mode["CPU"] = 1] = "CPU";
})(Mode || (Mode = {}));
var Thing;
(function (Thing) {
Thing[Thing["SPHERE"] = 0] = "SPHERE";
})(Thing || (Thing = {}));
var stringOfMode = function (mode) {
switch (mode) {
case 1: return 'cpu';
case 0: return 'gpu';
}
};
var height = (window.innerWidth < 600) ? (window.innerWidth - 20) : 600;
var width = height;
var initialMode = 1;
var camera = [
0, 0, 16, 0, 0, 1, 45
];
var lights = [
[-16, 16, 8],
[16, 16, 8],
];
var things = [
[0, 13,
1.0, 0.0, 0.0,
0.3, 0.7, 0.2, 1.0,
-2, 2, -2, 1],
[0, 13,
0.0, 1.0, 0.0,
0.3, 0.7, 0.2, 1.0,
0, 2, 0, 1],
[0, 13,
0.0, 0.0, 1.0,
0.3, 0.7, 0.2, 1.0,
2, 2, 2, 1],
[0, 13,
0.0, 1.0, 1.0,
0.3, 0.7, 0.2, 1.0,
-2, -2, 2, 1],
[0, 13,
1.0, 0.0, 1.0,
0.3, 0.7, 0.2, 1.0,
0, -2, 0, 1],
[0, 13,
1.0, 1.0, 0.0,
0.3, 0.7, 0.2, 1.0,
2, -2, -2, 1],
];
var constants = {
LIGHTSCOUNT: lights.length,
THINGSCOUNT: things.length
};
var opt = function (mode) {
return {
constants: constants,
debug: false,
dimensions: [width, height],
graphical: true,
mode: stringOfMode(mode),
safeTextureReadHack: false,
output: [width, height]
};
};
var gpu = new GPU();
var cpu = new GPU({ mode: 'cpu'});
function vectorDotProduct(V1x, V1y, V1z, V2x, V2y, V2z) {
return (V1x * V2x) + (V1y * V2y) + (V1z * V2z);
}
function unitVectorX(Vx, Vy, Vz) {
var magnitude = Math.sqrt((Vx * Vx) + (Vy * Vy) + (Vz * Vz));
var div = 1.0 / magnitude;
return div * Vx;
}
function unitVectorY(Vx, Vy, Vz) {
var magnitude = Math.sqrt((Vx * Vx) + (Vy * Vy) + (Vz * Vz));
var div = 1.0 / magnitude;
return div * Vy;
}
function unitVectorZ(Vx, Vy, Vz) {
var magnitude = Math.sqrt((Vx * Vx) + (Vy * Vy) + (Vz * Vz));
var div = 1.0 / magnitude;
return div * Vz;
}
function sphereNormalX(Sx, Sy, Sz, radius, Px, Py, Pz) {
var SPx = Px - Sx;
var SPy = Py - Sy;
var SPz = Pz - Sz;
var magnitude = (SPx * SPx) + (SPy * SPy) + (SPz * SPz);
var div = Infinity;
if (magnitude > 0)
div = 1.0 / magnitude;
return div * SPx;
}
function sphereNormalY(Sx, Sy, Sz, radius, Px, Py, Pz) {
var SPx = Px - Sx;
var SPy = Py - Sy;
var SPz = Pz - Sz;
var magnitude = (SPx * SPx) + (SPy * SPy) + (SPz * SPz);
var div = Infinity;
if (magnitude > 0)
div = 1.0 / magnitude;
return div * SPy;
}
function sphereNormalZ(Sx, Sy, Sz, radius, Px, Py, Pz) {
var SPx = Px - Sx;
var SPy = Py - Sy;
var SPz = Pz - Sz;
var magnitude = (SPx * SPx) + (SPy * SPy) + (SPz * SPz);
var div = Infinity;
if (magnitude > 0)
div = 1.0 / magnitude;
return div * SPz;
}
function vectorReflectX(Vx, Vy, Vz, Nx, Ny, Nz) {
var V1x = ((Vx * Nx) + (Vy * Ny) + (Vz * Nz)) * Nx;
return (V1x * 2) - Vx;
}
function vectorReflectY(Vx, Vy, Vz, Nx, Ny, Nz) {
var V1y = ((Vx * Nx) + (Vy * Ny) + (Vz * Nz)) * Ny;
return (V1y * 2) - Vy;
}
function vectorReflectZ(Vx, Vy, Vz, Nx, Ny, Nz) {
var V1z = ((Vx * Nx) + (Vy * Ny) + (Vz * Nz)) * Nz;
return (V1z * 2) - Vz;
}
function sphereIntersectionDistance(Sx, Sy, Sz, radius, Ex, Ey, Ez, Vx, Vy, Vz) {
var EOx = Sx - Ex;
var EOy = Sy - Ey;
var EOz = Sz - Ez;
var v = (EOx * Vx) + (EOy * Vy) + (EOz * Vz);
var discriminant = (radius * radius)
- ((EOx * EOx) + (EOy * EOy) + (EOz * EOz))
+ (v * v);
if (discriminant < 0) {
return Infinity;
}
else {
return v - Math.sqrt(discriminant);
}
}
var kernelFunctions = [
vectorDotProduct,
unitVectorX, unitVectorY, unitVectorZ,
sphereNormalX, sphereNormalY, sphereNormalZ,
vectorReflectX, vectorReflectY, vectorReflectZ,
sphereIntersectionDistance
];
kernelFunctions.forEach(function (f) {
cpu.addFunction(f);
gpu.addFunction(f);
});
var createKernel = function (mode) {
var kernel = (mode === 0 ? gpu : cpu).createKernel(function (camera, lights, things, eyeV, rightV, upV, halfHeight, halfWidth, pixelHeight, pixelWidth, lambertianReflectance, specularReflection) {
var x = this.thread.x;
var y = this.thread.y;
var rayPx = camera[0];
var rayPy = camera[1];
var rayPz = camera[2];
var xCompVx = ((x * pixelWidth) - halfWidth) * rightV[0];
var xCompVy = ((x * pixelWidth) - halfWidth) * rightV[1];
var xCompVz = ((x * pixelWidth) - halfWidth) * rightV[2];
var yCompVx = ((y * pixelHeight) - halfHeight) * upV[0];
var yCompVy = ((y * pixelHeight) - halfHeight) * upV[1];
var yCompVz = ((y * pixelHeight) - halfHeight) * upV[2];
var sumVx = eyeV[0] + xCompVx + yCompVx;
var sumVy = eyeV[1] + xCompVy + yCompVy;
var sumVz = eyeV[2] + xCompVz + yCompVz;
var rayVx = unitVectorX(sumVx, sumVy, sumVz);
var rayVy = unitVectorY(sumVx, sumVy, sumVz);
var rayVz = unitVectorZ(sumVx, sumVy, sumVz);
var closest = this.constants.THINGSCOUNT;
var closestDistance = Infinity;
for (var i = 0; i < this.constants.THINGSCOUNT; i++) {
var distance = sphereIntersectionDistance(things[i][9], things[i][10], things[i][11], things[i][12], rayPx, rayPy, rayPz, rayVx, rayVy, rayVz);
if (distance < closestDistance) {
closest = i;
closestDistance = distance;
}
}
if (closestDistance < Infinity) {
var px = rayPx + rayVx * closestDistance;
var py = rayPy + rayVy * closestDistance;
var pz = rayPz + rayVz * closestDistance;
var sx = things[closest][9];
var sy = things[closest][10];
var sz = things[closest][11];
var sRadius = things[closest][12];
var snVx = sphereNormalX(sx, sy, sz, sRadius, px, py, pz);
var snVy = sphereNormalY(sx, sy, sz, sRadius, px, py, pz);
var snVz = sphereNormalZ(sx, sy, sz, sRadius, px, py, pz);
var sRed = things[closest][2];
var sGreen = things[closest][3];
var sBlue = things[closest][4];
var ambient = things[closest][7];
var lambert = things[closest][6];
var lambertAmount = 0;
if (lambertianReflectance > 0 && lambert > 0) {
for (var i = 0; i < this.constants.LIGHTSCOUNT; i++) {
var LPx = px - lights[i][0];
var LPy = py - lights[i][1];
var LPz = pz - lights[i][2];
var uLPx = unitVectorX(LPx, LPy, LPz);
var uLPy = unitVectorY(LPx, LPy, LPz);
var uLPz = unitVectorZ(LPx, LPy, LPz);
var closestDistance_1 = Infinity;
for (var j = 0; j < this.constants.THINGSCOUNT; j++) {
var distance = Infinity;
var EOx = things[j][9] - px;
var EOy = things[j][10] - py;
var EOz = things[j][11] - pz;
var v = (EOx * uLPx) + (EOy * uLPy) + (EOz * uLPz);
var radius = things[j][12];
var discriminant = (radius * radius)
- ((EOx * EOx) + (EOy * EOy) + (EOz * EOz))
+ (v * v);
if (discriminant >= 0) {
distance = v - Math.sqrt(discriminant);
}
if (distance < closestDistance_1) {
closestDistance_1 = distance;
}
}
if (closestDistance_1 > -0.005) {
var PLx = -LPx;
var PLy = -LPy;
var PLz = -LPz;
var uPLx = unitVectorX(PLx, PLy, PLz);
var uPLy = unitVectorY(PLx, PLy, PLz);
var uPLz = unitVectorZ(PLx, PLy, PLz);
var contribution = vectorDotProduct(uPLx, uPLy, uPLz, snVx, snVy, snVz);
if (contribution > 0)
lambertAmount += contribution;
}
}
}
if (lambertianReflectance > 0)
lambertAmount = Math.min(1, lambertAmount);
var specular = things[closest][5];
var cVx = 0;
var cVy = 0;
var cVz = 0;
if (specularReflection > 0 && specular > 0) {
var rRayPx = px;
var rRayPy = py;
var rRayPz = pz;
var rRayVx = vectorReflectX(rayVx, rayVy, rayVz, snVx, snVy, snVz);
var rRayVy = vectorReflectY(rayVx, rayVy, rayVz, snVx, snVy, snVz);
var rRayVz = vectorReflectZ(rayVx, rayVy, rayVz, snVx, snVy, snVz);
var closest_1 = this.constants.THINGSCOUNT;
var closestDistance_2 = Infinity;
for (var i = 0; i < this.constants.THINGSCOUNT; i++) {
var distance = sphereIntersectionDistance(things[i][9], things[i][10], things[i][11], things[i][12], rRayPx, rRayPy, rRayPz, rRayVx, rRayVy, rRayVz);
if (distance < closestDistance_2) {
closest_1 = i;
closestDistance_2 = distance;
}
}
var reflectedRed = 1;
var reflectedGreen = 1;
var reflectedBlue = 1;
if (closestDistance_2 < Infinity) {
var px_1 = rRayPx + rRayVx * closestDistance_2;
var py_1 = rRayPy + rRayVy * closestDistance_2;
var pz_1 = rRayPz + rRayVz * closestDistance_2;
var sx_1 = things[closest_1][9];
var sy_1 = things[closest_1][10];
var sz_1 = things[closest_1][11];
var sRadius_1 = things[closest_1][12];
var snVx_1 = sphereNormalX(sx_1, sy_1, sz_1, sRadius_1, px_1, py_1, pz_1);
var snVy_1 = sphereNormalY(sx_1, sy_1, sz_1, sRadius_1, px_1, py_1, pz_1);
var snVz_1 = sphereNormalZ(sx_1, sy_1, sz_1, sRadius_1, px_1, py_1, pz_1);
var rsRed = things[closest_1][2];
var rsGreen = things[closest_1][3];
var rsBlue = things[closest_1][4];
var rambient = things[closest_1][7];
var rlambert = things[closest_1][6];
var rlambertAmount = 0;
if (lambertianReflectance > 0 && rlambert > 0) {
for (var i = 0; i < this.constants.LIGHTSCOUNT; i++) {
var LPx = px_1 - lights[i][0];
var LPy = py_1 - lights[i][1];
var LPz = pz_1 - lights[i][2];
var uLPx = unitVectorX(LPx, LPy, LPz);
var uLPy = unitVectorY(LPx, LPy, LPz);
var uLPz = unitVectorZ(LPx, LPy, LPz);
var closestDistance_3 = Infinity;
for (var j = 0; j < this.constants.THINGSCOUNT; j++) {
var distance = Infinity;
var EOx = things[j][9] - px_1;
var EOy = things[j][10] - py_1;
var EOz = things[j][11] - pz_1;
var v = (EOx * uLPx) + (EOy * uLPy) + (EOz * uLPz);
var radius = things[j][12];
var discriminant = (radius * radius)
- ((EOx * EOx) + (EOy * EOy) + (EOz * EOz))
+ (v * v);
if (discriminant >= 0) {
distance = v - Math.sqrt(discriminant);
}
if (distance < closestDistance_3) {
closestDistance_3 = distance;
}
}
if (closestDistance_3 > -0.005) {
var PLx = -LPx;
var PLy = -LPy;
var PLz = -LPz;
var uPLx = unitVectorX(PLx, PLy, PLz);
var uPLy = unitVectorY(PLx, PLy, PLz);
var uPLz = unitVectorZ(PLx, PLy, PLz);
var contribution = vectorDotProduct(uPLx, uPLy, uPLz, snVx_1, snVy_1, snVz_1);
if (contribution > 0)
rlambertAmount += contribution;
}
}
}
if (lambertianReflectance > 0)
rlambertAmount = Math.min(1, rlambertAmount);
reflectedRed = (rsRed * rlambertAmount * rlambert) + (rsRed * rambient);
reflectedGreen = (rsGreen * rlambertAmount * rlambert) + (rsGreen * rambient);
reflectedBlue = (rsBlue * rlambertAmount * rlambert) + (rsBlue * rambient);
cVx = cVx + (specular * reflectedRed);
cVy = cVy + (specular * reflectedGreen);
cVz = cVz + (specular * reflectedBlue);
}
}
var red = cVx + (sRed * lambertAmount * lambert) + (sRed * ambient);
var green = cVy + (sGreen * lambertAmount * lambert) + (sGreen * ambient);
var blue = cVz + (sBlue * lambertAmount * lambert) + (sBlue * ambient);
this.color(red, green, blue);
}
else {
this.color(0.95, 0.95, 0.95);
}
}, opt(mode));
return kernel;
};
var fps = {
startTime: 0,
frameNumber: 0,
getFPS: function () {
this.frameNumber++;
var d = new Date().getTime();
var currentTime = (d - this.startTime) / 1000;
var result = Math.floor(this.frameNumber / currentTime);
if (currentTime > 1) {
this.startTime = new Date().getTime();
this.frameNumber = 0;
}
return result;
}
};
var cameraPoint = new Vector(camera[0], camera[1], camera[2]);
var cameraVector = new Vector(camera[3], camera[4], camera[5]);
var eyeVector = Vector.norm(Vector.minus(cameraVector, cameraPoint));
var vpRight = Vector.norm(Vector.cross(eyeVector, new Vector(0, 1, 0)));
var vpUp = Vector.norm(Vector.cross(vpRight, eyeVector));
var fovRadians = Math.PI * (camera[6] / 2) / 180;
var heightWidthRatio = height / width;
var halfWidth = Math.tan(fovRadians);
var halfHeight = heightWidthRatio * halfWidth;
var cameraWidth = halfWidth * 2;
var cameraHeight = halfHeight * 2;
var pixelWidth = cameraWidth / (width - 1);
var pixelHeight = cameraHeight / (height - 1);
var gpuKernel = createKernel(0);
var cpuKernel = createKernel(1);
var gpuCanvas = gpuKernel.canvas;
var cpuCanvas = cpuKernel.canvas;
document.getElementById(stringOfMode(initialMode)).checked = true;
document.getElementById('container')
.appendChild((initialMode === 1) ? cpuCanvas : gpuCanvas);
var requestId = null;
var togglePause = function () {
if (requestId) {
if (document.getElementById('pause').value === 'Pause') {
cancelAnimationFrame(requestId);
document.getElementById('pause').value = 'Play';
}
else {
requestId = requestAnimationFrame(renderLoop);
document.getElementById('pause').value = 'Pause';
}
}
};
var f = document.getElementById('fps');
function renderLoop() {
f.innerHTML = fps.getFPS().toString();
var lambertianReflectance = (document.getElementById('lambert').checked)
? 1 : 0;
var specularReflection = (document.getElementById('specular').checked)
? 1 : 0;
if (document.getElementById('cpu').checked) {
cpuKernel(camera, lights, things, eyeVector.toArray(), vpRight.toArray(), vpUp.toArray(), halfHeight, halfWidth, pixelHeight, pixelWidth, lambertianReflectance, specularReflection);
var t2 = performance.now();
var canvas = cpuKernel.canvas;
var cv = document.getElementsByTagName('canvas')[0];
cv.parentNode.replaceChild(canvas, cv);
}
else {
gpuKernel(camera, lights, things, eyeVector.toArray(), vpRight.toArray(), vpUp.toArray(), halfHeight, halfWidth, pixelHeight, pixelWidth, lambertianReflectance, specularReflection);
var canvas = gpuKernel.canvas;
var cv = document.getElementsByTagName('canvas')[0];
cv.parentNode.replaceChild(canvas, cv);
}
things.forEach(function (thing) {
var height = this.height / (halfHeight * 2 * 100);
if (thing[10] < height)
thing[10] = (thing[10] + 0.02) % (height + 1);
else
thing[10] = -1 * height;
});
requestId = requestAnimationFrame(renderLoop);
}
window.onload = renderLoop;
</script>
</body>
</html>