raytrace-engine
v0.0.17
Published
A simple CPU-based ray tracer written in **vanilla JavaScript**, rendering directly to an HTML5 `<canvas>` element — no WebGL, no external libraries.
Downloads
39
Maintainers
Readme
CPU Raytracer
A simple CPU-based ray tracer written in vanilla JavaScript, rendering directly to an HTML5 <canvas> element — no WebGL, no external libraries.
Current Progress
✅ New features:
- Pointer-controlled camera rotation around the Y-axis. Turns the camera left or right (like shaking your head "no").
- Check index.html file on Github repo for example code.
- Pixel buffer integration — huge improvement in camera movement smoothness
- Progressive rendering + buffer: paint only after ~1% of pixels are ready → keeps the main thread responsive
- 🎥 Watch this on YouTube — See how camera movement becomes noticeably smoother with pixel buffer vs. without it. It is a work in progress!
✅ How it works?
- check the Camera rotation section
✅ Features so far:
- Ray-object intersections (sphere for now)
- Diffuse reflection (matte surfaces)
- Specular reflection (shiny surfaces)
- Shadows
- Multithreading
- Progressive rendering
- Camera rotation (along +ve Y axis)
🔄 Next Up
- More performace boosts and smoother camera movements
- More degrees of freedom for camera (pitch/X-axis, roll/Z-axis)
- More shapes: Triangles, Planes, etc.

