
·本专栏文章记录笔者阅读学习《Unity Shader入门精要》的感想与笔记,方便日后复习与查找
一、回顾Unity Shader的基本结构
包含Shader名字、Properties属性、SubShader、FallBack等语义块

二、顶点着色器与片元着色器
1.我的第一个Unity Shader
Shader "Unlit/SImple Shader"
{
SubShader{
Pass{
CGPROGRAM
#pragma vertex vert //编译指令
#pragma fragment frag
float4 vert(float4 v : POSITION) : SV_POSITION{
return UnityObjectToClipPos(v);
}
fixed4 frag() : SV_Target{
return fixed4(1,1,1,1);
}
ENDCG
}
}
FallBack "Diffuse"
}
1.1.编译指令
#pragma vertex vert
#pragma frament frag
都是编译指令,用于告诉Unity,哪一个函数对应是顶点/片元着色器
1.2.数据来源
- 模型顶点的坐标数据来自POSITION语义
- 模型顶点法线的数据来自NORMAL语义
- 模型第一套纹理数据来自TEXCOORD0语义(是的,还可以有好几套)
当我们需要同时往顶点着色器中传入这些数据的时候,我们可以使用一个结构体,来包含所有这些需要输入的数据(被语义修饰的变量,Unity会自动填入相关的数据)
2.顶点着色器与片元着色器的通信
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unlit/SImple Shader"
{
SubShader{
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct a2v{
float4 vertx : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
//这个结构体用来定义从顶点着色器到片段着色器的输出
struct v2f{
//SV_POSITION告诉Unity裁剪空间下的顶点位置存放在哪个变量里
float4 pos : SV_POSITION;
//COLOR可以用来存储颜色
fixed3 col : COLOR;
};
v2f vert(a2v v){
//声明输出的结构
v2f o;
o.pos = UnityObjectToClipPos(v.vertx);
o.col = v.normal * 0.5 + 0.5; //将法线从[-1,1]映射到[0,1]
return o;
}
fixed4 frag(v2f i) : SV_Target{
return fixed4(i.col, 1); //将颜色输出到屏幕上
}
ENDCG
}
}
FallBack "Diffuse"
}

顶点着色器一般负责输出【顶点变换后的坐标】、【顶点法线数据】、【纹理坐标】等传递给片元着色器
通常要输出这么多数据的时候,我们也是通过一个结构体来进行存储
这个结构体中也需要包含相关的语义
- SV_POSITION表示裁剪空间中的顶点坐标(这个非常重要,必须包含!)
- COLOR0只是一个普通变量,可有可无,用处由用户自己处理
这个着色器的效果如图所示:

3.使用属性
步骤:
①在Properties语句块中声明这个属性变量
②在CGPROGRAM下再定义一个和①中名字和类型都相同的变量

这个表格列出了ShaderLab属性类型与CG变量类型的匹配关系
三、Unity内置文件的函数与变量
1.内置文件的位置
有两种办法查看这些内置文件
方法一:官网下载

Unity 2022.x Unity官方下载_Unity新版_从Unity Hub下载安装 | Unity中国官网
可以在官网去下载这些文件来进行查看

解压后可以看到非常多的这些我们在UnityShader中可以使用的内置文件
方法二:Unity应用程序文件中查找

