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.
<!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>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);
}© 20072025 XoaX.net LLC. All rights reserved.