This JavaScript program demonstrates how to draw a raytraced, subsampled paraboloid 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="RaytracedParaboloid.js"></script>
<style>
.cFocus { border: 1px red solid; }
.cBlur { border: none; }
</style>
</head>
<body onload="Initialize()">
<div style="float:left;">
<canvas id="idCanvas" width="600" height ="600" style="background-color: #FFFFFF;"></canvas><br />
<label for="idTime">Rendering Time: <input id="idTime" type="text" size="10" /> milliseconds</label>
</div>
</body>
</html>// I will use a light from above to light the graph. Since this is a function, there are no shadows. class CGraph { constructor(fnF) { this.mdaaBounds = [[-1, 1], [-1, 1], [-1, 1]]; } // Get two intersections with a ray. The first one is semi-tranparent. The second is opaque, possibly with a grid. // Intersect the graph in between and composite it. Intersect(daP, daV, fnF, fnGradF) { // Makes sure that the direction is nonzero or nearly zero so that we do not divide by zero. Otherwise, we will skip it let baSkip = [false, false, false]; for (let i = 0; i < 3; ++i) { if (Math.abs(daV[i]) < 1.0e-15) { baSkip[i] = true; } } let iSideIntersections = []; let daT = []; for (let i = 0; i < 3; ++i) { // Makes sure that the direction is nonzero so that we do not divide by zero. if (!baSkip[i]) { // Loop over the low and high values for the range for this coordinate // Find the t values of the intersections for the low and high x values // e.g. For x, we have x0 = Px + t*Vx => t = (x0 - Px)/Vx for (let j = 0; j < 2; ++j) { let dT = (this.mdaaBounds[i][j] - daP[i])/daV[i]; // Loop over the other coordinates and make sure that the intersection is within the range let bInRange = true for (let k = 0; k < 2; ++k) { let iCoord = ((i + k + 1) % 3); let dP = daP[iCoord]+dT*daV[iCoord]; if (dP < this.mdaaBounds[iCoord][0] || dP > this.mdaaBounds[iCoord][1]) { bInRange = false; } } if (bInRange) { if (daT.length == 0) { daT.push(dT); iSideIntersections.push([i,j]); } else if (daT.length == 1) { // If the new dT value is greater, put it at the end. Otherwise, put it in the first position with unshift if (daT[0] < dT) { daT.push(dT); iSideIntersections.push([i,j]); } else { daT.unshift(dT); iSideIntersections.unshift([i,j]); } break; } } } } if (daT.length == 2) { break; } } // There are at most 6 entries. Sort them and remove duplicates let daColor = [0,0,0,255]; let daColorX = [255,200,200,255]; let daColorY = [200,255,200,255]; let daColorZ = [200,200,255,255]; let dAlpha = 0.0; let daaColors = [daColorX, daColorY, daColorZ]; for (let i = 0; i < daT.length; ++i) { // There is only two intersections. The alpha of the first intesection is just the second number. let dColorAlpha = i*.85 + .15; let iDim = iSideIntersections[i][0]; if (i == 1) { // Get the intersection near the front let dT1 = daT[0]; let dT2 = daT[1]; let daDF1 = [0.0, 0.0]; while ((dT1 >= daT[0]) && (dT1 <= daT[1]) && (Math.abs(dT1 - dT2) >= 1.0e-10)) { let dX1 = daP[0] + dT1*daV[0]; let dY1 = daP[1] + dT1*daV[1]; let dZ1 = daP[2] + dT1*daV[2]; let dF1 = fnF(dX1, dY1); daDF1 = fnGradF(dX1, dY1); dT2 = dT1; // Get the z values at each of these points and determine where the function crosses the the line // z = F(x, y), L(t) = [x(t), y(t), z(t)], G(t) = z(t) - F(x(t), y(t)) // G'(t) = z'(t) - [dF(x(t), y(t))/dx*x'(t) + dF(x(t), y(t))/dy*y'(t)] // x'(t), y'(t), and z'(t) are all constants since L(t) = [at + b, ct + d, et + f] // G'(t) = e - dF/dx(t)*a - dF/dy(t)*c // Newton's method x(n) = x(n-1) - f(x(n-1))/f'(x(n-1)), in this case, we use t. // t(n) = t(n-1) - G(t(n-1))/G'(t(n-1)) dT1 = dT1 - (dZ1 - dF1)/(daV[2] - daDF1[0]*daV[0] - daDF1[1]*daV[1]); } let dFirstT = dT1; if (Math.abs(dT1 - dT2) < 1.0e-10) { // Should this be the top or bottom of the surface? Check the gradient let dDot = daV[2] - daDF1[0]*daV[0] - daDF1[1]*daV[1]; // If we are seeing the top of the surface; if (dDot < 0.0) { let dMag = Math.sqrt(daDF1[0]*daDF1[0] + daDF1[1]*daDF1[1] + 1); let daGradient = [-daDF1[0]/dMag, -daDF1[1]/dMag, 1.0/dMag]; let daSurfaceColor = [255,255,255,255]; // Change the color to draw lines on the surface let dMx = 10.0*(daP[0] + dT1*daV[0]); let dMy = 10.0*(daP[1] + dT1*daV[1]); if ((Math.abs(dMx - Math.round(dMx)) < .02) || (Math.abs(dMy - Math.round(dMy)) < .02)) { daSurfaceColor = [200,200,200,255]; } daColor[0] += (1.0 - dAlpha)*.95*(daGradient[2])*daSurfaceColor[0]; daColor[1] += (1.0 - dAlpha)*.95*(daGradient[2])*daSurfaceColor[1]; daColor[2] += (1.0 - dAlpha)*.95*(daGradient[2])*daSurfaceColor[2]; dAlpha += (1.0 - dAlpha)*.95; } else { let daBlack = [0,0,0,255]; daColor[0] += (1.0 - dAlpha)*daBlack[0]; daColor[1] += (1.0 - dAlpha)*daBlack[1]; daColor[2] += (1.0 - dAlpha)*daBlack[2]; dAlpha += (1.0 - dAlpha); } } // Find a potential back intersection from the back side // This could only be a back side intersection because we have the first intersection already. // Also, the surface is a convex set. So, it has has at most 2 intersections. dT1 = daT[1]; dT2 = daT[0]; daDF1 = [0.0, 0.0]; while ((dT1 >= daT[0]) && (dT1 <= daT[1]) && (Math.abs(dT1 - dT2) >= 1.0e-10)) { let dX1 = daP[0] + dT1*daV[0]; let dY1 = daP[1] + dT1*daV[1]; let dZ1 = daP[2] + dT1*daV[2]; let dF1 = fnF(dX1, dY1); daDF1 = fnGradF(dX1, dY1); dT2 = dT1; dT1 = dT1 - (dZ1 - dF1)/(daV[2] - daDF1[0]*daV[0] - daDF1[1]*daV[1]); } // We want to make sure that the t values are different for the second intersection, before using it. if ((Math.abs(dT1 - dT2) < 1.0e-10) && (Math.abs(dT1 - dFirstT) > 1.0e-5)) { let daBlack = [0,0,0,255]; daColor[0] += (1.0 - dAlpha)*daBlack[0]; daColor[1] += (1.0 - dAlpha)*daBlack[1]; daColor[2] += (1.0 - dAlpha)*daBlack[2]; dAlpha += (1.0 - dAlpha); } } daColor[0] += (1.0 - dAlpha)*dColorAlpha*daaColors[iDim][0]; daColor[1] += (1.0 - dAlpha)*dColorAlpha*daaColors[iDim][1]; daColor[2] += (1.0 - dAlpha)*dColorAlpha*daaColors[iDim][2]; dAlpha += (1.0 - dAlpha)*dColorAlpha; } if (daT.length > 0) { return daColor; } return null; } } // z = 1 - x^2 - y^2 function Paraboloid(dX, dY) { return 1-dX*dX-dY*dY; } function GradParaboloid(dX, dY) { return [-2*dX, -2*dY]; } 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, 600, 600); } Left() { this.mqAlpha += Math.PI/48; } Right() { this.mqAlpha -= Math.PI/48; } Up() { if (this.mqBeta + Math.PI/48 < Math.PI/2 - .01) { this.mqBeta += Math.PI/48; } } Down() { if (this.mqBeta - Math.PI/48 > -Math.PI/2 + .01) { this.mqBeta -= Math.PI/48; } } 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() { gdStartTime = performance.now(); let qGraph = new CGraph(null); // 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)]; var daLightDir = [0, 0, -1]; 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; var dA = 0.0; // Pixel subsampling 4x4 = 16 samples per pixel. for (var m = 0; m < 4; ++m) { for (var n = 0; n < 4; ++n) { let daColor = qGraph.Intersect(daSubPos, daView, Paraboloid, GradParaboloid); if (daColor == null) { dR += 240; dG += 240; dB += 240; dA += 0; } else { dR += daColor[0]; dG += daColor[1]; dB += daColor[2]; dA += 16; } 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] = (dA > 255) ? 255 : dA; 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); gqTimeInput.value = Math.round(performance.now() - gdStartTime); } } var qCP = null; var gqTimeInput = null; var gdStartTime = 0.0; function Initialize() { gqTimeInput = document.getElementById("idTime"); 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.