Unity 渲染教程(二):着色器基础

原文出处:http://gad.qq.com/program/translateview/7173930


这是关于渲染基础的系列教程的第二部分。这个渲染基础的系列教程的第一部分是有关矩阵的内容。在这篇文章中我们将编写我们的第一个着色器代码并导入纹理。


这个系列教程是使用Unity 5.4.0开发的,这个版本目前还是开放测试版本。我使用的是build 5.4.0b10版本。

Unity 渲染教程(二):着色器基础
对球使用纹理。


1. 默认的场景

当你在Unity中创建新的场景的时候,你将使用默认的相机和定向的光源。  通过GameObject / 3D Object / Sphere这个菜单项来创建一个简单的球体,然后把这个简单的球体放在原点,并把相机放在它的前面。

Unity 渲染教程(二):着色器基础
默认场景中的默认球体。

这是一个非常简单的场景,但是已经有很多复杂的渲染内容了。为了更好地抓住渲染过程,摆脱所有奇怪的东西是非常有帮助的,首先让我们只关心渲染的基础部分。


1.1 剥离那些和渲染无关的内容

通过菜单项Window / Lighting来查看场景的光照设置。 这将打开一个具有三个选项卡的光照窗口。我们只对默认情况下处于激活状态的“场景”选项卡感兴趣。

Unity 渲染教程(二):着色器基础
默认的光照设置。

这是一个关于环境光照的部分,你可以在其中选择天空盒。这个天空盒目前用于场景背景、环境光照和反射。让我们将其设置为none进行关闭。

在你进行设置的时候,你还可以关闭预计算和实时全局光照的面板。我们不会很快使用到这些东西。

Unity 渲染教程(二):着色器基础
不再使用天空盒了。

在没有天空盒的情况下,环境光源会自动切换为纯色。环境光源的默认颜色为深灰色,具有非常浅的蓝色色调。而反射变为纯黑色,如警告框所示。

正如你可能期望的那样,球体会变得更暗,背景会是纯色。但是,得到的结果却是背景是深蓝色。那么这个颜色来自哪里?

Unity 渲染教程(二):着色器基础
简化后的光照

背景颜色是根据摄像机来定义的。它在默认情况下会渲染天空盒,但是它也会回落到纯色状态。

Unity 渲染教程(二):着色器基础
默认的相机设置。

 为什么背景颜色的透明通道值为5而不是255?

要进一步简化渲染的话,请取消方向光源对象的激活或将其删除。这将摆脱场景中的直接光照,以及由直接光照所投射的阴影。剩下的就是背景,会用环境颜色显示球体的轮廓。

Unity 渲染教程(二):着色器基础
球体处于黑暗之中。



2. 从物体到二维图像

我们这个非常简单的场景是用两个步骤绘制出来的。 首先,图像用相机的背景颜色进行填充。然后将我们的球体的轮廓绘制在填充颜色的上面。

Unity怎么知道它必须画一个球体? 我们有一个球体对象,这个对象有一个网格渲染器组件。如果此对象位于相机的视图内,那么就应该出现在最终的图像中。 Unity通过检查对象的包围盒否与相机的视锥体相交来验证这一点。


Unity 渲染教程(二):着色器基础
默认的球体。

变换组件用于改变网格和包围盒的位置、方向和大小。实际上,整个变换层次都会被用到,正如第1部分“矩阵”中所描述的那样。如果对象会出现在相机的视图中,则这个物体会被安排进行渲染。

最后,图形处理器负责渲染对象的网格。 特定的渲染指令由对象的材质定义。 材质引用了着色器 - 这是一个图形处理器程序,加上它可能有的任何设置。

Unity 渲染教程(二):着色器基础
每个组件控制着渲染哪些内容。

我们的对象目前有默认材质,它使用Unity的标准着色器。我们要用我们自己的着色器来代替它,我们将从头开始构建它。



2.1 你的第一个着色器程序

