// forked from erakan's code on 2008-12-27 // PolyTest.as // Control: Arrow keys or [WASD] keys. package { import flash.display.Shape; import flash.display.Sprite; import flash.display.BitmapData; import flash.display.Bitmap; import flash.display.Graphics; import flash.geom.Vector3D; import flash.geom.Matrix3D; import flash.geom.Point; import flash.geom.Transform; import flash.geom.Matrix; import flash.geom.PerspectiveProjection; import flash.text.*; import flash.events.Event; import flash.events.KeyboardEvent; [SWF(width="465", height="465", backgroundColor="0x000000", frameRate="30")] public class PolyTest extends Sprite { private const SCREEN_WIDTH:int = 465; private const SCREEN_HEIGHT:int = 465; private const FRAME_RATE:int = 30; private const BACK_COLOR:MyColor = new MyColor(210, 190, 240); private var _buffer:BitmapData; private var _screen:Bitmap; private const SCREEN_FOV:Number = 90.0; // private const SCREEN_FOV:Number = 60.0; private const NEAR_PLANE_DISTANCE:Number = 0.1; private const CULL_MIN_Z:Number = -1.0; private const CULL_MAX_Z:Number = 5.0; private const FOG_MIN_Z:Number = 1.0; private const FOG_MAX_Z:Number = 5.0; private var _input:Input = new Input(); private const PLAYER_SPEED:Number = 0.2; private const PLAYER_YAW_PER_SEC:Number = 180; private const PLAYER_START_Y:Number = 0.5; private const PLAYER_START_ANGLE:Number = 0; private var _player:Player = new Player(); private var _items:Vector.<Item> = new Vector.<Item>(); private var _itemObj:TextField = new TextField(); private var _localProj:Sprite; private var _localShift:Sprite; private var _localView:Sprite; private const BLOCK_WIDTH:Number = 1.0; private const BLOCK_HEIGHT:Number = 1.0; private const BLOCK_THICK:Number = 0.2; private const BLOCK_COLOR:MyColor = new MyColor(0, 0, 0xcc); private var _matProj:Matrix3D; private var _maze:Maze = new Maze( Vector.<String>([ "################", "# #", "# #", "# #", "# #", "# #", "# #", "# #", "# > S #", "################", ]) ); private const ROTATE_CYCLE:int = 60; private var _rotateFrame:int = 0; private var _polys:Vector.<MyPoly> = new Vector.<MyPoly>(); //////////////////////////////////////////////////////// public function PolyTest() { // coodinate player _player.pos = getPlayerPositionFromMaze(_maze); _player.pos.y = PLAYER_START_Y; // eye height adjust _player.angle = Math.PI * PLAYER_START_ANGLE / 180; // get items _items = getItemsFromMaze(_maze); // make maze polygons addMazePolys(_polys, _maze); // pre-compute projection and screen matrix _matProj = MyUtil.getMatrixPerspectiveFovLH( Math.PI * SCREEN_FOV / 180.0, SCREEN_WIDTH / SCREEN_HEIGHT); // frame buffer settings _buffer = new BitmapData(SCREEN_WIDTH, SCREEN_HEIGHT, false, 0); _screen = new Bitmap(_buffer); addChild(_screen); // item object var textFormat:TextFormat = new TextFormat(); textFormat.color = "0xff0000"; textFormat.size = 48; textFormat.bold = true; _itemObj.defaultTextFormat = textFormat; _itemObj.text = "?"; //------------------------------------------------------ // fl object coordinate // +Z // | // | // |0 width //------+-------+X // |` / // | ` / // | `/ // | @(AS3's camera pos?) _localView = new Sprite(); _localView.addChild(_itemObj); _localShift = new Sprite(); _localShift.addChild(_localView); _localProj = new Sprite(); _localProj.addChild(_localShift); // set projection var pp:PerspectiveProjection = new PerspectiveProjection(); pp.projectionCenter = new Point(SCREEN_WIDTH/2.0, SCREEN_HEIGHT/2.0); pp.fieldOfView = SCREEN_FOV; _localProj.transform.perspectiveProjection = pp; // maybe focalLength will updated when the setter invoked. // get camera distance pp = _localProj.transform.perspectiveProjection; var camDistance:Number = pp.focalLength; // matirix to flip down:+y to up:+y var matFlipY:Matrix3D = new Matrix3D(); matFlipY.appendScale(1, -1, 1); // matrix to scale to avoid near clip // (near clip distance will 0.05 * focalLength?) var matNorm:Matrix3D = new Matrix3D(); matNorm.appendScale(camDistance, camDistance, camDistance); // matrix to move origin to '@' var matShift:Matrix3D = new Matrix3D(); matShift.appendTranslation( SCREEN_WIDTH / 2.0, SCREEN_HEIGHT / 2.0, - camDistance); // set shift node var matTmp:Matrix3D = new Matrix3D(); matTmp.append(matFlipY); matTmp.append(matNorm); matTmp.append(matShift); _localShift.transform.matrix3D = matTmp; //------------------------------------------------------ // flash settings stage.addEventListener(KeyboardEvent.KEY_DOWN, _input.onKeyDown); stage.addEventListener(KeyboardEvent.KEY_UP, _input.onKeyUp); addEventListener(Event.ENTER_FRAME, onEnterFrame); } private function onEnterFrame(evt:Event):void { //////////////////////////////////////////////////////// // control var stride:Vector3D = _player.getDirection(); stride.x *= PLAYER_SPEED; stride.z *= PLAYER_SPEED; var move:Vector3D = new Vector3D(); if (_input.up) { move.x = stride.x; move.z = stride.z; } else if (_input.down) { move.x = -stride.x; move.z = -stride.z; } var theta:Number = Math.PI * PLAYER_YAW_PER_SEC / 180 / FRAME_RATE; if (_input.right) { _player.angle -= theta; } else if (_input.left) { _player.angle += theta; } _player.pos.x += move.x; fitToWall(_player.pos, _maze, true); _player.pos.z += move.z; fitToWall(_player.pos, _maze, false); _rotateFrame = (_rotateFrame + 1) % ROTATE_CYCLE; //////////////////////////////////////////////////////// // begin render _buffer.lock(); //------------------------------------------------------ // render background _buffer.fillRect(_buffer.rect, BACK_COLOR.rgb); var nodes:Vector.<MyNode>; //------------------------------------------------------ // transform polygons var dir:Vector3D = _player.getDirection(); var matView:Matrix3D = MyUtil.getMatrixLookAtLH( _player.pos, _player.pos.add(dir), new Vector3D(0, 1, 0)); // world view matrix var matWV:Matrix3D = matView.clone(); var cullPolys:Vector.<MyPoly> = MyUtil.getDistanceCullPolys( _polys, _player.pos, dir, CULL_MIN_Z, CULL_MAX_Z); var viewPolys:Vector.<MyPoly> = MyUtil.transformPolys( cullPolys, matWV); var clipPolys:Vector.<MyPoly> = MyUtil.getNearClipPolys( viewPolys, NEAR_PLANE_DISTANCE); nodes = MyUtil.transformPolysToScreen( clipPolys, _matProj, SCREEN_WIDTH, SCREEN_HEIGHT); //------------------------------------------------------ // transform items for each (var it:Item in _items) { var tmp:Vector3D = matView.transformVector(it); var newNode:MyNode = new MyNode(); newNode.type = NodeType.DECAL; newNode.z = tmp.z; newNode.obj = it; nodes.push(newNode); } //------------------------------------------------------ // sort nodes nodes.sort(MyUtil.onSortNode); //------------------------------------------------------ // render polygons // for polygon render var shape:Shape = new Shape(); var g:Graphics = shape.graphics; // g.lineStyle (1, 0x000000, 1.0); // for decal render _localView.transform.matrix3D = matView; // item model matrix var matModel:Matrix3D = new Matrix3D(); matModel.appendTranslation(-12, -24, 0); // centering matModel.appendScale(1.0/48, -1.0/48, 1.0/48); // 48dot -> 1m and Y flip matModel.appendRotation(360 * _rotateFrame / ROTATE_CYCLE, Vector3D.Y_AXIS); // spin for each (var node:MyNode in nodes) { switch (node.type) { case 0: // flat polygon { var prim:MyPrim = node.obj as MyPrim; var color:int; if (node.z <= FOG_MIN_Z) { color = BLOCK_COLOR.rgb; } else if (node.z >= FOG_MAX_Z) { color = BACK_COLOR.rgb; } else { var t:Number = (node.z - FOG_MIN_Z) / (FOG_MAX_Z - FOG_MIN_Z); color = (int)(BLOCK_COLOR.r + (BACK_COLOR.r - BLOCK_COLOR.r) * t) * 0x10000 + (int)(BLOCK_COLOR.g + (BACK_COLOR.g - BLOCK_COLOR.g) * t) * 0x100 + (int)(BLOCK_COLOR.b + (BACK_COLOR.b - BLOCK_COLOR.b) * t); } g.clear(); g.beginFill(color); var isFirst:Boolean = true; for each (var v:Point in prim.vertices) { if (isFirst) { isFirst = false; g.moveTo(v.x, v.y); } else { g.lineTo(v.x, v.y); } } g.endFill(); _buffer.draw(shape); } break; case 1: // item { var item:Item = node.obj as Item; var matWorld:Matrix3D = matModel.clone(); matWorld.appendTranslation(item.x, item.y, item.z); // position _itemObj.transform.matrix3D = matWorld; _buffer.draw(_localProj); } break; } } //------------------------------------------------------ // end render _buffer.unlock(); } //////////////////////////////////////////////////////// // for maze public function addMazePolys(polys:Vector.<MyPoly>, maze:Maze):void { for (var my:int = 0; my < maze.height; my++) { for (var mx:int = 0; mx < maze.width; mx++) { var symbol:String = maze.getSymbol(mx, my); if (symbol == "#") { addBlockPolys(polys, mx * BLOCK_WIDTH, -(my * BLOCK_WIDTH)); } } } } public function addBlockPolys(polys:Vector.<MyPoly>, x0:Number, z0:Number):void { var x1:Number = x0 + BLOCK_WIDTH; var z1:Number = z0 + -(BLOCK_WIDTH); var y0:Number = BLOCK_HEIGHT; var y1:Number = 0; var poly:MyPoly; // -Z poly = new MyPoly(); poly.vertices.push(new Vector3D(x1, y0, z0)); poly.vertices.push(new Vector3D(x0, y0, z0)); poly.vertices.push(new Vector3D(x0, y1, z0)); poly.vertices.push(new Vector3D(x1, y1, z0)); polys.push(poly); // +Z poly = new MyPoly(); poly.vertices.push(new Vector3D(x0, y0, z1)); poly.vertices.push(new Vector3D(x1, y0, z1)); poly.vertices.push(new Vector3D(x1, y1, z1)); poly.vertices.push(new Vector3D(x0, y1, z1)); polys.push(poly); // -X poly = new MyPoly(); poly.vertices.push(new Vector3D(x0, y0, z0)); poly.vertices.push(new Vector3D(x0, y0, z1)); poly.vertices.push(new Vector3D(x0, y1, z1)); poly.vertices.push(new Vector3D(x0, y1, z0)); polys.push(poly); // +X poly = new MyPoly(); poly.vertices.push(new Vector3D(x1, y0, z1)); poly.vertices.push(new Vector3D(x1, y0, z0)); poly.vertices.push(new Vector3D(x1, y1, z0)); poly.vertices.push(new Vector3D(x1, y1, z1)); polys.push(poly); } public function getPlayerPositionFromMaze(maze:Maze):Vector3D { for (var my:int = 0; my < maze.height; my++) { for (var mx:int = 0; mx < maze.width; mx++) { var symbol:String = maze.getSymbol(mx, my); if (symbol == "S") { return new Vector3D( mx * BLOCK_WIDTH + BLOCK_WIDTH/2, 0, -(my * BLOCK_WIDTH + BLOCK_WIDTH/2)); } } } return new Vector3D(); } public function getItemsFromMaze(maze:Maze):Vector.<Item> { var items:Vector.<Item> = new Vector.<Item>(); for (var my:int = 0; my < maze.height; my++) { for (var mx:int = 0; mx < maze.width; mx++) { var symbol:String = maze.getSymbol(mx, my); switch (symbol) { case "#": case " ": case "S": break; default: items.push(new Item( mx * BLOCK_WIDTH + BLOCK_WIDTH/2, 0.5, -(my * BLOCK_WIDTH + BLOCK_WIDTH/2) )); break; } } } return items; } public function fitToWall(pos:Vector3D, maze:Maze, isXMode:Boolean):void { var mx0:int = (int)((pos.x - BLOCK_THICK) / BLOCK_WIDTH); var mx1:int = (int)((pos.x + BLOCK_THICK) / BLOCK_WIDTH); var my1:int = -(int)((pos.z - BLOCK_THICK) / BLOCK_WIDTH); var my0:int = -(int)((pos.z + BLOCK_THICK) / BLOCK_WIDTH); for (var my:int = my0; my <= my1; my++) { for (var mx:int = mx0; mx <= mx1; mx++) { var symbol:String = maze.getSymbol(mx, my); if (symbol != "#") { continue; } var x0:Number = (mx) * BLOCK_WIDTH - BLOCK_THICK; var x1:Number = (mx + 1) * BLOCK_WIDTH + BLOCK_THICK; var z1:Number = -((my) * BLOCK_WIDTH - BLOCK_THICK); var z0:Number = -((my + 1) * BLOCK_WIDTH + BLOCK_THICK); if (pos.x > x0 && pos.x < x1 && pos.z > z0 && pos.z < z1) { if (isXMode) { pos.x = (pos.x < (x0+x1)/2) ? x0 : x1; } else { pos.z = (pos.z < (z0+z1)/2) ? z0 : z1; } } } } } } } function traceMatrix(m:Matrix3D):void { trace(m.rawData[ 0], m.rawData[ 1], m.rawData[ 2], m.rawData[ 3]); trace(m.rawData[ 4], m.rawData[ 5], m.rawData[ 6], m.rawData[ 7]); trace(m.rawData[ 8], m.rawData[ 9], m.rawData[10], m.rawData[11]); trace(m.rawData[12], m.rawData[13], m.rawData[14], m.rawData[15]); } ////////////////////////////////////////////////////// class Maze { public var _lines:Vector.<String> = new Vector.<String>(); public var _longestWidth:int = 0; public function Maze(lines:Vector.<String>) { _lines = lines; _longestWidth = 0; for each (var line:String in _lines) { if (line.length > _longestWidth) { _longestWidth = line.length; } } } public function get width():int { return _longestWidth; } public function get height():int { return _lines.length; } public function getSymbol(mx:int, my:int):String { if (my < 0 || my >= _lines.length) { return " "; } var line:String = _lines[my]; if (mx < 0 || mx >= line.length) { return " "; } return line.charAt(mx); } } import flash.geom.Vector3D; import flash.geom.Matrix3D; import flash.geom.Point; ////////////////////////////////////////////////////// class Player { public var pos:Vector3D = new Vector3D(); public var angle:Number = 0; public function getDirection():Vector3D { return new Vector3D( Math.cos(angle), 0, Math.sin(angle)); } } ////////////////////////////////////////////////////// class Item extends Vector3D { public function Item(x:Number = 0, y:Number = 0, z:Number = 0, w:Number = 0) { super(x, y, z, w); } } ////////////////////////////////////////////////////// class Input { import flash.events.Event; import flash.events.KeyboardEvent; public var left:Boolean = false; public var up:Boolean = false; public var right:Boolean = false; public var down:Boolean = false; public function onKeyUp(evt:KeyboardEvent):void { switch (evt.keyCode) { case 0x25: // [<] case 0x41: // [A] left = false; break; case 0x26: // [^] case 0x57: // [W] up = false; break; case 0x27: // [>] case 0x44: // [D] right = false; break; case 0x28: // [v] case 0x53: // [S] down = false; break; } } public function onKeyDown(evt:KeyboardEvent):void { switch (evt.keyCode) { case 0x25: // [<] case 0x41: // [A] left = true; break; case 0x26: // [^] case 0x57: // [W] up = true; break; case 0x27: // [>] case 0x44: // [D] right = true; break; case 0x28: // [v] case 0x53: // [S] down = true; break; } } } ////////////////////////////////////////////////////// class MyColor { public function MyColor(ir:int = 0, ig:int = 0, ib:int = 0) { r = ir; g = ig; b = ib; } public var r:Number; public var g:Number; public var b:Number; public function get rgb():int { return 0x10000 * r + 0x100 * g + b; } } ////////////////////////////////////////////////////// class MyPoly { public var vertices:Vector.<Vector3D> = new Vector.<Vector3D>(); } class MyPrim { public var vertices:Vector.<Point> = new Vector.<Point>(); public var color:int; } class MyNode { public var type:int; public var z:Number; public var obj:Object; } class NodeType { public static const POLYGON:int = 0; public static const DECAL:int = 1; } ////////////////////////////////////////////////////// class MyUtil { public static function getMatrixLookAtLH(eye:Vector3D, at:Vector3D, up:Vector3D):Matrix3D { var zaxis:Vector3D = at.subtract(eye); // var zaxis:Vector3D = eye.subtract(at); for RH zaxis.normalize(); var xaxis:Vector3D = up.crossProduct(zaxis); zaxis.normalize(); var yaxis:Vector3D = zaxis.crossProduct(xaxis); return new Matrix3D( Vector.<Number>([ xaxis.x, yaxis.x, zaxis.x, 0, xaxis.y, yaxis.y, zaxis.y, 0, xaxis.z, yaxis.z, zaxis.z, 0, - xaxis.dotProduct(eye), - yaxis.dotProduct(eye), - zaxis.dotProduct(eye), 1 ]) ); } public static function getMatrixPerspectiveFovLH(fovY:Number, aspect:Number):Matrix3D { var yScale:Number = 1.0 / Math.tan(fovY/2); var xScale:Number = yScale / aspect; return new Matrix3D( Vector.<Number>([ xScale, 0, 0, 0, 0, yScale, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, // 0, 0, zf / (zf-zn), -1, // 0, 0, zn*zf / (zf-zn), 0, ]) ); /* for RH return new Matrix3D( Vector.<Number>([ xScale, 0, 0, 0, 0, yScale, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, // 0, 0, -zf / (zf-zn), -1, // 0, 0, zn*zf / (zf-zn), 0, ]) ); */ } public static function transformPolys(polys:Vector.<MyPoly>, m:Matrix3D):Vector.<MyPoly> { var outPolys:Vector.<MyPoly> = new Vector.<MyPoly>(); for each (var poly:MyPoly in polys) { var outPoly:MyPoly = new MyPoly(); for each (var v:Vector3D in poly.vertices) { outPoly.vertices.push(m.transformVector(v)); } outPolys.push(outPoly); } return outPolys; } public static function getDistanceCullPolys( polys:Vector.<MyPoly>, pos:Vector3D, dir:Vector3D, minZ:Number, maxZ:Number):Vector.<MyPoly> { var outPolys:Vector.<MyPoly> = new Vector.<MyPoly>(); for each (var poly:MyPoly in polys) { var diff:Vector3D = poly.vertices[0].subtract(pos); var dot:Number = diff.dotProduct(dir); if (dot < minZ || dot > maxZ) { continue; } outPolys.push(poly); } return outPolys; } public static function getNearClipPolys(polys:Vector.<MyPoly>, dist:Number):Vector.<MyPoly> { var outPolys:Vector.<MyPoly> = new Vector.<MyPoly>(); for each (var poly:MyPoly in polys) { var outPoly:MyPoly = new MyPoly(); var isPrevClipped:Boolean = true; var prevPos:Vector3D = null; // near clip for (var i:int = 0; i < poly.vertices.length + 1; i++) { var v:Vector3D = poly.vertices[i % poly.vertices.length]; var isClipped:Boolean = (v.z < dist); if (isClipped != isPrevClipped && prevPos != null) { outPoly.vertices.push( MyUtil.getNearClipPosition(prevPos, v, dist)); } if (i >= poly.vertices.length) { continue; // clip only } if (!isClipped) { outPoly.vertices.push(v); } isPrevClipped = isClipped; prevPos = v; } if (outPoly.vertices.length >= 3) { outPolys.push(outPoly); } } return outPolys; } public static function getNearClipPosition(a:Vector3D, b:Vector3D, nearZ:Number):Vector3D { var diffX:Number = a.x - b.x; var diffY:Number = a.y - b.y; var diffZ:Number = a.z - b.z; var tmpZ :Number = nearZ - b.z; return new Vector3D( b.x + diffX * tmpZ / diffZ, b.y + diffY * tmpZ / diffZ, nearZ); } public static function transformPolysToScreen( polys:Vector.<MyPoly>, matProj:Matrix3D, screenWidth:Number, screenHeight:Number ):Vector.<MyNode> { var nodes:Vector.<MyNode> = new Vector.<MyNode>(); for each (var poly:MyPoly in polys) { var prim:MyPrim = new MyPrim(); var sumZ:Number = 0; for each (var v:Vector3D in poly.vertices) { var outPos:Point = new Point(); var tmp:Vector3D = matProj.transformVector(v); tmp.project(); outPos.x = (tmp.x * 0.5 + 0.5) * screenWidth; outPos.y = (tmp.y * -0.5 + 0.5) * screenHeight; sumZ += v.z; prim.vertices.push(outPos); } // normal clip var p0:Point = prim.vertices[0]; var p1:Point = prim.vertices[1]; var p2:Point = prim.vertices[2]; var v1:Point = new Point(p1.x - p0.x, p1.y - p0.y); var v2:Point = new Point(p2.x - p0.x, p2.y - p0.y); if (v1.x * v2.y - v1.y * v2.x <= 0) { continue; } var node:MyNode = new MyNode(); node.type = NodeType.POLYGON; node.z = sumZ / poly.vertices.length; node.obj = prim; nodes.push(node); } return nodes; } public static function onSortNode(p1:MyNode, p2:MyNode):Number { if (p1.z > p2.z) { return -1; } else if (p1.z < p2.z) { return 1; } else { return 0; } } } // MyUtil forked from: code on 2008-12-27