“AS3.0高级动画编程”学习:第三章等角投影(上)

什么是等角投影(isometric)?

刚接触这个概念时,我也很茫然,百度+google了N天后,找到了一些文章:

[转载]等角(斜45度)游戏与数学

[转载]使用illustrator和正交投影原理以及基本三视图制图

以及这篇ppt:http://files.cnblogs.com/yjmyzz/Isometric.rar

建议先耐心看完这三篇文章,再往下看:

在之前学习的3D基础3D线条与填充背面剔除与 3D 灯光 中,我们所采用的3D坐标系,基本上都属于3D透视投影坐标。通俗点讲:就是物体距离观察者越远,看上去就越小,最终消失在远处的某个点,也可以俗称为“带消失点的3D投影”。这种投影方法虽然精确,但是动画编程中严格按照它来处理,开销很大,计算量也很大,因为不同的z轴距离,或者距离观察点的位置不一样,物体的大小就要调整(如果考虑到光源等因素,处理量就更大了)。

而等角投影中,没有消失点,观察者的目光始终是平行的,投影方向与坐标轴的角度是固定值,虽然这样看上去略有失真,但是总体来讲立体感还是很明显的,重要的是:不管你把等角投影所形成的立体图形放在屏幕上哪一个位置,看上去都是相同的。

原书作者还给出了一个演示,用于帮助大家理解:在线演示

很明显:一个立方体的(正方形)顶部面,在经过等角投影后,在屏幕上会发生形变,成为一个菱形。(点击刚才的在线演示中的true isometric按钮,观察front视图中立方体的顶部)

上图是正方形经过标准等角投影后得到的菱形,其左右侧的角度为60度,通过计算可以得到长宽比例为1.73,但是这个比例通常在计算时,会弄出很多小数位,而且绘图师们也比较烦这个比例(因为用ps等软件画图时,同样也要设置长或宽为小数位才能保证这个比例)

所以在实际情况中,更常用的是"二等角"来代替"等角"(点击刚才的在线演示中的dimetric按钮,观察front视图中立方体的顶部)

可以看出,“二等角投影”形成的菱形要比“等角投影”更扁一些,但这种图形的宽/高比例正好是2,处理起来很方便,也好记忆。

有了上面这些基础,就可以来做些正经事儿了,思考一个问题:在常规3D空间中的图形,经过二等角投影(为方便起见,以下把二等角投影也通称为等角投影)后,要经过怎样的计算(或转换),才能得到最终的图形呢?

有鉴于任何几何图形,总是由若干个点连接而成的,我们先来定义一个常规的Point3D类:

package {
	public class Point3D {
		public var x:Number;
		public var y:Number;
		public var z:Number;
		public function Point3D(x:Number=0,y:Number=0,z:Number=0) {
			this.x=x;
			this.y=y;
			this.z=z;
		}
	}
}

所以上面的问题也可以简化为:等角空间中3D坐标点,如何转换为电脑屏幕上的2D坐标点?(或者反过来转换?)

转化公式: x1 = x - z y1 = y * 1.2247 + (x + z) * 0.5 z2 = (x + z) * 0.866 - y * 0.707 --用于层深排序,可以先不管

上面的公式可以把等角空间中的坐标点,转化为屏幕空间上的坐标点。(好奇心强烈的童鞋们,自己去看原书上的推导过程吧,我建议大家把这它当成定理公式记住就好,毕竟我们不是在研究数学)

为了方便以后重用,可以把这个公式封装到类IsoUtil.as里

package {

	import flash.geom.Point;
	public class IsoUtils {

		//public static const Y_CORRECT:Number=Math.cos(- Math.PI/6)*Math.SQRT2;		
		public static const Y_CORRECT:Number = 1.2247448713915892;

		//把等角空间中的一个3D坐标点转换成屏幕上的2D坐标点
		public static function isoToScreen(pos:Point3D):Point {
			var screenX:Number=pos.x-pos.z;
			var screenY:Number=pos.y*Y_CORRECT+(pos.x+pos.z)*0.5;
			return new Point(screenX,screenY);
		}

		//把屏幕上的2D坐标点转换成等角空间中的一个3D坐标点
		public static function screenToIso(point:Point):Point3D {
			var xpos:Number=point.y+point.x*.5;
			var ypos:Number=0;
			var zpos:Number=point.y-point.x*.5;
			return new Point3D(xpos,ypos,zpos);
		}
	}
}

