aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--index.html6
-rw-r--r--src/Geometry.ts5
-rw-r--r--src/Material.ts19
-rw-r--r--src/RaytraceContext.ts1
-rw-r--r--src/Raytracer.ts93
-rw-r--r--src/Vector.ts4
-rw-r--r--src/index.ts39
7 files changed, 145 insertions, 22 deletions
diff --git a/index.html b/index.html
index a08fcb9..cafe649 100644
--- a/index.html
+++ b/index.html
@@ -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,