raytrace on 2009-2-2
package
{
import flash.display.Sprite;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.events.Event;
import flash.utils.getTimer;
import flash.events.MouseEvent;
import flash.text.TextField;
import flash.text.TextFormat;
public class RayTracer extends Sprite
{
private var t:Number;
private var dt:Number = .01;
private var frameTimeTxt:TextField;
public static const BUFFER_WIDTH:int = 160;
public static const BUFFER_HEIGHT:int = 120;
public static const BUFFER_SCALEDDOWN:int = 320 / BUFFER_WIDTH;
public static const HALF_BUFFER_WIDTH:int = BUFFER_WIDTH / 2;
public static const HALF_BUFFER_HEIGHT:int = BUFFER_HEIGHT / 2;
private var outputBitmapData:BitmapData;
private var outputBitmap:Bitmap;
public var FOV:Number = 20;
public var sphereCenterX:Array = [0, 0, 0, 0];
public var sphereCenterY:Array = [0, -.2, .4, 100.5];
public var sphereCenterZ:Array = [4, 4, 4, 10];
public var sphereRadius:Array = [.35, .35, .25, 100];
public var sphereR:Array = [255, 0, 0, 20];
public var sphereG:Array = [0, 150, 0, 20];
public var sphereB:Array = [0, 0, 255, 20];
public var sphereReflects:Array = [false, false, false, true];
public var sphereReflectiveness:Array = [0,0,0,.3];
public var sphere2dX:Array = new Array(sphereCenterX.length);
public var sphere2dY:Array = new Array(sphereCenterX.length);
public var sphere2dR:Array = new Array(sphereCenterX.length);
public var numSpheres:Number = sphereCenterX.length;
public var skyR:Number = 20;
public var skyG:Number= 20;
public var skyB:Number = 20;
public var skyColor:Number = (skyR<16) + (skyG<8) + skyB;
public var ambientIllumination:Number = .1;
public var canvas:Sprite;
public var theta:Number = 0;
public var mouseIsDown:Boolean = false;
public var mouseDownTheta:Number = 0;
public var mouseDownX:Number = 0;
public function RayTracer()
{
outputBitmapData = new BitmapData(BUFFER_WIDTH, BUFFER_HEIGHT, false);
outputBitmap = new Bitmap(outputBitmapData);
addChild(outputBitmap);
//outputBitmap.smoothing = true;
outputBitmap.width= 320;
outputBitmap.height = 240;
canvas = new Sprite;
addChild(canvas);
canvas.buttonMode = true;
canvas.useHandCursor = true;
frameTimeTxt = new TextField();
frameTimeTxt.defaultTextFormat = new TextFormat("Arial");
frameTimeTxt.x = 8;
frameTimeTxt.y = 8;
frameTimeTxt.width = 640;
frameTimeTxt.textColor = 0xFFFFFF;
frameTimeTxt.selectable = false;
addChild(frameTimeTxt);
t = 0;
addEventListener(Event.ENTER_FRAME, update, false, 0, true);
canvas.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
canvas.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
}
public function mouseDownHandler(e:*):void
{
mouseIsDown = true;
mouseDownX = stage.mouseX;
mouseDownTheta = theta;
}
public function mouseUpHandler(e:*):void
{
mouseIsDown = false;
}
public function update(e:*):void
{
// start frame timer and update global time
var timer:Number = getTimer();
t += dt;
// handle mouse rotation
if( mouseIsDown ) theta = mouseDownTheta - .0015 * (stage.mouseX - mouseDownX);
theta += dt;
// do some funky animation
sphereCenterX[0] = .5*Math.sin(theta*5);
sphereCenterZ[0] =1 + .5*Math.cos(theta*5);
sphereCenterX[1] = .5*Math.sin(theta*5 + 2 * Math.PI / 3);
sphereCenterZ[1] = 1 + .5*Math.cos(theta*5 + 2 * Math.PI / 3);
sphereCenterX[2] = .5*Math.sin(theta*5 + 4 * Math.PI / 3);
sphereCenterZ[2] = 1 + .5*Math.cos(theta*5 + 4 * Math.PI / 3);
// reused variables
var x:int;
var y:int;
var i:int;
var j:int;
var r:int;
var g:int;
var b:int;
var dx:Number;
var dy:Number;
var rayDirX:Number;
var rayDirY:Number;
var rayDirZ:Number;
var rayDirMag:Number;
var reflectRayDirX:Number;
var reflectRayDirY:Number;
var reflectRayDirZ:Number;
var intersectionX:Number;
var intersectionY:Number;
var intersectionZ:Number;
var reflectIntersectionX:Number;
var reflectIntersectionY:Number;
var reflectIntersectionZ:Number;
var rayToSphereCenterX:Number;
var rayToSphereCenterY:Number;
var rayToSphereCenterZ:Number;
var lengthRTSC2:Number;
var closestApproach:Number;
var halfCord2:Number;
var dist:Number;
var normalX:Number;
var normalY:Number;
var normalZ:Number;
var normalMag:Number;
var illumination:Number;
var reflectIllumination:Number;
var reflectR:Number;
var reflectG:Number;
var reflectB:Number;
// setup light dir
var lightDirX:Number = .3;
var lightDirY:Number = -1;
var lightDirZ:Number = -.5;
var lightDirMag:Number = 1/Math.sqrt(lightDirX*lightDirX +lightDirY*lightDirY +lightDirZ*lightDirZ);
lightDirX *= lightDirMag;
lightDirY *= lightDirMag;
lightDirZ *= lightDirMag;
// vars used to in intersection tests
var closestIntersectionDist:Number;
var closestSphereIndex:int;
var reflectClosestSphereIndex:int;
// compute screen space bounding circles
//canvas.graphics.clear();
//canvas.graphics.lineStyle(1, 0xFF0000, .25);
for(i = 0; i < numSpheres; ++i)
{
sphere2dX[i] = (BUFFER_WIDTH / 2 + FOV * sphereCenterX[i] / sphereCenterZ[i]);
sphere2dY[i] = (BUFFER_HEIGHT /2 + FOV * sphereCenterY[i] / sphereCenterZ[i]);
sphere2dR[i] = (3 * FOV * sphereRadius[i] / sphereCenterZ[i]);
//canvas.graphics.drawCircle(sphere2dX[i]*BUFFER_SCALEDDOWN, sphere2dY[i]*BUFFER_SCALEDDOWN, sphere2dR[i]*BUFFER_SCALEDDOWN);
sphere2dR[i] *= sphere2dR[i]; // store the squared value
}
// write to each pixel
outputBitmapData.lock();
for(y = 0; y < BUFFER_HEIGHT; ++y)
{
for(x = 0; x < BUFFER_WIDTH; ++x)
{
// compute ray direction
rayDirX = x - HALF_BUFFER_WIDTH;
rayDirY = y - HALF_BUFFER_HEIGHT;
rayDirZ = FOV;
rayDirMag = 1/Math.sqrt(rayDirX * rayDirX + rayDirY * rayDirY +rayDirZ * rayDirZ);
rayDirX *= rayDirMag;
rayDirY *= rayDirMag;
rayDirZ *= rayDirMag;
/// trace the primary ray ///
closestIntersectionDist = Number.POSITIVE_INFINITY;
closestSphereIndex = -1
for(i = 0; i < numSpheres; ++i)
{
// check against screen space bounding circle
dx = x - sphere2dX[i];
dy = y - sphere2dY[i];
if( dx * dx + dy * dy > sphere2dR[i] ) continue;
// begin actual ray tracing if its inside the bounding circle
lengthRTSC2 = sphereCenterX[i] * sphereCenterX[i] +
sphereCenterY[i] * sphereCenterY[i] +
sphereCenterZ[i] * sphereCenterZ[i];
closestApproach = sphereCenterX[i] * rayDirX +
sphereCenterY[i] * rayDirY +
sphereCenterZ[i] * rayDirZ;
if( closestApproach < 0 ) // intersection behind the origin
continue;
halfCord2 = sphereRadius[i] * sphereRadius[i] - lengthRTSC2 + (closestApproach * closestApproach);
if( halfCord2 < 0 ) // ray misses the sphere
continue;
// ray hits the sphere
dist = closestApproach - Math.sqrt(halfCord2);
if( dist < closestIntersectionDist )
{
closestIntersectionDist = dist;
closestSphereIndex=i;
}
}
/// end of trace primary ray ///
// primary ray doesn't hit anything
if( closestSphereIndex == - 1)
{
outputBitmapData.setPixel(x, y, skyColor);
}
else // primary ray hits a sphere.. calculate shading, shadow and reflection
{
// location of ray-sphere intersection
intersectionX = rayDirX * closestIntersectionDist;
intersectionY = rayDirY * closestIntersectionDist;
intersectionZ = rayDirZ * closestIntersectionDist;
// sphere normal at intersection point
normalX = intersectionX - sphereCenterX[closestSphereIndex];
normalY = intersectionY - sphereCenterY[closestSphereIndex];
normalZ = intersectionZ - sphereCenterZ[closestSphereIndex];
normalX /= sphereRadius[closestSphereIndex]; // could be multiply by precacluated 1/rad
normalY /= sphereRadius[closestSphereIndex];
normalZ /= sphereRadius[closestSphereIndex];
// diffuse illumination coef
illumination = normalX * lightDirX +
normalY * lightDirY +
normalZ * lightDirZ;
if( illumination < ambientIllumination )
illumination = ambientIllumination;
/// trace a shadow ray ///
var isInShadow:Boolean = false;
for(j = 0; j < numSpheres; ++j)
{
if( j == closestSphereIndex ) continue;
rayToSphereCenterX = sphereCenterX[j] - intersectionX;
rayToSphereCenterY = sphereCenterY[j] - intersectionY;
rayToSphereCenterZ = sphereCenterZ[j] - intersectionZ;
lengthRTSC2 = rayToSphereCenterX * rayToSphereCenterX +
rayToSphereCenterY * rayToSphereCenterY +
rayToSphereCenterZ * rayToSphereCenterZ;
closestApproach = rayToSphereCenterX * lightDirX +
rayToSphereCenterY * lightDirY +
rayToSphereCenterZ * lightDirZ;
if( closestApproach < 0 ) // intersection behind the origin
continue;
halfCord2 = sphereRadius[j] * sphereRadius[j] - lengthRTSC2 + (closestApproach * closestApproach);
if( halfCord2 < 0 ) // ray misses the sphere
continue;
isInShadow = true;
break;
}
/// end of shadow ray ///
if( isInShadow ) illumination *= .5;
/// trace reflected ray ///
if( sphereReflects[closestSphereIndex] )
{
// calculate reflected ray direction
var reflectCoef:Number = 2 * (rayDirX * normalX + rayDirY * normalY + rayDirZ * normalZ);
reflectRayDirX = rayDirX - normalX * reflectCoef;
reflectRayDirY = rayDirY - normalY * reflectCoef;
reflectRayDirZ = rayDirZ - normalZ * reflectCoef;
closestIntersectionDist = Number.POSITIVE_INFINITY;
reflectClosestSphereIndex = -1
for(j = 0; j < numSpheres; ++j)
{
if( j == closestSphereIndex ) continue;
rayToSphereCenterX = sphereCenterX[j] - intersectionX;
rayToSphereCenterY = sphereCenterY[j] - intersectionY;
rayToSphereCenterZ = sphereCenterZ[j] - intersectionZ;
lengthRTSC2 = rayToSphereCenterX * rayToSphereCenterX +
rayToSphereCenterY * rayToSphereCenterY +
rayToSphereCenterZ * rayToSphereCenterZ;
closestApproach = rayToSphereCenterX * reflectRayDirX +
rayToSphereCenterY * reflectRayDirY +
rayToSphereCenterZ * reflectRayDirZ;
if( closestApproach < 0 ) // intersection behind the origin
continue;
halfCord2 = sphereRadius[j] * sphereRadius[j] - lengthRTSC2 + (closestApproach * closestApproach);
if( halfCord2 < 0 ) // ray misses the sphere
continue;
// ray hits the sphere
dist = closestApproach - Math.sqrt(halfCord2);
if( dist < closestIntersectionDist )
{
closestIntersectionDist = dist;
reflectClosestSphereIndex=j;
}
} // end loop through spheres for reflect ray
if( reflectClosestSphereIndex == - 1) // reflected ray misses
{
r = sphereR[closestSphereIndex] * illumination;
g = sphereG[closestSphereIndex] * illumination;
b = sphereB[closestSphereIndex] * illumination;
}
else
{
//trace("ref hit");
// location of ray-sphere intersection
reflectIntersectionX = reflectRayDirX * closestIntersectionDist + intersectionX;
reflectIntersectionY = reflectRayDirY * closestIntersectionDist + intersectionY;
reflectIntersectionZ = reflectRayDirZ * closestIntersectionDist + intersectionZ;
// sphere normal at intersection point
normalX = reflectIntersectionX - sphereCenterX[reflectClosestSphereIndex];
normalY = reflectIntersectionY - sphereCenterY[reflectClosestSphereIndex];
normalZ = reflectIntersectionZ - sphereCenterZ[reflectClosestSphereIndex];
normalX /= sphereRadius[reflectClosestSphereIndex]; // could be multiply by precacluated 1/rad
normalY /= sphereRadius[reflectClosestSphereIndex];
normalZ /= sphereRadius[reflectClosestSphereIndex];
// diffuse illumination coef
reflectIllumination = normalX * lightDirX +
normalY * lightDirY +
normalZ * lightDirZ;
if( reflectIllumination < ambientIllumination )
reflectIllumination = ambientIllumination;
r = sphereR[closestSphereIndex] * illumination + .5 * sphereR[reflectClosestSphereIndex] * reflectIllumination;
g = sphereG[closestSphereIndex] * illumination + .5 * sphereG[reflectClosestSphereIndex] * reflectIllumination;
b = sphereB[closestSphereIndex] * illumination + .5 * sphereB[reflectClosestSphereIndex] * reflectIllumination;
if( r > 255 ) r = 255;
if( g > 255 ) g = 255;
if( b > 255 ) b = 255;
} // end if reflected ray hits
} /// end if reflects
else // primary ray doesn't reflect
{
r = sphereR[closestSphereIndex] * illumination;
g = sphereG[closestSphereIndex] * illumination;
b = sphereB[closestSphereIndex] * illumination;
}
outputBitmapData.setPixel(x, y, (r<16) + (g<8) + b);
} // end if primary ray hit
} // end x loop
} // end y loop
outputBitmapData.unlock();
// compute FPS
var fps:Number = 1.0/((getTimer() - timer) / 1000.0);
frameTimeTxt.text = "Drag to rotate. FPS: " + int(fps);
}
}
}