//主要是学习冯乐乐的《UnityShader入门精要》记录学习的心得
==================================初级篇=================================
Shader基本介绍
shader即是着色器,在Unity中用于定义3D物体的渲染表面,可以控制多个方面,例如物体表面的光照,阴影,纹理,一些特殊效果,后处理等等,通常用于GPU,实现图形渲染
shader又分为几类:顶点着色器(Vertex Shader),片元着色器(Fragment Shader),曲面细分着色器(Tessellation Shader),几何着色器(Geometry Shader)等等。
Shader的写法构成
shader "ShaderName" //着色器名字
properties{
//一些属性,例如颜色,主纹理等等
}
SubShader{
//显卡A使用的子着色器
//可选的
[Tags] //渲染标签
结构如下所示:
Tags{"Queue" = "Transparent"} 控制对象的渲染队列
Tags{"RenderType" = "Opaque"} 对着色器分类,常用于替换着色器
Tags{"DisableBatching" = "True"} 指明是否可以批处理
Tags{"ForceNoShader" = "True"} 控制物体是否可以投射阴影
Tags{"IgnoreProjector" = "True"} 如果为True则不受Projectr影响
Tags{"CanUseSpriteAtlas" = "False"} 当用于精灵贴图使标签为false
Tags{"PreviewType" = "Plane"} 预览的材质类型
//可选的
[RenderSetup] //渲染状态
结构如下所示
Cull Back|front|off 控制剔除哪一面
ZTest Less/Greater |LEqual|GEqual|Equal|NotEqual|Always 深度写入比较的值
ZWrite On|Off 是否开启深度写入
Blend 控制混合因子,常见于透明物体
//以上部分不适用于下面的Pass代码段
Pass{
//除了这个Pass还有其他特殊的Pass 例如UsePass(复用其他的Pass)GrabPass(抓取屏幕的纹理以便后续操作)
[Name]
[Tags] //LightMode Tags{"LightMode" = "ForwardBase"}
Tags{"RanderOptions" = "SoftVegetation"}
[RanderSetup]
//Other
}
}
SubShader{
//显卡B使用的子着色器
}
Fallback "" //引用回调 如果着色器缺少的引用会去这个里面找(针对阴影部分),同时也是都不适用的时候回调的shader
//除此之外还有其他的语义
以上是Shader的一些基本语义,由于没有CG代码这里就用C#代替了吧
着色器的写法示例
Shader "Simple Shader" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
}
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert//顶点着色器
#pragma fragment frag//片元着色器
uniform fixed4 _Color;
//顶点着色器
struct a2v {
float4 vertex : POSITION; //顶点位置
float3 normal : NORMAL; //法线位置
float4 texcoord : TEXCOORD0; //第一张纹理
};
//片元着色器
struct v2f {
float4 pos : SV_POSITION; //裁剪空间下顶点位置
fixed3 color : COLOR0; //颜色
};
v2f vert(a2v v) {
v2f o;//以便与片元着色器通信
o.pos = UnityObjectToClipPos(v.vertex); //Unity内置函数转置顶点矩阵,模型空间到世界坐标系
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
}
}
}
ShaderLab:命令
本页面介绍有关在 ShaderLab 语言中使用命令的信息。
ShaderLab 命令分为以下类别:
- 用于在 GPU 上设置渲染状态的命令。
- 用于创建具有特定用途的通道。
- 如果使用旧版 “fixed function style” 命令,无需编写 HLSL 也可创建着色器程序。
可以通过 Category 代码块将 ShaderLab 命令组合起来。
用于设置渲染状态的命令
在 Pass 代码块中使用这些命令可为该 Pass 设置渲染状态,或者在 SubShader 代码块中使用这些命令可为该 SubShader 以及其中的所有 Pass 设置渲染状态。
- AlphaToMask:设置 alpha-to-coverage 模式。
- Blend:启用和配置 alpha 混合。
- BlendOp:设置 Blend 命令使用的操作。
- ColorMask:设置颜色通道写入掩码。
- Conservative:启用和禁用保守光栅化。
- Cull:设置多边形剔除模式。
- Offset:设置多边形深度偏移。
- Stencil:配置模板测试,以及向模板缓冲区写入的内容。
- ZClip:设置深度剪辑模式。
- ZTest:设置深度测试模式。
- ZWrite:设置深度缓冲区写入模式。
宏编译命令
通道命令
在 SubShader 中使用这些命令可定义具有特定用途的通道。
- UsePass 加名字,它从另一个 Shader 对象导入指定的通道的内容,比如外轮廓方便复用。
- GrabPass 创建一个通道,将屏幕内容抓取到纹理中,以便在之后的通道中使用。
- ShaderLab:命令 - Unity 手册
顶点着色器参数
片元着色器参数
光照模型
光照是我们可以看见物体的必备因素之一,在游戏引擎中模拟光照也格外重要,到目前为止,已经有很多种方法模拟光照,兰伯特模型,顶点着色,片元着色,半兰伯特等等,下面是一些经典的光照模型以及理论基础
理论部分:
着色
着色指的是根据材质属性(漫反射等),光源信息等使用某个公式去量化得到出射度的过程,这个也称之为光照模型
标准光照模型
这个模型的基本方法是:把进入摄像机的光线分为四个部分,每个部分使用一种方法计算贡献度,四个部分分别是:
自发光(emissive):这个部分用于描述给定一个方向时,表面本身会向该方向反射多少辐射量。需要注意的是,如果没有使用全局光照那么它只是看起来更亮了一点
标准光照模型使用自发光来计算这个部分的贡献度,它的计算也很简单,就是直接使用了这个材质的自发光物体颜色C(emissive) = M(emissive)
高反射光(specular):这个部分描述光线从光源照射到模型表面时,表面会在完全镜面反射多少辐射量
-->
漫反射(diffuse):这个部分用于描述,当光线从光源照射到模型表面时,模型会向每个防线散射多少辐射量
漫反射符合兰伯特定律 C(diffuse)= (C(light)•m(diffuse))max(0,•
)

环境光(ambient):这个部分描述其他类型的间接光照
例如一个棕色桌子下面是红色的地毯,那么它的底部也会有一些红色,在标准光照模型中,环境光通常是一个环境变量,即为

(图片摘自原书《unityshader入门精要》)
漫反射光照模型

逐顶点光照
在实现光照模型之前,首先要思考如何实现上文所说的计算公式,在这个公式之中要得到表面法线以及光源方向,表面法线可以由模型空间的normal得到,光源方向则是思考Unity内置函数的调用,漫反射系数我们则可以自己定义在属性中
Shader "LitMode/VertexMode"
{
Properties
{
_Diffuse("Diffuse", Color) = (1, 1, 1, 1)
}
SubShader
{
Pass
{
Tags {"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "UnityCG.cginc"
float4 _Diffuse;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 color : COLOR;
};
v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);//模型空间到裁剪空间
float3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
float3 worldNormal = normalize(mul(v.normal,(float3x3)unity_WorldToObject));//转置法线到世界坐标系下
float3 worldLight = normalize(_WorldSpaceLightPos0.xyz);//得到光线方向
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));
o.color = diffuse+ambient;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return fixed4(i.color, 1.0);
}
ENDCG
}
}
}
这里将运算都放置于顶点着色器中
逐片元光照模型(常用)
与逐顶点一样,都是根据上述公式实现光照模型,与之不同的是在片元着色器中计算结果,这样往往可以让颜色更加均匀没有锯齿
Shader "LitMode/PixelMode"
{
Properties
{
_Diffuse("Diffuse", Color) = (1, 1, 1, 1)
}
SubShader
{
Pass
{
Tags {"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "UnityCG.cginc"
float4 _Diffuse;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNomral : TEXCOORD0;
};
v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);//模型空间到裁剪空间
o.worldNomral = mul(v.normal,(float3x3)unity_WorldToObject);//转置法线到世界坐标系下
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
float3 worldNormal = normalize(i.worldNomral);
float3 WorldLightDir = normalize(_WorldSpaceLightPos0.xyz);//得到光线方向
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, WorldLightDir));
float3 color = diffuse+ambient;
return fixed4(color, 1.0);
}
ENDCG
}
}
}
半兰伯特光照模型
这一模型被用于游戏《半条命》中,这种模型的特点是,用一个系数加入运算的公式之中从而使得光照模型可以发射一些视觉上的改变而不是符合物理学,半兰伯特则是系数为0.5,这一系数使得模型看起来更加明亮,但是背部几乎没有什么阴影。
Shader "LitMode/LambertMode"
{
Properties
{
_Diffuse("Diffuse", Color) = (1, 1, 1, 1)
}
SubShader
{
Pass
{
Tags {"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "UnityCG.cginc"
float4 _Diffuse;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNomral : TEXCOORD0;
};
v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);//模型空间到裁剪空间
o.worldNomral = mul(v.normal,(float3x3)unity_WorldToObject);//转置法线到世界坐标系下
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
float3 worldNormal = normalize(i.worldNomral);
float3 WorldLightDir = normalize(_WorldSpaceLightPos0.xyz);//得到光线方向
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, WorldLightDir)*0.5+0.5);
float3 color = diffuse+ambient;
return fixed4(color, 1.0);
}
ENDCG
}
}
}
效果展示
加入些高光吧高光模型+(Blinn-Phong模型)
如图所示,这里引入了对视角方向进行运算,这个新的变量则应该需要一个摄像机的内置函数的引用以得到这个变量,因此,可以使用_WorldSpaceCameraPos来得到。
我们可以用上述内置函数来计算反射方向r,而材质的gloss属性我们可以自己定义在属性上
这样来实现高光模型(以逐像素为例)
Shader "LitMode/GlossPixelLit"
{
Properties
{
_Diffuse("Diffuse", Color) = (1, 1, 1, 1)
_Gloss("Gloss",Range(8.0,256)) = 20
_Specular("Specular",Color) = (1,1,1,1)
}
SubShader
{
Pass
{
Tags {"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "UnityCG.cginc"
float4 _Diffuse;
float4 _Specular;
float4 _Gloss;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNomral : TEXCOORD0;
float3 worldPos:TEXCOORD1;
};
v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);//模型空间到裁剪空间
o.worldNomral = mul(v.normal,(float3x3)unity_WorldToObject);//转置法线到世界坐标系下
o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
float3 worldNormal = normalize(i.worldNomral);//法线方向
float3 WorldLightDir = normalize(_WorldSpaceLightPos0.xyz);//得到光线方向
float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);//视角方向
float3 reflectDir = normalize(reflect(-WorldLightDir,worldNormal));//反射方向
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, WorldLightDir));
float3 specular = _LightColor0.rgb * _Specular.rgb * saturate(dot(viewDir,reflectDir));
float3 color = diffuse+ambient+specular;
return fixed4(color, 1.0);
}
ENDCG
}
}
}

