code on 2008-12-27
PolyTest.as
Control: Arrow keys or [WASD] keys.
// 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>([
"################",
"# # # #",
"# ###### # # #",
"# # # # ## ##",
"# # ## # # # #",
"# # #G # # # #",
"# # #### # # #",
"# # # # #",
"# ######## # #",
"# 3 ## ##",
"#######v### > ##",
"# # #2## #",
"# # #### #",
"#1## #### # ## #",
"# #### # ###",
"## # #### ## #",
"# # #### # #",
"# ## # ## #",
"# ##### ## #",
"## # # # #",
"# # ### # # #",
"# ## # # ### #",
"# # #",
"# ## ##### # # #",
"# ## # #^# #",
"# # # # ###",
"# ## # # #",
"# # # # ## # # #",
"# # #",
"# # ######## # #",
"# > 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