Path Tracing in Flash

I recently discovered a very simple global illumination rendering technique called Path Tracing. It only takes a couple of lines of code and produces great images. When optimized, you can achieve almost real time performance, see WebGL implementation or just search YouTube for “real time path tracing”.

You can check out my self-contained ActionScript 3 version put together using the links above. A couple of images it generated before the full source code.



package
{
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.Sprite;
	import flash.events.Event;
	
	[SWF(backgroundColor="#000000", frameRate="60", width="800", height="600")]
	public class PathTracing extends Sprite
	{
		public static const AA:int = 1; // antialiasing
		public static const WIDTH:int = 320 * AA;
		public static const HEIGHT:int = 240 * AA;
		
		
		public static const CAM_NEAR:Number = 2;		
		public static const CAM_FOV:Number = Math.PI * 0.3;		
		
		public static const MAX_DEPTH:int = 5;
		
		private var scene_:Array;

		private var bmd_:BitmapData;
		private var buffer_:Vector.<Vec3>;
		private var counts_:Vector.<int>;
		
		/**
		 * 
		 * 
		 */
		public function PathTracing()
		{
			var i:int;
			
			const camera:Vec3 = new Vec3(0, 0, 2);
			scene_ = Scenes.scene00;
			
			// add random spheres
			for (i = 0; i < 2; ++i)
			{
				scene_.push(new Sphere(0.1 + 0.5 * Math.random(), new Vec3(Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1), new Vec3(Math.random(), Math.random(), Math.random()), new Vec3()));
			}
			
			// transform scene to camera space
			for (i = 0; i < scene_.length; ++i)
			{
				var sphere:Sphere = scene_[i];
				sphere.center.x -= camera.x;
				sphere.center.y -= camera.y;
				sphere.center.z -= camera.z;
			}

			// display
			bmd_ = new BitmapData(WIDTH, HEIGHT, false, 0);
			// holds accumulated color			
			buffer_ = new Vector.<Vec3>(WIDTH * HEIGHT);
			// holds number of samples of that pixel
			counts_ = new Vector.<int>(WIDTH * HEIGHT);

			for (i = 0; i < WIDTH * HEIGHT; ++i)
			{
				buffer_[i] = new Vec3();
				counts_[i] = 0;
			}
		
			// upside down (we're using OpenGl style coordinate system)
			var b:Bitmap = new Bitmap(bmd_);
			b.smoothing = true;
			b.scaleY = -1;
			b.y = b.height;
			b.scaleX *= 1 / AA;
			b.scaleY *= 1 / AA;
			addChild(b);
			
			addEventListener(Event.ENTER_FRAME, onEnterFrame);
		}
		
		/**
		 * 
		 * @param event
		 * 
		 */
		private function onEnterFrame(event:Event):void
		{
			var x:int, y:int;
			var c:int = 2000;
			
			while (c--)
			{
				shootRay(Math.random() * WIDTH, Math.random() * HEIGHT);
			}
			
			bmd_.lock();
			
			for (y = 0; y < HEIGHT; ++y)
			{
				for (x = 0; x < WIDTH; ++x)
				{
					const i:int = y * WIDTH + x;
					
					if (counts_[i])
					{
						var color:Vec3 = buffer_[i];
						
						const r:int = Utils.clampi(color.x * 255 / counts_[i], 0, 0xff);
						const g:int = Utils.clampi(color.y * 255 / counts_[i], 0, 0xff);
						const b:int = Utils.clampi(color.z * 255 / counts_[i], 0, 0xff);
					
						bmd_.setPixel(x, y, (r << 16) | (g << 8) | b);
					}
				}
			}
			
			bmd_.unlock();
		}
		
		/**
		 * 
		 * @param x
		 * @param y
		 * 
		 */
		private function shootRay(x:Number, y:Number):void
		{
			var ray:Ray = new Ray();
			
			ray.o.x = 0; 
			ray.o.y = 0;
			ray.o.z = 0;
			
			const nx:Number = (2 * (x / WIDTH) - 1) * WIDTH / HEIGHT;
			const ny:Number = 2 * (y / HEIGHT) - 1;
			
			ray.d.x = CAM_NEAR * Math.tan(CAM_FOV * 0.5) * nx; 
			ray.d.y = CAM_NEAR * Math.tan(CAM_FOV * 0.5) * ny;
			ray.d.z = -CAM_NEAR; 
			
			ray.d = Utils.norm(ray.d);
			
			const i:int = int(y) * WIDTH + int(x);
			
			var c:Vec3 = radiance(ray, 0);
			
			buffer_[i].x += c.x;
			buffer_[i].y += c.y;
			buffer_[i].z += c.z;
			
			counts_[i]++;
		}
		
		/**
		 * 
		 * @param ray
		 * @param depth
		 * @return 
		 * 
		 */
		private function radiance(ray:Ray, depth:int):Vec3
		{
			var i:int;
			var sphere:Sphere;
			var t:Number;
			
			if (depth > MAX_DEPTH)
				return new Vec3();
			
			var nearest:Number = Infinity;
			var collider:Sphere = null;
			
			for (i = 0; i < scene_.length; ++i)
			{
				sphere = scene_[i];
				
				t = sphere.intersect(ray);
				
				if (t > 0 && t < nearest)
				{
					nearest = t;
					collider = sphere;
				}
			}
			
			if (collider == null)
				return new Vec3();
			
			
			// compute new ray
			var out:Ray = new Ray();
			
			out.o.x = ray.o.x + nearest * ray.d.x;
			out.o.y = ray.o.y + nearest * ray.d.y;
			out.o.z = ray.o.z + nearest * ray.d.z;
			
			var normal:Vec3 = new Vec3();
			normal.x = out.o.x - collider.center.x; 
			normal.y = out.o.y - collider.center.y; 
			normal.z = out.o.z - collider.center.z; 
			normal = Utils.norm(normal);

			
			var tangent:Vec3 = new Vec3();

			if (Math.abs(normal.x) < Utils.EPSILON)
			{
				tangent.x = 0;
				tangent.y = -normal.z;
				tangent.z = normal.y;
			}
			else
			{
				tangent.x = normal.y;
				tangent.y = -normal.x;
				tangent.z = 0;
			}
			
			tangent = Utils.norm(tangent);
			
			const bitangent:Vec3 = normal.cross(tangent);
			
			// random ray shooting from the normal hemisphere
			const angle:Number = Math.random() * Math.PI * 2;
			const radius2:Number = Math.random();
			const radius:Number = Math.sqrt(radius2);
			
			const cos:Number = Math.cos(angle) * radius;
			const sin:Number = Math.sin(angle) * radius;
			const sqrt:Number = Math.sqrt(1 - radius2);
			
			out.d.x = tangent.x * cos + bitangent.x * sin + normal.x * sqrt;
			out.d.y = tangent.y * cos + bitangent.y * sin + normal.y * sqrt;
			out.d.z = tangent.z * cos + bitangent.z * sin + normal.z * sqrt;
			
			out.d = Utils.norm(out.d);
			
			// compute new color
			var color:Vec3 = radiance(out, depth + 1);
			
			color.x *= collider.color.x;
			color.y *= collider.color.y;
			color.z *= collider.color.z;
			
			color.x += collider.emission.x;
			color.y += collider.emission.y;
			color.z += collider.emission.z;
			
			return color;
		}
	}
}

