时间:2025-08-12 16:00
人气:
作者:admin
#pragma vertex vert)。【从UnityURP开始探索游戏渲染】专栏-直达
空间转换的必要性
在渲染管线中,顶点数据需要经历多次坐标变换才能最终呈现在屏幕上。空间转换的核心目的是:
模型矩阵(M矩阵)
相关空间:模型空间 → 世界空间
作用:将顶点从模型局部坐标系转换到全局世界坐标系
必要性:
模型空间(局部坐标)仅描述物体自身结构,但场景中所有物体需统一参考系进行交互(如光照、碰撞)。
功能:
全局定位:物体位置、旋转、缩放统一到世界坐标系,实现场景布局。
物理计算:光照方向(如平行光)、碰撞检测依赖世界坐标。
空间关系:计算物体间距离或相对方向(如粒子特效跟随)。
关键参数:
unity_ObjectToWorld // 模型→世界矩阵 unity_WorldToObject // 世界→模型逆矩阵
典型应用:
mul(unity_ObjectToWorld, v.vertex)UnityObjectToWorldNormal()技术细节:
观察矩阵(V矩阵)
相关空间:世界空间 → 观察空间
作用:以摄像机为原点建立右手坐标系
必要性:
世界坐标需转换为以摄像机为原点的坐标系,确定顶点相对于摄像机的可见性。
功能:
x/w, y/w)实现近大远小效果。z/w 生成标准化深度值 [0,1],用于遮挡排序。URP接口:
UNITY_MATRIX_V // 世界→观察矩阵 GetWorldSpaceViewDir() // 获取观察方向
计算原理:
投影矩阵(P矩阵)
相关空间:观察空间 → 裁剪空间
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]
矩阵组合与应用
完整变换链:
MVP = P × V × M
clipPos = mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, v.vertex))
URP优化策略:
TransformXXX系列宏保证跨平台一致性调试技巧:
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;
}
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;
切线处理
切线空间转换:用于法线贴图采样:
hlsl
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 宏计算雾效混合因子
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,需改用相机不透明纹理或自定义渲染命令实现类似效果
语义解析的底层类
ShaderPass 与 ShaderCompiler:URP 通过 ShaderCompiler 解析 HLSL 代码中的语义(如 POSITION、NORMAL),并将其映射到 GPU 输入槽位。ShaderPass 负责将语义与渲染管线阶段绑定。InputLayoutBuilder:在 Unity 底层(如 InputLayoutBuilder 类)中,语义会被转换为 Direct3D/OpenGL 的顶点属性描述符,定义数据在 GPU 内存中的布局。自动识别机制
SV_POSITION 等系统语义由 GPU 驱动直接识别,光栅化阶段自动读取其值进行屏幕映射和裁剪。SV_POSITION 会被固定管线用于透视除法和视口变换,无需开发者干预。VertexAttribute 特性或 HLSL 结构体声明,URP 在编译时自动关联插值器寄存器(如 TEXCOORD0 对应插值器 0)。管线阶段协同
TEXCOORD0)在几何阶段处理后,由光栅化器插值,最终被片元着色器通过相同语义名读取。ShaderLibrary 通过宏(如 UNITY_VERTEX_INPUT_INSTANCE_ID)处理跨平台语义差异,确保 Vulkan/Metal 等 API 兼容。调试与验证
Shader Variant Log Level 检查语义是否被正确剥离或保留。URP 通过 ShaderCompiler 和底层图形 API 协作解析语义,系统值语义由硬件自动处理,自定义语义则通过插值器寄存器传递,最终实现数据在管线中的流动
| 语义 | 数据类型 | 描述 |
|---|---|---|
POSITION |
float3/float4 |
模型空间顶点坐标 |
NORMAL |
float3 |
模型空间法线向量 |
TANGENT |
float4 |
模型空间切线向量(.w分量存储副切线方向标志) |
TEXCOORDn |
float2/float4 |
顶点纹理坐标(n=0-7,如TEXCOORD0表示第一组UV) |
COLOR |
fixed4/float4 |
顶点颜色 |
SV_VertexID |
uint |
顶点ID |
SV_InstanceID |
uint |
实例ID |
0: 主UV坐标
1: 光照贴图UV/次UV
2: 动态光照UV
3: 顶点动画数据
4: 烘焙数据/自定义数据
5: 地形混合权重
6: GPU实例化数据
7: 自定义用途
包含了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开始探索游戏渲染】专栏-直达
(欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,????)