zoukankan      html  css  js  c++  java
  • “AS3.0高级动画编程”学习:第三章等角投影(上)

    什么是等角投影(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:https://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类:

     1 package {
     2     public class Point3D {
     3         public var x:Number;
     4         public var y:Number;
     5         public var z:Number;
     6         public function Point3D(x:Number=0,y:Number=0,z:Number=0) {
     7             this.x=x;
     8             this.y=y;
     9             this.z=z;
    10         }
    11     }
    12 }

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

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

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

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

     1 package {
     2  
     3     import flash.geom.Point;
     4     public class IsoUtils {
     5  
     6         //public static const Y_CORRECT:Number=Math.cos(- Math.PI/6)*Math.SQRT2;        
     7         public static const Y_CORRECT:Number = 1.2247448713915892;
     8  
     9         //把等角空间中的一个3D坐标点转换成屏幕上的2D坐标点
    10         public static function isoToScreen(pos:Point3D):Point {
    11             var screenX:Number=pos.x-pos.z;
    12             var screenY:Number=pos.y*Y_CORRECT+(pos.x+pos.z)*0.5;
    13             return new Point(screenX,screenY);
    14         }
    15  
    16         //把屏幕上的2D坐标点转换成等角空间中的一个3D坐标点
    17         public static function screenToIso(point:Point):Point3D {
    18             var xpos:Number=point.y+point.x*.5;
    19             var ypos:Number=0;
    20             var zpos:Number=point.y-point.x*.5;
    21             return new Point3D(xpos,ypos,zpos);
    22         }
    23     }
    24 }
    View Code

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

     1 package {
     2      
     3     import flash.display.Sprite;
     4     import flash.display.StageAlign;
     5     import flash.display.StageScaleMode;
     6     import flash.geom.Point;
     7      
     8     [SWF(backgroundColor=0xefefef,height="200",width="300")]
     9     public class IsoTransformTest extends Sprite {
    10         public function IsoTransformTest() {
    11             stage.align=StageAlign.TOP_LEFT;
    12             stage.scaleMode=StageScaleMode.NO_SCALE;
    13              
    14             var p0:Point3D=new Point3D(0,0,0);
    15             var p1:Point3D=new Point3D(100,0,0);
    16             var p2:Point3D=new Point3D(100,0,100);
    17             var p3:Point3D=new Point3D(0,0,100);
    18              
    19             var sp0:Point=IsoUtils.isoToScreen(p0);
    20             var sp1:Point=IsoUtils.isoToScreen(p1);
    21             var sp2:Point=IsoUtils.isoToScreen(p2);
    22             var sp3:Point=IsoUtils.isoToScreen(p3);
    23              
    24             var tile:Sprite = new Sprite();
    25             tile.x=150;
    26             tile.y=50;
    27             addChild(tile);
    28              
    29             tile.graphics.lineStyle(0);
    30             tile.graphics.moveTo(sp0.x, sp0.y);
    31             tile.graphics.lineTo(sp1.x, sp1.y);
    32             tile.graphics.lineTo(sp2.x, sp2.y);
    33             tile.graphics.lineTo(sp3.x, sp3.y);
    34             tile.graphics.lineTo(sp0.x, sp0.y);
    35              
    36             trace(Math.cos(- Math.PI/6)*Math.SQRT2);//1.2247448713915892            
    37             trace(tile.width,tile.height);//200 100 符合上面提到的2:1
    38         }
    39     }
    40 }
    View Code

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

     1 package {
     2     import flash.display.Sprite;
     3     import flash.geom.Point;
     4     import flash.geom.Rectangle;
     5      
     6     public class IsoObject extends Sprite {
     7          
     8         protected var _position:Point3D;
     9         protected var _size:Number;
    10         protected var _walkable:Boolean=false;
    11  
    12         //public static const Y_CORRECT:Number=Math.cos(- Math.PI/6)*Math.SQRT2;
    13         public static const Y_CORRECT:Number=1.2247448713915892;
    14          
    15         public function IsoObject(size:Number) {
    16             _size=size;
    17             _position = new Point3D();
    18             updateScreenPosition();
    19         }
    20          
    21         //更新屏幕坐标位置
    22         protected function updateScreenPosition():void {
    23             var screenPos:Point=IsoUtils.isoToScreen(_position);
    24             super.x=screenPos.x;
    25             super.y=screenPos.y;
    26         }
    27  
    28         override public function toString():String {
    29             return "[IsoObject (x:" + _position.x + ", y:" + _position.y+ ", z:" + _position.z + ")]";
    30         }
    31          
    32         //设置等角空间3D坐标点的x,y,z值
    33         override public function set x(value:Number):void {
    34             _position.x=value;
    35             updateScreenPosition();
    36         }
    37          
    38         override public function get x():Number {
    39             return _position.x;
    40         }
    41          
    42         override public function set y(value:Number):void {
    43             _position.y=value;
    44             updateScreenPosition();
    45         }
    46         override public function get y():Number {
    47             return _position.y;
    48         }
    49          
    50         override public function set z(value:Number):void {
    51             _position.z=value;
    52             updateScreenPosition();
    53         }
    54          
    55         override public function get z():Number {
    56             return _position.z;
    57         }
    58  
    59         //_position的属性封装
    60         public function set position(value:Point3D):void {
    61             _position=value;
    62             updateScreenPosition();
    63         }
    64         public function get position():Point3D {
    65             return _position;
    66         }
    67          
    68         //深度排序时会用到,现在不用理这个
    69         public function get depth():Number {
    70             return (_position.x + _position.z) * .866 - _position.y * .707;
    71         }
    72          
    73         //这个暂时也不用理
    74         public function set walkable(value:Boolean):void {
    75             _walkable=value;
    76         }
    77         public function get walkable():Boolean {
    78             return _walkable;
    79         }
    80          
    81         public function get size():Number {
    82             return _size;
    83         }
    84          
    85         public function get rect():Rectangle {
    86             return new Rectangle(x - size / 2, z - size / 2, size, size);
    87         }
    88     }
    89 }
    View Code

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

     1 package {
     2     public class DrawnIsoTile extends IsoObject {
     3         protected var _height:Number;
     4         protected var _color:uint;
     5         public function DrawnIsoTile(size:Number,color:uint,height:Number=0) {
     6             super(size);
     7             _color=color;
     8             _height=height;
     9             draw();
    10         }
    11          
    12         //画矩形"贴片"   
    13         protected function draw():void {
    14             graphics.clear();
    15             graphics.beginFill(_color);
    16             graphics.lineStyle(0,0,.5);
    17             graphics.moveTo(- size,0);
    18             graphics.lineTo(0,- size*.5);
    19             graphics.lineTo(size,0);
    20             graphics.lineTo(0,size*.5);
    21             graphics.lineTo(- size,0);
    22         }
    23          
    24         //height属性暂时不用管(在draw里也没用到)
    25         override public function set height(value:Number):void {
    26             _height=value;
    27             draw();
    28         }
    29         override public function get height():Number {
    30             return _height;
    31         }
    32          
    33         //设置颜色
    34         public function set color(value:uint):void {
    35             _color=value;
    36             draw();
    37         }
    38         public function get color():uint {
    39             return _color;
    40         }
    41     }
    42 }
    View Code

    试一下IsoTile:

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

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

     1 package {
     2     public class DrawnIsoBox extends DrawnIsoTile {
     3         public function DrawnIsoBox(size:Number, color:uint, height:Number) {
     4             super(size, color, height);
     5         }
     6         override protected function draw():void {
     7             graphics.clear();
     8              
     9             //提取r,g,b三色分量
    10             var red:int=_color>>16;
    11             var green:int=_color>>8&0xff;
    12             var blue:int=_color&0xff;
    13              
    14             //假如光源在右上方(所以左侧最暗,顶上最亮,右侧在二者之间)
    15             var leftShadow:uint = (red * .5) << 16 |(green * .5) << 8 |(blue * .5);
    16             var rightShadow:uint = (red * .75) << 16 |(green * .75) << 8 | (blue * .75);
    17             var h:Number=_height*Y_CORRECT;
    18  
    19             //顶部
    20             graphics.beginFill(_color);
    21             graphics.lineStyle(0, 0, .5);
    22             graphics.moveTo(-_size, -h);
    23             graphics.lineTo(0, -_size * .5 - h);
    24             graphics.lineTo(_size, -h);
    25             graphics.lineTo(0, _size * .5 - h);
    26             graphics.lineTo(-_size, -h);
    27             graphics.endFill();
    28              
    29             //左侧
    30             graphics.beginFill(leftShadow);
    31             graphics.lineStyle(0, 0, .5);
    32             graphics.moveTo(-_size, -h);
    33             graphics.lineTo(0, _size * .5 - h);
    34             graphics.lineTo(0, _size * .5);
    35             graphics.lineTo(-_size, 0);
    36             graphics.lineTo(-_size, -h);
    37             graphics.endFill();
    38              
    39             //右侧
    40             graphics.beginFill(rightShadow);
    41             graphics.lineStyle(0, 0, .5);
    42             graphics.moveTo(_size, -h);
    43             graphics.lineTo(0, _size * .5 - h);
    44             graphics.lineTo(0, _size * .5);
    45             graphics.lineTo(_size, 0);
    46             graphics.lineTo(_size, -h);
    47             graphics.endFill();
    48         }
    49     }
    50 }
    View Code

    测试一下IsoBox

     1 package {
     2      
     3      
     4     import flash.display.Sprite;
     5     import flash.display.StageAlign;
     6     import flash.display.StageScaleMode;
     7     import flash.events.*;
     8     import flash.events.MouseEvent;
     9     import flash.geom.Point;
    10     [SWF(backgroundColor=0xffffff,height=380,width=600)]
    11     public class BoxTest extends Sprite
    12     {
    13         private var world:Sprite;
    14          
    15         public function BoxTest()
    16         {
    17             stage.align = StageAlign.TOP_LEFT;
    18             stage.scaleMode = StageScaleMode.NO_SCALE;
    19             world = new Sprite();
    20             world.x = stage.stageWidth / 2;
    21             world.y = 50;
    22             addChild(world);
    23             for(var i:int = 0; i < 15; i++)
    24             {
    25                 for(var j:int = 0; j < 15; j++)
    26                 {
    27                     var tile:DrawnIsoTile = new DrawnIsoTile(20, 0xcccccc);
    28                     tile.position = new Point3D(i * 20, 0, j * 20);
    29                     world.addChild(tile);
    30                 }
    31             }
    32             world.addEventListener(MouseEvent.CLICK, onWorldClick);
    33             stage.addEventListener(Event.RESIZE,resizeHandler);
    34         }
    35          
    36         private function onWorldClick(event:MouseEvent):void
    37         {
    38             var box:DrawnIsoBox = new DrawnIsoBox(20, Math.random() * 0xffffff, 20);
    39             var pos:Point3D = IsoUtils.screenToIso(new Point(world.mouseX, world.mouseY));
    40             pos.x = Math.round(pos.x / 20) * 20;
    41             pos.y = Math.round(pos.y / 20) * 20;
    42             pos.z = Math.round(pos.z / 20) * 20;
    43             box.position = pos;
    44             world.addChild(box);
    45         }
    46          
    47         private function resizeHandler(e:Event):void{
    48             world.x = stage.stageWidth / 2;
    49         }
    50     }
    51 }
    View Code

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

    作者:菩提树下的杨过
    出处:http://yjmyzz.cnblogs.com 
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
     
  • 相关阅读:
    对物联网的认识
    读书笔记
    Intel:从屌丝逆袭成业界大佬
    实模式:奇葩的存在
    depot_tools Google代码管理工具包
    std::out_of_range异常
    SensorMode选择
    shell脚本学习(2)查找
    shell脚本学习(1)入门
    输入子系统
  • 原文地址:https://www.cnblogs.com/buerjiongjiong/p/4731004.html
Copyright © 2011-2022 走看看