今天我们来聊Unity特有的表面着色器以及很少提到的几何着色器。
表面着色器
在前文关于光照的计算中,我们学会了很多:我们学习了一系列光照模型,比如专门针对漫反射的兰伯特模型和改进的半兰伯特模型,又比如由高光、漫反射和环境光组成的冯模型以及改进了反射方向计算的布林-冯模型,我们学会了针对多光源场景的前向渲染和延迟渲染方式,我们还学会了针对透明物体和不透明物体的渲染队列设置。
但是在实际的开发过程中,场景可能比我们想象的还要复杂,如果我们依然手动的写好每一个顶点着色器、片元着色器,那么工作效率是非常低下的,是否有办法来改进呢?
答案是有的,兄弟,有的。

在使用之前有一些不得不提的是:

具体来说我们如何使用呢?
我们直接用一个代码示例:
cs
Shader "Chapter5/chapter5_1_sample"
{
// 定义Shader的属性(Properties),这些属性会显示在材质面板中,允许用户进行调整。
Properties
{
// 定义一个颜色属性,默认值为白色 (1,1,1,1)。
_Color ("Color", Color) = (1,1,1,1)
// 定义一个2D纹理属性,表示材质的基础颜色贴图(Albedo),默认值为白色纹理。
_MainTex ("Albedo (RGB)", 2D) = "white" {}
// 定义光滑度(Smoothness)的滑块,范围是0到1,默认值为0.5。
_Glossiness ("Smoothness", Range(0,1)) = 0.5
// 定义金属度(Metallic)的滑块,范围是0到1,默认值为0.0。
_Metallic ("Metallic", Range(0,1)) = 0.0
}
// 定义子着色器(SubShader),包含材质的具体渲染代码。
SubShader
{
// 设置渲染标签,标记为不透明材质(Opaque)。
Tags { "RenderType"="Opaque" }
// 定义细节级别(Level of Detail,LOD),值为200。
LOD 200
// 开始CG代码块,定义基于Surface Shader的着色代码。
CGPROGRAM
// 使用标准物理光照模型(Standard lighting model),并启用所有光源的阴影。
#pragma surface surf Standard fullforwardshadows
// 设置Shader的目标为3.0(Shader Model 3.0),以获得更好的光照效果。
#pragma target 3.0
// 定义2D纹理采样器变量,用于采样_MainTex纹理。
sampler2D _MainTex;
// 定义输入结构体(Input),用于传递顶点的UV坐标等数据。
struct Input
{
float2 uv_MainTex; // 接收_MainTex的UV坐标
};
// 定义变量,用于接收材质属性。
half _Glossiness; // 光滑度
half _Metallic; // 金属度
fixed4 _Color; // 颜色
// 添加实例化支持的缓冲区,用于GPU实例化技术。
UNITY_INSTANCING_BUFFER_START(Props)
// 可以在此处添加更多的每实例属性。
UNITY_INSTANCING_BUFFER_END(Props)
// 定义Surface函数,负责实现表面着色逻辑。
void surf (Input IN, inout SurfaceOutputStandard o)
{
// 从_MainTex纹理采样颜色,并乘以用户定义的颜色(_Color)。
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
// 设置材质的Albedo颜色(反射漫射颜色)。
o.Albedo = c.rgb;
// 使用滑块变量设置材质的金属度(Metallic)和光滑度(Smoothness)。
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
// 设置材质的透明度(Alpha值)。
o.Alpha = c.a;
}
ENDCG // 结束CG代码块
}
// 定义备用Shader,当目标设备不支持当前Shader时使用。
FallBack "Diffuse"
}
依然只说新东西,可以看到有很多新东西:
cs
// 使用标准物理光照模型(Standard lighting model),并启用所有光源的阴影。
#pragma surface surf Standard fullforwardshadows
// 设置Shader的目标为3.0(Shader Model 3.0),以获得更好的光照效果。
#pragma target 3.0
关于第一句:

这个所谓的标准物理光照模型PBR:

而关于target 3.0:

与其他版本的对比:

cs
// 添加实例化支持的缓冲区,用于GPU实例化技术。
UNITY_INSTANCING_BUFFER_START(Props)
// 可以在此处添加更多的每实例属性。
UNITY_INSTANCING_BUFFER_END(Props)

虽然之前的LearnOpenGL的内容中有提到过GPU的实例化,但在这里还是再复习一下:
cs
// 定义Surface函数,负责实现表面着色逻辑。
void surf (Input IN, inout SurfaceOutputStandard o)
{
// 从_MainTex纹理采样颜色,并乘以用户定义的颜色(_Color)。
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
// 设置材质的Albedo颜色(反射漫射颜色)。
o.Albedo = c.rgb;
// 使用滑块变量设置材质的金属度(Metallic)和光滑度(Smoothness)。
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
// 设置材质的透明度(Alpha值)。
o.Alpha = c.a;
}
这个就是表面着色器的内容部分,其中Input是我们自定义的结构体,SurfaceOutputStandard则是Unity内置的结构体类型,用于描述材质的物理属性(如漫反射颜色、金属度、光滑度等)。
cs
// 定义备用Shader,当目标设备不支持当前Shader时使用。
FallBack "Diffuse"
FallBack的作用已经写在注释里了,就是当前shader不适用时使用FallBack选定的shader。
效果如下:

曲面细分
如果表面着色器只是一个让你少写一些顶点着色器和片元着色器定义的工具,那他不会成为计算机图形学的核心技术,事实上,在表面着色器上还可以实现:曲面细分。

等一下。
我觉得有必要在这里插播一下一个基本的概念:片元、像素和网格的区别,因为我老是把这些概念弄混淆:

而曲面细分的对象则是网格。

曲面细分发生的阶段是顶点着色器到片元着色器之间的可选阶段,这个时候还没有产生片元(光栅化后输出)和片元(渲染管线流程后输出)。
而既然是要划分网格成更多的网格,那么划分的方式自然就是十分重要,常见的有以下三种:

我们来一个个用具体的代码案例测试一下:
固定数量:
cs
Shader "Chapter5/chapter5_2"
{
// 定义Shader的属性,这些属性会在材质编辑器中显示,允许用户调整。
Properties
{
// 定义一个控制细分(Tessellation)程度的滑块,范围是1到32,默认值为4。
_Tess ("Tessellation", Range(1,32)) = 4
// 定义基础颜色纹理属性,默认值为白色。
_MainTex ("Base (RGB)", 2D) = "white" {}
// 定义用于位移映射的纹理属性,默认值为灰色。
_DispTex ("Disp Texture", 2D) = "gray" {}
// 定义法线贴图属性,用于控制表面细节的法线效果,默认值为bump类型。
_NormalMap ("Normalmap", 2D) = "bump" {}
// 定义位移强度的滑块,范围是0到1,默认值为0.3。
_Displacement ("Displacement", Range(0, 1.0)) = 0.3
// 定义颜色属性,用于控制整体颜色,默认值为白色。
_Color ("Color", color) = (1,1,1,0)
// 定义镜面反射颜色属性,默认值为灰色。
_SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
}
// 定义子着色器部分,包含渲染逻辑。
SubShader
{
// 定义渲染标签,标记为不透明材质。
Tags
{
"RenderType"="Opaque"
}
// 设置着色器的LOD(细节级别)为300。
LOD 300
// 开始CG代码块,定义基于Surface Shader的着色逻辑。
CGPROGRAM
// 使用BlinnPhong光照模型,启用阴影,并添加顶点位移和细分着色。
#pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessFixed nolightmap
// 设置目标为Shader Model 4.6(支持更高级功能,如细分着色)。
#pragma target 4.6
// 定义应用程序顶点数据结构。
struct appdata
{
float4 vertex : POSITION; // 顶点位置
float4 tangent : TANGENT; // 切线,用于法线映射
float3 normal : NORMAL; // 法线
float2 texcoord : TEXCOORD0; // UV坐标
};
// 定义细分级别变量,用于控制细分程度。
float _Tess;
// 固定细分级别函数,返回细分强度。
float4 tessFixed()
{
return _Tess; // 由用户设置的_Tess值决定细分强度。
}
// 定义用于位移贴图的纹理采样器和位移强度变量。
sampler2D _DispTex;
float _Displacement;
// 位移函数,对顶点位置进行调整。
void disp(inout appdata v)
{
// 使用纹理采样lod方式获取位移贴图值,并乘以位移强度。
float d = tex2Dlod(_DispTex, float4(v.texcoord.xy, 0, 0)).r * _Displacement;
// 根据法线方向调整顶点位置,实现位移效果。
v.vertex.xyz += v.normal * d;
}
// 定义输入结构体,用于传递UV坐标。
struct Input
{
float2 uv_MainTex; // 主纹理的UV坐标。
};
// 定义其他所需变量。
sampler2D _MainTex; // 主纹理采样器。
sampler2D _NormalMap; // 法线贴图采样器。
fixed4 _Color; // 基础颜色。
// Surface函数,处理表面着色逻辑。
void surf(Input IN, inout SurfaceOutput o)
{
// 从主纹理中采样颜色,并乘以用户设置的基础颜色。
half4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
// 设置漫反射颜色(Albedo)。
o.Albedo = c.rgb;
// 设置镜面反射强度为0.2。
o.Specular = 0.2;
// 设置光泽度为1.0。
o.Gloss = 1.0;
// 从法线贴图中采样法线,并解包法线数据。
o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
}
ENDCG // 结束CG代码块
}
// 当目标设备不支持当前Shader时,回退到Diffuse着色器。
FallBack "Diffuse"
}
cs
// 定义Shader的属性,这些属性会在材质编辑器中显示,允许用户调整。
Properties
{
// 定义一个控制细分(Tessellation)程度的滑块,范围是1到32,默认值为4。
_Tess ("Tessellation", Range(1,32)) = 4
// 定义基础颜色纹理属性,默认值为白色。
_MainTex ("Base (RGB)", 2D) = "white" {}
// 定义用于位移映射的纹理属性,默认值为灰色。
_DispTex ("Disp Texture", 2D) = "gray" {}
// 定义法线贴图属性,用于控制表面细节的法线效果,默认值为bump类型。
_NormalMap ("Normalmap", 2D) = "bump" {}
// 定义位移强度的滑块,范围是0到1,默认值为0.3。
_Displacement ("Displacement", Range(0, 1.0)) = 0.3
// 定义颜色属性,用于控制整体颜色,默认值为白色。
_Color ("Color", color) = (1,1,1,0)
// 定义镜面反射颜色属性,默认值为灰色。
_SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
}
其中的_Tess用于控制细分程度,也就是我要把一个网格多分成几份。
_DispTex定义位移映射的纹理属性,_Displacement定义位移强度,_SpecColor定义反射颜色。
cs
// 使用BlinnPhong光照模型,启用阴影,并添加顶点位移和细分着色。
#pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessFixed nolightmap
// 设置目标为Shader Model 4.6(支持更高级功能,如细分着色)。
#pragma target 4.6
之前已经提到过的Shader Model版本,只有在4.0及以上才支持曲面细分。
关于表面着色器的定义:

cs
// 固定细分级别函数,返回细分强度。
float4 tessFixed()
{
return _Tess; // 由用户设置的_Tess值决定细分强度。
}
// 位移函数,对顶点位置进行调整。
void disp(inout appdata v)
{
// 使用纹理采样lod方式获取位移贴图值,并乘以位移强度。
float d = tex2Dlod(_DispTex, float4(v.texcoord.xy, 0, 0)).r * _Displacement;
// 根据法线方向调整顶点位置,实现位移效果。
v.vertex.xyz += v.normal * d;
}
这是我们自定义的细分函数和位移函数,用于设置相关参数。
效果如图:

然后是我们基于边长的曲面细分:
cs
Shader "Chapter5/chapter5_3"
{
// 定义材质属性,用于调整材质的外观效果。
Properties
{
// 边缘长度属性,用于控制细分曲面的细化程度,范围为2到50,默认值为15。
_EdgeLength ("Edge length", Range(2,50)) = 15
// 主纹理(基础颜色纹理),用于控制物体表面的颜色显示,默认值为白色。
_MainTex ("Base (RGB)", 2D) = "white" {}
// 位移贴图属性,用于生成位移效果,默认值为灰色纹理。
_DispTex ("Disp Texture", 2D) = "gray" {}
// 法线贴图属性,用于控制表面法线的细节显示,默认值为bump类型。
_NormalMap ("Normalmap", 2D) = "bump" {}
// 位移强度属性,范围为0到1,控制位移效果的强度,默认值为0.3。
_Displacement ("Displacement", Range(0, 1.0)) = 0.3
// 基础颜色属性,用于给材质添加颜色,默认值为白色。
_Color ("Color", color) = (1,1,1,0)
// 镜面反射颜色属性,控制镜面高光的颜色,默认值为灰色。
_SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
}
// 定义子着色器(SubShader),包含材质的具体渲染实现。
SubShader
{
// 设置渲染标签,标记为不透明材质。
Tags
{
"RenderType"="Opaque"
}
// 设置Shader的LOD(细节级别)为300,决定渲染质量和性能的平衡。
LOD 300
// 开始CG代码块,定义基于Surface Shader的渲染逻辑。
CGPROGRAM
// 使用BlinnPhong光照模型,启用阴影、全前向渲染、顶点位移函数(disp),以及边缘长度细分(tessEdge)。
#pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessEdge nolightmap
// 设置目标为Shader Model 4.6(支持高级功能,如细分曲面)。
#pragma target 4.6
// 包含Unity内置的细分曲面库,提供细分算法支持。
#include "Tessellation.cginc"
// 定义顶点数据结构,用于从应用程序传递数据到GPU。
struct appdata
{
float4 vertex : POSITION; // 顶点位置
float4 tangent : TANGENT; // 切线向量,用于法线贴图
float3 normal : NORMAL; // 法线向量
float2 texcoord : TEXCOORD0; // UV坐标
};
// 定义边缘长度属性变量,用于控制细分程度。
float _EdgeLength;
// 边缘长度细分函数,基于三角形顶点的位置计算细分因子。
float4 tessEdge(appdata v0, appdata v1, appdata v2)
{
// 使用Unity内置的基于边缘长度的细分函数。
return UnityEdgeLengthBasedTess(v0.vertex, v1.vertex, v2.vertex, _EdgeLength);
}
// 位移贴图采样器和位移强度变量,用于控制顶点位移效果。
sampler2D _DispTex;
float _Displacement;
// 顶点位移函数,根据法线方向和位移贴图调整顶点位置。
void disp(inout appdata v)
{
// 从位移贴图中采样高度值,并乘以位移强度。
float d = tex2Dlod(_DispTex, float4(v.texcoord.xy, 0, 0)).r * _Displacement;
// 根据法线方向调整顶点位置,实现位移效果。
v.vertex.xyz += v.normal * d;
}
// 定义输入结构体,用于传递UV坐标。
struct Input
{
float2 uv_MainTex; // 主纹理的UV坐标。
};
// 定义其他所需变量。
sampler2D _MainTex; // 主纹理采样器。
sampler2D _NormalMap; // 法线贴图采样器。
fixed4 _Color; // 基础颜色。
// Surface函数,处理表面着色逻辑。
void surf(Input IN, inout SurfaceOutput o)
{
// 从主纹理中采样颜色,并乘以基础颜色。
half4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
// 设置材质的漫反射颜色(Albedo)。
o.Albedo = c.rgb;
// 设置材质的镜面反射强度为0.2。
o.Specular = 0.2;
// 设置材质的光泽度为1.0。
o.Gloss = 1.0;
// 从法线贴图中采样法线数据,并解包为适用的法线值。
o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
}
ENDCG // 结束CG代码块
}
// 当目标设备不支持当前Shader时,回退到简单的Diffuse Shader。
FallBack "Diffuse"
}
可以看到和之前的不同:
cs
// 边缘长度属性,用于控制细分曲面的细化程度,范围为2到50,默认值为15。
_EdgeLength ("Edge length", Range(2,50)) = 15
这里是边缘长度属性。
cs
// 定义边缘长度属性变量,用于控制细分程度。
float _EdgeLength;
// 边缘长度细分函数,基于三角形顶点的位置计算细分因子。
float4 tessEdge(appdata v0, appdata v1, appdata v2)
{
// 使用Unity内置的基于边缘长度的细分函数。
return UnityEdgeLengthBasedTess(v0.vertex, v1.vertex, v2.vertex, _EdgeLength);
}
这里的根据边长进行曲面细分的函数中,可以看到我们使用了一个Unity内置的函数:UnityEdgeLengthBasedTess()。

其余基本和之前一样。
效果如图:

然后是基于距离的:
cs
Shader "Chapter5/chapter5_4_distance"
{
// 定义材质属性,供用户在编辑器中调整材质的参数。
Properties
{
// 控制细分曲面因子的强度,范围是1到32,默认值为4。
_Tess ("Tessellation", Range(1,32)) = 4
// 最大距离。
_MaxDis ("MaxDis", Range(1,100)) = 50
// 最小距离。
_MinDis ("MinDis", Range(1,100)) = 20
// 主纹理,用于显示基础颜色纹理,默认值为白色。
_MainTex ("Base (RGB)", 2D) = "white" {}
// 位移贴图,用于生成位移效果,默认值为灰色纹理。
_DispTex ("Disp Texture", 2D) = "gray" {}
// 法线贴图,用于控制材质表面法线的细节显示,默认值为bump类型。
_NormalMap ("Normalmap", 2D) = "bump" {}
// 位移强度,用于调整顶点位移效果的强度,范围为0到1,默认值为0.3。
_Displacement ("Displacement", Range(0, 1.0)) = 0.3
// 基础颜色,用于调整材质的颜色,默认值为白色。
_Color ("Color", color) = (1,1,1,0)
// 镜面高光颜色,用于调整材质的镜面反射颜色,默认值为中灰色。
_SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
}
// 定义子着色器,用于实现材质的具体渲染效果。
SubShader
{
// 设置渲染类型标签,将材质标记为不透明(Opaque)。
Tags
{
"RenderType"="Opaque"
}
// 设置细节级别(LOD),用于控制渲染质量与性能的平衡。
LOD 300
// 开始CG代码块,定义Shader的具体实现逻辑。
CGPROGRAM
// 使用Blinn-Phong光照模型,支持阴影、前向渲染和顶点位移(disp),并启用基于距离的细分曲面(tessDistance)。
#pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessDistance nolightmap
// 设置目标Shader模型版本为4.6,支持高级图形功能(如细分曲面)。
#pragma target 4.6
// 包含Unity内置的细分曲面库文件,提供相关算法和工具。
#include "Tessellation.cginc"
// 定义顶点数据结构,用于传递顶点信息。
struct appdata
{
float4 vertex : POSITION; // 顶点位置。
float4 tangent : TANGENT; // 切线向量,用于法线贴图。
float3 normal : NORMAL; // 法线向量。
float2 texcoord : TEXCOORD0; // UV纹理坐标。
};
// 定义细分曲面强度属性。
float _Tess;
float _MaxDis;
float _MinDis;
// 基于距离的细分曲面函数,使用Unity内置的算法。
float4 tessDistance(appdata v0, appdata v1, appdata v2)
{
// 设置最小和最大细分距离。
float minDist = _MinDis; // 最小距离,细分强度较大。
float maxDist = _MaxDis; // 最大距离,细分强度较小。
// 调用Unity的内置函数,根据顶点与相机的距离计算细分因子。
return UnityDistanceBasedTess(v0.vertex, v1.vertex, v2.vertex, minDist, maxDist, _Tess);
}
// 位移贴图采样器和位移强度变量,用于生成顶点位移效果。
sampler2D _DispTex;
float _Displacement;
// 顶点位移函数,通过位移贴图调整顶点位置。
void disp(inout appdata v)
{
// 从位移贴图中采样位移值(高度值),并根据位移强度调整。
float d = tex2Dlod(_DispTex, float4(v.texcoord.xy, 0, 0)).r * _Displacement;
// 根据顶点法线方向对顶点进行偏移,实现位移效果。
v.vertex.xyz += v.normal * d;
}
// 定义输入结构体,用于传递UV坐标等信息。
struct Input
{
float2 uv_MainTex; // 主纹理的UV坐标。
};
// 定义所需的采样器和颜色变量。
sampler2D _MainTex; // 主纹理采样器。
sampler2D _NormalMap; // 法线贴图采样器。
fixed4 _Color; // 基础颜色。
// Surface函数,用于处理材质的表面着色逻辑。
void surf(Input IN, inout SurfaceOutput o)
{
// 从主纹理中采样颜色,并与基础颜色相乘。
half4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
// 设置材质的漫反射颜色(Albedo)。
o.Albedo = c.rgb;
// 设置材质的镜面反射强度。
o.Specular = 0.2;
// 设置材质的光泽度(Gloss)。
o.Gloss = 1.0;
// 从法线贴图中采样法线数据,并解包为适用的法线值。
o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
}
ENDCG // 结束CG代码块
}
// 设置回退着色器。当目标设备不支持当前Shader时,使用简单的Diffuse Shader。
FallBack "Diffuse"
}
可以看到主要的区别依然是集中在这么几个地方:
cs
// 最大距离。
_MaxDis ("MaxDis", Range(1,100)) = 50
// 最小距离。
_MinDis ("MinDis", Range(1,100)) = 20
在材质属性中定义好最大最小距离。
cs
// 使用Blinn-Phong光照模型,支持阴影、前向渲染和顶点位移(disp),并启用基于距离的细分曲面(tessDistance)。
#pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessDistance nolightmap
表面着色器的一些设置:光照模型采用布林-冯模型,添加阴影、前向渲染,顶点着色器使用名为disp的函数(实际上是位移函数),曲面细分启用名为tessDistance的函数,禁用光照贴图。
cs
// 定义细分曲面强度属性。
float _Tess;
float _MaxDis;
float _MinDis;
// 基于距离的细分曲面函数,使用Unity内置的算法。
float4 tessDistance(appdata v0, appdata v1, appdata v2)
{
// 设置最小和最大细分距离。
float minDist = _MinDis; // 最小距离,细分强度较大。
float maxDist = _MaxDis; // 最大距离,细分强度较小。
// 调用Unity的内置函数,根据顶点与相机的距离计算细分因子。
return UnityDistanceBasedTess(v0.vertex, v1.vertex, v2.vertex, minDist, maxDist, _Tess);
}
关于曲面细分的函数,我们使用Unity的内置函数:

效果如图:

这样我们就完成了------固定数量、基于边长、基于距离的曲面细分方法,但是难道我们就一定只能按照网格的空间几何结构来划分吗?
其实不然:
cs
Shader "Chapter5/chapter5_5_phong"
{
Properties
{
_EdgeLength ("边缘长度", Range(2,50)) = 5 // 边缘长度,控制细分的精度
_Phong ("Phong 强度", Range(0,1)) = 0.5 // Phong高光强度
_MainTex ("基础纹理 (RGB)", 2D) = "white" {} // 基础纹理,默认为白色
_Color ("颜色", color) = (1,1,1,0) // 颜色,默认为白色
}
SubShader
{
Tags
{
"RenderType"="Opaque" // 渲染类型为不透明
}
LOD 300 // 设置细节等级
CGPROGRAM
#pragma surface surf Lambert vertex:dispNone tessellate:tessEdge tessphong:_Phong nolightmap
#include "Tessellation.cginc" // 引入细分曲面相关函数
// 顶点数据结构
struct appdata
{
float4 vertex : POSITION; // 顶点位置
float3 normal : NORMAL; // 法线向量
float2 texcoord : TEXCOORD0; // 纹理坐标
};
// 顶点处理函数,空函数,不做操作
void dispNone(inout appdata v)
{
}
// Phong高光强度和边缘长度
float _Phong;
float _EdgeLength;
// 细分计算函数,基于边缘长度来决定细分的精度
float4 tessEdge(appdata v0, appdata v1, appdata v2)
{
return UnityEdgeLengthBasedTess(v0.vertex, v1.vertex, v2.vertex, _EdgeLength);
}
// 输入结构体,包含纹理坐标
struct Input
{
float2 uv_MainTex; // 纹理坐标
};
// 颜色和纹理采样器
fixed4 _Color;
sampler2D _MainTex;
// 表面函数,定义了如何从纹理获取颜色并应用
void surf(Input IN, inout SurfaceOutput o)
{
half4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color; // 从纹理获取颜色并乘以颜色属性
o.Albedo = c.rgb; // 将RGB颜色设置为Albedo
o.Alpha = c.a; // 将Alpha通道设置为透明度
}
ENDCG
}
FallBack "Diffuse" // 如果此Shader不可用,则回退使用"Diffuse"Shader
}
这段代码就是一个基于边长和Phong强度进行曲面细分的着色器代码。
什么是Phong强度?

在代码中:
cs
#pragma surface surf Lambert vertex:dispNone tessellate:tessEdge tessphong:_Phong nolightmap
光照模型我们选择了Lambert,顶点着色器使用dispNone函数,曲面细分使用tessEdge函数而tessphong:_Phong中tessphong代表启用tessphong也就是基于Phong的曲面细分函数。
其他大致没有区别,效果如下:

现在让我们综合上述的内容,尝试做一个比较厉害的东西:水面渲染。
代码如下:
cs
Shader "Chapter5/chapter5_wave"
{
Properties
{
_Color ("Color", Color) = (0,0,1,1) // 水体基础颜色
_WaveHeight ("WaveHeight", float) = 1 // 波浪高度
_WaveSpeed ("WaveSpeed", float) = 1 // 波浪速度
_WaveGap ("WaveGap", float) = 1 // 波浪间隔
_FoamTex ("FoamTex", 2D) = "white" {} // 泡沫纹理
_NormalTex ("NormalTex", 2D) = "white" {} // 法线贴图
_NormalScale ("NormalScale", float) = 1 // 法线强度
_NormalSpeed ("NormalSpeed", float) = 1 // 法线贴图移动速度
}
SubShader
{
Tags
{
"RenderType" = "Transparent" // 渲染类型为透明
"Queue" = "Transparent" // 渲染队列为透明
"IgnoreProjector" = "True" // 忽略投影器
}
LOD 200 // 细节级别
CGPROGRAM
// 使用 Standard 光照模型,启用透明混合,保留透明度,启用全向阴影
#pragma surface surf Standard alpha:fade keepalpha fullforwardshadows vertex:vertexDataFunc
#pragma target 3.0 // 目标着色器模型为 3.0
sampler2D _MainTex; // 主纹理(未使用)
struct Input
{
float2 uv_FoamTex; // 泡沫纹理的 UV 坐标
float4 screenPos; // 屏幕空间坐标
};
fixed4 _Color; // 水体基础颜色
float _WaveHeight; // 波浪高度
float _WaveSpeed; // 波浪速度
float _WaveGap; // 波浪间隔
sampler2D _NormalTex; // 法线贴图
float _NormalScale; // 法线强度
float _NormalSpeed; // 法线贴图移动速度
// 顶点着色器函数
void vertexDataFunc(inout appdata_full v, out Input o)
{
UNITY_INITIALIZE_OUTPUT(Input, o); // 初始化输出结构
// 计算波浪高度,使用正弦函数模拟波浪效果
fixed height = sin(_Time.y * _WaveSpeed + v.vertex.z * _WaveGap + v.vertex.x) * _WaveHeight;
// 根据法线方向调整顶点位置,形成波浪效果
v.vertex.xyz += v.normal * height;
}
// 表面着色器函数
void surf(Input IN, inout SurfaceOutputStandard o)
{
// 法线贴图
// 根据时间偏移法线贴图的 UV 坐标
float2 speed = _Time.x * float2(_WaveSpeed, _WaveSpeed) * _NormalSpeed;
// 采样法线贴图,并解包法线
fixed3 bump1 = UnpackNormal(tex2D(_NormalTex, IN.uv_FoamTex.xy + speed)).rgb;
fixed3 bump2 = UnpackNormal(tex2D(_NormalTex, IN.uv_FoamTex.xy - speed)).rgb;
// 合并两个法线贴图的结果
fixed3 bump = normalize(bump1 + bump2);
// 调整法线强度
bump.xy *= _NormalScale;
bump = normalize(bump);
// 将法线赋值给输出结构
o.Normal = bump;
// 水体颜色
o.Albedo = _Color;
// 设置透明度
o.Alpha = _Color.a;
}
ENDCG
}
FallBack "Diffuse" // 回退到 Diffuse Shader
}
我们来一点一点分析:
cs
_Color ("Color", Color) = (0,0,1,1) // 水体基础颜色
_WaveHeight ("WaveHeight", float) = 1 // 波浪高度
_WaveSpeed ("WaveSpeed", float) = 1 // 波浪速度
_WaveGap ("WaveGap", float) = 1 // 波浪间隔
_FoamTex ("FoamTex", 2D) = "white" {} // 泡沫纹理
_NormalTex ("NormalTex", 2D) = "white" {} // 法线贴图
_NormalScale ("NormalScale", float) = 1 // 法线强度
_NormalSpeed ("NormalSpeed", float) = 1 // 法线贴图移动速度
参数的含义都写在注释里了。
cs
Tags
{
"RenderType" = "Transparent" // 渲染类型为透明
"Queue" = "Transparent" // 渲染队列为透明
"IgnoreProjector" = "True" // 忽略投影器
}
这里多了一个IgnoreProjector,这个是我们不常见的Tag,其含义是:

这里的所谓的Projector则是:

cs
// 使用 Standard 光照模型,启用透明混合,保留透明度,启用全向阴影
#pragma surface surf Standard alpha:fade keepalpha fullforwardshadows vertex:vertexDataFunc
使用表面着色器,标准光照模型,保留透明度。
cs
// 顶点着色器函数
void vertexDataFunc(inout appdata_full v, out Input o)
{
UNITY_INITIALIZE_OUTPUT(Input, o); // 初始化输出结构
// 计算波浪高度,使用正弦函数模拟波浪效果
fixed height = sin(_Time.y * _WaveSpeed + v.vertex.z * _WaveGap + v.vertex.x) * _WaveHeight;
// 根据法线方向调整顶点位置,形成波浪效果
v.vertex.xyz += v.normal * height;
}
顶点着色器函数,首先可以看到一个
cs
UNITY_INITIALIZE_OUTPUT(Input, o); // 初始化输出结构
这句代码的作用是:

然后是我们模拟波浪的函数:
cs
// 计算波浪高度,使用正弦函数模拟波浪效果
fixed height = sin(_Time.y * _WaveSpeed + v.vertex.z * _WaveGap + v.vertex.x) * _WaveHeight;
// 根据法线方向调整顶点位置,形成波浪效果
v.vertex.xyz += v.normal * height;
我们根据正弦函数计算得到的实时波浪高度在法线方向修改顶点位置来实现模拟波浪的效果。
然后是表面着色器函数:
cs
// 表面着色器函数
void surf(Input IN, inout SurfaceOutputStandard o)
{
// 法线贴图
// 根据时间偏移法线贴图的 UV 坐标
float2 speed = _Time.x * float2(_WaveSpeed, _WaveSpeed) * _NormalSpeed;
// 采样法线贴图,并解包法线
fixed3 bump1 = UnpackNormal(tex2D(_NormalTex, IN.uv_FoamTex.xy + speed)).rgb;
fixed3 bump2 = UnpackNormal(tex2D(_NormalTex, IN.uv_FoamTex.xy - speed)).rgb;
// 合并两个法线贴图的结果
fixed3 bump = normalize(bump1 + bump2);
// 调整法线强度
bump.xy *= _NormalScale;
bump = normalize(bump);
// 将法线赋值给输出结构
o.Normal = bump;
// 水体颜色
o.Albedo = _Color;
// 设置透明度
o.Alpha = _Color.a;
}
这里主要看我们对于法线贴图的一系列处理:
我们根据时间偏移乘以波浪速度,然后分别正向和反向采样法线后再叠加在一起,最后调整强度之后返回给输出的结构体。
效果如图:

几何着色器

几何着色器是卡在顶点着色器和片元着色器之间的一个着色器,我们可以选择性地使用。
几何着色器接受的是顶点着色器的输出(曲面细分之后),他最大的作用就是将顶点着色器输出的图元(一个点或者一个图形)修改成其他的不同的图形。直白地说,我们可以这样理解:对于顶点着色器来说,输出的就是一系列经过空间变换的顶点数据包,然后对于这一系列顶点数据包,几何着色器可以将其进行一系列处理达成一些视觉效果等。
cs
[maxcertexcount(N)]
void ShaderName (PrimitiveType InputVertexType InputName[NumElements],
inout StreamOutputObjectVertexType) OutputName){
// 几何着色器具体实现
}
这是几何着色器的一般写法。
其中:

且:

概括地说,我们几何着色器的输入必须是点,线,图,多段相邻的线以及多个相邻的图,输出则要求必须每次调用的顶点数乘以输出顶点结构体的标量数小于最大调用顶点数。
现在让我们来用几何着色器分别处理点、线、图输入,用代码实例来加快理解:
首先是输出点的:
cs
Shader "Chapter6/chapter6_point"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {} // 声明一个名为_MainTex的2D纹理属性,默认值为白色纹理
}
SubShader
{
Tags { "RenderType"="Opaque" } // 设置渲染类型为Opaque(不透明)
LOD 100 // 设置LOD(Level of Detail)为100,表示较低的细节级别
Pass
{
CGPROGRAM
#pragma vertex vert // 使用名为vert的顶点着色器
#pragma geometry geom // 使用名为geom的几何着色器
#pragma fragment frag // 使用名为frag的片段着色器
#include "UnityCG.cginc" // 引入UnityCG常用的着色器库
// 输入顶点结构体,包含顶点位置和纹理坐标
struct a2v
{
float4 vertex : POSITION; // 顶点位置
float2 uv : TEXCOORD0; // 顶点纹理坐标
};
// 顶点着色器到几何着色器之间的中间结构体
struct v2g
{
float4 vertex : POSITION; // 顶点位置
float2 uv : TEXCOORD0; // 顶点纹理坐标
};
// 从几何着色器到片段着色器之间的数据结构
struct g2f
{
float4 vertex : SV_POSITION; // 变换后的顶点位置
float2 uv : TEXCOORD0; // 纹理坐标
};
// 声明_MainTex纹理采样器和ST变换矩阵
sampler2D _MainTex;
float4 _MainTex_ST;
// 顶点着色器函数
v2g vert (a2v v)
{
v2g o;
o.vertex = v.vertex; // 传递顶点位置
o.uv = TRANSFORM_TEX(v.uv, _MainTex); // 变换纹理坐标
return o;
}
// 几何着色器函数
[maxvertexcount(1)] // 每个几何着色器输出1个顶点
void geom(triangle v2g input[3], inout PointStream<g2f> outStream)
{
// 计算输入的3个顶点的平均位置和纹理坐标
float4 vertex = float4(0, 0, 0, 0);
float2 uv = float2(0, 0);
vertex = (input[0].vertex + input[1].vertex + input[2].vertex) / 3; // 计算平均顶点位置
uv = (input[0].uv + input[1].uv + input[2].uv) / 3; // 计算平均纹理坐标
// 输出到片段着色器的数据
g2f o;
o.vertex = UnityObjectToClipPos(vertex); // 将顶点位置转换为裁剪空间
o.uv = uv; // 设置纹理坐标
outStream.Append(o); // 将输出数据加入到流中
}
// 片段着色器函数
fixed4 frag (g2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv); // 从_MainTex纹理中采样颜色
return col; // 返回采样的颜色
}
ENDCG
}
}
}
cs
#pragma vertex vert // 使用名为vert的顶点着色器
#pragma geometry geom // 使用名为geom的几何着色器
#pragma fragment frag // 使用名为frag的片段着色器
可以看到我们在pragma阶段使用了名为geom的几何着色器。
cs
// 输入顶点结构体,包含顶点位置和纹理坐标
struct a2v
{
float4 vertex : POSITION; // 顶点位置
float2 uv : TEXCOORD0; // 顶点纹理坐标
};
// 顶点着色器到几何着色器之间的中间结构体
struct v2g
{
float4 vertex : POSITION; // 顶点位置
float2 uv : TEXCOORD0; // 顶点纹理坐标
};
// 从几何着色器到片段着色器之间的数据结构
struct g2f
{
float4 vertex : SV_POSITION; // 变换后的顶点位置
float2 uv : TEXCOORD0; // 纹理坐标
};
定义了三个阶段的作为输入/输出的结构体,主要成员就是顶点位置和顶点纹理坐标。
cs
// 几何着色器函数
[maxvertexcount(1)] // 每个几何着色器输出1个顶点
void geom(triangle v2g input[3], inout PointStream<g2f> outStream)
{
// 计算输入的3个顶点的平均位置和纹理坐标
float4 vertex = float4(0, 0, 0, 0);
float2 uv = float2(0, 0);
vertex = (input[0].vertex + input[1].vertex + input[2].vertex) / 3; // 计算平均顶点位置
uv = (input[0].uv + input[1].uv + input[2].uv) / 3; // 计算平均纹理坐标
// 输出到片段着色器的数据
g2f o;
o.vertex = UnityObjectToClipPos(vertex); // 将顶点位置转换为裁剪空间
o.uv = uv; // 设置纹理坐标
outStream.Append(o); // 将输出数据加入到流中
}
可以看到我们的参数:
cs
triangle v2g input[3], inout PointStream<g2f> outStream


然后是我们的Append方法:

效果如图:

然后我们来输出线的:
cs
Shader "Chapter6/chapter6_line"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {} // 声明一个名为_MainTex的2D纹理属性,默认值为白色纹理
}
SubShader
{
Tags
{
"RenderType"="Opaque"
} // 设置渲染类型为Opaque(不透明)
LOD 100 // 设置LOD(Level of Detail)为100,表示较低的细节级别
Pass
{
CGPROGRAM
#pragma vertex vert // 使用名为vert的顶点着色器
#pragma geometry geom // 使用名为geom的几何着色器
#pragma fragment frag // 使用名为frag的片段着色器
#include "UnityCG.cginc" // 引入UnityCG常用的着色器库
// 输入顶点结构体,包含顶点位置和纹理坐标
struct a2v
{
float4 vertex : POSITION; // 顶点位置
float2 uv : TEXCOORD0; // 顶点纹理坐标
};
// 顶点着色器到几何着色器之间的中间结构体
struct v2g
{
float4 vertex : POSITION; // 顶点位置
float2 uv : TEXCOORD0; // 顶点纹理坐标
};
// 从几何着色器到片段着色器之间的数据结构
struct g2f
{
float4 vertex : SV_POSITION; // 变换后的顶点位置
float2 uv : TEXCOORD0; // 纹理坐标
};
// 声明_MainTex纹理采样器和ST变换矩阵
sampler2D _MainTex;
float4 _MainTex_ST;
// 顶点着色器函数
v2g vert(a2v v)
{
v2g o;
o.vertex = v.vertex; // 传递顶点位置
o.uv = TRANSFORM_TEX(v.uv, _MainTex); // 变换纹理坐标
return o;
}
// 几何着色器函数
[maxvertexcount(6)] // 最大顶点输出数为6个(因为每条边输出2个顶点,总共3条边)
void geom(triangle v2g input[3], inout LineStream<g2f> outStream)
{
// 遍历三角形的3个顶点
for (int i = 0; i < 3; i++)
{
g2f o;
// 处理当前顶点,将其转换为裁剪空间坐标并设置纹理坐标
o.vertex = UnityObjectToClipPos(input[i].vertex); // 将顶点位置从对象空间转换到裁剪空间
o.uv = input[i].uv; // 传递纹理坐标
// 将当前顶点添加到输出流
outStream.Append(o);
// 计算当前顶点的下一个顶点(形成边)
int next = (i + 1) % 3; // 使用环形索引来获得下一个顶点
// 处理下一个顶点
o.vertex = UnityObjectToClipPos(input[next].vertex); // 将下一个顶点的位置转换到裁剪空间
o.uv = input[next].uv; // 传递下一个顶点的纹理坐标
// 将下一个顶点添加到输出流
outStream.Append(o);
}
}
// 片段着色器函数
fixed4 frag(g2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv); // 从_MainTex纹理中采样颜色
return col; // 返回采样的颜色
}
ENDCG
}
}
}
可以看到我们的几何着色器有所变化:
cs
// 几何着色器函数
[maxvertexcount(6)] // 最大顶点输出数为6个(因为每条边输出2个顶点,总共3条边)
void geom(triangle v2g input[3], inout LineStream<g2f> outStream)
{
// 遍历三角形的3个顶点
for (int i = 0; i < 3; i++)
{
g2f o;
// 处理当前顶点,将其转换为裁剪空间坐标并设置纹理坐标
o.vertex = UnityObjectToClipPos(input[i].vertex); // 将顶点位置从对象空间转换到裁剪空间
o.uv = input[i].uv; // 传递纹理坐标
// 将当前顶点添加到输出流
outStream.Append(o);
// 计算当前顶点的下一个顶点(形成边)
int next = (i + 1) % 3; // 使用环形索引来获得下一个顶点
// 处理下一个顶点
o.vertex = UnityObjectToClipPos(input[next].vertex); // 将下一个顶点的位置转换到裁剪空间
o.uv = input[next].uv; // 传递下一个顶点的纹理坐标
// 将下一个顶点添加到输出流
outStream.Append(o);
}
}
分别遍历三个点并且将该点添加到输出流中。
效果如图:

关于输出面的:如果只是照常输出面的话我们只能看到一个正常的球,没啥意思,所以我们来做点新东西。
cs
Shader "Chapter6/chapter6_triangle_ani"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {} // 主纹理
_BottomColor ("Bottom Color", Color) = (0, 0, 0, 1) // 底部颜色
_TopColor ("Top Color", Color) = (1, 1, 1, 1) // 顶部颜色
_ExtrudeMaxValue ("Extrude Max Value", Range(0, 1)) = 1 // 最大挤出值
_ExtrudeRandomValue ("Extrude Random Value", Range(0, 1)) = 1 // 随机挤出值
_ExtrudeSpeed ("Extrude Speed", Float) = 1 // 挤出速度
}
SubShader
{
Tags
{
"RenderType"="Opaque"
} // 渲染类型为不透明
LOD 100 // 细节级别
Pass
{
Cull Off // 关闭背面剔除
CGPROGRAM
#pragma vertex vert // 顶点着色器
#pragma geometry geom // 几何着色器
#pragma fragment frag // 片段着色器
#include "UnityCG.cginc" // 包含Unity的CG库
// 顶点着色器输入结构
struct a2v
{
float4 vertex : POSITION; // 顶点位置
float2 uv : TEXCOORD0; // 纹理坐标
};
// 顶点着色器输出结构
struct v2g
{
float4 vertex : POSITION; // 顶点位置
float2 uv : TEXCOORD0; // 纹理坐标
};
// 几何着色器输出结构
struct g2f
{
float4 vertex : SV_POSITION; // 裁剪空间中的顶点位置
float2 uv : TEXCOORD0; // 纹理坐标
float4 color : COLOR; // 顶点颜色
};
sampler2D _MainTex; // 主纹理
float4 _MainTex_ST; // 主纹理的缩放和偏移
float4 _BottomColor; // 底部颜色
float4 _TopColor; // 顶部颜色
float _ExtrudeMaxValue; // 最大挤出值
float _ExtrudeRandomValue; // 随机挤出值
float _ExtrudeSpeed; // 挤出速度
// 顶点着色器
v2g vert(a2v v)
{
v2g o;
o.vertex = v.vertex; // 传递顶点位置
o.uv = TRANSFORM_TEX(v.uv, _MainTex); // 转换纹理坐标
return o;
}
// 计算法线
float3 ConstructNormal(float3 v1, float3 v2, float3 v3)
{
return normalize(cross(v2 - v1, v3 - v1)); // 通过叉积计算法线
}
// 几何着色器
[maxvertexcount(21)] // 最大顶点数
void geom(triangle v2g input[3], uint pid : SV_PrimitiveID, inout TriangleStream<g2f> outStream)
{
// 计算挤出量,基于时间和速度
// 计算基础挤出量:基于时间的周期性变化
float baseExtrude = cos(_Time.y * 2 * UNITY_PI * _ExtrudeSpeed); // 范围 [-1, 1]
baseExtrude = baseExtrude * 0.5 + 0.5; // 映射到 [0, 1]
baseExtrude *= _ExtrudeMaxValue; // 控制挤出量的最大幅度
// 计算随机挤出量:基于时间和图元 ID 的随机变化
float randomSeed = pid * 853.8425415; // 使用图元 ID 作为随机种子
float randomExtrude = sin(randomSeed + _Time.y * 2 * UNITY_PI * _ExtrudeSpeed); // 范围 [-1, 1]
randomExtrude = randomExtrude * 0.5 + 0.5; // 映射到 [0, 1]
randomExtrude *= _ExtrudeRandomValue; // 控制随机挤出量的强度
// 最终挤出量 = 基础挤出量 + 随机挤出量
float extrudeAmount = baseExtrude + randomExtrude;
// 计算法线并乘以挤出量
float3 normal = ConstructNormal(input[0].vertex, input[1].vertex, input[2].vertex);
normal = normal * extrudeAmount;
g2f o;
// 生成三角形的侧面
for (int i = 0; i < 3; i++)
{
int index = (i + 1) % 3;
// 底部顶点
o.vertex = input[i].vertex;
o.vertex = UnityObjectToClipPos(o.vertex);
o.uv = input[i].uv;
o.color = _BottomColor;
outStream.Append(o);
// 顶部顶点
o.vertex = input[i].vertex;
o.vertex.xyz += normal;
o.vertex = UnityObjectToClipPos(o.vertex);
o.uv = input[i].uv;
o.color = _TopColor;
outStream.Append(o);
// 下一个底部顶点
o.vertex = input[index].vertex;
o.vertex = UnityObjectToClipPos(o.vertex);
o.uv = input[index].uv;
o.color = _BottomColor;
outStream.Append(o);
outStream.RestartStrip(); // 结束当前三角形带
// 顶部顶点
o.vertex = input[i].vertex;
o.vertex.xyz += normal;
o.vertex = UnityObjectToClipPos(o.vertex);
o.uv = input[i].uv;
o.color = _TopColor;
outStream.Append(o);
// 下一个底部顶点
o.vertex = input[index].vertex;
o.vertex = UnityObjectToClipPos(o.vertex);
o.uv = input[index].uv;
o.color = _BottomColor;
outStream.Append(o);
// 下一个顶部顶点
o.vertex = input[index].vertex;
o.vertex.xyz += normal;
o.vertex = UnityObjectToClipPos(o.vertex);
o.uv = input[index].uv;
o.color = _TopColor;
outStream.Append(o);
outStream.RestartStrip(); // 结束当前三角形带
}
// 生成顶部三角形
for (int i = 0; i < 3; i++)
{
o.vertex = input[i].vertex;
o.vertex.xyz += normal;
o.vertex = UnityObjectToClipPos(o.vertex);
o.uv = input[i].uv;
o.color = _TopColor;
outStream.Append(o);
}
outStream.RestartStrip(); // 结束当前三角形带
}
// 片段着色器
fixed4 frag(g2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv) * i.color; // 采样纹理并乘以颜色
return col;
}
ENDCG
}
}
}
cs
Properties
{
_MainTex ("Texture", 2D) = "white" {} // 主纹理
_BottomColor ("Bottom Color", Color) = (0, 0, 0, 1) // 底部颜色
_TopColor ("Top Color", Color) = (1, 1, 1, 1) // 顶部颜色
_ExtrudeMaxValue ("Extrude Max Value", Range(0, 1)) = 1 // 最大挤出值
_ExtrudeRandomValue ("Extrude Random Value", Range(0, 1)) = 1 // 随机挤出值
_ExtrudeSpeed ("Extrude Speed", Float) = 1 // 挤出速度
}
设置了一个最大挤出值和随机挤出值,保证挤出值小于等于最大挤出值,然后每一个面的挤出值都是随机的。
依然主要看几何着色器:
cs
// 几何着色器
[maxvertexcount(21)] // 最大顶点数
void geom(triangle v2g input[3], uint pid : SV_PrimitiveID, inout TriangleStream<g2f> outStream)
{
// 计算挤出量,基于时间和速度
// 计算基础挤出量:基于时间的周期性变化
float baseExtrude = cos(_Time.y * 2 * UNITY_PI * _ExtrudeSpeed); // 范围 [-1, 1]
baseExtrude = baseExtrude * 0.5 + 0.5; // 映射到 [0, 1]
baseExtrude *= _ExtrudeMaxValue; // 控制挤出量的最大幅度
// 计算随机挤出量:基于时间和图元 ID 的随机变化
float randomSeed = pid * 853.8425415; // 使用图元 ID 作为随机种子
float randomExtrude = sin(randomSeed + _Time.y * 2 * UNITY_PI * _ExtrudeSpeed); // 范围 [-1, 1]
randomExtrude = randomExtrude * 0.5 + 0.5; // 映射到 [0, 1]
randomExtrude *= _ExtrudeRandomValue; // 控制随机挤出量的强度
// 最终挤出量 = 基础挤出量 + 随机挤出量
float extrudeAmount = baseExtrude + randomExtrude;
// 计算法线并乘以挤出量
float3 normal = ConstructNormal(input[0].vertex, input[1].vertex, input[2].vertex);
normal = normal * extrudeAmount;
g2f o;
// 生成三角形的侧面
for (int i = 0; i < 3; i++)
{
int index = (i + 1) % 3;
// 底部顶点
o.vertex = input[i].vertex;
o.vertex = UnityObjectToClipPos(o.vertex);
o.uv = input[i].uv;
o.color = _BottomColor;
outStream.Append(o);
// 顶部顶点
o.vertex = input[i].vertex;
o.vertex.xyz += normal;
o.vertex = UnityObjectToClipPos(o.vertex);
o.uv = input[i].uv;
o.color = _TopColor;
outStream.Append(o);
// 下一个底部顶点
o.vertex = input[index].vertex;
o.vertex = UnityObjectToClipPos(o.vertex);
o.uv = input[index].uv;
o.color = _BottomColor;
outStream.Append(o);
outStream.RestartStrip(); // 结束当前三角形带
// 顶部顶点
o.vertex = input[i].vertex;
o.vertex.xyz += normal;
o.vertex = UnityObjectToClipPos(o.vertex);
o.uv = input[i].uv;
o.color = _TopColor;
outStream.Append(o);
// 下一个底部顶点
o.vertex = input[index].vertex;
o.vertex = UnityObjectToClipPos(o.vertex);
o.uv = input[index].uv;
o.color = _BottomColor;
outStream.Append(o);
// 下一个顶部顶点
o.vertex = input[index].vertex;
o.vertex.xyz += normal;
o.vertex = UnityObjectToClipPos(o.vertex);
o.uv = input[index].uv;
o.color = _TopColor;
outStream.Append(o);
outStream.RestartStrip(); // 结束当前三角形带
}
// 生成顶部三角形
for (int i = 0; i < 3; i++)
{
o.vertex = input[i].vertex;
o.vertex.xyz += normal;
o.vertex = UnityObjectToClipPos(o.vertex);
o.uv = input[i].uv;
o.color = _TopColor;
outStream.Append(o);
}
outStream.RestartStrip(); // 结束当前三角形带
}
cs
[maxvertexcount(21)] // 最大顶点数
void geom(triangle v2g input[3], uint pid : SV_PrimitiveID, inout TriangleStream<g2f> outStream)
最大顶点数21,参数中接受顶点着色器的三角形输出(三个顶点),然后是一个独一的三角形图元ID,输出一个三角形图元流。
cs
// 计算挤出量,基于时间和速度
// 计算基础挤出量:基于时间的周期性变化
float baseExtrude = cos(_Time.y * 2 * UNITY_PI * _ExtrudeSpeed); // 范围 [-1, 1]
baseExtrude = baseExtrude * 0.5 + 0.5; // 映射到 [0, 1]
baseExtrude *= _ExtrudeMaxValue; // 控制挤出量的最大幅度
计算挤出的量,用一个cos函数得到初始值,移动到[0,1]区间之后乘以最大挤出量。
cs
// 计算随机挤出量:基于时间和图元 ID 的随机变化
float randomSeed = pid * 853.8425415; // 使用图元 ID 作为随机种子
float randomExtrude = sin(randomSeed + _Time.y * 2 * UNITY_PI * _ExtrudeSpeed); // 范围 [-1, 1]
randomExtrude = randomExtrude * 0.5 + 0.5; // 映射到 [0, 1]
randomExtrude *= _ExtrudeRandomValue; // 控制随机挤出量的强度
这部分是随机的挤出量,这里可以看到我们生成随机种子的方法:用我们的独一的图元ID去乘以一个数。
cs
// 最终挤出量 = 基础挤出量 + 随机挤出量
float extrudeAmount = baseExtrude + randomExtrude;
// 计算法线并乘以挤出量
float3 normal = ConstructNormal(input[0].vertex, input[1].vertex, input[2].vertex);
normal = normal * extrudeAmount;
最终挤出量等于基础挤出量加上随机挤出量,我们计算出法线之后乘以这个最终挤出量即可。
效果如图:
