【渲染流水线】[几何阶段]-[顶点着色]以UnityURP为例

【从UnityURP开始探索游戏渲染】专栏-直达

前情提要

【渲染流水线】主线索引-从数据到图像以UnityURP为例-CSDN博客

  • 作用‌:处理模型顶点数据(坐标、法线、UV),输出裁剪空间位置(如顶点的MVP矩阵转换顶点从模型空间到裁剪空间)。
  • 裁剪空间‌:MVP变换的终点,顶点坐标未归一化,需保留w分量用于深度计算‌
  • 可配置‌:通过 Shader 代码重写顶点函数(如 #pragma vertex vert)。

对渲染的探索是个持续不断完善的过程,记录这个过程将零散的内容整理起来,其中肯定会有理解偏差和问题,如果哪里有问题,欢迎在评论区探讨和指出)

核心功能

  • 顶点变换‌

    • 将模型空间顶点位置转换为齐次裁剪空间坐标(positionOS → positionHCS),这是渲染的基础步骤。顶点着色器输出的 SV_POSITION 语义变量决定了最终屏幕位置。
  • 数据预处理‌

    • 计算法线、切线、UV 等属性,并传递给片元着色器进行光照或纹理采样。例如,对 UV 进行 Tilling 和 Offset 操作以适应贴图重复。
  • 插值数据生成‌

    • 为片元着色器准备插值数据(如光照贴图坐标、雾效混合因子),确保三角面片内属性的平滑过渡。

关键技术应用‌

  • GPU Instancing 支持‌:通过 UNITY_MATRIX_MVP 等宏处理实例化对象的变换矩阵,提升批量渲染效率。
  • 特效实现‌:
    • 顶点偏移‌:动态修改顶点位置(如 v.positionOS.x += sin(_Time.y)),实现模型扭曲、幽灵效果等动态视觉。
    • 描边技术‌:通过平滑法线计算(存储在顶点色中)并外扩顶点,实现卡通渲染中的描边效果,避免尖端断裂问题