通过Assets / Create / Shader / Unlit Shader创建一个新的着色器,并且将它命名为类似My First Shader这样的名字。

Unity 渲染教程(二):着色器基础
你的第一个着色器程序。

打开着色器文件并删除其内容,所以我们可以从头开始。

着色器代码用Shader关键字定义。它后面是一个字符串,描述可用于选择此着色器的着色器菜单项。它不需要匹配文件名。然后跟着的是填充了着色器内容的块。
1
2
3
Shader "Custom/My First Shader" {
 
}
保存文件。 你将收到不支持这个着色器的警告,因为它没有子着色器或是备选着色器。这是因为它是空的缘故。

虽然这个着色器没有什么功能,但是我们可以将它分配给一个材质。因此,通过Assets / Create / Material来创建一个新材质,并从着色器菜单中选择我们的着色器。

Unity 渲染教程(二):着色器基础 Unity 渲染教程(二):着色器基础
使用了你的着色器的材质。

更改我们的球体对象,使我们的球体对象使用我们自己的材质,而不是默认材质。 球体将变为洋红色。发生这种情况是因为Unity会切换到一个错误的着色器,它使用这种颜色来引起你对问题的注意。

Unity 渲染教程(二):着色器基础 Unity 渲染教程(二):着色器基础
使用了你的着色器的材质。

着色器的错误提示信息中提到子着色器。你可以使用这些子着色器将多个着色器变量组合在一起。这允许你为不同的构建平台或者LOD值不同的情况下提供不同的子着色器。让我们举个简单的例子来说,你可以为桌面电脑上运行的那个应用使用一个子着色器,而为移动设备上运行的应用使用另一个子着色器。
1
2
3
4
5
6
Shader "Custom/My First Shader" {
 
    SubShader {
         
    }
}
子着色器里面必须包含至少一个通道。着色器通道是对象实际被渲染的地方。 我们将使用一个通道,但着色器里面可以有多个。具有多个通道意味着对象被多次渲染,这是很多效果所需要的。
1
2
3
4
5
6
7
8
9
Shader "Custom/My First Shader" {
 
    SubShader {
 
        Pass {
 
        }
    }
}
我们的球体现在可能变成白色,因为我们使用的是一个空通道的默认行为。如果发生这种情况,这意味着我们不再收到任何着色器错误的提示信息。但是,你可能仍然在控制台中看到旧的错误提示信息。编辑器倾向于坚持提示错误信息,因为当着色器重新编译而没有错误的时候,这些错误提示信息是不会被清除的。

Unity 渲染教程(二):着色器基础
一个白色的球体。



2.2 着色器程序

现在是时候来编写我们自己的着色器程序了。我们用Unity的着色语言来做这个功能,这是HLSL和CG着色语言的变体。我们必须用CGPROGRAM关键字指示我们的代码开始。我们必须以ENDCG关键字来指示我们的代码终止。
1
2
3
4
5
Pass {
    CGPROGRAM
 
    ENDCG
}

着色器编译器现在会发出警告,警告我们的着色器里面没有顶点程序和片段程序。着色器由两个程序组成,也就是顶点程序和片段程序。顶点程序负责处理网格的顶点数据。 这包括从对象空间到显示空间的转换,就像我们在第1部分“矩阵”中所做的那样。片段程序负责对位于网格三角形内的单个像素进行渲染。

Unity 渲染教程(二):着色器基础
顶点程序和片段程序。

 我们必须通过pragma指令告诉编译器使用哪些程序。
1
2
3
4
5
6
CGPROGRAM
 
#pragma vertex MyVertexProgram
#pragma fragment MyFragmentProgram
 
ENDCG

编译器会再次发出警告,这次因为它找不到我们指定的程序。这是因为我们还没有定义这些程序的缘故。

定义顶点程序和片段程序就像定义方法一样,非常像C#里面的做法,虽然它们通常被称为函数。让我们简单地创建两个空的返回void的方法,并给它们适当的名称。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
CGPROGRAM
 
