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