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 = 25; public var sphereCenterX:Array = [0, 0, 0, 0]; public var sphereCenterY:Array = [0, -.2, .4, 100.5]; public var sphereCenterZ:Array = [1.5, 1.5, 1.5, 10]; public var sphereRadius:Array = [.35, .35, .25, 100]; public var sphereR:Array = [255, 0, 0, 120]; public var sphereG:Array = [0, 150, 0, 120]; public var sphereB:Array = [0, 0, 255, 200]; public var sphereReflects:Array = [false, false, false, true]; 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 = 150; var skyG:int = 150; var skyB:int = 250; 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 = 0x0; 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] = (4 * 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 = .5 * sphereR[closestSphereIndex] * illumination; g = .5 * sphereG[closestSphereIndex] * illumination; b = .5 * 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 = .5 * sphereR[closestSphereIndex] * illumination + .5 * sphereR[reflectClosestSphereIndex] * reflectIllumination; g = .5 * sphereG[closestSphereIndex] * illumination + .5 * sphereG[reflectClosestSphereIndex] * reflectIllumination; b = .5 * 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); } } }