#pragma vertex MyVertexProgram
#pragma fragment MyFragmentProgram
 
void MyVertexProgram () {
 
}
 
void MyFragmentProgram () {
 
}
 
ENDCG
此时,着色器将编译,球体将消失。或者你仍然会得到错误信息提示。这取决于你的编辑器使用的是哪个渲染平台。如果你使用的是Direct3D 9渲染平台,你可能会得到错误信息提示。



2.3 着色器的编译

Unity的着色器编译器接受我们的代码,并将其转换为不同的程序,具体如何转换取决于目标平台。不同的平台需要不同的解决方案。 例如,如果是Windows 平台的话,需要的是Direct3D,如果是Mac平台的话,需要的是OpenGL ,如果是移动平台的话,需要的是OpenGL ES,等等。 我们不是在这里处理单个编译器,而是处理多个编译器。

你最终使用哪个编译器取决于你的定位。由于这些编译器并不相同,因此每个平台可能会产生不同的结果。举个简单的例子来说,我们的空程序使用OpenGL和Direct3D 11的话,就能正常工作,但在如果使用的是Direct3D 9,就会失败。

在编辑器中选择着色器,并查看检查器窗口。它会显示有关着色器的一些信息,包括当前的编译器错误。还有一个带有“编译和显示代码”按钮和下拉菜单的“编译代码”项。 如果单击“编译和显示代码”按钮,Unity将编译着色器代码并在编辑器中打开着色器代码的输出,因此你可以检查生成的代码具体是什么。

Unity 渲染教程(二):着色器基础
着色器检查器,会显示在所有平台上的错误信息。

 你可以通过下拉菜单手动选择编译着色器的平台。默认情况下是使用编辑器所使用的图形设备进行编译。你可以手动选择其他平台进行编译,无论是你当前的构建平台,还是你有许可证的所有平台,或是其他的自定义选择。这使你能够快速确保你的着色器在多个平台上能够正常的编译,而不必进行完整的构建。

Unity 渲染教程(二):着色器基础
选择OpenGLCore。

要编译所选的程序,请关闭弹出窗口,然后单击“编译并显示代码”按钮。单击弹出窗口中的小的“显示”按钮将显示使用的着色器变量,但是这在现在没有用。

举个简单的例子来说明,这里当我们的着色器是为OpenGlCore平台编译的时候得到的代码。
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
// Compiled shader for custom platforms, uncompressed size: 0.5KB
 
// Skipping shader variants that would not be included into build of current scene.
 
Shader "Custom/My First Shader" {
SubShader {
 Pass {
  GpuProgramID 16807
Program "vp" {
SubProgram "glcore " {
"#ifdef VERTEX
#version 150
#extension GL_ARB_explicit_attrib_location : require
#extension GL_ARB_shader_bit_encoding : enable
void main()
{
    return;
}
#endif
#ifdef FRAGMENT
#version 150
#extension GL_ARB_explicit_attrib_location : require
#extension GL_ARB_shader_bit_encoding : enable
void main()
{
    return;
}
#endif
"
}
}
Program "fp" {
SubProgram "glcore " {
"// shader disassembly not supported on glcore"
}
}
 }
}
}
 生成的代码被分割为两个块,vp和fp,分别用于顶点程序和片段程序。然而,在OpenGL的情况下,两个程序都在vp块中。两个主要函数对应于两个我们的空方法。 所以让我们关注这两个主要函数并忽略其他代码。
1
2
3
4
5
6
7
8
9
10
11
12
#ifdef VERTEX
void main()
{
    return;
}
#endif
#ifdef FRAGMENT
void main()
{
    return;
}
#endif
 这里是为Direct3D 11生成的代码,让我们只剥离出那些有趣的部分。它看起来很不同,但很明显,代码没有做太多的工作。
