自由学习记录(55)

Unity Shader 中一个用于支持 GPU Instancing 的宏定义
它是给每个顶点“打上实例编号”的工具,用于在 一个 draw call 中渲染多个对象实例 时区分每个象。

🎯 给每个顶点输入结构中,添加一个“实例 ID”,以便你在 Shader 中识别它属于哪个实例

它等价于:uint instanceID : SV_InstanceID;

o.vertex = UnityObjectToClipPos(v.vertex);
📌 将模型空间的顶点坐标转换为裁剪空间,供 GPU 渲染使用。

fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
📌 将模型法线 v.normal 转换成 世界空间法线向量 worldNormal,以便后续和光照方向对比。

fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
📌 获取当前方向光的方向(假设是 Directional Light),已在世界空间中 → 不需要额外转换。

✅ Unity 封装好的“黑箱方法”:

使用作用说明
UnityObjectToClipPos(v.vertex)顶点空间 → 裁剪空间顶点位置变换(包好了 MVP)
UnityObjectToWorldNormal(v.normal)模型法线 → 世界法线自动处理了缩放/变换后的法线方向
_WorldSpaceLightPos0光源方向(世界空间)Unity 自动传入主光源信息
saturate(x)等价于 clamp(x, 0, 1)内建 HLSL 函数
_LightColor0当前主光源的颜色Unity 自动提供(来自光源)

✅ 小贴士:怎么判断是不是 Unity 封装的?

只要满足下面任一条件,一般就是:

  1. 变量名以 _World, _Light, _Camera 开头的系统变量

  2. 函数名是 UnityObjectTo... 开头

  3. 不在你的 Properties{} 中声明,却能用

  4. 不需要你手动传值,就自动生效(比如光源位置、颜色)

Unity 内置 Shader 变量对照表(常用封装变量和它们的用途),可以帮助你快速识别哪些变量是 Unity 自动提供的:

🔑 变量名类型说明用途
_WorldSpaceLightPos0float4主方向光的位置或方向(世界空间)用于计算 dot(N, L)
_LightColor0float4主光源颜色(RGB)用于漫反射和高光乘色
_WorldSpaceCameraPosfloat3摄像机位置(世界空间)用于视线方向计算
_Timefloat4x = t, y = t*2, z = t*3, w = t*4时间动画控制、UV 扫描
_SinTime, _CosTimefloat4正余弦时间序列做周期运动
_Object2World / unity_ObjectToWorldfloat4x4模型到世界变换矩阵顶点变换
_World2Object / unity_WorldToObjectfloat4x4世界到模型矩阵法线变换
UNITY_MATRIX_MVPfloat4x4模型 → 裁剪空间位置投影
UNITY_MATRIX_IT_MVfloat4x4模型视图逆转置矩阵法线变换(旧方式)
unity_MatrixVPfloat4x4相机视图投影矩阵屏幕空间转换
_ProjectionParamsfloat4x: 1/near, y: far/near, z: 正交?, w: 1深度纹理、Z 空间判断
_ScreenParamsfloat4x: 宽度, y: 高度, z: 1/宽, w: 1/高屏幕坐标计算
unity_CameraProjectionfloat4x4投影矩阵从世界 → 屏幕
unity_CameraInvProjectionfloat4x4投影矩阵逆屏幕 → 世界
unity_StereoEyeIndexintVR 当前渲染眼双眼分离绘制(VR 支持)

为什么叫 saturate?它和“饱和”有什么关系?

📚 英文中的 "saturate" 本意就是:

“使达到极限”、“使充满”
在颜色 / 图像 / 物理中指:达到最大值或最小值边界,不再继续变化

  • 颜色值的有效范围通常是 [0, 1]

  • 如果一个亮度/颜色计算结果超出这个范围,例如 1.3,会导致“过曝”或“出界”

  • 为了让结果不溢出,我们限制它在 0~1 之间

  • 这个“限制到上下限”的过程,就叫:

saturation(饱和裁切)

为什么不直接用 clamp(x, 0, 1)

saturate(x)硬件优化后的专用指令

  • 它比 clamp() 更快

  • 编译时能更好映射到 GPU 的 ALU 指令

  • 所以在光照/颜色计算中极其常用

✅ 你定义或传入的值:

使用作用来源
_Diffuse表面材质颜色是你通过 Properties 定义或材质球中设置
v.normal / v.vertex顶点法线 / 顶点坐标Unity 自动传入每个 Mesh 顶点
o.color / i.color插值中间值你自己在 v2f 中定义并赋值传递

ambient = UNITY_LIGHTMODEL_AMBIENT.xyz

  • ✅ 是 Unity 提供的 全局环境光(你在 Lighting 面板设置的环境颜色)

  • 📌 它代表了四面八方、非方向性的背景亮度

  • ❌ 不依赖光源方向、不依赖法线、不产生阴影

✅ 为什么是 (+)而不是乘或者混合?

因为它们代表不同物理来源的 “独立能量通道”

成分物理含义数学操作原因
ambient全局环境亮度(间接光)加法无方向、常量补光
diffuse主光源照亮表面产生的亮度加法方向光直接作用于表面
specular镜面高光加法视角相关的高能聚光反射

半 Lambert 是什么?

“半 Lambert”这个说法通常指 在 Lambert 模型的基础上做出调整,使得背对光源(即 N⋅L<0N \cdot L < 0N⋅L<0)的区域依然有一定亮度,而不是直接为 0。

为何使用“半 Lambert”?

原始 Lambert半 Lambert 改进
光照在背面为 0,暗部偏死黑背面也有一定亮度,更柔和自然
符合物理真实更偏艺术需求、美术可控性
可能导致暗部太突兀暗部可以有一定“补光”或“柔光”效果

尤其在卡通渲染(Toon Shading)或 stylized 渲染中,“半 Lambert”常被用于增强轮廓、弱化物理限制。

Phong 高光模型(Phong Specular Model)

为何叫 “Phong”,不是 “Blinn-Phong”?

  • Phong 高光模型使用的是:
    reflect() 得到反射方向,和视角方向做点乘 → dot(R, V)

  • Blinn-Phong 模型使用的是:
    半角向量 H = normalize(L + V) 和法线 N 的点乘 → dot(N, H)

所以你图中写的是 Phong 不是 Blinn-Phong

Blinn-Phong 不是只有 specular,它是一整套经典光照模型,包括 ambient、diffuse、specular 三个部分,适用于标准光照。

如果你想进一步做得更真实(如 PBR),那就会引入:

  • 环境贴图(Ambient Cubemap / IBL)

  • 金属度 / 粗糙度

  • BRDF 函数替代 Blinn-Phong

但在非PBR、传统光照中,Blinn-Phong 处理 ambient 是默认流程中的一环。

fixed3 reflectDir = normalize(reflect(-worldLight, worldNormal));


fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - UnityObjectToWorldDir(v.vertex));


fixed3 specular =

_LightColor0.rgb * _Specular.rgb * pow(max(0, dot(reflectDir, viewDir)), _Gloss);


在 Unity 的 Shader 中,**只有通过 TEXCOORD(或其他语义标记)传出的数据,才会在顶点和片元之间被 GPU 插值,并传给 Fragment Shader。
如果你不在结构里明确写出带语义的变量,那些值就不会自动传过去。

  • 在底层 GPU 中,插值变量必须占据特定的寄存器或插值槽(interpolator slots)

  • TEXCOORD0, TEXCOORD1, ..., COLOR, NORMAL 等都是语义标记(Semantics),告诉 GPU 你希望这段值参与插值并传递到下一个阶段

  • Unity 中虽然语义是固定名字,但在最终 HLSL 会映射到 hardware slot。

UnityWorldSpaceLightDir(float4 worldPos)

