
本文算是专门为了之后的Shader开发备忘录部分做的一个铺垫(之后Editor可能也会单独搞一个关于U3D编程特性或者技巧的“铺垫”?),主要讲在U3D中shader代码的基本格式(模板)和一些常用语法,同时也会分享一些个人收集的资料来完善知识体系。没什么程序基础的建议先去【隔壁】把123节简单看一看。
与隔壁的大铺垫同样,本文主要着重于快速上手,所以像是渲染管线之类的概念只会为了方便理解简单提两嘴。诸如偏理论的东西以及关于shader更系统的知识,感兴趣的可以去啃已经被推荐烂了的《Unity Shader入门精要》,或者更通用的《GPU编程与CG语言之阳春白雪下里巴人》,《Real Time Rendering》等(我也有PDF的资源,懒得找的可以私信- -)。

一般的程序脚本是跑在CPU上的程序,shader则是跑在GPU(就是显卡的核心部件)上的程序。你玩游戏或者捏模型时所有能看到的东西,哪怕是没上贴图的白模,甚至是没有光照的纯色剪影,实际上都是shader算出来的效果。
当时我自学完shader之后是很兴奋的- -因为觉得,会了这玩意,我不就能把自己世界观里的那些魔法给造出来了?当场成为魔法师(当然很多进阶的效果光靠shader还是不够的- -运动变化的逻辑需要脚本控制,美术资源也需要单独制作。想成为一个优秀的魔法师将会是一个要求综合能力的事情)。所以对于一般人来说,相较于控制“规律与法则”的CPU程序,控制“表层现象”的shader可能带来的乐趣与成就感会更高一些。
自high到此为止,那么接下来正式开始。

不同于隔壁,这次会更加深入U3D内部的具体操作(毕竟是U3D shader- -···不过还是那句话,不同环境的shader逻辑和原理都一样,也就变量,方法名之类的稍有不同)。
1、在UnityHub中创建URP项目
下面是新版本的UnityHub的界面。老版本那个白色背景的也差不多,就是第二步选模板的时候选那个Universal Render Pipeline就行。

1-1-1

1-1-2
本人平时主要是用URP项目。URP和常规项目的内置管线不同,shader的内置方法以及一些特性也略有不同。不过不担心- -之后的模板这两种写法我都会给出。而且程序这东西姑且算是理科,要记就记代码的效果和用途,到时候根据用途度娘一下就好(其实能搭梯子的我更推荐谷哥。学术方面的问题不论内容丰富度还是讨论环境,个人都觉得如今的国内比不了- -至于英文的问题,上去右键页面翻译完全够用了。)。不要记具体的方法名,不然换一个语言或者引擎就是一套新的API(也就是内置方法),记不过来的。
2、创建Shader文件

1-2-1
成功了会在下面的Project窗口中出现一个写着大大的S的文件。
3、创建材质球
右键刚刚的shader文件创建材质球;或者创建完了材质按住shader拖到材质上;或者选择材质球,在inspector窗口的Shader里搜索刚刚新建的shader。

1-3-1
4、模型替换材质
随便拉个方块出来。

1-4-1
把刚刚的材质按住拖到方块上,方块的视觉效果发生变化,对应的材质面板也变成了当前的材质。

1-4-2
5、材质调参
按需要调整材质的参数面板(有哪些参数取决于shader怎么写,后面讲),比如这里丢了张贴图进去(贴图具体怎么贴上去,贴上去长什么样和模型的UV有关。这个等shader备忘录第一话讲贴图会说)。

1-5-1
shader的完整使用流程一般就是上面这样,即shader->材质->模型。上面右键出来的这个Unlit Shader是个系统自带的shader,效果就是简单的贴张图上去。可以调图的重复次数和缩放,其余的颜色变化或者光照之类都没有(但是它有个雾效,就是拉远了整体颜色会变灰,像是蒙上一层雾一样,这个实现也很简单,到时候有机会讲)。

