[翻译]XNA 3.0 Game Programming Recipes之five
PS:自己翻译的,转载请著明出处
2-7 使相机飞行
问题
在3D世界里你想平滑的从一个位置移动到另一个位置。当然,在过渡的过程中你也想相机的目标也平滑的改变他的位置。
详细点就是,你想要一种机制,将您的相机在光滑曲线的起始位置到被定义的目标位置。这一运动应该顺利开始和结束。要能够拿起相机经常使用,并没有任何故障用它作为第一人称的相机。
解决方案
这一过程将需要一个时间变量,这将是0当相机是在其起始位置1当相机已经达到了目的地的位置。
在中间操作过程中指定第三个你想要的相机位置, 你可以定义一个Bezier曲线。Bezier曲线是平滑曲线穿越开始和结束的位置,并会聚集在开始和结束点之间的非常近的任何额外点指定的点,这些点就是像这样的第三位置。给予这样的Bezier曲线的当前时间变量之间的值在0和1之间 , Bezier曲线在指定时间里将返回曲线上的点的位置。
目标的位置,将是起点和目标之间的线性插值。因此,该相机将在曲线的起始位置到目的地的位置,同时,其目标的行动路线是连接起始位置和最终目标位置。
利用这个第一人称的摄像机的源代码,你不能简单地改变摄象机目标的位置,因为它基于目标的位置和朝UP和RIGHT轴后的旋转来计算出目标的当前位置。
在你一切都准备好后,顺利的开始和结束的过程得益于使用MathHelper.SmoothStep这个方法。
注意 您可以使用Curve类列入的XNA框架脚相机复杂的路径,但它尚未成熟,(或过于复杂,如果您非要这么做),用于相机飞行,因为它错过一些细微的功能。例如,类不支持3D点,并希望您能手动设置切线向量的每一个Curve级的关键点。总之,本节唯一一部分是可取代Curve功能的是贝塞尔方法。这节其他百分之九十的也有何用,如果您想使用XNA框架的Curve功能代替这个贝塞尔办法。
如何工作的
为了让相机做贝塞尔飞行,你需要跟踪这些变量
2 Vector3 bezStartPositon;
3 Vector3 bezMidPosition;
4 Vector3 bezEndPosition;
5 Vector3 bezStartTarget;
6 Vector3 bezEndTarget;
在你的飞行过程中,你需要跟踪一些位置,如开始和结束的位置和目标的位置,同时你也需要计算一些中间的点这样能得到一条光滑的曲线。在飞行开始后这些都将会被指定。
在下面的代码中,你可以找到的方法,引发飞行行动。所有这些需要相机和目标开始和结束时的位置:
2 {
3 bezStartPosition=startPosition;
4 bezEndPosition=endPosition;
5 bezMidPosition=(bezStartPosition+bezEndPosition)/2.0f;
6 Vector3 midShiftDirecton=new Vector3(1,1,0)*2;
7 Vector3 cameraDirection=endPosition-startPosition;
8 Vector3 perpDirection=Vector3.Cross(upV,cameraDirection);
9 perpDirection.Normalize();
10 Vector3 midShiftDirection=new Vector3(0,1,0)+perpDirection;
11 bezMidPosition+=cameraDirection.Length()*midShiftDirection;
12 bezStartTarget=startTarget;
13 bezEndTarget=endTarget;
14 bezTime=0.0f;
15 }
所有的startPosition,endPosition,startTarget和endTarget的值都被存储在全局变量中。
midPosition是在起始点和终点额外加的点为了使曲线弯曲,这些都需要被计算。在起始位置和结束位置这些都要被第一精确的计算。偏离曲线一点边,你会改变这个位置,远离开始和最后相机直线之间的位置增加了垂直方向的线。
你可以找到方向垂直的两个方向采十字交叉法。在这种情况下,你想要垂直于相机方向的方向和向上的方向。首先计算相机方向的直线,然后你可以找到别的方向:采取的最后位置和减去原来的位置。然后你把跨乘积的方向和行动的方向,并找到垂直这两个方向的方向。
如果你想移动这个方向的中间点,这些点彼此接近你就可以得到一条从起始点到终点之间的完美曲线。如果他们之间的距离过大,
你的曲线就会肥大。解决这个问题可以把起始和结束之间的移动相乘。这个距离就是开始和终点之间的长度。
最后,设置bezTime变量为0说明飞行开始了。
一旦飞行被初始化,调用UpdateBezier方法来更新每一祯。
2 {
3 bezTime+=0.01f;
4 if(bezTime>1.0f)
5 return;
6 Vector3 newCamPos=Bezier(bezStartPosition,bezMidPosition,BezEndPosition,bezTime);
7 Vector3 newCamTarget=Vector3.Lerp(bezStartTarget,bezEndTarget,bezTime);
8 float updownRot;
9 float leftrightRot;
10 AngleFromDirection(newCamTarget-newCamPos,out updownRot,out leftrightRot);
11 fpsCam.UpDownRot=updownRot;
12 fpsCam.LeftRightRot=leftrightRot;
13 fpsCam.Position=newCamPos;
14 }
这种方法开始递增反略有进展.如果值大于1,飞行结束然后从方法中返回。
否则,下一行这一章节所包含的将继续执行。首先,你相机的下一个位置靠bezTime这个参数从Bezier曲线中获得,这个参数表明这个飞行后的距离。其次,你在起始目标和结束目标之间发现插入其中的当前目标。在5-9中可以查阅更多信息。
如果你想使用第一人称视角摄象机,需要计算updownRot和leftrightRot的值,它们被AnglesFromDirection方法调用。这种方法需要在三个参数:相机的方向 将面临(如果您的摄像机的位置减去的目标位置,您取得 由于观察方向)和两个你想知道的角度。通过他们的“传出” 参数,这方法将改变它的值,这个值将被存储到这个方法的变量中去。最后,用新的旋转和位置来更新你的相机信息。
贝塞尔曲线
第一行调用贝塞尔方法,它返回曲线上的位置, 基于bezTime参数,这将始终包含一个介于0和1的值 。功能需求三点来定义的曲线,但你已经在计算这些用InitBezier方法
贝塞尔曲线靠3个点来定义,用下面的函数你可以用任何时间给予的3个点:
当然看上去很困难,但实际不是这样。让我们来看看开始给的,
在这种情况下t会是bezTime的值。接下来的方法计算上面的函数式:
2 {
3 float invTime=1.0f=time;
4 float timePow=(float)Math.Pow(time,2);
5 float invTimePow=(float)Math.Pow(invTime,2);
6 Vector3 result=startPoint*invTimePow;
7 result+=2*minPoint*time*invTime;
8 result+=endPoint*timePow;
9 return result;
10 }
invTime在这个公式中是1-t,所以invTimePow是(1-t)(1-t).输出结果变量,返回调用代码的位置。
寻找旋转
当你再看看过去的代码行的UpdateBeizer方法,就可以看到下一相机的位置来计算贝塞尔方法和使用的下一个目标简单的线性插值(见5-9) 。
这将是最简单的飞行动作,因为您可以立即创建一个视景矩阵从目标和位置中(见2-1 ) 。但在飞行已经完成, 要能够拿起相机向右运动的飞行离开了摄像机。这个位置是自动储存的,但会改变旋转变量。所以,如果你想得到相机的位置和目标位置你只能计算找到相应的leftrightRotation和updownRotation,您可以存储这些所以他们对你的第一人称相机非常有用的代码。此外,这将使第一人称相机使相机的飞机飞离开,没有产生一个单一的故障。
这正是是由AnglesFromDirection方法实现的。 摄像头的方向正面临和计算UpdownRot和leftrightRot的值。两者值传递给该方法为“传出”的参数,这意味着将改变通过回到调用代码,因此改变了方法使这些变化都存储在变量的调用方法。
2 {
3 Vector3 floorProjection=new Vector3(direction.X,0,direction.Z);
4 float directionLength=floorProjection,Length();
5 updownAngle=(float)Math.Atan2(direction.Y,directionLength);
6 leftrightAngle=-(float)Math.Atan2(diection.X,-direction.Z);
7 }
4-7节详细的解释了怎么样调整一个旋转角度对适应一个方法。这种情况下,需要找到2个角,因为这个方向是在3D世界里的。开始寻找leftrightRot角度。图2-5展示了XZ坐标面包涵相机和目标。对角线是摄象机面对的方向,X和Z轴线是X和Z的向量组成成分。在90度的直角,如果你想找到corner的角度,所有您需要做的是对面的角采取反正切侧,除以旁边的角落最短的线段。这种情况是X轴被Z所分。Atan2的函数功能允许指定两个值,因为这消除了一个含糊不清划分问题。这就是如何找到leftrightRot角度。
为了找updownAngle,使用图2-6。虚线所示的是摄象机面向的方向。你想找出Y轴正向和XZ底面的所成的角度。所以你传递它们到
Antan2这个方法中,你就会得到updownAngle
用法
确保是在Update方法中调用UpdateBezier方法
UpdateBezier();
现在你所要做的是调用InitBezier方法使飞行开始。
2 InitBezier(new Vector3(0,10,0),new Vector3(0,0,0),new Vector3(0,0,-20),new Vector3(0,0,-10));
当您想将您的飞行与您的用户的摄像头形成整体,您就可以开始从您的摄象机和目标的位置入手:
2 InitBezier(fpsCam.Position,fpsCam.Position+fpsCam.Forward*10.0f,new Vector3(0,0,-20),new Vector3(0,0,-10));
我把目标布置在开始的位置前面的10个单位。这就容易得到一个顺畅的结果,否则相机在开时跟踪的目标将不得不作出尖锐。
顺利启动和加速
通过bezTime从0到1的固定数额的速度的不断增加,相机移动的Bezier曲线是一个常数。这将导致一个相当不合适的
开始和结束。你真的想要的是有bezTime首次从0到0.2缓慢上升 然后突然加到0.8,在0.8到1之间又突然降低。这正是MathHelper.SmoothStep做的:你给它一个不断增加的值在0和1之间,它将返回一个值0和1之间的值顺利启动和结束。
的唯一地方,这是你需要的UpdateBezier方法,以便取代中间两行的代码:
2 Vector3 newCamPos=Bezier(bezStartPosition,bezMidPosition,bezEndPosition,smoothValue);
3 Vector3 newCamTarget=Vector3.Lerp(bezStarTarget,bezEndTarget,smoothValue);
这个smoothValue变量将存有0到1之间的光滑的值,这个参数将传递到这个方法中,而不是不断增加bezTime变量。在飞行结束时解决错误/快速相机的旋转
这部分的节解决了一个问题,可能会出现在某些情况下的。这类情况之一是在2-7的左部分的图,那里的摄象机的路径的位置和曲线的目标是如上面所示。开始时,相机朝向左边,直到穿过曲线。在这点上,相机将会突然的转变,它朝向右边!你可以纠正这个情况靠简单改变曲线上的中间点朝向左,而不是向右。如右部分所示的曲线,相机开始时面朝右并没有变成向左。
你怎么知道中间的点会在哪一边?图2-8上面2副图,2种情况。虚线表示的目标移动路径,实线表示的是相机的路径。
在这2种情况下,中间的点应该移动到另外的方向。这可以探测到两十字之间的方向。在这两种情况下,将导致向量垂直于平面;然而,在一种情况下,它将点,以及其他情况下,它会指向down.Let的猜想它是指向向上,把它叫做upVector 。下一步,如果你一个跨产品的upVector和相机的方向,你将会得到一个向量是
垂直方向的摄像头和upVector。这个方向的最终向量取决于是否upVector指向上升或下降,而这又取决于方法中的位置和目标路径互相穿过。因为它是垂直的位置路径和upVector,它可以被用来立即转移的中点。
因此,使用此代码来找到正确的中点的曲线,在InitBezier方法:
2 Vector3 cameraDirection=endPosition-startPosition;
3 Vector3 targDirection=endTarget-startTarget;
4 Vector3 upVector=Vector3.Cross(new Vector3(targDirection.X,0,targDirection.Z),new Vector3(cameraDirection.X,0,cameraDirection.Z));
5 Vector3 perpDirection=Vector3.Cross(upVector,cameraDirection);
6 perpDirection.Normalize();
upVector是被乘积的位置和目标的路径来判断出的,双方预计在XZ平面的Y部分设置为0 (这样,你会得到相同的线所显示的图片)您找到方向垂直于这个upVector和路径的位置,其中采取交叉的乘积。
当你发现垂直的方向,你可以向那个方向移动中间的点,就向之前所做的那样:
2 bezMidPosition+=cameraDirection.Length()*midShiftDirection;
在某种情况下不能成功使用。如果targDirection和cameraDirection是彼此平行的,或者upVector和cameraDirection是彼此平行的,这个十字方法会失败,原因是perpDirection变量变为(0,0,0),这将导致错误当您正常化这一向量。所以,你会发现这种情况下处理问题被设置了一个任意值:
2 perpDirection=new Vector3(0,1,0);
把这个代码的在perpDirection行前正常化
代码
2 private void InitBezier(Vector3 startPosition,Vector3 startTarget,Vector3 endPosition,Vector3 endTarget)
3 {
4 bezStartPosition=startPosition;
5 bezEndPosition=endPosition;
6 bezMidPosition=(bezStartPosition+bezEndPosition)/2.0f;
7 Vector3 cameraDirection=endPosition-startPosition;
8 Vector3 targDirection=endTarget-startTarget;
9 Vector3 upVector=Vector3.Cross(new Vector3(targDirection.X,0,targDirection.Z),new Vector3(cameraDirection.X,0,cameraDirection.Z));
10 Vector3 perpDirection=Vector3.Cross(upVector,cameraDirection);
11 if(perpDirection==new Vector3())
12 perpDirection==new Vector3(0,1,0);
13 perpDirection.Normalize();
14 Vector3 midShifDirection=new Vector3(0,1,0)+perpDirection;
15 bezMidPosition+=cameraDirection.Length()*midShiftDirection;
16 bezStartTarget=startTarget;
17 bezEndTarget=endTarget;
18 bezTime=0.0f;
19 }//执行时,UpdateBezier应该被每一祯调用为了计算相机的新位置和旋转:
20 private void UpdateBezier()
21 {
22 bezTime+=0.01f;
23 if(bezTime>0.1f)
24 return;
25 float smoothValue=MathHelper.SmoothStep(0,1,bezTime);
26 Vector3 newCamPos=Bezier(bezStarPosition,bezMidPosition,bezEndPosition,smoothValue);
27 Vector3 newCamTarget=Vector3.Lerp(bezStartTarget,bezEndTarget,smoothValue);
28 float updownRot;
29 float leftrightRot;
30 AnglesFromDirection(newCameTarget-newCamPos,out updownRot,out leftrightRot);
31 fpsCam.UpDownRot=updownRot;
32 fpsCam.LeftRightRot=leftrightRot;
33 fpsCam.Position=newCamPos;
34 }
35 private Vector3 Bezier(Vector3 startPoint,Vector3 midPoint,Vector3 endPoint,float time)
36 {
37 float invTime=1.0f-time;
38 float timePow=(float)Math.Pow(time,2);
39 float invTimePow=(float)Math.Pow(invTime,2);
40 Vector3 result=startPoint*invTimePow;
41 result+=2*midPoint*time*invTime;
42 result+=endPoint*timePow;
43 return result;
44 }//下面是关于旋转的方法
45 private Vector3 Bezier(Vector3 startPoint,Vector3 midPoint,Vector3 endPoint,float time)
46 {
47 float invTime=1.0f-time;
48 float timePow=(float)Math.Pow(time,2);
49 float invTimePow=(float)Math.Pow(invTime,2);
50 Vector3 result=startPoint*invTimePow;
51 result+=2*midPoint*time*invTime;
52 result+=endPoint*timePow;
53 result result;
54 }