diff options
| author | James Barnett <noreply@jamesbarnett.xyz> | 2022-01-04 21:44:49 +0000 |
|---|---|---|
| committer | James Barnett <noreply@jamesbarnett.xyz> | 2022-01-04 21:44:49 +0000 |
| commit | 16b0f20415efd3ed2c181df74a473ebd5265892b (patch) | |
| tree | 2d94216df0dca5b378f74b47515af5b910fc5ae7 | |
| parent | 496c63266d02bc9369e3406353b877a8ebbae60a (diff) | |
| download | js-raytracer-16b0f20415efd3ed2c181df74a473ebd5265892b.tar.xz js-raytracer-16b0f20415efd3ed2c181df74a473ebd5265892b.zip | |
Fix specular lighting. Update scene.
| -rw-r--r-- | index.html | 56 | ||||
| -rw-r--r-- | src/Geometry.ts | 17 | ||||
| -rw-r--r-- | src/RaytraceContext.ts | 5 | ||||
| -rw-r--r-- | src/Raytracer.ts | 15 | ||||
| -rw-r--r-- | src/index.ts | 110 | ||||
| -rw-r--r-- | style.css | 25 |
6 files changed, 144 insertions, 84 deletions
@@ -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(); @@ -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 |