什么是等角投影(isometric)?

原作者:菩提树下的杨过
出处:http://yjmyzz.cnblogs.com

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

[转载]等角(斜45度)游戏与数学  ( 原文链接:http://www.javaeye.com/articles/1225

[转载]使用illustrator和正交投影原理以及基本三视图制图   (http://www.vanqy.cn/index.php/2009/03/working-with-orthographic-projections-and-basic-isometrics/)

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

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

在之前学习的3D基础( http://www.cnblogs.com/yjmyzz/archive/2010/05/08/1730697.html )、3D线条与填充 ( http://www.cnblogs.com/yjmyzz/archive/2010/05/14/1735154.html)、背面剔除与 3D 灯光 ( http://www.cnblogs.com/yjmyzz/archive/2010/06/06/1752674.html)中,我们所采用的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实例

作者:菩提树下的杨过
出处:http://yjmyzz.cnblogs.com 
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
 

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

  1. “AS3.0高级动画编程”学习:第一章高级碰撞检测

    AdvancED ActionScript 3.0 Animation 是Keith Peters大师继"Make Things Move"之后的又一力作,网上已经有中文翻译版本了 ...

  2. “AS3.0高级动画编程”学习:第二章转向行为(下)

    在上一篇里,我们学习了“自主角色”的一些基本行为:寻找(seek).避开(flee).到达(arrive).追捕(pursue).躲避(evade).漫游(wander).这一篇将继续学习其它更复杂, ...

  3. “AS3.0高级动画编程”学习:第二章转向行为(上)

    因为这一章的内容基本上都是涉及向量的,先来一个2D向量类:Vector2D.as (再次强烈建议不熟悉向量运算的童鞋,先回去恶补一下高等数学-07章空间解释几何与向量代数.pdf) 原作者:菩提树下的 ...

  4. “AS3.0高级动画编程”学习:第四章 寻路(AStar/A星/A*)算法 (下)

    在前一部分的最后,我们给出了一个寻路的示例,在大多数情况下,运行还算良好,但是有一个小问题,如下图: 很明显,障碍物已经把路堵死了,但是小球仍然穿过对角线跑了出来! 问题在哪里:我们先回顾一下ASta ...

  5. [书籍翻译] 《JavaScript并发编程》第三章 使用Promises实现同步

    本文是我翻译<JavaScript Concurrency>书籍的第三章 使用Promises实现同步,该书主要以Promises.Generator.Web workers等技术来讲解J ...

  6. C#高级编程 (第六版) 学习 第三章:对象和类型

    第三章 对象和类型 1,类和结构 类存储在托管堆上 结构存储在堆栈上   2,类成员 类中的数据和函数称为类成员 数据成员 数据成员包括了字段.常量和事件   函数成员 方法:与某个类相关的函数,可以 ...

  7. Java基础知识二次学习--第三章 面向对象

    第三章 面向对象   时间:2017年4月24日17:51:37~2017年4月25日13:52:34 章节:03章_01节 03章_02节 视频长度:30:11 + 21:44 内容:面向对象设计思 ...

  8. Struts2框架学习第三章——Struts2基础

    本章要点 —  Struts 1框架的基本知识 — 使用Struts 1框架开发Web应用 —  WebWork框架的基本知识 — 使用WebWork框架开发Web应用 — 在Eclipse中整合To ...

  9. python网络编程学习笔记(三):socket网络服务器(转载)

    1.TCP连接的建立方法 客户端在建立一个TCP连接时一般需要两步,而服务器的这个过程需要四步,具体见下面的比较. 步骤 TCP客户端 TCP服务器 第一步 建立socket对象  建立socket对 ...

随机推荐

  1. Python全栈之路----函数----匿名函数

    用lambda声明匿名函数,对lambda定义名字,才能被调用.下面的calc和func功能一致. def calc(x,y): return x*y func = lambda x,y: x*y # ...

  2. 作用域&&闭包

    在了解闭包之前,先了解作用域一,作用域简单来说就是变量和函数可以访问的范围,在es5中变量作用域一般分为全局作用域和局部作用域,这个主要依据是全局变量还是局部变量 情景1: <script> ...

  3. CocosCreator引擎修改与定制

    1.CCGame.js 修改部分数据脚本的加载时机,避免首屏卡顿 // Load game scripts var jsList = config[CONFIG_KEY.jsList]; if (js ...

  4. Java面向对象 第4节 类的多态性

    一.多态的概念 在面向对象语言中,多态是指在一棵继承树中的类中可以有多个同名但不同方法体及不同形参的方法.通常有两种途径实现多态:方法的重载和覆盖. 多态性允许以统一的风格处理已存在的变量及相关的类. ...

  5. JDK 8 安装及配置

    1.配置java环境变量 注意:jdk文件夹名字取名不要用汉语取名. 1)鼠标右键点击我的电脑(计算机)选择属性栏 2)再点击左边高级系统设置 3)点击环境变量 4)在用户变量窗口新建变量名为JAVA ...

  6. [C++/Python] 如何在C++中使用一个Python类? (Use Python-defined class in C++)

    最近在做基于OpenCV的车牌识别, 其中需要用到深度学习的一些代码(Python), 所以一开始的时候开发语言选择了Python(祸患之源). 固然现在Python的速度不算太慢, 但你一定要用Py ...

  7. Docker修改本地镜像与容器的存储位置

    这个方法里将通过软连接来实现. 首先停掉Docker服务: systemctl restart docker或者service docker stop 然后移动整个/var/lib/docker目录到 ...

  8. python3 访问百度返回压缩格式

    import urllib, urllib.request, urllib.parse import random import zlib import re import os, time Save ...

  9. 管理工具:SWOT、PDCA、6W2H、SMART、WBS、时间管理

    01:SWOT分析法 Strengths:优势 Weaknesses:劣势 Opportunities:机会 Threats:威胁 意义:帮您清晰地把握全局,分析自己在资源方面的优势与劣势,把握环境提 ...

  10. DevExpress破解和消除弹出框问题

    在解决方案管理器列表中找到 license.licx 设置属性 将[生成操作]选项改为[无]