Canvas JavaScript

A Raytraced, Subsampled Sphere

This JavaScript program demonstrates how to draw a raytraced, subsampled sphere with lighting using an image data instance to set individual pixels on a canvas element. Use the arrow keys to change the view.

RaytraceWithSubsampling.html

<!DOCTYPE html>
<html>
  <head>
    <title>XoaX.net's Javascript</title>
    <script type="text/javascript" src="RaytraceWithSubsampling.js"></script>
    <style>
    	.cFocus { border: 1px red solid; }
    	.cBlur { border: none; }
    </style>
  </head>
  <body onload="Initialize()">
    <canvas id="idCanvas" width="600" height ="600" style="background-color: #F0F0F0;"></canvas>
  </body>
</html>

RaytraceWithSubsampling.js

class CSphere {
	constructor(daC, dR) {
		this.mdaC = [daC[0], daC[1], daC[2]];;
		this.mdR = dR;
	}
	
	// Pass in the position and direction for the ray
	Intersect(daP, daV, daTanDir) {
		// Sphere: (x - cx)^2 + (y - cy)^2 + (z - cz)^2 = r^2
		// Sphere with ray: (px + tvx - cx)^2 + (py + tvy - cy)^2 + (pz + tvz - cz)^2 = r^2
		// Solve for terms: (px^2 - 2pxcx + cx^2) + (py^2 - 2pycy + cy^2) + (pz^2 - 2pzcz + cz^2) - r^2
		// + 2t[(vx(px - cx)) + (vy(py - cy)) + (vz(pz - cz))]
		// + t^2(vx^2 + vy^2 + vz^2)
		var dT = NaN;
		var dC = daP[0]*daP[0] + daP[1]*daP[1] + daP[2]*daP[2] + 
			-2.0*(daP[0]*this.mdaC[0] + daP[1]*this.mdaC[1] + daP[2]*this.mdaC[2]) +
			this.mdaC[0]*this.mdaC[0] + this.mdaC[1]*this.mdaC[1] + this.mdaC[2]*this.mdaC[2] -  this.mdR*this.mdR;
		var dB = 2.0*(daV[0]*(daP[0] - this.mdaC[0]) + daV[1]*(daP[1] - this.mdaC[1]) + daV[2]*(daP[2] - this.mdaC[2]));
		var dA = daV[0]*daV[0] + daV[1]*daV[1] + daV[2]*daV[2];
		var dDisc = dB*dB - 4.0*dA*dC;
		if (dDisc > 0) {
			// T is either 2C/(-B - sqrt(B*B - 4AC)) or (-B - sqrt(B*B - 4AC))/2A
			// The second anser is closer. So, we use that one to give the first intersection.
			dT = (-dB - Math.sqrt(dDisc))/(2.0*dA);
			// The tangent plane direction is [2(x - cx), 2(y - cy), 2(z - cz)]
			// x = px + t*vx, y = py + tvy, z = pz + tvz
			var dMag =  Math.sqrt(2.0*(daP[0] + dT*daV[0] - this.mdaC[0])*2.0*(daP[0] + dT*daV[0]  - this.mdaC[0]) +
				2.0*(daP[1] + dT*daV[1] - this.mdaC[1])*2.0*(daP[1] + dT*daV[1] - this.mdaC[1]) +
				2.0*(daP[2] + dT*daV[2] - this.mdaC[2])*2.0*(daP[2] + dT*daV[2] - this.mdaC[2]));
			daTanDir[0] = 2.0*(daP[0] + dT*daV[0] - this.mdaC[0])/dMag;
			daTanDir[1] = 2.0*(daP[1] + dT*daV[1] - this.mdaC[1])/dMag;
			daTanDir[2] = 2.0*(daP[2] + dT*daV[2] - this.mdaC[2])/dMag;
		}
		return dT;
	}
}

class CCanvasPlane {
	// Pass in the canvas size in pixels and the size in space
	// The pixels of the canvas will be cenered at the origin
	// So, if the canvas is 600x600 pixels. Pixel (299.5, 299.5) will be at the origin,
	// with pixels starting at 0 and going to 599
	constructor(sCanvasId, dW, dH)  {
		var qCanvas = document.getElementById(sCanvasId);
		this.mqContext = qCanvas.getContext("2d");
		this.miPixelW = qCanvas.width;
		this.miPixelH = qCanvas.height;
		this.mqImData = this.mqContext.createImageData(this.miPixelW, this.miPixelH);
		this.mdW = dW;
		this.mdH = dH;
		// Set some default angles
		this.mqAlpha = Math.PI/6;
		this.mqBeta = -Math.PI/6;
	}
	
	Clear() {
		this.mqContext.clearRect(0, 0, 640, 480);
	}
	
