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