Animation in ActionScript3.0 这本书总算快学完了,今天继续:上一回Flash/Flex学习笔记(50):3D线条与填充 里,我们知道任何一个3D多面体上的某一个面,都可以分解为多个三角形的组合。比立方体为例,每个面都由二个三角形组成,但在那一篇的示例中明显有一个问题:不管立方体的某一个面是不是应该被人眼看见(比如转到背面的部分,应该是看不见的),这一面都被绘制出来了。
在这一篇的学习中,我将带大家一起学习如何将背面(即看不见的面)删除掉,即所谓的“背面剔除”。
先做一些预备知识的铺垫:立方体中每个面都有一个"外面"和"里面"。外面即正对观察者向外的这一面,里面指朝向立方体内部的这一面。我们在3D编程里,通常指的都是“外面”
如上图:这是立方体的前面,分解为0-1-2和0-2-3二个三角形(注意三个顶点的顺序为"顺时针"方向),当立方体的"前面"旋转到"后面"所处位置时,三角形的顶点顺序由“顺时针”改变为“逆时针”。
言外之意:如果我们能判断出某个三角形的顶点顺序为“逆时针”时,这个三角形肯定处于背面,这时应该将它隐藏或不绘制。
所以,如果我们在构建立方体每个面的三角形时,都遵守上面的“三角形顶点顺时针法则”,那么上面的解决办法应该就能满足要求了,回顾一下立方体三角形数组的构建代码:
02 |
triangles[ 0 ] = new Triangle(points[ 0 ], points[ 1 ], points[ 2 ], 0x6666cc ); |
03 |
triangles[ 1 ] = new Triangle(points[ 0 ], points[ 2 ], points[ 3 ], 0x6666cc ); |
05 |
triangles[ 2 ] = new Triangle(points[ 0 ], points[ 5 ], points[ 1 ], 0x66cc66 ); |
06 |
triangles[ 3 ] = new Triangle(points[ 0 ], points[ 4 ], points[ 5 ], 0x66cc66 ); |
08 |
triangles[ 4 ] = new Triangle(points[ 4 ], points[ 6 ], points[ 5 ], 0xcc6666 ); |
09 |
triangles[ 5 ] = new Triangle(points[ 4 ], points[ 7 ], points[ 6 ], 0xcc6666 ); |
11 |
triangles[ 6 ] = new Triangle(points[ 3 ], points[ 2 ], points[ 6 ], 0xcc66cc ); |
12 |
triangles[ 7 ] = new Triangle(points[ 3 ], points[ 6 ], points[ 7 ], 0xcc66cc ); |
14 |
triangles[ 8 ] = new Triangle(points[ 1 ], points[ 5 ], points[ 6 ], 0x66cccc ); |
15 |
triangles[ 9 ] = new Triangle(points[ 1 ], points[ 6 ], points[ 2 ], 0x66cccc ); |
17 |
triangles[ 10 ] = new Triangle(points[ 4 ], points[ 0 ], points[ 3 ], 0xcccc66 ); |
18 |
triangles[ 11 ] = new Triangle(points[ 4 ], points[ 3 ], points[ 7 ], 0xcccc66 ); |
建议大家去买一个立体魔方玩具,每个点按照上一篇里的顶点数字拿笔标记起来,对比上面的代码发现,这样的代码正好是遵守这一规则的,当然代码不必完全跟这一样,比如:
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,已经成功了一半,如何判断三角形处于背面?
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; |
在Triangle.cs中增加这个私有方法即可(我也不知道怎么来的,反正这个函数确实管用,就当公式死记下来好了.)
最后一个小问题:在旋转的过程中,三角形的三个顶点“z轴深度”(zPos值)都在变化,有可能出现某个三角形的顶点挡住了另外一个三角形的顶点。所以我们还得解决三角形的z轴排序问题,这里有一个法则,可以把三个顶点中离观察者最近的一个顶zPos值,认为是三角形的z轴深度,所以Triangle.cs中还得增加一个z轴属性:depth,最终Triangle.cs的内容如下:
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 ) { |
14 |
public function draw(g:Graphics): void { |
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); |
29 |
private function isBackFace(): Boolean { |
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; |
39 |
public function get depth(): Number { |
40 |
var zpos: Number = Math.min(pointA.z,pointB.z); |
41 |
zpos = Math.min(zpos,pointC.z); |
罗嗦了一堆,激动人心的时刻终于来了,原来的立方体示例代码中,只要增加一行代码:
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 ; |
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); |
19 |
triangles.sortOn( "depth" , Array .DESCENDING | Array .NUMERIC); |
22 |
for (i = 0 ; i < triangles.length; i++) { |
23 |
triangles[i].draw(graphics); |
编译运行,最终将得到一个仅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向量类:
06 |
private var _brightness: Number ; |
08 |
public function Light(x: Number =- 200 ,y: Number =- 200 ,z: Number =- 200 ,brightness: Number = 1 ) { |
14 |
this .brightness = brightness; |
17 |
public function set brightness(b: Number ): void { |
19 |
_brightness = Math.max(b, 0 ); |
20 |
_brightness = Math.min(_brightness, 1 ); |
23 |
public function get brightness(): Number { |
那么,如果计算向量的矢量积,以及夹角呢?先给出数学公式:
叉积公式:
夹角公式
点积(也称数量积或内积)公式
ok,理论知识准备得差不多了,下面来改造Triangle三角形基类:
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; |
09 |
public function Triangle(a:Point3D,b:Point3D,c:Point3D,color: uint ) { |
16 |
public function draw(g:Graphics): void { |
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); |
29 |
private function getAdjustedColor(): uint { |
31 |
var red: Number = color >> 16 ; |
32 |
var green: Number = color >> 8 & 0xff ; |
33 |
var blue: Number = color & 0xff ; |
35 |
var lightFactor: Number = getLightFactor(); |
41 |
return red << 16 | green << 8 | blue; |
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 (); |
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); |
62 |
var dotProd: Number = norm.x * light.x + norm.y * light.y + norm.z * light.z; |
65 |
var normMag: Number = Math.sqrt(norm.x * norm.x + norm.y * norm.y + norm.z * norm.z); |
68 |
var lightMag: Number = Math.sqrt(light.x * light.x + light.y * light.y + light.z * light.z); |
71 |
var angle: Number = Math.acos(dotProd / (normMag * lightMag); |
73 |
return (angle / Math.PI) * light.brightness; |
77 |
private function isBackFace(): Boolean { |
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; |
87 |
public function get depth(): Number { |
88 |
var zpos: Number = Math.min(pointA.z,pointB.z); |
89 |
zpos = Math.min(zpos,pointC.z); |
可以看到,我们几乎把所有的处理工作都放在Triangle.cs中完成了,好好体会一下。这一切完成之后,主动画中就能自动体现出3D光线的效果了么?No,我们还没给立方体添加光源呢!不过这个很容易,改一个地方即可:
01 |
function Init(): void { |
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 ); |
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 ); |
13 |
for ( var i: uint = 0 ; i < pointNum; i++) { |
14 |
points[i].setVanishingPoint(vpX, vpY); |
15 |
points[i].setCenter( 0 , 0 , 50 ); |
19 |
triangles = new Array (); |
21 |
var _t: Number = 0xFF0000 ; |
26 |
triangles[ 0 ] = new Triangle(points[ 1 ],points[ 2 ],points[ 0 ],_t); |
27 |
triangles[ 1 ] = new Triangle(points[ 0 ],points[ 2 ],points[ 3 ],_t); |
34 |
triangles[ 4 ] = new Triangle(points[ 5 ],points[ 4 ],points[ 6 ],_t); |
35 |
triangles[ 5 ] = new Triangle(points[ 4 ],points[ 7 ],points[ 6 ],_t); |
42 |
triangles[ 2 ] = new Triangle(points[ 1 ],points[ 0 ],points[ 4 ],_t); |
43 |
triangles[ 3 ] = new Triangle(points[ 1 ],points[ 4 ],points[ 5 ],_t); |
49 |
triangles[ 6 ] = new Triangle(points[ 3 ],points[ 2 ],points[ 6 ],_t); |
50 |
triangles[ 7 ] = new Triangle(points[ 3 ],points[ 6 ],points[ 7 ],_t); |
55 |
triangles[ 8 ] = new Triangle(points[ 2 ],points[ 1 ],points[ 5 ],_t); |
56 |
triangles[ 9 ] = new Triangle(points[ 2 ],points[ 5 ],points[ 6 ],_t); |
61 |
triangles[ 10 ] = new Triangle(points[ 4 ],points[ 0 ],points[ 3 ],_t); |
62 |
triangles[ 11 ] = new Triangle(points[ 4 ],points[ 3 ],points[ 7 ],_t); |
65 |
var light:Light = new Light(- 275 ,- 200 ,- 150 ); |
66 |
for (i = 0 ; i < triangles.length; i++) { |
67 |
triangles[i].light = light; |
70 |
addEventListener(Event.ENTER_FRAME, EnterFrameHandler); |
71 |
stage.addEventListener(KeyboardEvent.KEY_DOWN, KeyDownHandler); |
注意打星号的部分,只需要给三角形数组中的每个三角形赋值同样的光源实例即可,其它地方都不用动。
总算写完了,累啊,这一章确实有些难度,想起了毛主席的经典语录:“学好数理化,走遍天下都不怕!”