Project Structure
src/CanvasManager.js
Responsible for creating, updating, and destroying the canvas element used for rendering.
src/RaytracingManager.js
- Starts and stops web workers.
- Progressively loads pixels.
src/mathServices.js
Contains vector algebra and geometry utilities used by the ray tracer.
src/RaytracingWorker.js
Contains the web worker logic which is the core ray tracing logic:
- Generates rays.
- Computes intersections from shape data.
- Computes reflections (matte, specular, other objects on surface) and shadows from lighting data.
Getting Started
To run the raytracer locally:
- Clone github repository.
- To build the project run command
npm iand thennpm run build - Serve
index.htmlin any modern browser. If using VSCode try Live Server Extension. - Watch the pixel-by-pixel ray tracing in action.
- Alternatively, NPM package
'raytrace-engine'can be installed and used.
Understanding the index.html File
Appropriate comments are added to index.html. It is advised to read it alongside this explanation:
Usage
To use this raytrace engine, you need two managers:
CanvasManagerandRaytracingManager.The options provided to
CanvasManagerduring instantiation are:target: An HTML element which will contain the HTML canvas element.height: Height of the canvas.width: Width of the canvas.
The options provided to
RaytracingManagerduring instantiation are:canvasHeight: Determines viewport height.canvasWidth: Determines viewport width.cameraPosition: The{x, y, z}coordinates of the camera.distanceFromCameraToViewport: DistanceDfrom the camera to the viewport.shapeData: An array of objects describing each shape (currently only spheres supported).lightData: An array of objects describing light sources.noIntersectionColor: Background color when a ray hits nothing.reflectiveRecursionLimit: Max number of bounces for reflective rays.putPixelCallback: A callback that receives an array of pixel objects{x, y, color}to render on the canvas.
shapeData Format
Each object in shapeData represents a sphere.
Required properties:
center:{ x, y, z }— position of the sphere in 3D space.radius:number— radius of the sphere.color:{ r, g, b }— color of the sphere (0–255 for each channel).
Optional properties:
specular:number— higher values make the surface shinier (e.g., 500 for glossy, 0 for matte).reflective:number(0 to 1) — determines how reflective the surface is.1is a mirror,0means no reflection.
Example:
shapeData = [
{
center: { x: 0, y: -1, z: 3 },
radius: 1,
color: { r: 255, g: 0, b: 0 },
specular: 500,
},
{
center: { x: -1.5, y: 0.5, z: 3 },
radius: 1,
color: { r: 255, g: 255, b: 255 },
specular: 500,
reflective: 0.9,
},
{
center: { x: 1.5, y: 1, z: 3 },
radius: 1,
color: { r: 0, g: 255, b: 0 },
specular: 800,
},
];lightData Format
There are 3 types of supported lights:
Ambient Light
{
type: "ambient",
intensity: 0.2, // Value between 0 and 1
}- Applies uniform brightness to the entire scene.
Point Light
{
type: "point",
intensity: 0.6,
position: { x: 2, y: 1, z: 0 }
}- Emits light from a specific position like a bulb.
Directional Light
{
type: "directional",
intensity: 0.2,
direction: { x: 1, y: 4, z: 4 }
}- Simulates light from a distant source like the sun.
Understanding how to use types
- The below example shows how to use the types provided with NPM package raytrace-engine.
import {
CanvasManager,
RaytracingManager,
Pixel, // type
Light, // type
Shape, // type
CanvasManagerProps, // type
RaytracingManagerProps, // type
EnablePointerMovementsProps, // type
} from "raytrace-engine";
// Set canvas dimensions
let canvasHeight = 720;
let canvasWidth = 1080;
// Camera setup
let distanceFromCameraToViewport = 1;
let cameraPosition = { x: 0, y: 0, z: 0 };
// SHAPE DEFINITIONS
let shapeData: Shape[] = [];
/* Example scene with 3 spheres:
- Red sphere in the center
- White reflective sphere to the left
- Green shiny sphere to the right
*/
shapeData.push(
{
center: { x: 0, y: -1, z: 3 },
radius: 1,
color: { r: 255, g: 0, b: 0 }, // Red
specular: 500, // How shiny it is
},
{
center: { x: -1.5, y: 0.5, z: 3 },
radius: 1,
color: { r: 255, g: 255, b: 255 }, // White
specular: 500,
reflective: 0.9, // Very reflective
},
{
center: { x: 1.5, y: 1, z: 3 },
radius: 1,
color: { r: 0, g: 255, b: 0 }, // Green
specular: 800, // Very shiny
}
);
// LIGHT DEFINITIONS
let lightData: Light[] = [
{
type: "ambient", // Applies uniformly to the whole scene
intensity: 0.2,
},
{
type: "point", // Emits light from a specific position in space
intensity: 0.6,
position: { x: 2, y: 1, z: 0 },
},
{
type: "directional", // Light with a direction but no position (like sunlight)
intensity: 0.2,
direction: { x: 1, y: 4, z: 4 },
},
];
// Create and display the canvas
let cmOptions: CanvasManagerProps = {
target: document.getElementById("root") as HTMLDivElement,
height: canvasHeight,
width: canvasWidth,
};
let cm = new CanvasManager(cmOptions);
cm.showCanvas();
// throttle the pointer move event handler calls
function throttle(fn, delay = 50) {
let lastCall = 0;
return (...args) => {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
fn(...args);
}
};
}
// Adding pointer movements to the canvas in order to change camera angle
// This will use pointe events which will be useful for Touch devices also
// Pointer movements can be disabled. Use disablePointerMovements() method on Canvas Manager instance.
let prevClientX = null;
let enablePointerMovementsOptions:EnablePointerMovementsProps = {
pointerdown: (e) => {
prevClientX = e.clientX;
},
pointermove: throttle((e) => {
if (prevClientX !== null) {
const currentX = e.clientX;
const diff = currentX - prevClientX;
prevClientX = currentX;
// Left swipe → diff < 0 → rotate left
// Right swipe → diff > 0 → rotate right
const angle = diff * 0.001;
// Initially the camera is positioned at {x:0, y:0, z:0} and looks at +ve Z direction into the screen
// A -ve angle will make the camera look left
// A +ve angle will make the camera look right
rm.lookAt(angle);
}
}, 16),
pointerup: (e) => {
prevClientX = null;
}
};
cm.enablePointerMovements();
// Options passed to the raytracing engine
let rmOptions: RaytracingManagerProps = {
canvasHeight: canvasHeight,
canvasWidth: canvasWidth,
cameraPosition: cameraPosition,
distanceFromCameraToViewport: distanceFromCameraToViewport,
shapeData: shapeData,
lightData: lightData,
noIntersectionColor: { r: 255, g: 255, b: 255 }, // Background color
reflectiveRecursionLimit: 3, // Max depth for recursive reflections
putPixelCallback: (pixelData: Pixel[]) => {
cm.putPixel(pixelData); // Draws pixels to the canvas
},
};
// Start rendering and measure how long it takes
let t1 = window.performance.now();
let rm = new RaytracingManager(rmOptions);
rm.start().then(() => {
let t2 = window.performance.now();
// Display render time in top-left corner
let target = document.getElementById("root");
let div = document.createElement("div");
div.innerText = t2 - t1 + " " + "ms";
div.style.position = "absolute";
div.style.top = "0px";
div.style.left = "0px";
div.style.backgroundColor = "black";
div.style.color = "white";
(target as HTMLDivElement).append(div);
});For more details about the ray tracing algorithm, see the theoretical notes below and source code.
Raytracing Algo
- Ray tracing begins with a scene.
- A scene consists of a camera, a viewport, and a 3D world.
- Think of the camera as an eye and the viewport as a screen with small square openings. Each opening corresponds to a pixel on the canvas.
- For every square (or pixel) on the viewport, a ray is cast from the camera through it into the 3D world.
- If the ray intersects a 3D object, the corresponding pixel on the canvas is painted with the color of that object at the point of intersection.