代码编辑

2-1-1
可以通过以上两种方式(前面的Edit按钮较低版本的U3D好像没有)或者双击shader文件的方式来打开。当然,前提是你装了VS或VC(当然如果是大闲者也可以用记事本)。一般装U3D的时候都会默认给你装个VS,VC得手动装。如果装了多个开发工具想要切换,按如下流程操作。

2-1-2
如果用VC,可以顺手装俩shader的代码提示插件。本人不太用VS写shader所以不清楚VS那边的插件支持情况,但应该也有。

2-1-3
代码结构
不多废话。下面直接放一个基于个人写法习惯的shader,功能与上面的Unlit完全相同(除了雾效没加),各部分都是干什么用的看注释即可。
Shader "NewShader" //shader的名字,就是在材质面板的shader选择栏中的名字
{
Properties //下面写你要用到的参数,它们会显示在材质面板上。
{
//布局属性
[Header(Texture)] //插入一行英文标题。括号中只能是英文,且不能带双引号
[Space(20)] //在材质面板中插入一个垂直方向20像素宽度的间隔
//以下为常用的几种参数写法
_MainTex ("主贴图", 2D) = "white" {} //纹理(贴图)类型。格式————变量名(材质面板显示名称, 2D/3D/CUBE) = 默认颜色 {} //3D没用过,2D就是二维图像,CUBE是环境球
//_Float ("浮点型", float) = 默认值
//_Range ("范围浮点型", range(最小值, 最大值)) = 默认值
//_Vector ("四维向量", vector) = (x, y, z, w) //xyzw为四个分量的默认值
//_Color ("颜色", color) = (R, G, B, A) //RGBA为四个通道的默认值
//_Int ("整型", int) = 默认值
//参数前还可以加某种“特性”,以下列出常用的几种
//[HideInInspector] 在材质面板中隐藏该参数
//[NoScaleOffset] 隐藏纹理类型的Tiling和Offset
//[HDR] 表示颜色具有高动态范围(不严谨的说就是色值可以超过1)
//材质面板还能通过脚本实现定制化,以后在Editor中会讲
}
//渲染代码块,可以有多个
SubShader
{
//渲染设置
Tags
{
"RenderPipeline" = "UniversalPipeline" //渲染管线类型,一般不动
"RenderType" = "Opaque" //渲染类型。常用的三种:Opaque不透明物体,TransparentCutout硬边透明物体,Transparent软边透明物体
"Queue" = "Geometry" //渲染队列(顺序)。常用的三种:一般Opaque对应Geometry,TransparentCutout对应AlphaTest,Transparent对应Transparent
//还有些其他的,但不常用,有兴趣自行度娘
}
// LOD 100 用于区分高低端机或场景距离远近不同时跑不同SubShader的标识符,需要在外部通过脚本控制,本人没用过
//渲染计算代码块,可以有多个,但URP在一般情况下只支持单Pass,需要renderfeature等特殊手段才能多Pass
Pass
{
// Blend SrcAlpha OneMinusSrcAlpha // Transparent渲染类型下的透明混合模式,类似于PS图层混合模式,其他渲染类型不写。示例为最常用的混合方式
Cull back //表面剔除,不写默认back。可设置front(不渲染正面),back(不渲染背面),off(双面渲染)
//ZTest LEqual //深度测试,默认开启,除特殊效果一般不用写
ZWrite on //深度写入,可设置on,off,不写默认on。一般Opaque和TransparentCutout开启,Transparent关闭
// Stencil //模板测试,用的比较少
// {
// Ref 2
// Comp NotEqual
// Pass Replace
// }
// ColorMask 0 //颜色缓冲遮罩,用的比较少
HLSLPROGRAM //不要动
#pragma vertex vert //不要动
#pragma fragment frag //不要动
//如果用几何着色器或细分曲面着色器,这里还会多两三个东西,但超纲,不讲
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" //引用的HLSL方法库,等同于C#中的using,可以在项目工程的Packages中找到源码。常用的有Core和Lighting,也可以自己写
//这块写参与计算的参数声明。除了上面的材质面板参数外,还可能有一些外部传入的变量。
CBUFFER_START(UnityPerMaterial) //SRP合批优化的关键字,不要动
sampler2D _MainTex; float4 _MainTex_ST; //纹理变量名_ST是系统内部一种默认的变量名,_ST变量中存储了材质面板中的Tiling和Offset值
//其他的变量声明
CBUFFER_END //不要动
//你需要从模型中拿来用的数据有哪些,这个结构体中的变量就有哪些。冒号前面的变量名随便改,但冒号后面的东西是用来告诉系统这个变量是什么,不要改
struct a2v
{
half4 posOS : POSITION; //模型顶点的本地空间(模型空间)坐标
float2 uv : TEXCOORD0; //模型第一套UV
//以下是另几种常用的数据
//half3 nDirOS : NORMAL; //模型空间顶点法线坐标
//half4 tDirOS : TANGENT; //模型空间顶点切线坐标。half4没打错,第四个分量用于采样法线贴图时矫正副切线的正反
//float2 uv2 : TEXCOORD1; //模型第二套UV
//half4 color : COLOR; //模型顶点色
};
//你需要在像素shader,也就是下面的frag代码段中用到哪些数据,这个结构体中的变量就有哪些。
struct v2f
{
half4 posCS : SV_POSITION; //裁剪空间(也就是视椎体)坐标,不管什么shader都有这么一行,不要改
float2 uv : TEXCOORD0; //不同于a2v结构体,v2f中除了SV_POSITION,其他各项冒号后都是TEXCOORD+数字的形式,一般从0开始依次往后排即可
};
//顶点shader。主要在其中预计算一些像素shader中需要用到的变量,以及做一些像是顶点动画的效果
//可以粗略理解成在同一时间内,模型上的每个顶点都在跑这段方法
v2f vert (a2v i)
{
v2f o; //不要动
//其他要计算的东西
//坐标
o.posCS = TransformObjectToHClip(i.posOS.xyz); //决定模型的顶点在屏幕上显示的位置,所以若是有顶点动画,需要将改变后的顶点坐标传入方法
//UV
o.uv = TRANSFORM_TEX(i.uv, _MainTex); //在有_ST变量的前提下,这种写法等价于_MainTex_ST.xy * i.uv + _MainTex_ST.zw;
return o; //不要动
}
//像素shader(或者叫片元/片段shader)。一般shader绝大多数的计算都在这里完成。不严格的说,该方法的返回值便是屏幕上某个像素的色值,所以是half4(或float4)
//可以粗略理解成在同一时间内,模型显示在屏幕上的每个像素都在跑这段方法
half4 frag (v2f i) : SV_Target
{
//其他要计算的东西
half4 var_MainTex = tex2D(_MainTex, i.uv); //最常见的纹理采样方法,意思获得_MainTex中i.uv位置的色值。
half3 finalCol = var_MainTex.rgb;
return half4(finalCol, 1.0);
}
ENDHLSL //不要动
}
}
FallBack "Hidden/Universal Render Pipeline/FallbackError" //当上面的shader因为性能等原因无法执行时会跑这个shader,可以不写
} 一般的shader基本就分为上述几大块。而在实际编写时,大部分时间其实都会花在像素shader中,其次是顶点shader,而代码的写法其实和隔壁的C#差别不大,甚至因为shader整体倾向于面向过程编程(类C语言),所以还会更简单(一般全是数值方面的运算)。其他的配置等部分,初学暂且不需要过度关注,大概知道干什么用的即可。
下面分别给出URP和内置管线下的纯净模板,用的话没有特殊需求直接在这个基础上往里塞东西即可。
URP
//URP
Shader "Shader名字"
{
Properties
{
//参数列表
}
SubShader
{
Tags
{
"RenderPipeline" = "UniversalPipeline"
"RenderType" = "Opaque" //渲染类型
"Queue" = "Geometry" //渲染队列
}
Pass
{
// Blend SrcAlpha OneMinusSrcAlpha
Cull back
ZWrite on
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
CBUFFER_START(UnityPerMaterial)
//参数列表
CBUFFER_END
struct a2v
{
half4 posOS : POSITION;
//其他用到的模型数据
};
struct v2f
{
half4 posCS : SV_POSITION;
//其他在像素shader中要用到的数据
};
v2f vert (a2v i)
{
v2f o;
o.posCS = TransformObjectToHClip(i.posOS.xyz);
//其他计算
return o;
}
half4 frag (v2f i) : SV_Target
{
//计算
return 1;
}
ENDHLSL
}
}
FallBack "Hidden/Universal Render Pipeline/FallbackError"
} 内置
//Build in
Shader "Shader名字"
{
Properties
{
//参数列表
}
SubShader
{
Tags
{
"RenderType" = "Opaque" //渲染类型
"Queue" = "Geometry" //渲染队列
}
Pass
{
// Blend SrcAlpha OneMinusSrcAlpha
Cull back
ZWrite on
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
//参数列表
struct a2v
{
half4 posOS : POSITION;
//其他用到的模型数据
};
struct v2f
{
half4 posCS : SV_POSITION;
//其他在像素shader中要用到的数据
};
v2f vert (a2v i)
{
v2f o;
o.posCS = UnityObjectToClipPos(i.posOS.xyz);
//其他计算
return o;
}
half4 frag (v2f i) : SV_Target
{
//计算
return 1;
}
ENDCG
}
}
FallBack "Diffuse"
} 可以发现,其实URP和内置的区别一般也就那么几个。换句话说,项目移植很方便。
1、Tags里少了一项管线配置
2、HLSL变成了CG(其实就是HLSL语言和CG语言,还有个GLSL)
3、引用库变了
4、参数列表不需要CBUFFER上下包夹了
5、裁剪空间的变换函数名变了(但效果没变)
6、FallBack变了