用代码来画一个等角图形,测试上面的代码是否正确

package {
	
	import flash.display.Sprite;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
	import flash.geom.Point;
	
	[SWF(backgroundColor=0xefefef,height="200",width="300")]
	public class IsoTransformTest extends Sprite {
		public function IsoTransformTest() {
			stage.align=StageAlign.TOP_LEFT;
			stage.scaleMode=StageScaleMode.NO_SCALE;
			
			var p0:Point3D=new Point3D(0,0,0);
			var p1:Point3D=new Point3D(100,0,0);
			var p2:Point3D=new Point3D(100,0,100);
			var p3:Point3D=new Point3D(0,0,100);
			
			var sp0:Point=IsoUtils.isoToScreen(p0);
			var sp1:Point=IsoUtils.isoToScreen(p1);
			var sp2:Point=IsoUtils.isoToScreen(p2);
			var sp3:Point=IsoUtils.isoToScreen(p3);
			
			var tile:Sprite = new Sprite();
			tile.x=150;
			tile.y=50;
			addChild(tile);
			
			tile.graphics.lineStyle(0);
			tile.graphics.moveTo(sp0.x, sp0.y);
			tile.graphics.lineTo(sp1.x, sp1.y);
			tile.graphics.lineTo(sp2.x, sp2.y);
			tile.graphics.lineTo(sp3.x, sp3.y);
			tile.graphics.lineTo(sp0.x, sp0.y);
			
			trace(Math.cos(- Math.PI/6)*Math.SQRT2);//1.2247448713915892			
			trace(tile.width,tile.height);//200 100 符合上面提到的2:1
		}
	}
}

正如在OO世界里,很多语言都习惯于弄一个Object基类做为祖先一样,在等角世界里,我们也可以弄一个IsoObject的基类,把坐标变换这一套东西封装在里面,方便重用

package {
	import flash.display.Sprite;
	import flash.geom.Point;
	import flash.geom.Rectangle;
	
	public class IsoObject extends Sprite {
		
		protected var _position:Point3D;
		protected var _size:Number;
		protected var _walkable:Boolean=false;

		//public static const Y_CORRECT:Number=Math.cos(- Math.PI/6)*Math.SQRT2;
		public static const Y_CORRECT:Number=1.2247448713915892;
		
		public function IsoObject(size:Number) {
			_size=size;
			_position = new Point3D();
			updateScreenPosition();
		}
		
		//更新屏幕坐标位置
		protected function updateScreenPosition():void {
			var screenPos:Point=IsoUtils.isoToScreen(_position);
			super.x=screenPos.x;
			super.y=screenPos.y;
		}

		override public function toString():String {
			return "[IsoObject (x:" + _position.x + ", y:" + _position.y+ ", z:" + _position.z + ")]";
		}
		
		//设置等角空间3D坐标点的x,y,z值
		override public function set x(value:Number):void {
			_position.x=value;
			updateScreenPosition();
		}
		
		override public function get x():Number {
			return _position.x;
		}
		
		override public function set y(value:Number):void {
			_position.y=value;
			updateScreenPosition();
		}
		override public function get y():Number {
			return _position.y;
		}
		
		override public function set z(value:Number):void {
			_position.z=value;
			updateScreenPosition();
		}
		
		override public function get z():Number {
			return _position.z;
		}

		//_position的属性封装
		public function set position(value:Point3D):void {
			_position=value;
			updateScreenPosition();
		}
		public function get position():Point3D {
			return _position;
		}
		
		//深度排序时会用到,现在不用理这个
		public function get depth():Number {
			return (_position.x + _position.z) * .866 - _position.y * .707;
		}
		
		//这个暂时也不用理
		public function set walkable(value:Boolean):void {
			_walkable=value;
		}
		public function get walkable():Boolean {
			return _walkable;
		}
		
		public function get size():Number {
			return _size;
		}
		
		public function get rect():Rectangle {
			return new Rectangle(x - size / 2, z - size / 2, size, size);
		}
	}
}