注意事项‌

  • 空间转换顺序‌:透视矫正插值必须在片元着色器中进行,顶点着色器中直接使用屏幕坐标会导致错误。
  • 渲染流程触发‌:通过 CommandBuffer 提交绘制命令后,GPU 自动执行顶点着色器,无需手动调用
  • 作用‌:处理模型顶点数据(坐标、法线、UV),输出裁剪空间位置(如顶点的MVP矩阵转换顶点从模型空间到裁剪空间)。
  • 裁剪空间‌:MVP变换的终点,顶点坐标未归一化,需保留w分量用于深度计算‌
  • 可配置‌:通过 Shader 代码重写顶点函数(如 #pragma vertex vert)。

核心功能

  • 顶点变换‌:将模型空间顶点位置转换为齐次裁剪空间坐标(positionOS → positionHCS),这是渲染的基础步骤。顶点着色器输出的 SV_POSITION 语义变量决定了最终屏幕位置。
  • 数据预处理‌:计算法线、切线、UV 等属性,并传递给片元着色器进行光照或纹理采样。例如,对 UV 进行 Tilling 和 Offset 操作以适应贴图重复。
  • 插值数据生成‌:为片元着色器准备插值数据(如光照贴图坐标、雾效混合因子),确保三角面片内属性的平滑过渡。

关键技术应用‌

  • GPU Instancing 支持‌:通过 UNITY_MATRIX_MVP 等宏处理实例化对象的变换矩阵,提升批量渲染效率。
  • 特效实现‌:
    • 顶点偏移‌:动态修改顶点位置(如 v.positionOS.x += sin(_Time.y)),实现模型扭曲、幽灵效果等动态视觉。
    • 描边技术‌:通过平滑法线计算(存储在顶点色中)并外扩顶点,实现卡通渲染中的描边效果,避免尖端断裂问题

注意事项‌

  • 空间转换顺序‌:透视矫正插值必须在片元着色器中进行,顶点着色器中直接使用屏幕坐标会导致错误。
  • 渲染流程触发‌:通过 CommandBuffer 提交绘制命令后,GPU 自动执行顶点着色器,无需手动调用

顶点的空间转换

  • 空间转换的必要性

    在渲染管线中,顶点数据需要经历多次坐标变换才能最终呈现在屏幕上。空间转换的核心目的是:

    1. 统一计算基准:将模型、光照、相机等数据对齐到同一坐标系
    2. 优化渲染效率:裁剪空间转换后可直接进行视锥剔除
    3. 实现视觉效果:如透视投影、法线贴图等特效依赖特定空间的计算
  • 模型矩阵(M矩阵)

    相关空间:模型空间 → 世界空间

    • 作用:将顶点从模型局部坐标系转换到全局世界坐标系

    • 必要性‌:

      模型空间(局部坐标)仅描述物体自身结构,但场景中所有物体需统一参考系进行交互(如光照、碰撞)。

    • 功能‌:

      • 全局定位‌:物体位置、旋转、缩放统一到世界坐标系,实现场景布局。

      • 物理计算‌:光照方向(如平行光)、碰撞检测依赖世界坐标。

      • 空间关系‌:计算物体间距离或相对方向(如粒子特效跟随)。

      • 关键参数

        unity_ObjectToWorld // 模型→世界矩阵 unity_WorldToObject // 世界→模型逆矩阵

    • 典型应用

      • 计算世界坐标:mul(unity_ObjectToWorld, v.vertex)
      • 法线转换需使用逆转置矩阵:UnityObjectToWorldNormal()

    技术细节:

    • 包含旋转(R)、平移(T)、缩放(S)分量:M = T × R × S
    • 非统一缩放会导致法线扭曲,必须特殊处理
  • 观察矩阵(V矩阵)

    相关空间:世界空间 → 观察空间

    • 作用:以摄像机为原点建立右手坐标系

    • 必要性‌:

      世界坐标需转换为以摄像机为原点的坐标系,确定顶点相对于摄像机的可见性。

    • 功能‌:

      • 裁剪准备‌:后续裁剪操作需基于摄像机视角(如视锥体裁剪)。
      • 视角相关效果‌:实现边缘光、雾效等依赖视角方向的特效。
      • 透视校正‌:齐次除法(x/w, y/w)实现近大远小效果。
      • 深度缓冲‌:z/w 生成标准化深度值 [0,1],用于遮挡排序。
      • 简化投影‌:观察空间是投影变换的输入基准。
      • 特性
        • Z轴指向摄像机前方
        • 坐标系原点在摄像机位置
    • URP接口

      UNITY_MATRIX_V // 世界→观察矩阵 GetWorldSpaceViewDir() // 获取观察方向

    计算原理:

    • 由摄像机位置/旋转参数构建
    • 实际是世界→观察的逆变换
  • 投影矩阵(P矩阵)

    相关空间:观察空间 → 裁剪空间

    • 核心功能

      1. 透视/正交投影转换
      2. 定义视锥体范围(近/远裁剪面)
      3. 生成齐次坐标(w分量用于透视除法)
    • URP实现

      UNITY_MATRIX_P // 观察→裁剪矩阵 UnityObjectToClipPos() // 整合MVP的快捷宏

    透视矩阵特性:

    • 产生近大远小效果
    • 计算公式:
      • [x'] = [ (2n)/(r-l) 0 (r+l)/(r-l) 0 ] [x]
      • [y'] = [ 0 (2n)/(t-b) (t+b)/(t-b) 0 ] [y]
      • [y'] = [ 0 (2n)/(t-b) (t+b)/(t-b) 0 ] [y]
      • [w'] = [ 0 0 -1 0 ] [w]
  • 矩阵组合与应用

    1. 完整变换链

      MVP = P × V × M

      clipPos = mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, v.vertex))

    2. URP优化策略

      • 预计算VP矩阵减少乘法次数
      • 使用TransformXXX系列宏保证跨平台一致性
    3. 调试技巧

      • 通过Frame Debugger验证各空间坐标
      • 使用Visualize Space着色器调试工具
    // URP 顶点着色器片段
    v2f vert (Attributes v) {
        v2f o;
        // M 转换:模型 → 世界
        float3 worldPos = TransformObjectToWorld(v.positionOS);
        // V 转换:世界 → 观察
        float3 viewPos = TransformWorldToView(worldPos);
        // P 转换:观察 → 裁剪
        o.positionCS = TransformWViewToHClip(viewPos);
        return o;
    }

URP 顶点着色器中关键功能

  • MVP矩阵应用‌

    • 模型空间→裁剪空间转换‌使用 UnityObjectToClipPos 宏(内部封装 MVP 矩阵乘法)将顶点坐标转换到裁剪空间:

      hlsl
      v2f vert (appdata v) {
          v2f o;
          o.pos = UnityObjectToClipPos(v.vertex); // 等效于 mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, v.vertex))
          return o;
      }
      
    • 手动拆分计算‌(需处理实例化):

      hlsl
      float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
      o.pos = mul(UNITY_MATRIX_VP, worldPos);
      
  • UV采样与变形‌

    • 基础UV传递‌:通过 TEXCOORD0 语义传递UV坐标:

      hlsl
      struct v2f {
          float2 uv : TEXCOORD0;
          //...
      };
      o.uv = v.uv; // 直接传递
      
    • 动态UV偏移‌(如水流效果):

      hlsl
      o.uv = v.uv + float2(0, _Time.y * _Speed); // 垂直滚动
      
  • 法线处理‌

    • 世界空间法线计算‌:使用 UnityObjectToWorldNormal 宏处理非统一缩放:

      hlsl
      o.worldNormal = UnityObjectToWorldNormal(v.normal); // 自动处理逆转置矩阵
      
    • 法线贴图支持‌:传递切线空间基向量:

      hlsl
      o.tangent = UnityObjectToWorldDir(v.tangent.xyz);
      o.bitangent = cross(o.normal, o.tangent) * v.tangent.w;
      
  • 切线处理‌

    • 切线空间转换‌:用于法线贴图采样:

      hlslCopy Code
      struct appdata {
          float4 tangent : TANGENT; // 切线(w分量决定副切线方向)
      };
      v2f vert(appdata v) {
          o.tangent = mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0)).xyz;
      }
      
  • 常用功能扩展‌

    • 顶点动画‌(如正弦波动画):

      hlsl
      v.vertex.y += sin(_Time.y + v.vertex.x) * _Amplitude;
      
  • GPU实例化支持‌:通过 UNITY_INSTANCING_BUFFER_START 宏传递实例数据。

  • 雾效坐标生成‌:使用 UNITY_TRANSFER_FOG 宏计算雾效混合因子

URP 顶点着色器 写法与内置管线的区别

Unity URP 顶点着色器的写法相较内置管线有多处关键差异,主要体现在宏函数、结构命名、Pass配置、文件包含和数据类型上。核心区别如下:

  • 坐标变换宏的使用‌:

    内置管线使用 UnityObjectToClipPos(或旧版 mul(UNITY_MATRIX_VP, ...))进行模型到裁剪空间转换;URP 中也支持此宏,但需通过 HLSLPROGRAM 声明并包含 URP 专属库文件(如 Core.hlsl)。

    hlsl
    // URP 顶点着色器示例
    v2f vert (Attributes v) {
        v2f o;
        o.vertex = TransformObjectToHClip(v.positionOS); // URP 专用宏
        return o;
    }
    
  • 输入输出结构命名惯例‌:

    内置管线常用 appdata(输入)和 v2f(输出)结构体3;URP 推荐改用 Attributes(输入)和 Varying(输出)作为命名约定,但非强制要求。

  • Pass 标签与光照处理‌:

    内置管线依赖多 Pass 处理光源(每个动态光源独立 Pass);URP 通过单 Pass 前向渲染实现光源计算,Pass 标签需设为 "LightMode"="UniversalForward" 或省略(默认 "SRPDefaultUnlit")。

  • 文件包含与编程块‌:

    内置管线使用 CGPROGRAM/ENDCG 并包含 UnityCG.cginc;URP 必须改用 HLSLPROGRAM/ENDHLSL,并包含 URP 库文件(如 Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl),以避免宏冲突。

  • 数据类型限制‌:

    内置管线支持 fixed 精度类型;URP 中需替换为 half(中等精度)。

  • 不支持的特性‌:

    URP 完全弃用表面着色器(#pragma surface),仅支持顶点/片元着色器;同时不支持 GrabPass,需改用相机不透明纹理或自定义渲染命令实现类似效果

URP中语义解析的核心机制

  • ‌语义解析的底层类‌

    • ShaderPass 与 ShaderCompiler:URP 通过 ShaderCompiler 解析 HLSL 代码中的语义(如 POSITIONNORMAL),并将其映射到 GPU 输入槽位。ShaderPass 负责将语义与渲染管线阶段绑定。
    • InputLayoutBuilder:在 Unity 底层(如 InputLayoutBuilder 类)中,语义会被转换为 Direct3D/OpenGL 的顶点属性描述符,定义数据在 GPU 内存中的布局。
  • ‌自动识别机制‌

    • 系统值语义 SV_前缀
      • SV_POSITION 等系统语义由 GPU 驱动直接识别,光栅化阶段自动读取其值进行屏幕映射和裁剪。
      • 例如,顶点着色器输出的 SV_POSITION 会被固定管线用于透视除法和视口变换,无需开发者干预。
    • 自定义语义 如 TEXCOORD0
      • 通过 VertexAttribute 特性或 HLSL 结构体声明,URP 在编译时自动关联插值器寄存器(如 TEXCOORD0 对应插值器 0)。
      • 光栅化阶段根据插值规则(如透视校正)生成片元数据,传递给片元着色器。
  • ‌管线阶段协同‌

    • 顶点着色器输出到片元着色器‌:
      • 语义标记的数据(如 TEXCOORD0)在几何阶段处理后,由光栅化器插值,最终被片元着色器通过相同语义名读取。
    • 平台适配‌:
      • URP 的 ShaderLibrary 通过宏(如 UNITY_VERTEX_INPUT_INSTANCE_ID)处理跨平台语义差异,确保 Vulkan/Metal 等 API 兼容。
  • ‌调试与验证‌

    • 帧调试器Frame Debugger‌:可查看语义数据在管线各阶段的状态(如顶点着色器输出的裁剪空间坐标)。
    • Shader 变体日志‌:通过 Shader Variant Log Level 检查语义是否被正确剥离或保留。
  • ‌URP 通过 ShaderCompiler 和底层图形 API 协作解析语义,系统值语义由硬件自动处理,自定义语义则通过插值器寄存器传递,最终实现数据在管线中的流动

常用语义

语义数据类型描述
POSITIONfloat3/float4模型空间顶点坐标‌
NORMALfloat3模型空间法线向量‌
TANGENTfloat4模型空间切线向量(.w分量存储副切线方向标志)‌
TEXCOORDnfloat2/float4顶点纹理坐标(n=0-7,如TEXCOORD0表示第一组UV)‌
COLORfixed4/float4顶点颜色‌
SV_VertexIDuint顶点ID
SV_InstanceIDuint实例ID

TEXCOORD常见用途

0: 主UV坐标
1: 光照贴图UV/次UV
2: 动态光照UV
3: 顶点动画数据
4: 烘焙数据/自定义数据
5: 地形混合权重
6: GPU实例化数据
7: 自定义用途

展示了URP顶点着色器中所有常用语义的使用方式

  • 包含了POSITION/NORMAL/TANGENT等基础语义

  • 演示了TEXCOORD0-7的典型用途分配

  • 使用了SV_VertexID和SV_InstanceID实现特殊效果

  • 包含了完整的URP着色器结构和必要的HLSL包含文件

  • 展示了顶点着色器到片段着色器的数据传递方式

  • URPVertexShaderExample.shader

    // HLSL
    Shader "Custom/URPVertexShaderExample"
    {
        Properties
        {
            _MainTex ("Texture", 2D) = "white" {}
            _Color ("Color", Color) = (1,1,1,1)
        }
    
        SubShader
        {
            Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }
            
            Pass
            {
                HLSLPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                
                #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
    
                struct Attributes
                {
                    float4 positionOS   : POSITION;
                    float3 normalOS     : NORMAL;
                    float4 tangentOS    : TANGENT;
                    float4 color        : COLOR;
                    float2 uv0          : TEXCOORD0;
                    float2 uv1          : TEXCOORD1;
                    float2 uv2          : TEXCOORD2;
                    float3 uv3          : TEXCOORD3;
                    float4 uv4          : TEXCOORD4;
                    float2 uv5          : TEXCOORD5;
                    float4 uv6          : TEXCOORD6;
                    float2 uv7          : TEXCOORD7;
                    uint vertexID       : SV_VertexID;
                    uint instanceID     : SV_InstanceID;
                };
    
                struct Varyings
                {
                    float4 positionCS   : SV_POSITION;
                    float3 normalWS     : TEXCOORD0;
                    float4 tangentWS    : TEXCOORD1;
                    float4 color        : TEXCOORD2;
                    float2 uv           : TEXCOORD3;
                    float2 lightmapUV   : TEXCOORD4;
                    float3 dynamicLight : TEXCOORD5;
                    float3 animData     : TEXCOORD6;
                    float4 bakedData    : TEXCOORD7;
                };
    
                TEXTURE2D(_MainTex);
                SAMPLER(sampler_MainTex);
                CBUFFER_START(UnityPerMaterial)
                    float4 _MainTex_ST;
                    float4 _Color;
                CBUFFER_END
    
                Varyings vert(Attributes IN)
                {
                    Varyings OUT;
                    
                    // 使用所有输入语义
                    VertexPositionInputs positionInputs = GetVertexPositionInputs(IN.positionOS.xyz);
                    VertexNormalInputs normalInputs = GetVertexNormalInputs(IN.normalOS, IN.tangentOS);
                    
                    OUT.positionCS = positionInputs.positionCS;
                    OUT.normalWS = normalInputs.normalWS;
                    OUT.tangentWS = float4(normalInputs.tangentWS, IN.tangentOS.w);
                    OUT.color = IN.color * _Color;
                    
                    // 处理各种UV用途
                    OUT.uv = TRANSFORM_TEX(IN.uv0, _MainTex);  // 主UV
                    OUT.lightmapUV = IN.uv1;                   // 光照贴图UV
                    OUT.dynamicLight = float3(IN.uv2, 0);      // 动态光照数据
                    OUT.animData = IN.uv3;                     // 顶点动画数据
                    OUT.bakedData = IN.uv4;                    // 烘焙数据
                    
                    // 使用顶点ID和实例ID进行特殊处理
                    if (IN.vertexID % 2 == 0) {
                        OUT.color.rgb *= 0.9;
                    }
                    
                    if (IN.instanceID > 0) {
                        OUT.positionCS.y += sin(_Time.y * 2.0 + IN.instanceID) * 0.1;
                    }
                    
                    return OUT;
                }
    
                half4 frag(Varyings IN) : SV_Target
                {
                    half4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, IN.uv) * IN.color;
                    return col;
                }
                ENDHLSL
            }
        }
    }
    

接下来:【渲染流水线】[几何阶段]-[曲面细分]以UnityURP为例-CSDN博客


【从UnityURP开始探索游戏渲染】专栏-直达

(欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

淡海水

感谢支持 共同进步 好运++

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值