数据类型
经过上面的代码示例可以发现,shader里频繁出现half以及half或float后跟着数字的数据类型。
half就是个低精度的float,它同样是小数。其实shader原本还有个fixed,是个比half精度更低的小数,但如今在大部分平台上half和fixed好像都合并了,所以就不用fixed了。
用half是因为它在性能上相较float有一定的优势,但在有些要求精度的计算中,用half可能会出现显示上的问题,并且不同平台间的系统精度不同,比如经常会出现在电脑上看着好好的,打包放到手机上就各种粉的现象。(U3D材质丢失或材质出现问题会让模型呈现粉色)目前个人的习惯是,除了UV用float之外都先用half,如果出问题再debug提高对应变量的精度。不过如果是自己做作品做着玩的话- -一水的float就完事了嗷。
至于half或float后跟数字的写法也很好猜,其实就是代表多维度。可以通过.xyzw或.rgba的方式,任意个数、任意顺序甚至可重复的访问其中的某个或某些维度。效果上等同于U3D脚本中的Vector2、Vector3、Vector4,并且用起来更灵活。(比如想在shader中改一个颜色color的R和B通道,直接color.rb = half2(0.5, 0.5);就可以做到,而在C#脚本中还得完全new出来一个新的,并且所有通道都要重新赋值- -)
常用方法
这里直接放上3个个人收藏的链接,里面收集了大量CG与HLSL的内置方法(说实话里面的大部分我也没用过- -···) https://blog.csdn.net/fengya1/article/details/105577477/
https://blog.csdn.net/swj524152416/article/details/52980011
https://blog.csdn.net/qq_42115447/article/details/103934503