Canvas JavaScript

A Raytraced Sphere with Lighting

This JavaScript program demonstrates how to draw a raytraced sphere with lighting using an image data instance to set individual pixels on a canvas element.

RaytracedSphereWithLighting.html

<!DOCTYPE html>
<html>
  <head>
    <title>XoaX.net's Javascript</title>
    <script type="text/javascript" src="RaytracedSphereWithLighting.js"></script>
  </head>
  <body onload="Initialize()">
    <canvas id="idCanvas" width="600" height ="600" style="background-color: #F0F0F0;"></canvas>
  </body>
</html>

RaytracedSphereWithLighting.js

function Initialize() {
	var qCanvas = document.getElementById("idCanvas");
	var qContext = qCanvas.getContext("2d");
	var qImageData = qContext.createImageData(600, 600);
	
	const kiWidth = 600;
	const kiHeight = 600;
	// The viewing plane is perpendicular to the view vector (x + 2y + 3z = 0)
	// It is oriented so that the positive z-axis aligns with the up in the image data, or negative y
	// To find the postive x-axis of the canvas in space, take the cross product of the view vector vector (-1, -2, -3)/sqrt(14) and (0,0,1)
	// This gives (-2, 1, 0)/sqrt(5), after normalization to unit length 
	const kdaView = [-1.0/Math.sqrt(14), -3.0/Math.sqrt(14), -2.0/Math.sqrt(14)];
	const kdaDeltaX = [-2.0/Math.sqrt(5), 1.0/Math.sqrt(5), 0.0];
	const kdaDeltaY = [kdaDeltaX[2]*kdaView[1] - kdaDeltaX[1]*kdaView[2],
										kdaDeltaX[0]*kdaView[2] - kdaDeltaX[2]*kdaView[0],
										kdaDeltaX[1]*kdaView[0] - kdaDeltaX[0]*kdaView[1]];
	// Assuming that the canvas is 6 by 6 in space and centered at the origin, the upper left pixel (0, 0) is given below (note the half pixel offset)
	const kdPixelOffset = 1.0/100.0;
	const kdMult = 3.0 - (kdPixelOffset/2.0);
	const kdaUpperLeft = [-kdMult*(kdaDeltaX[0] + kdaDeltaY[0]), -kdMult*(kdaDeltaX[1] + kdaDeltaY[1]), -kdMult*(kdaDeltaX[2] + kdaDeltaY[2])];
	var iPixel = 0;
	var daPos = [kdaUpperLeft[0], kdaUpperLeft[1], kdaUpperLeft[2]];
	var dRow = 0.0;
	for (var y = 0; y < 600; ++y) {
		for (var x = 0; x < 600; ++x) {
			// The parametric line for each pixel is defined by the position and the view vector: p + tv and we want to find the lowest intersecting t value
			// We define a sphere at (0,0,0) with radius 1 and a plane at z = -.5 that it sits on
			// Find where the line intersects the sphere x^x + y*y + z*z = 1
			// Solve for the t values and take the lower value, if they are real
			// (px + t*vx)*(px + t*vx) + (py + t*vy)*(py + t*vy) + (pz + t*vz)*(pz + t*vz) = 1
			// px*px + py*py + pz*pz - 1 + 2t*(px*vx + py*vy + pz*vz) + t^2*(vx*vx + vy*vy + vz*vz) = 0
			var dA = kdaView[0]*kdaView[0] + kdaView[1]*kdaView[1] + kdaView[2]*kdaView[2];
			var dB = 2.0*(daPos[0]*kdaView[0] + daPos[1]*kdaView[1] + daPos[2]*kdaView[2]);
			var dC = daPos[0]*daPos[0] + daPos[1]*daPos[1] + daPos[2]*daPos[2] - 1.0;
			var dDisc = dB*dB - 4.0*dA*dC;
			
			var daLight = [-1.0/Math.sqrt(6), -1.0/Math.sqrt(6) , -2.0/Math.sqrt(6)];
			
			// The line intersects the sphere only if there are real roots
			if (dDisc >= 0.0) {
				var dLowT = (-dB - Math.sqrt(dDisc))/(2.0*dA);
				var daSurf = [daPos[0] + dLowT*kdaView[0], daPos[1] + dLowT*kdaView[1], daPos[2] + dLowT*kdaView[2]];
				var daNeg = [-daSurf[0], -daSurf[1], -daSurf[2]];
				var dDot = daLight[0]*daNeg[0] + daLight[1]*daNeg[1] + daLight[2]*daNeg[2];
				qImageData.data[iPixel] = dDot*255;
				qImageData.data[iPixel+1] = dDot*128;
				qImageData.data[iPixel+2] = dDot*0;
				qImageData.data[iPixel+3] = 255;
			} else {
				// Otherwise, find the intersection with the plane z = -1.0
				// pz + t*vz = -1 --> t = -(pz + 1)/vz
				var dIntersectT = -(daPos[2] + 1.0)/kdaView[2];
				var daGround = [daPos[0] + dIntersectT*kdaView[0], daPos[1] + dIntersectT*kdaView[1], daPos[2] + dIntersectT*kdaView[2]];
				var iFloorX = Math.floor(daGround[0]);
				var iFloorY = Math.floor(daGround[1]);
				// Antialias at points near edges, possibly or subsample
				if (((iFloorX + iFloorY) % 2) == 0) {
					qImageData.data[iPixel] = 128;
					qImageData.data[iPixel+1] = 128;
					qImageData.data[iPixel+2] = 255;
					qImageData.data[iPixel+3] = 255;
				} else {
					qImageData.data[iPixel] = 255;
					qImageData.data[iPixel+1] = 255;
					qImageData.data[iPixel+2] = 128;
					qImageData.data[iPixel+3] = 255;			
				}
				// Check for a shadow
				// Take the intersection point with the light vector to create the line. If the discriminant is positive, the point is in the shadow
				var dA2 = daLight[0]*daLight[0] + daLight[1]*daLight[1] + daLight[2]*daLight[2];
				var dB2 = 2.0*(daGround[0]*daLight[0] + daGround[1]*daLight[1] + daGround[2]*daLight[2]);
				var dC2 = daGround[0]*daGround[0] + daGround[1]*daGround[1] + daGround[2]*daGround[2] - 1.0;
				var dDisc2 = dB2*dB2 - 4.0*dA2*dC2;
				if (dDisc2 >= 0.0) {
					var dIntT = (-dB2 + Math.sqrt(dDisc2))/(2.0*dA2);
					dIntT = (-dIntT > 1.0) ? -1.0 : dIntT;
					qImageData.data[iPixel] *= -dIntT;
					qImageData.data[iPixel+1] *= -dIntT;
					qImageData.data[iPixel+2] *= -dIntT;
				}
			}

			// Update the position and pixel
			daPos[0] += kdaDeltaX[0]/100.0;
			daPos[1] += kdaDeltaX[1]/100.0;
			daPos[2] += kdaDeltaX[2]/100.0;
			iPixel += 4;
		}
		// The position must be reset for each row. Note row equals y + 1. So, may not be needed, but y is an integer and the conversion may not be correct.
		dRow += 1.0;
		daPos[0] = kdaUpperLeft[0] + dRow*kdaDeltaY[0]/100.0;
		daPos[1] = kdaUpperLeft[1] + dRow*kdaDeltaY[1]/100.0;
		daPos[2] = kdaUpperLeft[2] + dRow*kdaDeltaY[2]/100.0;
	}

	// Use the image data to draw the pixels at (0, 0)
	qContext.putImageData(qImageData, 0, 0);
}
 

Output

 
 

© 2007–2025 XoaX.net LLC. All rights reserved.