前置计算:通过ddx、ddy计算 T、B

数学推导

1. 曲面参数化定义

设三维几何表面由纹理坐标 $(u, v)$ 参数化,表面上的任意点 $P$ 可表示为:

$$ P(u, v) = \begin{bmatrix} x(u,v) \\ y(u,v) \\ z(u,v) \end{bmatrix} $$

其中,$P \in \mathbb{R}^3$ 为世界空间坐标,$(u, v) \in \mathbb{R}^2$ 为纹理坐标。我们的目标是恢复该点处的切线(Tangent)$T$ 与副切线(Bitangent)$B$ ,定义为:

$$ T = \frac{\partial P}{\partial u}, \quad B = \frac{\partial P}{\partial v} $$

2. 屏幕空间导数与链式法则

在光栅化过程中,GPU 可计算几何属性相对于屏幕空间坐标 $(x_s, y_s)$ 的偏导数。记屏幕空间导数为 $\frac{\partial}{\partial x_s}$ 和 $\frac{\partial}{\partial y_s}$ 。根据链式法则,位置 $P$ 对屏幕坐标的导数可展开为:

$$ \frac{\partial P}{\partial x_s} = \frac{\partial P}{\partial u} \frac{\partial u}{\partial x_s} + \frac{\partial P}{\partial v} \frac{\partial v}{\partial x_s} $$

$$ \frac{\partial P}{\partial y_s} = \frac{\partial P}{\partial u} \frac{\partial u}{\partial y_s} + \frac{\partial P}{\partial v} \frac{\partial v}{\partial y_s} $$

引入简写符号,令屏幕空间位置导数为

$$ P_x = \frac{\partial P}{\partial x_s}, P_y = \frac{\partial P}{\partial y_s} $$

UV 导数为

$$ \begin{aligned} u_x &= \frac{\partial u}{\partial x_s}, & v_x &= \frac{\partial v}{\partial x_s} \\ u_y &= \frac{\partial u}{\partial y_s}, & v_y &= \frac{\partial v}{\partial y_s} \end{aligned} $$

代入 $T$ 和 $B$ 的定义,上述关系式可重写为:

$$ P_x = u_x T + v_x B $$

$$ P_y = u_y T + v_y B $$

3. 线性方程组构建

将上述关系视为关于未知向量 $T$ 和 $B$ 的线性方程组。写成矩阵形式如下:

$$ \begin{bmatrix} P_x \\ P_y \end{bmatrix} = \begin{bmatrix} u_x & v_x \\ u_y & v_y \end{bmatrix} \begin{bmatrix} T \\ B \end{bmatrix} $$

其中,系数矩阵由 UV 的屏幕空间导数构成,已知量 $P_x, P_y$ 由几何位置的屏幕空间导数给出。

4. 切向量解析解

为求解 $T$ 和 $B$ ,计算系数矩阵的行列式 $\Delta$ :

$$ \Delta = u_x v_y - u_y v_x $$

假设 $\Delta \neq 0$ (即 UV 映射非退化),利用克莱姆法则或直接求逆,可得 $T$ 和 $B$ 的解析解:

$$ T = \frac{1}{\Delta} (v_y P_x - v_x P_y) $$

$$ B = \frac{1}{\Delta} (u_x P_y - u_y P_x) $$

在实际着色器实现中,由于后续通常会对切线空间基底进行归一化和重新正交化,常数因子 $\frac{1}{\Delta}$ 常被省略,仅保留方向信息:

$$ T \propto v_y P_x - v_x P_y $$

$$ B \propto u_x P_y - u_y P_x $$

示例代码

HLSL
float3 dpdx = ddx(worldPos);
float3 dpdy = ddy(worldPos);

float2 duvdx = ddx(uv);
float2 duvdy = ddy(uv);

// 忽略 Δ
// float determinant = duvdx.x * duvdy.y - duvdx.y * duvdy.x;

// float3 tangent = (duvdy.y * dpdx - duvdx.y * dpdy) / determinant;
float3 tangent = (duvdy.y * dpdx - duvdx.y * dpdy);

