diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/Colour.ts | 1 | ||||
| -rw-r--r-- | src/Geometry.ts | 14 | ||||
| -rw-r--r-- | src/Light.ts | 7 | ||||
| -rw-r--r-- | src/Material.ts | 9 | ||||
| -rw-r--r-- | src/RaytraceContext.ts | 35 | ||||
| -rw-r--r-- | src/RaytraceDispatcher.ts | 70 | ||||
| -rw-r--r-- | src/Raytracer.ts | 91 | ||||
| -rw-r--r-- | src/index.ts | 84 |
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) { |