接触过3D渲染或动画的朋友们也许都知道,通常人们习惯弄出一个最基本的三角形(或其它小形状)做为基本贴片,用这些小贴片最终构成复杂的3D模型,类似的,我们也可以做一个基本的IsoTile贴片类

package {
	public class DrawnIsoTile extends IsoObject {
		protected var _height:Number;
		protected var _color:uint;
		public function DrawnIsoTile(size:Number,color:uint,height:Number=0) {
			super(size);
			_color=color;
			_height=height;
			draw();
		}
		
		//画矩形"贴片"	
		protected function draw():void {
			graphics.clear();
			graphics.beginFill(_color);
			graphics.lineStyle(0,0,.5);
			graphics.moveTo(- size,0);
			graphics.lineTo(0,- size*.5);
			graphics.lineTo(size,0);
			graphics.lineTo(0,size*.5);
			graphics.lineTo(- size,0);
		}
		
		//height属性暂时不用管(在draw里也没用到)
		override public function set height(value:Number):void {
			_height=value;
			draw();
		}
		override public function get height():Number {
			return _height;
		}
		
		//设置颜色
		public function set color(value:uint):void {
			_color=value;
			draw();
		}
		public function get color():uint {
			return _color;
		}
	}
}

测试一下IsoTile:

可以把这个当做游戏中的空白地图,ok,继续,光画一个平面,也许没什么意思,再考虑更复杂一些的物体:比如(立方体)盒子(其实基本思路不复杂,把贴片提高一些位置,然后向下伸出同等长度的线条,最终连接起来即可)。

在等角世界中,站在观察者的角度,一个立方体最终能看到的只有三个面(top,left,right),如果再加个光源的话,还应该体现出颜色的明暗度差别(比如如果一个光源从盒子的右上方照过来,上方应该是最亮的,右侧其次,左侧最暗)

package {
	public class DrawnIsoBox extends DrawnIsoTile {
		public function DrawnIsoBox(size:Number, color:uint, height:Number) {
			super(size, color, height);
		}
		override protected function draw():void {
			graphics.clear();
			
			//提取r,g,b三色分量
			var red:int=_color>>16;
			var green:int=_color>>8&0xff;
			var blue:int=_color&0xff;
			
			//假如光源在右上方(所以左侧最暗,顶上最亮,右侧在二者之间)
			var leftShadow:uint = (red * .5) << 16 |(green * .5) << 8 |(blue * .5);
			var rightShadow:uint = (red * .75) << 16 |(green * .75) << 8 | (blue * .75);
			var h:Number=_height*Y_CORRECT;

			//顶部
			graphics.beginFill(_color);
			graphics.lineStyle(0, 0, .5);
			graphics.moveTo(-_size, -h);
			graphics.lineTo(0, -_size * .5 - h);
			graphics.lineTo(_size, -h);
			graphics.lineTo(0, _size * .5 - h);
			graphics.lineTo(-_size, -h);
			graphics.endFill();
			
			//左侧
			graphics.beginFill(leftShadow);
			graphics.lineStyle(0, 0, .5);
			graphics.moveTo(-_size, -h);
			graphics.lineTo(0, _size * .5 - h);
			graphics.lineTo(0, _size * .5);
			graphics.lineTo(-_size, 0);
			graphics.lineTo(-_size, -h);
			graphics.endFill();
			
			//右侧
			graphics.beginFill(rightShadow);
			graphics.lineStyle(0, 0, .5);
			graphics.moveTo(_size, -h);
			graphics.lineTo(0, _size * .5 - h);
			graphics.lineTo(0, _size * .5);
			graphics.lineTo(_size, 0);
			graphics.lineTo(_size, -h);
			graphics.endFill();
		}
	}
}

测试一下IsoBox