	Left() {
		this.mqAlpha +=  Math.PI/12;
	}
	Right() {
		this.mqAlpha -=  Math.PI/12;
	}
	Up() {
		if (this.mqBeta + Math.PI/12 <- .01) {
			this.mqBeta +=  Math.PI/12;
		}
	}
	Down() {
		if (this.mqBeta - Math.PI/12 > -Math.PI/2 + .01) {
			this.mqBeta -=  Math.PI/12;
		}
	}
	
	CreateCoordinateVectors() {
		var daaA = [];
			// This is the x direction of the canvas inside the plane z= 0 in space coordinates
			daaA[0] = [Math.cos(this.mqAlpha), Math.sin(this.mqAlpha), 0];
			// cos(beta)*up + sin(beta)*(vector perp to x pointing forward), since beta is angle between the canvas and the z-axis
			daaA[1] = [-daaA[0][1]*Math.sin(this.mqBeta), daaA[0][0]*Math.sin(this.mqBeta), Math.cos(this.mqBeta)];
			// The vector straigth out of canvas. The cross product of the previous vectors
			daaA[2] = [daaA[0][1]*daaA[1][2] - daaA[0][2]*daaA[1][1],
				daaA[0][2]*daaA[1][0] - daaA[0][0]*daaA[1][2],
				daaA[0][0]*daaA[1][1] - daaA[0][1]*daaA[1][0]];
		return daaA;
	}
	
	DrawScene() {
		// Create a sphere
		var qUnitSphere = new CSphere([0.0, 0.0, 0.0], 1.0);
		
		// The pixel width and height in the coordinates of the space
		var dPixWidth = this.mdW/this.miPixelW;
		var dPixHeight = this.mdH/this.miPixelH;
		var qCanvasInSpace = this.CreateCoordinateVectors();
		var daDirX = qCanvasInSpace[0];
		var daDirY = [-qCanvasInSpace[1][0], -qCanvasInSpace[1][1], -qCanvasInSpace[1][2]];
		var daView = qCanvasInSpace[2];
		var dPixStartX = dPixWidth*.5 - (this.mdW/2);
		var dPixStartY = dPixHeight*.5 - (this.mdH/2);
		// The position in space of the center of the first pixel
		var daPixPosInit = [dPixStartX*daDirX[0]+dPixStartY*daDirY[0], 
			dPixStartX*daDirX[1]+dPixStartY*daDirY[1], 
			dPixStartX*daDirX[2]+dPixStartY*daDirY[2]];
		// The position of the center of the first pixel in the current row.
		var daPixPosRow = [daPixPosInit[0], daPixPosInit[1], daPixPosInit[2]];
		// The center point of the current pixel.
		var daPixPos = [daPixPosInit[0], daPixPosInit[1], daPixPosInit[2]];
		// The translation vector for a pixel in the x-direction
		var daPixDx = [dPixWidth*daDirX[0], dPixWidth*daDirX[1], dPixWidth*daDirX[2]];
		// The translation vector for a pixel in the y-direction
		var daPixDy = [dPixHeight*daDirY[0], dPixHeight*daDirY[1], dPixHeight*daDirY[2]];
		// The current pixel index
		var iPix = 0;
		var daTanDir = [0.0, 0.0, 0.0];
		var daLightDir = [-1.0/Math.sqrt(14.0), -2.0/Math.sqrt(14.0), -3.0/Math.sqrt(14.0)]
		for (var i = 0; i < this.miPixelW; ++i) {
			for (var j = 0; j < this.miPixelH; ++j) {
				var daSubDx = [daPixDx[0]/4.0, daPixDx[1]/4.0, daPixDx[2]/4.0];
				var daSubDy = [daPixDy[0]/4.0, daPixDy[1]/4.0, daPixDy[2]/4.0];
				var daSubPosRow = [daPixPos[0] - 1.5*(daSubDx[0] + daSubDy[0]),
					daPixPos[1] - 1.5*(daSubDx[1] + daSubDy[1]),
					daPixPos[2] - 1.5*(daSubDx[2] + daSubDy[2])];
				var daSubPos = [daSubPosRow[0], daSubPosRow[1], daSubPosRow[2]];
				var dR = 0.0;
				var dG = 0.0;
				var dB = 0.0;
				// Pixel subsampling 4x4 = 16 samples per pixel.
				for (var m = 0; m < 4; ++m) {
					for (var n = 0; n < 4; ++n) {
						var dT = qUnitSphere.Intersect(daSubPos, daView, daTanDir);
						if (Number.isNaN(dT)) { // This branch renders the floor z = -1.0
							// Make a checkboard using the floor function pz + t*vz = -1.0 --> t = -(pz + 1.0)/vz
							if (daView[2] < 0.0) {
								var dFloorT = -(daSubPos[2] + 1.0)/daView[2];
								// Check for a shadow and reduce the color accordingly.
								var daGround = [daSubPos[0] + dFloorT*daView[0], daSubPos[1] + dFloorT*daView[1], daSubPos[2] + dFloorT*daView[2]];
								var iFloorX = Math.floor(daGround[0]);
								var iFloorY = Math.floor(daGround[1]);
								var daToLight = [daLightDir[0], daLightDir[1], daLightDir[2]];
								var daIgnored = [0.0,0.0,0.0];
								var dShadowT = qUnitSphere.Intersect(daGround, daToLight, daIgnored);
								if (Number.isNaN(dShadowT)) {
									if (((iFloorX + iFloorY) % 2) == 0) {
										dR += 64;
										dG += 64;
										dB += 64;
									} else {
										dR += 128;
										dG += 0;
										dB += 0;		
									}
								} else { // Shadow
									var dReduce = (1.0 - Math.exp(-dShadowT*dShadowT/16.0));
									if (((iFloorX + iFloorY) % 2) == 0) {
										dR += 64*dReduce;
										dG += 64*dReduce;
										dB += 64*dReduce;
									} else {
										dR += 128*dReduce;
										dG += 0*dReduce;
										dB += 0*dReduce;		
									}							
								}
							} else {
								dR += 128;
								dG += 0;
								dB += 0;
							}
						} else { // This branch renders the sphere with diffuse lighting
							var dDot = daTanDir[0]*daLightDir[0] + daTanDir[1]*daLightDir[1] + daTanDir[2]*daLightDir[2];
							dDot = (dDot < 0.0) ? -dDot: 0.0;
							dR += 0;
							dG += 128*dDot;
							dB += 128*dDot;
						}
						daSubPos[0] += daSubDx[0];
						daSubPos[1] += daSubDx[1];
						daSubPos[2] += daSubDx[2];
					}
					daSubPosRow[0] += daSubDy[0];
					daSubPosRow[1] += daSubDy[1];
					daSubPosRow[2] += daSubDy[2];
					daSubPos[0] = daSubPosRow[0];
					daSubPos[1] = daSubPosRow[1];
					daSubPos[2] = daSubPosRow[2];
				}
				// The sum is over 16 samples. So, divide by the sample size.
				this.mqImData.data[iPix] = dR/16.0;
				this.mqImData.data[iPix+1] = dG/16.0;;
				this.mqImData.data[iPix+2] = dB/16.0;;
				this.mqImData.data[iPix+3] = 255;

				daPixPos[0] += daPixDx[0];
				daPixPos[1] += daPixDx[1];
				daPixPos[2] += daPixDx[2];
				iPix += 4;
			}
			daPixPosRow[0] += daPixDy[0];
			daPixPosRow[1] += daPixDy[1];
			daPixPosRow[2] += daPixDy[2];
			daPixPos[0] = daPixPosRow[0];
			daPixPos[1] = daPixPosRow[1];
			daPixPos[2] = daPixPosRow[2];
		}
		// Use the image data to draw the pixels at (0, 0)
		this.mqContext.putImageData(this.mqImData, 0, 0);
	}
}

