源码:http://files.cnblogs.com/flash3d/d3.rar
大家好,我又写东西了!写这个东西花费了我两天的时间,其中遇到的困难五花八门,什么数据搞错的,公式推错的,结构设计失误的。。吃一蛰长一智,总算也是学到了一些经验,有些在代码中已经体现,有些尚未实现。
由于代码量有点多,不能像以往那样把代码全贴上去(700多行,放博客上要好几篇),这里就把一些重要公式,数据结构以及一些初步优化策略和大家简述下。如果对其感兴趣的朋友,可以留言和我讨论,也可以留下邮箱索要源文件。
1.点绕单位向量旋转变换公式
做这个变换,首先需要一个空间单位向量q(q1,q2,q3)作为点要绕的轴。
其次需要点p的初值p(x,y,z)。
再次,需要p绕q旋转的角度theta。
然后 我们可以先计算出一些量
s=sin(theta)
c=cos(theta)
t=1-c
然后 我们构造一个3*3矩阵A
|t*q1*q1+c, t*q1*q2+s*q3, t*q1*q3-s*q2|
|t*q1*q2-s*q3, t*q2*q2+c, t*q2*q3+s*q1|
|t*q1*q3+s*q2, t*q2*q3-s*q1, t*q3*q3+c|
这样我们就能通过一下矩阵计算公式算出p顺时针绕q轴theta弧度之后的坐标
|x'| |t*q1*q1+c, t*q1*q2+s*q3, t*q1*q3-s*q2| |x'|
|y'| = |t*q1*q2-s*q3, t*q2*q2+c, t*q2*q3+s*q1| * |y'|
|z'| |t*q1*q3+s*q2, t*q2*q3-s*q1, t*q3*q3+c| |z'|
这个公式被用在计算显示容器围绕自身坐标系的一个轴(x轴,y轴,z轴)旋转时,其他两个轴的数值变化
2.坐标系叠加公式
因为这个3D变换系统中,有显示容器的概念,实际上是构造一个3D显示列表,显示容器就是显示列表中的一个节点,父级的一切变换将影响到子级,子级变换不影响父级。这样,所有要显示的叶子节点,他的各个点具体的坐标就需要通过各个祖先节点的变换属性迭代计算得出。
我们在设计的时候,每个显示容器都拥有一个独立的坐标系,这个坐标系是通过四个三维点来描述的,一个坐标原点以及三个分别在其三个坐标轴上的向量 (模长为缩放值)。显示容器的坐标系位置是相对于其父级坐标系的。也就说,如果要求出显示容器坐标系相对于世界坐标系的值,必须将显示容器的坐标系和其父 级坐标以及所有祖先坐标系叠加。
现在给出子坐标系和父坐标系叠加的计算方法
构造父坐标矩阵P
|vx_x, vy_x, vz_x, x|
|vx_y, vy_y, vz_y, y|
|vx_z, vy_z, vz_z, z|
vx前缀代表x轴方向上的向量,vy前缀代表y轴方向上的向量,vz前缀代表z轴方向上的向量
构造子坐标系矩阵C
|vx_x, vy_x, vz_x, x|
|vx_y, vy_y, vz_y, y|
|vx_z, vy_z, vz_z, z|
|0, 0, 0, 1|
那么两个坐标系叠加之后的结果为
|vx_x, vy_x, vz_x, x|
|vx_y, vy_y, vz_y, y| = P * C
|vx_z, vy_z, vz_z, z|
在做渲染的时候,就要从显示列表的根节点开始,不断把自身坐标系和子坐标系叠加,直到叶子节点。
有的朋友可能看到这么多矩阵头就晕了。其实,看到矩阵完全不必害怕,矩阵只是一种算式的表示方式,在实际计算中,还是要化为乘法和加法进行计算的。
矩阵乘法的法则是:
乘号左边矩阵的第M行乘上乘号右边矩阵的第N列,然后把计算结果储存到结果矩阵的第M行第N列的位置。
行和列相乘的时候,将行的元素和列里面对应的元素相乘,然后将所有相乘的结果相加(如果还是不懂,求教百度吧)。
由于本人贪图方便,没有去写矩阵计算类,结果在程序中数组乘数组,尼玛眼睛都花了。。。教训呀!写程序千万不能偷懒,不然得不偿失。
3.三维坐标点到二维平面的透视投影
投影是三维世界到二维平面转换的关键。
三维到二维的投影一般分为透视投影和平行投影。
平行投影中三维世界的所有点到屏幕上对应点的连线都是平行的。所以,平行投影并不会给人远近的信息。也就说,所有投影在屏幕上的画面,均没有近大远小的感觉。
而透视投影中,三维世界的所有点到屏幕上的连线,其延长线最终会交于一点。这个交点叫做焦点,焦点到屏幕的距离叫做焦距。
现在给定一个三维点(x,y,z)和焦距f,这里,我们定义世界坐标系原点和屏幕的二维坐标系原点重合。
那么我们可以计算得到一个变换系数k=f/(f+z)
利用这个变换系数,可以得到三维点在屏幕上透视投影的坐标
x'=k*x
y'=k*y
4.用栈进行深度优先遍历
前面讲过,在渲染的时候,需要对显示列表进行遍历叠加坐标系。程序采用深度优先的顺序进行遍历。
为了提高程序程序速度,我们应该坚决抛弃函数递归方法。虽然递归的写法确实是很容易理解,也不容易出错,可是递归的效率低下也是显而易见的。其低效 的原因是每调用一次函数,都需要在系统的栈中压入函数体内的局部变量,每执行完一个函数,都需要在系统栈中弹出局部变量。这样进进出出又费时又费力。
这里我们讲一下用栈进行深度优先遍历的基本思想。
首先在栈中压入根节点。然后进入一个循环。
循环直到栈中没有数据后停止。
在每次循环内,从栈中弹出一个节点,先对这个节点该干嘛干嘛。处理好了之后,如果有子节点的,就将子节点逆序压入栈中,然后进入下一轮循环。
这个方法的好处是它非常节省,甚至连用于确定节点具体位置的index都不需要。
5.在渲染中忽略没有必要更新的数据
这个优化的思想可以用以下几个问题来回答
如果舞台上没有任何一个物体变化过,那么渲染的时候,物体的坐标需要重新计算么?
答:明显不用,我们可以复用最近一次更新数据计算的结果。
如果舞台上有两个物体,一个变化了,一个没有变化,那么没有变化的物体需要重新计算么?
答:不用,没有变化的物体,有最近一次更新数据计算的结果可以复用。
如果舞台上物体没有变化,而他的父级变化了,需要重新计算么?
答:需要重新计算,父级变化影响子级,导致子级坐标系发生变化。
这些问题总结起来,就是说,只要一个节点的父级或者祖先变化了,这个节点就需要更新数据,否则不用。
6.其他优化策略
等待留言啊。。。要源文件的留言啊。。。