接收一个世界空间中的顶点位置 worldPos,返回从该点指向光源的方向向量。​需要注意的是,该函数返回的方向向量未归一化,因此在使用时通常需要对结果进行归一化处理。

worldPos 是将模型空间中的顶点位置转换到世界空间,lightDir 是从该点指向光源的方向向量。

UnityWorldSpaceViewDir(float3 worldPos)

接收一个世界空间中的位置 worldPos,返回从该点指向摄像机的方向向量。​需要注意的是,该函数返回的方向向量未归一化,因此在使用时通常需要对结果进行归一化处理。

To access different vertex data, you need to declare the vertex structure yourself, or add input parameters to the vertex shader. Vertex data is identified by Cg/HLSL semantics, and must be from the following list:

半角计算方式,省去了

直接使用(light入射向量(向外)+Viewdir)出来的向量与法线点乘

模拟出射向量点乘viewDir

viewDir是不会自动normalize向量的,需要自己来,但lightDir一般都是单位向量,,在顶点上

法线贴图是改变normal的值

Height Map

  • Grayscale image:

    • White = high, black = low

  • Used for:

    • Generating normal maps dynamically

    • Parallax mapping

    • Displacement mapping

  • Doesn't store direction, only scalar height.

Model-Space Normal Map

  • RGB channels = XYZ directions in model space

  • Bright, colorful look with sharp edges between faces

  • Used in:

    • Static objects

    • Precise lighting (baked normals)

  • Not suitable for animated or deforming models (because normals don't follow skinning)

Tangent-Space Normal Map

  • Most common normal map type

  • RGB encodes the normal direction relative to the surface’s tangent/bitangent/normal basis

  • Appears purple-ish because the “base normal” (0, 0, 1) = (0.5, 0.5, 1) in color

  • ✅ Suitable for:

    • Skinned meshes

    • Dynamic characters

    • Most modern game engines (Unity, Unreal)

the main difference is the coordinate space.

But this difference leads to very different use cases, flexibility, and performance implications.

Model-Space Normals

  • Normals are stored in absolute coordinates (world-independent).

  • RGB in the normal map directly encodes directions in model space:

    • X → right

    • Y → up

    • Z → forward

  • The shader interprets these normals as fixed directions.

  • 🔒 Tied to the model’s geometry and orientation.

Tangent-Space Normals

  • Normals are stored relative to the surface (i.e., local to each triangle).

  • They rely on a local Tangent-Bitangent-Normal (TBN) basis.

  • This lets the normal direction “follow” deformation, animation, or UV operations.

  • 🔁 Reusable across many models with similar UV layout.

In Tangent Space:

We define:

  • Tangent (T) → along the U axis of the texture

  • Bitangent (B) → along the V axis of the texture

  • Normal (N) → points outward from the surface

❗ Question:

Is TBN a right-handed or left-handed basis?

That depends on:

  • How your engine defines bitangent (Unity, OpenGL, etc.)

  • Whether you compute B = cross(N, T) or use the handedness sign (w component of tangent)

tangent.w 是什么?

在图形编程中,尤其是在使用法线贴图进行光照计算时,切线空间(Tangent Space)的构建至关重要。切线空间由三个正交的向量组成:

  • Tangent(切线):​通常与纹理的 U 方向对齐。

  • Bitangent(副切线或称为 Binormal):​通常与纹理的 V 方向对齐。

  • Normal(法线):​垂直于表面。​

为了确保这三个向量形成一个一致的右手坐标系,通常会将副切线计算为:

其中,sign 是一个标志位,用于指示当前三角形的切线空间是右手系还是左手系。这个标志位通常存储在切线向量的第四个分量中,即 tangent.w。​

  • 如果 tangent.w = +1,表示当前三角形的切线空间是右手系。

  • 如果 tangent.w = -1,表示当前三角形的切线空间是左手系,需要在计算副切线时进行方向修正。

  • 叉乘的计算公式在不同引擎中是一致的,但由于引擎可能采用不同的坐标系(右手或左手),导致相同的叉乘操作在不同引擎中可能产生方向相反的结果。

  • tangent.w 是一个标志位,用于指示当前三角形的切线空间是右手系还是左手系。在构建 TBN 矩阵时,根据 tangent.w 的值调整副切线的方向,确保切线空间的一致性。​

#define TANGENT_SPACE_ROTATION \
    float3 binormal = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w; \
    float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);

