Forrest Briggs throwing down with a real-time raytracer in AS3. Also a C++ OpenGL version sample on the page.

Real-time pixel manipulation in flash is getting faster, but is still probably going to have to be faked in AS3, maybe AS4 will provide us per pixel speeds that Andre Michelle has been harping on since flash 8.5. Native operations can be much faster in that area. AIF might look to change some of that but that is Flash 10.

Here is the code for the as3 raytracer. Read more at laserpirate.

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 = sphereCenterX.length;

var skyR:int = 20;

var skyG:int = 20;

var skyB:int = 20;

var skyColor:int = (skyR< <16) + (skyG<<8) + skyB;
var ambientIllumination:Number = .1;
var canvas:BlankClip;
var theta:Number = 0;
var mouseIsDown:Boolean = false;
var mouseDownTheta:Number = 0;
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 BlankClip;
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:*)
{
// 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);
}
}
}[/sourcecode]