1
2
3
4
5
6
7
8
9
10
11
12
Program "vp" {
SubProgram "d3d11 " {
      vs_4_0
   0: ret
}
}
Program "fp" {
SubProgram "d3d11 " {
      ps_4_0
   0: ret
}
}
当我们处理我们的程序时,我会经常显示OpenGL Core和D3D11平台的编译代码,所以就可以对具体内部发生了什么有一个比较明确的认识。



2.4 导入其他文件

要生成具有功能的着色器代码,你会需要很多模板代码。比如定义公共变量、函数和其他东西的代码。 如果这是一个C#程序的话,我们会将这些代码放在其他的类中。但是着色器没有类的概念。它们只是一个包含所有代码的大文件,没有类或命名空间提供的分组功能。

幸运的是,我们可以将代码拆分成多个文件。你可以使用#include指令将不同文件的内容加载到当前文件中。一个典型的文件包括UnityCG.cginc,所以让我们这样做一下看看效果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CGPROGRAM
 
#pragma vertex MyVertexProgram
#pragma fragment MyFragmentProgram
 
#include "UnityCG.cginc"
 
void MyVertexProgram () {
 
}
 
void MyFragmentProgram () {
 
}
 
ENDCG
 UnityCG.cginc是与Unity捆绑在一起的着色器导入文件之一。它包括一些其他必要的文件,并包含一些通用的功能。

Unity 渲染教程(二):着色器基础
导入文件的层次结构,从UnityCG开始。

UnityShaderVariables.cginc 定义了渲染所需要的一大堆着色器变量,例如变换、相机和光照数据。 这些都是在需要的情况下,在Unity编辑器里面进行设置的。

HLSLSupport.cginc 设置的是那些无论你的目标平台是什么,你都可以使用相同的代码的东西。所以你不需要担心使用特定于某个平台的数据类型等事情。

UnityInstancing.cginc 是专门用于实例化支持的,这是一种减少绘制调用的特定渲染技术。虽然它不直接导入文件,它取决于UnityShaderVariables的信息。

请注意,这些文件的内容被有效地复制到你自己的文件中,替换了导入指令。这个过程发生在预处理步骤的期间,预处理步骤会执行所有的预处理指示。这些指令都是以哈希开头的语句,例如#include和#pragma。在预处理步骤完成之后,着色器代码被再次处理,并且被实际编译。




2.5  生成输出

为了渲染某些东西,我们的着色器程序必须能够输出结果。顶点程序必须返回顶点的最终坐标。 一共会有多少个坐标? 四个,因为我们使用的是4×4变换矩阵,正如这个系列的第1部分《矩阵》中所描述的那样。

将函数的类型从void更改为float4。float4只是四个浮点数的集合。但是现在,让我们只返回0。
1
2
3
float4 MyVertexProgram () {
    return 0;
}
我们现在得到的错误信息提示是关于缺少语义。着色器的编译器看到我们返回一个四个浮点数的集合,但是它不知道这个四个浮点数的集合代表着什么。所以它不知道图形处理器应该用它做什么。我们必须非常具体地了解我们的程序的输出。
在这种情况下,我们试图输出顶点的位置。 我们必须通过将SV_POSITION语义附加到我们的方法来指明这一点。 SV表示系统值,而POSITION表示最终顶点位置。
1
2
3
float4 MyVertexProgram () : SV_POSITION {
    return 0;
}
片段程序应该输出一个像素的RGBA颜色值。 我们可以使用float4 类型。返回0将为这个像素使用一个固定的颜色。
1
2
3
float4 MyFragmentProgram () {
    return 0;
}
片段程序也需要语义。 在这种情况下,我们必须指出最终的颜色应该写在哪里。 我们使用SV_TARGET,它是默认的着色器目标,也就是帧缓冲区,其中包含着我们正在生成的图像。
1
2
3
float4 MyFragmentProgram () : SV_TARGET {
    return 0;
}
 但是,等等,顶点程序的输出会被用作片段程序的输入。这表明片段程序应该得到一个与顶点程序的输出相匹配的参数。