构建的是一个三维正交矩阵 rotation,它由:

  • X轴:切线(Tangent)

  • Y轴:副切线(Binormal/Bitangent)

  • Z轴:法线(Normal)

这就是所谓的 TBN 矩阵,它定义了一个“局部表面坐标系”。

这个矩阵 rotation 可以实现:

应用含义
TBN * vec_in_tangent_space把切线空间的向量转换为模型空间(或世界空间)
transpose(TBN) * vec_in_model_space把模型空间中的向量转换为切线空间

Unity 自动处理 = 自动生成 Mesh.tangents 数据

  • Unity 会根据:

    • 顶点法线 normals

    • UV 坐标 uv

  • 为每个顶点计算:

    • tangent.xyz(表示切线方向)

    • tangent.w(表示副切线方向修正,±1)

👉 最终形成的切线数据是:

Vector4 tangent = new Vector4(x, y, z, w);

模型导入时(如 FBX、OBJ)

在你导入模型(.fbx/.obj)时,Unity 会自动:

  • 检查是否带有 Tangent 属性(模型软件导出)

  • 如果没有带,Unity 就自己 根据 UV 和法线生成切线 + w

模型 → Inspector → Model → Tangents → Calculate / Import

如果你用 C# 自己创建网格(Mesh),Unity 不会自动生成切线!

这时你必须自己调用:mesh.RecalculateTangents();

⚠️ 这会在 CPU 上重新计算切线(包括 w 分量),计算逻辑与导入时相同。

在 Unity 里用 cross(normal, tangent)cross(tangent, normal)顺序是有影响的,因为它决定了副切线(bitangent)的方向
但 ✔️ 如果你正确使用 tangent.w 去修正结果方向,最终确实可以消除这个“顺序影响”,得到正确的右手坐标系。

tangent.w 就是来解决这个“方向模糊性”的

binormal = cross(normal, tangent.xyz) * tangent.w;

“不管你叉乘结果是哪个方向,我用 w = ±1 来修正它,让它始终变成我想要的方向。”

  • cross(normal, tangent) + 正确使用 w ✅ ✅

  • cross(tangent, normal) + 正确使用 -w ✅ ✅

最终都可以得到一致的 binormal,构建正确的右手系 TBN。

你可以这样判断 TBN 是否为右手系(debug 时):

float3x3 TBN = float3x3(tangent, binormal, normal);
float determinant = determinant(TBN); // 如果 ≈1 就是右手系,≈-1 就是左手系

变量来源作用
v.texcoord顶点数据中传入的 TEXCOORD0模型原始 UV 坐标
_MainTex_STUnity 自动为 _MainTex 生成的缩放+偏移(4分量)控制颜色贴图的 tiling & offset
_BumpMap_STUnity 自动为 _BumpMap 生成的 ST 参数控制法线贴图的 tiling & offset

把视角和光照转换到tangent空间

当你使用 法线贴图(normal map) 时,确实必须将:

🚩 光照方向(lightDir)
🚩 视角方向(viewDir)

从世界空间(或模型空间)转换到切线空间(Tangent Space),才能与法线贴图中的“切线空间法线”进行一致的 dot 运算(例如 Lambert、Phong、Blinn-Phong 等光照模型)

法线贴图中的法线是贴图空间(Tangent Space)下的值:

  • 红色通道(R)= x 轴 → 沿 Tangent 方向

  • 绿色通道(G)= y 轴 → 沿 Bitangent 方向

  • 蓝色通道(B)= z 轴 → 沿 Normal 方向

