Unity URP管线着色器库攻略part1
骚话王又来分享知识了!今天咱们聊聊Unity URP管线中的着色器库,这可是个相当实用的话题。如果你正在做URP项目,或者准备从Built-in管线转向URP,那这篇文章绝对值得你收藏点赞。
什么是URP着色器库,为什么要了解它
在Unity的URP(Universal Render Pipeline)中,官方为我们提供了一套完整的着色器库系统。这些库文件就像是一个个工具箱,里面装满了各种现成的函数和工具方法,让我们在编写自定义着色器时不用从零开始造轮子。
作为开发者,我们经常会遇到需要获取摄像机位置、处理光照计算、变换坐标系统等常见需求。如果每次都要自己写这些基础功能,那效率实在太低了。而URP的着色器库就是为了解决这个问题而存在的------它把这些常用的功能都封装好了,我们只需要简单地引入就能使用。
着色器库的位置和结构
URP的着色器库文件都集中放在一个特定的目录下:Packages/com.unity.render-pipelines.universal/ShaderLibrary/
。这个路径看起来有点长,但记住它很重要,因为我们后面会经常用到。
这个目录下包含了各种功能不同的.hlsl
文件,每个文件都专门负责特定的功能模块。比如Core.hlsl负责核心功能,Lighting.hlsl处理光照相关的计算,等等。
如何正确引入着色器库
基本引入语法
在URP中引入着色器库的方式非常直接,我们使用#include
指令来完成这个操作。最重要的是,这个指令必须放在HLSLPROGRAM
和ENDHLSL
之间,这样编译器才能正确识别和处理。
hlsl
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
// 你的着色器代码...
ENDHLSL
这里的Core.hlsl
是最基础也是最重要的库文件,它包含了大量的核心函数和工具方法,比如坐标变换、数学运算等等。
实际使用示例
引入库文件后,我们就可以直接使用里面的函数了。比如,想要获取摄像机在世界空间中的位置,我们可以这样写:
hlsl
float3 cameraPosition = GetCameraPositionWS();
这个GetCameraPositionWS()
函数就是从Core.hlsl库中来的。函数名中的"WS"表示World Space(世界空间),这是URP库中的命名约定。
类似地,我们还可以使用其他实用的函数,比如:
GetVertexPositionInputs()
:获取顶点位置的各种变换结果GetMainLight()
:获取主光源信息TransformObjectToHClip()
:将物体空间的坐标转换到裁剪空间
这些函数名虽然看起来有点复杂,但它们的命名都遵循了一定的规律,熟悉后你会发现非常好用。
InputData:着色器中的万能工具包
说到URP着色器库,有一个结构体你绝对不能忽视,那就是InputData
。
InputData是什么
InputData
是URP中定义的一个结构体,它把着色器中最常用的各种数据都打包在一起。你可以把它想象成一个"信息集合器",里面包含了位置、法线、光照、雾效等等各种渲染需要的信息。
hlsl
struct InputData
{
float3 positionWS; // 世界空间中的位置
float4 positionCS; // 裁剪空间中的位置
float3 normalWS; // 世界空间中的法线
half3 viewDirectionWS; // 世界空间中的视角方向
float4 shadowCoord; // 阴影贴图坐标
half fogCoord; // 雾效坐标
half3 vertexLighting; // 顶点光照
half3 bakedGI; // 烘焙的全局光照
float2 normalizedScreenSpaceUV; // 标准化的屏幕空间UV
half4 shadowMask; // 阴影遮罩
half3x3 tangentToWorld; // 切线空间到世界空间的变换矩阵
// ...还有一些调试用的字段
};
每个字段的具体含义
让我们逐个来看看这些字段都是干什么的:
positionWS 和 positionCS 这两个字段分别存储了当前像素在世界空间和裁剪空间中的位置,它们在渲染管线中扮演着不同的角色。
positionWS
(World Space Position)是当前像素在世界坐标系中的三维位置,这个坐标系是整个3D场景的"基准坐标系"。它最常用的场景包括:
- 距离衰减计算:比如点光源的光照强度会随着距离衰减,你需要计算光源位置和像素位置之间的距离
- 环境映射和反射:环境立方体纹理的采样需要从像素位置到摄像机的方向向量
- 雾效计算:很多雾效算法需要知道物体距离摄像机的实际距离
- 程序化纹理:比如基于世界位置的噪声纹理,可以让相同材质的不同物体无缝衔接
positionCS
(Clip Space Position)是经过完整MVP变换后的四维齐次坐标,这是GPU进行深度测试和裁剪的关键数据。它的应用场景包括:
- 深度缓冲计算:GPU使用Z坐标进行深度测试,决定像素是否可见
- 屏幕坐标转换:通过透视除法可以得到标准化设备坐标(NDC)
- 自定义裁剪:在某些特殊效果中,你可能需要基于裁剪空间坐标进行自定义裁剪
normalWS 这是当前像素的世界空间法线向量,在整个光照计算体系中占据核心地位。法线不仅仅是"垂直于表面的向量"这么简单,它在各种光照模型中都有关键作用:
- 兰伯特漫反射:漫反射强度 = max(0, dot(normal, lightDirection)),法线和光线方向的点积决定了表面接收到多少光照
- 镜面反射计算:在Phong和Blinn-Phong模型中,法线用于计算反射方向,进而计算镜面高光
- 菲涅耳效应:通过法线和视角方向的夹角,可以模拟材质在不同观察角度下的反射特性
- 法线贴图混合:当使用多层法线贴图时,需要在世界空间中正确混合法线信息
- 环境光遮蔽:一些SSAO算法会用到世界空间法线来计算遮蔽因子
需要注意的是,这个法线必须是归一化的单位向量,否则光照计算会出现错误。
viewDirectionWS 从当前像素指向摄像机的方向向量,这是实现各种视觉效果的重要数据。它的计算通常是normalize(cameraPosition - pixelPosition)
,在以下场景中发挥重要作用:
- 镜面反射高光:在Phong模型中,需要计算视角方向和反射方向的夹角来确定高光强度
- 菲涅耳效应:fresnel = pow(1 - dot(normal, viewDirection), fresnelPower),这个公式广泛用于模拟玻璃、水面等材质
- 边缘光效果:rim lighting效果通过检测法线和视角方向的夹角来在物体边缘产生光晕
- 各向异性反射:金属拉丝、丝绸等材质需要根据视角方向调整反射特性
- 视差映射:parallax mapping技术需要视角方向来计算纹理坐标的偏移
shadowCoord 用于阴影贴图采样的特殊坐标,这是阴影系统的核心数据结构。URP使用级联阴影贴图(Cascaded Shadow Maps)技术,shadowCoord包含了采样阴影贴图所需的所有信息:
- 坐标变换:从世界空间位置变换到光源的投影空间,这个过程涉及光源的视图矩阵和投影矩阵
- 级联选择:URP根据像素距离摄像机的远近选择合适的阴影级联,近处用高精度贴图,远处用低精度贴图
- 深度比较:通过比较当前像素的深度和阴影贴图中存储的深度来判断是否处于阴影中
- 软阴影采样:PCF(Percentage Closer Filtering)等软阴影技术需要对阴影贴图进行多次采样
- 阴影偏移:为了避免阴影失真,URP会在shadowCoord中应用适当的偏移
fogCoord 雾效计算的关键参数,这个值通常表示当前像素到摄像机的距离或者基于高度的雾效密度。URP支持多种雾效模式:
- 线性雾:雾效强度在起始距离和结束距离之间线性变化,fogCoord直接对应距离值
- 指数雾:雾效密度随距离指数增长,更符合真实世界的雾效特性
- 指数平方雾:比指数雾衰减更快,适合表现浓雾效果
- 高度雾:基于像素的世界空间Y坐标计算雾效,可以模拟山谷中的雾气效果
fogCoord的计算通常在顶点着色器中完成,然后插值传递给片段着色器使用。
vertexLighting 和 bakedGI 这两个字段存储的是预计算的光照信息,是URP性能优化的重要组成部分:
vertexLighting
存储在顶点级别计算的光照结果,这种技术的优势是:
- 性能优化:顶点数量通常远少于像素数量,在顶点级别计算光照可以大大降低计算量
- 移动端友好:对于GPU性能有限的移动设备,顶点光照是一个很好的性能与效果的平衡
- 简单光源处理:对于一些不太重要的附加光源,使用顶点光照可以节省大量性能
- 距离衰减:远距离的物体可以自动降级到顶点光照,实现LOD效果
bakedGI
存储的是烘焙的全局光照信息,包括:
- 光照贴图:静态物体的直接光照和间接光照都可以预先烘焙到纹理中
- 光照探针:对于动态物体,使用空间中布置的光照探针来提供烘焙的环境光信息
- 反射探针:预计算的环境反射信息,让金属等反射材质有更真实的反射效果
- 间接光照:光线弹射产生的间接照明效果,这种计算非常耗时,烘焙是唯一实用的解决方案
normalizedScreenSpaceUV 这是当前像素在屏幕空间中的标准化UV坐标,范围从(0,0)到(1,1),是屏幕空间效果的基础数据。它的坐标系统定义为:
- 坐标原点:左下角为(0,0),这与OpenGL的约定一致
- 坐标范围:右上角为(1,1),整个屏幕被映射到0-1的范围内
- 精度考虑:在高分辨率屏幕上,UV坐标的精度变得非常重要
主要应用场景包括:
- 后处理效果:bloom、DOF、色彩分级等后处理需要根据屏幕位置进行采样
- 屏幕空间反射:SSR技术需要在屏幕空间中追踪反射光线
- UI覆盖效果:比如血迹、裂纹等覆盖在屏幕上的效果
- 视差效果:某些视差滚动效果需要基于屏幕位置计算偏移
- 噪声采样:很多程序化效果需要基于屏幕坐标采样噪声纹理
shadowMask 这是Unity混合光照模式的核心数据,代表了静态阴影的烘焙信息。这个系统的设计思想是将不同类型的阴影分别处理:
混合光照的工作流程:
- 静态阴影烘焙:静态物体投射到静态物体上的阴影被烘焙到shadowMask纹理中
- 动态阴影实时计算:动态物体的阴影仍然使用传统的实时阴影贴图技术
- 混合策略:最终的阴影结果是烘焙阴影和实时阴影的智能混合
shadowMask的数据格式:
- RGBA通道:每个通道可以存储一个光源的阴影信息,最多支持4个光源
- 数值含义:0表示完全在阴影中,1表示完全被照亮,中间值表示部分阴影
- 距离衰减:超过一定距离后,实时阴影会淡出到烘焙阴影,实现平滑过渡
性能优势:
- 减少DrawCall:烘焙阴影不需要额外的渲染通道
- 降低填充率压力:减少了阴影贴图的采样次数
- 提升视觉质量:可以支持更多的阴影投射物体
tangentToWorld 这是一个3x3的变换矩阵,负责将切线空间的向量转换到世界空间。这个矩阵的构成和原理相当复杂:
矩阵构成:
- 第一行(Tangent):切线向量,沿着纹理U方向
- 第二行(Bitangent):副切线向量,沿着纹理V方向
- 第三行(Normal):法线向量,垂直于表面
法线贴图处理:
- 存储空间:法线贴图中的法线是相对于模型表面定义的,这被称为切线空间
- 变换过程:tangentToWorld矩阵将这些相对法线转换为世界空间中的绝对法线
- 细节表现:这样可以在平滑的表面上添加大量细节,而不增加几何复杂度
高级应用:
- 各向异性反射:金属拉丝效果需要知道表面的切线方向来计算反射
- 毛发渲染:毛发着色器需要切线信息来模拟毛发的各向异性高光
- 布料渲染:丝绸等材质需要根据纤维方向调整反射特性
- 程序化细节:可以基于切线空间生成程序化的表面细节
数学原理:
hlsl
float3 worldNormal = normalize(mul(tangentSpaceNormal, tangentToWorld));
这个变换确保了无论模型如何旋转缩放,表面细节都能正确显示。
顶点处理的得力助手:VertexPositionInputs和VertexNormalInputs
除了InputData
这个片段着色器的万能工具包外,URP还为我们提供了两个专门处理顶点数据的结构体。如果说InputData
是片段着色器的管家,那么VertexPositionInputs
和VertexNormalInputs
就是顶点着色器的左膀右臂。
VertexPositionInputs:多空间位置的集合器
hlsl
struct VertexPositionInputs
{
float3 positionWS; // 世界空间位置
float3 positionVS; // 视图空间位置
float4 positionCS; // 齐次裁剪空间位置
float4 positionNDC; // 齐次标准化设备坐标
};
这个结构体的设计理念是"一次计算,多处使用"。在顶点着色器中,我们经常需要将顶点位置转换到不同的坐标空间,而手动进行这些变换不仅容易出错,还会产生重复计算。VertexPositionInputs
把所有常用的空间变换结果都打包在一起,让开发者可以根据需要选择合适的坐标空间。
positionWS(世界空间位置) 这是顶点在世界坐标系中的位置,是大多数光照计算的基础。它的主要用途包括:
- 光照距离计算:点光源和聚光灯的衰减都需要计算光源到顶点的距离,这个计算必须在世界空间中进行
- 环境效果:雾效、风场效果、程序化动画等都需要知道顶点在世界中的绝对位置
- 多光源处理:URP的Forward+渲染需要根据世界位置来确定影响当前顶点的光源列表
- 跨物体效果:比如全局的草地摆动、水波纹扩散等效果需要基于世界坐标进行计算
- LOD计算:根据距离摄像机的远近来调整模型细节层次
世界空间位置的计算公式是:positionWS = mul(unity_ObjectToWorld, positionOS)
,其中positionOS是模型空间中的原始顶点位置。
positionVS(视图空间位置)
这是顶点相对于摄像机的位置,摄像机位于原点,Z轴指向摄像机的前方。视图空间在以下场景中特别有用:
- 深度雾效:线性雾效通常基于视图空间的Z坐标计算,因为这直接对应距离摄像机的远近
- 视锥体裁剪:在视图空间中可以很容易地判断顶点是否在摄像机的视锥体范围内
- Billboard效果:广告牌、粒子等始终面向摄像机的效果需要在视图空间中计算旋转
- 屏幕空间效果预处理:很多屏幕空间效果需要先在视图空间中进行初步计算
- 相机相关动画:比如根据与摄像机的相对位置来调整动画强度
视图空间的特点是摄像机永远位于原点(0,0,0),这让很多相机相关的计算变得简单直观。
positionCS(齐次裁剪空间位置) 这是经过完整MVP(模型-视图-投影)变换后的四维齐次坐标,是GPU硬件光栅化的直接输入。这个坐标的重要性在于:
- 硬件光栅化:GPU的固定功能硬件使用这个坐标进行三角形光栅化
- 深度测试:Z/W值用于深度缓冲测试,决定像素的可见性
- 视锥体裁剪:GPU硬件会自动丢弃超出视锥体范围的图元
- 透视校正:W分量用于透视校正插值,确保纹理和其他属性正确插值
- 自定义裁剪:在某些特殊效果中,你可能需要基于裁剪空间坐标进行自定义裁剪
值得注意的是,这是一个四维齐次坐标,真正的3D坐标需要通过透视除法得到:realPosition = positionCS.xyz / positionCS.w
。
positionNDC(标准化设备坐标) 这是透视除法后的坐标,范围通常在[-1,1](OpenGL)或[0,1](DirectX)之间。它的应用场景包括:
- 屏幕坐标计算:通过简单的缩放和平移就能得到屏幕像素坐标
- 纹理投影:投影纹理效果需要将3D位置映射到2D纹理坐标
- 阴影映射:阴影贴图的采样坐标本质上就是从光源视角看到的NDC坐标
- 后处理集成:很多后处理效果需要知道顶点在NDC空间中的位置
- 自定义投影:比如鱼眼镜头、VR显示等需要特殊投影的场景
VertexNormalInputs:法线系统的完整方案
hlsl
struct VertexNormalInputs
{
real3 tangentWS; // 世界空间切线
real3 bitangentWS; // 世界空间副切线
float3 normalWS; // 世界空间法线
};
这个结构体定义了完整的切线空间基础向量在世界空间中的表示,是实现高质量表面细节的关键。这三个向量共同构成了一个标准正交基,用于将切线空间的法线贴图转换到世界空间。
tangentWS(世界空间切线) 切线向量沿着纹理的U方向,在表面处理中扮演着重要角色:
- 法线贴图变换:作为切线空间到世界空间变换矩阵的第一行
- 各向异性效果:金属拉丝、CD光盘表面等效果需要知道表面的"纹理方向"
- 毛发渲染:毛发着色器需要切线方向来计算各向异性高光,模拟真实毛发的光泽效果
- 布料材质:丝绸、缎子等材质的光泽方向与纤维方向相关
- 程序化细节:可以基于切线方向生成条纹、划痕等程序化表面细节
切线向量的计算通常在建模软件中完成,或者通过Unity的自动计算得到。它必须与UV坐标的U方向保持一致。
bitangentWS(世界空间副切线) 副切线(也叫binormal)垂直于切线和法线,沿着纹理的V方向:
- 完整TBN基础:与切线和法线一起构成完整的切线空间基础
- 双向各向异性:某些材质在两个方向上都有各向异性特性
- 织物渲染:编织物的经纬线效果需要两个方向的纹理信息
- 表面流向:模拟液体流动、风吹效果时的方向参考
- UV空间计算:某些特殊效果需要在UV空间中进行计算
副切线通常通过叉积计算:bitangent = cross(normal, tangent) * tangentSign
,其中tangentSign处理镜像UV的情况。
normalWS(世界空间法线) 顶点级别的世界空间法线,是光照计算的基础:
- 顶点光照:对于使用顶点光照的简单着色器,这是主要的光照计算数据
- 光照插值:在光照计算中,顶点法线会被插值到像素级别
- 轮廓检测:通过检测法线的不连续性可以检测物体轮廓
- 环境光遮蔽:基于顶点的AO计算需要准确的法线信息
- TBN矩阵构建:作为切线空间变换矩阵的第三行
GetVertexPositionInputs函数:坐标变换的统一入口
GetVertexPositionInputs
函数是URP着色器库中负责顶点坐标变换的核心函数,它将模型空间的顶点位置一次性转换为所有常用的坐标空间表示。通过分析其官方实现,我们可以深入理解URP的坐标变换策略和优化思路。
函数实现分析
hlsl
VertexPositionInputs GetVertexPositionInputs(float3 positionOS)
{
VertexPositionInputs input;
input.positionWS = TransformObjectToWorld(positionOS);
input.positionVS = TransformWorldToView(input.positionWS);
input.positionCS = TransformWorldToHClip(input.positionWS);
float4 ndc = input.positionCS * 0.5f;
input.positionNDC.xy = float2(ndc.x, ndc.y * _ProjectionParams.x) + ndc.w;
input.positionNDC.zw = input.positionCS.zw;
return input;
}
第一阶段:模型空间到世界空间变换
hlsl
input.positionWS = TransformObjectToWorld(positionOS);
这一步使用unity_ObjectToWorld
矩阵将顶点从模型局部坐标系转换到世界坐标系。TransformObjectToWorld
函数内部执行的是标准的矩阵乘法:mul(unity_ObjectToWorld, float4(positionOS, 1.0)).xyz
。这个变换包含了模型的位置、旋转和缩放信息,是所有后续变换的基础。
第二阶段:世界空间到视图空间变换
hlsl
input.positionVS = TransformWorldToView(input.positionWS);
此步骤将世界坐标转换为相对于摄像机的视图坐标。TransformWorldToView
使用UNITY_MATRIX_V
(视图矩阵)进行变换。在视图空间中,摄像机位于原点,Z轴指向摄像机前方,这种表示方式便于进行与摄像机相关的计算,如深度雾效和视锥体裁剪。
第三阶段:世界空间到裁剪空间变换
hlsl
input.positionCS = TransformWorldToHClip(input.positionWS);
这里直接从世界空间跳转到裁剪空间,跳过了视图空间这一中间步骤。TransformWorldToHClip
内部使用的是UNITY_MATRIX_VP
(视图投影矩阵),等效于mul(UNITY_MATRIX_VP, float4(positionWS, 1.0))
。这种做法避免了重复的矩阵乘法,体现了URP对性能的优化考虑。
第四阶段:NDC坐标计算
hlsl
float4 ndc = input.positionCS * 0.5f;
input.positionNDC.xy = float2(ndc.x, ndc.y * _ProjectionParams.x) + ndc.w;
input.positionNDC.zw = input.positionCS.zw;
NDC坐标的计算相对复杂,涉及透视除法的预处理。首先将裁剪空间坐标缩放0.5倍,然后处理Y轴翻转问题。_ProjectionParams.x
包含了平台相关的Y轴翻转信息:在某些图形API(如DirectX)中为-1,在其他API(如OpenGL)中为1。最终的XY坐标通过ndc.xy + ndc.w
的形式存储,这种表示方式允许在片段着色器中通过简单的除法得到真正的NDC坐标。
函数采用了"世界空间优先"的变换策略,即先计算世界空间坐标,再从世界空间分别变换到视图空间和裁剪空间。这种设计有以下技术优势:
- 减少累积误差:避免了多次矩阵乘法的误差累积
- 提高精度:世界空间坐标作为中间结果,保证了后续变换的精度
- 便于调试:世界空间坐标具有直观的物理意义,便于开发者理解和调试
函数将所有变换结果存储在连续的内存结构中,这种设计有利于GPU的向量化操作和缓存效率。现代GPU架构中,这种数据布局可以显著减少内存带宽的消耗。
NDC坐标计算中对_ProjectionParams.x
的使用体现了URP对跨平台兼容性的考虑。不同图形API在坐标系统上存在差异,URP通过统一的参数化处理这些差异,确保着色器在所有支持的平台上表现一致。
顶点着色器阶段 该函数主要在顶点着色器的入口处调用,为后续的光照计算、纹理采样和特效处理提供必要的坐标信息。典型的调用时机包括:
- 标准光照着色器:需要世界空间坐标进行光照计算
- 特效着色器:需要多种坐标空间进行复杂的视觉效果
- 后处理着色器:需要屏幕空间坐标进行后处理采样
性能敏感场景 在大量顶点的场景中,该函数的执行效率直接影响渲染性能。URP的实现通过减少冗余计算和优化内存访问模式,确保在高顶点数量的情况下仍能保持良好的性能表现。
多平台部署 在需要支持多种图形API的项目中,该函数提供了统一的坐标变换接口,开发者无需关心底层平台的差异,专注于着色器逻辑的实现。
除了基本的位置变换外,URP还提供了针对不同需求的函数变体:
hlsl
// 仅处理法线的变换
VertexNormalInputs GetVertexNormalInputs(float3 normalOS);
VertexNormalInputs GetVertexNormalInputs(float3 normalOS, float4 tangentOS);
GetVertexNormalInputs函数:TBN空间构建的核心机制
GetVertexNormalInputs
函数专门负责构建顶点的TBN(Tangent-Bitangent-Normal)坐标系统,是URP中处理法线贴图和表面细节的关键组件。该函数提供了两个重载版本,分别适用于不同的输入数据配置和渲染需求。
函数重载版本分析
简化版本:仅法线输入
hlsl
VertexNormalInputs GetVertexNormalInputs(float3 normalOS)
{
VertexNormalInputs tbn;
tbn.tangentWS = real3(1.0, 0.0, 0.0);
tbn.bitangentWS = real3(0.0, 1.0, 0.0);
tbn.normalWS = TransformObjectToWorldNormal(normalOS);
return tbn;
}
这个版本适用于不需要法线贴图或切线信息的简单着色器。函数的实现策略是提供默认的切线空间基向量:
- 默认切线:设置为世界空间的X轴方向(1,0,0),提供一个标准的参考方向
- 默认副切线:设置为世界空间的Y轴方向(0,1,0),与切线垂直
- 变换法线 :使用
TransformObjectToWorldNormal
将模型空间法线转换到世界空间
这种默认值设置策略确保了即使在没有切线数据的情况下,TBN矩阵仍然是有效的,避免了未定义行为的发生。
完整版本:法线与切线输入
hlsl
VertexNormalInputs GetVertexNormalInputs(float3 normalOS, float4 tangentOS)
{
VertexNormalInputs tbn;
// mikkts space compliant. only normalize when extracting normal at frag.
real sign = real(tangentOS.w) * GetOddNegativeScale();
tbn.normalWS = TransformObjectToWorldNormal(normalOS);
tbn.tangentWS = real3(TransformObjectToWorldDir(tangentOS.xyz));
tbn.bitangentWS = real3(cross(tbn.normalWS, float3(tbn.tangentWS))) * sign;
return tbn;
}
完整版本处理具有完整切线信息的顶点数据,其实现包含多个关键的技术细节:
切线符号处理
hlsl
real sign = real(tangentOS.w) * GetOddNegativeScale();
tangentOS.w
存储了切线的符号信息,用于处理镜像UV映射的情况。当模型的UV映射被水平翻转时,需要调整副切线的方向以保持TBN空间的手性(chirality)一致性。GetOddNegativeScale()
函数检测模型变换矩阵是否包含奇数个负缩放,确保在非均匀缩放情况下TBN空间的正确性。
法线变换
hlsl
tbn.normalWS = TransformObjectToWorldNormal(normalOS);
法线的变换使用专门的TransformObjectToWorldNormal
函数,而不是标准的方向变换。这是因为法线在非均匀缩放下的变换需要使用变换矩阵的逆转置,以保持法线与表面的垂直关系。
切线变换
hlsl
tbn.tangentWS = real3(TransformObjectToWorldDir(tangentOS.xyz));
切线作为表面的方向向量,使用标准的方向变换函数。TransformObjectToWorldDir
内部使用变换矩阵的旋转部分,忽略平移信息。
副切线计算
hlsl
tbn.bitangentWS = real3(cross(tbn.normalWS, float3(tbn.tangentWS))) * sign;
副切线通过法线和切线的叉积计算得出,然后乘以符号因子。这种计算方式确保了TBN空间的正交性和正确的手性。
函数实现中的注释"mikkts space compliant"表明该实现遵循MikkTSpace标准。MikkTSpace是业界广泛采用的切线空间生成算法,被Blender、Substance等主流建模软件支持。URP采用这一标准确保了与外部工具链的兼容性。
注释"only normalize when extracting normal at frag"指出了URP的优化策略:在顶点着色器阶段不进行向量归一化,而是将这一步骤延迟到片段着色器。这种策略的优势包括:
- 减少顶点计算负担:归一化操作的计算成本被分摊到像素级别
- 提高插值精度:未归一化的向量在插值过程中保持更好的数值稳定性
- 符合硬件特性:现代GPU在片段着色器中的向量操作效率更高
函数使用real3
类型而不是float3
,这是URP的精度管理策略。在移动平台上,real
通常映射到half
(16位浮点),而在桌面平台上映射到float
(32位浮点)。这种自适应精度策略在保证渲染质量的同时优化了性能。
函数通过GetOddNegativeScale()
检测变换矩阵的行列式符号,处理包含反射变换的情况。这种检测机制确保了即使在复杂的模型变换下,TBN空间仍能保持正确的方向性。
简化版本的应用范围
- 基础光照着色器:仅需要法线信息进行漫反射和镜面反射计算
- 程序化材质:通过算法生成表面细节,不依赖法线贴图
- 性能优化场景:在资源受限的平台上简化TBN计算
完整版本的应用范围
- 高质量材质渲染:需要法线贴图、视差贴图等高级表面效果
- 物理基础渲染:PBR材质需要准确的TBN空间进行各向异性计算
- 特殊效果着色器:毛发、织物等需要精确表面方向信息的材质
GetScaledScreenParams函数:屏幕参数的统一访问接口
GetScaledScreenParams
函数是URP中访问屏幕尺寸信息的标准接口,尽管其实现极其简洁,但它封装的_ScaledScreenParams
变量在屏幕空间计算中发挥着基础性作用。
函数实现与设计理念
hlsl
float4 GetScaledScreenParams()
{
return _ScaledScreenParams;
}
函数的实现看似平凡,仅仅是对全局变量_ScaledScreenParams
的直接返回,但这种封装体现了URP的接口设计哲学。通过函数访问全局变量而非直接使用,为后续的功能扩展和优化预留了空间,同时提供了统一的访问模式。
_ScaledScreenParams的数据结构
_ScaledScreenParams
是一个float4
类型的全局变量,其各分量承载着不同的屏幕相关信息:
x分量:屏幕宽度(像素) 存储当前渲染目标的宽度值,以像素为单位。这个值在屏幕空间到标准化坐标的转换中起到关键作用。
y分量:屏幕高度(像素) 存储当前渲染目标的高度值,同样以像素为单位。与宽度值配合,提供完整的屏幕尺寸信息。
z和w分量:扩展参数 虽然在当前URP版本中的具体用途不明显,但这两个分量为未来的功能扩展保留了空间,可能用于存储屏幕比例、缩放因子或其他渲染相关的派生参数。
屏幕空间坐标变换的核心角色
_ScaledScreenParams
在屏幕空间坐标变换中担任核心角色,特别是在以下关键函数中:
标准化屏幕空间UV计算
hlsl
float2 GetNormalizedScreenSpaceUV(float2 positionCS)
{
float2 normalizedScreenSpaceUV = positionCS.xy * rcp(GetScaledScreenParams().xy);
TransformNormalizedScreenUV(normalizedScreenSpaceUV);
return normalizedScreenSpaceUV;
}
这个函数展现了_ScaledScreenParams
最重要的应用场景。rcp
函数计算倒数,因此rcp(GetScaledScreenParams().xy)
得到(1/width, 1/height)
的向量。将裁剪空间坐标与此向量相乘,实现了从像素坐标到[0,1]范围标准化坐标的转换。
屏幕UV变换处理
hlsl
void TransformScreenUV(inout float2 uv)
{
#if UNITY_UV_STARTS_AT_TOP
TransformScreenUV(uv, GetScaledScreenParams().y);
#endif
}
在跨平台兼容性处理中,函数使用屏幕高度值处理不同图形API之间的UV坐标系差异。某些平台(如DirectX)的UV原点位于左上角,而其他平台(如OpenGL)位于左下角,屏幕高度信息是进行这种变换的必要参数。
IsPerspectiveProjection函数:投影模式的智能识别
IsPerspectiveProjection
函数是URP中用于区分摄像机投影模式的核心工具,它通过简洁的逻辑判断当前视图是采用透视投影还是正交投影。这种区分对于正确计算视角方向、深度处理和各种空间变换至关重要。
函数实现与判断机制
hlsl
// Returns 'true' if the current view performs a perspective projection.
bool IsPerspectiveProjection()
{
return (unity_OrthoParams.w == 0);
}
函数的实现基于对unity_OrthoParams.w
分量的检查,这个全局参数由Unity引擎在渲染过程中自动设置。unity_OrthoParams
的完整定义揭示了其存储的投影相关信息:
hlsl
// x = orthographic camera's width
// y = orthographic camera's height
// z = unused
// w = 1.0 if camera is ortho, 0.0 if perspective
float4 unity_OrthoParams;
投影模式的数学本质
透视投影模拟人眼和摄像机的成像原理,具有以下数学特征:
- 距离衰减:远处物体显得更小,符合视觉感知规律
- 视锥体几何:投影空间是一个截锥体(frustum),而非长方体
- 非线性深度分布:深度值在近平面附近分布密集,远平面附近稀疏
- 透视除法:需要进行齐次坐标的透视除法(除以w分量)
正交投影保持平行线的平行性,常用于工程制图和特定艺术风格:
- 无距离衰减:远近物体保持相同尺寸比例
- 长方体投影空间:投影区域是规则的长方体
- 线性深度分布:深度值在整个深度范围内均匀分布
- 无透视除法:w分量保持为1,无需透视校正
透视投影下的视角方向
hlsl
float3 GetWorldSpaceViewDir(float3 positionWS)
{
if (IsPerspectiveProjection())
{
// Perspective
return GetCurrentViewPosition() - positionWS;
}
// ...
}
在透视投影中,视角方向是从当前世界位置指向摄像机位置的向量。每个像素点都有独特的视角方向,这种差异正是透视效果的数学基础。视角方向的计算公式体现了透视投影的点光源特性------所有光线都汇聚到摄像机位置这一点。
正交投影下的视角方向
hlsl
float3 GetWorldSpaceViewDir(float3 positionWS)
{
if (IsPerspectiveProjection())
{
// ...
}
else
{
// Orthographic
return -GetViewForwardDir();
}
}
正交投影中,所有像素点共享相同的视角方向------摄像机的前进方向。这反映了正交投影的平行光源特性,所有投影光线都是平行的,不存在汇聚点。
投影模式的区分在深度相关计算中同样重要,特别是在粒子系统和深度采样中:
hlsl
// 来自Particles.hlsl的示例
float sceneZ = (unity_OrthoParams.w == 0) ?
LinearEyeDepth(rawDepth, _ZBufferParams) :
LinearDepthToEyeDepth(rawDepth);
这种条件分支处理体现了两种投影模式在深度处理上的本质差异:
- 透视投影 :需要进行非线性深度转换,使用
LinearEyeDepth
函数 - 正交投影 :使用线性深度转换,直接采用
LinearDepthToEyeDepth
函数
不同投影模式对光照计算产生深远影响,特别是在以下方面:
镜面反射计算 透视投影中,每个像素的视角方向不同,导致镜面高光的位置和强度呈现自然的透视变化。正交投影中,统一的视角方向会产生平行的反射效果,适合特定的艺术风格或技术可视化。
环境映射采样 环境立方体纹理的采样方向计算依赖准确的视角方向。透视投影提供真实的环境反射效果,而正交投影则产生技术化的平行反射。
边缘光效果 基于视角的边缘光效果(Rim Lighting)在两种投影模式下表现截然不同。透视投影中的边缘光随观察角度自然变化,正交投影则产生均匀的边缘效果。
虽然函数内部只是简单的比较操作,但其返回值常被用于条件分支,这在GPU着色器中具有重要的性能含义:
现代GPU架构对于统一的分支条件具有良好的处理能力。由于同一渲染批次中的所有像素通常使用相同的投影模式,分支预测的成功率很高,性能损失较小。
GetCameraPositionWS - 世界空间摄像机位置获取
在着色器开发中,获取摄像机的世界空间位置是一个基础但至关重要的操作。GetCameraPositionWS()
函数作为URP管线中的核心工具函数,为开发者提供了统一且可靠的摄像机位置访问接口。
hlsl
float3 GetCameraPositionWS()
{
// Currently we do not support Camera Relative Rendering so
// we simply return the _WorldSpaceCameraPos until then
return _WorldSpaceCameraPos;
// We will replace the code above with this one once
// we start supporting Camera Relative Rendering
//#if (SHADEROPTIONS_CAMERA_RELATIVE_RENDERING != 0)
// return float3(0, 0, 0);
//#else
// return _WorldSpaceCameraPos;
//#endif
}
_WorldSpaceCameraPos
变量在不同的渲染模式下有着不同的定义方式:
普通渲染模式
hlsl
float3 _WorldSpaceCameraPos;
立体渲染模式
hlsl
#define _WorldSpaceCameraPos unity_StereoWorldSpaceCameraPos[unity_StereoEyeIndex]
在立体渲染中,左右眼需要使用不同的摄像机位置来产生正确的立体视差效果,通过数组索引unity_StereoEyeIndex
可以自动选择对应眼部的摄像机位置。
代码中被注释的部分揭示了Unity对未来摄像机相对渲染(Camera Relative Rendering)技术的前瞻性设计。
在传统的世界坐标系统中,当场景规模达到数千公里级别时,浮点数精度限制会导致明显的渲染抖动和精度丢失问题。特别是在天体渲染、地理信息系统或大型开放世界游戏中,这种问题尤为突出。
摄像机相对渲染将摄像机位置作为局部坐标系的原点,所有计算都基于相对位置进行。这样可以显著提高数值精度,消除因浮点数精度限制带来的渲染问题。
GetCameraPositionWS()
函数在各种光照计算中扮演着不可替代的角色:
视角方向计算
hlsl
float3 viewDirection = normalize(GetCameraPositionWS() - positionWS);
Fresnel效应计算 Fresnel效应需要用到表面法线与视角方向的夹角,摄像机位置是计算视角方向的必要条件。
环境反射采样 在基于图像的光照(IBL)中,需要根据视角方向对环境贴图进行采样,摄像机位置直接影响反射效果的准确性。
在实际的着色器开发中,GetCameraPositionWS()
函数通常与其他函数组合使用:
与GetWorldSpaceNormalizeViewDir的配合
hlsl
float3 viewDir = GetWorldSpaceNormalizeViewDir(positionWS);
// 内部实际调用了GetCameraPositionWS()进行计算
距离衰减计算
hlsl
float distanceToCamera = length(positionWS - GetCameraPositionWS());
float attenuation = 1.0 / (1.0 + distanceToCamera * distanceToCamera);
LOD系统集成 通过计算像素到摄像机的距离,可以实现基于距离的细节层级(LOD)选择,优化渲染性能。
未完待续...