1
2
3
float4 MyFragmentProgram (float4 position) : SV_TARGET {
    return 0;
}
 不管我们给参数起什么名字,我们都必须确保使用正确的语义。
1
2
3
4
5
float4 MyFragmentProgram (
    float4 position : SV_POSITION
) : SV_TARGET {
    return 0;
}
我们的着色器再次编译,没有错误信息提示,但是球体消失了。这不应该令人感到惊讶的,因为我们将球体所有的顶点折叠到了一个点。

如果你看下编译后的OpenGL核心程序,你会看到他们现在在写入输出值。我们的单颜色值确实已被四分量向量所代替。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifdef VERTEX
void main()
{
    gl_Position = vec4(0.0, 0.0, 0.0, 0.0);
    return;
}
#endif
#ifdef FRAGMENT
layout(location = 0) out vec4 SV_TARGET0;
void main()
{
    SV_TARGET0 = vec4(0.0, 0.0, 0.0, 0.0);
    return;
}
#endif
 D3D11程序也是如此,尽管语法是不同的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Program "vp" {
SubProgram "d3d11 " {
      vs_4_0
      dcl_output_siv o0.xyzw, position
   0: mov o0.xyzw, l(0,0,0,0)
   1: ret
}
}
Program "fp" {
SubProgram "d3d11 " {
      ps_4_0
      dcl_output o0.xyzw
   0: mov o0.xyzw, l(0,0,0,0)
   1: ret
}
}



2.6  对顶点进行变换

为了能够让我们的球再次显示出来,我们的顶点程序必须产生一个正确的顶点位置。 为此,我们需要知道顶点在物体空间中的位置。我们可以通过向我们的函数添加一个带POSITION语义的变量来访问它。然后将该位置提供为齐次坐标的形式  ,所以它的类型是float4。
1
2
3
float4 MyVertexProgram (float4 position : POSITION) : SV_POSITION {
    return 0;
}
 让我们从直接返回这个位置信息开始。
1
2
3
float4 MyVertexProgram (float4 position : POSITION) : SV_POSITION {
    return position;
}
 编译后的顶点程序现在有一个顶点的输入信息并将其复制到其输出里面去。
1
2
3
4
5
6
in  vec4 in_POSITION0;
void main()
{
    gl_Position = in_POSITION0;
    return;
}
1
2
3
4
5
6
Bind "vertex" Vertex
      vs_4_0
      dcl_input v0.xyzw
      dcl_output_siv o0.xyzw, position
   0: mov o0.xyzw, v0.xyzw
   1: ret

Unity 渲染教程(二):着色器基础
原始顶点位置。

黑色球体将变得可见,但它的位置被扭曲。这是因为我们使用的是物体空间的位置,我们把球在物体空间的位置当做了球的显示位置。因此,移动球体将在视觉上不会产生差别。

我们必须将原始的顶点位置乘以模型-视图-投影矩阵。模型-视图-投影矩阵将对象的变换层次与相机变换和投影相结合,就像我们在这个系列的第1部分《矩阵》中做的那样。

4 x 4的模型-视图-投影矩阵在UnityShaderVariables中被定义为UNITY_MATRIX_MVP。我们可以使用mul函数将它与顶点的位置相乘。这将把我们的球体正确地投影到显示器上去。你还可以移动、旋转和缩放它,并且图像将按照预期改变。
1
2
3
float4 MyVertexProgram (float4 position : POSITION) : SV_POSITION {
    return mul(UNITY_MATRIX_MVP, position);
}

Unity 渲染教程(二):着色器基础
位置投影正确的球体。

 如果你检查OpenGLCore平台上编译出来的顶点程序,你会注意到一个统一的变量突然出现在代码里面。即使它们没有被代码使用并且将被忽略,访问矩阵这个事情触发了编译器将整个块都导入进来了。

 你还将看到矩阵乘法被编码为一堆乘法和加法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