但你的光照方向 L、视角方向 V 通常是在 世界空间模型空间

所以必须

L_tangent = mul(L_world, TBN);
V_tangent = mul(V_world, TBN);

切线空间向量含义映射结果(世界空间)
(1, 0, 0)纯粹沿切线方向的向量T
(0, 1, 0)纯粹沿副切线方向(bitangent)B = cross(N, T) * w
(0, 0, 1)纯粹沿法线方向N

这张图正在从世界空间推导切线空间变换矩阵(TBN 的逆矩阵),通过:

  • 把世界空间下的 Tangent / Bitangent / Normal 三个向量

  • 乘一个变换矩阵(你标的“变化矩阵”)

  • 得到标准基 (1,0,0) / (0,1,0) / (0,0,1)
    💡 正在寻找:“哪个矩阵能把世界方向投影到切线空间基向量上”

 精炼一下这张图表达的数学含义:

你的“变化矩阵”是什么?

它就是你想要求解的:

世界向量 → tangent space 的变换矩阵
在 Unity 中我们通常写成:

float3x3 TBN = float3x3(T, B, N); // 这个是 tangent → world
float3x3 TBN_inv = transpose(TBN); // world → tangent

前提是世界bitangent已知,但世界bitangent是需要通过世界normal和世界tangent算出的

用于构建 TBN 的世界向量

在 Shader 或图形引擎中,我们构建 TBN 通常只依赖:

  • 顶点的 world normal

  • 顶点的 world tangent(+ .w

float3 bitangent = cross(normal, tangent) * tangentW;
✅ 这个 bitangent 是可以实时由 N 和 T 推出的,不需要模型提供。

所以只依靠世界normal和世界tangent这两个输入的值,就可以算出bitangent的方向,

通过了叉乘,,但这个叉乘的方向是左手还是右手坐标系的,,这个取决于世界tangent.w,,

而这个世界tangent.w也是属于输入的appdata v中的v.tangent的一部分,是在一开始就已知的

背后为什么需要这个 w

因为:

  • 在模型构建或 UV 展开时,有些面是镜像展开或使用了负缩放

  • 这会导致模型某些面上的 T、N 所构成的平面,在数学上可能构成 左手系

  • 为了确保 Shader 中 TBN 始终是右手系,Unity 在导入时会比较真实的 bitangent 和 cross(N, T) 得到的方向是否一致

  • 如果不一致,就设置 tangent.w = -1,否则是 +1

所以最终你只需要乘一下,就自动统一方向正确性

“只要在每个顶点处先通过标准向量(基底)构造出一个局部的变化矩阵,后续所有需要从世界空间变换到切线空间的向量,就都可以直接乘这个矩阵,不需要再次重建。”

✔️ “我们只需通过世界空间下的 tangent 和 normal 构造出每个顶点的转换矩阵(TBN⁻¹),就可以将任意世界空间方向向量变换到 tangent 空间进行统一计算。”

“a 左乘 b” 的意思是:
a 在左边,b 在右边,写作 a × bmul(a, b)

说法实际含义写法
向量左乘矩阵向量在左,矩阵在右v × M = mul(v, M)
矩阵右乘向量(等价)同上,若视向量是行向量Mᵀ × vᵀ
“a 左乘 b”a × b,a 在左mul(a, b)
“a 右乘 b”b × a,a 在右mul(b, a)

float3 worldNormal  = UnityObjectToWorldNormal(v.normal);
float3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
float3 worldBitangent = cross(worldNormal, worldTangent) * v.tangent.w;
float3x3 TBN = float3x3(worldTangent, worldBitangent, worldNormal);

float3x3 TBN = float3x3(T, B, N);                // TBN: tangent → world
float3x3 TBN_inv = transpose(TBN);               // 世界 → 切线

float3 lightDir_world = normalize(...);          // 世界空间方向
float3 lightDir_tangent = mul(lightDir_world, TBN_inv); // ← 你说的“左乘 TBN 矩阵”

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值