技术美术成长之路——UnityShader篇(四)、开启Shader之旅
本文参考:相关文章汇总:
第五章、开启Unity Shader之旅
一、环境
关于环境,我是高版本应该没啥问题,学起来再说吧~
二、最简单的顶点/片元着色器
window-rendering-lighting-environment-天空盒给他换掉就行了
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "unityShaderBook/chapter5/simpleShader"{
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.0, 1.0, 1.0, 1.0);
}
ENDCG
}
}
}这里代码copy过来还给了一个更新提示,MVP矩阵可以写成上边的代码了。
这里主要的新内容:#pragma vertex vert及下一行。这俩是告诉unity哪个函数包含了顶点着色器的代码,哪个是片元着色器。因为这俩函数是我们自定义函数名字的。
指定的形式就是:#pragma vertex name,一般习惯用vert和frag。
其中的语义SV_POSITION就表示裁剪空间的坐标。POSITION就是模型空间坐标。SV_Target就是指的输出的颜色值。
语义的意思就是我这里的有参数,你应该传进来什么?也就是unity会根据我们的语义来对参数进行对应的填充。那unity的这些数据从哪里来的,是使用该材质的mesh render组件提供的。在每一帧drawcall调用的时候他会把这些需要的纹理等给到unity shader。
对于顶点着色器从应用程序层得到的法线等数据,这个一般都是原地不动往后传递给片元着色器使用的,那么对于两者之间的通信如何进行的?
也就是通过返回值和参数,因为这里东西又不只是一个坐标,所以应该搞一个结构体出来。另外注意,从顶点着色器虽说是传给了片元着色器,但是并不是直接传递的,他俩根本不是一个层级的。对于顶点shader操作的是每一个顶点,返回的数据是每一个顶点返回一个。而片元shader是针对片元的。
这里是顶点的结果经过插值,然后得到片元对应的值,然后再作为片元shader的输入。
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "unityShaderBook/chapter5/simpleShader"{
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 color : COLOR0;
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.color = v.normal * 0.5 * fixed3(0.5, 0.5, 0.5);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
return fixed4(i.color, 1.0);
}
ENDCG
}
}
}这里我们的球就出现效果了:
可能因为版本的问题,他书上的代码vert多写了一个返回值的语义,因为我们在结构体里面写过了,这里应去掉,和输入参数类似。
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "unityShaderBook/chapter5/simpleShader"{
Properties
{
_Color("Color Tint", Color) = (1.0, 1.0, 1.0, 1.0)
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
fixed4 _Color;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 color : COLOR0;
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.color = v.normal * 0.5 * fixed3(0.5, 0.5, 0.5);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed3 c = i.color;
c *= _Color.rgb;
return fixed4(c, 1.0);
}
ENDCG
}
}
}现在就可以根据颜色拾取器来控制球的颜色了。注意要把变量在pass里面再进行定义一次。
再进行定义的时候需要注意名字和类型都要匹配,名字必须一样,但是注意,但是对于类型,shaderlab中有的类型在HLSL这种语言中不一定有,所以这里他们有一个分量个数的对应就够了:
你是4维向量,我是4个float,那咱俩就能对应上。
三、unity中的内置文件和变量
内置文件也就是类似c++中的库,头文件,include之后就能用。
相关的常用结构体:
struct appdata_base {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct appdata_tan {
float4 vertex : POSITION;
float4 tangent : TANGENT;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct appdata_full {
float4 vertex : POSITION;
float4 tangent : TANGENT;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 texcoord1 : TEXCOORD1;
float4 texcoord2 : TEXCOORD2;
float4 texcoord3 : TEXCOORD3;
fixed4 color : COLOR;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct appdata_img
{
float4 vertex : POSITION;
half2 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f_img
{
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
};还有相关的函数:
针对这些函数的实现可以看一下,看一下别人的实现思路。当然他实现的时候套娃,用VSCode来看有点费劲没法转到定义。
四、Unity提供的CG/HLSL语义
1、语义what
语义其实就是对于参数和返回值的一个补充解释。
其实这个东西对于a2f变量来说,是很重要的东西,因为给顶点着色器赋值的是应用程序,是机器操作,他只会比对,发现这个是TEXCOOD0,那好他就会把他那里TEXCOOD0的纹理给赋值过去。
而对于v2f的语义,其实并不重要,因为两头都是由我们进行控制的,对于顶点着色器输出的COLOR0,我们其实并不一定要装颜色,装其他也可以,比如装顶点坐标,到了片元着色器时,我们把他当作顶点坐标使用就好了。这是完全没问题的。
对于语义有的带有SV,这种表示system-value,也就是他是系统相关的语义,这样的都具有特定的意义,比如SV_POSITION表示的时裁剪空间的坐标,SV_Target表示的时fragment shader的输出颜色。
因为GPU会拿他们做一些特定的事情,所以这些不能随便改动,你改了GPU找不着了就。当然这里的俩SV语义,是可以等价于POSITION和COLOR的,但是有一些平台等价,你可以用这俩替换系统值,但是有些不可以!
所以最好还是在这些关键的地方使用SV,保证跨平台的时候没问题。
所以总的来说,就是和机器交互的部分,你得正儿八经写语义,包括顶点着色器的所有输入参数和输出的坐标值,还有像素着色器的返回值。且该用SV系统值的时候就用。其他的语义实际上意义就不大。
struct v2f
{
float4 pos : SV_POSITION;
float3 color : CO;
};对于v2f也就是顶点shader的输出结构体,我这里改成了CO,也是完全ok的,效果不变,因为就是一个完全由我们控制的,我们知道他是咋对应的,所以语义写成啥都行。
2、unity中支持的语义
Shader Model是啥?好像是和一些硬件啥啊的有关。。
注意这里都写了几个字,但不是必需的。通常的一些自定义的变量就是用TEXCOOD0~7来进行传递的。
这里还说了:
意思好像是说,关于一个语义能够使用的寄存器是有上限的!!!
五、Shader的调试
1、第一种方法
一种方式是把我们需要调试的变量当作颜色输出到屏幕上,因为这个是唯一的输出,只能通过这个输出,通过输出颜色来查验结果。
在unity shader中,颜色的范围是0到1,所以我们需要把调试的变量需要映射一下。如果不知道范围,针对超出范围的还是1,小于0的就是0,可以进行相关的尝试。
如果调试的一个一维变量,那么输出的时候就作为一个颜色分量输出,其他都0。当然多维的可以一个一个分析,也可以同时多个正好对应到颜色的三个分量上。
frac函数是返回小数部分 x - (int)x。decimal fraction小数
saturate函数是把结果限制在0到1范围内,小于0的取0,大于1的取1。
any(x) 参数中只要有一个不为0就返回true 否则返回false。Test if any component of x is nonzero.
其实都是把范围进行缩放一下,基本没有别的啥操作。
额,脚本不会用,试了一下,拖拽之后,component中也有了,但是依旧提示下面的:
但是利用QQ可以截图。
2、第二种方法:利用VS的图形调试器
等我出bug了,我再去学学。
3、第三种方式:帧调试器
回头再学(114页)
六、平台差异
针对一般情况的渲染输出没有问题,而我们以后可以把结果输出到渲染目标上,然后用渲染纹理来进行存储,这时候可能出一些问题,但是unity也帮我们处理了。
但是有一种例外情况,就是我们开启了抗锯齿并渲染到纹理时,就会有问题了。
至于为啥,看的糊里糊涂的。
这里的第二个也没看懂,表面着色器,不太了解,先放着吧。
关于语义已经说过了,尽可能在该使用SV的时候使用带SV的。
更多的官方去看吧:
七、Shader 整洁之道
1、关于float half fixed三种变量类型的选择:
这个精度和具体平台有关系,这里不一定。
主要就是移动平台需要注意!
2、语法规范
3、避免不必要的计算
因为对于片元着色器本身是一个需要大量执行的,一个三角形很多像素,实际程序中三角形数量又很多,所以他的高效执行就非常重要。
而且从内存上看,如果它使用太多的寄存器或者指令会出问题,因为它本身是高度并行的一个程序,那么对于寄存器这些资源大家都需要用,所以是有上限的。
这里我们可以调整我们的Shader Model,也就是和硬件相关的一些限制。
4、慎用循环和分支
因为GPU就是一帮小孩,你给他一个初中题,可能硬憋一会也能做出来,但是他比正常要慢很多。CPU就像高中生,大学生,对这些小操作很快就能搞定。小孩的优点在于,我人多啊。
5、不要除以0
这一章,还是一些相关的东西的介绍,对于shader现在差不多的东西都介绍了,下面应该就是算法相关了。 可以多交流哦
页:
[1]