// float3 bitangent = (duvdx.x * dpdy - duvdy.x * dpdx) / determinant;
float3 bitangent = (duvdx.x * dpdy - duvdy.x * dpdx);

tangent = normalize(tangent);
bitangent = normalize(bitangent);
点击展开查看更多

法一:通过法线重建

核心区别:使用插值后的顶点法线,再用 Gram–Schmidt 修正 T/B,使 TBN 与平滑着色法线一致。 适用场景:普通网格 + normal mapping 的实时渲染,基本上非特殊情况用这个就行。

数学推导

已知由屏幕空间导数求得的初始切线为 $T_{\text{raw}}$ ,且当前片元接收到由顶点插值而来的几何法线为 $N$ 。由于 $T_{\text{raw}}$ 是纯粹根据当前像素的几何微分计算出的,它通常不会严格垂直于插值后的平滑 $N$ 。 具体方法是使用施密特正交化方法,将 $T_{\text{raw}}$ 投影到 $N$ 所在的法平面上,剔除掉它在 $N$ 方向上的分量。

$T_{\text{raw}}$ 在 $N$ 方向上的投影长度为:

$$ T_{\text{raw}} \cdot N $$

对应的投影向量为:

$$ (T_{\text{raw}} \cdot N) N $$

从原始切线中减去该投影分量,得到与 $N$ 严格正交的切线向量:

$$ T_{\text{raw}} - (T_{\text{raw}} \cdot N) N $$

归一化得到切线 T:

$$ T = \text{normalize}(T_{\text{raw}} - (T_{\text{raw}} \cdot N) N) $$

计算副切线 B:

$$ B = N \times T $$

最终构建的 TBN 矩阵:

$$ \mathbf{M}_{TBN} = \begin{bmatrix} T_x & B_x & N_x \\ T_y & B_y & N_y \\ T_z & B_z & N_z \end{bmatrix} $$

代码示例

HLSL
// 前置计算只需要计算 T 即可
float3 T = normalize(T_raw - N * dot(N, T_raw));
// 通过 Normal 计算 B
float3 B = cross(N, T);
// 最终 TBN 矩阵
float3x3 TBN = float3x3(T, B, N);
点击展开查看更多

法二:完全通过导数重建

核心区别:从位置导数直接计算,完全由当前像素的几何微分决定,完全数学正确,但会三角化。
适用场景程序化或没有顶点法线的数据,如屏幕空间表面、raymarch、FFT 水面、位移曲面等。

数学推导

已知由屏幕空间导数求得的初始切线为 $T_{\text{raw}}$ 与初始副切线 $B_{\text{raw}}$ 。由于它们是直接从 $P$ 对 $u, v$ 的偏导数解析解中获得的,它们严格定义了当前像素处的几何切平面。

直接推导出当前像素的几何法线:

$$ N = \text{normalize}(T_{\text{raw}} \times B_{\text{raw}}) $$

重新计算副切线,确保副切线与两者皆垂直:

$$ B = \text{normalize}(N \times T_{\text{raw}}) $$

为了保证切线空间基底的单位正交性,需先对向量进行重新归一化与正交化处理:

$$ T = \text{normalize}(T_{\text{raw}}) $$

最终构建的 TBN 矩阵:

$$ \mathbf{M}_{TBN} = \begin{bmatrix} T_x & B_x & N_x \\ T_y & B_y & N_y \\ T_z & B_z & N_z \end{bmatrix} $$

代码示例

HLSL
// 1. 计算几何法线 (基于原始导数向量)
float3 N = normalize(cross(T_raw, B_raw));

// 2. 重新计算副切线以确保正交 (使用原始 T_raw 即可)
float3 B = normalize(cross(N, T_raw));

// 3. 归一化切线
float3 T = normalize(T_raw);

// 4. 最终构建 TBN
float3x3 TBN = float3x3(T, B, N);
点击展开查看更多

版权声明

作者: Cheyne Xie

链接: https://chaim.eu.org/posts/7087033e/

许可证: CC BY-NC-SA 4.0

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. Please attribute the source, use non-commercially, and maintain the same license.

开始搜索

输入关键词搜索文章内容

↑↓
ESC
⌘K 快捷键