uniform     vec4 _Time;
uniform     vec4 _SinTime;
uniform     vec4 _CosTime;
uniform     vec4 unity_DeltaTime;
uniform     vec3 _WorldSpaceCameraPos;
in  vec4 in_POSITION0;
vec4 t0;
void main()
{
    t0 = in_POSITION0.yyyy * glstate_matrix_mvp[1];
    t0 = glstate_matrix_mvp[0] * in_POSITION0.xxxx + t0;
    t0 = glstate_matrix_mvp[2] * in_POSITION0.zzzz + t0;
    gl_Position = glstate_matrix_mvp[3] * in_POSITION0.wwww + t0;
    return;
}
 D3D11的编译器不会包含未使用的变量。 它用一个mul和三个mad指令对矩阵乘法进行编码。mad指令表示加法之后紧跟着乘法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Bind "vertex" Vertex
ConstBuffer "UnityPerDraw" 352
Matrix 0 [glstate_matrix_mvp]
BindCB  "UnityPerDraw" 0
      vs_4_0
      dcl_constantbuffer cb0[4], immediateIndexed
      dcl_input v0.xyzw
      dcl_output_siv o0.xyzw, position
      dcl_temps 1
   0: mul r0.xyzw, v0.yyyy, cb0[1].xyzw
   1: mad r0.xyzw, cb0[0].xyzw, v0.xxxx, r0.xyzw
   2: mad r0.xyzw, cb0[2].xyzw, v0.zzzz, r0.xyzw
   3: mad o0.xyzw, cb0[3].xyzw, v0.wwww, r0.xyzw
   4: ret



3.  给像素颜色

现在我们得到了正确的形状,让我们来添加一些颜色。最简单的是使用固定的颜色,例如黄色。
1
2
3
4
5
float4 MyFragmentProgram (
    float4 position : SV_POSITION
) : SV_TARGET {
    return float4(1, 1, 0, 1);
}

Unity 渲染教程(二):着色器基础
黄颜色的球体。

 当然,你不总是想要黄色的对象。在理想情况下,我们的着色器将支持任何的颜色。然后,你可以使用该材质配置你要应用的颜色。这是通过着色器的属性完成的。




3.1 着色器的属性

着色器属性在单独的块中声明。让我们将它添加到着色器代码的顶部。
1
2
3
4
5
6
7
8
9
Shader "Custom/My First Shader" {
 
    Properties {
    }
 
    SubShader {
        
    }
}
 在新的块中放入一个名为_Tint的属性。你可以给它任何名称,但通常的约定是以下划线开始,后面跟一个大写字母,然后是小写字母。这么做是确保没有什么别的地方会使用这个名字,以防止意外的重复名称。
1
2
3
Properties {
    _Tint
}
 属性的名称后面必须跟着一个字符串和一个类型,放在圆括号中,就像调用一个方法一样。该字符串用于标记材质检查器中的属性。 在这种情况下,类型是isColor。
1
2
3
Properties {
    _Tint ("Tint", Color)
}
 属性声明的最后一部分是给一个默认值赋值。让我们将这个默认值设置为白色。
1
2
3
Properties {
    _Tint ("Tint", Color) = (1, 1, 1, 1)
}
 我们的tint属性不应该出现在我们着色器检查器的属性部分。

Unity 渲染教程(二):着色器基础
着色器属性。

 当你选择材质的时候,你将看到新的Tint属性,被设置为白色。你可以将Tint属性更改为任何你喜欢的颜色,比如说是绿色。

Unity 渲染教程(二):着色器基础
 材质的属性。




3.2 访问属性

 要实际使用属性,我们向着色器代码添加了一个变量。它的名称必须完全匹配属性名称,因此它的名称将是_Tint。然后我们可以在我们的片段程序中简单地返回这个变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