我们已经整了那么多,这里也就改改高光的代码就可以了,换个公式带入即可
Shader "LitMode/Binn-Phong" {
Properties {
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}

再贴个书上的内置函数()

基础纹理
单张纹理
纹理作为游戏贴图中至关重要的部分,如何将一张贴图贴到三位物体上就是这章讨论的关键,我们可以定义一个贴图的类型,这里也补充一下查阅Unity的文档,官方对于属性的定义类型
在着色器代码中,通常所有属性名称都以下划线字符开头。本页面上的示例遵循此约定。
| 类型 | 示例语法 | 注释 |
|---|---|---|
| 整数 | _ExampleName ("Integer display name", Integer) = 1 | This type is backed by a real integer (unlike the legacy Int type described below, which is backed by a float). Use this instead of Int when you want to use an integer. |
| Int(旧版) | _ExampleName ("Int display name", Int) = 1 | Note: This legacy type is backed by a float, rather than an integer. It is supported for backwards compatibility reasons only. Use the Integer type instead. |
| Float | _ExampleName ("Float display name", Float) = 0.5_ExampleName ("Float with range", Range(0.0, 1.0)) = 0.5 | 范围滑动条的最大值和最小值包含在内。 |
| Texture2D | _ExampleName ("Texture2D display name", 2D) = "" {}_ExampleName ("Texture2D display name", 2D) = "red" {} | 将以下值置于默认值字符串中可使用 Unity 的内置纹理之一:“white”(RGBA:1,1,1,1)、“black”(RGBA:0,0,0,1)、“gray”(RGBA:0.5,0.5,0.5,1)、“bump”(RGBA:0.5,0.5,1,0.5)或“red”(RGBA:1,0,0,1)。 如果将该字符串留空或输入无效值,则它默认为 “gray”。 注意:这些默认纹理在 Inspector 中不可见。 |
| Texture2DArray | _ExampleName ("Texture2DArray display name", 2DArray) = "" {} | 有关更多信息,请参阅纹理数组。 |
| Texture3D | _ExampleName ("Texture3D", 3D) = "" {} | 默认值为 “gray”(RGBA:0.5,0.5,0.5,1)纹理。 |
| Cubemap | _ExampleName ("Cubemap", Cube) = "" {} | 默认值为 “gray”(RGBA:0.5,0.5,0.5,1)纹理。 |
| CubemapArray | _ExampleName ("CubemapArray", CubeArray) = "" {} | 请参阅立方体贴图数组。 |
| Color | _ExampleName("Example color", Color) = (.25, .5, .5, 1) | 这会在着色器代码中映射到 float4。 材质 Inspector 会显示一个拾色器。如果更愿意将值作为四个单独的浮点数进行编辑,请使用 Vector 类型。 |
| Vector | _ExampleName ("Example vector", Vector) = (.25, .5, .5, 1) | 这会在着色器代码中映射到 float4。 材质 Inspector 会显示四个单独的浮点数字段。如果更愿意使用拾色器编辑值,请使用 Color 类型。 |
下面先贴代码再讲解
Shader "basedTex/singleTex"
{
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
//ST表示偏移量
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
// Or just call the built-in function
// o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
// Use the texture to sample the diffuse color
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
return fixed4(ambient + diffuse, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
在这段Unity的Shader代码中,结构体a2v中的texcoord是float4类型,而v2f结构体中的uv是float2类型。这是因为在Shader编程中,输入的纹理坐标通常会被存储为float4,即使实际使用时只需要两个分量(x和y)。这是为了与GPU的顶点数据格式保持一致,GPU通常会将顶点属性(如纹理坐标)存储为四元数(四个分量)。
在a2v结构体中,texcoord是float4类型,因为它直接来自于模型的顶点数据,通常模型的纹理坐标会被存储为四个分量(x, y, z, w),尽管在实际应用中,z和w通常被忽略。在v2f结构体中,uv是float2类型,因为在传递给片段着色器时,只需要x和y两个分量来进行纹理采样。
至于在属性中定义贴图时,这段代码使用了_MainTex_ST来处理纹理的缩放和偏移。_MainTex_ST是一个由Unity自动维护的矩阵,用于将纹理坐标从模型的纹理空间转换到Shader的纹理空间。_MainTex_ST的含义是ST,即Scale(缩放)和Translate(偏移)。在顶点着色器中,o.uv通过_MainTex_ST进行变换,以确保纹理坐标正确地缩放和偏移到合适的位置。
Shader "Unity Shaders Book/Chapter 7/Texture Properties" {
Properties {
_MainTex ("Main Tex", 2D) = "white" {}
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
struct a2v {
float4 vertex : POSITION;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 position : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert(a2v v) {
v2f o;
// Transform the vertex from object space to projection space
o.position = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed4 c = tex2D(_MainTex, i.uv);
return fixed4(c.rgb, 1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}//直接贴贴图是这样的,来源于《UnityShader入门精要》
凹凸纹理(法线纹理)
为了游戏整体节约开销,我们可以在建模过程中将一个高精度模型的表面细节烘焙到一个低精度模型上,这样得到的贴图叫法线纹理,当然还可以烘焙别的细节,比如环境光的,粗糙度什么的。与单张纹理相同的是,原理都是定义一张图片然后塞进去,用顶点着色器和片元着色器运算。
由于法向分量的值在[-1,1]之间,于是乎我们需要做一个映射 pixel = (pixel+1)/2 来映射到[0,1]中,除此之外,还分为切线空间和模型空间中的法线纹理。模型空间中的法线纹理好处是实现简单,可以提供更平滑的边缘。切线空间中的法线纹理好处是记录的是绝对法线信息,可以扩展到别的模型上,自由度高,比如可以做一些熔岩特效
一般来说法线纹理都是存储的法线经过映射得到的像素值,所以我们需要反映射回来,也就是解压法线得到正确的法线信息
解压法线是处理法线贴图(Normal Map)时的重要步骤。法线贴图用于模拟表面细节,通过在表面上添加细微的凹凸纹理来创建更复杂的视觉效果。以下是解压法线的原因以及为什么需要这个过程。
1. 法线贴图的格式
法线贴图在纹理中存储的法线信息通常是压缩的。一般来说,法线贴图的 RGB 通道代表一个单位法线向量,其值在0到1之间。这种表示方法需要将颜色值转化为法线向量,这就是"解压"过程的重点:
- 颜色值范围:法线贴图的每个像素的颜色值在
[0, 1]范围内。RGB 颜色通常表示的形式是[R, G, B],但我们需要将这些值转换为实际的法线向量,范围是[-1, 1]。
那么怎么在Shader中加入凹凸纹理呢,思路是在单张纹理Shader的基础上增加一个凹凸纹理的属性,和单张纹理一样需要再之后进行一个声明,我们可以将片元着色器中的UV扩展至float4,这样可以用一个属性的四个通道存储两张贴图,节省了空间
然后我们需要在顶点着色器中将bump的属性存储到uv中做一些坐标变换,在片段着色器中采样法线纹理(tex2D(_BumpMap, i.uv.zw),解压法线信息(UnpackNormal(packedNormal))最后在计算漫反射的部分计算光照方向和法线的关系即可
代码如下
Shader "basedTex/Bump"
{
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1) // 颜色选择器,默认颜色为白色
_MainTex ("Main Tex", 2D) = "white" {} // 主纹理,默认设置为白色纹理
_BumpMap ("Normal Map", 2D) = "bump" {} // 法线贴图,默认为法线纹理
_BumpScale ("Bump Scale", Float) = 1.0 // 控制法线贴图强度的浮点数,默认值为1.0
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" } // 定义此着色器的光照模式
CGPROGRAM // 开始CG代码块
#pragma vertex vert // 指定顶点着色器
#pragma fragment frag // 指定片段着色器
#include "Lighting.cginc" // 引入照明相关的函数库
fixed4 _Color; // 顶部定义的颜色变量
sampler2D _MainTex; // 主纹理的采样器
float4 _MainTex_ST; // 主纹理的平铺和偏移信息
sampler2D _BumpMap; // 法线贴图的采样器
float4 _BumpMap_ST; // 法线贴图的平铺和偏移信息
float _BumpScale; // 法线贴图强度的控制变量
// 定义输入结构体,用于存储顶点属性
struct a2v {
float4 vertex : POSITION; // 顶点位置
float3 normal : NORMAL; // 顶点法线
float4 tangent : TANGENT; // 顶点切线
float4 texcoord : TEXCOORD0; // 纹理坐标
};
// 定义输出结构体,用于存储片段着色器的输入
struct v2f {
float4 pos : SV_POSITION; // 片元屏幕空间位置
float4 uv : TEXCOORD0; // 片元的主要纹理坐标
float3 lightDir: TEXCOORD1; // 片段光源方向
float3 viewDir : TEXCOORD2; // 片段观察方向
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex); // 将顶点从对象空间转换为裁剪空间
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; // 计算主纹理的 UV 坐标
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw; // 计算法线纹理的 UV 坐标
// Compute the binormal
TANGENT_SPACE_ROTATION; // 用于生成从模型空间到切线空间的旋转矩阵
// Transform the light direction from object space to tangent space
o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz; // 将光源方向转换到切线空间
// Transform the view direction from object space to tangent space
o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz; // 将观察方向转换到切线空间
return o;
}
fixed4 frag(v2f i) : SV_Target { // 片段着色器主函数
fixed3 tangentLightDir = normalize(i.lightDir); // 归一化切线空间的光源方向
fixed3 tangentViewDir = normalize(i.viewDir); // 归一化切线空间的观察方向
// Get the texel in the normal map
fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw); // 从法线贴图中采样一像素,取出法线信息
fixed3 tangentNormal; // 存储切线空间的法线
// 使用内置函数解压法线
tangentNormal = UnpackNormal(packedNormal); // 解压法线 (内部进行映射,因为法线需要的是[-1,1]范围,但是颜色通道是[0,1],法线贴图一半是存储的压缩的法线信息
tangentNormal.xy *= _BumpScale; // 按 BumpScale 调整法线
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy))); // 计算 Z 分量的法线信息
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb; // 从主纹理中获取漫反射颜色,并乘以颜色着色器的颜色
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; // 计算环境光
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir)); // 计算漫反射颜色
// 返回最终颜色
return fixed4(ambient + diffuse, 1.0); // 只返回环境光和漫反射光的合并结果
}
ENDCG // 结束CG代码块
}
}
FallBack "Diffuse" // 备用着色器
}
这里使用了切线空间下的法线信息,所以需要在顶点着色器中生成切线空间的旋转矩阵,计算观察方向也是为后续光照方向和法线方向的计算服务的
渐变纹理
目前为止,所学的纹理是为了渲染物体表面颜色的,但是用一张纹理图也是可以控制表面的光照信息的,回顾之前的内容,我们是用光照方面和表面法线方向点乘再乘以材质表面反射率的到漫反射的光照信息。
但是如果在tex2D这个采样函数中,采样的不是i.uv而是别的一个纹理坐标会如何呢
效果如下

