【渲染流水线】[几何阶段]-[顶点着色]以UnityURP为例
- 作用:处理模型顶点数据(坐标、法线、UV),输出裁剪空间位置(如顶点的MVP矩阵转换顶点从模型空间到裁剪空间)。
- 裁剪空间:MVP变换的终点,顶点坐标未归一化,需保留w分量用于深度计算
- 可配置:通过 Shader 代码重写顶点函数(如
#pragma vertex vert)。
【从UnityURP开始探索游戏渲染】专栏-直达
核心功能
- 顶点变换:将模型空间顶点位置转换为齐次裁剪空间坐标(positionOS → positionHCS),这是渲染的基础步骤。顶点着色器输出的 SV_POSITION 语义变量决定了最终屏幕位置。
- 数据预处理:计算法线、切线、UV 等属性,并传递给片元着色器进行光照或纹理采样。例如,对 UV 进行 Tilling 和 Offset 操作以适应贴图重复。
- 插值数据生成:为片元着色器准备插值数据(如光照贴图坐标、雾效混合因子),确保三角面片内属性的平滑过渡。
关键技术应用
- GPU Instancing 支持:通过 UNITY_MATRIX_MVP 等宏处理实例化对象的变换矩阵,提升批量渲染效率。
- 特效实现:
- 顶点偏移:动态修改顶点位置(如 v.positionOS.x += sin(_Time.y)),实现模型扭曲、幽灵效果等动态视觉。
- 描边技术:通过平滑法线计算(存储在顶点色中)并外扩顶点,实现卡通渲染中的描边效果,避免尖端断裂问题
注意事项
- 空间转换顺序:透视矫正插值必须在片元着色器中进行,顶点着色器中直接使用屏幕坐标会导致错误。
- 渲染流程触发:通过 CommandBuffer 提交绘制命令后,GPU 自动执行顶点着色器,无需手动调用
顶点的空间转换
空间转换的必要性
在渲染管线中,顶点数据需要经历多次坐标变换才能最终呈现在屏幕上。空间转换的核心目的是:
- 统一计算基准:将模型、光照、相机等数据对齐到同一坐标系
- 优化渲染效率:裁剪空间转换后可直接进行视锥剔除
- 实现视觉效果:如透视投影、法线贴图等特效依赖特定空间的计算
模型矩阵(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矩阵)
相关空间:观察空间 → 裁剪空间
- 核心功能:
- 透视/正交投影转换
- 定义视锥体范围(近/远裁剪面)
- 生成齐次坐标(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]矩阵组合与应用
完整变换链:
MVP = P × V × MclipPos = mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, v.vertex))URP优化策略:
- 预计算VP矩阵减少乘法次数
- 使用
TransformXXX系列宏保证跨平台一致性
调试技巧:
- 通过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;
切线处理
切线空间转换:用于法线贴图采样:
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宏计算雾效混合因子
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 代码中的语义(如POSITION、NORMAL),并将其映射到 GPU 输入槽位。ShaderPass负责将语义与渲染管线阶段绑定。 -
InputLayoutBuilder:在 Unity 底层(如InputLayoutBuilder类)中,语义会被转换为 Direct3D/OpenGL 的顶点属性描述符,定义数据在 GPU 内存中的布局。
-
自动识别机制
- 系统值语义 SV_前缀
SV_POSITION等系统语义由 GPU 驱动直接识别,光栅化阶段自动读取其值进行屏幕映射和裁剪。- 例如,顶点着色器输出的
SV_POSITION会被固定管线用于透视除法和视口变换,无需开发者干预。
- 自定义语义 如 TEXCOORD0
- 通过
VertexAttribute特性或 HLSL 结构体声明,URP 在编译时自动关联插值器寄存器(如TEXCOORD0对应插值器 0)。 - 光栅化阶段根据插值规则(如透视校正)生成片元数据,传递给片元着色器。
- 通过
- 系统值语义 SV_前缀
管线阶段协同
- 顶点着色器输出到片元着色器:
- 语义标记的数据(如
TEXCOORD0)在几何阶段处理后,由光栅化器插值,最终被片元着色器通过相同语义名读取。
- 语义标记的数据(如
- 平台适配:
- URP 的
ShaderLibrary通过宏(如UNITY_VERTEX_INPUT_INSTANCE_ID)处理跨平台语义差异,确保 Vulkan/Metal 等 API 兼容。
- URP 的
- 顶点着色器输出到片元着色器:
调试与验证
- 帧调试器Frame Debugger:可查看语义数据在管线各阶段的状态(如顶点着色器输出的裁剪空间坐标)。
- Shader 变体日志:通过
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 |
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开始探索游戏渲染】专栏-直达
(欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,)
【渲染流水线】[几何阶段]-[顶点着色]以UnityURP为例的更多相关文章
- GPU渲染流水线的简单概括
GPU流水线 主要分为两个阶段:几何阶段和光栅化阶段 几何阶段 顶点着色器 --> 曲面细分着色器(可选)----->几何着色器(可选)----->裁剪-->屏幕 ...
- 《UnityShader入门精要》学习笔记之渲染流水线
第一种分类方式: 图形管道(如下7步): 顶点数据 : 由3D模型传递的三角形网格 顶点着色 : 编写CG程序对各个顶点进行着色 生成几何图元 : 连接特定的顶点生成几何图元,例如连接三个顶点生成一个 ...
- Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第五章:渲染流水线
原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第五章:渲染流水线 学习目标 了解几个用以表达真实场景的标志和2D图像 ...
- Opengl_入门学习分享和记录_03_渲染管线(二)再谈顶点着色器以及顶点属性以及属性链接
---恢复内容开始--- 写在前面的废话:岂可修!感觉最近好忙啊,本来今天还有同学约我出去玩的.(小声bb) 正文开始:之前已经编译好的着色器中还有一些问题,比如 layout(location=0) ...
- Shader 入门笔记(二) CPU和GPU之间的通信,渲染流水线
渲染流水线 1)应用阶段(CPU处理) 首先,准备好场景数据(摄像机位置,视锥体,模型和光源等) 接着,做粗粒度剔除工作. 最后,设置好每个模型的渲染状态(使用的材质,纹理,shader等) 这一阶段 ...
- Unity Shader 之 渲染流水线
Unity Shader 之渲染流水线 什么是渲染流水线 一个渲染流程分成3个步骤: 应用阶段(Application stage) 几何阶段(Geometry stage) 光栅化阶段(Raster ...
- Unity Shader入门精要学习笔记 - 第2章 渲染流水线
来源作者:candycat http://blog.csdn.net/candycat1992/article/ 2.1 综述 渲染流水线的最终目的在于生成或者说是渲染一张二维纹理,即我们在电脑屏 ...
- Unity 渲染流水线 :CPU与GPU合作创造的艺术wfd
前言 对于Unity渲染流程的理解可以帮助我们更好对Unity场景进行性能消耗的分析,进而更好的提升场景渲染的效率,最后提升游戏整体的性能表现 Unity的游戏画面的最终的呈现是由CPU与GPU相互配 ...
- 顶点着色器详解 (Vertex Shaders)
学习了顶点处理,你就知道固定功能流水线怎么将顶点从模型空间坐标系统转化到屏幕空间坐标系统.虽然固定功能流水线也可以通过设置渲染状态和参数来改变最终输出的结果,但是它的整体功能还是受限.当我们想实现一个 ...
- Opengl_入门学习分享和记录_02_渲染管线(一)顶点着色器&片段着色器
写在前面的废话:今天俺又来了哈哈,真的好棒棒! 今天的内容:之前我们大概描述了,我们自己定义的顶点坐标是如何被加载到GPU之中,并且介绍了顶点缓冲对象VBO用于管理这一块内存.今天开始详细分析它的具体 ...
随机推荐
- 20250528 - Usual 攻击事件: 价差兑换与请君入瓮
背景信息 项目背景 VaultRouter 合约有用特权身份,可以通过 Usd0PP 合约将 USD0++ 以 1:1 的比例兑换成 USD0,随后通过 UniV3 将 USD0 swap 成 sUS ...
- 【实战】基于 Tauri 和 Rust 实现基于无头浏览器的高可用网页抓取
一.背景 在 Saga Reader 的早期版本中,存在对网页内容抓取成功率不高的问题.主要原因是先前采用的方案为后台进程通过 reqwest 直接发起 GET 请求获取网站 HTML 的方案,虽然仿 ...
- Dify发布V1.5.0:可视化故障排查!超实用
Dify 本周又发布了一个实用的大版本,直接从 V1.4.3 版本干到 V1.5.0 了,那问题来了,这次更新了哪些内容呢?接下来我们一起来看. 官方给这次更新的定义是:一个简洁.强大的更新,通过简化 ...
- 以数据驱动PCB制造革新:盘古信息引领行业领军企业数字化范式跃迁
智能车间内,机械臂正以高精度程序设定完成钻孔作业:每台设备上方的电子看板实时跳动生产数据--订单交付周期.工艺参数偏差值.设备OEE效率指标清晰可见:物料配送AGV小车穿梭于货架间,通过RFID标签自 ...
- pg 判断表或者模式是否存在 满足条件后执行创建表sql
记录一下. 是这么个事,执行初始化脚本的时候报错了 ,原因是引用了其他模式下的表,但是这个模式还没有创建,就导致我有个视图无法创建. 其实这玩意有两个方法,要不然就判断下其他模式下的脚本是否存在,存在 ...
- 使用three.js,实现微信3D小游戏系列教程,框架篇(一)
引言 在三维图形和游戏开发领域,three.js 作为一个基于 WebGL 的 JavaScript 库,提供了强大的功能来创建和显示动画化的 3D 计算机图形.它使得开发者能够轻松地在网页上构建复杂 ...
- java--使用正则对象实现正则的获取功能
获取需要使用到正则的两个对象: 使用的是用正则对象Pattern 和匹配器Matcher. 用法: 范例: Pattern p = Pattern.compile("a*b"); ...
- pdf工具类之根据页码复制(分割)pdf
实现思路:将原pdf中第m页和第n页的内容复制到目标pdf中 代码如下: 1 /** 2 * 复制(分割)pdf 3 * 4 * @param sourceFilePath 源文件地址 5 * @pa ...
- url中文 + MPC 识别
主要内容转载自:http://blog.163.com/zhangjie_0303/blog/static/9908270620148251658993/ 今天在调的时候同事发过来包里包含汉字,我tc ...
- less 剖析
简介 不熟悉less,经常该样式要花费很多时间所以进行系统性的学习 参考链接 https://www.bilibili.com/video/BV1YW411T7vd?p=8 http://lesscs ...