NOTE这是全系列最硬核也最美的一部分。光栅化追求”快得能实时交互”,光线追踪追求”正确到能当参考图”。本章作为渲染方程 / 辐射度量学 / BRDF / 蒙特卡洛积分 / 路径追踪的 canonical 位置——Part 2 光栅化里写到的光照模型、Part 6 里的 PBR/RTX/NeRF,都回链到这里。
目录
- 辐射度量学基础 — 立体角 / 辐射通量 / 辐射度 / 辐照度(整个体系的物理单位)
- 渲染方程 — 积分形式 / 递归形式 / Neumann 级数 / 光传输算子
- BRDF 与材质模型 — 定义 / 性质 / Lambert / Phong / Cook-Torrance / GGX
- 光线-几何体相交 — Ray 参数化 / 球 / 三角形(Möller-Trumbore)/ AABB Slab
- 空间加速结构 — BVH(canonical)/ SAH / KD-Tree / 八叉树对比
- 蒙特卡洛积分 — 估计量方差 / 重要性采样 / 分层 / MIS
- 路径追踪 — Whitted / 基础路径追踪 / 俄罗斯轮盘赌 / 直接+间接分离
- 高级技术概述 — 双向路径追踪 / 光子映射 / MLT(Part 6 前导)
十六、辐射度量学基础
WARNING为什么先讲辐射度量学:渲染方程里每一个符号都有严格的物理单位。Part 2 的 Blinn-Phong 用 “光强 × cos” 的朴素模型可以写出看起来对的图,但要做 PBR、对比真实世界、做反演(逆渲染),就必须回到辐射度量学的严格定义。
16.1 立体角
16.1.1 定义
立体角是平面角在三维空间的推广:一个锥体在单位球上截下的面积。
- 整个球面的立体角是 sr
- 半球是 sr
16.1.2 球面坐标下的微分立体角
用 参数化( 为极角/与法线夹角, 为方位角):
NOTE这个 因子很关键:接近两极时相同的 对应的球面面积缩小。后面半球积分 都要记得这一点。
16.1.3 验证半球面积
16.2 辐射度量四件套
| 量 | 符号/单位 | 定义 | 直觉 |
|---|---|---|---|
| 辐射通量 | [W] | 单位时间通过表面/区域的能量 | ”每秒多少焦耳的光” |
| 辐射强度 | [W/sr] | 单位立体角上的辐射通量 | 点光源的各向强度分布 |
| 辐照度 | [W/m²] | 表面单位面积接收到的辐射通量 | 墙上”每平米多亮”,与观察方向无关 |
| 辐射度 | [W/(m²·sr)] | 每单位投影面积、单位立体角的辐射通量 | ”某方向上看某点的亮度”,相机直接感知这个量 |
16.2.1 辐射度的严格定义
其中 是 与表面法线的夹角。投影面积 是辐射度比辐照度更基本的原因——辐射度沿真空传播不变(能量守恒的简洁表达)。
16.2.2 辐照度与辐射度的关系
这就是半球辐照度积分,是渲染方程的右侧的雏形。
16.3 Lambert 余弦定律
对一个面元接收平行光,单位法向面积上的功率 = 入射功率 × 。这就是为什么渲染方程里到处有 。
十七、渲染方程(canonical)
WARNINGKajiya 1986 的渲染方程是整个物理渲染的根基。这一章是全系列渲染方程的规范位置,后面 Part 6 的 PBR、RTX、NeRF 都会回链这里。
17.1 渲染方程的积分形式
在表面点 、出射方向 上,出射辐射度满足:
各项含义:
- :出射辐射度(我们要算的目标)
- :自发光项(光源的内禀辐射)
,下一章详述 - :入射辐射度
余弦 - :上半球,积分微元
17.2 递归形式
本身就是从 方向射向 的光——如果场景中沿 走遇到点 ,那么:
代入原方程得到递归形式:
这是一个第二类 Fredholm 积分方程——未知函数 同时出现在等号两侧。
17.3 Neumann 级数展开
定义光传输算子 :
渲染方程写成算子形式:。形式上求解:
📌 各项的物理意义
- :0 次弹射——直接看到光源本身
- :1 次弹射——光源发光 → 经 1 次反射到眼睛(Whitted 直接光照模型止步于此)
- :2 次弹射——间接光照的第一层
- :n 次弹射——完整全局光照
路径追踪本质上就是用蒙特卡洛方法对这个无穷级数做无偏估计。
17.4 收敛性
谱半径条件: 时级数收敛。
物理上对应能量守恒:每次反射能量损失一部分(BRDF 半球积分 ),无穷次弹射总能量有限。
TIP工程上用俄罗斯轮盘赌把无穷级数无偏地截断成有限路径(见 §22.3)。
十八、BRDF 与材质模型
18.1 BRDF 的定义
双向反射分布函数(Bidirectional Reflectance Distribution Function):
直觉:单位入射辐照度带来的单位出射辐射度。注意它的单位是 ,不是无量纲。
18.2 BRDF 必须满足的三条性质
性质 1 — 非负性:
性质 2 — Helmholtz 互易(时间反演对称):
性质 3 — 能量守恒:对任意
WARNINGPart 2 的 Blinn-Phong 模型不满足能量守恒(没归一化),PBR 里用 Blinn-Phong 前要乘归一化因子 才合法。
18.3 Lambert 漫反射(完美各向同性散射)
定义:
称为漫反射率(albedo)。
📌 为什么分母是 (完整推导)
要求能量守恒(取等号):
即 时是完美白漫反射——入多少出多少,无方向偏好。
18.4 Phong / Blinn-Phong(经验模型)
是入射方向关于法线的完美镜面反射方向。归一化因子 是满足能量守恒的必要条件(Part 2 §10 里省略了,这里补上)。
Blinn-Phong 用半向量 取代 ,归一化因子为 。
18.5 Cook-Torrance(物理微表面模型)
现代 PBR 的主流选择。假设表面由大量微小镜面(microfacet)组成:
三个分量各司其职:
- 法线分布 :微表面法线与宏观法线夹角的分布(GGX/Beckmann)
- 菲涅尔 :镜面反射率随入射角的变化(Schlick 近似)
- 几何项 :自阴影/自遮蔽(Smith 模型)
18.5.1 GGX(Trowbridge-Reitz)分布
。GGX 相比 Beckmann 有更长的高光尾巴,匹配真实材质的观测。
18.5.2 Schlick 菲涅尔近似
是正入射时的反射率;金属用 RGB 三通道值,非金属典型 。
18.5.3 Smith 几何项
Part 6 会给完整实现,这里先有个概念即可。
十九、光线-几何体相交
19.1 光线参数化
- :起点
- :方向(通常取单位向量)
- :避免自相交(典型 )
struct Ray { Eigen::Vector3f origin; Eigen::Vector3f direction; // 单位向量 float t_min = 1e-4f; float t_max = std::numeric_limits<float>::infinity();
Eigen::Vector3f at(float t) const { return origin + t * direction; }};19.2 光线-球体求交
球面方程 ,代入 得:
取 为单位向量时 ,判别式 (其中 ,)。
bool intersect_sphere(const Ray& r, const Eigen::Vector3f& c, float radius, float& t_hit) { Eigen::Vector3f oc = r.origin - c; float b = 2.0f * oc.dot(r.direction); float c_term = oc.squaredNorm() - radius * radius; float disc = b * b - 4.0f * c_term; if (disc < 0) return false; float sqrt_d = std::sqrt(disc); float t0 = (-b - sqrt_d) * 0.5f; float t1 = (-b + sqrt_d) * 0.5f; if (t0 > t1) std::swap(t0, t1); t_hit = (t0 >= r.t_min) ? t0 : t1; return t_hit >= r.t_min && t_hit <= r.t_max;}WARNING当 远离球心、 很小时 ,裸减法会灾难性消去。工业实现(PBRT)用 先投影再算垂直距离,数值更稳。
19.3 光线-三角形:Möller-Trumbore
核心思想:把交点用重心坐标表示(Part 2 §8.3 的定义回顾), 既是求解量也是插值坐标,一次解完。
设三角形 ,交点满足:
移项:
其中 。
用 Cramer 法则 + 标量三重积恒等式 ,可不显式求逆:
其中 。
bool moller_trumbore(const Ray& r, const Eigen::Vector3f& v0, const Eigen::Vector3f& v1, const Eigen::Vector3f& v2, float& t, float& u, float& v) { const float EPS = 1e-8f; Eigen::Vector3f e1 = v1 - v0; Eigen::Vector3f e2 = v2 - v0; Eigen::Vector3f p = r.direction.cross(e2); float det = p.dot(e1); if (std::abs(det) < EPS) return false; // 光线平行于三角形 float inv_det = 1.0f / det; Eigen::Vector3f s = r.origin - v0; u = p.dot(s) * inv_det; if (u < 0 || u > 1) return false; Eigen::Vector3f q = s.cross(e1); v = q.dot(r.direction) * inv_det; if (v < 0 || u + v > 1) return false; t = q.dot(e2) * inv_det; return t >= r.t_min && t <= r.t_max;}TIP得到 后,可以直接做 Part 2 §8.3 的重心坐标插值——法线、纹理坐标、顶点色全都在这一次求交里顺便算出。
19.4 光线-AABB:Slab 法
AABB = 三对平行轴对齐平面。光线与每对平面求交得 ,取三组区间的交,非空即相交。
相交条件 且 。
struct AABB { Eigen::Vector3f pmin, pmax;
bool intersect(const Ray& r, float& t_enter, float& t_exit) const { t_enter = -std::numeric_limits<float>::infinity(); t_exit = std::numeric_limits<float>::infinity(); for (int i = 0; i < 3; ++i) { float d = r.direction[i]; if (std::abs(d) < 1e-8f) { // 光线平行于第 i 轴 if (r.origin[i] < pmin[i] || r.origin[i] > pmax[i]) return false; continue; } float inv_d = 1.0f / d; float t0 = (pmin[i] - r.origin[i]) * inv_d; float t1 = (pmax[i] - r.origin[i]) * inv_d; if (t0 > t1) std::swap(t0, t1); t_enter = std::max(t_enter, t0); t_exit = std::min(t_exit, t1); if (t_enter > t_exit) return false; } return t_exit >= std::max(0.0f, r.t_min); }};二十、空间加速结构(BVH canonical)
WARNING本章是全系列 BVH 的规范位置。Part 2 光栅化不用 BVH(光栅化按屏幕顺序扫描),但光线追踪、碰撞检测、剔除都会回链这里。
20.1 为什么需要加速结构
朴素光线追踪对每条光线都与全场景 个图元求交,复杂度 。一张 1080p 图像就是 条主光线,Cornell Box 几十个三角形还行,换到百万三角形场景直接崩。加速结构把每条光线的期望代价降到 。
20.2 BVH 的基本思想
Bounding Volume Hierarchy = 包围盒层次:用二叉树递归把场景切分,内部节点的 AABB 包含所有后代,叶子节点放少量图元。
光线遍历时:
- 与根 AABB 求交失败 → 整棵树跳过
- 与内部节点相交 → 递归进两个子节点
- 到达叶子 → 才与真正的三角形求交
20.2.1 数据结构
struct BVHNode { AABB bounds; BVHNode* left = nullptr; BVHNode* right = nullptr; std::vector<const Object*> prims; // 仅叶子非空 bool is_leaf() const { return left == nullptr && right == nullptr; }};20.2.2 递归构建
BVHNode* build(std::vector<const Object*>& objs) { auto* node = new BVHNode; for (auto* o : objs) node->bounds.expand(o->bounds());
if (objs.size() <= 4) { // 叶子阈值 node->prims = objs; return node; }
// 沿最长轴划分:按质心中位数分两半(Naive SAH 的替代) int axis = node->bounds.longest_axis(); std::nth_element(objs.begin(), objs.begin() + objs.size()/2, objs.end(), [axis](const Object* a, const Object* b) { return a->bounds().centroid()[axis] < b->bounds().centroid()[axis]; }); std::vector<const Object*> left(objs.begin(), objs.begin() + objs.size()/2); std::vector<const Object*> right(objs.begin() + objs.size()/2, objs.end());
node->left = build(left); node->right = build(right); return node;}20.2.3 光线遍历
bool traverse(const BVHNode* n, const Ray& r, Intersection& best) { float t_enter, t_exit; if (!n->bounds.intersect(r, t_enter, t_exit)) return false; if (t_enter > best.t) return false; // 已有更近的交点,整棵子树剪枝
if (n->is_leaf()) { bool hit = false; for (auto* p : n->prims) { Intersection tmp; if (p->intersect(r, tmp) && tmp.t < best.t) { best = tmp; hit = true; } } return hit; } // 内部节点:先访问更近的一侧 bool hl = traverse(n->left, r, best); bool hr = traverse(n->right, r, best); return hl || hr;}20.3 SAH:表面积启发式
目标:让划分方式最小化期望光线-节点相交代价。对一个内部节点,代价为:
其中 是 AABB 表面积, 是左右子图元数。
核心假设:光线方向均匀 → 光线击中子盒的概率正比于表面积。
20.3.1 桶分(Bucketing)加速
朴素 SAH 要试 个划分位置,每次 计算成本 → 。实用做法分桶:把图元质心按最长轴分到 个桶里,只在 个桶边界处试划分:
constexpr int NUM_BUCKETS = 12;struct Bucket { int count = 0; AABB bounds; };
int best_split_sah(const std::vector<const Object*>& objs, int axis, const AABB& all) { Bucket buckets[NUM_BUCKETS]; for (auto* o : objs) { Eigen::Vector3f c = o->bounds().centroid(); float t = (c[axis] - all.pmin[axis]) / (all.pmax[axis] - all.pmin[axis]); int b = std::min(NUM_BUCKETS - 1, int(t * NUM_BUCKETS)); buckets[b].count++; buckets[b].bounds.expand(o->bounds()); }
float best_cost = std::numeric_limits<float>::infinity(); int best_i = -1; for (int i = 0; i < NUM_BUCKETS - 1; ++i) { AABB b0, b1; int n0 = 0, n1 = 0; for (int j = 0; j <= i; ++j) { b0.expand(buckets[j].bounds); n0 += buckets[j].count; } for (int j = i + 1; j < NUM_BUCKETS; ++j) { b1.expand(buckets[j].bounds); n1 += buckets[j].count; } if (n0 == 0 || n1 == 0) continue; float cost = 0.125f + (n0 * b0.surface_area() + n1 * b1.surface_area()) / all.surface_area(); if (cost < best_cost) { best_cost = cost; best_i = i; } } return best_i;}20.4 与其他加速结构对比
| 结构 | 划分方式 | 优势 | 劣势 |
|---|---|---|---|
| BVH | 对象空间,每节点一个 AABB | 一个图元只进一个叶子;动态更新友好;实现简洁 | 兄弟 AABB 可能重叠 |
| KD-Tree | 空间二分,轴对齐分割面 | 遍历高效,经典理论最优 | 图元可能跨边界被复制;重建贵 |
| 八叉树 | 空间八分(2³ 均匀) | 实现极简;体素/粒子友好 | 图元分布不均时浪费严重 |
| 均匀网格 | 空间均匀切成固定大小格子 | 遍历每格;构建极快 | Teapot-in-stadium(尺寸悬殊)最坏情况极差 |
TIP生产级光线追踪器(Embree、OptiX、PBRT)基本全用 SAH BVH。动态场景用 refit 或 LBVH(线性 BVH)。
二十一、蒙特卡洛积分
NOTE渲染方程是一个高维积分,维度随弹射次数指数增长。高维数值积分唯一不怕维度诅咒的方法就是蒙特卡洛:收敛速率 与维度无关。
21.1 基本蒙特卡洛估计量
无偏性:。
21.2 方差分析
📌 方差最小的采样分布(最优重要性采样)
用拉格朗日乘子法最小化 在约束 下:
此时方差为零(若 )。但这需要知道 ——正是我们要算的量。实用策略:让 尽量正比于 ,即重要性采样。
21.3 重要性采样(IS)
渲染方程被积函数 里, 通常只在光源方向上大, 在法线附近大, 在镜面方向附近大。沿这些”大值方向”采样能显著降方差。
21.3.1 余弦加权半球采样
采样 (恰好和 Lambert 的 项匹配)。用 Malley 方法:
- 在单位圆盘上均匀采样
- 取
Eigen::Vector3f sample_cosine_hemisphere(float u1, float u2) { float r = std::sqrt(u1); float phi = 2.0f * MY_PI * u2; float x = r * std::cos(phi); float y = r * std::sin(phi); float z = std::sqrt(std::max(0.0f, 1.0f - u1)); return {x, y, z};}float cosine_hemisphere_pdf(float cos_theta) { return cos_theta / MY_PI; }21.3.2 均匀半球采样(对比基线)
:
Lambert 材质下余弦加权方差远小于均匀方差。
21.4 分层采样
将积分域分成 个等分块,每块独立采 个样本:
工程上最简单的 网格 + 格内抖动,就能明显减噪。
21.5 多重重要性采样(MIS)
当有多个采样策略(比如 BRDF 采样 + 光源采样)时,用加权组合:
权函数要求 在 处。Veach 1997 的幂启发式():
TIPMIS 是”既采 BRDF 又采光源”能无偏结合的数学基础——没它就要在两种策略里二选一,光泽高光(BRDF 好采)和点光源(光源好采)只能兼顾其一。
二十二、路径追踪
22.1 Whitted 风格(经典基线)
Whitted 1980 的非物理但漂亮的模型:
- 直接光照:显式遍历光源 + Phong 模型
- 镜面反射 / 折射:递归追踪理想反射方向 / Snell 折射方向
- 终止:固定递归深度
Eigen::Vector3f whitted(const Ray& r, const Scene& s, int depth) { if (depth <= 0) return Eigen::Vector3f::Zero(); Intersection hit = s.intersect(r); if (!hit.happened) return s.background;
Eigen::Vector3f color = hit.material->emission;
// 直接光照(所有光源累加) for (const auto& light : s.lights) { Eigen::Vector3f L_dir = (light.pos - hit.p).normalized(); float dist = (light.pos - hit.p).norm(); Ray shadow{hit.p + EPSILON * hit.n, L_dir, EPSILON, dist - EPSILON}; if (!s.occluded(shadow)) { float cos_t = std::max(0.0f, hit.n.dot(L_dir)); color += hit.material->brdf(-r.direction, L_dir, hit.n) * light.intensity * cos_t / (dist * dist); } } // 镜面递归 if (hit.material->specular) { Eigen::Vector3f R = reflect(r.direction, hit.n); Ray refl{hit.p + EPSILON * hit.n, R}; color += hit.material->Ks * whitted(refl, s, depth - 1); } return color;}局限:只有”光源 → 镜面链 → 眼”这一种路径被显式算。diffuse 之间的相互反射(color bleeding)完全缺失。
22.2 基础路径追踪(Kajiya 1986)
每次弹射用蒙特卡洛随机采一个方向,递归直到俄罗斯轮盘赌终止。本质是对 Neumann 级数 的无偏估计。
22.2.1 朴素实现(只按 BRDF 采样)
Eigen::Vector3f path_trace_naive(const Ray& r, const Scene& s, int depth) { Intersection hit = s.intersect(r); if (!hit.happened) return s.background;
Eigen::Vector3f L = hit.material->emission; if (depth >= MAX_DEPTH) return L;
// 俄罗斯轮盘赌 const float p_rr = 0.8f; if (random_float() > p_rr) return L;
// 按 BRDF 采样新方向 Eigen::Vector3f wi; float pdf; Eigen::Vector3f f = hit.material->sample(-r.direction, hit.n, wi, pdf); if (pdf < 1e-8f) return L;
float cos_t = std::max(0.0f, hit.n.dot(wi)); Ray next{hit.p + EPSILON * hit.n, wi}; Eigen::Vector3f Li = path_trace_naive(next, s, depth + 1);
L += f.cwiseProduct(Li) * cos_t / pdf / p_rr; return L;}问题:小光源击中率低,噪点可怕。
22.3 分直接/间接的路径追踪(GAMES101 PA7 风格)
关键改进:每次交点显式采光源算直接光照,只让递归负责”下一次弹射后的间接光”——这样光源采样的低方差被充分利用。
Eigen::Vector3f shade(const Intersection& hit, const Eigen::Vector3f& wo, const Scene& s) { if (hit.material->has_emission()) return hit.material->emission;
// —— 直接光照:从光源上采一点 —— Eigen::Vector3f L_dir = Eigen::Vector3f::Zero(); Intersection light_hit; float light_pdf; s.sample_light(light_hit, light_pdf); Eigen::Vector3f ws = (light_hit.p - hit.p).normalized(); float dist2 = (light_hit.p - hit.p).squaredNorm(); Ray to_light{hit.p + EPSILON * hit.n, ws}; Intersection chk = s.intersect(to_light); if (chk.happened && (chk.p - light_hit.p).squaredNorm() < 1e-3f) { Eigen::Vector3f f = hit.material->eval(ws, wo, hit.n); float cos_t = std::max(0.0f, hit.n.dot(ws)); float cos_tl = std::max(0.0f, light_hit.n.dot(-ws)); L_dir = light_hit.material->emission.cwiseProduct(f) * cos_t * cos_tl / dist2 / light_pdf; }
// —— 间接光照:俄罗斯轮盘赌 + BRDF 采样 —— Eigen::Vector3f L_indir = Eigen::Vector3f::Zero(); const float p_rr = 0.8f; if (random_float() < p_rr) { Eigen::Vector3f wi = hit.material->sample(wo, hit.n); float pdf = hit.material->pdf(wi, wo, hit.n); if (pdf > 1e-8f) { Ray next{hit.p + EPSILON * hit.n, wi}; Intersection next_hit = s.intersect(next); if (next_hit.happened && !next_hit.material->has_emission()) { Eigen::Vector3f f = hit.material->eval(wi, wo, hit.n); float cos_t = std::max(0.0f, hit.n.dot(wi)); L_indir = shade(next_hit, -wi, s).cwiseProduct(f) * cos_t / pdf / p_rr; } } } return L_dir + L_indir;}22.3.1 为什么要跳过命中光源的间接项
直接光照里我们已经显式采了光源。如果间接递归再撞上光源,那条路径被算了两次,结果偏亮。判据 !next_hit.material->has_emission() 就是排除这种重复。
22.3.2 俄罗斯轮盘赌为什么无偏
设继续概率 ,继续时把贡献除以 :
与直接算 等价。但方差被放大 ,所以 不能太小(典型 0.8)。
22.4 BRDF/光源 MIS 组合
当表面是光泽(非漫反射、非纯镜面)时,BRDF 采样可能比光源采样更有效。MIS 把两种策略的样本加权合并——工业级路径追踪器的标配。完整实现见 PBRT 第 13 章。
二十三、高级技术概述(Part 6 前导)
23.1 双向路径追踪(BDPT)
从摄像机走一条路径 + 从光源走一条路径,然后在所有 组合连接点上评估贡献,MIS 合并。
- 强项:焦散、镜面-漫反射-镜面(SDS)路径
- 代价:每像素评估连接次数爆炸
23.2 光子映射
两阶段:(1) 从光源发射光子、存到 KD-Tree; (2) 相机视角光线交到表面时,对附近光子做密度估计。
- 强项:焦散(如玻璃杯在桌面上的光斑)
- 弱项:低频偏差,需要仔细选半径
23.3 Metropolis Light Transport(MLT)
基于马尔科夫链的采样,对贡献大的路径局部扰动,自适应聚焦困难光路。对 SDS、细缝光极其有效,但收敛分析困难。
23.4 体渲染与参与介质
当光穿过雾、烟、皮下组织时,Beer-Lambert 衰减 + phase function + in-scattering/out-scattering——渲染方程要扩展成体渲染方程(Volume Rendering Equation)。NeRF 的数学底层就是它,Part 6 展开。
小结
| 主题 | 工具 | canonical 位置 |
|---|---|---|
| 辐射度量学 | 四件套 | §16 — 是 Part 2 光照、Part 6 PBR 的物理基础 |
| 渲染方程 | Kajiya 1986 积分/递归/Neumann | §17 — 后面所有章节回链 |
| BRDF | 定义 / 互易 / 能量守恒 / Lambert-Phong-Cook-Torrance | §18 — Part 2 光照模型的物理正名 |
| 求交 | Ray 参数化 / 球 / Möller-Trumbore / Slab | §19 |
| BVH | 递归构建 / SAH / 桶分 | §20 — 光线追踪与碰撞检测的通用加速 |
| 蒙特卡洛积分 | / 重要性采样 / 分层 / MIS | §21 — 渲染方程唯一的数值工具 |
| 路径追踪 | Whitted / 朴素 PT / 直接+间接分离 / RR 终止 | §22 — 物理渲染的黄金算法 |
| 高级技术 | BDPT / 光子映射 / MLT / 体渲染 | §23 → 细节在 Part 6 |
下一部分进入 Part 5:动画与物理模拟——关键帧、样条插值、粒子系统、弹簧质点、刚体动力学、流体模拟。