class Vec3
{
	public var x:Number;
	public var y:Number;
	public var z:Number;
	
	public function Vec3(x:Number = 0, y:Number = 0, z:Number = 0)
	{
		this.x = x;
		this.y = y;
		this.z = z;
	}
	
	public function cross(v:Vec3):Vec3
	{
		var o:Vec3 = new Vec3();
		
		o.x = y * v.z - z * v.y;
		o.y = z * v.x - x * v.z;
		o.z = x * v.y - y * v.x;
		
		return o;
	}
}

class Ray
{
	public var o:Vec3 = new Vec3();
	public var d:Vec3 = new Vec3();
}

class Sphere
{
	public var radius:Number;
	public var center:Vec3;
	public var color:Vec3;
	public var emission:Vec3;
	
	public function Sphere(radius:Number, center:Vec3, color:Vec3, emission:Vec3)
	{
		this.radius = radius;	
		this.center = center;	
		this.color = color;	
		this.emission = emission;	
	}
	
	/**
	 * 
	 * @param ray
	 * @return 
	 * 
	 */
	public function intersect(ray:Ray):Number
	{
		const px:Number = ray.o.x - center.x;
		const py:Number = ray.o.y - center.y;
		const pz:Number = ray.o.z - center.z;
		
		const pp:Number = px * px + py * py + pz * pz;
		const pd:Number = px * ray.d.x + py * ray.d.y + pz * ray.d.z;
		
		const A:Number = 1;
		const B:Number = 2 * pd;
		const C:Number = pp - radius * radius;
		
		const det:Number = B * B - 4 * A * C;
		
		if (det < 0)
			return -1;
		
		const det2:Number = Math.sqrt(det);
		
		const sol:Number = (-B - det2) / (2 * A);
		
		if (sol < 0)
			return -1;
		
		return sol;
	}
}

class Utils
{
	public static const EPSILON:Number = 0.0001;
	
	public static function clamp(v:Number, min:Number, max:Number):Number
	{
		return Math.max(Math.min(v, max), min);
	}

	public static function clampi(v:int, min:int, max:int):int
	{
		return Math.max(Math.min(v, max), min);
	}
	
	public static function norm(v:Vec3):Vec3
	{
		var o:Vec3 = new Vec3(v.x, v.y, v.z);
		
		const dd:Number = v.x * v.x + v.y * v.y + v.z * v.z;
		if (dd == 0) return o;
		
		const invd:Number = 1 / Math.sqrt(dd);
		o.x *= invd;		
		o.y *= invd;		
		o.z *= invd;		
		
		return o;
	}
}
	
class Scenes
{
	public static const R:Number = 1e5;
	public static const G:Number = 0.5;
	
	public static const scene00:Array = 
		[
			new Sphere(R, new Vec3(-R-1, 0, 0), new Vec3(G, G, G), new Vec3()),//left
			new Sphere(R, new Vec3(R+1, 0, 0), new Vec3(G, G, G), new Vec3()),//right
			new Sphere(R, new Vec3(0, 0, R+1), new Vec3(G, G, G), new Vec3()),//front
			new Sphere(R, new Vec3(0, 0, -R-1), new Vec3(G, G, G), new Vec3()),//back
			new Sphere(R, new Vec3(0, R+1, 0), new Vec3(G, G, G), new Vec3()),//top
			new Sphere(R, new Vec3(0, -R-1, 0), new Vec3(G, G, G), new Vec3()),//bottom
			new Sphere(0.5, new Vec3(0, 1+0.25, 0), new Vec3(), new Vec3(12,12,12))//light
		];
}

  • http://twitter.com/haoleSan Ted Kady

    Great Jan :)