一般是在 你的Unity安装的位置/Data/CGIncludes 中
CGPROGRAM
#include "UnityCG.cginc" //这样引入使用了
ENDCG
2.常用的内置文件与变量
| 文件名 | 描述 |
|---|---|
| UnityCGcginc | 包含了最常用的帮助函数、宏和结构体等 |
| UnityShaderVariables.cginc | 在编译UnityShader的时候,会被自动包含进来,里面有很多内置的全局变量比如UNITY_MATRIX_MVP等 |
| Lighting.cginc | 包含了各种内置的光照模型,如果编写的是Surface Shader的话,会自动包含进来 |
| HLSLSupport.cginc | 在编译Unity Shader的时候,会被自动包含进来,生命力很多用于跨平台编译的宏和定义 |
| 名称 | 描述 | 包含的变量 |
|---|---|---|
| appdata_base | 可用于顶点着色器的输入 | 顶点位置、顶点法线、第一组纹理坐标 |
| appdata_tan | 可用于顶点着色器的输入 | 顶点位置、顶点法线、顶点切线、第一组纹理坐标 |
| appdata_full | 可用于顶点着色器的输入 | 顶点位置、顶点法线、顶点切线、四组(更多)纹理坐标 |
| appdata_img | 可用于顶点着色器的输入 | 顶点位置、第一组纹理坐标 |
| v2f_img | 可用于顶点着色器的输出 | 裁剪空间中的位置、纹理坐标 |
| 函数名 | 描述 |
|---|---|
| float3 WorldSpaceViewDir(float4 v) | 输入一个模型空间中的顶点位置,返回世界空间中从该点到摄像机的观察方向 |
| float3 ObjSpaceViewDir(float4 v) | 输入一个模型空间中的顶点位置,返回模型空间中从该点到摄像机的观察方向 |
| float3 WorldSpaceLightDir(float4 v) | 仅可用于前向渲染。输入一个模型空间中的顶点位置,返回从世界空间中从该顶点到光源的光照方向。没有归一化 |
| float3 ObjSpaceLightDir(float4 v) | 仅可用于前向渲染。输入一个模型空间中的顶点位置,返回从模型空间中从该顶点到光源的光照方向。没有归一化 |
| float3 UnityObjectToWorldNormal(float3 norm) | 把法线方向从模型空间转化到世界空间 |
| float3 UnityWorldToObjectDir(float3 dir) | 把方向向量从世界空间转化到模型空间 |
| float3 UnityObjectToWolrdDir(float3 dir) | 把方向向量从模型空间转化到世界空间 |
显然可以看出,顶点一般都是四维表示(float4),而矢量是三维表示(float3)
这是因为顶点包含位置信息,而矢量不需要
四、基础语义
语义的作用是让Unity知道把数据输入到哪里,以及从哪里再获取到数据;让Shader知道把数据读取,以及输出到哪里
常见的读取用的语义有:
1.从应用阶段到顶点着色器阶段的(a2v)
- POSITION:模型空间的顶点位置(float4)
- NORMAL:顶点法线(float3)
- TANGENT:顶点切线(float4)
- TEXCOORDn(例如TEXCOORD1):顶点的纹理坐标(可以有好几组)(float2或float4)
- COLOR:顶点颜色(fixed4或者float4)
2.从顶点着色器到片元着色器阶段的(v2f)
- SV_POSITION:裁剪空间中的顶点坐标。结构体中必须有一个(虽然有的POSITION也可以,但是最好都用SV_POSITION了现在)
- COLOR0:通常用于输出第一组顶点颜色,但不是必须的
- COLOR1:通常用于输出第二组顶点颜色,但不是必须的
- TEXCOORD0~TEXCOORD7:通常用于输出纹理坐标,但不是必须的
其实除了SV_POSITION之外,剩下三个里面装的是什么数据其实都不重要,可以装一些你想要传到片元着色器中的数据
五、如何Debug调试
1.把要调试的变量映射到[0,1]作为颜色显示在材质上
一维分量,那就选择一个单独颜色分量(R)进行调试输出,其他设置为0
多维数据,选择其中一个分量进行单独调试,或者选择多个颜色分量进行输出
虽然有些原始,但是大部分情况下还是可以用的。
通过像素颜色的值判断它对不对

比如这个是把法线值映射到[0,1]之后作为颜色输出到屏幕上
2.VS--调试之神
VS2012以上版本有了Graphics Debugger可以使用。
具体操作可以去官网看的VS对Unity Shader的调试功能手册(需要科学上网)
3.Frame Debugger
Unity中自带的

可以这样打开它,然后可以滑动滑块看它一步一步的渲染过程

这些以Draw开头的就是一个Draw Call
六、不同平台的差异
1.渲染纹理坐标的差异
我们通过渲染纹理来保存输出到不同渲染目标上的 渲染结果

