Eccentric Developments

Ray Tracing

Full disclosure, I won't be explaining how the ray tracing algorithm works in detail, you can go to this wikipedia article to learn more. Maybe some day I'll do an in-depth explanation, but that is not today.

In this article I want to focus on a very simple thing: modifiying the ray casting algorithm to support simple ligthing, and thus, converting it in a very basic ray tracing implementation.

This whole implementation is based on the previous ray casting article, not all the code is visible or editable, but you can see it if you choose to look at this page's source. Only the code that needs to be modified is going to be shown in code editors.

The first code that needs updating is `createScene`, we want to add an sphere, and a point light. Point lights as their name implies, is just a single point in 3D space that emits light.

``````function createScene() {
return {
scene: [
{
center: [0.0, 0.0, 0.0],
color: [0, 0, 1.0],
},
{
center: [0.0, -9000.0 - 5.0, 0.0],
color: [1.0, 1.0, 1.0],
},
],
lights: [
{
origin: [10.0, 10.0, -10.0]
}
]
};
}
``````
``````function createScene() {
return {
scene: [
{
center: [0.0, 0.0, 0.0],
color: [0, 0, 1.0],
},
{
center: [0.0, -9000.0 - 5.0, 0.0],
color: [1.0, 1.0, 1.0],
},
],
lights: [
{
origin: [10.0, 10.0, -10.0]
}
]
};
}
``````

To calculate the light that incides in a sphere, we need first to know the normal of the surface at the point that the ray intersected, this is easily calculated as substracting the point the ray hit the sphere from its center.

``````function createIntersectionFunction(args) {
const { vector } = args;
const intersect = (ray, sphere) => {
const { center, radius } = sphere;
const oc = vector(ray.origin).sub(center);
const a = vector(ray.direction).dot(ray.direction).value();
const b = oc.dot(ray.direction).value();
const dis = b * b - a * c;

if (dis > 0) {
const e = Math.sqrt(dis);
let t = (-b - e) / a;
if (t > 0.007) {
return {
hit: true,
distance: t,
point,
// This is the new code to calculate the normal
normal: vector(point).sub(center).unit().value(),
};
}

t = (-b + e) / a;
if (t > 0.007) {
return {
hit: true,
distance: t,
point,
// This is the new code to calculate the normal
normal: vector(point).sub(center).unit().value(),
};
}
}
return {
hit: false,
};
};
return {
intersect,
};
}
``````

The normal of the intersection point is included as part of the hit structure, so it will allow us to calculate the angle of incidence of ligth.

To approximate the amount of light that reaches the hit point, the next step is to add a new shading function.

``````function createShadingFunction(args) {
const { vector, trace, lights } = args;
const shading = (point, normal) =>
lights.map((light) => {
const tmp = vector(light.origin).sub(origin);
const maxDistance = tmp.norm().value();
const direction = tmp.unit().value();
const lightRay = {
origin,
direction,
};
const intersection = trace(lightRay);
if(intersection.hit && intersection.distance < maxDistance) {
return 0;
}
return Math.max(0, vector(direction).dot(normal).value());
}).reduce((acc, v) => acc + v, 0);
return {
};
}
``````

The shading function calculates how much light reaches the point, if at all. This function is used in the `generateBitmap` function to apply shading for each pixel where an object has been found.

``````function* generateBitmap(args) {
const {
traceResults,
imageGeometry: { width, height },
vector,
} = args;
const bitmap = new Uint32Array(width * height);
let count = 0;
let idx = 0;

for(const result of traceResults) {
const { intersection } = result;
const { hit, sphere, point, normal } = intersection;
let pixel = 0xff000000;
if (hit) {
const [r, g, b] = vector(sphere.color).scale(intensity).value();
pixel = (255 << 24) | (Math.floor(b * 255) << 16) | (Math.floor(g * 255) << 8) | Math.floor(r * 255);
}
bitmap[idx++] = pixel;
if (++count === width * 16) {
count = 0;
yield;
}
}

return {
bitmap,
};
}
``````

There are more nuisances to physical accurate lighting, for now, we only use the angle of indicende to apply lighting to the found surfaces. Another simple change that can be included later is to add intensity to the point light so it will illuminate less the farther the surface is.

``````const canvas = document.getElementById("canvas-1");
const ctx = canvas.getContext("2d");
const width = canvas.width;
const height = canvas.height;
const renderingPipeline = pipeline([
createAspectRatioFunction,
createScene,
createCamera,
createImageGeometry,
createVectorFunction,
calculatePrimaryRays,
createIntersectionFunction,
createTraceFunction,