var qCP = null;

function Initialize() {
	qCP = new CCanvasPlane("idCanvas", 4.0, 4.0);
	window.onkeydown=KeyDownFunction;
	window.addEventListener("focus", FocusFunction);
	window.addEventListener("blur", BlurFunction);
	window.focus();
	FocusFunction();
	qCP.DrawScene();
}

function FocusFunction() {
	document.getElementById("idCanvas").className ='cFocus';
}

function BlurFunction() {
	document.getElementById("idCanvas").className ='cBlur';
}

function KeyDownFunction(e) {
	var iKeyUp = 38;
	var iKeyLeft = 37;
	var iKeyRight = 39;
	var iKeyDown = 40;
	var iKeyCode = 0;
	if (e) {
		iKeyCode = e.which;
	} else {
		iKeyCode = window.event.keyCode;
	}
	qCP.Clear();
	switch (iKeyCode) {
		case iKeyUp: {
			qCP.Up();
			// Prevent the window scrolling
      e.preventDefault();
			break;
		}
		case iKeyLeft: {
			qCP.Left();
			// Prevent the window scrolling
      e.preventDefault();
			break;
		}
		case iKeyRight: {
			qCP.Right();
			// Prevent the window scrolling
      e.preventDefault();
			break;
		}
		case iKeyDown: {
			qCP.Down();
			// Prevent the window scrolling
      e.preventDefault();
			break;
		}
		default: {
			break;
		}
	}
	qCP.DrawScene();
}
 

Output

 
 

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