传输公式

传输方程是控制光线在吸收、发射和散射辐射的介质中的行为的基本方程。它解释了第11章中描述的所有体积散射过程——吸收、发射和内、外散射。并给出了一个描述环境中辐射分布的方程。光传输方程实际上是传输方程的一个特例,由于忽略参与介质而进行简化,并专门用于从表面散射。

在方程的基本形式中,传输方程式是一个描述光束在空间中某一点上的辐射量如何变化的积分微分方程。它可以转化为一个纯积分方程,用以描述描述了沿射线无穷多个坐标点的参与介质的效果。

回忆一下11.1.4章节的辐射源项Ls,它表示p点处ω方向上由于从其他介质中自发光和内散射产生的光线而导致的辐射量的变化。
\[ L_s(p,\omega)=L_e(p,\omega)+\sigma_s(p,\omega)\int_{g^2}p(p,\omega',\omega)L_i(p,\omega')d\omega' \]
辐射源项包含了所有光线辐射量增加的过程。其中衰减系数σt(p, ω)包含了所有光线辐射量减少的过程,包括了吸收与外散射。描述其效果的微分方程为:
\[ dL_o(p,+t\omega,\omega)=-\sigma_t(p,\omega)L_i(p,-\omega)dt \]
当计算光线上一点p’的辐射量变化微分时,将以上两种不同的效应结合在一起,从而得到传输方程的积分微分形式:
\[ \frac{\partial}{\partial t}L_o(p+t\omega,\omega)=-\sigma_t(p,\omega)L_i(p,-\omega)+L_s(p,\omega) \]
当采用适当的边界条件时,这个方程可以转换为纯积分方程。例如:如果我们假定场景中没有反射与自发光表面,那么光线就不会被遮挡并且光的长度是无限,那么传输积分方程为:
\[ L_i(p,\omega)=\int^{\infty}_0T_r(p'\rightarrow p)L_s(p',-\omega)dt \]
其中p'=p+tω的意思为:从既定方向到达某一点的辐射量,即该点与光线上所有点的辐射量累加结果。其辐射量将根据光束总的透射率减少。

通常来说,如果场景中存在反射或者自发光表面,光线的长度就不会无限长,并且光线与该表面相交后会影响自身的辐射量,增加表面处该点的出射辐射量以及阻止
光线中超出交点的点对源点的辐射量产生影响。如果光线在表面的p0处相交,其距离为t,则传输积分公式为:
\[ L_i(p,\omega)=T_r(p_0\rightarrow p)L_o(p_0,-\omega)+\int^t_0 T_r(p'\rightarrow p)L_s(p',-\omega)dt' \]
其中p0=p+tω为表面上的点,p’=p+t'ω为光线上的点。

这个方程描述了沿着光线发生的两种对辐射量产生影响的现象。首先,Lo项代表了从表面反射回来的辐射量,它包括了表面自发光与反射回来的辐射量。这种辐射量可能会因为参与介质而减弱,光线源点到p0点之间的透射率解释了这点。第二项解释了由于体积散射和自发光使得沿射线辐射量增加,但只到射线与表面交点为止;超过这一点的点不再影响光线的辐射量。

采样空间散射

在讨论参与介质中光散射效应模型算法之前,我们首先定义一些用于从参与介质与空间位置无关介质的光速透射率相关分布中采样的功能块。

Medium接口定了Sample(),它接受一个世界空间的ray(p,ω)以及MediumInteraction对象指针。输入ray通常将会与场景多边形进行相交测试,因此该方法不应该在光线上超出tmax值的点上采样介质。在不失一般性的前提下,下面的讨论是在总是有一个tmax小于正无穷的曲面的假设下。

这种方法的目的是对传递方程的积分形式进行采样:
\[ L_i(p,\omega)=T_r(p_0\rightarrow p)L_o(p_0,-\omega)+\int^t_0T_r(p+t\omega\rightarrow p)L_s(p+t\omega,-\omega)dt \]
其中p0= p + tmax ω是表面上的点。我们讲忽略介质的自发光并假设介质属性为定向常数,此时:
\[ L_s(p,\omega)=\sigma_s(p)\int_{g^2}p(p,\omega',\omega)L_i(p,\omega')d\omega' \]
两种情况可能会发生:如果Sample()采样的交点没有在给定光线的区间[0,tMax]中,那么表面相关项Tr与Lo就为估算值。如果它采样了一个点,那么第二个积分项将会是估算项,并且提供的MediumInteraction对象应该相应的初始化。

假设pt(t)表示每单位距离的概率p+tω产生交互作用的位置。由于不采样介质相互作用的可能性,这个函数一般不积分为1,我们将psurf定义为对表面项采样的相关离散概率:
\[ p_{surf}=1-\int^{t_{max}}_0p_t(t)dt \]
有了这些定义,我们可以指定Sample()的语义,它不同于之前遇到的散射函数所用的技术,比如BSDF::Sample_f(),因为它不向调用者提供关于函数值和采样位置处的PDF信息。一般来说并不需要这种信息,一些介质模型(特别是不均匀的介质)在计算psurf的比值时,会使用更有效的采样方案。

当选择表面项时,方法应该返回一个权重值:
\[ \beta_{surf}=\frac{T_r(p\rightarrow p+t\omega)}{p_{surf}} \]
它符合采样第一个和。注意,出射辐射量的值Lo不包括在βsurf内。调用者有责任去解释这个项。在介质中时,方法返回:
\[ \beta_{med}=\frac{\sigma(p+t\omega)T_r(p\rightarrow p_t\omega)}{p_t(t)} \]
它对应了除了内散射光积分(公式15.6)外所有介质相关项都会被采样,必须单独处理。

散射系数与透射率允许光谱产生变化,因此这个方法返回一个光谱值权重因子,用以来更新路径的表面出射权重β或介质散射事件。

均匀介质

HomogeneousMedium类的这个方法实现得较为简单。唯一的复杂性源自需要处理随波长变化的衰减系数。

在13.3.1章节,我们推导了定义域为[0,∞)的指数分布的采样方法。对于\(f(t)=e^{-\sigma_tt}\),可以有:
\[ t=\frac{In(1-\xi)}{\sigma_t} \]
其中pdf为:
\[ p_t(t)=\sigma_te^{-\sigma_tt} \]
然而衰减系数σt通常随波长变化而变化。它在介质中多点采样是不可取的,因此首先采用均匀采样来选择光谱通道i;相应的标量σi t值用于样本距离的分布。
\[ \hat{p}^i_t(t)=\sigma^i_te^{-\sigma^i_tt} \]
采用15.9中的方法。所得到的采样密度为各个\(p^i_t\)的均值:
\[ \hat{p}_t(t)=\frac{1}{n}\sum^n_{i=1}\sigma^i_te^{-\sigma^i_tt} \]
在t = tmax处对表面相互作用进行采样的概率(离散)是在t=0和t=tmax之间产生中间散射事件的补充。计算出的概率等于所有n个光谱通道的平均透过率:
\[ p_{surf}=1-\int^{t_{max}}_0\hat{p}_t(t)dt=\frac{1}{n}\sum^n_{i=1}e^{-\sigma^i_tt_{max}} \]
根据15.11的实现提取出的样本;如果采样距离在射线与图元相交之前(如果有),则通过初始化MediumInteraction来记录介质散射事件。否则,忽略介质中的采样点,积分器应使用相应的表面相互作用作为下一个路径顶点。这种采样方法符合自然规律:产生介质相互作用而不是表面相互作用的概率正好等于1减去选定波长的光束透过率。因此,给定光学薄介质(或短射线范围),更经常使用表面相互作用,而对于厚介质(或长射线),更可能取样介质相互作用。

int channel = std::min((int)(sampler.Get1D() * Spectrum::nSamples),
Spectrum::nSamples - 1);
Float dist = -std::log(1 - sampler.Get1D()) / sigma_t[channel];
Float t = std::min(dist * ray.d.Length(), ray.tMax);
bool sampledMedium = t < ray.tMax;
if (sampledMedium)
*mi = MediumInteraction(ray(t), -ray.d, ray.time, this,
ARENA_ALLOC(arena, HenyeyGreenstein)(g));

在这两种情况下,利用比尔定律(11.3)可以很容易地计算光束透射率Tr,就像在HomogeneousMedium::Tr()。

Spectrum Tr = Exp(-sigma_t * std::min(t, MaxFloat) * ray.d.Length());

最后,该方法使用方程(15.11)或(15.12)计算样本密度并返回采样权重βsurf与βmed。

Spectrum density = sampledMedium ? (sigma_t * Tr) : Tr;
Float pdf = 0;
for (int i = 0; i < Spectrum::nSamples; ++i)pdf += density[i];
pdf *= 1 / (Float)Spectrum::nSamples;
return sampledMedium ? (Tr * sigma_s / pdf) : (Tr / pdf);

不均匀的介质

对于GridDensityMedium,需要额外的工作来处理介质的异质性。当空间变化可以分解为均匀区域(如分段常数体素)时,常规追踪技术将标准均匀介质技术分别应用于各个体素;这种方法的一个缺点是,当体素量较多时,它会消耗过多性能。由于网格密度介质依赖于线性插值,因此不能采用这种方法。

其他技术是建立在直接从具有空间变化的衰减公式(15.10)中均匀采样pdf。
\[ p_t(t)=\sigma_t(t)e^{-\int^t_0\sigma_t(t')dt'} \]
其中σt(t)=σt(p+tω)估算射线上距离为t的衰减值。最常用的方法是重要抽样方程(15.13),称为ray marching。该方法通过将范围[0,tmax]划分为若干子区间,对每个区间内的积分进行数值近似计算,最后对该离散表示形式进行反演,从而反演近似的累积分布。然而这种方法会产生系统性的统计偏差,这意味着使用ray marching的积分器通常不会收敛到正确的结果(即使每个像素使用无限多个样本)。

因此,我们更喜欢Woodcock等人(1965)提出的另一种无偏方法,该方法最初是为了模拟原子反应堆中中子的体积散射而开发的。这种技术被称为delta追踪,并且在衰减系数σt是单色的情况下更容易实现。PBRT的实现包括一个断言测试(这里没有显示),以验证情况确实如此。
注意,散射和吸收系数仍然允许波长不同——不过,它们的和σt=σs+σa必须统一。

delta追踪可以解释为在介质中填充额外粒子(虚拟),直到它的衰减系数处处为常数。利用基础指数公式(15.9)可以很容易地对均匀介质进行采样。然而,无论何时发生与粒子的交互,仍然有必要确定它所涉及的是“真实”粒子还是“虚拟”粒子(在这种情况下交互被忽略)。woodcock等人的见解是:根据基于“真实粒子”的局部分数随机做出判断,因此这也引出了样本匹配方程(15.13)分布。

下面是GridDensityMedium构造函数的一部分,它会预计算整个介质最大密度因子的倒数,在接下来的delta追踪中这将十分有用。

<Precompute values for Monte Carlo sampling of GridDensityMedium> ≡ 690
sigma_t = (sigma_a + sigma_s)[0];
Float maxDensity = 0;
for (int i = 0; i < nx * ny * nz; ++i)
maxDensity = std::max(maxDensity, density[i]);
invMaxDensity = 1 / maxDensity;
<GridDensityMedium Private Data> +≡ 690
Float sigma_t;
Float invMaxDensity;

Sample()方法首先将射线转换为介质坐标系,并对射线方向进行归一化;ray.tMax因为归一化的关系会进行适当缩放。

Spectrum GridDensityMedium::Sample(const Ray &rWorld, Sampler &sampler,MemoryArena &arena,MediumInteraction *mi) const {ProfilePhase _(Prof::MediumSample);Ray ray = WorldToMedium(Ray(rWorld.o, Normalize(rWorld.d), rWorld.tMax * rWorld.d.Length()));// Compute $[\tmin, \tmax]$ interval of _ray_'s overlap with medium boundsconst Bounds3f b(Point3f(0, 0, 0), Point3f(1, 1, 1));Float tMin, tMax;if (!b.IntersectP(ray, &tMin, &tMax)) return Spectrum(1.f);// Run delta-tracking iterations to sample a medium interactionFloat t = tMin;while (true) {t -= std::log(1 - sampler.Get1D()) * invMaxDensity / sigma_t;if (t >= tMax) break;if (Density(ray(t)) * invMaxDensity > sampler.Get1D()) {// Populate _mi_ with medium interaction information and returnPhaseFunction *phase = ARENA_ALLOC(arena, HenyeyGreenstein)(g);*mi = MediumInteraction(rWorld(t), -rWorld.d, rWorld.time, this,phase);return sigma_s / sigma_t;}}return Spectrum(1.f);
}

接下来实现计算光线与介质边界重叠的参数范围,即单位立方体[0,1]^3。这一步在技术上并不需要较为正确的操作,但通常是一个好主意:减少考虑的射线段的长度可以转化为相对较少的增量跟踪迭代。(适当的减少追踪光线的步数,以节约性能)<Compute [tmin, tmax] interval of ray’s overlap with medium bounds>

假设整个介质中的最大消光值为σt,max。每个delta追踪的迭代次数为i,其在均匀介质中每步执行的标准指数方程为:
\[ t_i=t_i-1-\frac{ln(1-\xi_{2i})}{\sigma_{t,max}} \]
其中t0=tmin。上述步骤会不断重复执行,直至符合以下两项停止准则之一:

  1. 如果ti>tmax,那么光线就离开了介质,不会产生交互作用,并且Medium::Sample()不会再采样散射事件。另外每次追踪循环终止的概率为σt(ti)/σt,max,也就是真实粒子的局部分数。 ξ2i+1为随机消耗量,每次迭代两个均匀样本。

不采样介质相互作用的概率等于射线[tmin, tmax]区间的透射率;因此在方程(15.7)中为采样权重βsurf返回1.0。

最终我们还必须实现tr()来计算沿射线方向其中一段的透射率。

由于通过介质的概率等于透射率,因此该随机变量具有正确的均值,可用于无偏蒙特卡罗积分。多次调用Tr()并对结果求平均值将会产生越来越精确的透射率估算值,尽管在实践中这样做通常成本太高。

<GridDensityMedium Method Definitions> +≡
Spectrum GridDensityMedium::Tr(const Ray &rWorld,
Sampler &sampler) const {
Ray ray = WorldToMedium(Ray(rWorld.o, Normalize(rWorld.d),
rWorld.tMax * rWorld.d.Length()));
<Compute [tmin, tmax] interval of ray’s overlap with medium bounds 896>
<Perform ratio tracking to estimate the transmittance value 898>
}
<Perform ratio tracking to estimate the transmittance value> ≡ 898
Float Tr = 1, t = tMin;
while (true) {
t -= std::log(1 - sampler.Get1D()) * invMaxDensity / sigma_t;
if (t >= tMax)
break;
Float density = Density(ray(t));
Tr *= 1 - std::max((Float)0, density * invMaxDensity);
}
return Spectrum(Tr);

采样相位函数

能够从相位函数描述的分布中提取样本也很有用——应用包括使用多重重要性采样来计算参与介质中的直接照明,以及对参与介质中的间接照明样本的散射方向进行采样。对于这些应用,PhaseFunction类必须实现Sample_p()。

注意,与BxDF的采样方法不同,Sample_p()不会同时返回相位函数的值及其PDF。相反,pbrt假设相位函数是用完全匹配其分布的PDFs采样的。结合相位函数必须归一化的要求(式(11.4)),一个返回值对两个值进行编码。当只需要PDF的值时,调用PhaseFunction::p()就足够了。

virtual Float Sample_p(const Vector3f &wo, Vector3f *wi,const Point2f &u) const = 0;

Henyey-Greenstein相位函数的pdf为分离的θ与φ组件,其中p(φ)=1/(2π)。主要的任务为采样cosθ。

对于Henyey–Greenstein分布的θ为:
\[ cos\theta=\frac{1}{2g}(1+g^2-(\frac{1-g^2}{1-g+2g\xi})^2) \]
如果g不为0,否则\(cos\theta=1-2\xi\)返回一个球体坐标方向的均匀采样。

Float cosTheta;
if (std::abs(g) < 1e-3)cosTheta = 1 - 2 * u[0];
else {Float sqrTerm = (1 - g * g) /(1 - g + 2 * g * u[0]);cosTheta = (1 + g * g - sqrTerm * sqrTerm) / (2 * g);
}

将角度坐标(cosθ,φ)转换为ωi方向。

Float sinTheta = std::sqrt(std::max((Float)0,
1 - cosTheta * cosTheta));
Float phi = 2 * Pi * u[1];
Vector3f v1, v2;
CoordinateSystem(wo, &v1, &v2);
*wi = SphericalDirection(sinTheta, cosTheta, phi, v1, v2, -wo);

体积光透射

这些采样构建块让各种各样的在参与介质中光线投射算法的实现成为可能。我们现在可以实现14.3.1节中EstimateDirect()中的内容,该函数处理与参与媒体相关的案例。

首先,在光线被采样后,如果在参与介质中的产生了散射事件,那么久必须为出射方向与入射光方向计算相位函数的值,以及用于多重重要性采样的采样方向的pdf值。因为我们假设相位函数时完美采样的,所以这些值都是相同的。

const MediumInteraction &mi = (const MediumInteraction &)it;
Float p = mi.phase->p(mi.wo, wi);
f = Spectrum(p);
scatteringPdf = p;

方向光计算需要取得一个相位函数分布样本,Sample_p()提供了这种功能;如上所述,它返回的值同时给出了相位函数的值和pdf的值。

const MediumInteraction &mi = (const MediumInteraction &)it;
Float p = mi.phase->Sample_p(mi.wo, &wi, uScattering);
f = Spectrum(p);
scatteringPdf = p;

路径追踪

VolPathIntegrator是一种SamplerIntegrator,它可以用来描述参与介质、次表面中散射与衰减。实现在integrators/volpath.h和integrators/volpath.cpp中。它跟PathIntegrator的一般结构非常相似。因此在这里我们只讨论这两个类之间的区别。

作为SamplerIntegrator,VolPathIntegrator主要实现了Li(),它的实现与PathIntegrator::Li()非常相似,只是在参与介质的相关部分有些改动。

Spectrum VolPathIntegrator::Li(const RayDifferential &r, const Scene &scene,Sampler &sampler,MemoryArena &arena,int depth) const {ProfilePhase p(Prof::SamplerIntegratorLi);Spectrum L(0.f), beta(1.f);RayDifferential ray(r);bool specularBounce = false;int bounces;// Added after book publication: etaScale tracks the accumulated effect// of radiance scaling due to rays passing through refractive// boundaries (see the derivation on p. 527 of the third edition). We// track this value in order to remove it from beta when we apply// Russian roulette; this is worthwhile, since it lets us sometimes// avoid terminating refracted rays that are about to be refracted back// out of a medium and thus have their beta value increased.Float etaScale = 1;for (bounces = 0;; ++bounces) {// Intersect _ray_ with scene and store intersection in _isect_SurfaceInteraction isect;bool foundIntersection = scene.Intersect(ray, &isect);// Sample the participating medium, if presentMediumInteraction mi;if (ray.medium) beta *= ray.medium->Sample(ray, sampler, arena, &mi);if (beta.IsBlack()) break;// Handle an interaction with a medium or a surfaceif (mi.IsValid()) {// Terminate path if ray escaped or _maxDepth_ was reachedif (bounces >= maxDepth) break;++volumeInteractions;// Handle scattering at point in medium for volumetric path tracerconst Distribution1D *lightDistrib =lightDistribution->Lookup(mi.p);L += beta * UniformSampleOneLight(mi, scene, arena, sampler, true,lightDistrib);Vector3f wo = -ray.d, wi;mi.phase->Sample_p(wo, &wi, sampler.Get2D());ray = mi.SpawnRay(wi);specularBounce = false;} else {++surfaceInteractions;// Handle scattering at point on surface for volumetric path tracer// Possibly add emitted light at intersectionif (bounces == 0 || specularBounce) {// Add emitted light at path vertex or from the environmentif (foundIntersection)L += beta * isect.Le(-ray.d);elsefor (const auto &light : scene.infiniteLights)L += beta * light->Le(ray);}// Terminate path if ray escaped or _maxDepth_ was reachedif (!foundIntersection || bounces >= maxDepth) break;// Compute scattering functions and skip over medium boundariesisect.ComputeScatteringFunctions(ray, arena, true);if (!isect.bsdf) {ray = isect.SpawnRay(ray.d);bounces--;continue;}// Sample illumination from lights to find attenuated path// contributionconst Distribution1D *lightDistrib =lightDistribution->Lookup(isect.p);L += beta * UniformSampleOneLight(isect, scene, arena, sampler,true, lightDistrib);// Sample BSDF to get new path directionVector3f wo = -ray.d, wi;Float pdf;BxDFType flags;Spectrum f = isect.bsdf->Sample_f(wo, &wi, sampler.Get2D(), &pdf,BSDF_ALL, &flags);if (f.IsBlack() || pdf == 0.f) break;beta *= f * AbsDot(wi, isect.shading.n) / pdf;DCHECK(std::isinf(beta.y()) == false);specularBounce = (flags & BSDF_SPECULAR) != 0;if ((flags & BSDF_SPECULAR) && (flags & BSDF_TRANSMISSION)) {Float eta = isect.bsdf->eta;// Update the term that tracks radiance scaling for refraction// depending on whether the ray is entering or leaving the// medium.etaScale *=(Dot(wo, isect.n) > 0) ? (eta * eta) : 1 / (eta * eta);}ray = isect.SpawnRay(wi);// Account for attenuated subsurface scattering, if applicableif (isect.bssrdf && (flags & BSDF_TRANSMISSION)) {// Importance sample the BSSRDFSurfaceInteraction pi;Spectrum S = isect.bssrdf->Sample_S(scene, sampler.Get1D(), sampler.Get2D(), arena, &pi, &pdf);DCHECK(std::isinf(beta.y()) == false);if (S.IsBlack() || pdf == 0) break;beta *= S / pdf;// Account for the attenuated direct subsurface scattering// componentL += beta *UniformSampleOneLight(pi, scene, arena, sampler, true,lightDistribution->Lookup(pi.p));// Account for the indirect subsurface scattering componentSpectrum f = pi.bsdf->Sample_f(pi.wo, &wi, sampler.Get2D(),&pdf, BSDF_ALL, &flags);if (f.IsBlack() || pdf == 0) break;beta *= f * AbsDot(wi, pi.shading.n) / pdf;DCHECK(std::isinf(beta.y()) == false);specularBounce = (flags & BSDF_SPECULAR) != 0;ray = pi.SpawnRay(wi);}}// Possibly terminate the path with Russian roulette// Factor out radiance scaling due to refraction in rrBeta.Spectrum rrBeta = beta * etaScale;if (rrBeta.MaxComponentValue() < rrThreshold && bounces > 3) {Float q = std::max((Float).05, 1 - rrBeta.MaxComponentValue());if (sampler.Get1D() < q) break;beta /= 1 - q;DCHECK(std::isinf(beta.y()) == false);}}ReportValue(pathLength, bounces);return L;
}

在每一次散射路径采样中,光线先与场景中的表面进行相交测试,来寻找最近的表面交点(如果有的话)。接下来通过调用Medium::Sample()方法来解析参与介质信息,如果一个在下一个路径上的顶点为交互介质,那么则初始化提供的MediumInteraction对象。在任一情况下,Sample()会返回一个光束透射率与表面、交互介质的采样pdf因子。
<Sample the participating medium, if present>

在散射介质密度非常大的场景中,由于Medium::Sample()通常会生成交互介质,所以在第一次寻找表面交点时回浪费性能。对于这种场景,一种更有效的实现方法是首先采样一个交互介质,在光线与场景图元相交之前更新对应光线的tMax值。反过来锁,由于要测试的光线通常相当短,所以表面相交测试将会更加高效。

取决于是否为该射线在参与介质或表面上的某个点进行交互采样,其中之一控制计算该点直接光照以及下一个采样方向。

由于之前章节定义的代码,UniformSampleOneLight()已经支持估算参与介质中某点的直接光照,所以我们只需要将MediumInteraction对象传递给这个函数就可以了,然后调用Sample_p()就可以得到光线离开介质的方向。

对于散射表面,执行的计算几乎与常规PathIntegrator完全相同,只是在采样时通过调用VisibilityTester::Tr()而不是VisibilityTester::Unoccluded()来合并从光源到表面交叉点的辐射衰减。

采样次表面反射函数

我们现在将要实现采样次表面散射公式(5.6.2)的技术,基于11.4章节的BSSRDF接口。我们的目标是估算:
\[ L_o=(p_o,\omega_o)=\int_A\int_{H^2(m)}S(p_o,\omega_i,\omega_i)L_i(p_i,\omega_i)\left|cos\theta_i\right|d\omega_idA \]
图15.6显示了计算该积分的复杂性。为了使用标准的蒙特卡洛估算这个公式,最后得出某点出射辐射量,我们需要一种采样在表面上的点pi以及计算这些点的入射辐射量的技术,

以及为每个采样点pi与入射方向计算指定BSSRDF中的S(po,ωo,pi,ωi)的有效方法。

VolPathIntegrator可以用来估算BSSRDF:
给定在表面上的一对点与一对方向

积分器可以用来计算
pi点处来自ωi方向的入射光与物体上po处向ωo方向射出的出射光的比值。其中出射光遵循light-carrying路径,在介质中发生多次散射事件。除了标准的路径跟踪或双向路径跟踪技术,许多其他的光传输算法也适用于这项任务。

然而许多半透明物体具有非常高的反照率,经典方法在这里效果不太好。例如,Jensen等人(2001b)测量了脱脂牛奶的散射特性,发现反照率为0.9987。

当所有的光线在介质中的每一次相互作用中都只发生散射而几乎没有被吸收时,光线很容易传输到离它最初进入介质处较为的位置。要计算出准确的结果,必须计算成百上千次的散射事件;考虑到牛奶的高反照率,100次散射事件后,87.5%的入射光仍然由一条路径携带,500次散射事件后为51%,1000次散射事件后为26%。

BSSRDF类实现了之类介质的散射现象,因此可以相当有效地呈现它们。BSSRDF::Sample_S()必须提供在内部散射后光线从表面射出的表面位置。

BSSRDF中两点以及方向的值是直接返回的,相关表面相交记录以及概率密度通过si与pdf变量返回。必须提供2个样本:一个1维样本用于分离采样决策(选择外形的特定光谱通道)和映射到si上的二维样本。很快我们就可以看到,BSSRDF实现了能够根据场景多边形跟踪光线来找到si,因此场景也作为参数提供。

采样可分离的BSSRDF

回想一下11.4.1节中介绍的简化假设,该假设将BSSRDF分解为独立采样的空间与方向组件。公式11.6定义S为单个空间项与一对与入射方向与出射方向相关的方向项的点乘。
\[ S(p_o,\omega_o,p_i,\omega_i)=(1-F_r(cos\theta_o))S_p(p_o,p_i)S_\omega(\omega_i) \]
空间项Sp进一步简化为径向外形函数Sr:
\[ S_p(p_o,p_i)=S_r(\mid\mid p_o-p_i \mid\mid) \]
SeparableBSSRDF类实现了采样接口,它适用于径向外形函数Sr。TabulatedBSSRDF类(15.4.2节)派生自该类,并提供特定外形的的高效估算与精准重要性采样功能。

回到公式15.14中,如果我们假设BSSRDF只采样透射过表面边界的光线,其中透射概率为(1-Fr(cos θo)),那么这里不需要做(1-Fr(cos θo))那部分。这是调用代码的合理期望,因为这种方法提供了良好的蒙特卡罗效率。

Sp与Sω项通过调用SeparableBSSRDF::Sample_Sp()获得,并且这个函数还会返回位置变量si。

Spectrum SeparableBSSRDF::Sample_S(const Scene &scene, Float u1,const Point2f &u2, MemoryArena &arena,SurfaceInteraction *si, Float *pdf) const {ProfilePhase pp(Prof::BSSRDFSampling);Spectrum Sp = Sample_Sp(scene, u1, u2, arena, si, pdf);if (!Sp.IsBlack()) {// Initialize material model at sampled surface interactionsi->bsdf = ARENA_ALLOC(arena, BSDF)(*si);si->bsdf->Add(ARENA_ALLOC(arena, SeparableBSSRDFAdapter)(this));si->wo = Vector3f(si->shading.n);}return Sp;
}

如果采样某个位置成功,那么方法使用SeparableBSSRDFAdapter类的实例来初始化si->bsdf,它代表了方向项Sω(ωi)是一个BxDF。虽然这个BxDF并不真正依赖于出射方向si->wo,但是我们仍然需要用一个虚拟方向来初始化它。

SeparableBSSRDFAdapter类是一个精简的SeparableBSSRDF::Sw()包裹器。回想一下公式11.7中的Sω被定义为一个diffuse-like项,它被归一化的菲尼尔投射缩放。因此SeparableBSSRDFAdapter将自己分成BSDF_DIFFUSE并且使用BxDF::Sample_f()提供的默认余弦权重采样程序。

类似于折射BSDFs,与光线传输模式相关的缩放因子必须应用f()返回Sω项。

    Spectrum f(const Vector3f &wo, const Vector3f &wi) const {Spectrum f = bssrdf->Sw(wi);// Update BSSRDF transmission term to account for adjoint light// transportif (bssrdf->mode == TransportMode::Radiance)f *= bssrdf->eta * bssrdf->eta;return f;}

对空间项进行采样,我们需要一个将2维分布函数映射到任意表面的方法,该方法使用一个参数化出射位置附近的表面。获取一个参数化的直接概念是测地线。但是找到与估算他们并不简单,并且需要对支持的每个形状进行大量的实现工作。我们使用了一种更简单的方法,即使用光线跟踪将径向外形Sr映射到场景几何上。

15.8的代码说明基本思想:位置变量po与该位置的法线变量no可以定义一个近似平面。使用2维极坐标,我们首先采样围绕着po的一个方位φ和一个半径值r,并且通过一个垂直偏移射线与图元相交,生成坐标pi的方式将这个坐标映射到真正的表面上。SeparableBSSRDF类只支持径向对称的概要函数;因此φ是从一个均匀分布在[0,2π)得到的以及r是根据径向剖面函数Sr得到的。

但这个基础方法仍有一些困难:

  1. 径向剖面Sr在波长上不一定是均匀的——实际上,在不同的光谱通道之间,平均自由路径可以相差几个数量级。
  2. 如果表面多边形与平面极不近似,以及dot(no,ni)≈0。其中ni为pi点处的法线。那么探测射线将会命中处于掠射角的表面。所以pi与数值相对较高的S(po,ωo, pi)被采样的概率可能会低。导致结果会有很多噪点。
  3. 探测射线可能在其方向与同一表面进行多次相交,而这些会对反射辐射量产生贡献。

前两个问题可以用一种熟悉的方法解决:即引入额外的定制抽样分布,并使用多重重要性抽样将它们组合起来。第三个问题之后再处理。

我们使用不同波长的采样技术来处理光谱变化并且每一种技术都额外复制3次不同投射坐标的根据基础局部结构向量,最终得到3*pectrum::nSamples样本的采样技术。这确保了每一个S点的合理相交概率都为一个不可忽略值。这种技术组合是在SeparableBSSRDF::Sample_Sp()中实现的。

Spectrum SeparableBSSRDF::Sample_Sp(const Scene &scene, Float u1,const Point2f &u2, MemoryArena &arena,SurfaceInteraction *pi, Float *pdf) const {ProfilePhase pp(Prof::BSSRDFEvaluation);// Choose projection axis for BSSRDF samplingVector3f vx, vy, vz;if (u1 < .5f) {vx = ss;vy = ts;vz = Vector3f(ns);u1 *= 2;} else if (u1 < .75f) {// Prepare for sampling rays with respect to _ss_vx = ts;vy = Vector3f(ns);vz = ss;u1 = (u1 - .5f) * 4;} else {// Prepare for sampling rays with respect to _ts_vx = Vector3f(ns);vy = ss;vz = ts;u1 = (u1 - .75f) * 4;}// Choose spectral channel for BSSRDF samplingint ch = Clamp((int)(u1 * Spectrum::nSamples), 0, Spectrum::nSamples - 1);u1 = u1 * Spectrum::nSamples - ch;// Sample BSSRDF profile in polar coordinatesFloat r = Sample_Sr(ch, u2[0]);if (r < 0) return Spectrum(0.f);Float phi = 2 * Pi * u2[1];// Compute BSSRDF profile bounds and intersection heightFloat rMax = Sample_Sr(ch, 0.999f);if (r >= rMax) return Spectrum(0.f);Float l = 2 * std::sqrt(rMax * rMax - r * r);// Compute BSSRDF sampling ray segmentInteraction base;base.p =po.p + r * (vx * std::cos(phi) + vy * std::sin(phi)) - l * vz * 0.5f;base.time = po.time;Point3f pTarget = base.p + l * vz;// Intersect BSSRDF sampling ray against the scene geometry// Declare _IntersectionChain_ and linked liststruct IntersectionChain {SurfaceInteraction si;IntersectionChain *next = nullptr;};IntersectionChain *chain = ARENA_ALLOC(arena, IntersectionChain)();// Accumulate chain of intersections along rayIntersectionChain *ptr = chain;int nFound = 0;while (true) {Ray r = base.SpawnRayTo(pTarget);if (r.d == Vector3f(0, 0, 0) || !scene.Intersect(r, &ptr->si))break;base = ptr->si;// Append admissible intersection to _IntersectionChain_if (ptr->si.primitive->GetMaterial() == this->material) {IntersectionChain *next = ARENA_ALLOC(arena, IntersectionChain)();ptr->next = next;ptr = next;nFound++;}}// Randomly choose one of several intersections during BSSRDF samplingif (nFound == 0) return Spectrum(0.0f);int selected = Clamp((int)(u1 * nFound), 0, nFound - 1);while (selected-- > 0) chain = chain->next;*pi = chain->si;// Compute sample PDF and return the spatial BSSRDF term $\Sp$*pdf = this->Pdf_Sp(*pi) / nFound;return this->Sp(*pi);
}

我们从选择一个投影轴开始,注意,当表面接近平面时,

沿着法线变量SeparableBSSRDF::ns进行投影显然是最佳策略,探测射线

沿标准的可分离bssrdf::ns投影显然是最佳的采样策略,因为沿另外两个轴的探测射线很可能会错过表面。因此,我们将相当大一部分(50%)的样本预先分配给垂直光线。另一半在沿着切线向量投影SeparableBSSRDF::ss和SeparableBSSRDF::ts之间平分。选择的三个轴坐标系统存储在vx,vy与vz中,之后,按照惯例以z轴为准测量球坐标的角度θ。

在这个分离采样操作结束后,我们缩放并且偏移u1,以便额外的采样操作将其作为统一变量重复使用。

其他两个轴的代码也是相似的,因此没有包含在这里。接下来我们一致选择一个光谱通道,并再次缩放u1。

2D径向采样操作时是在极坐标下使用SeparableBSSRDF::Sample_Sr()。这个方法返回一个否决半径。

将半径采样方法SeparableBSSRDF::Sample_Sr()与及其关联的密度函数SeparableBSSRDF::Pdf_Sr()声明为纯虚函数;它会在TabulatedBSSRDF中实现,下一章将会详细介绍。

因为轮廓线下降得很快,我们对远离po2
的坐标pi不感兴趣。为了减少计算光线追踪所花费步长,探测射线将限制在po为中心的半径为rmax的球体内。另外,调用SeparableBSSRDF::Sample_Sr()可以确定rmax。

假设该函数实现了完美重要性采样方案是基于反演法(第13.3.1节),Sample_Sr()将一个采样值x映射到一个包含散射能量分数x的球体的半径上。

这里我们设置rMax使图15.8中的球体包含99.9%的散射能量。当r位于rmax之外时,会使得采样失败—但这使得探测射线保持较短距离,从而显著提高运行时性能。给定r和rmax,那么探测射线与半径为rmax的球体相交的长度为:
\[ l=2\sqrt{r^2_{max}-r^2} \]

给定采样的极坐标值,我们可以计算出世界空间下位于球体边界的射线的原点与它离开球体的目标点pTarget。

实际上,探测射线上可能有不止一个交点,我们想把它们都收集到这里。我们将为所有找到的交互创建一个链表。

让我们维护这个列表IntersectionChain,再一次使用MemoryArena,让节点空间分配变得高效。

我们现在开始在在球体中沿着区间寻找交点,列表的尾部节点的SurfaceInteraction被每个交点信息初始化,以及更新基础交互,以便可以在相交表面的另一侧生成下一条射线。

当追踪光线到图元表面附近时的采样点时,忽略场景中其他图元的交点是很重要的。(这里隐含了一个假设:积分器将会处理图元间的散射,并且BSSRDF应该被限制为只处理当个图元的散射)这里的实现使用了相应的材质指针作为判断是否在相同图元上相交的代理。有效的交叉点被附加到链上,变量nFound在循环终止时记录它们的总数。

当有这些交点集后,我们必须选择其中之一,因为Sample_Sp()只能返回一个pi。下面的代码最后一次使用变量u1,来选择均匀概率表其中一个点。

最后我们可以调用SeparableBSSRDF::Pdf_Sp()来估算组合所有抽样策略的pdf。即计算从IntersectionChain中选择的pi的分离概率后在除以nFound。最后,这个方法返回Sp(pi)的值。

SeparableBSSRDF::Pdf_Sp()返回每单位面积总数为3*Spectrum::nSamples的采样点pi的概率。该采样技术也可用于bssrdf::Sample_Sp()。

首先,nLocal使用在pi点出的表面法线来初始化。dLocal使用po-pi来初始化。两者均使用po处的局部坐标表示。

为了确定组合起来的pdf,对于所有技术来说我们必须查询采样辐射剖切面半径所匹配的那一对(po,pi)的概率。这个半径是在2D中测量的,因此依赖于所选择的投影轴(图15.12)。rProj变量记录垂直于ss、ts和ns的投影的半径。

其余的实现只是简单地循环所有的光谱通道和投影轴的组合,并总结选择每种技术的概率及其投影到po表面下的面积密度的乘积。

警告读者,可能需要注意上述定义中有一点不一致:选择(nFound)其中一个交点的概率在SeparableBSSRDF:: Sample_Sp()中就应该已经在SeparableBSSRDF::Pdf_Sp()中计算了。而不是在计算采样pdf与返回特定BSSRDF Sp项时计算。在实际应用中,检测到的交点数量随着投影轴和光谱通道的不同而变化;要在PDF计算中正确地考虑到这一点,需要计算沿共3*Spectrum::nSamples探测光线的每个样本交点的数量!我们忽略了这个问题,以少量误差为代价以此更加效率。

采样TabulatedBSSRDF

前一章节完成了BSSRDF采样除了Pdf_Sr()与Sample_Sr()外的讨论。这些方法在SeparableBSSRDF中声明为纯虚拟函数。TabulatedBSSRDF将实现他们。

TabulatedBSSRDF::Sample_Sr()样本半径值与剖面半径函数Sr成比例关系。回忆一下11.4.2节,剖面隐含了基于当前表面位置出的反照率p,并且TabulatedBSSRDF提供插值估算Sr(ρ,r)使用二维张量点积样条为基础的函数。于是TabulatedBSSRDF::Sample_Sr()决定了给定光谱通道的反照率p,并且取得仅存的一维函数Sr(p)的样本比例。如果在通道ch上既没有散射又没有吸收那么产生样本就会失败。

在11.4.2节中,与FourierBSDF实现有相当多的重叠。这里的采样操作实际上简化为调用SampleCatmullRom2D(),该调用以前在FourierBSDF::Sample_f()中使用过。

Float TabulatedBSSRDF::Sample_Sr(int ch, Float u) const {
if (sigma_t[ch] == 0)return -1;
return SampleCatmullRom2D(table.nRhoSamples, table.nRadiusSamples,
table.rhoSamples.get(), table.radiusSamples.get(),
table.profile.get(), table.profileCDF.get(),
rho[ch], u) / sigma_t[ch];
}

这个函数基于在初始化BSSRDFTable时预先计算好的cdf数组。

Pdf_Sr()返回通过Sample_Sr()获得的样本的pdf。它估算轮廓函数除以归一化常量ρeff的值(公式11.11)。

开头类似于TabulatedBSSRDF::Sr()中的样条计算代码。这段代码计算样条权重来插值在通道ch上的BSSRDF密度,与该方法中计算样条权值来插值通道ch上的BSSRDF匹配,但如果光学半径超出样条所表示的范围,该方法立即返回零。

Float TabulatedBSSRDF::Pdf_Sr(int ch, Float r) const {// Convert $r$ into unitless optical radius $r_{\roman{optical}}$Float rOptical = r * sigma_t[ch];// Compute spline weights to interpolate BSSRDF density on channel _ch_int rhoOffset, radiusOffset;Float rhoWeights[4], radiusWeights[4];if (!CatmullRomWeights(table.nRhoSamples, table.rhoSamples.get(), rho[ch],&rhoOffset, rhoWeights) ||!CatmullRomWeights(table.nRadiusSamples, table.radiusSamples.get(),rOptical, &radiusOffset, radiusWeights))return 0.f;// Return BSSRDF profile density for channel _ch_Float sr = 0, rhoEff = 0;for (int i = 0; i < 4; ++i) {if (rhoWeights[i] == 0) continue;rhoEff += table.rhoEff[rhoOffset + i] * rhoWeights[i];for (int j = 0; j < 4; ++j) {if (radiusWeights[j] == 0) continue;sr += table.EvalProfile(rhoOffset + i, radiusOffset + j) *rhoWeights[i] * radiusWeights[j];}}// Cancel marginal PDF factor from tabulated BSSRDF profileif (rOptical != 0) sr /= 2 * Pi * rOptical;return std::max((Float)0, sr * sigma_t[ch] * sigma_t[ch] / rhoEff);
}

剩下的实现非常类似于使用张量样条插值来设置BSSRDF值Sr[ch]的代码,除了这里,我们也对tabulation中的ρeff与其他类包含的这个变量进行插值。

次表面路径追踪

我们现在有能力将蒙特卡洛积分应用到5.6.2节中的广义散射方程(5.11)中。
\[ L_o(p_o,\omega_o)\approx\frac{S(p_o,\omega_o,P_i,\omega_i)(L_d(p_i,\omega_i)+L_i(p_i,\omega_i))\left|cos\theta_i\right|}{p(P_i)p(\omega_i)} \]
其中Ld为直接入射辐射量,Li为间接入射辐射量。样本(pi,ωi)生成于以下两个步骤:

  1. 给定po与ωo,调用BSSRDF::Sample_S()返回坐标pi,其分布与遵照pi的S临界分布相同。
  2. 接下来我们采样入射方向ωi。记住SSRDF::Sample_S()接口故意让这两个步骤分开,而不是同事产生pi与ωi,它返回一个特殊BSDF实例,它的bsdf场用于采样方向步骤。这种方法不会丢失通用性:返回的BSDF可以完全是任意的,并且显式地允许依赖于BSSRDF::Sample_S()中计算的信息。好处是,我们可以重用大量现有的基础架构来计算BSDF和Li的点乘积分。

PathIntegrator类的代码段将调用以下代码来计算这个估算。

<Account for subsurface scattering, if applicable> ≡ 877
if (isect.bssrdf && (flags & BSDF_TRANSMISSION)) {
<Importance sample the BSSRDF 915>
<Account for the direct subsurface scattering component 915>
<Account for the indirect subsurface scattering component 916>
}

当BSSRDF采样实例启动时,路径追踪器会首先调用BSSRDF::Sample_S()来生成pi,并在成功时将得到的采样权合并到吞吐量权变量beta中。

<Importance sample the BSSRDF> ≡ 915
SurfaceInteraction pi;
Spectrum S = isect.bssrdf->Sample_S(scene, sampler.Get1D(),
sampler.Get2D(), arena, &pi, &pdf);
if (S.IsBlack() || pdf == 0)
break;
beta *= S / pdf;

也因为BSSRDF::Sample_S()初始化pi处的BSDF,描绘它依赖于ωi上的S,我们可以重用现有的直接光照计算架构。只需要一行代码就可以计算pi处的直接光照对po处反射辐射量的贡献。

<Account for the direct subsurface scattering component> ≡ 915
L += beta * UniformSampleOneLight(pi, scene, arena, sampler);

类似地在新采样入射点的间接光照与在PathIntegrator计算BSDF的间接光照几乎相同,只是pi用于下一个路径顶点而不是isect。

<Account for the indirect subsurface scattering component> ≡ 915
Spectrum f = pi.bsdf->Sample_f(pi.wo, &wi, sampler.Get2D(), &pdf,
BSDF_ALL, &flags);
if (f.IsBlack() || pdf == 0)
break;
beta *= f * AbsDot(wi, pi.shading.n) / pdf;
specularBounce = (flags & BSDF_SPECULAR) != 0;
ray = pi.SpawnRay(wi);

转载于:https://www.cnblogs.com/blueroses/p/10563420.html

PBRT笔记(14)——光线传播2:体积渲染相关推荐

  1. Volumetric Integration(体积渲染——烟雾)

    VolumetricIntegration 作者:SebH,网址:https://www.shadertoy.com/view/XlBSRz 关键词:体积渲染.参与介质 介绍: 这里有一个演示,介绍了 ...

  2. 从NeRF到Plenoxels学习 透明度 体积渲染方程

    Plenoxels :Method 稀疏体素网格,每个体素(voxel corner就是voxel,他这里写的容易让人误解成体元的顶点)对每个色彩通道储存一个透明度标量sigma,以及一个球谐函数向量 ...

  3. node.js学习笔记14—微型社交网站

    node.js学习笔记14-微型社交网站 1.功能分析 微博是以用户为中心,因此需要有注册和登录功能. 微博最核心的功能是信息的发表,这个功能包括许多方面,包括:数据库访问,前端显示等. 一个完整的微 ...

  4. Linux学习笔记14

    Linux学习笔记14 Linux学习笔记14 DNS服务搭建 基本介绍 使用bind搭建dns服务 增加一个域名zone 配置DNS转发 配置主从 测试主从同步 后续课程 DNS服务搭建 基本介绍 ...

  5. 【转】 C#学习笔记14——Trace、Debug和TraceSource的使用以及日志设计

    [转] C#学习笔记14--Trace.Debug和TraceSource的使用以及日志设计 Trace.Debug和TraceSource的使用以及日志设计   .NET Framework 命名空 ...

  6. 台大李宏毅Machine Learning 2017Fall学习笔记 (14)Unsupervised Learning:Linear Dimension Reduction

    台大李宏毅Machine Learning 2017Fall学习笔记 (14)Unsupervised Learning:Linear Dimension Reduction 本博客整理自: http ...

  7. LEADTOOLS V20,医学影像浏览器中3D体积渲染控件

    2018 年 9 月 ,LEAD Technologies 发布了 LEADTOOLS V20 破解版本的更新,对整个产品线进行了许多速度和精度的优化. 除了更高效的 OCR,文件格式编解码器和图像显 ...

  8. [AI 笔记] BP反向传播算法

    [AI 笔记] BP反向传播算法 说明 BP算法 链式法则 计算图 简单例子 前向传播 反向传播 聚焦计算节点,深入理解 计算节点的代码实现 例子 各种门的功能 多元链式法则 向量化BP算法 参考资料 ...

  9. 笔记14 灯光、预设体、3d物体的事件、碰撞体、刚体、炸箱子、触发器、物理材质、射线

    笔记14 灯光.预设体.3d物体的事件.碰撞体.刚体.炸箱子.触发器.物理材质.射线 灯光 基本项 烘焙 灯光的种类 Lighting窗口 预设体 了解预设体 利用代码操作预设体 实例化预设体Inst ...

最新文章

  1. 好看的dialog,sweet Alert Dialog 导入Android Studio
  2. 【GAN】如何生动有趣地对GAN进行可视化?Google的GAN Lab推荐你了解一下
  3. logrotate管理nginx日志文件
  4. centos7安装svn1.9解决与svn服务器版本不一致的问题
  5. 网关 Spring Cloud Gateway
  6. 算法动画 - 理解函数曲线
  7. Python结合Shell/Hadoop实现MapReduce
  8. php数组的几个函数和超全局变量
  9. backgroud背景色样式兼容ie8 rgba()用法
  10. 硬件电路设计之与非门触发器74HC30和74HCT20
  11. cdr2022更新24.1版CorelDRAW2022稳定版
  12. 初中英语语法(004)-代词
  13. 考研人最烦的6句话,教你霸气回怼!
  14. 抖音究竟是通过什么让我男朋友他爸成功刷到了我妈?,历经30天
  15. 鸿蒙系统几年不卡,华为手机别乱选,这四款机型基本三年不卡,并且可以升级鸿蒙系统...
  16. pikachu站点所有问题
  17. UDEC v4.00.153 1CD(完全安装版)
  18. hive安装 初始化报错Error: Duplicate key name ‘PCS_STATS_IDX‘ (state=42000,code=1061)
  19. 一文搞懂什么是SaaS、BaaS、PaaS和IaaS
  20. 怎么在Github上单独下载一个文件或文件夹

热门文章

  1. 看到这么多人都很焦虑,我就放心了
  2. [matlab]10种经典的时间序列预测模型
  3. ABP VNext学习日记29
  4. OpenCV-Python图像运算变换处理:开运算和闭运算以及不同核矩阵的影响分析
  5. Java 给PowerPoint文档设置背景颜色和背景图片
  6. 【MacBookPro】苹果电脑如何在/Users目录下创建一个管理员可共享的新目录【】
  7. 高精地图 | 看他起高楼,眼看他宴宾客,眼看他楼塌了?
  8. 笔记本安装ubantu系统
  9. python初学者感想-学习python的感想
  10. 微信小程序「渲染层网络层错误」解决方法