大多数情况下,不同屏幕空间的差异不会造成影响

使用渲染到纹理技术(RTT)的时候:把屏幕图像渲染到一张渲染纹理中(图中黄色的箭头),不处理的话就会导致纹理翻转过来。(但是好在Unity一般会帮助我们自动翻转(翻转的是采样纹理坐标UV)来避免这个问题)

因为抓取屏幕图像这一操作是由硬件指令发出的,不同硬件支持的图像API接口可能是不一样的(有的是OpenGL,有的是DirectX),而不同的图像API就可能会使得截取过程使用的坐标系不同,导致截取出来的图像数据的排列顺序是相反的(支持的是DirectX时),这时候再用这个数据去采用渲染到渲染纹理上的时候(按OpenGL标准),那纹理自然就会翻转了
Unity自动翻转失效

当我们开启了抗锯齿之后使用了渲染到纹理(RTT)技术
- 开启MSAA后,抗锯齿处理是由显卡硬件完成的,Unity只能拿到硬件处理后的结果纹理
- 硬件处理抗锯齿时跳过了Unity的坐标翻转层(
ComputeGrabScreenPos失效),导致纹理保持DirectX原生格式(左上角原点) - 当使用Unity提供的
Graphics.Blit()进行全屏绘制时,引擎会在内部自动处理坐标系转换 - 当同时处理屏幕图像和法线纹理时,这些图像在竖直方向朝向可能不同
- 对非Blit传递的纹理(如深度图/脚本传递的RT),需主动在顶点着色器翻转坐标


此时只有我们手动翻转这些没有自动翻转的纹理了
#if UNITY_UV_STARTS_AT_TOP //判断当前平台是否为DirectX
if(_MainTex_TexelSize.y < 0) //任意纹理都可以这样判断是否需要改变采样方式
uv.y = 1-uv.y
#endif
2.Shader语法差异
2.1.变量赋值不全
Dierct9/11对Shader语义更加严格
float4 v = float4(0.0) 只在OpenGL中可以
float4 v = float4(0.0,0.0,0.0,0.0) 在DirectX和OpenGL都可以
2.2.初始化问题
如果顶点着色器中没有对所有要out修饰的参数进行初始化,那也是报错
void vert (inout appdata_full v, out Input o) {
//使用Unity内置的UNITY_INITIALIZE_OUTPUT宏进行初始化
UNITY_INITIALZE_OUTPUT(Input,o);
}
这样子之后才不会报错
3.Shader语义差异
- 有的顶点着色器输出裁剪空间顶点坐标位置的变量语义修饰是POSITION,有的是SV_POSITION
- 有的Shader的片元着色器的输出颜色的变量的语义是COLOR0,有的是SV_Target
总而言之,都用SV_POSITION和SV_Target就好了
七、编写Unity Shader的注意事项
①尽量用fixed,它和half以及float的区别只是精度更低而已,它们都是浮点值(对移动平台友好)
②更加严格的语法,向DirectX的要求看齐
③不要在Shader中进行大量的运算,这不是GPU该干的事
④慎用循环与分支(是要要用那就尽量条件变量是常数、分支中操作尽可能少、分支嵌套数尽可能少)
⑤不要除以0
八、总结
①顶点着色器中接受的数据是从应用中提供的(Mesh Render组件),这些数据通过语义来告诉它要存在哪个变量里。而从顶点着色器到片元着色器中提供的数据也是通过语义来指定的,一些内置的函数以及数据结构可以帮助我们完成很多基础的变量传递变换操作。
②顶点着色器与片元着色器是哪个函数是通过编译指令来指定的
③对Shader进行Debug的方法有颜色输出观察法、VS调式、和Frame Debugger三种
④平台不同可能会导致一些最终渲染结果的纹理图像有所差异,需要针对处理
⑤编写Shader的时候要尽可能严格语法,并且考虑不同平台的适配性,以及尽可能进行优化

1777

被折叠的 条评论
为什么被折叠?



