开始学习计算机图形经典Real-Time Rendering啦,自己的一些学习心得和总结,以及书上的效果会慢慢总结并在unity上实现以下。这篇文章主要是对数据Real-Time Rendering前三章的总结,同时文章标题使用的该书第二章的标题。
渲染管线包括三部分,应用部分,几何部分,光栅化。如下图所示。
1.The Application Stage
顾名思义,开发者拥有对这个阶段完全控制的权力,因为这个阶段发生在CPU上。一般像Collision Detection(遮挡剔除)、动画变幻、视口裁剪等会由开发者在此阶段实现。从而优化后面阶段的工作量。CPU并不是并行架构,对于CPU来说处理分支等指令更有效率,但不适合执行大量相同的计算逻辑。所以一般在应用层面会在CPU处理以model为单位的各种逻辑或计算。(可能我这里表述并不准确,我的理解举例在unity中,是以一个Render为最小单位的)。并将通过第一阶段的model需要计算的顶点、三角形顶点序列、法线、贴图以及其他相关参数传入第二阶段。
2.The Geometry Stage
第二各阶段开始处理多边形和顶点级别的数据。在这里又被细分几个阶段,如下图所示。
通常在unity的渲染流程中,我们在vertex shading阶段获取到当前顶点的模型坐标。我们会将它转换到世界坐标系下做各种顶点相关的操作。转换过程即Unity中的转换矩阵M(UNITY_MATRIX_M)。即使我们不需要处理每个顶点。我们仍需做此操作并在之后再将顶点转换到当前摄像机的坐标系下。即Unity中的转换矩阵V(UNITY_MATRIX_V)。最后我们需要对顶点进行投影以及裁剪。即Unity中的转换矩阵P(UNITY_MATRIX_P)。
上述皆为我对unity的渲染工作流的理解,也是unity在顶点着色器过程中最常说的mvp变幻。但是片元着色阶段接受的是可映射屏幕的范围域。根据不同的图形标准会将顶点映射到不同的域中。opengl标准中会被映射到[-1, 1]的闭区间,而dx标准中会被映射到[0, 1]的闭区间。而上述过程显然并没有发生映射过程,即最后顶点在clip coordinate(裁剪坐标系)的坐标的x、y、z并没有除w。我猜测在顶点着色器后unity会自动帮我们做这件事,然后再将屏幕映射后的坐标传给后面的渲染管线。
基础几何图元如点、线、三角、多边形几何体只有完全在视野内或部分在视野内,才应该进入接下来的光栅化处理阶段。举个例子,一条线段一个顶点在视锥体外面一个顶点在视锥体内,我们会保留视锥体内的顶点,并将线段与视锥体相交处的点作为新顶点取代在视锥体外部的顶点。
3.The Rasterizer Stage
光栅化阶段同样被划分出几个阶段,如下图所示。
在TriangleSetup和TriangleTraversal阶段中。三角形面的不同和一些其他的信息被计算后,每个三角形面所覆盖的像素点被检测并产生一个片元。这个过程通常被称为Triangle Traversal or Scan Conversion。每个三角形片元的属性在三个顶点间使用内插值生成。
接下来进入到片元着色阶段。此阶段便是Pixel Shader(片元着色器发挥的时候)。
每个像素的信息被存放在Color Buffer中。通过一系列计算最终生成颜色值输出到Back Buffer,这个缓冲区的意义是为了防止人们看到正在被光栅化和送向屏幕的图元。一旦屏幕图像在Back Buffer中渲染完毕,就会与之前显示在屏幕上的Front Buffer交换。
GPU缓冲区当然不止上述部分。现代GPU还有诸如用于检测深度的Z-Buffer,Stencil Buffer等缓存,一般在渲染系统中他们被统称为Frame Buffer。
接下来我们重点总结一下Shader部分的内容。随着近些年渲染管线和Shader的发展。Shader Model标准从1.0发展到4.0,现在一般的设备都不再支持1.0。这里列出其他几个版本间的差别。
在Shader Model4.0中定义了一个新的可编程阶段 :The Geometry Shader。一般在这个shader中可以裁剪或生成新的图元。同时还有一个流式输出的概念,术语为Stream Output。貌似做流动效果的时候比较有用。
在光栅化的片元着色阶段,最后输出时有一个MRT的技术。可以并行渲染多个目标。我猜测Unity中使用RenderTexture实现各种后处理特效的过程就在此处进行。
在光栅化的合成阶段中,大家熟知的ZTest,Blend等会发生。在书中我看到这样一句话,使用ZTest后,当一个片元没有通过ZTest被舍弃,整个渲染管线的自动优化都会失效。不知道这是否意味着在Unity中对半透的处理上,Blend的性能是否强于CutOff。当然这应该要分画面复杂度。当复杂度非常高时,使用Transparent导致OverDraw的直线上升性能损耗应该是更高的。
第三章最后介绍了一个NVIDIA's FX的特效的一部分,称为Gooch Shading。内容和实现非常简单。主要实现了随着光源与物体位置的不同,物体身上的色调从冷色调到暖色调的变化。下面简单说下Unity的效果实现。
Shader部分:需要定义两个Color表示冷色调和暖色调的具体颜色值,一个向量表示光源在世界空间下的坐标,一个主贴图。然后在顶点着色器中传入uv,世界空间下的法向量,世界空间下顶点到光源位置的方向,经过MVP后的顶点坐标。然后在片元着色器中通过点乘法向量方向和顶点到光源的方向得到一个-1到1的值。将该值变化到0-1的区间插值冷暖色调,最后与主贴图采样的颜色相乘即可。
变量和结构体定义部分:
sampler2D _MainTex;
fixed4 _WarmColor;
fixed4 _CoolColor;
float3 _Light_Pos;
struct a2v {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 light_vec : TEXCOORD1;
float3 world_normal : TEXCOORD2;
};
顶点着色器部分:
v2f vert (a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
float4 vertexInWorld = mul(unity_ObjectToWorld, v.vertex);
o.light_vec = (_Light_Pos.xyz - vertexInWorld);
o.world_normal = UnityObjectToWorldNormal(v.normal);
return o;
}
片元着色器部分:
fixed4 frag (v2f i) : Color {
fixed4 color = tex2D(_MainTex, i.uv);
float3 light_vec = normalize(i.light_vec);
float3 world_normal = normalize(i.world_normal);
float ldn = dot(light_vec, world_normal);
float mixer = 0.5 * (ldn + 1.0);
fixed4 res = lerp(_CoolColor, _WarmColor, mixer);
return res * color;
}
最后还需要一个脚本传入光源位置,我直接就用unity提供的Directional Light来表示光的位置了。仅仅表示光的位置而已。代码逻辑很简单,每帧告诉Shader当前位置即可。
public class LightPosPara : MonoBehaviour
{
private Transform mTrans;
void Awake() {
mTrans = transform;
}
void Update()
{
Shader.SetGlobalVector("_Light_Pos", mTrans.position);
}
}
源码地址:https://github.com/guishengshi/Real-Time-Rendering。
在工程中Scene/Chapter3文件夹可找到GoochShading.unity场景文件。这个就是上述实例了。