原文地址: http://www.adobe.com/cn/devnet/flashplayer/articles/perspective-projection.html
引言
在本教程中,你将了解透视的概念。 透视是开发使用3D渲染技术的Flash项目时使用的一个基本主题。你将充分了解如何使用Stage3D API并且通过使用Stage3D的透视技术来渲染3D世界。本教程是一个关于使用Stage3D的系列教程的一部分,并且它是基于之前描述如何渲染一个三角形的若干教程的信息而编写的。通过使用透视技术渲染3D场景,你能够将该范例项目的功能提升至一个更高水平。
在真实的世界中,我们可以按照一种称为“透视”的方式观看物体。
透视是指一种远处的对象比离你较近的对象看起来似乎要小一些的概念。 透视也指如果你坐在一条笔直的道路中央,那么你实际看到道路边缘能够成为两条会聚的直线。
这就是透视。 在3D项目中,透视至关重要。 如果没有透视技术,那么3D世界看起来就不真实。
尽管这看起来似乎是自然并且显而易见的道理,但是你必须考虑到,当你在电脑上创建3D渲染时,你是在电脑屏幕上试图对3D世界进行模拟,而该屏幕采用的是2D平面。
设想在电脑屏幕后面有某种真实的3D场景,并且你能够透过你的电脑屏幕的“玻璃”观看它。通过使用透视技术,你的目标是创建能够渲染投影到你屏幕“玻璃”上的对象的代码,就好像在屏幕后面存在这么一个真实的3D世界。而唯一需要说明的是该3D世界并非真实存在…它仅仅是3D世界的一个精确模拟。
因此,当使用3D渲染技术在3D世界中模拟一个场景,然后将该3D场景投影至你的电脑屏幕的2D表面时,该过程被称为透视投影。
首先,凭直觉预想一下你想要获得的效果。 如果某个对象离观看者较近,那么该对象必须看起来较大。 如果该对象例观看者较远,那么它必须看起来较小。 此外,如果某个对象不断远离观看者,那么你希望它沿着一条直线在屏幕中心汇聚,就好像它一直远离直到消失在远方。
当你观看图1中的插图时,设想一下在你的3D场景中放置一个对象。 在3D世界中,该对象的位置可以用xW,yW,zW来描述,这是一个从视点角度看过去的具有坐标原点的3D坐标系统。 这是该对象实际存在的位置,它位于屏幕之外的3D场景中。
当观看者在屏幕上观察该对象时,相应的3D对象会被投影至一个利用xP和yP描述的2D位置,它是2D坐标系统的屏幕(投影平面)。
为了将这些值放入一个数学公式中,我将使用3D坐标系统作为世界坐标(world coordinate),其中,x坐标轴指向右方,y轴指向上方,而z轴正方向指向屏幕内部。 3D原点是指观看者眼睛的位置。 因此,屏幕的玻璃位于与z轴正交的(即直角)平面上,有时我将z轴称之为zProj。
你可以通过将世界位置(world position)xW和yW除以zW的方式计算出投影的位置xP和yP,如下所示:
xP = K1 * xW / zW yP = K2 * yW / zW
K1和K2是根据几何因子得到的常数,这些几何因子包括你的投影平面(你的视角)的宽高比以及你的眼睛的“视野”,并且它们考虑了广角视野的范围。
你能够看到该转换如何对透视进行模拟。 靠近屏幕两侧的点随着与眼睛之间距离(zW)的增加而逐渐被推向中央位置。 与此同时,靠近中央(0,0)的点受到与眼睛之间距离的影响较小并且仍保持靠近中央的位置。
这种由z轴进行的分割就是众所周知的“透视分割(perspective divide)”。
现在,假定3D场景中的某个对象被定义为一系列顶点。 因此,通过对所有几何图形的顶点应用该类型的转换,你可以有效地确保当对象远离眼点时,它会逐渐缩小。
在下一章节中,你能够将该透视投影规则应用到ActionScript中,而在你的Flash 3D项目中你可以使用这些ActionScript。
当你将这种透视分割技术从一个数学表述转换成代码时,你需要使用矩阵。
当你第一次使用透视投影和矩阵时,其过程非常棘手。 矩阵转换是一种线性转换:需要转换的矢量组件仅仅是输入矢量的线性组合。 而线性转换仅仅支持平移(translation)、旋转(rotation)、缩放(scaling)以及歪斜(skewing)。 然而它们却不支持透视分割(perspective divide)等操作,而透视分割是指一个组件分割另一个组件的操作。
现在,记住三维坐标通常由(x,y,z,w)形式的四维矢量来表示,其中w的值通常是1。 基于矩阵的针对透视分割问题的解决方案是以创新的方式使用第四维坐标w,这可以通过将zW坐标存储至已转换的矢量的w坐标来实现。
该已转换的矢量的其它组件均为xP和yP与zW的前乘形式。
你需要使用下面的转换:
xW -> xP' = xP * zW = K1 * xW yW -> yP' = yP * zW = K2 * yW
乍一看,由于你将投影到2D屏幕,因此仅仅计算投影坐标xP和yP似乎就已经足够。 但是,渲染管线(rendering pipeline)并不会完全对深度(z)坐标的跟踪,因为它需要对以渲染的不同像素进行深度排序(depth-sort)。 相应的标准过程涉及计算“投影”zP (zP’),当zW的值等于zNear时它的值等于0,而我们将zNear定义为“近剪切距离(near clipping distance)”。 近剪切距离(near clipping distance)正是我们希望渲染的最近的距离。 稍后,我将在本教程中更加详细地讨论剪切的概念,不过,下面是用于计算已转换的zP'的公式,它将用于剪切:
zW -> zP' = K3 * (zW - zNear)
使用线性矩阵转换完全能够实现整个转换过程,因为已转换矢量均是需要转换的世界矢量(world vector)的一个线性组合。
接下来,通过将转换的x、y、z组件除以w可以获得实际转换的xP和yP值,如下所示:
xP = K1 * xW yP = K2 * yW zP = K3 * (zW - zNear) / zW
上述所示的计算过程正是你在下面章节中需要建立的部分。
使用剪切空间和归一化设备坐标(Normalized Device Coordinates)
Stage3D期望你能够在你的Vertex Shader中使用一个能够将顶点转换为特殊空间的矩阵:
(x, y, z, w) = (xP', yP', zP', zW)
在按照上面方式对xP’、yP’、zP’和zW进行定义,并且选定常数K1、K2和K3之后,在3D世界中所有可见点xP和yP均落在区间(-1,1)之间,并且zP落在区间(0,1)之间。
这意味着某个落在屏幕右边缘的对象,一旦被投影之后,其xP将等于1,而落在屏幕左边缘的对象,其xP将等于-1。
我们称这一四维空间(xP’,yP’,zP’,zW)为剪切空间(clip space),因为它常常是剪切发生的区域。 (xP,yP,zP) 坐标在分割之后具有区间(-1,1)(用于xP和yP)和区间(0,1)(用于zP),我们将其称为归一化设备坐标(Normalized Device Coordinates (NDC))。
Stage3D和GPU使用来源于剪切空间表单中的Shader输出的数据,以便能够继续在内部使用透视分割技术。
剪切注意事项
位于“我们希望进行渲染的最近距离”的对象,即当zW = zNear时,其zP=0。 然而,那些位于较远距离(定义为zW = zFar)的对象则在NDC空间中被转换成zP=1。
zNear和zFar能够定义相应的剪切平面。 比zNear距离更近的对象将被剪切(不是拉伸),正如比zFar距离更远的对象一样。 此外,位于区间(-1,1)之外,坐标为xP和yP的对象也将被剪切。
为简单起见,我使用的点对象也位于此区间。 你可以对一个实际扩展的对象进行部分剪切,因为其中一部分落在视图之内,而其它的部分落在视图之外。
上述提到的用于获取xP、yP、zP的NDC区间的K1、K2和K3的正确值为:
K1 = zProj / aspect K2 = zProj K3 = zFar / (zFar – zNear)
在本范例中,aspect表示视口宽高比。
核查这些值很容易,因为这些落在投影平面(即zW = zProj)上的世界点(world point)(xW,yW,zW)和xP = (-1, 1)、yP = (-1, 1)
和zP = (0, 1)
的NDC区间对应xW = (-aspect, aspect)
和yW = (-1, 1)
的世界区间(world range)。 位于不同距离zW的世界点(world point)将根据透视投影公式来进行相应的缩放。 相似地,zP区间(0,1)对应世界区间(world range)zW = (zNear, zFar)
。
通常,根据fov(field of view,视野区域)角度(定义眼睛广角视野范围的角度),而非使用zProj距离来指定这些常数更为便捷。 图2给出了在投影参考系统的side视图中定义的fov 和zProj。
请使用下面的方法计算zProj的值:
zProj = 1 / tg (fov/2)
相应的缩放常数变为:
K1 = 1 / (aspect*tg(fov/2)) K2 = 1 / tg(fov/2) K3 = zFar / (zFar – zNear)
你可以使用由Adobe开发的Matrix3D类的一个简单扩展功能来帮助实现该过程。 下载PerspectiveMatrix3D类;在编写本文时,它已经很接近官方版本。
在下载该包之后,检查一下PerspectiveMatrix3D类。 它能够实现一些用来创建透视矩阵转换的简单函数,而本项目需要这些函数,它们包含常数K1,K2和K3的正确值:
PerspectiveMatrix3D::perspectiveFieldOfViewLH PerspectiveMatrix3D::perspectiveFieldOfViewRH
在本教程中,我使用世界坐标系统(world coordinate system),其中x指向右方,y指向上方,而z轴正方向指向屏幕,这是一个这是一个左手坐标系统(left handed coordinate system)。因此,我将使用LH风格的矩阵函数。
在使用PerspectiveMatrix3D之后,创建适合Stage3D的透视矩阵的过程将变得非常简单,因为它仅仅要求你定义少量参数。 例如,你可以使用下面的代码来设置必要变量的值:
var aspect:Number = 4/3; var zNear:Number = 0.1; var zFar:Number = 1000; var fov:Number = 45*Math.PI/180; var projectionTransform:PerspectiveMatrix3D = new PerspectiveMatrix3D(); projectionTransform.perspectiveFieldOfViewLH(fov, aspect, zNear, zFar);
如上所述,zNear和zFar 分别是近剪切平面和远剪切平面;aspect是宽高比,而fov则是视角区域。
在本章节中,你将使用矩阵来创建一个范例应用程序的简单更新版本,我已经在我之前的Developer Center教程的一篇标题称为Hello Triangle的文章中提供该应用程序。 如果在学习本教程之前你还没有完成相应的系列教程的阅读,那么请按照那些教程的指南来学习如何构建该范例项目的基础组件,而你将利用下面步骤对该范例项目进行功能扩展。
在本范例中,你将渲染一个矩形而非一个三角形,这样能够更易于观察到透视的效果。
首先,将投影矩阵附加(前乘)至用来定位和旋转对象的矩阵转换队列中。 此外,下面的代码还能够给这些旋转添加一个不同的自旋功能(spin),这样能够使得透视可见。
var m:Matrix3D = new Matrix3D(); m.appendRotation(getTimer()/30, Vector3D.Y_AXIS); m.appendRotation(getTimer()/10, Vector3D.X_AXIS); m.appendTranslation(0, 0, 2); m.append(projectionTransform);
下面是在透视投影演示应用程序中使用的完整代码范例:
public class PerspectiveProjection extends Sprite { [Embed( source = "RockSmooth.jpg" )] protected const TextureBitmap:Class; protected var context3D:Context3D; protected var vertexbuffer:VertexBuffer3D; protected var indexBuffer:IndexBuffer3D; protected var program:Program3D; protected var texture:Texture; protected var projectionTransform:PerspectiveMatrix3D; public function PerspectiveProjection() { stage.stage3Ds[0].addEventListener( Event.CONTEXT3D_CREATE, initMolehill ); stage.stage3Ds[0].requestContext3D(); stage.scaleMode = StageScaleMode.NO_SCALE; stage.align = StageAlign.TOP_LEFT; addEventListener(Event.ENTER_FRAME, onRender); } protected function initMolehill(e:Event):void { context3D = stage.stage3Ds[0].context3D; context3D.configureBackBuffer(800, 600, 1, true); var vertices:Vector.<Number> = Vector.<Number>([ -0.3,-0.3,0, 0, 0, // x, y, z, u, v -0.3, 0.3, 0, 0, 1, 0.3, 0.3, 0, 1, 1, 0.3, -0.3, 0, 1, 0]); // 4 vertices, of 5 Numbers each vertexbuffer = context3D.createVertexBuffer(4, 5); // offset 0, 4 vertices vertexbuffer.uploadFromVector(vertices, 0, 4); // total of 6 indices. 2 triangles by 3 vertices each indexBuffer = context3D.createIndexBuffer(6); // offset 0, count 6 indexBuffer.uploadFromVector (Vector.<uint>([0, 1, 2, 2, 3, 0]), 0, 6); var bitmap:Bitmap = new TextureBitmap(); texture = context3D.createTexture(bitmap.bitmapData.width, bitmap.bitmapData.height, Context3DTextureFormat.BGRA, false); texture.uploadFromBitmapData(bitmap.bitmapData); var vertexShaderAssembler : AGALMiniAssembler = new AGALMiniAssembler(); vertexShaderAssembler.assemble( Context3DProgramType.VERTEX, "m44 op, va0, vc0 " + // pos to clipspace "mov v0, va1" // copy uv ); var fragmentShaderAssembler : AGALMiniAssembler= new AGALMiniAssembler(); fragmentShaderAssembler.assemble( Context3DProgramType.FRAGMENT, "tex ft1, v0, fs0 <2d,linear,nomip> " + "mov oc, ft1" ); program = context3D.createProgram(); program.upload( vertexShaderAssembler.agalcode, fragmentShaderAssembler.agalcode); projectionTransform = new PerspectiveMatrix3D(); var aspect:Number = 4/3; var zNear:Number = 0.1; var zFar:Number = 1000; var fov:Number = 45*Math.PI/180; projectionTransform.perspectiveFieldOfViewLH(fov, aspect, zNear, zFar); } protected function onRender(e:Event):void { if ( !context3D ) return; context3D.clear ( 1, 1, 1, 1 ); // vertex position to attribute register 0 context3D.setVertexBufferAt (0, vertexbuffer, 0, Context3DVertexBufferFormat.FLOAT_3); // uv coordinates to attribute register 1 context3D.setVertexBufferAt(1, vertexbuffer, 3, Context3DVertexBufferFormat.FLOAT_2); // assign texture to texture sampler 0 context3D.setTextureAt(0, texture); // assign shader program context3D.setProgram(program); var m:Matrix3D = new Matrix3D(); m.appendRotation(getTimer()/30, Vector3D.Y_AXIS); m.appendRotation(getTimer()/10, Vector3D.X_AXIS); m.appendTranslation(0, 0, 2); m.append(projectionTransform); context3D.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, m, true); context3D.drawTriangles(indexBuffer); context3D.present(); } }
下一步阅读方向
在本教程中,你已经了解3D渲染中一个最重要话题:透视投影。 在你较深入了解透视技术以及如何在Stage3D中实现它之后,你可以继续学习本系列教程的下一教程。 在本系列教程的下一教程中,你将学习如何使用3D Camera并且了解如何在3D场景中实现一个运动的视点。