相机、渲染和场景

一般地讲,相机可表示为视见点并以此渲染场景,即3D空间内的一点,并以此产生场景视图,根据既定视角和视域,经捕捉后,将以像素形式光栅化至纹理中。此后,结合之前的渲染结果,渲染至屏幕上。

相机Gizmo

当相机在Scene选项卡中被选取,且开启Gizmo显示时,将根据某些属性,例如视域,显示视锥Gizmo。其中,可查看相机在场景中的位置,以及基于当前视图的相机视见内容。

Gizmo对于所选相机的定位十分重要,并可据此获得最佳的场景视图。但是,会想要在未被选择的相机中定位对象。此时用户需要在相机的视锥体内移动特定对象,并确保对象相对于该相机可见。默认条件下,当相机处于未选取状态时,通常并不会显示其视锥Gizmo。这意味着,当移动对象时,需要持续选取相机,以检测移动对象是否真正位于相机视锥体内,并在必要时调整其位置。需要提供稳定的视锥体Gizmo查看机制,即使当前相机未被选中。将以下代码作为相机的组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using UnityEngine;
using System.Collections;

[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]

public class DrawFrustumRefined : MonoBehaviour
{
private Camera Cam = null;
public bool ShowCamGizmo = true;

void Awake()
{
Cam = GetComponent<Camera>();
}

void OnDrawGizmos()
{
//Should we show qizmo?
if (!ShowCamGizmo) return;

//Get size (dimensions) of Game Tab
Vector2 v = DrawFrustumRefined.GetGameViewSize();

float GameAspect = v.x / v.y; //Calculate tab aspect ratio
float FinalAspect = GameAspect / Cam.aspect;

Matrix4x4 LocalToWorld = transform.localToWorldMatrix;
Matrix4x4 ScaleMatrix = Matrix4x4.Scale(new
Vector3(Cam.aspect * (Cam.rect.width / Cam.rect.height), FinalAspect, 1));

Gizmos.matrix = LocalToWorld * ScaleMatrix;
Gizmos.DrawFrustum(transform.position, Cam.fieldOfView,
Cam.nearClipPlane, Cam.farClipPlane, FinalAspect);
Gizmos.matrix = Matrix4x4.identity; //Reset gizmo matrix
}

//Function to get dimensions of game tab
public static Vector2 GetGameViewSize()
{
System.Type T = System.Type.GetType("UnityEditor.GameView, UnityEditor");
System.Reflection.MethodInfo GetSizeOfMainGameView =
T.GetMethod("GetSizeOfMainGameView", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
return (Vector2)GetSizeOfMainGameView.Invoke(nullnull);
}
}

  • Gizmos.DrawFrustum()函数接收世界场景空间内的参数(而非局部空间),例如位置和旋转状态,即全部位置参数须首先通过某一矩阵从局部空间转换至世界空间,并可通过Transform类的localToWorldMatrix成员予以实现。除此之外,还需进一步计算实际视口高度和宽度,以及游戏窗口宽度和高度之间的宽高比参数。

