This JavaScript program demonstrates how to draw a raytraced, earth-textured 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>
<link rel="icon" href="data:,">
<title>XoaX.net's Javascript</title>
<script type="text/javascript" src="RaytraceWithEarthTexture.js"></script>
<style>
.cFocus { border: 1px red solid; }
.cBlur { border: none; }
</style>
</head>
<body onload="LoadImage()">
<canvas id="idCanvas" width="600" height ="600" style="background-color: #F0F0F0;"></canvas>
</body>
</html>var qCP = null;
var qImageTexture = null;
var qEarthImageData = null;
var iImageWidth = 0;
var iImageHeight = 0;
function LoadImage() {
qImageTexture = new Image();
qImageTexture.onload = Initialize;
qImageTexture.src = "Earth1000x500.jpg";
}
function Initialize() {
iImageWidth = qImageTexture.width;
iImageHeight = qImageTexture.height;
var qEarthCanvas = new OffscreenCanvas(iImageWidth, iImageHeight);
var qEarthContext = qEarthCanvas.getContext("2d");
qEarthContext.drawImage(qImageTexture, 0, 0);
qEarthImageData = qEarthContext.getImageData(0, 0, iImageWidth, iImageHeight);
qCP = new CCanvasPlane("idCanvas", 4.0, 4.0);
window.onkeydown=KeyDownFunction;
window.addEventListener("focus", FocusFunction);
window.addEventListener("blur", BlurFunction);
window.focus();
FocusFunction();
qCP.DrawScene();
}
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, iaRGB) {
// 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;
}
// Get the tangent in spherical coordinates.
var dTheta = Math.atan2(daTanDir[0], daTanDir[1]);
var dPhi = Math.acos(daTanDir[2]);
var dPixelX = Math.floor((iImageWidth - 1.0e-10)*dTheta/(2.0*Math.PI)); // Make sure that the number is less than the width
var dPixelY = Math.floor((iImageHeight - 1.0e-10)*dPhi/Math.PI); // Make sure that the number is less than the height
var iPixelIndex = 4*dPixelX + 4*1000*dPixelY;
iaRGB[0] = qEarthImageData.data[iPixelIndex];
iaRGB[1] = qEarthImageData.data[iPixelIndex + 1];
iaRGB[2] = qEarthImageData.data[iPixelIndex + 2];
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 < Math.PI/2 +.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 daRGB = [0.0,0.0,0.0];
var dT = qUnitSphere.Intersect(daPixPos, daView, daTanDir, daRGB);
if (Number.isNaN(dT)) {
this.mqImData.data[iPix] = 0;
this.mqImData.data[iPix+1] = 0;
this.mqImData.data[iPix+2] = 0;
this.mqImData.data[iPix+3] = 255;
} else {
var dDot = daTanDir[0]*daLightDir[0] + daTanDir[1]*daLightDir[1] + daTanDir[2]*daLightDir[2];
dDot = (dDot < 0.0) ? -dDot: 0.0;
this.mqImData.data[iPix] = daRGB[0]*(.2 + .8*dDot);
this.mqImData.data[iPix+1] = daRGB[1]*(.2 + .8*dDot);
this.mqImData.data[iPix+2] = daRGB[2]*(.2 + .8*dDot);
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);
}
}
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();
}
© 20072025 XoaX.net LLC. All rights reserved.