14.Shader得到VAT数据并赋值

14.具体实现-渲染部分-Shader得到VAT数据并赋值


14.1 知识点

主要思路和步骤

在 Shader 中利用 VAT 和对应配置信息,在顶点阶段读出当前帧位置并写回顶点。

  1. 让新建的顶点/片元着色器支持 GPU Instancing(#pragma multi_compile_instancing、实例化宏与缓冲区等,见补充知识篇)。
  2. 新建相关属性:
    • VAT 纹理以及纹素(用于之后索引与 UV 换算)
    • 顶点数
    • 顶点最大、最小值(解压缩)
    • 帧率、起始像素索引、当前动画总帧数、偏移位置(多走实例化属性,便于每实例不同)
  3. 在顶点着色器里按规则取 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
        }
    }
}

分析进行顶点位置的相关处理,需要得到相关数据

  1. 当前动画的起始索引位置 = pixelIndex

  2. 计算当前动画帧(带偏移)

    • 将时间转换为帧索引,使用取余实现循环播放
    • 公式:(_Time.y * frameRate + offsetIndex) % frameCount
  3. 计算当前帧的索引起始位置

    • 公式:帧数 * _VertexCount
  4. 计算最终顶点索引

    • 公式:当前帧的索引起始位置 + 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

×

喜欢就点赞,疼爱就打赏