aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Colour.ts1
-rw-r--r--src/Geometry.ts14
-rw-r--r--src/Light.ts7
-rw-r--r--src/Material.ts9
-rw-r--r--src/RaytraceContext.ts35
-rw-r--r--src/RaytraceDispatcher.ts70
-rw-r--r--src/Raytracer.ts91
-rw-r--r--src/index.ts84
8 files changed, 231 insertions, 80 deletions
diff --git a/src/Colour.ts b/src/Colour.ts
index 4f2c4a6..3c9d8da 100644
--- a/src/Colour.ts
+++ b/src/Colour.ts
@@ -1,4 +1,5 @@
import {Vector} from './Vector';
+import 'reflect-metadata';
export class Colour extends Vector {
constructor(readonly r: number, readonly g: number, readonly b: number) {
diff --git a/src/Geometry.ts b/src/Geometry.ts
index 4fcc11b..9bef1a7 100644
--- a/src/Geometry.ts
+++ b/src/Geometry.ts
@@ -1,13 +1,21 @@
+import {Type} from 'class-transformer';
import {Colour} from './Colour';
import {Material} from './Material';
import {Vector} from './Vector';
export class Sphere {
+ @Type(() => Vector)
+ readonly centerPoint: Vector;
+ @Type(() => Material)
+ readonly material: Material;
constructor(
- readonly centerPoint: Vector,
readonly radius: number,
- readonly material: Material
- ) {}
+ centerPoint: Vector,
+ material: Material
+ ) {
+ this.centerPoint = centerPoint;
+ this.material = material;
+ }
}
export class Plane {
diff --git a/src/Light.ts b/src/Light.ts
index 1d9c8bd..58e4185 100644
--- a/src/Light.ts
+++ b/src/Light.ts
@@ -1,5 +1,10 @@
+import {Type} from 'class-transformer';
import {Vector} from './Vector';
export class Light {
- constructor(readonly position: Vector, readonly intensity: number) {}
+ @Type(() => Vector)
+ readonly position: Vector;
+ constructor(position: Vector, readonly intensity: number) {
+ this.position = position;
+ }
}
diff --git a/src/Material.ts b/src/Material.ts
index 6f199a3..3abff57 100644
--- a/src/Material.ts
+++ b/src/Material.ts
@@ -1,11 +1,16 @@
+import {Type} from 'class-transformer';
import {Colour} from './Colour';
export class Material {
+ @Type(() => Colour)
+ readonly diffuseColour: Colour;
constructor(
- readonly diffuseColour: Colour,
+ diffuseColour: Colour,
readonly diffuseAlbedo: number,
readonly specularAlbedo: number,
readonly reflectionAlbedo: number,
readonly specularExponent: number
- ) {}
+ ) {
+ this.diffuseColour = diffuseColour;
+ }
}
diff --git a/src/RaytraceContext.ts b/src/RaytraceContext.ts
new file mode 100644
index 0000000..3629f5a
--- /dev/null
+++ b/src/RaytraceContext.ts
@@ -0,0 +1,35 @@
+import {Type} from 'class-transformer';
+import {Plane, Sphere} from './Geometry';
+import {Light} from './Light';
+
+export class RaytraceContext {
+ @Type(() => Sphere)
+ readonly spheres: Sphere[];
+ @Type(() => Plane)
+ readonly planes: Plane[];
+ @Type(() => Light)
+ readonly lights: Light[];
+ constructor(
+ readonly height: number,
+ readonly width: number,
+ readonly fov: number,
+ spheres: Sphere[],
+ planes: Plane[],
+ lights: Light[],
+ readonly options: RaytracerOptions
+ ) {
+ this.spheres = spheres;
+ this.planes = planes;
+ this.lights = lights;
+ }
+}
+
+export interface RaytracerOptions {
+ numThreads: number;
+ shadows: boolean;
+ diffuseLighting: boolean;
+ specularLighting: boolean;
+ reflections: boolean;
+ maxRecurseDepth: number;
+ maxDrawDistance: number;
+}
diff --git a/src/RaytraceDispatcher.ts b/src/RaytraceDispatcher.ts
new file mode 100644
index 0000000..866d2d1
--- /dev/null
+++ b/src/RaytraceDispatcher.ts
@@ -0,0 +1,70 @@
+import {Colour} from './Colour';
+import {Framebuffer} from './Framebuffer';
+import {RaytraceContext} from './RaytraceContext';
+import {instanceToPlain} from 'class-transformer';
+import 'reflect-metadata';
+
+export class RaytraceDispatcher {
+ private renderStartMs: number;
+ private responsesReceived = 0;
+ constructor(
+ readonly framebuffer: Framebuffer,
+ readonly context: RaytraceContext
+ ) {
+ this.renderStartMs = new Date().getTime();
+ }
+
+ requestRender() {
+ // Assumes height and threads are always even
+ const rowBatchSize = this.context.height / this.context.options.numThreads;
+
+ for (let y = 0; y < this.context.height; y += rowBatchSize) {
+ const rowStartIndex = y;
+ const rowEndIndex = y + rowBatchSize - 1;
+ this.dispatchRaytraceWorker(rowStartIndex, rowEndIndex, this.context);
+ }
+ }
+
+ private dispatchRaytraceWorker(
+ rowStartIndex: number,
+ rowEndIndex: number,
+ context: RaytraceContext
+ ) {
+ console.log(
+ `Dispatching worker for lines ${rowStartIndex} to ${rowEndIndex}`
+ );
+
+ const raytracer = new Worker(new URL('./Raytracer.ts', import.meta.url));
+
+ raytracer.postMessage({
+ type: 'raytraceStart',
+ rowStartIndex: rowStartIndex,
+ rowEndIndex: rowEndIndex,
+ context: JSON.stringify(instanceToPlain(context)),
+ });
+
+ raytracer.onmessage = ({data}) => {
+ if (data.type === 'raytraceResultRow') {
+ this.processResponse(data.rowIndex, data.resultBuffer);
+ }
+ if (data.type === 'raytraceComplete') {
+ this.handleWorkerComplete();
+ }
+ };
+ }
+
+ private handleWorkerComplete() {
+ if (++this.responsesReceived === this.context.options.numThreads) {
+ console.log(
+ `Render completed in ${new Date().getTime() - this.renderStartMs}ms`
+ );
+ }
+ }
+
+ private processResponse(rowIndex: number, rowData: Colour[]) {
+ for (let x = 0; x < this.framebuffer.width; x++) {
+ this.framebuffer.writePixelAt(x, rowIndex, rowData[x]);
+ }
+ this.framebuffer.flush();
+ }
+}
diff --git a/src/Raytracer.ts b/src/Raytracer.ts
index b609604..68d5138 100644
--- a/src/Raytracer.ts
+++ b/src/Raytracer.ts
@@ -1,10 +1,28 @@
import {Colour} from './Colour';
-import {Framebuffer} from './Framebuffer';
import {Plane, Sphere} from './Geometry';
-import {Light} from './Light';
import {Material} from './Material';
+import {RaytraceContext} from './RaytraceContext';
import {Vector} from './Vector';
+import {plainToInstance} from 'class-transformer';
+import 'reflect-metadata';
+
+self.onmessage = ({data}) => {
+ if (data.type === 'raytraceStart') {
+ const serialisedContext: Object = JSON.parse(data.context);
+ const context = plainToInstance(RaytraceContext, serialisedContext);
+
+ const raytracer = new Raytracer(
+ data.rowStartIndex,
+ data.rowEndIndex,
+ context
+ );
+
+ raytracer.process();
+ self.close();
+ }
+};
+
class Ray {
constructor(readonly origin: Vector, readonly direction: Vector) {}
}
@@ -22,50 +40,47 @@ class RayIntersectionResult {
constructor(readonly geometryHit: boolean, readonly rayDistance: number) {}
}
-export interface RaytracerOptions {
- shadows: boolean;
- diffuseLighting: boolean;
- specularLighting: boolean;
- reflections: boolean;
- maxRecurseDepth: number;
- maxDrawDistance: number;
-}
-
-export class Raytracer {
+class Raytracer {
constructor(
- readonly framebuffer: Framebuffer,
- readonly fov: number,
- readonly spheres: Sphere[],
- readonly planes: Plane[],
- readonly lights: Light[],
- readonly options: RaytracerOptions
+ readonly rowStartIndex: number,
+ readonly rowEndIndex: number,
+ readonly context: RaytraceContext
) {}
- render() {
- const height = this.framebuffer.height;
- const width = this.framebuffer.width;
+ process() {
+ for (let y = this.rowStartIndex; y <= this.rowEndIndex; y++) {
+ const rowResultBuffer: Colour[] = [];
- for (let y = 0; y < height; y++) {
- for (let x = 0; x < width; x++) {
- const rayX = x + 0.5 - width / 2;
- const rayY = -(y + 0.5) + height / 2;
- const rayZ = -height / (2 * Math.tan(this.fov / 2));
+ 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 rayZ =
+ -this.context.height / (2 * Math.tan(this.context.fov / 2));
const rayDirection = new Vector(rayX, rayY, rayZ).normalise();
const ray = new Ray(new Vector(0, 0, 0), rayDirection);
const pixelValue = this.raytrace(ray);
- this.framebuffer.writePixelAt(x, y, pixelValue);
+ rowResultBuffer.push(pixelValue);
}
+
+ self.postMessage({
+ type: 'raytraceResultRow',
+ resultBuffer: rowResultBuffer,
+ rowIndex: y,
+ });
}
- this.framebuffer.flush();
+ self.postMessage({type: 'raytraceComplete'});
}
private raytrace(ray: Ray, recursionDepth = 0): Colour {
const result = this.processSceneGeometry(ray);
- if (recursionDepth > this.options.maxRecurseDepth || !result.geometryHit) {
+ if (
+ recursionDepth > this.context.options.maxRecurseDepth ||
+ !result.geometryHit
+ ) {
// No hit, show background colour
// return new Colour(50, 178, 203);
// return new Colour(150, 150, 150);
@@ -73,7 +88,7 @@ export class Raytracer {
}
let reflectionColour = new Colour(0, 0, 0);
- if (this.options.reflections) {
+ if (this.context.options.reflections) {
const reflectionDirection = this.calculateReflection(
ray.direction,
result.normal
@@ -101,7 +116,7 @@ export class Raytracer {
let normal = new Vector(0, 0, 0);
let material = new Material(new Colour(0, 0, 0), 0, 0, 0, 0);
- this.spheres.forEach(sphere => {
+ this.context.spheres.forEach(sphere => {
const result = this.intersectSphere(ray, sphere);
if (result.geometryHit && result.rayDistance < geometryDistance) {
@@ -112,7 +127,7 @@ export class Raytracer {
}
});
- this.planes.forEach(plane => {
+ this.context.planes.forEach(plane => {
const result = this.intersectPlane(ray, plane);
if (result.geometryHit && result.rayDistance < geometryDistance) {
@@ -123,7 +138,7 @@ export class Raytracer {
}
});
- const geometryHit = geometryDistance < this.options.maxDrawDistance;
+ const geometryHit = geometryDistance < this.context.options.maxDrawDistance;
return new RayTraceResult(geometryHit, hitPoint, normal, material);
}
@@ -136,7 +151,7 @@ export class Raytracer {
let diffuseLightIntensity = 0;
let specularLightIntensity = 0;
- this.lights.forEach(light => {
+ this.context.lights.forEach(light => {
const lightDirection = light.position
.subtract(result.hitPoint)
.normalise();
@@ -163,11 +178,11 @@ export class Raytracer {
let rgbVector = result.material.diffuseColour.toVector();
- if (this.options.diffuseLighting) {
+ if (this.context.options.diffuseLighting) {
rgbVector = rgbVector.multiply(diffuseLightIntensity);
}
- if (this.options.specularLighting) {
+ if (this.context.options.specularLighting) {
const totalSpecularIntensity = new Vector(255, 255, 255)
.multiply(specularLightIntensity)
.multiply(result.material.specularAlbedo);
@@ -177,7 +192,7 @@ export class Raytracer {
.add(totalSpecularIntensity);
}
- if (this.options.reflections) {
+ if (this.context.options.reflections) {
rgbVector = rgbVector.add(
reflectionColour.multiply(result.material.reflectionAlbedo)
);
@@ -204,7 +219,7 @@ export class Raytracer {
lightDistance: number,
result: RayTraceResult
): boolean {
- if (!this.options.shadows) {
+ if (!this.context.options.shadows) {
return false;
}
diff --git a/src/index.ts b/src/index.ts
index f10ef84..5696fe0 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -3,21 +3,16 @@ import {Framebuffer} from './Framebuffer';
import {Plane, Sphere} from './Geometry';
import {Light} from './Light';
import {Material} from './Material';
-import {Raytracer, RaytracerOptions} from './Raytracer';
+import {RaytraceDispatcher} from './RaytraceDispatcher';
+import {RaytraceContext, RaytracerOptions} from './RaytraceContext';
import {Vector} from './Vector';
-function parseOptions(): RaytracerOptions {
- return {
- shadows: getInputElement('shadows-toggle').checked,
- diffuseLighting: getInputElement('diffuse-toggle').checked,
- specularLighting: getInputElement('specular-toggle').checked,
- reflections: getInputElement('reflections-toggle').checked,
- maxRecurseDepth: 5,
- maxDrawDistance: 1000,
- };
+function render() {
+ const dispatcher = initDispatcher(parseOptions());
+ dispatcher.requestRender();
}
-function initRaytracer(options: RaytracerOptions): Raytracer {
+function initDispatcher(options: RaytracerOptions): RaytraceDispatcher {
const width = 960;
const height = 720;
const fov = Math.PI / 3;
@@ -27,18 +22,16 @@ function initRaytracer(options: RaytracerOptions): Raytracer {
const matWhite = new Material(new Colour(102, 102, 77), 0.6, 0.3, 0.1, 50);
const matRed = new Material(new Colour(77, 26, 26), 0.9, 0.1, 0.0, 10);
const matMirror = new Material(new Colour(193, 193, 193), 0.0, 10, 0.8, 1000);
- const matGreen= new Material(new Colour(77, 255, 26), 0.3, 0.1, 0.0, 2);
+ const matGreen = new Material(new Colour(77, 255, 26), 0.3, 0.1, 0.0, 2);
const spheres = [
- new Sphere(new Vector(-3, 0, -16), 2, matWhite),
- new Sphere(new Vector(-1, -1.5, -12), 2, matGreen),
- new Sphere(new Vector(1.5, -0.5, -18), 3, matRed),
- new Sphere(new Vector(7, 5, -18), 4, matMirror),
+ new Sphere(2, new Vector(-3, 0, -16), matWhite),
+ new Sphere(2, new Vector(-1, -1.5, -12), matGreen),
+ new Sphere(3, new Vector(1.5, -0.5, -18), matRed),
+ new Sphere(4, new Vector(7, 5, -18), matMirror),
];
- const planes = [
- new Plane(-4, 10, -10, -30, 0.7)
- ];
+ const planes = [new Plane(-4, 10, -10, -30, 0.7)];
const lights = [
new Light(new Vector(-20, 20, 20), 1.5),
@@ -46,32 +39,51 @@ function initRaytracer(options: RaytracerOptions): Raytracer {
new Light(new Vector(30, 20, 30), 1.7),
];
- return new Raytracer(framebuffer, fov, spheres, planes, lights, options);
+ const context = new RaytraceContext(
+ height,
+ width,
+ fov,
+ spheres,
+ planes,
+ lights,
+ options
+ );
+
+ return new RaytraceDispatcher(framebuffer, context);
}
-function render() {
- const raytracer = initRaytracer(parseOptions());
- showLoadingIndicator();
-
- // Give browser time to repaint
- setTimeout(() => {
- const startMs = new Date().getTime();
- raytracer.render();
- console.log(`Render took ${new Date().getTime() - startMs}ms`);
- hideLoadingIndicator();
- }, 50);
+function parseOptions(): RaytracerOptions {
+ return {
+ numThreads: getDesiredThreadCount(),
+ shadows: getInputElement('shadows-toggle').checked,
+ diffuseLighting: getInputElement('diffuse-toggle').checked,
+ specularLighting: getInputElement('specular-toggle').checked,
+ reflections: getInputElement('reflections-toggle').checked,
+ maxRecurseDepth: 5,
+ maxDrawDistance: 1000,
+ };
}
function registerEventListeners() {
document.getElementById('render')?.addEventListener('click', render); // TODO disable once clicked
-}
-function showLoadingIndicator() {
- document.getElementById('output-wrapper')?.classList.add('loading');
+ const threadsSlider = getInputElement('threads');
+ threadsSlider.addEventListener('input', () => {
+ getInputElement('threads-value').textContent = threadsSlider.value;
+ });
+
+ const threadToggle = getInputElement('enable-threads-toggle');
+ threadToggle.addEventListener('change', () => {
+ threadsSlider.disabled = !threadToggle.checked;
+ });
}
-function hideLoadingIndicator() {
- document.getElementById('output-wrapper')?.classList.remove('loading');
+function getDesiredThreadCount(): number {
+ if (getInputElement('enable-threads-toggle').checked) {
+ return Number.parseInt(getInputElement('threads').value);
+ } else {
+ return 1;
+ }
}
function getInputElement(elementId: string) {