Modelling Lighting
Types of Light Sources
Based on origin:
- Emissive Light: Light emitted directly from a source (e.g., bulb, sun).
- Scattered Light: Light reflected off surfaces. Acts as a secondary source.
Based on mathematical modeling:
- Point Light: Light originates from a single point. Each surface point has a different light vector (e.g., a bulb).
- Directional Light: Light comes from a far-away source. All surface points share the same direction vector (e.g., sunlight).
- Ambient Light: A constant, low-intensity light representing indirect scattering from the environment.
Surface Types
- Matte Surface: Reflects light equally in all directions (diffuse reflection).
- Shiny Surface: Reflects light in a specific direction (specular reflection).
Diffuse Reflection Calculation

To compute how a matte surface reflects light:
- I: Light intensity (thickness of the light beam)
- A: Surface area over which the light spreads
- L: Light vector (from point to light source)
- N: Surface normal at the point
- a: Angle between L and N
Reflected Intensity = Light Intensity x (I/A) = Light Intensity × cos(a)
- When a approaches 0 degrees, the ratio I/A approaches 1 (maximum reflection)
- When a approaches 90 degrees, A approaches infinity, the ratio I/A approaches 0 (no reflection)
To compute how I/A is same as cos(a) from the diagram:
- angle SPR = a + b = 90 degrees
- angle ZRP = 90 degrees - b = a
- cosine(a) = RZ/RP = (I/2) / (A/2) = I/A
This models how light spreads over a larger area at shallow angles, thus reducing its intensity.
Specular Reflection Calculation

To compute how a shiny surface reflects light:
- L — Light vector (from the point on the surface to the light source)
- N — Surface normal at that point
- R — Perfectly reflected light vector
- Vi — View vectors (e.g., V1, V2, V3, V4) from the point toward the camera
- a — Angle between the reflected light vector (R) and a view vector (e.g., V3)
No surface is perfectly smooth — meaning light isn't only reflected in the exact direction of R, but also slightly around it. This gives rise to specular highlights, which appear brighter when:
- The view direction is aligned with the reflected light
- The surface is highly polished or shiny
We calculate the reflected light intensity as follows:
Reflected Intensity = Light Intensity × (cos(a))^specular
- The
specularexponent determines how shiny the surface is. - Higher values produce smaller, sharper highlights.
- Lower values produce broader, softer highlights.
- This is because higher specular value makes the cosine curve narrower. Check the image below for cos(a)^b

When a = 0 degrees (perfect alignment), the intensity is maximum. As a increases toward 90 degrees, intensity drops rapidly. Raising cos(a) to a high power compresses the reflection into a narrow beam — simulating a shiny surface.
Working of Raytracing and Light
During ray tracing, if the ray vector does not intersect with anything, then { r: 0, g: 0, b: 0 } is returned.
If the ray intersects with something, then depending on the intensity of light reflected by the surface, its color is modified like:
{ r: valueR * ReflectedLightIntensity, g: valueG * ReflectedLightIntensity, b: valueB * ReflectedLightIntensity }
Therefore, when no lights are present, it will be pitch black as shown in below video:
Modelling Shadows
- In raytracing we cast a ray from camera and find its intersection with an object.
- From the point of intersection (P) towards the light source we have a vector called the light vector (L) which we saw earlier in lights modelling section.
- We form a new ray vector of the form P + t*L where t is a positive number and can vary, P and L are 3D vectors.
- This ray starts at the intersection point and travels in the direction of the light source.
- If this shadow ray intersects any other object before reaching the light, it means the light is blocked, and point P lies in shadow.
- If no object obstructs the shadow ray, then the light reaches point P, and we proceed with lighting calculations (diffuse, specular)
Modelling Reflections of other objects on surface
To make surfaces look shiny or mirror-like, we simulate how light bounces off them. When a ray of light hits a reflective object, we send another ray in the direction it would bounce — just like how you'd see your reflection in a mirror.
This new ray continues the same process: it might hit something else, reflect again, and so on. We recursively repeat this bounce a few times to create realistic reflections, but stop after a set limit to avoid infinite loop (like mirror infront of mirror scenario)
Limitation Right now, all of this happens on the main thread — and since every pixel may involve multiple recursive rays, the UI can freeze. Using Web Workers to offload this heavy computation can make rendering much smoother and faster.
Camera rotation
Currently it is possible to rotate camera along +ve Y-axis/Yaw
In raytracing the idea is to cast a ray from camera into the scene and compute intersections. Therefore on camera rotation the direction vector of the ray changes. Which is calculated by transforming the vector with a rotation matrix.
Notes
- This raytracer runs entirely on the CPU using
CanvasRenderingContext2D - No WebGL, no Three.js, no shaders — just math and canvas

