aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--index.html56
-rw-r--r--src/Geometry.ts17
-rw-r--r--src/RaytraceContext.ts5
-rw-r--r--src/Raytracer.ts15
-rw-r--r--src/index.ts110
-rw-r--r--style.css25
6 files changed, 144 insertions, 84 deletions
diff --git a/index.html b/index.html
index cafe649..c65728e 100644
--- a/index.html
+++ b/index.html
@@ -8,33 +8,26 @@
</head>
<body>
- <div class="container grid-xl">
+ <div class="container">
<div class="columns">
<div class="column col-xl-12">
<div id="output-wrapper" class="p-centered">
<canvas id="render-output"></canvas>
</div>
</div>
+ </div>
+ <div class="container grid-xl">
+ <div class="columns controls">
<div class="column">
- <div class="columns controls">
- <!-- <div class="column col-12 col-xl-3">
- <p>foo</p>
+ <div class="columns">
+ <div class="column col-3">
+ <p>A minimal ray tracing engine written from scratch in JS. No graphics APIs or libraries are used, only a single HTML5 canvas call to draw the generated bitmap image.</p>
+ <p>Various effects are supported, including recursive optical reflections and refractions, and <a href="https://en.wikipedia.org/wiki/Phong_reflection_model">Phong shading</a>. Basic multi-threading is implemented using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API" target="_blank">Web Workers API</a>.</p>
+ <p>The source is on <a href="https://github.com/jamesbarnett91/js-raytracer" target="_blank">GitHub</a>.</p>
</div>
- <div class="column col-12 col-xl-1 hide-xl">
- <div class="divider"></div>
- </div> -->
- <div class="column col-12 col-xl-9">
- <div class="form-group">
- <label class="form-label label-sm" for="res">Resolution</label>
- <select id="res" class="form-select select-sm">
- <option value="720p" selected>720p</option>
- <option value="1080p">1080p</option>
- <option value="1440p">1440p</option>
- <option value="4k">4k</option>
- <option value="8k">8k</option>
- </select>
- </div>
- <p>Render options</p>
+ <div class="divider-vert"></div>
+ <div class="column col-2">
+ <span>Render options</span>
<div class="form-group">
<label class="form-switch">
<input id="diffuse-toggle" type="checkbox" checked>
@@ -65,9 +58,11 @@
<i class="form-icon"></i> Refractions
</label>
</div>
- <p>Performance options</p>
+ </div>
+ <div class="column col-3">
+ <span>Performance options</span>
<div class="form-group">
- <label class="form-switch tooltip">
+ <label class="form-switch">
<input id="buffer-draw" type="checkbox" checked>
<i class="form-icon"></i> Buffer draw calls
</label>
@@ -88,12 +83,25 @@
<label class="form-label label-sm" for="threads">Render threads</label>
<!-- TODO style -->
<div class="input-group">
- <input id="threads" class="slider" type="range" min="2" max="12" value="2" step="2">
- <span id="threads-value" class="input-group-addon">2</span>
+ <input id="threads" class="slider" type="range" min="2" max="12" value="4" step="2">
+ <span id="threads-value" class="input-group-addon">4</span>
</div>
</div>
+ </div>
+ <div class="column col-4">
+ <div class="form-group">
+ <label id="res-label" class="form-label label-sm" for="res">Resolution</label>
+ <select id="res" class="form-select select-sm">
+ <option value="360p">360p</option>
+ <option value="480p">480p</option>
+ <option value="720p" selected >720p</option>
+ <option value="1080p">1080p</option>
+ <option value="1440p">1440p</option>
+ <option value="4k">4k</option>
+ </select>
+ </div>
<button id="render" class="btn btn-primary">Render</button>
- <button id="view-full" class="btn btn-link">view full size</button>
+ <button id="view-full" class="btn btn-link">View full image</button>
<pre class="code"><code id="console"></code></pre>
</div>
</div>
diff --git a/src/Geometry.ts b/src/Geometry.ts
index 41d6023..556c7ea 100644
--- a/src/Geometry.ts
+++ b/src/Geometry.ts
@@ -19,21 +19,30 @@ export class Sphere {
}
export class Plane {
+ @Type(() => Colour)
+ readonly checkerboardColour1: Colour;
+ @Type(() => Colour)
+ readonly checkerboardColour2: Colour;
constructor(
readonly yPos: number,
readonly width: number, // How far the plane extends into x/-x from 0
readonly zStartPos: number,
readonly zEndPos: number,
- readonly checkerboardScale: number
- ) {}
+ readonly checkerboardScale: number,
+ checkerboardColour1: Colour,
+ checkerboardColour2: Colour
+ ) {
+ this.checkerboardColour1 = checkerboardColour1;
+ this.checkerboardColour2 = checkerboardColour2;
+ }
getMaterialAtPoint(x: number, z: number): Material {
let colour: Colour;
// prettier-ignore
if ((Math.round(this.checkerboardScale * x) + Math.round(this.checkerboardScale * z)) & 1) {
- colour = new Colour(15, 15, 15);
+ colour = this.checkerboardColour1;
} else {
- colour = new Colour(200, 200, 200);
+ colour = this.checkerboardColour2;
}
return new Material(colour, new Albedo(1, 0, 0, 0), 0, 1);
}
diff --git a/src/RaytraceContext.ts b/src/RaytraceContext.ts
index f7c9315..acd4336 100644
--- a/src/RaytraceContext.ts
+++ b/src/RaytraceContext.ts
@@ -1,4 +1,5 @@
import {Type} from 'class-transformer';
+import {Colour} from './Colour';
import {Plane, Sphere} from './Geometry';
import {Light} from './Light';
@@ -9,6 +10,8 @@ export class RaytraceContext {
readonly planes: Plane[];
@Type(() => Light)
readonly lights: Light[];
+ @Type(() => Colour)
+ readonly backgroundColour: Colour;
constructor(
readonly height: number,
readonly width: number,
@@ -16,11 +19,13 @@ export class RaytraceContext {
spheres: Sphere[],
planes: Plane[],
lights: Light[],
+ backgroundColour: Colour,
readonly options: RaytracerOptions
) {
this.spheres = spheres;
this.planes = planes;
this.lights = lights;
+ this.backgroundColour = backgroundColour;
}
}
diff --git a/src/Raytracer.ts b/src/Raytracer.ts
index 895073c..34cbe00 100644
--- a/src/Raytracer.ts
+++ b/src/Raytracer.ts
@@ -53,7 +53,7 @@ class Raytracer {
for (let x = 0; x < this.context.width; x++) {
const rayX = x + 0.5 - this.context.width / 2;
- const rayY = -(y + 0.5) + this.context.height / 2;
+ const rayY = -(y + 100) + this.context.height / 2;
const rayZ =
-this.context.height / (2 * Math.tan(this.context.fov / 2));
const rayDirection = new Vector(rayX, rayY, rayZ).normalise();
@@ -91,10 +91,7 @@ class Raytracer {
recursionDepth > this.context.options.maxRecurseDepth ||
!result.geometryHit
) {
- // No hit, show background colour
- // return new Colour(50, 178, 203);
- // return new Colour(150, 150, 150);
- return new Colour(0, 128, 128);
+ return this.context.backgroundColour;
}
let reflectionColour = new Colour(0, 0, 0);
@@ -218,7 +215,9 @@ class Raytracer {
}
});
- let rgbVector = result.material.diffuseColour.toVector();
+ let rgbVector = result.material.diffuseColour
+ .toVector()
+ .multiply(result.material.albedo.diffuseAlbedo);
if (this.context.options.diffuseLighting) {
rgbVector = rgbVector.multiply(diffuseLightIntensity);
@@ -229,9 +228,7 @@ class Raytracer {
.multiply(specularLightIntensity)
.multiply(result.material.albedo.specularAlbedo);
- rgbVector = rgbVector
- .multiply(result.material.albedo.diffuseAlbedo)
- .add(totalSpecularIntensity);
+ rgbVector = rgbVector.add(totalSpecularIntensity);
}
if (this.context.options.reflections) {
diff --git a/src/index.ts b/src/index.ts
index a52399d..df4d988 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -21,50 +21,63 @@ function initDispatcher(options: RaytracerOptions): RaytraceDispatcher {
const framebuffer = new Framebuffer(width, height);
- const matWhite = new Material(
- new Colour(102, 102, 77),
- new Albedo(0.6, 0.3, 0.1, 0.0),
- 50,
+ const materialMirror = new Material(
+ new Colour(220, 220, 220),
+ new Albedo(0.1, 1, 0.8, 0.0),
+ 2500,
1
);
- const matRed = new Material(
- new Colour(77, 26, 26),
- new Albedo(0.9, 0.1, 0.0, 0.0),
- 10,
- 1
- );
- const matMirror = new Material(
- new Colour(193, 193, 193),
- new Albedo(0.0, 10, 0.8, 0.0),
- 1000,
- 1
- );
- const matGreen = new Material(
- new Colour(77, 255, 26),
- new Albedo(0.3, 0.1, 0.0, 0.0),
- 2,
- 1
- );
- const matGlass = new Material(
+
+ const materialGlass = new Material(
new Colour(153, 179, 204),
new Albedo(0.0, 0.5, 0.1, 0.8),
125,
1.5
);
- const spheres = [
- new Sphere(2, new Vector(-3, 0, -16), matWhite),
- new Sphere(2, new Vector(-1, -1.5, -12), matGlass),
- new Sphere(3, new Vector(1.5, -0.5, -18), matRed),
- new Sphere(4, new Vector(7, 5, -18), matMirror),
+ const spheres: Sphere[] = [
+ new Sphere(1, new Vector(-6.5, -3, -26), matteMaterial(27, 118, 255)),
+ new Sphere(1, new Vector(-14, -3, -25), matteMaterial(146, 80, 188)),
+ new Sphere(1, new Vector(-10, -3, -25), matteMaterial(0, 146, 178)),
+ new Sphere(1, new Vector(-10, -3, -20), matteMaterial(185, 18, 27)),
+ new Sphere(1, new Vector(-2.5, -3, -20), matteMaterial(115, 45, 217)),
+ new Sphere(1.5, new Vector(-10.5, -2.5, -16), materialMirror),
+ new Sphere(1, new Vector(-3, -3, -16), matteMaterial(247, 178, 173)),
+ new Sphere(1, new Vector(-6, -3, -18), matteMaterial(154, 188, 167)),
+ new Sphere(1, new Vector(-6, -3, -12), matteMaterial(96, 125, 139)),
+ new Sphere(1, new Vector(-9.5, -3, -12), matteMaterial(122, 186, 242)),
+ new Sphere(1, new Vector(0, -3, -14), matteMaterial(250, 91, 15)),
+ new Sphere(1, new Vector(-3, -3, -11), materialGlass),
+ new Sphere(1, new Vector(-1, -3, -10), matteMaterial(54, 95, 182)),
+ new Sphere(1, new Vector(-4.5, -3, -8), matteMaterial(139, 195, 74)),
+
+ new Sphere(4, new Vector(4, 0, -18), materialMirror),
+ new Sphere(1, new Vector(4, -3, -12), matteMaterial(115, 45, 217)),
+ new Sphere(1.5, new Vector(8.5, -2.5, -10), materialMirror),
+ new Sphere(1, new Vector(1, -3, -11.5), matteMaterial(255, 200, 0)),
+ new Sphere(1, new Vector(1.2, -3, -8.2), materialGlass),
+ new Sphere(1, new Vector(4, -3, -7), matteMaterial(244, 67, 54)),
+ new Sphere(1, new Vector(5.5, -3, -9.5), matteMaterial(150, 237, 137)),
+ new Sphere(1, new Vector(6.5, -3, -15), matteMaterial(14, 234, 255)),
+ new Sphere(1, new Vector(10, -3, -16), matteMaterial(171, 71, 188)),
+ new Sphere(1, new Vector(11, -3, -20), matteMaterial(190, 189, 191)),
];
- const planes = [new Plane(-4, 10, -10, -30, 0.7)];
+ const planes = [
+ new Plane(
+ -4,
+ 50,
+ 40,
+ -45,
+ 1.5,
+ new Colour(116, 101, 87),
+ new Colour(92, 78, 70)
+ ),
+ ];
const lights = [
- new Light(new Vector(-20, 20, 20), 1.5),
- new Light(new Vector(30, 50, -25), 1.8),
- new Light(new Vector(30, 20, 30), 1.7),
+ new Light(new Vector(30, 50, 40), 2.5),
+ new Light(new Vector(-20, 50, -25), 0.5),
];
const context = new RaytraceContext(
@@ -74,6 +87,7 @@ function initDispatcher(options: RaytracerOptions): RaytraceDispatcher {
spheres,
planes,
lights,
+ new Colour(221, 221, 221),
options
);
@@ -102,17 +116,19 @@ function parseOptions(): RaytracerOptions {
function parseResolution(): {width: number; height: number} {
switch (getInputElement('res').value) {
+ case '360p':
+ return {width: 640, height: 360};
+ case '480p':
+ return {width: 854, height: 480};
case '720p':
default:
- return {width: 960, height: 720};
+ return {width: 1280, height: 720};
case '1080p':
- return {width: 1440, height: 1080};
+ return {width: 1920, height: 1080};
case '1440p':
- return {width: 1920, height: 1440};
+ return {width: 2560, height: 1440};
case '4k':
- return {width: 2880, height: 2160};
- case '8k':
- return {width: 5760, height: 4320};
+ return {width: 3840, height: 2160};
}
}
@@ -157,6 +173,24 @@ function getInputElement(elementId: string) {
return document.getElementById(elementId) as HTMLInputElement;
}
+function matteMaterial(r: number, g: number, b: number): Material {
+ return new Material(
+ new Colour(r, g, b),
+ new Albedo(0.4, 0.0, 0.0, 0.0),
+ 0,
+ 1
+ );
+}
+
+function glossMaterial(r: number, g: number, b: number): Material {
+ return new Material(
+ new Colour(r, g, b),
+ new Albedo(0.4, 0.1, 0.05, 0),
+ 250,
+ 1
+ );
+}
+
document.addEventListener('DOMContentLoaded', () => {
registerEventListeners();
render();
diff --git a/style.css b/style.css
index 453687e..b4c8897 100644
--- a/style.css
+++ b/style.css
@@ -3,19 +3,16 @@ body {
}
p {
- margin: 0;
+ margin: 0 0 0.8rem;
}
#output-wrapper {
- position: relative;
- width: 960px;
- height: 720px;
margin-top: 0.5em;
}
#render-output {
- width: 960px;
- height: 720px;
+ width: 100%;
+ height: 65vmin;
object-fit: contain;
}
@@ -33,8 +30,18 @@ p {
}
#console {
- height: 150px;
- width: 300px;
- padding: 0rem;
+ height: 120px;
+ padding: 0 0 0 1em;
white-space: pre-line;
+}
+
+#res-label {
+ padding-bottom: 4px;
+ padding-top: 0px;
+ margin-top: -1px;
+ font-size: 15px;
+}
+
+.divider-vert {
+ padding: 0;
} \ No newline at end of file