#include "UnityCG.cginc"
 
float4 _Tint;
 
float4 MyVertexProgram (float4 position : POSITION) : SV_POSITION {
    return mul(UNITY_MATRIX_MVP, position);
}
 
float4 MyFragmentProgram (
    float4 position : SV_POSITION
) : SV_TARGET {
    return _Tint;
}
请注意,变量必须在使用之前进行定义。虽然你可以改变C#类中的字段和方法的顺序,这在C#中没有问题,但是对于着色器不是这样的。着色器的编译器是从上到下工作的。它不会往后看一下。

编译号的片段程序现在包括tint变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
uniform     vec4 _Time;
uniform     vec4 _SinTime;
uniform     vec4 _CosTime;
uniform     vec4 unity_DeltaTime;
uniform     vec3 _WorldSpaceCameraPos;
uniform     vec4 _Tint;
layout(location = 0) out vec4 SV_TARGET0;
void main()
{
    SV_TARGET0 = _Tint;
    return;
}
1
2
3
4
5
6
7
8
ConstBuffer "$Globals" 112
Vector 96 [_Tint]
BindCB  "$Globals" 0
      ps_4_0
      dcl_constantbuffer cb0[7], immediateIndexed
      dcl_output o0.xyzw
   0: mov o0.xyzw, cb0[6].xyzw
   1: ret

Unity 渲染教程(二):着色器基础
颜色为绿色的球。




3.3 从顶点程序传递到片段程序

到目前为止,我们给了所有的像素相同的颜色,但是这是相当受限的情况。通常情况下,顶点数据起着很大的作用。举个例子来说,我们可以将位置解释为颜色。然而,变换后的位置不是非常有用的。因此,让我们使用网格中的局部位置信息作为颜色。我们该如何将额外的数据从顶点程序传递给片段程序?

GPU通过光栅化三角形来创建图像。它需要三个经过处理的顶点并在它们之间进行插值。对于由三角形覆盖的每个像素,它会调用片段程序,并传递内插值后的数据。

Unity 渲染教程(二):着色器基础
对顶点数据进行插值。

因此,顶点程序的输出并不是直接用作片段程序的输入。插值过程位于两者之间。 在这个图里面SV_POSITION数据被进行内插值,但是其他数据也可以进行内插值。

要访问插值后的局部位置信息,请向片段程序中添加参数。 因为我们只需要X、Y和Z组件,我们用float3就足够了。然后我们可以输出位置信息,就像它是一种颜色一样。我们必须提供第四个颜色分量,可以简单的只保留为1。
1
2
3
4
5
6
float4 MyFragmentProgram (
    float4 position : SV_POSITION,
    float3 localPosition
) : SV_TARGET {
    return float4(localPosition, 1);
}
 再次提醒下,我们必须使用语义来告诉编译器该如何解释这些数据。这一次我们将使用TEXCOORD0。
1
2
3
4
5
6
float4 MyFragmentProgram (
    float4 position : SV_POSITION,
    float3 localPosition : TEXCOORD0
) : SV_TARGET {
    return float4(localPosition, 1);
}
 编译好的片段着色器现在将使用内插值后的数据而不是使用统一的颜色。
1
2
3
4
5
6
7
8
in  vec3 vs_TEXCOORD0;
layout(location = 0) out vec4 SV_TARGET0;
void main()
{
    SV_TARGET0.xyz = vs_TEXCOORD0.xyz;
    SV_TARGET0.w = 1.0;
    return;
}
1
2
3
4
5
6
  ps_4_0
   dcl_input_ps linear v0.xyz
   dcl_output o0.xyzw
0: mov o0.xyz, v0.xyzx
1: mov o0.w, l(1.000000)
2: ret
 当然顶点程序必须输出局部位置信息才能正常工作。我们可以通过向它添加一个输出参数,使用相同的TEXCOORD0语义来做到这一点。 顶点和片段函数的参数名称不需要匹配。这是通过语义进行匹配的。