  • GetGameViewSize()函数返回2D向量,并以此表示Game选项卡视图的实际像素尺寸。该函数通过非正式发布的编辑器特性接收此类数据值。这里,请注意函数调用的“非正式”特性,代码的有效性很容易受到某些特性,甚至是维护性版本的影响。

下图显示了视锥体效果。

可见性

对象X是否相对于相机Y可见?对象X是否相对于任意相机均可见?或者,针对特定相机或任意相机,对象何时处于可见状态或不可见状态?如果相机Y移至位置Z,则对象X是否可见?需要根据全部的相机位置,并针对当前帧考察对象的可见性。了解对象(例如敌方角色)相对于相机的可见性有助于定义其运动行为和AI:当对象处于不可见状态时,某些行为和计算则无须进一步执行,进而可节省处理的载荷量。进一步讲,若知晓相机移动过程中对象的可见性,则可预测对象在下一帧中的可见性,进而提前进行准备。下面先讨论狭长场景中对象的可见性。

关于对象的可见性,存在两种主要概念,即视锥体和遮挡行为。具体而言,各种透视相机均包含了视锥体,该视锥体表示为梯形空间体,从相机镜头处向外扩展,并包含了视域定义的对应区域,以及剪裁面距离属性。实际上,视锥体从数学角度定义了相机的视野,即相机可潜在观察到的场景区域。这里,术语“潜在”是指,即使处于活动状态的可见对象位于相机视锥体内,但并不意味着相对于相机可见,其原因可解释为:某些视锥体内的对象可遮挡其中的其他对象,较近的对象可全部或部分遮挡或掩盖其后的对象。因此,实际的可见性测试至少涉及了两个处理步骤:首先,需要确定对象是否位于视锥体内;其次,还应进一步确定该对象是否被遮挡。仅当对象通过了上述两个测试,方可归类于相机可见对象中。随后,可见对象将被着色器或其他后处理操作进行渲染。

检测对象的可见性

最为简单、直接的对象可见性测试则是判断对象何时相对于相机可见。

针对包含渲染器组件的任意对象,两个伴随事件OnBecameVisible和OnBecamelnvisible将自动被调用,包括MeshRenderer和SkinnedMeshRenderer。

当然,空游戏对象并不会被调用,即使此类对象位于相机的视见范围内。从技术上讲,这一类对象并未包含任何可见成分,尽管其中的全部内容均实现了空间定位。此类事件的处理过程如示例代码所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using UnityEngine;
using System. Collections;

public class ViewTester : MonoBehaviour
{
void OnBecameVisible()
{
Debug.Log("Became visible");
}

void OnBecameInvisible()
{
Debug.Log("Became Invisible");
}
}

此处,需要注意OnBecameVisible和OnBecameInvisible事件。首先,这里所讨论的可见性仅表示对象位于相机视锥体内,可被其他较近的对象所遮挡,因而实际不一定可见。其次,事件应用于全部相机,而非某一特定相机。OnBecameVisible事件调用一次后表明,之前的不可见对象进入了相机(至少一部相机)的视锥体。类似地,OnBecamelnvisible事件调用一次后表明,之前的可见对象当前离开全部相机的视锥体。最后,此类函数还涵盖了场景相机的可见性。这也说明,如果用户利用Scene选项卡测试游戏,且对象在该选项卡中可见,则该对象即处于可见状态。简而言之,仅当用户行为依赖于场景中的全部可见性(或不可见)时, OnBecameVisible和OnBecamelnvisible方法将十分有用。其中,可见性仅对应于视锥体的呈现效果。换而言之,此类事件可用于对相关行为进行切换,例如与可见性相关的AI行为、NPC的恐慌行为,以及其他NPC的交互类型。

关于对象可见性的其他问题

除了测试对象何时进入/离开相机的可见性范围之外,另一项重要的检测则是对象与特定相机间的可见性。与OnBecame Visible和OnBecamelnvisible不同(当对象进入或驶离视锥体时,执行单次调用),该测试类型关注对象的当前状态,并假设对其之前的情况并不了解。对此,可采用OnWillRenderObject事件。该事件持续在某一对象上被调用,针对各可见相机每帧调用一次。这里,可见性是指“位于相机视锥体内部”。再次强调,其中并未涉及遮挡测试。在如下示例代码中,需要注意的是,该事件内部的Camera.current成员可用于获取指向(对象可见)相机的引用,同时也包括场景视见相机。

1
2
3
4
void OnWillRenderObject()
{
Debug.Log(Camera.current.name);
}

视锥体测试-渲染器

用户可能会简单地测试某一相机是否可看到渲染器;可见对象是否处于不可见状态;相机是否可查看到空间内的特定点:如果相机移至新位置,相机是否可观察到特定对象。此类内容均可视为不同环境下的重要可见性测试,且需要执行某种程度上的手动测试。

相关函数经过适当整合后将作为静态函数定义于CamUtility类中。下面首先创建一个函数,并测试特定渲染器是否位于特定Camera对象的视锥体内,如示例代码所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
using UnityEngine;
using System.Collections;

public class CamUtility
{
//Function to determine whether a renderer is within frustum of a specified camera
//Returns true if renderer is within frustum, else false
public static bool IsRendererInFrustum(Renderer Renderable, Camera Cam)
{
//Construct frustum planes from camera
//Each plane represents one wall of frustrum
Plane[] planes = GeometryUtility.CalculateFrustumPlanes(Cam);

//Test whether renderable is within frustum planes
return GeometryUtility.TestPlanesAABB(planes, Renderable.bounds);
}

//Function to determine whether a point in the scene is within frustum of a specified camera
//Returns true if point is within frustum, else false
//The out param ViewPortLoc defines the location of the point on screen, if function returns true
public static bool IsPointInFrustum(Vector3 Point, Camera Cam, out Vector3 ViewPortLoc)
{
//Create new bounds with no size
Bounds B = new Bounds(Point, Vector3.zero);

//Construct frustum planes from camera
//Each plane represents one wall of frustrum
Plane[] planes = GeometryUtility.CalculateFrustumPlanes(Cam);

//Test whether point is within frustum planes
bool IsVisible = GeometryUtility.TestPlanesAABB(planes, B);

//Assign viewport location
ViewPortLoc = Vector3.zero;

//If visible then get viewport location of point
if(IsVisible)
ViewPortLoc = Cam.WorldToViewportPoint(Point);

return IsVisible;
}

//Function to determine whether an object is visible (in frustum and has unbroken line to camera)
public static bool IsVisible(Renderer Renderable, Camera Cam)
{
//If in frustrum then cast line
if(CamUtility.IsRendererInFrustum(Renderable, Cam))
return !Physics.Linecast(Renderable.transform.position, Cam.transform.position); //Is direct line between camera and object?

return false; //No line found or not in frustum
}
}

GeometryUtility类用来生成平面对象数组,并以此描述相机的视锥体。相应地, 3D空间内的平面对应于2D空间内的直线,并定义了3D空间内的虚构延展表面。此处,视锥体平面表示为6个平面构成的集合,在3D空间内经过适当的旋转和对齐操作,进而表示为完整的梯形相机视锥体。随后,TestPlanesAABB函数、轴对齐包围盒(AABB)均会使用到该数组,并以此判断网格渲染器的碰撞包围体是否位于上述平面所定义的视锥体内部。

视锥体测试-点

当然,用户并非总是针对可见性测试渲染器。相反,某些时候需要对点进行测试,其中包含了两个原因。首先,用户需要了解某一对象,例如粒子或射击目标位置,是否真实可见。其次,用户不仅需要知晓一点是否可见,同时还需进一步确定该点在屏幕空间内的具体位置。对应结果通过相机进行渲染。示例代码5-5实现了这一功能,其中,代码测试了某一点是否位于相机视锥体内。若是,则返回该点在规范化视口空间内、屏幕上的渲染位置(位于0-1之间)。

视锥体测试-遮挡

如前所述,严格意义上的可见性的处理过程包含了两个阶段。当前,全部可见性测试仅检测了对象在相机视锥体内的呈现结果。通常情况下,该过程已然足够且应用范围广泛。但在视锥体内的诸多对象中,对象间可能存在彼此遮挡这一状况。例如,较远的对象可能会被较近的对象部分或全部遮挡。就自身而言,这并非绝对意义上的问题且时有发生。确定对象可见性的主要目标是,针对一组性能敏感的行为(例如AI行为),需要知晓相机与其是否足够近。该目标并非是真正意义上的可见性测试,而是一种距离计算。其中,重点内容并非是对象间的遮挡行为,而是对象是否位于视锥体内。尽管如此,某些时候依然需要对遮挡行为予以考虑。例如,当玩家查看特定对象时,将显示GUI元素或消息内容。期间,遮挡行为将变得较为重要-对象的GUI不应在墙外加以显示。某些时候,用户可通过碰撞器、触发器以及准确的对象定位避免此类行为;但在某些场合下,则只能通过遮挡测试剔除视锥体内的对象。视锥体中的对象遮挡测试占用了较大的性能开销,对此,较好的做法是使用相对简单的Physics.LineCast方法调用,并确定相机与目标对象间的虚构直线是否与碰撞器相交。通常,该方法工作良好,但读者也应对其局限性有所了解。首先,该方案假设全部可见对象均具有碰撞器,否则将无法通过LineCast方法进行检测。其次,碰撞器仅近似表达了网格包围体,且并未包含全部网格顶点。若网格包含内部孔洞,则LineCast方法将会失效。最后,包含透明材质的网格(可查看到其后方对象)通常无法正常调用LineCast方法。下面考察示例代码5-6.

相机前、后视觉

在某些游戏中,例如RTS游戏或休闲游戏,相机的远剪裁面并不十分重要-相机仅查看其前方事物。此时,当对象位于视锥体外部时,该对象仅在x和y平面中位于外部,而非局部2轴上。也就是说,隐藏对象之所以处于隐藏状态,只因相机未直接对其进行查看。然而,当对相机的方向进行适当调整时,对象的距离将被拉近,其不会超出远剪裁面。其中,可见性测试转化为简单、快速的方向测试。因此,问题从“对象是否位于视锥体内且未被遮挡”变为“对象位于相机的前方还是后方”。相应地,最终答案也有所变化,问题不再是可见性计算,而是方向计算,即相机和目标对象之间的方向关系:目标对象位于相机的前方或是后方。当对此进行测试时,可采用向量的点积计算。这里,点积接收两个向量作为参数,并将其简化为一维数值计算作为输出结果。该值描述了两个输入向量之间的角度关系。在示例代码5-7中, CamFieldView类与相机绑定,并检测相机是否可查看到目标对象,目标对象位于相机前方的既定视域内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using UnityEngine;
using System.Collections;

public class CamFieldView : MonoBehaviour
{
//Field of view (degrees) in which can see in front of us
//Measure in degrees from forward vector (left or right)
public float AngleView = 30.0f;

//Target object for seeing
public Transform Target = null;

//Local transform
private Transform ThisTransform = null;

// Use this for initialization
void Awake ()
{
//Get local transform
ThisTransform = transform;
}

// Update is called once per frame
void Update ()
{
//Update view between camera and target
Vector3 Forward = ThisTransform.forward.normalized;
Vector3 ToObject = (Target.position - ThisTransform.position).normalized;

//Get Dot Product
float DotProduct = Vector3.Dot(Forward, ToObject);
float Angle = DotProduct * 180f;

//Check within field of view
if(Angle >= 180f-AngleView)
Debug.Log ("Object can be seen");
}
}

正交相机

默认状态下, Unity中新创建的相机对象均设置为透视相机,当然,用户可对此进行调整。这一类相机模型接近于真实相机设备:在3D空间内具有一定的位置,配备了曲面透镜,并通过相关方法将捕捉到的图像转换至2D平面上,例如屏幕。这一类相机的特征主要体现在透视收缩方面,并应用于渲染对象上。特别地,随着距离的增加,渲染对象将随之变小。另外,随着与视见中心之间距离的不断增加,对象的形状和外观也将发生变化。与此同时,全部平行线相交于远处(地平线或辅助线)某一消失点。与透视相机相比,正交相机适合创建真正的2D等轴测游戏,而非等轴测仿制游戏。当采用正交相机时,其镜头展为一个平面且不再具备透视收缩特征,即平行线处于平行状态,对象不再随距离的变化而收缩,即使远离视见中心位置, 2D内容依然保持2D状态,等等。通过Object Inspector中的Projection类型设置,用户可方便地在Perspective和Orthographi之间进行切换,如图5-3所示。

在将Perspective类型转换为Orthographic之后,相机的视锥体也随之从梯形转换为盒体形状。其中,盒体内的全部内容均处于可见状态,且近距离对象将遮挡远处的对象。除此之外,全部深度信息均将丢失,如图5-4所示,因而此类相机适用于2D游戏。

当与正交相机协同工作时,核心问题是如何在世界单位(场景)与像素(屏幕)之间生成1:1的关系。在2D游戏和GUI中,正交相机可通过默认的正确尺寸(定义于纹理文件中)将图形显示于屏幕上。相比较而言,在大多数3D游戏中,纹理贴图、透视缩减以及透视关系意味着纹理将产生变形,并投影至3D对象表面上。即使在照片编辑软件中,对应内容也将以透视方式展示。对于2D游戏和精灵对象,情况则有所不同-图形多采用直视方式进行查看。因此,可通过默认尺寸和逐像素方式显示此类图像。这种显示方式称作完美像素,其中,纹理中的各个像素均以不变的方式显示于屏幕或游戏中,实际操作过程中则需要采用特殊的方案。简而言之,将1个世界单位映射为1个像素, Camera选项卡中的Size文本框需要将游戏的垂直分辨率设置为一半。因此,如果游戏在1024x768分辨率下运行,则Size应为364 (768 /2=364) ,如图5-5所示。

用户可直接在编辑器中设置Size值,但这仅在游戏分辨率不变的情况下有效。如果用户重新调整游戏窗口,并改变了游戏的分辨率,则需要在脚本中更新相机的尺寸,如示例代码5-8所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using UnityEngine;
using System.Collections;

[RequireComponent(typeof(Camera))] //Requires camera component to work
public class OrthoCam : MonoBehaviour
{
//private reference to camera component
private Camera Cam = null;

//Reference to Pixels to World Units Scale
public float PixelsToWorldUnits = 200f;

void Awake ()
{
//Get camera reference
Cam = GetComponent<Camera>();
}

void LateUpdate ()
{
//Update orthographic size
Cam.orthographicSize = Screen.height / 2f / PixelsToWorldUnits;
}
}

需要注意的是,第13行代码中添加了成员变量PixelsToworldUnits,并根据导入后的精灵对象纹理中的Pixels To Units缩放正交尺寸,如图5-6所示。当在屏幕上显示时,这可确保精灵对象以正确的像素尺寸予以显示。除此之外,全部精灵对象还将通过该值进行缩放,进而将纹理的像素尺寸映射至世界单位。

相机渲染和后处理

Unity相机和对象针对场景的渲染方式提供了较大的灵活性,且常见于后处理操作中。特别地,针对常规渲染之外的内容,其中涉及了辅助编辑和修正操作,包括模糊效果、色彩调整、鱼眼效果等。

将尝试构建一个相机变化系统,相机间呈现为平滑的淡入淡出效果。也就是说,相机间不会直接切换,而是通过渐变方式改变相机的景深。具体地讲,第一部相机的输出内容将以非透明方式逐渐切换,进而显示第二部相机的输出内容。下面将对此展开讨论。

图5-7所示场景项目包含了两个独立的区域。其中,各区域应绑定独立的相机对象,因而场景中设置了两部相机,且各相机组件应处于禁用状态--这可防止相机对其自身进行自动渲染。此处将通过手动方式渲染相机,进而整合源自各相机的渲染器,以实现二者间的淡入淡出效果。

各部相机均移除了AudioListener组件,其原因在于,任意时刻, Unity场景仅可包含一个处于活动状态的AudioListener。

随后,可在场景原点处创建第三部相机(标记为MainCamera),并设置空剔除遮罩,以确保相机处于活动状态,但不会对任何内容进行渲染。这定义为主场景相机,并整合了源自其他相机的渲染器,如图5-8所示。

当前场景包含了3部相机,其中,两部独立相机(相机x和相机Y)位于不同位置且处于禁用状态,一部主相机(相机Z)位于场景原点处。在此基础上,相机Z采用了示例代码5-9,并在按空格键时在相机x和相机Y之间实现渐变效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
//Class to fade from camera 0 to 1, and back from 1 to 0
//This class assumes there are only two scene cameras

using UnityEngine;
using System.Collections;

public class CameraFader : MonoBehaviour
{
//Reference to cameras (all cameras in the scene to be composited)
public Camera[] Cameras;

//Reference to camera color (color to multiply with render)
public Color[] CamCols = null;

//Fade in/out time in seconds (total time for a fade in one direction)
public float FadeTime = 2.0f;

//Final material to apply to render (Can be used to apply a shader to final rendered pixels)
public Material Mat = null;

void Start ()
{
//Assign render textures to each camera
foreach(Camera C in Cameras)
C.targetTexture = new RenderTexture(Screen.width, Screen.height, 24); //Create texture
}

//This function is called once per frame after the camera has
//finished rendering but before the render is shown
//It has a companion function OnPreRender (which is called before rendering)
void OnPostRender()
{
//Define screen rect
Rect ScreenRct = new Rect(0,0,Screen.width,Screen.height);

//Source Rect
Rect SourceRect = new Rect(0,1,1,-1);

//Render each camera to their target texture
for(int i = 0; i<Cameras.Length; i++)
{
//Render camera
Cameras[i].Render();

//Draw camera textures to screen using this camera
GL.PushMatrix();
GL.LoadPixelMatrix(); //Get pixel space matrix
Graphics.DrawTexture(ScreenRct, Cameras[i].targetTexture, SourceRect, 0,0,0,0, CamCols[i]); //Draws each camera as layer
GL.PopMatrix(); //Reset matrix
}
}

//This function is called afer OnPostRender
//And when final pixels are to be shown on screen
//src = current render from camera
//dst = texture to be shown on screen
void OnRenderImage(RenderTexture src, RenderTexture dst)
{
//Frame finished rendering, now push final pixels to screen with Mat applied (can apply custom shader here)
Graphics.Blit(src, dst, Mat);
}

//Function to lerp between color From to Color To over period TotalTime
//This function is used to fade alpha for topmost rendered camera CamCols[1]
public IEnumerator Fade(Color From, Color To, float TotalTime)
{
float ElapsedTime = 0f;

//Loop while total time is not met
while(ElapsedTime <= TotalTime)
{
//Update color
CamCols[1] = Color.Lerp(From, To, ElapsedTime/TotalTime);

//Wait until next frame
yield return null;

//Update Time
ElapsedTime += Time.deltaTime;
}

//Apply final color
CamCols[1] = Color.Lerp(From, To, 1f);
}

//Sample update function for testing camera functionality
//Press space bar to fade in and out between cameras
void Update()
{
//Fade camera in or out when space is pressed
if(Input.GetKeyDown(KeyCode.Space))
{
StopAllCoroutines();

//Should we fade out or in
if(CamCols[1].a <= 0f)
StartCoroutine(Fade(CamCols[1], new Color(0.5f,0.5f,0.5f,1f), FadeTime)); //Fade in
else
StartCoroutine(Fade(CamCols[1], new Color(0.5f,0.5f,0.5f,0f), FadeTime)); //Fade out
}
}
}

示例代码5-9中的部分解释内容如下所示。
口第011-020行代码:CamerFader类负责处理Camera[0]和Camera[1]之间的渐变效果,对此需要定义多个变量。Cameras数组维护一个相机列表,当前示例中包含了两部相机。另外, CamCols数组链接至Cameras上且定义了颜色值,并与相机的渲染器执行乘法运算,其中的Alpha值可使渲染器处于透明状态。变量FadeTime定义了相机某一方向上的淡入淡出时间(以秒计)。最后, Mat变量引用有效的材质并应用于源自主相机的渲染器上。也就是说,完整渲染器中的像素,包括其他相机照的全部内容。

口第023-038行代码:Start方法针对各个相机创建了RenderTexture,并将纹理赋予其TargetTexture成员中。实际上,各相机将赋予内部纹理,其渲染器于本地对其进行合成。

口第033-052行代码:针对场景中的活动相机对象, Unity自动调用OnPostRender事件,每帧调用一次,并在相机完成了其常规渲染后进行。此时,对象可在渲染完毕的数据上渲染其他相机或元素。这里, Cameras数组中各相机的Render方法将被调用,并采用手动方式将相机渲染至其渲染器纹理中,而非屏幕上。该纹理渲染结束后, Graphics.DrawTexture函数将各相机的RenderTexture按照数组中的顺序渲染至屏幕上,且依次叠加。需注意的是,各DrawTexture调用将纹理乘以CamCols颜色值,其中也涉及透明度中的Alpha分量。

口第059-063行代码:类似于OnPostRender事件, Unity也会在活动相机对象上自动调用OnRenderlmage事件,且每帧调用一次,其调用顺序位于OnPostRender之后,相机渲染器呈现于屏幕之前。该事件提供了两个参数,即src和dst,其中, src参数表示为指向渲染器纹理的引用,包含了源自相机的完整的渲染器,并表示为源自OnPostRender的输出结果。参数dst引用则定义了渲染器纹理,当OnRenderImage事件结束后将显示于屏幕上。简单地讲,该函数可通过代码以手动方式或者通过着色器编辑渲染器的像素。此处, Graphics.Blit函数将被调用,并将源数据复制至目标渲染器纹理中,并采用了与材质引用Mat关联的着色器。

口第067-085行代码: Fade表示为一个协同例程(CoRoutine) ,其中,颜色From在一段时间内(TotalTime)渐变为To颜色。该CoRoutine在0-1之间渐变相机颜色中的Alpha值,即从透明状态转变为不透明状态。
图5-9显示了淡入淡出相机效果。

相机震动

相机和动画

在相机漫游动画中,相机处于运动和旋转状态并途经多个特定位置,进而实现某种影视效果,其重要性主要体现于创建过场动画。相机漫游对于第三人称相机以及鸟瞰视野均十分有效。一种预定义方式是使用Unity中的动画编辑器,或者第三方工具,例如Maya、Blender或3ds Max。然而,某些时候,也需要通过编程方式对相机进行控制,进而通过手动方式调整其位置(偏离其平均中心位置),利用平滑的曲线运动方式,途经多个点并跟随特定的预置路径。本节将对此介绍3种实现方案。

或许,跟随相机是一种较为常见的方式,即相机跟随特定的场景对象,并在二者间保持一定的距离。这对于第三人称相机十分有用,例如RTS游戏中的过肩视角,以及鸟瞰视角。

对于此类相机,简单的跟随行为通常难以满足要求(例如简单地将相机作为当前对象的父对象)。通常情况下,相机运动应呈现某种平缓或阻尼效果,也就是说,速度衰减使得相机逐渐停止并到达目标处,而非相机全速运动过程中的夏然而止。对此,可使用Quaternion.Slerp和Vector3.SmoothDamp函数。示例代码中的类可与任何相机进行绑定,并实现对象的平滑跟随效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
using UnityEngine;
using System.Collections;

public class CamFollow : MonoBehaviour
{
// Follow target
public Transform Target = null;

// Reference to local transform
private Transform ThisTransform = null;

// Linear distance to maintain from target (in world units)
public float DistanceFromTarget = 10.0f;

// Height of camera above target
public float CamHeight = 1f;

// Damping for rotation
public float RotationDamp = 4f;

// Damping for position
public float PosDamp = 4f;

void Awake()
{
//Get transform for camera
ThisTransform = GetComponent<Transform>();
}

void LateUpdate()
{
// Get output velocity
Vector3 Velocity = Vector3.zero;

// Calculate rotation interpolate
ThisTransform.rotation = Quaternion.Slerp(ThisTransform.rotation, Target.rotation, RotationDamp * Time.deltaTime);

// Get new position
Vector3 Dest = ThisTransform.position = Vector3.SmoothDamp(ThisTransform.position, Target.position, ref Velocity, PosDamp * Time.deltaTime);

// Move away from target
ThisTransform.position = Dest - ThisTransform.forward * DistanceFromTarget;

// Set height
ThisTransform.position = new Vector3(ThisTransform.position.x, CamHeight, ThisTransform.position.z);

// Look at dest
ThisTransform.LookAt(Dest);
}
}

相机和曲线

对于场景切换、菜单背景以及简单的相机漫游效果,相机可采用直线运动方式。当然,在相机运动过程中也允许出现某些曲线运动,以及速度上的起伏。期间,相机以初始速度运动,在该过程中,速度缓慢下降并到达路径的终点。相应地,用户可通过Unity的动画编辑器使用预置脚本动画,或者使用动画曲线,后者提供了较大的灵活性,并可在操作过程中控制对象的转换。

当创建相机控制脚本并控制对象的速度和运动行为时,包括曲线运动以及平缓的阻尼效果,对应代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
using UnityEngine;
using System.Collections;

public class CameraMover : MonoBehaviour
{
// Total time for animation
public float TotalTime = 5.0f;

// Total Distance to move on each axis
public float TotalDistance = 30.0f;

// Curves for motion
public AnimationCurve XCurve;
public AnimationCurve YCurve;
public AnimationCurve ZCurve;

// Transform for this object
private Transform ThisTransform = null;

void Start()
{
// Get transform component
ThisTransform = GetComponent<Transform>();

// Start animation
StartCoroutine(PlayAnim());
}

public IEnumerator PlayAnim()
{
// Time that has passed since anim start
float TimeElapsed = 0.0f;

while(TimeElapsed < TotalTime)
{
// Get normalized time
float NormalTime= TimeElapsed / TotalTime;

// Sample graph for X Y and Z
Vector3 NewPos = ThisTransform.right.normalized * XCurve.Evaluate(NormalTime) * TotalDistance;

NewPos += ThisTransform.up.normalized * YCurve.Evaluate(NormalTime) * TotalDistance;

NewPos += ThisTransform.forward.normalized * ZCurve.Evaluate(NormalTime) * TotalDistance;

// Update position
ThisTransform.position = NewPos;

// Wait until next frame
yield return null;

// Update time
TimeElapsed += Time.deltaTime;
}
}
}

当使用CameraMover类时,可将脚本绑定至相机上,在Object Inspector中,可单击各个X、Y、Z曲线文本框,并标绘相机的距离和速度数据。通过单击Graph样板,用户可编辑图线,添加点并定义应用于该轴向上的运动行为。需要注意的是,X、Y、Z运动相对于对象的局部轴进行设置(前、上和右向),而非世界轴(x,y,z)。当体现动画数据的相关性时,这可实现运动的相对性,并提供对象的根级控制。

Unity并未提供可编程运动路径,这可使得GameObject(例如相机对象)通过球面插值平滑地跟随一条路径或样条。其中,路径定义为一系列的连接游戏对象。这一特性在实际应用过程中已有所体现,且相机运动行为可采用预置脚本动画定义,并在Unity的动画编辑器中创建。

对于相对灵活的运动路径可编程控制行为,路径可采用一系列的路点定义,并可通过代码对其进行调整。该功能十分有用,例如,在太空射击游戏中,敌方飞船的轨迹将遵循平滑的曲线路径,并可根据玩家飞船的位置对该路径进行调整。在Unity中,存在多种方式可实现这一效果,其中较为简洁、方便的方法是使用免费的扩展组件iTween,并可直接从Unity的Asset Store中下载并导入。

除了默认的iTween包之外,还可免费下载Visual iTween Path Editor。

待导入iTween包后,下一步则是据此创建路径上的动画对象。当采用漫游相机时,可将脚本iTweenPath拖曳至相机对象上。该脚本可生成独立的路径,其中包含了多个路点。

当定义路径上的多个路点时,可在Node Count中输入全部路点数量。随后可在Scene视口中选择各节点的Gizmo。需要注意的是,各点所绘制的曲线路径仅大致描述了相机的运动路径。

随后,可令相机在运行期内跟随该路径,并向相机添加如下代码。

1
2
3
4
5
6
7
8
9
10
using UnityEngine;
using System.Collections;

public class cam_itween_mover : MonoBehaviour
{
void Start()
{
iTween.MoveTo(gameObject, iTween.Hash("path", iTweenPath.GetPath("Camera Fly"), "time", 4f, "easetype", iTween.EaseType.easeInOutSine));
}
}

本章小结

本章主要讨论了与相机相关的多项任务。相机在Unity以及游戏引擎中不可或缺,并体现了场景相对于屏幕的渲染视角。

本章首先介绍了渲染中的Gizmo--即使在相机未被选择的情况下,也可在场景视口中渲染相机Gizmo。

随后讨论了对象与相机之间的可见方式,其中涉及了多种重要的测试行为,例如视锥体表达方式以及遮挡测试。

另外,本章还阐述了正交相机的创建和配置方法,并在无透视偏差的基础上渲染2D元素。

接下来本章考察了基于渲染器纹理的、相机渲染器的编辑和增强方式,并重新定义了一系列的相机事件,以及源自其他相机的混合渲染器,进而生成相机的淡入淡出效果。

与此同时,本章还探讨了高级相机运动行为的构建方式,例如相机的震动效果。

最后,本章考察了相机路径问题。其中,对应路径通过一系列的游戏对象路点加以定义;抑或简单地表示为所跟随的对象。

0%