• Flash/Flex学习笔记(55):背面剔除与 3D 灯光


    Animation in ActionScript3.0 这本书总算快学完了,今天继续:上一回Flash/Flex学习笔记(50):3D线条与填充 里,我们知道任何一个3D多面体上的某一个面,都可以分解为多个三角形的组合。比立方体为例,每个面都由二个三角形组成,但在那一篇的示例中明显有一个问题:不管立方体的某一个面是不是应该被人眼看见(比如转到背面的部分,应该是看不见的),这一面都被绘制出来了。

    在这一篇的学习中,我将带大家一起学习如何将背面(即看不见的面)删除掉,即所谓的“背面剔除”。

    先做一些预备知识的铺垫:立方体中每个面都有一个"外面"和"里面"。外面即正对观察者向外的这一面,里面指朝向立方体内部的这一面。我们在3D编程里,通常指的都是“外面”

     

    如上图:这是立方体的前面,分解为0-1-2和0-2-3二个三角形(注意三个顶点的顺序为"顺时针"方向),当立方体的"前面"旋转到"后面"所处位置时,三角形的顶点顺序由“顺时针”改变为“逆时针”。

    言外之意:如果我们能判断出某个三角形的顶点顺序为“逆时针”时,这个三角形肯定处于背面,这时应该将它隐藏或不绘制。

    所以,如果我们在构建立方体每个面的三角形时,都遵守上面的“三角形顶点顺时针法则”,那么上面的解决办法应该就能满足要求了,回顾一下立方体三角形数组的构建代码:

    01 //前
    02 triangles[0] = new Triangle(points[0], points[1], points[2], 0x6666cc);
    03 triangles[1] = new Triangle(points[0], points[2], points[3], 0x6666cc);
    04 //上
    05 triangles[2] = new Triangle(points[0], points[5], points[1], 0x66cc66);
    06 triangles[3] = new Triangle(points[0], points[4], points[5], 0x66cc66);
    07 //后
    08 triangles[4] = new Triangle(points[4], points[6], points[5], 0xcc6666);
    09 triangles[5] = new Triangle(points[4], points[7], points[6], 0xcc6666);
    10 //底
    11 triangles[6] = new Triangle(points[3], points[2], points[6], 0xcc66cc);
    12 triangles[7] = new Triangle(points[3], points[6], points[7], 0xcc66cc);
    13 //右
    14 triangles[8] = new Triangle(points[1], points[5], points[6], 0x66cccc); 
    15 triangles[9] = new Triangle(points[1], points[6], points[2], 0x66cccc);
    16 //左
    17 triangles[10] =new Triangle(points[4], points[0], points[3], 0xcccc66);
    18 triangles[11] =new Triangle(points[4], points[3], points[7], 0xcccc66);

    建议大家去买一个立体魔方玩具,每个点按照上一篇里的顶点数字拿笔标记起来,对比上面的代码发现,这样的代码正好是遵守这一规则的,当然代码不必完全跟这一样,比如:

    1 //前
    2 triangles[0] = new Triangle(points[0], points[1], points[2], 0x6666cc);
    3 triangles[1] = new Triangle(points[0], points[2], points[3], 0x6666cc);

    也可以写成:

    1 triangles[0] = new Triangle(points[1],points[2],points[0],0x6666cc);
    2 triangles[1] = new Triangle(points[0],points[2],points[3],0x6666cc);

    1 triangles[0] = new Triangle(points[1],points[2],points[3],0x6666cc);
    2 triangles[1] = new Triangle(points[1],points[3],points[0],0x6666cc);

    只要满足顺时针规则即可.ok,已经成功了一半,如何判断三角形处于背面?

    1 //判断是否在背面
    2 private function isBackFace():Boolean {
    3     var cax:Number = pointC.screenX - pointA.screenX;
    4     var cay:Number = pointC.screenY - pointA.screenY;
    5     var bcx:Number = pointB.screenX - pointC.screenX;
    6     var bcy:Number = pointB.screenY - pointC.screenY;
    7     return cax * bcy > cay * bcx;
    8 }

    在Triangle.cs中增加这个私有方法即可(我也不知道怎么来的,反正这个函数确实管用,就当公式死记下来好了.)

    最后一个小问题:在旋转的过程中,三角形的三个顶点“z轴深度”(zPos值)都在变化,有可能出现某个三角形的顶点挡住了另外一个三角形的顶点。所以我们还得解决三角形的z轴排序问题,这里有一个法则,可以把三个顶点中离观察者最近的一个顶zPos值,认为是三角形的z轴深度,所以Triangle.cs中还得增加一个z轴属性:depth,最终Triangle.cs的内容如下:

    01 package {
    02     import flash.display.Graphics;
    03     public class Triangle {
    04         private var pointA:Point3D;
    05         private var pointB:Point3D;
    06         private var pointC:Point3D;
    07         private var color:uint;
    08         public function Triangle(a:Point3D,b:Point3D,c:Point3D,color:uint) {
    09             pointA = a;
    10             pointB = b;
    11             pointC = c;
    12             this.color = color;
    13         }
    14         public function draw(g:Graphics):void {
    15             //如果是背面,则不绘制
    16             if (isBackFace()) {
    17                 return;
    18             }
    19   
    20             g.beginFill(color);
    21             g.moveTo(pointA.screenX,pointA.screenY);
    22             g.lineTo(pointB.screenX,pointB.screenY);
    23             g.lineTo(pointC.screenX,pointC.screenY);
    24             g.lineTo(pointA.screenX,pointA.screenY);
    25             g.endFill();
    26         }
    27   
    28         //判断是否在背面
    29         private function isBackFace():Boolean {
    30             // 见 http://www.jurjans.lv/flash/shape.html
    31             var cax:Number = pointC.screenX - pointA.screenX;
    32             var cay:Number = pointC.screenY - pointA.screenY;
    33             var bcx:Number = pointB.screenX - pointC.screenX;
    34             var bcy:Number = pointB.screenY - pointC.screenY;
    35             return cax * bcy > cay * bcx;
    36         }
    37   
    38         //取得三角形所在的z轴深度(以三个顶点中离观察者最近的点为准)
    39         public function get depth():Number {
    40             var zpos:Number = Math.min(pointA.z,pointB.z);
    41             zpos = Math.min(zpos,pointC.z);
    42             return zpos;
    43         }
    44     }
    45 }

    罗嗦了一堆,激动人心的时刻终于来了,原来的立方体示例代码中,只要增加一行代码:

    01 function EnterFrameHandler(e:Event):void {
    02     var dx:Number = mouseX - vpX;
    03     var dy:Number = mouseY - vpY;
    04     var angleX:Number = dy * 0.001;
    05     var angleY:Number = dx * 0.001;
    06     var angleZ:Number = Math.sqrt(dx * dx + dy * dy) * 0.0005;
    07     if (dx > 0) {
    08         angleZ *=  -1;
    09     }
    10   
    11   
    12     for (var i:uint = 0; i < pointNum; i++) {
    13         var point:Point3D = points[i];
    14         point.rotateX(angleX);
    15         point.rotateY(angleY);
    16         point.rotateZ(angleZ);
    17     }
    18   
    19     triangles.sortOn("depth", Array.DESCENDING | Array.NUMERIC);//增加三角形数组的z轴排序
    20       
    21     graphics.clear();
    22     for (i = 0; i < triangles.length; i++) {
    23         triangles[i].draw(graphics);
    24     }
    25 }

    编译运行,最终将得到一个仅2.7k的swf动画,而且还带有鼠标交互的3D立方体,cool 吧!

    其它示例修改后,效果如下:

    3D光线:

    这部分内容比较难理解(需要有一定的线性代数基础),先上最终的效果图(光源的位置在左顶点,z轴“-100”处--即flash动画左上顶点距离屏幕垂直向外100的地方,需要一点想象力)

    理解原理需要线性代数中“向量的矢量积”以及“向量的数量积”、“向量夹角计算”这三个关键概念(不熟悉的童鞋们,请先下载“高等数学-07章空间解释几何与向量代数.pdf”回忆一下数学老师教给我们的东西,有点痛苦!)

    如上图,对于每个三角形必须先确定其“法向”向量norm,norm即为向量ab与向量bc的叉积。然后光源light本身也是一个向量,向量light与向量norm会形成一个夹角θ,θ的取值范围在0~PI(即180度)之间,θ为180度时即为正面直射,θ为0度时即为背面照射(实际上小于等于90度时,已经照不到了),直射意味着三角形所在平面颜色应该正常显示(最明亮),背面或照不到时,应该颜色变暗,接近黑色。

    关于这个结论,可以先来看下面的演示:(光源的位置我设置为动画中心,距离屏幕向外100px的位置,即正对着屏幕中心照射)

    一步一步来,先定义Light向量类:

    01 package {
    02     public class Light {
    03         public var x:Number;
    04         public var y:Number;
    05         public var z:Number;
    06         private var _brightness:Number;
    07           
    08         public function Light(x:Number=-200,y:Number=-200,z:Number=-200,brightness:Number=1) {
    09             //light向量的空间坐标
    10             this.x = x;
    11             this.y = y;
    12             this.z = z;
    13             //亮度
    14             this.brightness = brightness;
    15         }
    16           
    17         public function set brightness(b:Number):void {
    18             //亮度值通常要求在0与1之间
    19             _brightness = Math.max(b,0);
    20             _brightness = Math.min(_brightness,1);
    21         }
    22           
    23         public function get brightness():Number {
    24             return _brightness;
    25         }
    26     }
    27 }

    那么,如果计算向量的矢量积,以及夹角呢?先给出数学公式:

    叉积公式:

    夹角公式

    点积(也称数量积或内积)公式

    ok,理论知识准备得差不多了,下面来改造Triangle三角形基类:

    01 package {
    02     import flash.display.Graphics;
    03     public class Triangle {
    04         private var pointA:Point3D;
    05         private var pointB:Point3D;
    06         private var pointC:Point3D;
    07         private var color:uint;
    08         public var light:Light;//每个三角形增加一个光源Light
    09         public function Triangle(a:Point3D,b:Point3D,c:Point3D,color:uint) {
    10             pointA = a;
    11             pointB = b;
    12             pointC = c;
    13             this.color = color;
    14         }
    15   
    16         public function draw(g:Graphics):void {
    17             if (isBackFace()) {
    18                 return;
    19             }
    20             g.beginFill(getAdjustedColor());//★★★这里改为根据光线的角度来动态填充颜色
    21             g.moveTo(pointA.screenX,pointA.screenY);
    22             g.lineTo(pointB.screenX,pointB.screenY);
    23             g.lineTo(pointC.screenX,pointC.screenY);
    24             g.lineTo(pointA.screenX,pointA.screenY);
    25             g.endFill();
    26         }
    27       
    28         //★★★根据光线得到动态调整后的颜色
    29         private function getAdjustedColor():uint {
    30             //取出红,绿,蓝三色分量
    31             var red:Number = color >> 16;
    32             var green:Number = color >> 8 & 0xff;
    33             var blue:Number = color & 0xff;
    34               
    35             var lightFactor:Number = getLightFactor();//★★★根据光线得到的颜色调整因子!!! 这是关键!
    36               
    37               
    38             red *=  lightFactor;
    39             green *=  lightFactor;
    40             blue *=  lightFactor;
    41             return red << 16 | green << 8 | blue;
    42         }
    43           
    44         //根据光线得到的颜色调整因子(最难理解的的一个函数)
    45         private function getLightFactor():Number {
    46             var ab:Object = new Object();
    47             ab.x = pointA.x - pointB.x;
    48             ab.y = pointA.y - pointB.y;
    49             ab.z = pointA.z - pointB.z;
    50             var bc:Object = new Object();
    51             bc.x = pointB.x - pointC.x;
    52             bc.y = pointB.y - pointC.y;
    53             bc.z = pointB.z - pointC.z;
    54             var norm:Object = new Object();
    55               
    56             //计算法向向量norm的坐标值 
    57             norm.x = (ab.y * bc.z) - (ab.z * bc.y);
    58             norm.y = -((ab.x * bc.z) - (ab.z * bc.x));
    59             norm.z = (ab.x * bc.y) - (ab.y * bc.x);
    60               
    61             //向量norm与向量light的点积(数量积)
    62             var dotProd:Number = norm.x * light.x + norm.y * light.y + norm.z * light.z;
    63               
    64             //向量norm的模长
    65             var normMag:Number = Math.sqrt(norm.x * norm.x + norm.y * norm.y + norm.z * norm.z);
    66               
    67             //向量light的模长            
    68             var lightMag:Number = Math.sqrt(light.x * light.x + light.y * light.y + light.z * light.z);
    69               
    70             //夹角angle
    71             var angle:Number = Math.acos(dotProd / (normMag * lightMag);
    72                                            
    73             return (angle / Math.PI) * light.brightness; //夹角除取值范围最大值PI,将得到一个0到1之间的小数,然后再乘light的亮度值,即得到最终的光线调整因子
    74         }
    75   
    76         //是否处于背面
    77         private function isBackFace():Boolean {
    78               
    79             var cax:Number = pointC.screenX - pointA.screenX;
    80             var cay:Number = pointC.screenY - pointA.screenY;
    81             var bcx:Number = pointB.screenX - pointC.screenX;
    82             var bcy:Number = pointB.screenY - pointC.screenY;
    83             return cax * bcy > cay * bcx;
    84         }
    85   
    86         //z轴深度
    87         public function get depth():Number {
    88             var zpos:Number = Math.min(pointA.z,pointB.z);
    89             zpos = Math.min(zpos,pointC.z);
    90             return zpos;
    91         }
    92     }
    93 }

    可以看到,我们几乎把所有的处理工作都放在Triangle.cs中完成了,好好体会一下。这一切完成之后,主动画中就能自动体现出3D光线的效果了么?No,我们还没给立方体添加光源呢!不过这个很容易,改一个地方即可:

    01 function Init():void {
    02     //前面四个点
    03     points[0] = new Point3D(-50,-50,-50);
    04     points[1] = new Point3D(50,-50,-50);
    05     points[2] = new Point3D(50,50,-50);
    06     points[3] = new Point3D(-50,50,-50);
    07     //后面四个点
    08     points[4] = new Point3D(-50,-50,50);
    09     points[5] = new Point3D(50,-50,50);
    10     points[6] = new Point3D(50,50,50);
    11     points[7] = new Point3D(-50,50,50);
    12   
    13     for (var i:uint = 0; i < pointNum; i++) {
    14         points[i].setVanishingPoint(vpX, vpY);
    15         points[i].setCenter(0, 0, 50);
    16     }
    17   
    18     //根据顶点赋值三角形数组
    19     triangles = new Array();
    20   
    21     var _t:Number = 0xFF0000;
    22   
    23   
    24   
    25     //前面
    26     triangles[0] = new Triangle(points[1],points[2],points[0],_t);
    27     triangles[1] = new Triangle(points[0],points[2],points[3],_t);
    28       
    29   
    30     _t = 0xFF0000;;
    31   
    32   
    33     //后面
    34     triangles[4] = new Triangle(points[5],points[4],points[6],_t);
    35     triangles[5] = new Triangle(points[4],points[7],points[6],_t);
    36   
    37   
    38     _t = 0x00FF00;;
    39   
    40   
    41     //上面
    42     triangles[2] = new Triangle(points[1],points[0],points[4],_t);
    43     triangles[3] = new Triangle(points[1],points[4],points[5],_t);
    44   
    45   
    46     _t = 0x00FF00;;
    47   
    48     //下面
    49     triangles[6] = new Triangle(points[3],points[2],points[6],_t);
    50     triangles[7] = new Triangle(points[3],points[6],points[7],_t);
    51   
    52     _t = 0x0000FF;
    53   
    54     //右面
    55     triangles[8] = new Triangle(points[2],points[1],points[5],_t);
    56     triangles[9] = new Triangle(points[2],points[5],points[6],_t);
    57   
    58     _t = 0x0000FF;
    59   
    60     //左面
    61     triangles[10] = new Triangle(points[4],points[0],points[3],_t);
    62     triangles[11] = new Triangle(points[4],points[3],points[7],_t);
    63   
    64     //★★★只要增加下面的代码,给每个三角形赋值同样的光源实例即可!
    65     var light:Light = new Light(-275,-200,-150);
    66     for (i = 0; i < triangles.length; i++) {
    67         triangles[i].light = light;
    68     }
    69   
    70     addEventListener(Event.ENTER_FRAME, EnterFrameHandler);
    71     stage.addEventListener(KeyboardEvent.KEY_DOWN, KeyDownHandler);
    72 }

    注意打星号的部分,只需要给三角形数组中的每个三角形赋值同样的光源实例即可,其它地方都不用动。

    总算写完了,累啊,这一章确实有些难度,想起了毛主席的经典语录:“学好数理化,走遍天下都不怕!”

  • 相关阅读:
    AtCoder Grand Contest 013 C:Ants on a Circle
    AtCoder Grand Contest 010 C:Cleaning
    055 列表推导式
    054 三元表达式
    05 Python爬虫之信息标记与提取方法
    053 迭代器
    052 装饰器
    051 闭包函数
    04 Python爬虫之Beautiful Soup库
    03 Python爬虫之Requests网络爬取实战
  • 原文地址:https://www.cnblogs.com/happysky97/p/1884657.html
Copyright © 2020-2023  润新知