14.具体实现-渲染部分-Shader得到VAT数据并赋值
14.1 知识点
主要思路和步骤
在 Shader 中利用 VAT 和对应配置信息,在顶点阶段读出当前帧位置并写回顶点。
- 让新建的顶点/片元着色器支持 GPU Instancing(
#pragma multi_compile_instancing、实例化宏与缓冲区等,见补充知识篇)。 - 新建相关属性:
- VAT 纹理以及纹素(用于之后索引与 UV 换算)
- 顶点数
- 顶点最大、最小值(解压缩)
- 帧率、起始像素索引、当前动画总帧数、偏移位置(多走实例化属性,便于每实例不同)
- 在顶点着色器里按规则取 VAT 像素、反映射,再输出裁剪空间与主纹理 UV。
创建 Shader,复制之前补充知识中写的支持 GPU Instancing 的代码
// 支持GPU实例化的Unlit通用着色器
// 适用于不透明物体渲染,可通过实例化属性批量修改物体参数
Shader "Unlit/VATShader"
{
// 材质面板可编辑的属性
Properties
{
// 主纹理:用于物体表面的基础贴图,默认填充白色
_MainTex ("Texture", 2D) = "white" {}
}
// 子着色器:核心渲染逻辑定义
SubShader
{
// 渲染标签:标记为不透明物体,适配Unity渲染管线
Tags
{
"RenderType"="Opaque"
}
// 细节级别:控制渲染距离,100为基础不透明物体标准值
LOD 100
// 渲染通道:执行顶点/片元渲染的核心通道
Pass
{
CGPROGRAM
// 编译指令:指定顶点着色器函数
#pragma vertex vert
// 编译指令:指定片元着色器函数
#pragma fragment frag
// 编译指令:启用雾效多版本编译,适配场景雾效
#pragma multi_compile_fog
// 编译指令:启用GPU实例化支持,核心功能开关
#pragma multi_compile_instancing
// 引入Unity内置CG函数库,包含空间转换、雾效等核心函数
#include "UnityCG.cginc"
// ==================== GPU实例化属性缓冲区 ====================
// 定义实例化参数组,每个物体实例可独立修改以下参数
UNITY_INSTANCING_BUFFER_START(MrTao)
// 实例化颜色:每个实例独立的颜色参数
UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
// 实例化位置:每个实例独立的位置偏移参数
UNITY_DEFINE_INSTANCED_PROP(float4, _Pos)
// 实例化偏移索引:用于区分不同实例的序号参数
UNITY_DEFINE_INSTANCED_PROP(int, _OffsetIndex)
UNITY_INSTANCING_BUFFER_END(MrTao)
// ==================== 顶点着色器输入结构体 ====================
// 接收网格数据:顶点位置、UV、实例ID
struct appdata
{
float4 vertex : POSITION; // 模型空间顶点坐标
float2 uv : TEXCOORD0; // 模型第一套纹理坐标
UNITY_VERTEX_INPUT_INSTANCE_ID // 内置宏:存储GPU实例ID(无分号)
};
// ==================== 顶点着色器输出结构体 ====================
// 传递数据给片元着色器:UV、雾效、裁剪空间坐标、实例ID
struct v2f
{
float2 uv : TEXCOORD0; // 传递给片元的纹理坐标
UNITY_FOG_COORDS(1) // 内置宏:存储雾效计算数据(无分号!)
float4 vertex : SV_POSITION;// 裁剪空间顶点坐标(屏幕坐标)
UNITY_VERTEX_INPUT_INSTANCE_ID // 传递实例ID到片元着色器(无分号)
};
// 主纹理采样器:用于采样纹理颜色
sampler2D _MainTex;
// 主纹理缩放偏移:Unity自动生成,适配纹理Tiling/Offset属性
float4 _MainTex_ST;
// ==================== 顶点着色器 ====================
// 功能:处理顶点空间转换、实例化数据传递、纹理坐标计算
v2f vert(appdata appdata)
{
// 定义输出结构体变量
v2f v2f;
// 初始化实例ID:必须在访问实例化属性前调用
UNITY_SETUP_INSTANCE_ID(appdata);
// 传递实例ID:将顶点输入的实例ID复制到输出结构体
UNITY_TRANSFER_INSTANCE_ID(appdata, v2f);
// 获取当前物体实例的自定义颜色属性
float4 color = UNITY_ACCESS_INSTANCED_PROP(MrTao, _Color);
// 空间转换:将模型空间顶点转换为屏幕裁剪空间坐标
v2f.vertex = UnityObjectToClipPos(appdata.vertex);
// 纹理坐标计算:应用纹理的缩放和偏移
v2f.uv = TRANSFORM_TEX(appdata.uv, _MainTex);
// 雾效数据计算:传递顶点雾效参数
UNITY_TRANSFER_FOG(v2f, v2f.vertex);
// 将处理完成的数据传递给片元着色器
return v2f;
}
// ==================== 片元着色器 ====================
// 功能:采样纹理、应用雾效、输出最终像素颜色
fixed4 frag(v2f v2f) : SV_Target
{
// 初始化实例ID:片元着色器中使用实例属性的必备步骤
UNITY_SETUP_INSTANCE_ID(v2f);
// 纹理采样:根据UV坐标获取主纹理的颜色
fixed4 col = tex2D(_MainTex, v2f.uv);
// 雾效应用:根据雾效数据混合最终颜色
UNITY_APPLY_FOG(v2f.fogCoord, col);
// 输出最终渲染颜色
return col;
}
ENDCG
}
}
}
属性块中增加 VAT 纹理属性
Shader "Unlit/VATShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
// 顶点动画纹理(Vertex Animation Texture):存储每帧顶点位置数据
_VAT_Tex ("VAT_Tex", 2D) = "white" {}
}
}
Pass 内生成VAT 采样、纹素与实例化缓冲区
Pass 里声明 VAT 与顶点相关 uniform,并把实例化缓冲改成 VAT 动画用的字段(帧率、偏移、起始像素、总帧数)。
Shader "Unlit/VATShader"
{
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
#include "UnityCG.cginc"
// 主纹理
sampler2D _MainTex;
// 主纹理缩放与偏移参数(xy: Tiling, zw: Offset)
float4 _MainTex_ST;
// 顶点动画纹理(VAT)
sampler2D _VAT_Tex;
// VAT纹理纹素尺寸(xy: 1/尺寸, zw: 实际尺寸)
float4 _VAT_Tex_TexelSize;
uint _VertexCount; // 模型顶点总数
int _VertexMax; // 顶点坐标最大值(用于解压缩)
int _VertexMin; // 顶点坐标最小值(用于解压缩)
// 定义实例化属性缓冲区(组名:VAT)
UNITY_INSTANCING_BUFFER_START(VAT)
UNITY_DEFINE_INSTANCED_PROP(uint, _FrameRate) // 动画帧率
UNITY_DEFINE_INSTANCED_PROP(uint, _OffsetIndex) // 帧偏移索引
UNITY_DEFINE_INSTANCED_PROP(uint, _PixelIndex) // VAT纹理中该动画的起始像素位置
UNITY_DEFINE_INSTANCED_PROP(uint, _FrameCount) // 动画总帧数
UNITY_INSTANCING_BUFFER_END(VAT)
//... struct appdata / v2f、vert、frag 见文末完整文件
ENDCG
}
}
}
分析进行顶点位置的相关处理,需要得到相关数据
当前动画的起始索引位置 =
pixelIndex计算当前动画帧(带偏移)
- 将时间转换为帧索引,使用取余实现循环播放
- 公式:
(_Time.y * frameRate + offsetIndex) % frameCount
计算当前帧的索引起始位置
- 公式:
帧数 * _VertexCount
- 公式:
计算最终顶点索引
- 公式:
当前帧的索引起始位置 + vid
- 公式:
计算顶点在 VAT 纹理中的索引位置
// ====================== 计算VAT纹理索引 ======================
// 思路:将"时间"转换为"纹理坐标",找到当前顶点在VAT中的位置
// 完整公式:index = 动画起始位置 + 当前帧偏移 + 当前顶点ID
float index = pixelIndex + // [变量] pixelIndex: 该动画在VAT纹理中的起始像素位置
floor(fmod(_Time.y * frameRate + offsetIndex, frameCount)) * _VertexCount +
// [变量] _Time.y:时间(秒), frameRate:帧率, offsetIndex:帧偏移, frameCount:总帧数, _VertexCount:顶点总数
vid; // [变量] vid: 当前顶点ID(系统内置,代表这是第几个顶点)
计算坐标,把 1D 索引转成 2D 纹理坐标
// 假设纹理尺寸为 2048x2048
// [变量] _VAT_Tex_TexelSize.zw = (2048, 2048) - 纹理的实际尺寸
// [变量] _VAT_Tex_TexelSize.xy = (1/2048, 1/2048) - 纹素大小,用于归一化
float x = fmod(index, _VAT_Tex_TexelSize.z); // X坐标 = 索引 对 2048 取余(得到0-2047之间的值)
x = x * _VAT_Tex_TexelSize.x; // 归一化:将[0,2047]范围转换到[0,1],即 x / 2048
float y = index * _VAT_Tex_TexelSize.x; // Y坐标 = 索引 除以 2048(整数除法,得到行号)
y = y * _VAT_Tex_TexelSize.y; // 归一化:将[0,2047]范围转换到[0,1],即 y / 2048
// 组合成最终的UV采样坐标
float2 uv = float2(x, y);
采样与解压缩,得到纹理中存储的位置,基于最大最小值,得到正常的顶点返回,并且覆盖掉模型穿过来的顶点
// ====================== 采样与解压缩 ======================
// 从VAT纹理中读取顶点位置(使用tex2Dlod避免mipmap模糊)
float3 vertexPos = tex2Dlod(_VAT_Tex, float4(uv, 0, 0));
// [变量] _VertexMax/_VertexMin: 顶点坐标的最大/最小值
// 解压缩:将[0,1]范围的存储值映射回原始的世界坐标范围
vertexPos = vertexPos * (_VertexMax - _VertexMin) + _VertexMin;
// 用VAT计算出的新位置覆盖原始顶点位置
appdata.vertex = float4(vertexPos, appdata.vertex.w);
将顶点从模型空间转换到裁剪空间,传递主纹理的UV坐标
// ====================== 标准输出 ======================
// 将顶点从模型空间转换到裁剪空间(Unity内置函数)
v2f.vertex = UnityObjectToClipPos(appdata.vertex);
// 传递主纹理的UV坐标(应用Tiling和Offset)
v2f.uv = TRANSFORM_TEX(appdata.uv, _MainTex);
14.2 知识点代码
VATShader.shader
Shader "Unlit/VATShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
//VAT纹理属性
_VAT_Tex ("VAT_Tex", 2D) = "white" {}
}
SubShader
{
Tags
{
"RenderType"="Opaque"
}
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//添加的代码
#pragma multi_compile_instancing
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
//添加的代码
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
//添加的代码
UNITY_VERTEX_INPUT_INSTANCE_ID //只要你要访问片元着色器中的实例化属性时才添加
};
// 主纹理
sampler2D _MainTex;
// 主纹理缩放与偏移参数(xy: Tiling, zw: Offset)
float4 _MainTex_ST;
// VAT纹理
sampler2D _VAT_Tex;
// VAT纹理纹素尺寸:xy是纹理的尺寸分之一 zw是纹理的尺寸
float4 _VAT_Tex_TexelSize;
uint _VertexCount; // 顶点数
int _VertexMax; //顶点最大值
int _VertexMin; //顶点最小值
//开始定义一个实例缓冲区,名字叫 Props(这是你自己起的组名)
UNITY_INSTANCING_BUFFER_START(VAT)
UNITY_DEFINE_INSTANCED_PROP(int, _FrameRate) //帧率
UNITY_DEFINE_INSTANCED_PROP(int, _OffsetIndex) //顶点偏移
UNITY_DEFINE_INSTANCED_PROP(int, _PixelIndex) //像素索引起始位置
UNITY_DEFINE_INSTANCED_PROP(int, _FrameCount) //当前动画总帧数
UNITY_INSTANCING_BUFFER_END(VAT) //结束定义
v2f vert(appdata appdata, uint vid:SV_VertexID)
{
v2f v2f;
//添加的代码
UNITY_SETUP_INSTANCE_ID(appdata);
UNITY_TRANSFER_INSTANCE_ID(appdata, v2f);
//帧率
int frameRate = UNITY_ACCESS_INSTANCED_PROP(VAT, _FrameRate);
//索引偏移
int offsetIndex = UNITY_ACCESS_INSTANCED_PROP(VAT, _OffsetIndex);
//索引起始
int pixelIndex = UNITY_ACCESS_INSTANCED_PROP(VAT, _PixelIndex);
//总帧数
int frameCount = UNITY_ACCESS_INSTANCED_PROP(VAT, _FrameCount);
//进行顶点位置的相关处理
//1.当前动画的起始索引位置 = pixelIndex
//2.当前动画播放到哪一帧了(偏移位置)
// 把当前时间转换成帧 0~n秒的时间 取余
// (_Time.y * frameRate + offsetIndex)%frameCount = 当前动画的帧数
//3.用帧数*顶点数 得到当前帧的索引起始位置
// 帧数 * _VertexCount = 当前帧的索引起始位置
//4.当前是该帧动画中的第几个顶点
// 当前帧的索引起始位置 + vid = 最终顶点索引
// ====================== 计算VAT纹理索引 ======================
// 思路:将"时间"转换为"纹理坐标",找到当前顶点在VAT中的位置
// 完整公式:index = 动画起始位置 + 当前帧偏移 + 当前顶点ID
float index = pixelIndex + // [变量] pixelIndex: 该动画在VAT纹理中的起始像素位置
floor(fmod(_Time.y * frameRate + offsetIndex, frameCount)) * _VertexCount +
// [变量] _Time.y:时间(秒), frameRate:帧率, offsetIndex:帧偏移, frameCount:总帧数, _VertexCount:顶点总数
vid; // [变量] vid: 当前顶点ID(系统内置,代表这是第几个顶点)
// ====================== 1D索引转2D纹理坐标 ======================
// 假设纹理尺寸为 2048x2048
// [变量] _VAT_Tex_TexelSize.zw = (2048, 2048) - 纹理的实际尺寸
// [变量] _VAT_Tex_TexelSize.xy = (1/2048, 1/2048) - 纹素大小,用于归一化
float x = fmod(index, _VAT_Tex_TexelSize.z); // X坐标 = 索引 对 2048 取余(得到0-2047之间的值)
x = x * _VAT_Tex_TexelSize.x; // 归一化:将[0,2047]范围转换到[0,1],即 x / 2048
float y = index * _VAT_Tex_TexelSize.x; // Y坐标 = 索引 除以 2048(整数除法,得到行号)
y = y * _VAT_Tex_TexelSize.y; // 归一化:将[0,2047]范围转换到[0,1],即 y / 2048
// 组合成最终的UV采样坐标
float2 uv = float2(x, y);
// ====================== 采样与解压缩 ======================
// 从VAT纹理中读取顶点位置(使用tex2Dlod避免mipmap模糊)
float3 vertexPos = tex2Dlod(_VAT_Tex, float4(uv, 0, 0));
// [变量] _VertexMax/_VertexMin: 顶点坐标的最大/最小值
// 解压缩:将[0,1]范围的存储值映射回原始的世界坐标范围
vertexPos = vertexPos * (_VertexMax - _VertexMin) + _VertexMin;
// 用VAT计算出的新位置覆盖原始顶点位置
appdata.vertex = float4(vertexPos, appdata.vertex.w);
// ====================== 标准输出 ======================
// 将顶点从模型空间转换到裁剪空间(Unity内置函数)
v2f.vertex = UnityObjectToClipPos(appdata.vertex);
// 传递主纹理的UV坐标(应用Tiling和Offset)
v2f.uv = TRANSFORM_TEX(appdata.uv, _MainTex);
return v2f;
}
fixed4 frag(v2f v2f) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(v2f);
// sample the texture
fixed4 col = tex2D(_MainTex, v2f.uv);
return col;
}
ENDCG
}
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com