日期:2008年11月
从这篇文章开始,我将开始介绍3D物体及其在空间中运动和交互。这里提到的物体是指单个的实体,比如银河系中的一颗恒星,那么空间就是银河系了。不过,所有的一切都是相对的,当一个分子作为例子中的实体的时候,那么一个细胞也可以作为3D的空间来看待(一个细胞是由很多的分子组成),同理你可以知道细胞相对于一个生物(空间)来说也是一个物体。有些说多了,不过我想让你明白,我们用程序模拟一只小狗,或者一个人作为一个整体,但是不可能完全真实的模拟它。因为,人体由数不清的细胞组成,每一个细胞都是一个物体,做着自己的运动,除非使用计算机真实模拟着人体的每一个细胞以及它的运动,否则永远不可能得到一个真实模拟的人。但是使用现代的计算机科技是不可能模拟组成人体的所有细胞,那就更不用说组成每个细胞的分子。
还是言归正传来看一个3D物体的例子,这也是第一个绘制一个3D物体的例子。这个程序里,创建一个正方体并且让它围绕着正方体的对角线交点自转,不过这个正方体还是由8个好朋友小P组成,每个顶点站一个,由它们来勾勒这个正方体的框架。
(非常抱歉,下面的Flash文件不再支持)
一个小P组成的正方体,鼠标掠过开始旋转
动画制作步骤
1. 首先在Flash IDE里绘制一个物体小P。
2. 开始设置还是和以前一样,原点,摄像机,焦距等等,另外不要忘记创建一个旋转角度object,存放物体在x,y和z轴的旋转角度变量。
var PI = 3.1415926535897932384626433832795;
// origin is the center of the view point in 3d space
// everything scale around this point
// these lines of code will shift 3d space origin to the center
var origin = new Object();
origin.x = stage.stageWidth/2;
origin.y = stage.stageHeight/2;
origin.z = 0;
// focal length of viewer's camera
var focal_length = 300;
// now create a scene object to hold the spinning box
var scene = new Sprite();
scene.x = origin.x;
scene.y = origin.y
this.addChild(scene);
var axis_rotation = new Object();
axis_rotation.x = 0;
axis_rotation.y = 0;
axis_rotation.z = 0;
var camera = new Object();
camera.x = 0;
camera.y = 0;
camera.z = 0;
3. 写一个函数,用它来创建空间中的一个点,scale_point代表这个点在投射到2D平面上后位置缩放的比率。
function vertex3d(x, y, z, scale = 1):Object
{
var point3d = new Object();
point3d.x = x;
point3d.y = y;
point3d.z = z;
point3d.scale_point = scale;
return point3d;
}
4. 下面发挥一下你的空间想象力,使用第3步的函数创建正方体的8个顶点,并且把它们添加到一个数组里。
var len = 50; // half of the cube width
// now create the vertexes for the cube
var points = [
// x y z
vertex3d(-len, -len, -len), // rear upper left
vertex3d(len, -len, -len), // rear upper right
vertex3d(len, -len, len), // front upper right
vertex3d(-len, -len, len), // front upper left
vertex3d(-len, len, -len), // rear lower left
vertex3d(len, len, -len), // rear lower right
vertex3d(len, len, len), // front lower right
vertex3d(-len, len, len), // front lower left
];
5. 初始化8个小P,并且把它们放在8个顶点(映射到xy轴上的点)所在的x和y位置。
for (var i = 0; i < points.length; i++)
{
var ball = new Sphere();
ball.x = points[i].x;
ball.y = points[i].y;
ball.z = 0;
scene.addChild(ball);
}
6. 这个函数你在摄像机空间旋转一篇文章中应该见过,函数的功能是把3D空间的点,映射到2D平面xy上。函数执行的步骤是这样的:
a) 提前计算出x,y和z旋转角度的正余弦值。
b) 使用for loop遍历物体所有的顶点。
c) 使用计算出的正余弦和三角函数对三个轴的旋转分别进行计算,得出旋转后顶点的x,y和z。
d) 然后计算出物体在2D平面上映射后的x和y值。
e) 并且把这些2D点添加到一个新的数组里。
f) 最后返回这个数组。
{
var projected = [];
// declare some variable for saving function call
var sin_x = Math.sin(axis_rotation.x);
var cos_x = Math.cos(axis_rotation.x);
var sin_y = Math.sin(axis_rotation.y);
var cos_y = Math.cos(axis_rotation.y);
var sin_z = Math.sin(axis_rotation.z);
var cos_z = Math.cos(axis_rotation.z);
var x, y, z, // 3d x, y, z
xy, xz, // rotate about x axis
yx, yz, // rotate about y axis
zx, zy, // rotate about z axis
scale; // 2d scale transform
for (var i = 0; i < points.length; i++)
{
x = points[i].x;
y = points[i].y;
z = points[i].z;
// here is the theroy:
// suppose a is the current angle, based on given current_x, current_y on a plane
// (can be x, y plane, or y, z plane or z, x plane), rotate angle b
// then the new x would be radius*cos(a+b) and y would be radius*sin(a+b)
// radius*cos(a+b) = radius*cos(a)*cos(b) - radius*sin(a)*sin(b)
// radius*sin(a+b) = radius*sin(a)*cos(b) + radius*cos(a)*sin(b) // rotate about x axis
xy = cos_x*y - sin_x*z;
xz = sin_x*y + cos_x*z;
// rotate about y axis
yz = cos_y*xz - sin_y*x;
yx = sin_y*xz + cos_y*x;
// rotate about z axis
zx = cos_z*yx - sin_z*xy;
zy = sin_z*yx + cos_z*xy;
// scale it
scale = focal_length/(focal_length+yz-camera.z);
x = zx*scale - camera.x; // get x position in the view of camera
y = zy*scale - camera.y; // get x position in the view of camera
projected[i] = vertex3d(x, y, yz, scale);
}
return projected;
}
这样就得到一个数组,包含所有需要的2D数据。并不困难,你完全可以把这一段代码叫做这个程序的3D引擎,它负责了所有点的数据在空间里旋转计算和输出。
7. 下面是动画执行的循环函数。需要注意的一点,在以后的文章中我都将使用基于时间的运动。在这里你只要知道下面的公式就可以了:旋转角度=角速度X时间。使用上面的公式,递增物体围绕y轴和x轴的旋转角度。然后使用第6步的函数计算所有的3D顶点旋转后的位置并且得到映射后的2D点。剩下你应该能想到,就是把相应的小P定位到这些定点上,并且对小球进行缩放比率scale_point,最后不要忘记对小P进行z排序。
{
// well we use time based movement in this tutorial
var current_time = new Date().getTime(); // sampe the current time
// increment the rotation around y axis
axis_rotation.y += 0.0008*(current_time-start_time);
// increment the rotation around x axis
axis_rotation.x += 0.0006*(current_time-start_time);
start_time = current_time; // reset the start time
var projected = project_pts(points); // 3d ponts to 2d transformation
// now we have all the data we need to position the balls
for (var i = 0; i < scene.numChildren; i++) // loop throught the scene
{
// positioning the ball
scene.getChildAt(i).x = projected[i].x;
scene.getChildAt(i).y = projected[i].y;
scene.getChildAt(i).z = projected[i].z;
scene.getChildAt(i).scaleX = scene.getChildAt(i).scaleY = projected[i].scale_point;
}
swap_depth(scene); // sort out the depth
}
// bubble sort algo
function swap_depth(container:Sprite)
{
for (var i = 0; i < container.numChildren - 1; i++)
{
for (var j = container.numChildren - 1; j > 0; j--)
{
if (Object(container.getChildAt(j-1)).z < Object(container.getChildAt(j)).z)
{
container.swapChildren(container.getChildAt(j-1), container.getChildAt(j));
}
}
}
}
// now add the event listener and spin the box
this.addEventListener(Event.ENTER_FRAME, move);
注意
例子中物体沿着x和y轴旋转,但是并没有添加z轴旋转,你可以自己添加上,看看有什么不同。
注意
例子中我们使用了两个for loop,第一次遍历所有的顶点把3D点转化为2D点,第二个把相应的小P定位到这些2D点的位置。虽然这样看起来会降低执行速度,但是这样会使程序的流程一目了然。如果你已经非常熟练,你可以试着修改这两个函数提高执行速度。
使用Flash绘制API
上面的例子看起来不错,不过只有顶点有小球,我们看起来还不满意,那么接下来做一个动态绘制的正方体。这个例子里,基本的框架并没有什么变化,正方体所有的边都是用Flash的moveTo()和lineTo()来绘制。那么我就把需要更改代码的地方解释一下。
(非常抱歉,下面的Flash文件不再支持)
一个正方体的框架,鼠标掠过开始旋转
制作步骤
1. 基本上的代码和前面是一样的,同样需要设置场景,创建正方体的顶点,注意不要再在舞台上添加小P。
2. 当把3D点映射到2D平面上后,使用黑线把正方体相邻的两个点连接起来。非常容易理解,不过注意,有很多种办法连接这些点,你可以先连接正方体顶面的点,然后底面的点,最后连接顶面和底面,不要局限于这几种,试着去发现新的方法,找到合适你思维方式的一种。
{
// well we use time based movement in this tutorial
var current_time = new Date().getTime(); // sampe the current time
// increment the rotation around y axis
axis_rotation.y += 0.0008*(current_time-start_time);
// increment the rotation around x axis
axis_rotation.x += 0.0006*(current_time-start_time);
start_time = current_time; // reset the start time
var projected = project_pts(points); // 3d ponts to 2d transformation
// now we start drawing the cube
with (scene.graphics)
{
clear();
lineStyle(0.5, 0x0F6F9F, 1);
// top face
moveTo(projected[0].x, projected[0].y);
lineTo(projected[1].x, projected[1].y);
lineTo(projected[2].x, projected[2].y);
lineTo(projected[3].x, projected[3].y);
lineTo(projected[0].x, projected[0].y);
// bottom face
moveTo(projected[4].x, projected[4].y);
lineTo(projected[5].x, projected[5].y);
lineTo(projected[6].x, projected[6].y);
lineTo(projected[7].x, projected[7].y);
lineTo(projected[4].x, projected[4].y);
// vertical lines
moveTo(projected[0].x, projected[0].y);
lineTo(projected[4].x, projected[4].y);
moveTo(projected[1].x, projected[1].y);
lineTo(projected[5].x, projected[5].y);
moveTo(projected[2].x, projected[2].y);
lineTo(projected[6].x, projected[6].y);
moveTo(projected[3].x, projected[3].y);
lineTo(projected[7].x, projected[7].y);
}
}
3. 还有一个地方需要改动,因为不再对顶点的物体进行缩放,所以就必须要传递scale_point这个属性。
function vertex3d(x, y, z):Object
{
var point3d = new Object();
point3d.x = x;
point3d.y = y;
point3d.z = z;
return point3d;
}
建议
试着把上面的两种框架构建方式结合在一起,制作一个旋转的物体被线连着,试一试一条螺旋体的模型,或者如果你想增加难度的话,你还可以做一个DNA链。
(非常抱歉,下面的Flash文件不再支持)
DNA链
那么到目前为止,你已经知道如何使用框架构建一个方体,不过现实中物体总是有纹理和填充色的。你也许会想,使用Flash的beginFill()函数就可以给物体加上填充色了,这不是很简单。Hum,很接近不过如果要给物体上色的话,还有很多工作要做,后面的文章中将重点开始介绍着色筛选和相关内容。
关于Time Based和Frame Based运动
文章第一个例子中的制作步骤里,提到关于基于时间的运动公式(只要知道了物体运动的速度,那么根据牛顿第一运动定律就可以得出物体在某个时间点的位移):
回想一下,前面的几篇文章里使用的都是基于祯的运动,然而基于祯的运动是不稳定的,它的公式是:
基于祯的运动不管程序执行流逝了多少时间,只在function执行的时候给物体的x或者y加减一定的值。这种运动是不稳定的,所以我建议大家使用基于时间的运动,下面的两个动画分别用两种运动模式做成,点击一下动画就会在function执行时执行大量的junk运算,这时你就会看到两种运动的差异。而基于时间的运动中,当速度恒定时,物体会处在正确的位置;基于祯的运动,你就会看到物体运动慢下来很多, 并不能达到物体在某个时间点应该到达的位置。由于这个页面里还有另外3个使用大量CPU运算的动画,所以我把下面的动画移到另外一个页面了,点击这里到另外一个文章里查看下面的两个动画,如果感觉动画还是不够连贯的话,那么你可以下载这两个动画到本机察看。
非常抱歉,文中暂时不提供源文件下载,如果你需要源文件,请来信或者留言给我。
作者:Yang Zhou 出处:http://yangzhou1030.cnblogs.com 本文版权归作者和博客园共有,转载未经作者同意必须保留此段声明。请在文章页面明显位置给出原文连接,作者保留追究法律责任的权利。 |