1
2
3
4
5
6
float4 MyVertexProgram (
    float4 position : POSITION,
    out float3 localPosition : TEXCOORD0
) : SV_POSITION {
    return mul(UNITY_MATRIX_MVP, position);
}
 要通过顶点程序传递数据,从位置数据localPosition里面复制X、Y和Z分量。
1
2
3
4
5
6
7
float4 MyVertexProgram (
    float4 position : POSITION,
    out float3 localPosition : TEXCOORD0
) : SV_POSITION {
    localPosition = position.xyz;
    return mul(UNITY_MATRIX_MVP, position);
}
 额外的顶点程序输出包含在编译器着色器中,这样我们将看到我们的球体被正确的渲染。
1
2
3
4
5
6
7
8
9
10
11
12
in  vec4 in_POSITION0;
out vec3 vs_TEXCOORD0;
vec4 t0;
void main()
{
    t0 = in_POSITION0.yyyy * glstate_matrix_mvp[1];
    t0 = glstate_matrix_mvp[0] * in_POSITION0.xxxx + t0;
    t0 = glstate_matrix_mvp[2] * in_POSITION0.zzzz + t0;
    gl_Position = glstate_matrix_mvp[3] * in_POSITION0.wwww + t0;
    vs_TEXCOORD0.xyz = in_POSITION0.xyz;
    return;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Bind "vertex" Vertex
ConstBuffer "UnityPerDraw" 352
Matrix 0 [glstate_matrix_mvp]
BindCB  "UnityPerDraw" 0
      vs_4_0
      dcl_constantbuffer cb0[4], immediateIndexed
      dcl_input v0.xyzw
      dcl_output_siv o0.xyzw, position
      dcl_output o1.xyz
      dcl_temps 1
   0: mul r0.xyzw, v0.yyyy, cb0[1].xyzw
   1: mad r0.xyzw, cb0[0].xyzw, v0.xxxx, r0.xyzw
   2: mad r0.xyzw, cb0[2].xyzw, v0.zzzz, r0.xyzw
   3: mad o0.xyzw, cb0[3].xyzw, v0.wwww, r0.xyzw
   4: mov o1.xyz, v0.xyzx
   5: ret

Unity 渲染教程(二):着色器基础
将局部位置信息解释为颜色。




3.4 使用结构

你是否认为我们的程序的参数列表看起来很乱?其实它只会变得更糟,因为我们要在顶点程序和片段程序之间传递越来越多的数据。因为顶点程序的输出应该匹配片段程序的输入,所以如果我们可以在一个地方定义参数列表将是非常方便的。 幸运的是,我们可以这样做。

我们可以定义数据结构,它们只是变量的集合。除了语法有点不同以外,它们类似于C#中的结构。这里是一个结构体,定义了我们正在内插值的数据。请注意要在定义后使用分号。
1
2
3
4
struct Interpolators {
    float4 position : SV_POSITION;
    float3 localPosition : TEXCOORD0;
};
 使用这个结构来让我们的代码更整洁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
float4 _Tint;
 
struct Interpolators {
    float4 position : SV_POSITION;
    float3 localPosition : TEXCOORD0;
};
 
Interpolators MyVertexProgram (float4 position : POSITION) {
    Interpolators i;
    i.localPosition = position.xyz;
    i.position = mul(UNITY_MATRIX_MVP, position);
    return i;
}
 
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
    return float4(i.localPosition, 1);
}




3.5 调整颜色
因为颜色值为负的情况被修正为零,我们的球体在渲染的时候看起来相当的暗。 因为默认球体在物体空间中的半径为1/2,所以颜色通道的值最终位于-1/2和1/2之间。 我们想将它们移动到0-1范围,我们可以通过给所有通道的值加上1/2来改变这一点。
相关文章
相关标签/搜索