Shader "basedTex/RampTex"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_RampTex ("Ramp Tex", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType" = "Opaque" }
Pass
{
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
float4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _RampTex;
float4 _RampTex_ST;
struct a2v
{
float4 vertex : POSITION;
float3 normal:NORMAL;
float4 texcoord:TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float3 worldNormal:TEXCOORD1;
float3 worldPos:TEXCOORD2;
};
v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv.xy = v.texcoord.xy*_MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy*_RampTex_ST.xy + _RampTex_ST.zw;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 worldNormal = normalize(i.worldNormal);
float3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
float3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//渐变纹理控制
fixed halfLambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;
fixed3 diffuseColor = tex2D(_RampTex, fixed2(halfLambert, halfLambert)).rgb * _Color.rgb;
fixed3 diffuse = _LightColor0.rgb * diffuseColor;
fixed4 col = tex2D(_MainTex, i.uv);
return fixed4((ambient + diffuse)*col, 1.0);
}
ENDCG
}
}
Fallback "Default"
}
这里主要是在渐变纹理控制的部分处理的,首先是用到了半兰博特光照模型,再用这个模型和ramp贴图进行一个采样最后结果和光的颜色相乘得到
遮罩纹理
我们可以使用遮罩纹理来遮罩一些不需要高光控制的部分,来实现阴影的效果,当然,遮罩还有别的一些用法,用来具体控制某些像素区域,遮罩的应用十分的广泛
下面是Cg代码段
Shader "basedTex/MaskTex"
{
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
_BumpMap ("Normal Map", 2D) = "bump" {}
_BumpScale("Bump Scale", Float) = 1.0
_SpecularMask ("Specular Mask", 2D) = "white" {}
_SpecularScale ("Specular Scale", Float) = 1.0
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float _BumpScale;
sampler2D _SpecularMask;
float _SpecularScale;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 lightDir: TEXCOORD1;
float3 viewDir : TEXCOORD2;
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
TANGENT_SPACE_ROTATION;
o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 tangentLightDir = normalize(i.lightDir);
fixed3 tangentViewDir = normalize(i.viewDir);
fixed3 tangentNormal = UnpackNormal(tex2D(_BumpMap, i.uv));
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
//颜色,环境光,基础色
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));
//计算遮罩部分
fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
// Get the mask value
fixed specularMask = tex2D(_SpecularMask, i.uv).r * _SpecularScale;
// Compute specular term with the specular mask
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss) * specularMask;
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
顶点着色器中需要解释这些
TANGENT_SPACE_ROTATION; 及后续的 o.lightDir 和 o.viewDir 相关代码的作用主要是进行从对象空间到切线空间的方向转换,以便进行正确的光照计算。下面是具体的解释:
1. 切线空间旋转
TANGENT_SPACE_ROTATION;
这行宏定义用于设置切线空间的旋转矩阵。切线空间的定义基于物体表面的一点的法线、切线和副切线(binormal),这些方向会影响如何处理法线贴图和光照。
- 切线空间(Tangent Space):这是一个局部坐标系统,通常用于处理法线贴图。当使用法线贴图时,法线的方向是以切线空间表示的,因此需要将光照和视线方向从对象空间转换为切线空间,以确保正确的法线方向。
2. 光照方向转换
o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
ObjSpaceLightDir(v.vertex)计算光源方向向量,这个向量是在对象空间内的。mul(rotation, ...)将光照方向向量从对象空间转换到切线空间。这是通过乘以之前定义的切线空间旋转矩阵rotation来完成的。- 最终,
o.lightDir将保存经过转换后的光照方向向量。
3. 视线方向转换
o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;
ObjSpaceViewDir(v.vertex)计算视线方向向量,这是观察者(相机)到物体的方向向量,同样是在对象空间内。mul(rotation, ...)将视线方向向量转换到切线空间。- 最终,
o.viewDir将保存转换后的视线方向向量。
片段代码中这些需要解释一下
-
归一化光照方向:
fixed3 tangentLightDir = normalize(i.lightDir);i.lightDir是输入的光照方向向量。通过normalize函数将光照方向归一化,使得其长度为1,这样可以更方便地进行后续的计算。
-
归一化视线方向:
fixed3 tangentViewDir = normalize(i.viewDir);i.viewDir是输入的视线方向(观察者到表面的方向)。同样使用normalize来保证方向的长度为1。
-
从法线贴图中提取法线:
fixed3 tangentNormal = UnpackNormal(tex2D(_BumpMap, i.uv));tex2D(_BumpMap, i.uv)从法线贴图(_BumpMap)中根据给定的纹理坐标i.uv读取法线数据。UnpackNormal通常是将从法线贴图中提取的值转换成正确的法线格式,法线贴图中的法线通常是以切线空间存储的,其范围一般在0到1之间。
-
调整法线缩放:
tangentNormal.xy *= _BumpScale;tangentNormal.xy表示法线的切线分量(X和Y分量)。这里通过_BumpScale对这两个分量进行缩放,调整法线的强度,使高光或起伏效果更加明显。
-
计算法线的Z分量:
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));dot(tangentNormal.xy, tangentNormal.xy)计算法线在X和Y平面的点积,得到切线法线的平方长度。- 使用
saturate函数确保该值在0到1之间,以避免出现负值。 sqrt(1.0 - ...)计算法线的Z分量。根据法线的单位化特性(归一化),一个法线的X、Y和Z分量的平方和必须等于1。因此,Z分量可以通过计算 1 减去 X和Y分量的平方和来得到。
-
计算半程向量(Half Vector):
fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);tangentLightDir是光照方向的切线表示。tangentViewDir是视线方向的切线表示。- 这行代码计算半程向量
halfDir,它是光源方向和视线方向的归一化向量(即,方向的长度调整为1)。半程向量常用于计算高光,使高光更加平滑且自然。
-
获取高光掩模值(Specular Mask):
fixed specularMask = tex2D(_SpecularMask, i.uv).r * _SpecularScale;- 这里使用
tex2D函数从_SpecularMask纹理中获取高光掩模值。该函数根据纹理坐标i.uv读取纹理数据,并从中提取红色通道的值(.r)。 _SpecularScale是一个加权因子,可以用来调整高光的强度。这使得物体的高光效果可以根据纹理进行变化。
- 这里使用
-
计算高光项:
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss) * specularMask;_LightColor0.rgb是光源的颜色。_Specular.rgb是材质的高光颜色。dot(tangentNormal, halfDir)计算切线法线与半程向量的点积,如果该值为负,则使用max(0, ...)确保不会出现负值。pow(..., _Gloss)将点积结果提升到光泽度(_Gloss)的幂次,以加大高光的强度和锐利度。- 最后,通过乘以
specularMask来调整最终的高光值。
关于顶点着色器和片段着色器
在学习顶点和片段着色器的过程中,经常对这些属性的定义和为什么放在这里感到相当懵逼。查阅了资料逐渐知道了一些原理。顶点着色器的职责类似于摄像机,将物体摆好,根据摄像机的位置和物体位置准备拍照(顶点变换(模型空间转为裁剪空间),空间变换什么的),而片段着色器则是按下快门的瞬间做的对光照,纹理,反射的一些像素级别计算。
顶点着色器(Vertex Shader)
功能与作用
-
顶点处理:
- 顶点着色器负责处理几何体的每一个顶点。它接收顶点数据,如位置、法线、切线和纹理坐标,通过计算将它们转换到裁剪空间以便后续处理。
-
变换计算:
- 通过执行矩阵乘法,顶点着色器将顶点从局部模型空间转换为视图空间和裁剪空间。这一过程通常会涉及将顶点位置乘以模型视图投影矩阵。
-
插值准备:
- 顶点着色器输出的数据会在 rasterization(光栅化)过程中进行插值,用于在片段着色器中处理中间生成像素(片段)。这包括纹理坐标、法线、颜色等。
-
传递数据:
- 顶点着色器将处理结果(如变换后的顶点位置、纹理坐标、光照方向等)传递到片段着色器,以供后续计算使用。
片段着色器(Fragment Shader)
功能与作用
-
片段处理:
- 片段着色器处理光栅化生成的每个片段(即计算产生的像素)。每个片段代表了模型在屏幕上的一个小区域。
-
颜色计算与纹理映射:
- 负责使用传递的纹理坐标从纹理中采样颜色,计算漫反射、高光、环境光等光照效果,从而生成最终的片段颜色。
-
深度与模板测试:
- 在片段着色器中,可以设置片段的深度值,以便在深度缓冲区进行深度测试,决定片段是否可见。
-
导出最终颜色:
- 片段着色器的输出是最终渲染的颜色,可直接显示在屏幕上。可以根据不同条件调整颜色、透明度等。
总结
- 顶点着色器主要负责将每个顶点的空间位置转换并准备数据供片段着色器使用,包括进行变换、插值准备等。
- 片段着色器则将焦点转向如何在屏幕上正确着色每个片段,包括计算表面颜色、进行光照计算等,最终决定像素的颜色和透明度。
透明度,模版测试,深度测试,混合
1. 深度测试(Depth Test)
作用
-
解决物体前后遮挡问题:确保离相机近的物体会遮挡后面的物体
-
性能优化:通过提前丢弃被遮挡的片段(fragment),避免不必要的着色计算
工作原理
-
每个片段都有深度值(z值)
-
比较当前片段深度值与深度缓冲区中的值
-
根据比较函数(DepthFunction)决定是否保留:
-
GL_LESS(默认):当前片段深度 < 缓冲区深度 → 保留 -
GL_LEQUAL、GL_GREATER等不同比较方式
-
代码控制(Unity Shader)
hlsl
ZTest Less | Greater | LEqual | GEqual | Equal | NotEqual | Always ZWrite On | Off
2. 模板测试(Stencil Test)
作用
-
选择性渲染:只在特定区域渲染内容
-
特殊效果:如镜子、轮廓效果、UI遮罩等
-
多步骤渲染:标记特定区域进行后续处理
工作原理
-
使用8位模板缓冲区(值范围0-255)
-
比较当前片段模板值与缓冲区中的值
-
根据比较结果决定是否丢弃片段
常见操作
-
比较(Compare)
-
通过后的操作(Pass)
-
未通过的操作(Fail)
-
深度测试失败时的操作(ZFail)
代码示例(Unity Shader)
Stencil {
Ref 1
Comp Equal
Pass Keep
Fail Zero
ZFail Replace
}
3. 透明度处理(Alpha Handling)
两种透明处理方式
-
Alpha Test(已弃用,现为Clip)
-
完全透明或完全不透明
-
使用
clip()函数丢弃片段
hlsl
clip(color.a - 0.5); // 透明度<0.5时完全丢弃
-
-
Alpha Blend
-
真正的半透明效果
-
需要处理渲染顺序(从后向前渲染)
-
4. 混合(Blending)
作用
-
处理半透明物体:将当前片段颜色与帧缓冲中已有颜色混合
-
特殊效果:叠加、滤色、加法混合等效果
混合公式
最终颜色 = (源颜色 × 源因子) OP (目标颜色 × 目标因子)
其中OP通常是加操作(GL_FUNC_ADD)
常用混合模式
hlsl
// 标准alpha混合(透明) Blend SrcAlpha OneMinusSrcAlpha // 加法混合(光效) Blend One One // 乘法混合 Blend DstColor Zero // 预乘alpha混合 Blend One OneMinusSrcAlpha
完整混合参数
hlsl
复制
下载
Blend [SrcFactor] [DstFactor], [SrcFactorA] [DstFactorA] BlendOp [Op], [OpAlpha]
各环节执行顺序
在输出合并阶段,这些测试和操作按照固定顺序执行:
-
模板测试 → 2. 深度测试 → 3. 混合操作
只有通过了前面的测试,才会进行后续的处理。这种顺序设计是为了最大程度优化性能,尽早丢弃不需要处理的片段。
实际应用案例
1. 角色轮廓效果(使用模板测试)
hlsl
// 第一次渲染:标记角色区域
Stencil {
Ref 1
Comp Always
Pass Replace
}
// 第二次渲染:只在标记区域绘制轮廓
Stencil {
Ref 1
Comp Equal
}
2. 半透明玻璃效果
// 定义shader名称 - 这个名称会在Unity的材质选择器中显示
Shader "Custom/MobileGlass"
{
// Properties块:定义在Inspector中可以调整的参数
// 这些参数可以在运行时动态调整,也可以在材质面板中手动设置
Properties {
// 主纹理贴图 - 用于玻璃表面的基础纹理
// 2D表示这是一个2D纹理
// "white" {} 是默认值,表示如果没有指定纹理就使用白色
_MainTex ("Texture", 2D) = "white" {}
// 玻璃颜色 - 用于调整玻璃的整体色调
// Color类型包含RGBA四个分量
// (1,1,1,0.5) 表示白色,alpha为0.5(半透明)
_Color ("Glass Color", Color) = (1,1,1,0.5)
// 透明度 - 控制玻璃的透明程度
// Range(0, 1) 表示在Inspector中会显示一个0-1的滑动条
// 0表示完全透明,1表示完全不透明
_Transparency ("Transparency", Range(0, 1)) = 0.5
// 反射强度 - 控制环境反射的强度
// 0表示没有反射,1表示完全反射
_ReflectionStrength ("Reflection Strength", Range(0, 1)) = 0.3
// 菲涅尔效果强度 - 控制边缘高光的锐利程度
// 值越大,边缘高光越明显
// 0-10的范围允许精细调节
_FresnelPower ("Fresnel Power", Range(0, 10)) = 3
}
// SubShader块:定义渲染管线
// 一个shader可以包含多个SubShader,Unity会按顺序尝试使用
SubShader {
// 渲染标签设置 - 告诉Unity如何渲染这个shader
Tags {
// 渲染队列 - 决定渲染顺序
// "Transparent" 表示在透明队列中渲染
// 透明对象通常在最后渲染,以确保正确的混合效果
"Queue"="Transparent"
// 渲染类型 - 用于后处理效果和渲染路径选择
// "Transparent" 表示这是一个透明shader
"RenderType"="Transparent"
// 忽略投影器 - 防止透明对象被投影器影响
// 透明对象通常不需要投影
"IgnoreProjector"="True"
}
// Pass块:定义渲染通道
// 一个SubShader可以包含多个Pass,每个Pass都会执行一次渲染
Pass {
// 渲染状态设置 - 控制GPU的渲染状态
// 深度写入 - 控制是否写入深度缓冲区
// Off表示不写入深度缓冲区
// 对于透明对象,通常关闭深度写入以避免排序问题
ZWrite Off
// 混合模式 - 定义如何将当前像素与背景像素混合
// SrcAlpha:使用源(当前像素)的alpha值
// OneMinusSrcAlpha:使用1减去源alpha值作为背景的混合因子
// 公式:FinalColor = SrcColor * SrcAlpha + DstColor * (1 - SrcAlpha)
Blend SrcAlpha OneMinusSrcAlpha
// CG程序块开始 - 使用HLSL/CG语言编写着色器代码
CGPROGRAM
// 编译指令 - 告诉Unity编译器如何处理这个shader
// 指定顶点着色器函数名
// vert函数会在每个顶点上执行一次
#pragma vertex vert
// 指定片元着色器函数名
// frag函数会在每个像素上执行一次
#pragma fragment frag
// 包含Unity内置函数库
// UnityCG.cginc包含了Unity提供的常用函数和宏
#include "UnityCG.cginc"
// 声明Properties中定义的变量
// 这些变量会自动与Properties中的参数关联
// 主纹理采样器 - 用于从纹理中读取颜色值
sampler2D _MainTex;
// 玻璃颜色 - 用于调整玻璃的整体色调
// fixed4是低精度的4分量向量,适合移动端
fixed4 _Color;
// 透明度 - 控制玻璃的透明程度
float _Transparency;
// 反射强度 - 控制环境反射的强度
float _ReflectionStrength;
// 菲涅尔效果强度 - 控制边缘高光的锐利程度
float _FresnelPower;
// 顶点着色器输出结构体
// 这个结构体定义了从顶点着色器传递给片元着色器的数据
struct v2f {
// 裁剪空间位置 - 顶点在裁剪空间中的坐标
// SV_POSITION是系统语义,表示这是最终的位置
float4 pos : SV_POSITION;
// UV坐标 - 用于纹理采样
// TEXCOORD0是用户定义的语义,表示纹理坐标
float2 uv : TEXCOORD0;
// 世界空间法向量 - 用于光照计算
// 法向量定义了表面的朝向
float3 worldNormal : TEXCOORD1;
// 世界空间视线方向 - 从表面点到摄像机的方向
// 用于反射和菲涅尔效果计算
float3 worldViewDir : TEXCOORD2;
};
// 顶点着色器函数
// 这个函数在每个顶点上执行一次
// appdata_base是Unity提供的输入结构体,包含顶点数据
v2f vert(appdata_base v) {
v2f o; // 创建输出结构体
// 将顶点从对象空间转换到裁剪空间
// UnityObjectToClipPos是Unity提供的函数
// 它执行了:对象空间 -> 世界空间 -> 视图空间 -> 裁剪空间
o.pos = UnityObjectToClipPos(v.vertex);
// 传递UV坐标
// v.texcoord是顶点的纹理坐标
o.uv = v.texcoord;
// 将法向量从对象空间转换到世界空间
// UnityObjectToWorldNormal是Unity提供的函数
// 它考虑了对象的旋转和缩放
o.worldNormal = UnityObjectToWorldNormal(v.normal);
// 计算世界空间视线方向
// 这个过程分为几个步骤:
// 1. mul(unity_ObjectToWorld, v.vertex).xyz:将顶点转换到世界空间
// 2. UnityWorldSpaceViewDir:计算从世界空间顶点到摄像机的方向
// 3. normalize:标准化向量,确保长度为1
o.worldViewDir = normalize(UnityWorldSpaceViewDir(mul(unity_ObjectToWorld, v.vertex).xyz));
return o; // 返回处理后的数据
}
// 片元着色器函数
// 这个函数在每个像素上执行一次
// v2f i是顶点着色器传递过来的数据
// SV_Target表示输出到渲染目标
fixed4 frag(v2f i) : SV_Target {
// 计算菲涅尔效果
// 菲涅尔效果模拟了真实世界中光线在不同角度下的反射行为
// 1. normalize(i.worldNormal):标准化法向量
// 2. dot(normalize(i.worldNormal), i.worldViewDir):计算法向量和视线方向的点积
// 点积结果范围是-1到1,表示两个向量的夹角
// 3. saturate(...):将值限制在0-1范围内
// 当视线与法向量平行时,结果为1;垂直时,结果为0
// 4. 1.0 - saturate(...):反转结果
// 当视线与法向量平行时,结果为0;垂直时,结果为1
// 5. pow(..., _FresnelPower):应用菲涅尔强度
// 值越大,边缘高光越锐利
float fresnel = pow(1.0 - saturate(dot(normalize(i.worldNormal), i.worldViewDir)), _FresnelPower);
// 简单的反射计算
// 反射模拟了光线从表面反弹的现象
// 1. reflect(-i.worldViewDir, normalize(i.worldNormal)):计算反射方向
// -i.worldViewDir:入射光线方向(视线方向的反向)
// nor
3. 深度遮挡高亮
hlsl
ZTest Greater // 只渲染被遮挡的部分
性能优化建议
-
合理使用深度测试:
-
不透明物体保持
ZWrite On -
半透明物体使用
ZWrite Off
-
-
模板测试开销:
-
只在必要时使用
-
简单遮罩效果可考虑使用Shader中的clip代替
-
-
混合性能消耗:
-
半透明物体数量尽可能少
-
确保从后向前正确排序渲染
-
-
提前拒绝片段:
-
在片段着色器之前使用
AlphaToMask或clip尽早丢弃片段
-
透明效果
通常有两种实现透明度的方式,一种是透明度测试(AlphaTest,但是无法得到半透明效果)还有一种则是透明度混合(Alpha Blending)
1. 透明度测试(Alpha Test/Clip)
核心特点
-
二元性处理:像素要么完全透明(被丢弃),要么完全不透明(保留)
-
无混合计算:不进行颜色混合计算,性能开销相对较小
-
使用
clip()函数:在Shader中直接丢弃低于阈值的片段
工作原理
hlsl
// 在片段着色器中使用clip函数
fixed4 frag(v2f i) : SV_Target {
fixed4 col = tex2D(_MainTex, i.uv);
clip(col.a - _Cutoff); // 当alpha < _Cutoff时完全丢弃该片段
return col;
}
优点
-
性能较好(不需要混合计算)
-
不会出现渲染顺序问题
-
适合边缘硬切的透明效果(如树叶、栅栏等)
缺点
-
边缘锯齿明显(需要配合MSAA或Alpha To Coverage)
-
无法实现真正的半透明效果
-
过渡生硬,没有平滑的透明度渐变
适用场景
-
带有镂空的纹理(植物、铁丝网等)
-
需要完全透明或完全不透明的物体
-
性能敏感的场景
2. 透明度混合(Alpha Blend)
核心特点
-
真正的半透明:可以实现0到1之间的任意透明度
-
颜色混合:当前片段颜色与背景颜色按比例混合
-
渲染顺序敏感:必须从后向前渲染
工作原理
hlsl
// 在SubShader或Pass中定义混合模式
SubShader {
Tags {"Queue" = "Transparent"}
Blend SrcAlpha OneMinusSrcAlpha // 标准alpha混合
ZWrite Off
Pass {
// ...着色器代码...
}
}
混合公式
最终颜色 = (源颜色 × 源Alpha) + (目标颜色 × (1 - 源Alpha))
优点
-
能实现真实的半透明效果
-
边缘过渡平滑自然
-
适合玻璃、液体等需要渐变透明的材质
缺点
-
性能开销较大(需要混合计算)
-
必须正确排序渲染(从远到近)
-
深度写入(ZWrite)通常需要关闭
-
容易出现渲染顺序问题(特别是多层半透明物体叠加)
适用场景
-
玻璃、水面等透明材质
-
粒子效果(烟雾、火焰等)
-
UI元素的半透明效果
-
需要颜色叠加的特殊效果
关键区别对比表
| 特性 | 透明度测试(Alpha Test) | 透明度混合(Alpha Blend) |
|---|---|---|
| 透明效果 | 全有或全无 | 真正的半透明 |
| 性能 | 较高(无混合计算) | 较低(需要混合计算) |
| 边缘质量 | 可能有锯齿 | 平滑过渡 |
| 深度写入 | 通常开启 | 通常关闭 |
| 渲染顺序 | 不影响(不透明物体方式) | 必须从后向前渲染 |
| 实现方式 | 使用clip函数丢弃片段 | 设置Blend混合模式 |
| 典型应用 | 镂空物体、植被 | 玻璃、液体、粒子效果 |
选择建议
-
使用透明度测试(Clip)当:
-
需要完全透明或完全不透明的效果
-
处理带有镂空图案的纹理
-
性能是关键考虑因素
-
不需要平滑的透明度过渡
-
-
使用透明度混合(Blend)当:
-
需要真实的半透明效果
-
实现玻璃、液体等材质
-
制作粒子特效
-
可以接受额外的性能开销
-
进阶技巧
-
Alpha To Coverage:
-
将透明度测试与多重采样抗锯齿(MSAA)结合
-
改善透明度测试的边缘锯齿问题
AlphaToMask On
-
-
预乘Alpha(Premultiplied Alpha):
-
特殊的混合方式,避免颜色渗漏
Blend One OneMinusSrcAlpha
-
-
复杂混合模式:
-
加法混合:
Blend One One(用于光效) -
乘法混合:
Blend DstColor Zero(用于阴影等)
-
以上介绍了关于透明度测试和透明度混合的相关信息,由于关闭了深度写入,因此我们需要很小心物体的渲染顺序,在Unity中,我们也不难发现在材质的面板上有渲染队列的一栏(默认2000)当然还有其他的队列比如:
1.Background(1000) 在其他队列之前渲染,通常绘制背景
2.Geometry(2000) 默认渲染队列
3.AlphaTest(2450)需要透明度测试的物体用这个队列
4.Transparent(3000)这个队列会在所有Geometry和AlphaTest物体渲染之后再从后往前渲染,适用于透明度混合的物体
5.Overlay(4000)这个队列实现一些叠加效果,需要最后渲染的物体使用这个队列
AlphaTest:
Shader "Transparency/Alpha Test" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
_Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5
}
SubShader {
Tags {"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}
Pass {
Tags { "LightMode"="ForwardBase" }
// Turn off culling
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _Cutoff;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed4 texColor = tex2D(_MainTex, i.uv);
clip (texColor.a - _Cutoff);
fixed3 albedo = texColor.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
return fixed4(ambient + diffuse, 1.0);
}
ENDCG
}
}
FallBack "Transparent/Cutout/VertexLit"
}
AlphaBlend:
Shader "transparency/Alpha Blend" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
_AlphaScale ("Alpha Scale", Range(0, 1)) = 1
}
SubShader {
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
Pass {
Tags { "LightMode"="ForwardBase" }
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed4 texColor = tex2D(_MainTex, i.uv);
fixed3 albedo = texColor.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
}
ENDCG
}
}
FallBack "Transparent/VertexLit"
}
一、混合因子 (Blend Factors)
混合因子决定了源颜色(当前片段颜色)和目标颜色(已存在于颜色缓冲中的颜色)如何参与混合计算。
1. 源颜色混合因子 (Source Factor)
| 混合因子 | 描述 |
|---|---|
One | 使用值1.0(完全不透明) |
Zero | 使用值0.0(完全透明) |
SrcColor | 使用源颜色的RGB值 |
SrcAlpha | 使用源颜色的Alpha值 |
DstColor | 使用目标颜色的RGB值 |
DstAlpha | 使用目标颜色的Alpha值 |
OneMinusSrcColor | 使用(1 - 源颜色RGB) |
OneMinusSrcAlpha | 使用(1 - 源Alpha) |
OneMinusDstColor | 使用(1 - 目标颜色RGB) |
OneMinusDstAlpha | 使用(1 - 目标Alpha) |
2. 目标颜色混合因子 (Destination Factor)
与源因子相同,但应用于目标颜色。
二、混合操作 (Blend Operations)
混合操作定义了源和目标颜色如何组合在一起。
标准混合操作
| 混合操作 | 描述 | 公式 |
|---|---|---|
Add | 源和目标相加 | Final = Src * SrcFactor + Dst * DstFactor |
Sub | 源减去目标 | Final = Src * SrcFactor - Dst * DstFactor |
RevSub | 目标减去源 | Final = Dst * DstFactor - Src * SrcFactor |
Min | 取各分量最小值 | Final = min(Src, Dst) |
Max | 取各分量最大值 | Final = max(Src, Dst) |
高级混合操作 (需要 OpenGL ES 3.0 或更高)
| 混合操作 | 描述 |
|---|---|
LogicalClear | 清除目标 |
LogicalSet | 直接设置 |
LogicalCopy | 复制源到目标 |
LogicalCopyInverted | 复制源的补码 |
LogicalNoop | 保持目标不变 |
LogicalInvert | 反转目标 |
LogicalAnd | 按位与 |
LogicalNand | 按位与非 |
LogicalOr | 按位或 |
LogicalNor | 按位或非 |
LogicalXor | 按位异或 |
LogicalEquiv | 按位等价 |
LogicalAndReverse | 反向按位与 |
LogicalAndInverted | 反转源后按位与 |
LogicalOrReverse | 反向按位或 |
LogicalOrInverted | 反转源后按位或 |
三、常用混合模式示例
1. 标准Alpha混合(透明效果)
hlsl
Blend SrcAlpha OneMinusSrcAlpha
公式:Final = Src.rgb * Src.a + Dst.rgb * (1 - Src.a)
2. 加法混合(光效)
hlsl
Blend One One
公式:Final = Src.rgb * 1 + Dst.rgb * 1
3. 乘法混合(阴影/染色)
hlsl
Blend DstColor Zero
公式:Final = Src.rgb * Dst.rgb + Dst.rgb * 0
4. 预乘Alpha混合
hlsl
Blend One OneMinusSrcAlpha
公式:Final = Src.rgb * 1 + Dst.rgb * (1 - Src.a)
5. 软加法混合
hlsl
Blend OneMinusDstColor One
公式:Final = Src.rgb * (1 - Dst.rgb) + Dst.rgb * 1
四、独立控制颜色和Alpha通道
可以分别指定颜色和Alpha的混合模式:
hlsl
Blend SrcAlpha OneMinusSrcAlpha, One Zero BlendOp Add, Max
这表示:
-
颜色通道:
Add操作,SrcAlpha和OneMinusSrcAlpha因子 -
Alpha通道:
Max操作,One和Zero因子
五、混合状态设置
在ShaderLab中,完整的混合语法如下:
hlsl
Blend [SrcFactor] [DstFactor] // 颜色混合 Blend [SrcFactorRGB] [DstFactorRGB], [SrcFactorA] [DstFactorA] // 分别控制RGB和A BlendOp [ColorOp], [AlphaOp] // 混合操作
通过这些能实现和Ps一样正片叠底,变暗,变淡,柔光等一些效果
=================================中级篇===================================
前向渲染与延迟渲染的区别
一、核心原理差异
前向渲染 (Forward Rendering)
-
单阶段着色:在单个渲染通道中同时处理几何体和光照
-
逐物体光照:每个物体单独计算所有影响它的光源
-
多通道处理:复杂光照可能需要多次渲染通道
延迟渲染 (Deferred Rendering)
-
两阶段架构:
-
几何阶段:将所有可见表面的几何信息(位置、法线、材质等)存储到G-Buffer
-
光照阶段:基于G-Buffer数据独立计算光照
-
-
屏幕空间计算:光照在屏幕空间而非物体空间计算
-
单光源单通道:每个光源独立计算,互不影响
二、技术实现对比
| 特性 | 前向渲染 | 延迟渲染 |
|---|---|---|
| 渲染流程 | 几何+光照同时处理 | 先几何后光照 |
| 数据存储 | 无中间存储 | 使用G-Buffer存储几何信息 |
| 光照计算 | 在物体着色器中完成 | 在屏幕空间后处理完成 |
| MSAA支持 | 原生支持 | 需要特殊处理(如TAA) |
| 透明物体 | 天然支持 | 需要额外前向渲染通道 |
| 带宽需求 | 较低 | 较高(G-Buffer读写) |
| 内存占用 | 较低 | 较高(G-Buffer占用显存) |
三、光照处理能力
前向渲染的光照限制
-
逐像素光源数受限:Unity默认最多4个逐像素光
-
光照计算复杂度:O(物体数 × 光源数)
-
解决方案:
-
重要光源使用逐像素光照
-
次要光源使用逐顶点或球谐光照
-
使用光照贴图烘焙静态光
-
延迟渲染的光照优势
-
理论上无限光源:每个光源独立计算
-
光照计算复杂度:O(屏幕像素数 × 光源数)
-
统一处理:所有光源都是逐像素光
-
高效剔除:可基于屏幕区域和深度剔除不可见光源
四、性能特性对比
前向渲染性能特点
-
适合场景:
-
光源数量少(特别是动态光)
-
需要高质量抗锯齿
-
透明物体多的场景
-
-
瓶颈:过度绘制(Overdraw)问题严重
延迟渲染性能特点
-
适合场景:
-
大量动态光源
-
复杂材质效果
-
需要多后期处理效果
-
-
瓶颈:G-Buffer带宽和显存占用,不支持真正的抗锯齿MSAA
五、画质与效果支持
| 效果 | 前向渲染 | 延迟渲染 |
|---|---|---|
| 抗锯齿 | 完美支持MSAA | 需要FXAA/TAA等后处理AA |
| 透明物体 | 原生支持 | 需要额外前向通道 |
| 多光照组合 | 需要复杂Shader | 天然支持 |
| 材质多样性 | 灵活但性能敏感 | 受G-Buffer限制(通常4-8个通道) |
| 动态阴影 | 每个光源单独计算 | 统一阴影处理 |
六、Unity中的具体实现
前向渲染设置
// Camera设置
camera.renderingPath = RenderingPath.Forward;
// Shader中的光照处理
#pragma multi_compile_fwdbase
#pragma multi_compile_fwdadd
延迟渲染设置
// Camera设置
camera.renderingPath = RenderingPath.DeferredShading;
// 需要Shader支持
Shader "Deferred" {
Properties {
_Color ("Color", Color) = (1,1,1,1)
}
SubShader {
Tags { "RenderType"="Opaque" }
Pass {
// 填充G-Buffer的Pass
}
}
}
[这里我自己写的和参考教程一般是在pass的tag里选择的ForwardBase什么的],除此之外还可以通过Unity的Edit -> Project Settings -> Player -> OtherSettings -> Rendering path中选择需要的渲染路径,如果使用多个不同的渲染路径则是在Camera的面板中调整
七、选择建议
使用前向渲染当:
-
项目需要大量透明物体
-
使用MSAA抗锯齿
-
场景中动态光源较少
-
目标平台内存/带宽有限(移动平台)
使用延迟渲染当:
-
场景需要大量动态光源
-
需要复杂屏幕空间效果(SSR, SSAO等)
-
硬件支持良好(桌面/主机平台)
-
材质系统相对统一
八、混合方案
Unity还提供延迟光照(Deferred Lighting)作为折中方案:
-
仅延迟计算光照,前向计算着色
-
平衡了性能与效果
-
比完整延迟渲染更节省带宽
一、主要渲染路径类型
1. 前向渲染 (Forward Rendering)
-
标签:
"LightMode" = "ForwardBase"或"ForwardAdd" -
URP下的标签为"LightMode" = "UniversalForward" "LightMode" = "SRPDefaultUnlit"等等 -
特点:
-
基础通道处理环境光、主方向光和光照贴图
-
附加通道处理每个额外逐像素光
-
适合透明物体和移动平台
-
2. 延迟渲染 (Deferred Shading)
-
标签:
"LightMode" = "Deferred" -
特点:
-
使用 G-Buffer 存储几何信息
-
单独的光照计算阶段
-
适合大量动态光源的场景
-
3. 延迟光照 (Deferred Lighting)
-
标签:
"LightMode" = "PrepassBase"或"PrepassFinal" -
特点:
-
混合方法,只延迟光照计算
-
比完整延迟渲染更节省内存
-
Unity 2018+ 已逐渐淘汰此路径
-
4. 顶点光照 (Vertex Lit)
-
标签:
"LightMode" = "Vertex" -
特点:
-
最简单的渲染路径
-
所有光照在顶点阶段计算
-
适用于低端硬件
-
5. 阴影投射 (Shadow Caster)
-
标签:
"LightMode" = "ShadowCaster" -
用途:
-
专门渲染到阴影贴图
-
所有需要投射阴影的物体都需要此Pass
-
6. 运动矢量 (Motion Vectors)
-
标签:
"LightMode" = "MotionVectors" -
用途:
-
生成运动矢量用于运动模糊或TAA
-
需要记录每帧物体运动数据
-
二、重要ShaderLab标签详解
通用渲染标签
hlsl
Tags {
"RenderType" = "Opaque" // 控制渲染队列和后处理
"Queue" = "Geometry" // 渲染顺序控制
"DisableBatching" = "True" // 是否禁用动态批处理
"ForceNoShadowCasting" = "True" // 禁用阴影投射
"IgnoreProjector" = "True" // 忽略Projector影响
}
渲染路径相关标签
hlsl
// 前向渲染
Pass {
Tags { "LightMode" = "ForwardBase" } // 主光源Pass
// 或
Tags { "LightMode" = "ForwardAdd" } // 附加光源Pass
}
// 延迟渲染
Pass {
Tags { "LightMode" = "Deferred" } // G-Buffer填充Pass
}
// 阴影Pass
Pass {
Tags { "LightMode" = "ShadowCaster" }
}
// 深度/法线纹理生成
Pass {
Tags { "LightMode" = "DepthOnly" } // 或 "DepthNormals"
}
三、特殊用途渲染路径
1. SRP兼容路径 (Scriptable Render Pipeline)
-
轻量级渲染管线(LWRP/URP)标签:
Tags { "RenderPipeline" = "UniversalPipeline" } -
高清渲染管线(HDRP)标签:
Tags { "RenderPipeline" = "HDRenderPipeline" }
2. 自定义G-Buffer路径
Pass {
Tags { "LightMode" = "CustomGBuffer" }
// 自定义G-Buffer布局
}
3. 屏幕空间反射(SSR)路径
Pass {
Tags { "LightMode" = "SSRReflection" }
}
四、RenderType标签详解
RenderType标签对后期处理非常重要:
| RenderType值 | 用途描述 |
|---|---|
| "Opaque" | 不透明物体(默认) |
| "Transparent" | 透明物体 |
| "TransparentCutout" | AlphaTest透明物体 |
| "Background" | 天空盒等背景物体 |
| "Overlay" | UI覆盖层、镜头光晕等 |
| "TreeOpaque" | 树木不透明部分 |
| "TreeTransparent" | 树木透明部分(如树叶) |
| "Grass" | 草地 |
| "GrassBillboard" | 广告牌式草地 |
shader动画(时间函数以及其应用)
首先大概介绍一下支持动画的关键时间函数
我来详细介绍Unity ShaderLab中的时间函数,这些函数在制作动画效果时非常有用。
## Unity内置时间变量
1. `_Time` - 基础时间变量
// _Time是一个float4向量,包含四个时间值
float4 _Time; // (t/20, t, t*2, t*3) 其中t是时间
// 各个分量的含义:
_Time.x // t/20 - 较慢的时间变化
_Time.y // t - 标准时间(秒)
_Time.z // t*2 - 较快的时间变化
_Time.w // t*3 - 最快的时间变化
```
### 2. `_SinTime` - 正弦时间
```hlsl
// _SinTime是一个float4向量,包含正弦值
float4 _SinTime; // (sin(t/8), sin(t/4), sin(t/2), sin(t))
// 各个分量的含义:
_SinTime.x // sin(t/8) - 最慢的正弦波
_SinTime.y // sin(t/4) - 较慢的正弦波
_SinTime.z // sin(t/2) - 较快的正弦波
_SinTime.w // sin(t) - 标准正弦波
```
### 3. `_CosTime` - 余弦时间
```hlsl
// _CosTime是一个float4向量,包含余弦值
float4 _CosTime; // (cos(t/8), cos(t/4), cos(t/2), cos(t))
// 各个分量的含义:
_CosTime.x // cos(t/8) - 最慢的余弦波
_CosTime.y // cos(t/4) - 较慢的余弦波
_CosTime.z // cos(t/2) - 较快的余弦波
_CosTime.w // cos(t) - 标准余弦波
```
### 4. `unity_DeltaTime` - 帧时间差
```hlsl
// unity_DeltaTime是一个float4向量,包含时间差信息
float4 unity_DeltaTime; // (dt, 1/dt, smoothDt, 1/smoothDt)
// 各个分量的含义:
unity_DeltaTime.x // dt - 当前帧的时间差
unity_DeltaTime.y // 1/dt - 当前帧的倒数(FPS)
unity_DeltaTime.z // smoothDt - 平滑的时间差
unity_DeltaTime.w // 1/smoothDt - 平滑FPS
```
## 实际应用示例
### 1. 简单的颜色变化动画
```hlsl
Shader "Custom/ColorAnimation"
{
Properties {
_Color ("Color", Color) = (1,1,1,1)
_Speed ("Animation Speed", Range(0.1, 10)) = 1
}
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
fixed4 _Color;
float _Speed;
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert(appdata_base v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
fixed4 frag(v2f i) : SV_Target {
// 使用_SinTime创建颜色变化
float3 animatedColor = _Color.rgb * (0.5 + 0.5 * sin(_SinTime.w * _Speed));
return fixed4(animatedColor, _Color.a);
}
ENDCG
}
}
}
```
### 2. 波浪效果
```hlsl
Shader "Custom/WaveEffect"
{
Properties {
_MainTex ("Texture", 2D) = "white" {}
_WaveSpeed ("Wave Speed", Range(0.1, 5)) = 1
_WaveAmplitude ("Wave Amplitude", Range(0, 1)) = 0.1
_WaveFrequency ("Wave Frequency", Range(1, 50)) = 10
}
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float _WaveSpeed;
float _WaveAmplitude;
float _WaveFrequency;
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert(appdata_base v) {
v2f o;
// 创建波浪效果
float wave = sin(v.vertex.x * _WaveFrequency + _Time.y * _WaveSpeed) * _WaveAmplitude;
v.vertex.y += wave;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
fixed4 frag(v2f i) : SV_Target {
return tex2D(_MainTex, i.uv);
}
ENDCG
}
}
}
```
### 3. 旋转动画
```hlsl
Shader "Custom/RotationAnimation"
{
Properties {
_MainTex ("Texture", 2D) = "white" {}
_RotationSpeed ("Rotation Speed", Range(0, 10)) = 1
}
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float _RotationSpeed;
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert(appdata_base v) {
v2f o;
// 计算旋转角度
float angle = _Time.y * _RotationSpeed;
float cosA = cos(angle);
float sinA = sin(angle);
// 应用旋转变换
float2 rotatedUV = float2(
v.texcoord.x * cosA - v.texcoord.y * sinA,
v.texcoord.x * sinA + v.texcoord.y * cosA
);
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = rotatedUV;
return o;
}
fixed4 frag(v2f i) : SV_Target {
return tex2D(_MainTex, i.uv);
}
ENDCG
}
}
}
```
### 4. 呼吸效果
```hlsl
Shader "Custom/BreathingEffect"
{
Properties {
_MainTex ("Texture", 2D) = "white" {}
_BreathingSpeed ("Breathing Speed", Range(0.1, 5)) = 1
_BreathingIntensity ("Breathing Intensity", Range(0, 1)) = 0.1
}
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float _BreathingSpeed;
float _BreathingIntensity;
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert(appdata_base v) {
v2f o;
// 创建呼吸效果(缩放)
float breathing = 1.0 + sin(_Time.y * _BreathingSpeed) * _BreathingIntensity;
v.vertex.xyz *= breathing;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
fixed4 frag(v2f i) : SV_Target {
return tex2D(_MainTex, i.uv);
}
ENDCG
}
}
}
```
### 5. 序列帧动画(改进版)
```hlsl
Shader "Custom/SequenceAnimation"
{
Properties {
_MainTex ("Image Sequence", 2D) = "white" {}
_HorizontalAmount ("Horizontal Amount", Float) = 4
_VerticalAmount ("Vertical Amount", Float) = 4
_Speed ("Speed", Range(0.1, 100)) = 30
_Loop ("Loop", Range(0, 1)) = 1
}
SubShader {
Tags {"Queue"="Transparent" "RenderType"="Transparent"}
Blend SrcAlpha OneMinusSrcAlpha
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float _HorizontalAmount;
float _VerticalAmount;
float _Speed;
float _Loop;
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert(appdata_base v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
fixed4 frag(v2f i) : SV_Target {
// 计算总帧数
float totalFrames = _HorizontalAmount * _VerticalAmount;
// 计算当前时间帧
float time = _Time.y * _Speed;
// 处理循环
if (_Loop > 0.5) {
time = fmod(time, totalFrames);
} else {
time = min(time, totalFrames - 1);
}
// 计算行列
float row = floor(time / _HorizontalAmount);
float column = fmod(time, _HorizontalAmount);
// 计算UV
float2 uv = i.uv;
uv.x = (uv.x + column) / _HorizontalAmount;
uv.y = (uv.y - row) / _VerticalAmount;
return tex2D(_MainTex, uv);
}
ENDCG
}
}
}
```
## 时间函数的特点和注意事项
1. **性能考虑**:时间函数是全局变量,性能开销很小
2. **精度**:`_Time.y`提供秒级精度,适合大多数动画需求
3. **范围**:时间值会持续增长,注意处理大数值可能导致的精度问题
4. **同步**:所有使用相同时间变量的shader会同步播放
5. **暂停**:当游戏暂停时,时间函数也会暂停
纹理动画
从简单的序列帧动画开始吧,动画的话顾名思义是由一个个图片组成的,通过时间函数一帧帧播放,基于这个原理,我们先从序列帧动画开始入门shader动画
火焰的序列帧动画
// 定义shader名称
Shader "Unlit/VertexAni0"
{
// Properties块:定义在Inspector中可以调整的参数
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1) // 颜色调整参数,默认白色
_MainTex ("Image Sequence", 2D) = "white" {} // 图像序列纹理,用于动画
_HorizontalAmount ("Horizontal Amount", Float) = 4 // 水平方向的图片数量
_VerticalAmount ("Vertical Amount", Float) = 4 // 垂直方向的图片数量
_Speed ("Speed", Range(1, 100)) = 30 // 播放速度,范围1-100,默认30
}
// SubShader块:定义渲染管线
SubShader {
// 渲染标签设置
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
// Queue=Transparent:在透明队列中渲染
// IgnoreProjector=True:忽略投影器
// RenderType=Transparent:标记为透明渲染类型
// Pass块:定义渲染通道
Pass {
Tags { "LightMode"="ForwardBase" } // 使用前向渲染基础光照模式
// 渲染状态设置
ZWrite Off // 关闭深度写入
Blend SrcAlpha OneMinusSrcAlpha // 设置混合模式:源alpha + (1-源alpha)*目标
// CG程序块开始
CGPROGRAM
// 编译指令
#pragma vertex vert // 指定顶点着色器函数名
#pragma fragment frag // 指定片元着色器函数名
// 包含Unity内置函数库
#include "UnityCG.cginc"
// 声明Properties中定义的变量
fixed4 _Color; // 颜色调整参数
sampler2D _MainTex; // 主纹理
float4 _MainTex_ST; // 纹理的Tiling和Offset参数
float _HorizontalAmount; // 水平图片数量
float _VerticalAmount; // 垂直图片数量
float _Speed; // 播放速度
// 顶点着色器输入结构体
struct a2v {
float4 vertex : POSITION; // 顶点位置
float2 texcoord : TEXCOORD0; // UV坐标
};
// 顶点着色器输出结构体(传递给片元着色器)
struct v2f {
float4 pos : SV_POSITION; // 裁剪空间位置
float2 uv : TEXCOORD0; // UV坐标
};
// 顶点着色器函数
v2f vert (a2v v) {
v2f o; // 创建输出结构体
o.pos = UnityObjectToClipPos(v.vertex); // 将顶点从对象空间转换到裁剪空间
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); // 应用纹理的Tiling和Offset
return o; // 返回处理后的数据
}
// 片元着色器函数
fixed4 frag (v2f i) : SV_Target {
// 计算当前时间帧
float time = floor(_Time.y * _Speed); // _Time.y是时间,乘以速度后取整得到当前帧
// 计算当前帧在纹理图集中的行列位置
float row = floor(time / _HorizontalAmount); // 计算行号
float column = time - row * _HorizontalAmount; // 计算列号
// 注释掉的UV计算方式(旧版本)
// half2 uv = float2(i.uv.x /_HorizontalAmount, i.uv.y / _VerticalAmount);
// uv.x += column / _HorizontalAmount;
// uv.y -= row / _VerticalAmount;
// 新的UV计算方式
half2 uv = i.uv + half2(column, -row); // 在原始UV基础上加上偏移
uv.x /= _HorizontalAmount; // 水平方向除以图片数量,得到在单张图片中的UV
uv.y /= _VerticalAmount; // 垂直方向除以图片数量,得到在单张图片中的UV
// 采样纹理
fixed4 c = tex2D(_MainTex, uv); // 根据计算出的UV采样纹理
c.rgb *= _Color; // 应用颜色调整
return c; // 返回最终颜色
}
// CG程序块结束
ENDCG
}
}
// 备用shader:当主shader不支持时使用
FallBack "Transparent/VertexLit"
}
滚动背景的序列帧动画
// 升级提示:将 'mul(UNITY_MATRIX_MVP,*)' 替换为 'UnityObjectToClipPos(*)'
// 这是因为Unity版本更新,矩阵乘法函数发生了变化
Shader "Unity Shaders Book/Chapter 11/Scrolling Background" {
// Properties块:定义在Inspector中可以调整的参数
Properties {
_MainTex ("Base Layer (RGB)", 2D) = "white" {} // 基础层纹理
_DetailTex ("2nd Layer (RGB)", 2D) = "white" {} // 第二层纹理(细节层)
_ScrollX ("Base layer Scroll Speed", Float) = 1.0 // 基础层滚动速度
_Scroll2X ("2nd layer Scroll Speed", Float) = 1.0 // 第二层滚动速度
_Multiplier ("Layer Multiplier", Float) = 1 // 层混合倍数
}
SubShader {
// 渲染标签:不透明渲染,几何体队列
Tags { "RenderType"="Opaque" "Queue"="Geometry"}
Pass {
Tags { "LightMode"="ForwardBase" } // 前向渲染基础光照模式
CGPROGRAM
#pragma vertex vert // 指定顶点着色器函数
#pragma fragment frag // 指定片元着色器函数
#include "UnityCG.cginc" // 包含Unity内置函数库
// 声明Properties中定义的变量
sampler2D _MainTex; // 基础层纹理
sampler2D _DetailTex; // 第二层纹理
float4 _MainTex_ST; // 基础层纹理的Tiling和Offset
float4 _DetailTex_ST; // 第二层纹理的Tiling和Offset
float _ScrollX; // 基础层滚动速度
float _Scroll2X; // 第二层滚动速度
float _Multiplier; // 混合倍数
// 顶点着色器输入结构体
struct a2v {
float4 vertex : POSITION; // 顶点位置
float4 texcoord : TEXCOORD0; // UV坐标
};
// 顶点着色器输出结构体
struct v2f {
float4 pos : SV_POSITION; // 裁剪空间位置
float4 uv : TEXCOORD0; // UV坐标(xy为基础层,zw为第二层)
};
// 顶点着色器函数
v2f vert (a2v v) {
v2f o;
// 将顶点从对象空间转换到裁剪空间
o.pos = UnityObjectToClipPos(v.vertex);
// 计算基础层的UV坐标
// TRANSFORM_TEX应用纹理的Tiling和Offset
// frac(float2(_ScrollX, 0.0) * _Time.y) 创建水平滚动效果
// _Time.y是当前时间(秒),乘以滚动速度得到偏移量
// frac()函数取小数部分,确保UV值在0-1范围内
o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex) + frac(float2(_ScrollX, 0.0) * _Time.y);
// 计算第二层的UV坐标(使用不同的滚动速度)
o.uv.zw = TRANSFORM_TEX(v.texcoord, _DetailTex) + frac(float2(_Scroll2X, 0.0) * _Time.y);
return o;
}
// 片元着色器函数
fixed4 frag (v2f i) : SV_Target {
// 采样基础层纹理
fixed4 firstLayer = tex2D(_MainTex, i.uv.xy);
// 采样第二层纹理
fixed4 secondLayer = tex2D(_DetailTex, i.uv.zw);
// 使用第二层的alpha通道进行混合
// lerp函数:lerp(a, b, t) = a * (1-t) + b * t
// 当secondLayer.a = 0时,显示firstLayer
// 当secondLayer.a = 1时,显示secondLayer
// 当secondLayer.a在0-1之间时,进行插值混合
fixed4 c = lerp(firstLayer, secondLayer, secondLayer.a);
// 应用混合倍数,调整整体亮度
c.rgb *= _Multiplier;
return c;
}
ENDCG
}
}
// 备用shader
FallBack "VertexLit"
}
接下来就到了正式顶点动画的部分了,顶点动画顾名思义,主要是在顶点着色器部分处理逻辑的,但是在这之前需要禁用合批,为什么呢?因为合批会导致因为顶点动画会导致每个对象有不同的变换
代码如下
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
// 升级提示:将旧的矩阵乘法替换为新的UnityObjectToClipPos函数
Shader "Unity Shaders Book/Chapter 11/Water" {
// 定义着色器的属性,这些可以在Inspector面板中调整
Properties {
_MainTex ("Main Tex", 2D) = "white" {} // 主纹理贴图
_Color ("Color Tint", Color) = (1, 1, 1, 1) // 颜色色调调整
_Magnitude ("Distortion Magnitude", Float) = 1 // 扭曲幅度
_Frequency ("Distortion Frequency", Float) = 1 // 扭曲频率
_InvWaveLength ("Distortion Inverse Wave Length", Float) = 10 // 扭曲波长的倒数
_Speed ("Speed", Float) = 0.5 // 移动速度
}
SubShader {
// 需要禁用批处理,因为顶点动画会导致每个对象有不同的变换
Tags {
"Queue"="Transparent" // 渲染队列:透明队列
"IgnoreProjector"="True" // 忽略投影器
"RenderType"="Transparent" // 渲染类型:透明
"DisableBatching"="True" // 禁用批处理
}
Pass {
Tags { "LightMode"="ForwardBase" } // 光照模式:前向渲染基础通道
// 渲染状态设置
ZWrite Off // 关闭深度写入
Blend SrcAlpha OneMinusSrcAlpha // 设置混合模式:标准透明混合
Cull Off // 关闭背面剔除
CGPROGRAM // 开始CG程序块
#pragma vertex vert // 声明顶点着色器函数
#pragma fragment frag // 声明片段着色器函数
#include "UnityCG.cginc" // 包含Unity的CG库
// 声明在Properties中定义的变量
sampler2D _MainTex; // 主纹理采样器
float4 _MainTex_ST; // 纹理的缩放和偏移参数
fixed4 _Color; // 颜色调整
float _Magnitude; // 扭曲幅度
float _Frequency; // 扭曲频率
float _InvWaveLength; // 波长倒数
float _Speed; // 移动速度
// 顶点着色器输入结构体
struct a2v {
float4 vertex : POSITION; // 顶点位置
float4 texcoord : TEXCOORD0; // 纹理坐标
};
// 顶点着色器输出结构体(传递给片段着色器)
struct v2f {
float4 pos : SV_POSITION; // 裁剪空间位置
float2 uv : TEXCOORD0; // 纹理坐标
};
// 顶点着色器函数
v2f vert(a2v v) {
v2f o; // 创建输出结构体
// 计算顶点偏移量
float4 offset;
offset.yzw = float3(0.0, 0.0, 0.0); // Y、Z、W分量设为0
// 计算X方向的偏移量,创建水波效果
// 使用正弦函数创建周期性波动
// _Frequency * _Time.y:时间因子,控制波动频率
// v.vertex.x/y/z * _InvWaveLength:空间因子,控制波长
// _Magnitude:控制波动幅度
offset.x = sin(_Frequency * _Time.y +
v.vertex.x * _InvWaveLength +
v.vertex.y * _InvWaveLength +
v.vertex.z * _InvWaveLength) * _Magnitude;
// 将顶点位置加上偏移量,然后转换到裁剪空间
o.pos = UnityObjectToClipPos(v.vertex + offset);
// 计算纹理坐标
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
// 添加垂直方向的移动,创建流动效果
o.uv += float2(0.0, _Time.y * _Speed);
return o; // 返回输出结构体
}
// 片段着色器函数
fixed4 frag(v2f i) : SV_Target {
// 采样主纹理
fixed4 c = tex2D(_MainTex, i.uv);
// 将纹理颜色与调整颜色相乘
c.rgb *= _Color.rgb;
return c; // 返回最终颜色
}
ENDCG // 结束CG程序块
}
}
// 如果当前着色器不支持,则使用这个备用着色器
FallBack "Transparent/VertexLit"
}
===========================高级篇=====================================
屏幕后处理
顾名思义首先要拿到屏幕的渲染纹理,我们可以用脚本的方式获得屏幕的渲染纹理以及材质,做脚本和shader之间的交互,这样我们可以用更多运行时动态控制shader的功能可以基于此实现,在本书中,使用了后处理顶点基类来进行控制和规范
using UnityEngine;
using System.Collections;
// 在编辑模式下也执行此脚本,方便在Scene视图中预览效果
[ExecuteInEditMode]
// 要求GameObject必须附加Camera组件
[RequireComponent (typeof(Camera))]
public class PostEffectsBase : MonoBehaviour {
// 在开始时调用,检查平台是否支持后处理效果
protected void CheckResources() {
// 检查当前平台是否支持图像效果
bool isSupported = CheckSupport();
// 如果不支持,则禁用该效果
if (isSupported == false) {
NotSupported();
}
}
// 在CheckResources中调用,检查当前平台对后处理效果的支持情况
protected bool CheckSupport() {
// 检查平台是否支持图像效果和渲染纹理
// SystemInfo.supportsImageEffects:是否支持图像效果
// SystemInfo.supportsRenderTextures:是否支持渲染纹理
if (SystemInfo.supportsImageEffects == false || SystemInfo.supportsRenderTextures == false) {
Debug.LogWarning("This platform does not support image effects or render textures.");
return false;
}
return true;
}
// 当平台不支持此效果时调用
protected void NotSupported() {
// 禁用该MonoBehaviour组件,停止后处理效果
enabled = false;
}
// Unity生命周期方法,在Start时检查资源支持情况
protected void Start() {
CheckResources();
}
// 当需要创建此效果使用的材质时调用
// 参数:shader - 要使用的着色器,material - 现有的材质(可能为null)
protected Material CheckShaderAndCreateMaterial(Shader shader, Material material) {
// 如果着色器为空,返回null
if (shader == null) {
return null;
}
// 如果着色器受支持,且材质存在且材质使用的着色器正确,直接返回现有材质
// 这避免了重复创建材质,提高性能
if (shader.isSupported && material && material.shader == shader)
return material;
// 如果着色器不受支持,返回null
if (!shader.isSupported) {
return null;
}
else {
// 创建新的材质并应用着色器
material = new Material(shader);
// 设置材质标志为不保存,避免在场景中保存材质资源
material.hideFlags = HideFlags.DontSave;
// 如果材质创建成功,返回材质;否则返回null
if (material)
return material;
else
return null;
}
}
}
可以看见基类主要关心的地方有是否支持后处理,如果不支持则不采用这个效果(禁用组件的形式)
亮度,饱和度,灰度案例
通过这个基类了解到了C#层面代码应该如何编写和构建,那么我们可以针对于这个案例编写一份专门的脚本来控制这个后处理效果
using UnityEngine;
using System.Collections;
// 亮度、饱和度和对比度后处理效果类
// 继承自PostEffectsBase基类,获得平台兼容性检查和材质管理功能
public class BrightnessSaturationAndContrast : PostEffectsBase {
// 公开的Shader引用,在Inspector中可以指定对应的shader文件
public Shader briSatConShader;
// 私有的材质变量,用于缓存创建的材质
private Material briSatConMaterial;
// 公开的材质属性,提供对材质的访问
// 使用属性访问器,确保每次访问时都检查并创建材质
public Material material {
get {
// 调用基类方法检查shader并创建材质
// 如果材质已存在且shader正确,直接返回;否则创建新材质
briSatConMaterial = CheckShaderAndCreateMaterial(briSatConShader, briSatConMaterial);
return briSatConMaterial;
}
}
// 亮度参数,使用Range特性限制在0.0f到3.0f之间
// 在Inspector中显示为滑动条,默认值1.0f表示无变化
[Range(0.0f, 3.0f)]
public float brightness = 1.0f;
// 饱和度参数,使用Range特性限制在0.0f到3.0f之间
// 在Inspector中显示为滑动条,默认值1.0f表示无变化
[Range(0.0f, 3.0f)]
public float saturation = 1.0f;
// 对比度参数,使用Range特性限制在0.0f到3.0f之间
// 在Inspector中显示为滑动条,默认值1.0f表示无变化
[Range(0.0f, 3.0f)]
public float contrast = 1.0f;
// Unity后处理的标准接口方法
// 在渲染管线中被调用,用于处理渲染纹理
// src: 源纹理(通常是摄像机的渲染结果)
// dest: 目标纹理(处理后的结果)
void OnRenderImage(RenderTexture src, RenderTexture dest) {
// 检查材质是否成功创建
if (material != null) {
// 将C#中的参数值传递给shader中的对应变量
// 这些变量名必须与shader中的Properties名称完全匹配
material.SetFloat("_Brightness", brightness); // 设置亮度值
material.SetFloat("_Saturation", saturation); // 设置饱和度值
material.SetFloat("_Contrast", contrast); // 设置对比度值
// 使用Graphics.Blit进行全屏后处理渲染
// 将源纹理通过指定材质渲染到目标纹理
// 这会调用shader的顶点和片段着色器对每个像素进行处理
Graphics.Blit(src, dest, material);
} else {
// 如果材质创建失败(例如shader不支持),直接复制源纹理到目标纹理
// 这确保了即使后处理失败,游戏仍能正常运行
Graphics.Blit(src, dest);
}
}
}
然后编写shader中的方法
// 升级提示:Unity 5.0+ 将 'mul(UNITY_MATRIX_MVP,*)' 替换为 'UnityObjectToClipPos(*)'
// 这是因为Unity 5.0引入了新的渲染管线,矩阵乘法方式发生了变化
// mul(UNITY_MATRIX_MVP, vertex) 是旧版本的写法
// UnityObjectToClipPos(vertex) 是新版本的写法,功能相同但更高效
Shader "PostEffect/basetest" {
// Properties块:定义在Inspector中可以调节的参数
// 这些参数可以在运行时动态修改,影响shader的渲染效果
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {} // 输入纹理,默认白色
// 这是后处理的源图像,通常是摄像机的渲染结果
_Brightness ("Brightness", Float) = 1 // 亮度参数,默认值1(无变化)
// 范围通常在0.0-3.0之间,1.0表示原始亮度
_Saturation("Saturation", Float) = 1 // 饱和度参数,默认值1(无变化)
// 控制颜色的鲜艳程度,0.0为黑白,1.0为原始饱和度
_Contrast("Contrast", Float) = 1 // 对比度参数,默认值1(无变化)
// 控制明暗对比,1.0为原始对比度
}
SubShader {
Pass {
// 渲染状态设置 - 这些设置对后处理效果至关重要
ZTest Always // 总是通过深度测试
// 后处理是全屏四边形,不需要与场景中的物体进行深度比较
Cull Off // 关闭背面剔除
// 确保四边形的两面都会被渲染,避免某些情况下看不到效果
ZWrite Off // 关闭深度写入
// 后处理不需要写入深度缓冲,避免影响后续的渲染操作
CGPROGRAM // 开始CG程序块
#pragma vertex vert // 指定顶点着色器函数名为vert
#pragma fragment frag // 指定片段着色器函数名为frag
#include "UnityCG.cginc" // 包含Unity的常用函数库
// 提供UnityObjectToClipPos等Unity特有函数
// 声明变量,对应Properties中的参数
// 这些变量会在C#脚本中通过material.SetFloat等方法设置
sampler2D _MainTex; // 输入纹理采样器,用于读取源图像
half _Brightness; // 亮度值,half是16位浮点数,精度足够且性能更好
half _Saturation; // 饱和度值
half _Contrast; // 对比度值
// 顶点着色器输出结构体
// 定义从顶点着色器传递给片段着色器的数据
struct v2f {
float4 pos : SV_POSITION; // 裁剪空间位置,GPU需要这个来知道像素在屏幕上的位置
half2 uv: TEXCOORD0; // UV坐标,用于纹理采样,告诉GPU从纹理的哪个位置读取颜色
};
// 顶点着色器:处理顶点数据
// appdata_img是Unity提供的后处理专用顶点数据结构
v2f vert(appdata_img v) {
v2f o; // 创建输出结构体
// 将顶点从对象空间转换到裁剪空间
// UnityObjectToClipPos是Unity 5.0+的新函数,替代了mul(UNITY_MATRIX_MVP, v.vertex)
// 这个转换是必要的,因为GPU需要知道顶点在屏幕上的位置
o.pos = UnityObjectToClipPos(v.vertex);
// 传递UV坐标给片段着色器
// UV坐标用于纹理采样,告诉片段着色器从输入纹理的哪个位置读取颜色
o.uv = v.texcoord;
return o; // 返回处理后的顶点数据
}
// 片段着色器:处理每个像素的颜色
// 这是后处理效果的核心,每个像素都会执行这个函数
fixed4 frag(v2f i) : SV_Target {
// 采样输入纹理,获取当前像素的颜色
// tex2D是纹理采样函数,根据UV坐标从纹理中读取颜色
// renderTex包含了原始像素的RGBA值
fixed4 renderTex = tex2D(_MainTex, i.uv);
// 第一步:应用亮度调整
// 直接对RGB值进行乘法运算
// 值>1增加亮度,值<1降低亮度
// 例如:brightness=2.0时,所有颜色值翻倍,图像变亮
fixed3 finalColor = renderTex.rgb * _Brightness;
// 第二步:应用饱和度调整
// 计算亮度值(使用标准RGB到灰度的转换系数)
// 0.2125*R + 0.7154*G + 0.0721*B 是ITU-R BT.709标准的RGB到亮度转换公式
// 这些系数反映了人眼对不同颜色的敏感度(绿色最敏感,蓝色最不敏感)
fixed luminance = 0.2125 * renderTex.r + 0.7154 * renderTex.g + 0.0721 * renderTex.b;
// 创建灰度版本的颜色(所有通道都是相同的亮度值)
// 这相当于将彩色图像转换为黑白图像
fixed3 luminanceColor = fixed3(luminance, luminance, luminance);
// 在灰度版本和原色之间进行线性插值
// lerp(a, b, t) = a + (b-a)*t,其中t是插值因子
// 饱和度>1增强色彩,<1降低色彩
// 例如:saturation=0.0时,图像变为黑白;saturation=2.0时,色彩更鲜艳
finalColor = lerp(luminanceColor, finalColor, _Saturation);
// 第三步:应用对比度调整
// 定义中性灰色(0.5, 0.5, 0.5)作为对比度调整的基准
// 0.5是RGB颜色空间的中点,代表中等亮度
fixed3 avgColor = fixed3(0.5, 0.5, 0.5);
// 在基准色和当前颜色之间进行线性插值
// 对比度>1增强差异,<1降低差异
// 例如:contrast=0.0时,所有像素变为中性灰色;contrast=2.0时,明暗对比更强烈
finalColor = lerp(avgColor, finalColor, _Contrast);
// 返回最终颜色,保持原始alpha值不变
// fixed4(finalColor, renderTex.a) 将RGB和Alpha组合成完整的颜色
// Alpha通道通常用于透明度,在后处理中保持原始值
return fixed4(finalColor, renderTex.a);
}
ENDCG // 结束CG程序块
}
}
// 如果shader不支持,不提供备用方案
// Fallback Off表示如果当前shader无法运行,不会尝试使用其他shader
// 这确保了后处理效果的准确性,避免使用不兼容的备用shader
Fallback Off
}
4646

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