package {
	
	
	import flash.display.Sprite;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
	import flash.events.*;
	import flash.events.MouseEvent;
	import flash.geom.Point;
	[SWF(backgroundColor=0xffffff,height=380,width=600)]
	public class BoxTest extends Sprite
	{
		private var world:Sprite;
		
		public function BoxTest()
		{
			stage.align = StageAlign.TOP_LEFT;
			stage.scaleMode = StageScaleMode.NO_SCALE;
			world = new Sprite();
			world.x = stage.stageWidth / 2;
			world.y = 50;
			addChild(world);
			for(var i:int = 0; i < 15; i++)
			{
				for(var j:int = 0; j < 15; j++)
				{
					var tile:DrawnIsoTile = new DrawnIsoTile(20, 0xcccccc);
					tile.position = new Point3D(i * 20, 0, j * 20);
					world.addChild(tile);
				}
			}
			world.addEventListener(MouseEvent.CLICK, onWorldClick);
			stage.addEventListener(Event.RESIZE,resizeHandler);
		}
		
		private function onWorldClick(event:MouseEvent):void
		{
			var box:DrawnIsoBox = new DrawnIsoBox(20, Math.random() * 0xffffff, 20);
			var pos:Point3D = IsoUtils.screenToIso(new Point(world.mouseX, world.mouseY));
			pos.x = Math.round(pos.x / 20) * 20;
			pos.y = Math.round(pos.y / 20) * 20;
			pos.z = Math.round(pos.z / 20) * 20;
			box.position = pos;
			world.addChild(box);
		}
		
		private function resizeHandler(e:Event):void{
			world.x = stage.stageWidth / 2;
		}
	}
}

在线演示

稍加解释,上面这段代码先画一个空白地图,然后在地图上注册鼠标点击事件,每次点击将在地图上生成一个IsoBox实例

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏chenjx85的技术专栏

leetcode-453-Minimum Moves to Equal Array Elements

32860
来自专栏落影的专栏

程序员进阶之算法练习(十八)

前言 最近在接触新知识,也是选择2017年的方向。 其他文集更新会放缓,没有学习就没有心得,肚中无墨就无从下笔。 但是算法练习还是挺好玩的,欢迎关注algo...

35450
来自专栏PPV课数据科学社区

【学习】《R实战》读书笔记(第五章)

读书会是一种在于拓展视野、宏观思维、知识交流、提升生活的活动。PPV课R语言读书会以“学习、分享、进步”为宗旨,通过成员协作完成R语言专业书籍的精读和分享,达到...

50490
来自专栏chenjx85的技术专栏

leetcode-441-Arranging Coins

15340
来自专栏数据结构与算法

16:矩阵剪刀石头布

16:矩阵剪刀石头布 总时间限制: 5000ms 内存限制: 65536kB描述 Bart的妹妹Lisa在一个二维矩阵上创造了新的文明。矩阵上每个位置被三种...

41760
来自专栏Python数据科学

如何用Python过一个完美的七夕节?

一年一度的七夕节又到了,每年重复的过,花样各种有,很多男同胞又开始发愁了,该准备点什么呢?前一段时间非常火的电影 “西红市首富” 突然给了我点灵感,男主全城放烟...

78410
来自专栏菩提树下的杨过

Flash/Flex学习笔记(43):动量守恒与能量守恒

动能公式: ? 动量公式: ? 动量守恒: ? 能量守恒: ? 根据这些规律可以得到下列方程组: ? ? 解该方程组,得到下面的公式: ? ? 把这二个公式相...

19170
来自专栏小樱的经验随笔

2017广东工业大学程序设计竞赛决赛 题解&源码(A,数学解方程,B,贪心博弈,C,递归,D,水,E,贪心,面试题,F,贪心,枚举,LCA,G,dp,记忆化搜索,H,思维题)

心得: 这比赛真的是不要不要的,pending了一下午,也不知道对错,直接做过去就是了,也没有管太多! Problem A: 两只老虎 Description ...

33160
来自专栏数据结构与算法

P2658 汽车拉力比赛

题目描述 博艾市将要举行一场汽车拉力比赛。 赛场凹凸不平,所以被描述为M*N的网格来表示海拔高度(1≤ M,N ≤500),每个单元格的海拔范围在0到10^9之...

28380
来自专栏数据结构与算法

2000 楼房重建 2012年

 时间限制: 1 s  空间限制: 256000 KB  题目等级 : 大师 Master 题解  查看运行结果 题目描述 Description   小A的楼...

36570

扫码关注云+社区

领取腾讯云代金券