diff options
| author | James Barnett <noreply@jamesbarnett.xyz> | 2022-01-03 21:09:23 +0000 |
|---|---|---|
| committer | James Barnett <noreply@jamesbarnett.xyz> | 2022-01-03 21:09:23 +0000 |
| commit | 496c63266d02bc9369e3406353b877a8ebbae60a (patch) | |
| tree | 492af5590ac21a70099e2ac7cb38ca91dce3a694 | |
| parent | 026a006b410a0132c2cb573edff4352b4333b857 (diff) | |
| download | js-raytracer-496c63266d02bc9369e3406353b877a8ebbae60a.tar.xz js-raytracer-496c63266d02bc9369e3406353b877a8ebbae60a.zip | |
Implement refraction
| -rw-r--r-- | index.html | 6 | ||||
| -rw-r--r-- | src/Geometry.ts | 5 | ||||
| -rw-r--r-- | src/Material.ts | 19 | ||||
| -rw-r--r-- | src/RaytraceContext.ts | 1 | ||||
| -rw-r--r-- | src/Raytracer.ts | 93 | ||||
| -rw-r--r-- | src/Vector.ts | 4 | ||||
| -rw-r--r-- | src/index.ts | 39 |
7 files changed, 145 insertions, 22 deletions
@@ -59,6 +59,12 @@ <i class="form-icon"></i> Reflections </label> </div> + <div class="form-group"> + <label class="form-switch"> + <input id="refractions-toggle" type="checkbox" checked> + <i class="form-icon"></i> Refractions + </label> + </div> <p>Performance options</p> <div class="form-group"> <label class="form-switch tooltip"> diff --git a/src/Geometry.ts b/src/Geometry.ts index 9bef1a7..41d6023 100644 --- a/src/Geometry.ts +++ b/src/Geometry.ts @@ -1,6 +1,6 @@ import {Type} from 'class-transformer'; import {Colour} from './Colour'; -import {Material} from './Material'; +import {Albedo, Material} from './Material'; import {Vector} from './Vector'; export class Sphere { @@ -29,11 +29,12 @@ export class Plane { 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); } else { colour = new Colour(200, 200, 200); } - return new Material(colour, 1, 0, 0, 0); + return new Material(colour, new Albedo(1, 0, 0, 0), 0, 1); } } diff --git a/src/Material.ts b/src/Material.ts index 3abff57..f0af5e2 100644 --- a/src/Material.ts +++ b/src/Material.ts @@ -4,13 +4,24 @@ import {Colour} from './Colour'; export class Material { @Type(() => Colour) readonly diffuseColour: Colour; + @Type(() => Albedo) + readonly albedo: Albedo; constructor( diffuseColour: Colour, - readonly diffuseAlbedo: number, - readonly specularAlbedo: number, - readonly reflectionAlbedo: number, - readonly specularExponent: number + albedo: Albedo, + readonly specularExponent: number, + readonly refractiveIndex: number ) { this.diffuseColour = diffuseColour; + this.albedo = albedo; } } + +export class Albedo { + constructor( + readonly diffuseAlbedo: number, + readonly specularAlbedo: number, + readonly reflectionAlbedo: number, + readonly refractionAlbedo: number + ) {} +} diff --git a/src/RaytraceContext.ts b/src/RaytraceContext.ts index 34f20dd..f7c9315 100644 --- a/src/RaytraceContext.ts +++ b/src/RaytraceContext.ts @@ -30,6 +30,7 @@ export interface RaytracerOptions { diffuseLighting: boolean; specularLighting: boolean; reflections: boolean; + refractions: boolean; maxRecurseDepth: number; maxDrawDistance: number; bufferDrawCalls: boolean; diff --git a/src/Raytracer.ts b/src/Raytracer.ts index 673977b..895073c 100644 --- a/src/Raytracer.ts +++ b/src/Raytracer.ts @@ -1,6 +1,6 @@ import {Colour} from './Colour'; import {Plane, Sphere} from './Geometry'; -import {Material} from './Material'; +import {Albedo, Material} from './Material'; import {RaytraceContext} from './RaytraceContext'; import {Vector} from './Vector'; @@ -99,7 +99,7 @@ class Raytracer { let reflectionColour = new Colour(0, 0, 0); if (this.context.options.reflections) { - const reflectionDirection = this.calculateReflection( + const reflectionDirection = this.calculateReflectionVector( ray.direction, result.normal ); @@ -117,14 +117,45 @@ class Raytracer { ); } - return this.processLighting(result, ray.direction, reflectionColour); + let refractionColour = new Colour(0, 0, 0); + if (this.context.options.refractions) { + const refractionDirection = this.calculateRefractionVector( + ray.direction, + result.normal, + result.material.refractiveIndex + ); + let refractionOrigin: Vector; + if (refractionDirection.dotProduct(result.normal) < 0) { + refractionOrigin = result.hitPoint.subtract( + result.normal.multiply(0.001) + ); + } else { + refractionOrigin = result.hitPoint.add(result.normal.multiply(0.001)); + } + refractionColour = this.raytrace( + new Ray(refractionOrigin, refractionDirection), + ++recursionDepth + ); + } + + return this.processLighting( + result, + ray.direction, + reflectionColour, + refractionColour + ); } private processSceneGeometry(ray: Ray): RayTraceResult { let geometryDistance = 99999; let hitPoint = new Vector(0, 0, 0); let normal = new Vector(0, 0, 0); - let material = new Material(new Colour(0, 0, 0), 0, 0, 0, 0); + let material = new Material( + new Colour(0, 0, 0), + new Albedo(0, 0, 0, 0), + 0, + 0 + ); this.context.spheres.forEach(sphere => { const result = this.intersectSphere(ray, sphere); @@ -156,7 +187,8 @@ class Raytracer { private processLighting( result: RayTraceResult, direction: Vector, - reflectionColour: Colour + reflectionColour: Colour, + refractionColour: Colour ): Colour { let diffuseLightIntensity = 0; let specularLightIntensity = 0; @@ -176,7 +208,7 @@ class Raytracer { Math.pow( Math.max( 0, - this.calculateReflection( + this.calculateReflectionVector( lightDirection, result.normal ).dotProduct(direction) @@ -195,23 +227,29 @@ class Raytracer { if (this.context.options.specularLighting) { const totalSpecularIntensity = new Vector(255, 255, 255) .multiply(specularLightIntensity) - .multiply(result.material.specularAlbedo); + .multiply(result.material.albedo.specularAlbedo); rgbVector = rgbVector - .multiply(result.material.diffuseAlbedo) + .multiply(result.material.albedo.diffuseAlbedo) .add(totalSpecularIntensity); } if (this.context.options.reflections) { rgbVector = rgbVector.add( - reflectionColour.multiply(result.material.reflectionAlbedo) + reflectionColour.multiply(result.material.albedo.reflectionAlbedo) + ); + } + + if (this.context.options.refractions) { + rgbVector = rgbVector.add( + refractionColour.multiply(result.material.albedo.refractionAlbedo) ); } return Colour.fromVector(rgbVector); } - private calculateReflection( + private calculateReflectionVector( incidentAngle: Vector, surfaceNormal: Vector ): Vector { @@ -223,6 +261,41 @@ class Raytracer { ); } + private calculateRefractionVector( + incidentAngle: Vector, + surfaceNormal: Vector, + refractiveIndex: number + ): Vector { + // See https://en.wikipedia.org/wiki/Snell's_law#Vector_form + let cosIncidenceAngle = -Math.max( + -1, + Math.min(1, incidentAngle.dotProduct(surfaceNormal)) + ); + let n1 = 1; + let n2 = refractiveIndex; + + let normal = surfaceNormal; + if (cosIncidenceAngle < 0) { + cosIncidenceAngle = -cosIncidenceAngle; + n1 = refractiveIndex; + n2 = 1; + normal = surfaceNormal.negative(); + } + + const r = n1 / n2; + const cosRefractedAngle = Math.sqrt( + 1 - r * r * (1 - cosIncidenceAngle * cosIncidenceAngle) + ); + + if (cosRefractedAngle * cosRefractedAngle < 0) { + return new Vector(0, 0, 0); + } else { + return incidentAngle + .multiply(r) + .add(normal.multiply(r * cosIncidenceAngle - cosRefractedAngle)); + } + } + private isShadowCast( lightDirection: Vector, lightDistance: number, diff --git a/src/Vector.ts b/src/Vector.ts index 3cebf6d..2be8e57 100644 --- a/src/Vector.ts +++ b/src/Vector.ts @@ -33,4 +33,8 @@ export class Vector { norm(): number { return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); } + + negative(): Vector { + return new Vector(-this.x, -this.y, -this.z); + } } diff --git a/src/index.ts b/src/index.ts index 6b86f57..a52399d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import {Colour} from './Colour'; import {Framebuffer} from './Framebuffer'; import {Plane, Sphere} from './Geometry'; import {Light} from './Light'; -import {Material} from './Material'; +import {Albedo, Material} from './Material'; import {RaytraceDispatcher} from './RaytraceDispatcher'; import {RaytraceContext, RaytracerOptions} from './RaytraceContext'; import {Vector} from './Vector'; @@ -21,14 +21,40 @@ function initDispatcher(options: RaytracerOptions): RaytraceDispatcher { const framebuffer = new Framebuffer(width, height); - 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 matWhite = new Material( + new Colour(102, 102, 77), + new Albedo(0.6, 0.3, 0.1, 0.0), + 50, + 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( + 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), matGreen), + 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), ]; @@ -66,6 +92,7 @@ function parseOptions(): RaytracerOptions { diffuseLighting: getInputElement('diffuse-toggle').checked, specularLighting: getInputElement('specular-toggle').checked, reflections: getInputElement('reflections-toggle').checked, + refractions: getInputElement('refractions-toggle').checked, maxRecurseDepth: 5, maxDrawDistance: 1000, bufferDrawCalls: getInputElement('buffer-draw').checked, |