prt1:precompute part Diffuse Case
Outline
渲染方程(漫反射)的积分项可以拆成"光"和"传输"两部分,各自投影到球谐基(SH),渲染时只做点积:
Precompute lighting and light transport for each individual shading point.
- 光系数
:环境贴图投影到 SH(全局一份)。 - 传输系数
:每个顶点的 投影到 SH(每顶点一份)。
系数就是"函数在基函数轴上的投影":
PrecomputeCubemapSH光系数
离散
积分无法解析计算,逐像素近似求和(cubemap 的每个像素贡献一份):
| 量 | 对应 |
|---|---|
Le(该像素 RGB) | |
sh::EvalSH(l, m, dirD) | |
CalcArea(x, y, width, height) |
像素方向
每个面的方向由三个轴向量拼出(cubemapFaceDirections):
cpp
Eigen::Vector3f dir = (faceDirX * u + faceDirY * v + faceDirZ).normalized();
// u, v ∈ [-1, 1],取自像素中心 (x + 0.5) / width求和sum
cpp
for (int i = 0; i < 6; i++) // 6 个面
for (int y = 0; y < height; y++)
for (int x = 0; x < width; x++) {
Eigen::Vector3f dir = cubemapDirs[i * width * height + y * width + x];
int index = (y * width + x) * channel;
Eigen::Array3f Le(images[i][index + 0], images[i][index + 1],
images[i][index + 2]);
float angle = CalcArea(x, y, width, height); // 注意签名 (u_, v_, width, height)...某种比较经典的求球面积分图...?
Eigen::Vector3d dirD = dir.cast<double>(); // EvalSH 要 double
for (int l = 0; l <= SHOrder; ++l)
for (int m = -l; m <= l; ++m)
SHCoeffiecents[sh::GetIndex(l, m)] +=
Le * sh::EvalSH(l, m, dirD) * angle; // f × Y × dω
}GetIndex(l, m) = l*(l+1)+mEvalSH需要Eigen::Vector3d,dir是Vector3f,要cast<double>()。- 2 阶 SH → 9 个系数,
GetIndex下标范围 [0, 8]。
EvalSH
cpp
double EvalSH(int l, int m, const Eigen::Vector3d& dir) {
if (l <= kHardCodedOrderLimit) {
// Validate l and m here (don't do it generally since EvalSHSlow also
// checks it if we delegate to that function).
CHECK(l >= 0, "l must be at least 0.");
CHECK(-l <= m && m <= l, "m must be between -l and l.");
CHECK(NearByMargin(dir.squaredNorm(), 1.0), "dir is not unit.");
switch (l) {
case 0:
return HardcodedSH00(dir);
case 1:
switch (m) {
case -1:
return HardcodedSH1n1(dir);
case 0:
return HardcodedSH10(dir);
case 1:
return HardcodedSH1p1(dir);
}
case 2:
switch (m) {
case -2:
return HardcodedSH2n2(dir);
case -1:
return HardcodedSH2n1(dir);
case 0:
return HardcodedSH20(dir);
case 1:
return HardcodedSH2p1(dir);
case 2:
return HardcodedSH2p2(dir);
}
case 3:
switch (m) {
case -3:
return HardcodedSH3n3(dir);
case -2:
return HardcodedSH3n2(dir);
case -1:
return HardcodedSH3n1(dir);
case 0:
return HardcodedSH30(dir);
case 1:
return HardcodedSH3p1(dir);
case 2:
return HardcodedSH3p2(dir);
case 3:
return HardcodedSH3p3(dir);
}
case 4:
switch (m) {
case -4:
return HardcodedSH4n4(dir);
case -3:
return HardcodedSH4n3(dir);
case -2:
return HardcodedSH4n2(dir);
case -1:
return HardcodedSH4n1(dir);
case 0:
return HardcodedSH40(dir);
case 1:
return HardcodedSH4p1(dir);
case 2:
return HardcodedSH4p2(dir);
case 3:
return HardcodedSH4p3(dir);
case 4:
return HardcodedSH4p4(dir);
}
}
// This is unreachable given the CHECK's above but the compiler can't tell.
return 0.0;
} else {
// Not hard-coded so use the recurrence relation (which will convert this
// to spherical coordinates).
return EvalSHSlow(l, m, dir);
}
}Filter
- 和"邻域像素取平均再存回"的空间域模糊不同(split sum等...?)。
- 是投影到 SH 基 + 截断到 2 阶:高频细节全部丢掉,等价于低通滤波。
- 依据(Ramamoorthi & Hanrahan 2001):漫反射 BRDF 本身就是[低通滤波器],光照与 cos 核卷积后只含低频,9 个系数近似误差约 1%。
shFunc + ProjectFunction物体系数
outline
shFunc(phi, theta):给定一个方向,返回一个数——传输函数值。 ProjectFunction(order, func, sample_count):内部做分层均匀球面采样 + 乘基函数累加 + 乘权重,返回系数向量。
投影(上面写过的循环等)在 ProjectFunction 里完成。
ProjectFunction
cpp
std::unique_ptr<std::vector<double>> ProjectFunction(
int order, const SphericalFunction& func, int sample_count) {
CHECK(order >= 0, "Order must be at least zero.");
CHECK(sample_count > 0, "Sample count must be at least one.");
// This is the approach demonstrated in [1] and is useful for arbitrary
// functions on the sphere that are represented analytically.
const int sample_side = static_cast<int>(floor(sqrt(sample_count)));
std::unique_ptr<std::vector<double>> coeffs(new std::vector<double>());
coeffs->assign(GetCoefficientCount(order), 0.0);
// generate sample_side^2 uniformly and stratified samples over the sphere
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<> rng(0.0, 1.0);
for (int t = 0; t < sample_side; t++) {
for (int p = 0; p < sample_side; p++) {
double alpha = (t + rng(gen)) / sample_side;
double beta = (p + rng(gen)) / sample_side;
// See http://www.bogotobogo.com/Algorithms/uniform_distribution_sphere.php
double phi = 2.0 * M_PI * beta;
double theta = acos(2.0 * alpha - 1.0);
// evaluate the analytic function for the current spherical coords
double func_value = func(phi, theta);
// evaluate the SH basis functions up to band O, scale them by the
// function's value and accumulate them over all generated samples
for (int l = 0; l <= order; l++) {
for (int m = -l; m <= l; m++) {
double sh = EvalSH(l, m, phi, theta);
(*coeffs)[GetIndex(l, m)] += func_value * sh;
}
}
}
}
// scale by the probability of a particular sample, which is
// 4pi/sample_side^2. 4pi for the surface area of a unit sphere, and
// 1/sample_side^2 for the number of samples drawn uniformly.
double weight = 4.0 * M_PI / (sample_side * sample_side);
for (unsigned int i = 0; i < coeffs->size(); i++) {
(*coeffs)[i] *= weight;
}
return coeffs;
}unshadowed
cpp
auto shFunc = [&](double phi, double theta) -> double {
Eigen::Array3d d = sh::ToVector(phi, theta);
const auto wi = Vector3f(d.x(), d.y(), d.z());
if (m_Type == Type::Unshadowed)
return std::max(0.0f, n.dot(wi)); // 就是 H,一个数
...
};shadowed:可见性 = 一次射线求交
cpp
else // Shadowed
{
float H = n.dot(wi);
if (H < 0.0) return 0.0f; // 下半球直接 0,省一次求交
Intersection its;
Ray3f ray(v, wi); // 默认 mint = Epsilon,自动避免自相交
if (scene->rayIntersect(ray, its))
return 0.0f; // 有遮挡 → V = 0
return H; // 无遮挡 → V = 1
}scene->rayIntersect(const Ray3f&, Intersection&) const:命中返回 true。Ray3f(o, d)默认mint = Epsilon(ray.h),不需要手动偏移起点。- 先判 H 后打射线,省一半求交;
m_SampleCount(默认 100)控制精度,每顶点要打几百根射线,这就是预计算的耗时来源。
ProjectFunction 内部的权重
cpp
auto shCoeff = sh::ProjectFunction(SHOrder, shFunc, m_SampleCount);
for (int j = 0; j < shCoeff->size(); j++)
m_TransportSHCoeffs.col(i).coeffRef(j) = (*shCoeff)[j];- 采样数会被取成不大于它的最大完全平方数。
- 权重
来自均匀球面采样的概率密度(球面积 ),在库内部已乘好。
关于讲义里的 rgb offset
讲义伪代码:
result[j + red_offset] += value;
result[j + green_offset] += value;
result[j + blue_offset] += value;- 传输项是"无色"标量,光系数是 RGB 三通道 → 把同一标量复制三份对齐 RGB。
- 本框架不需要:
m_TransportSHCoeffs是SHCoeffLength × vertexCount的标量矩阵,Li()里用同一个向量分别和rL/gL/bL点积,天然对齐。
(Li)
cpp
Color3f c0 = Color3f(rL.dot(sh0), gL.dot(sh0), bL.dot(sh0)); // 光系数 · 传输系数result
!

Cost
the complexity is still O(n).