Skip to content

Commit

Permalink
Add specular lighting computation for raytracer
Browse files Browse the repository at this point in the history
  • Loading branch information
ElHacker committed Nov 27, 2019
1 parent 3f511c4 commit a66b351
Show file tree
Hide file tree
Showing 2 changed files with 220 additions and 0 deletions.
11 changes: 11 additions & 0 deletions raytracer-03.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Raytracer 03</title>
</head>
<body>
<canvas id="canvas" width=600 height=600 style="border: 1px grey solid">
<script src="./raytracer-03.js" charset="utf-8"></script>
</body>
</html>
209 changes: 209 additions & 0 deletions raytracer-03.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@

// ======================================================================
// Low-level canvas access.
// ======================================================================
let canvas = document.getElementById('canvas');
let canvas_context = canvas.getContext('2d');
let canvas_buffer = canvas_context.getImageData(0, 0, canvas.width, canvas.height);
let canvas_pitch = canvas_buffer.width * 4;


let PutPixel = (x, y, color) => {
x = canvas.width / 2 + x;
y = canvas.height / 2 - y - 1;

if (x < 0 || x >= canvas.width || y < 0 || y >= canvas.height) {
return;
}

let offset = 4 * x + canvas_pitch * y;
canvas_buffer.data[offset++] = color[0];
canvas_buffer.data[offset++] = color[1];
canvas_buffer.data[offset++] = color[2];
canvas_buffer.data[offset++] = 255; // Alpha = 255 (full opacity)
}


// Displays the contents of the offscreen buffer into the canvas.
let UpdateCanvas = () => {
canvas_context.putImageData(canvas_buffer, 0, 0);
}


// ======================================================================
// Linear algebra and helpers.
// ======================================================================
// Dot product of two 3D vectors.
let DotProduct = (v1, v2) => {
return (v1[0] * v2[0]) + (v1[1] * v2[1]) + (v1[2] * v2[2]);
}

// Computes v1 - v2.
let Subtract = (v1, v2) => {
return [v1[0] - v2[0], v1[1] - v2[1], v1[2] - v2[2]];
}

// Length of a vector.
let Length = (vec) => {
return Math.sqrt(DotProduct(vec, vec));
}

// Computes k * vec.
let Multiply = (k, vec) => {
return [k * vec[0], k * vec[1], k * vec[2]];
}

// Computes v1 + v2.
var Add = (v1, v2) => {
return [v1[0] + v2[0], v1[1] + v2[1], v1[2] + v2[2]];
}

// Clamps a color to the canonical color range.
var Clamp = (vec) => {
return [Math.min(255, Math.max(0, vec[0])),
Math.min(255, Math.max(0, vec[1])),
Math.min(255, Math.max(0, vec[2]))];
}

// ======================================================================
// A raytracer with diffuse and specular illumination.
// ======================================================================

let Sphere = function(center, radius, color, specular) {
this.center = center;
this.radius = radius;
this.color = color;
this.specular = specular;
}

let Light = function(type, intensity, position) {
this.type = type;
this.intensity = intensity;
this.position = position;
}

Light.AMBIENT = 0;
Light.POINT = 1;
Light.DIRECTIONAL = 2;

// Scene setup.
let viewport_size = 1;
let projection_plane_z = 1;
let camera_position = [0, 0, 0];
let background_color = [255, 255, 255];
let spheres = [new Sphere([0, -1, 3], 1, [255, 0, 0], 500),
new Sphere([2, 0, 4], 1, [0, 0, 255], 500),
new Sphere([-2, 0, 4], 1, [0, 255, 0], 10),
new Sphere([0, -5001, 0], 5000, [255, 255, 0], 1000)];

let lights = [
new Light(Light.AMBIENT, 0.2),
new Light(Light.POINT, 0.6, [2, 1, 0]),
new Light(Light.DIRECTIONAL, 0.2, [1, 4, 4])
];

// Converts 2D canvas coordinates to 3D viewport coordinates.
let CanvasToViewport = (p2d) => {
return [p2d[0] * viewport_size / canvas.width,
p2d[1] * viewport_size / canvas.height,
projection_plane_z];
}


// Computes the intersection of a ray and a sphere. Returns the values
// of t for the intersections.
let IntersectRaySphere = (origin, direction, sphere) => {
const oc = Subtract(origin, sphere.center);

const k1 = DotProduct(direction, direction);
const k2 = 2 * DotProduct(oc, direction);
const k3 = DotProduct(oc, oc) - sphere.radius * sphere.radius;

const discriminant = k2 * k2 - 4 * k1 * k3;
if (discriminant < 0) {
return [Infinity, Infinity];
}

t1 = (-k2 + Math.sqrt(discriminant)) / (2 * k1)
t2 = (-k2 - Math.sqrt(discriminant)) / (2 * k1)
return [t1, t2];
}

let ComputeLighting = (point, normal, view, specular) => {
let intensity = 0;
const length_n = Length(normal); // Since this is a normal vector, the length should be 1.0 always.
const length_v = Length(view);
for (let i = 0; i < lights.length; i++) {
let light = lights[i];
if (light.type === Light.AMBIENT) {
intensity += light.intensity;
} else {
let vec_l;
if (light.type === Light.POINT) {
vec_l = Subtract(light.position, point);
} else if (light.type === Light.DIRECTIONAL) {
vec_l = light.position;
}

// Diffuse
const n_dot_1 = DotProduct(normal, vec_l);
if (n_dot_1 > 0) {
intensity += light.intensity * n_dot_1 / (length_n * Length(vec_l));
}

// Specular
if (specular != -1) {
let vec_r = Subtract(Multiply(2.0 * DotProduct(normal, vec_l), normal), vec_l);
let r_dot_v = DotProduct(vec_r, view);
if (r_dot_v > 0) {
intensity += light.intensity * Math.pow(r_dot_v / (Length(vec_r) * length_v), specular);
}
}
}
}
return intensity;
}

// Traces a ray against the set of spheres in the scene.
let TraceRay = (origin, direction, min_t, max_t) => {
let closest_t = Infinity;
let closest_sphere = null;
for (let i = 0; i < spheres.length; i++) {
const sphere = spheres[i]
tValues = IntersectRaySphere(origin, direction, sphere);
const t1 = tValues[0];
const t2 = tValues[1];
if (t1 > min_t && t1 < max_t && t1 < closest_t) {
closest_t = t1;
closest_sphere = sphere;
}
if (t2 > min_t && t2 < max_t && t2 < closest_t) {
closest_t = t2;
closest_sphere = sphere;
}
}
if (closest_sphere == null) {
return background_color;
}
const point = Add(origin, Multiply(closest_t, direction));
// Compute normal at intersection.
let normal = Subtract(point, closest_sphere.center);
normal = Multiply(1.0 / Length(normal), normal);

const view = Multiply(-1, direction);
const lighting = ComputeLighting(point, normal, view, closest_sphere.specular);
return Multiply(lighting, closest_sphere.color);
}


//
// Main loop.
//
for (let x = -canvas.width/2; x < canvas.width/2; x++) {
for (let y = -canvas.height/2; y < canvas.height/2; y++) {
let direction = CanvasToViewport([x, y])
let color = TraceRay(camera_position, direction, 1, Infinity);
PutPixel(x, y, color);
}
}
UpdateCanvas();

0 comments on commit a66b351

Please sign in to comment.