This JavaScript program demonstrates how to draw an interactive raytraced paraboloid with subsampling and lighting using an image data instance to set individual pixels on a canvas element. Use the arrow keys to change the view.
Unlike the other raytraced parboloid, this version runs sufficiently quickly to be interactive; two main improvements have been made to increase the speed of the prior rendering. First, the intersection algorithm uses an explicit algebraic intersection of the ray and the paraboloid, which employs the quadratic formula, instead of the more prior Newton-Raphson iterations. Second, the initial rendering uses only one sample per pixels, rather than 16. The higher quality 16 samples per pixel rendering is only used when the user stops moving the view.
<!DOCTYPE html>
<html>
<head>
<title>XoaX.net's Javascript</title>
<script type="text/javascript" src="InteractiveRaytracedParaboloid.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>function Intersect(daP, daV) {
// 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 dBound = -1 + 2.0*j;
let dT = (dBound - 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 < -1.0 || dP > 1.0) {
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 two intersections using the quadratic formula
// z = F(x, y) = 1 - x^2 - y^2
// GradF(x, y) = [-2x, -2y]
// G(x, y, z) = z - F(x, y) = z + x^2 + y^2 - 1
// L(t) = [at + b, ct + d, et + f] = [x(t), y(t), z(t)]
// G(L(t)) = z(t) + x(t)^2 + y(t)^2 - 1 = et + f + (at + b)^2 + (ct + d)^2 - 1
// = (a^2 + c^2)*t^2 + (e + 2ab + 2cd)t + f + b^2 + d^2 - 1
// GradG(x, y, z) = (2x, 2y, 1) = (2at + 2b, 2ct + 2d, 1)
// t = (-b +- sqrt(b^2 - 4ac))/2a
// t1 = (-b - sqrt(b^2 - 4ac))/2a
// t2 = 2c/(-b + sqrt(b^2 - 4ac))
let dA = daV[0]*daV[0] + daV[1]*daV[1];
let dB = daV[2] + 2.0*daV[0]*daP[0] + 2.0*daV[1]*daP[1];
let dC = daP[2] + daP[0]*daP[0] + daP[1]*daP[1] - 1.0;
let dSqrtDisc = Math.sqrt(dB*dB - 4.0*dA*dC);
let dT1 = (-dB - dSqrtDisc)/(2.0*dA);
let dT2 = (2.0*dC)/(-dB - dSqrtDisc);
if (dT1 > dT2) {
let dSwap = dT1;
dT1 = dT2;
dT2 = dSwap;
}
let daParaT = [dT1, dT2];
for (let j = 0; j < daParaT.length; ++j) {
if (daParaT[j] > daT[0] && daParaT[j] < daT[1]) {
let dX = daParaT[j]*daV[0] + daP[0];
let dY = daParaT[j]*daV[1] + daP[1];
let dZ = daParaT[j]*daV[2] + daP[2];
let daDF = [-2.0*dX, -2.0*dY];
let dDot = daV[2] - daDF[0]*daV[0] - daDF[1]*daV[1];
// If we are seeing the top of the surface;
if (dDot < 0.0) {
let dMag = Math.sqrt(daDF[0]*daDF[0] + daDF[1]*daDF[1] + 1);
let daGradient = [-daDF[0]/dMag, -daDF[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);
}
}
}
}
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;
}
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(bRenderFast = true) {
gdStartTime = performance.now();
// 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) {
if (bRenderFast) {
var dR = 0.0;
var dG = 0.0;
var dB = 0.0;
var dA = 0.0;
let daColor = Intersect(daPixPos, daView);
if (daColor == null) {
dR += 240;
dG += 240;
dB += 240;
dA += 0;
} else {
dR += daColor[0];
dG += daColor[1];
dB += daColor[2];
dA += 255;
}
this.mqImData.data[iPix] = dR;
this.mqImData.data[iPix+1] = dG;
this.mqImData.data[iPix+2] = dB;
} else {
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);
let daColor = Intersect(daSubPos, daView);
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;
var giTimeOutId = 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();
giTimeOutId = setTimeout(RenderSlow, 1000);
}
function FocusFunction() {
document.getElementById("idCanvas").className ='cFocus';
}
function BlurFunction() {
document.getElementById("idCanvas").className ='cBlur';
}
function RenderSlow() {
qCP.DrawScene(false);
}
function KeyDownFunction(e) {
clearTimeout(giTimeOutId);
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();
giTimeOutId = setTimeout(RenderSlow, 1000);
}
© 20072026 XoaX.net LLC. All rights reserved.