本文出自Physcially Base Rendering概括翻译下面是翻译中遇到的一些数学性问题级扩展数学推论: 数学推导部分: 推荐网站 http://www.brucelindbloom.com/index.html?Eqn_Spect_to_XYZ.html 下面数学推论引用自 http://www.brucelindbloom.com/index.html?Eqn_Spect_to_XYZ.html 的翻译 XYZ三色刺激值(tristimulus values) ![]() CIE(标准观察者颜色扩展函数):为了模拟人类的三个视锥细胞对世界上物体捕捉到的颜色,由于SPD转换为显示屏上的RGB不是线性(有负值)【SPD转换为显示屏中的RGB(带有人类对红绿蓝三种颜色的刺激)】,所以CIE提出了一套测量颜色的标准条件,把SPD先转换为线性的空间。(具体先前说明过) ![]() 这里面用到了积分,但因为匹配函数是非数学概述的(上图的3个曲线),所以这个公式不可用,我们可以另找方法,用采样和线性的叠加方法计算XYZ: ![]() 这里的下表i代表第几个刻度的采样。采样间隔(spacing)一般是1到20nm,采样空间是整个可见光波段。 对于一个发射源(例如监视器)的给定的光谱功率分布P(λ), XYZ三刺激值是这样计算的: ![]() 其中x y和z是CIE标准的观察者函数(2度或10度,取决于应用)。 积分计算在可见光谱(约360至830nm)上。 但在实际中,这些积分中的函数要么是由实验得到的,要么是由测量得到的。因此,没有数学方程来表示它们。相反,它们以离散样本的形式存在,因此积分被求和代替: ![]() 这里有使用的波长跨度和样本间距的问题。标准观测函数在360到830nm范围内大于零。光谱功率分布范围可能小于这一范围,这取决于所使用的测量设备。样品间距一般在1到20nm之间。为了进行求和,标准观测函数的跨度和间距必须与样本的谱功率分布相匹配。重采样,插值,甚至可能是外推都被用来完成这一任务。 从光谱数据计算XYZ(反射和透射情况) 涉及反射样品和透射样品的情况与上述发射样品有强烈的相似之处,但又有重要的区别。 样本的光谱功率分布P(λ)被样本的光谱反射率(或透过率)S(λ)乘以参考光源的光谱功率分布乘以参考光源的光谱功率分布I(λ)所取代。 ![]() 为什么需要参考光源? 如果你考虑发射的情况,在一个黑暗的房间里观察颜色样本,你可以看到它的颜色,因为它产生了光。 然而,当你在同样的环境中观察反射(或透射)样本时,你什么也看不见,因为这样的样本不会产生光。 看到它的唯一方法是先照亮它。 很明显,样品颜色的外观受它所接收到的光照类型的影响。 为了使颜色样本的XYZ值明确,光源必须以某种方式被识别并与之关联。 这就产生了一个问题,因为有无限多的可能光源。 所以使用参考光源代替实际光源。 常用的参考光源有C、D50、D65等。 XYZ的引用光源只是通过引用来创建的。 例如,“D50”指的是整个标准谱功率分布。 注意,上面的发射情况不涉及参考光源,因为样品本身提供了光能。 同样,积分通常被求和代替,原因与发射情况相同: ![]() XYZ和RGB之间的互相转换 XYZ转换到RGB: ![]() (此矩阵只使用sRGB中的define 对与右边的XYZ也要做规范法) 得到之后的线性空间之后,一般渲染器都会在线性空间中进行光照计算,但是要把最后的渲染结果输出,就要对每个像素做Gamma: ![]() 校正后的sRGB是单位化,各个分量取值范围是【0.0,1.0】,输出要乘以255. RGB到XYZ 当输入的RGB是sRGB时,需要做逆gamma校正,公式如下: ![]() 得到的线性空间的RGB值后,可以用如下公式转换到XYZ: ![]() 5 颜色和辐射度量学(Color and Radiometry) 为了更精确的描述光是如何被表示的如何采样来计算图像,我们必须首先在辐射学中建立(establish)一些背景-辐射学是研究电磁辐射在环境中的传播,在渲染中特别有趣的是电磁辐射的波长(λ),大约在380nm和780nm之间,这是人类可见光的范围,较低的波长(λ≈400nm)为淡蓝色,中间波长(λ≈550nm)为绿色,较高波长(λ≈650nm)为红色。 flux,intensity,irradiance,radiance 通量、强度、辐照度、辐射度 在本章中,我们将介绍四个描述电磁辐射的关键量:通量、强度、辐照度和辐射度。这些辐射量都由它们的光谱功率分布(SPD)来描述,这是波长的分布函数,描述了每个波长上的光量。在第5.1节中定义的Spectrum类,用于表示pbrt中的spd。 现实世界物体的spd可能相当复杂;图5.1为荧光发射光谱分布图和柠檬皮反射率光谱分布图。使用spd进行计算的渲染器需要一种紧凑、高效和准确的方式来表示这样的函数。在实践中,需要在这些品质之间做出一些取舍。 通过寻找适合的基函数来表示SPDs,可以建立一个研究这些问题的一般框架。基函数背后的思想是将无限维的可能SPD函数空间映射到低维的系数空间。例如,用一个常数函数 B(λ) = 1作为基底,在这个基低函数中,任意SPD可以用一个等于其平均值的系数c来表示,因此其近似值将是 cB(λ) = c。这显然是一个糟糕的近似,因为大多数SPDs比这个单基函数精确表示的复杂得多。 许多不同的基函数已被用于计算机图形学中的光谱表示;不同的基函数可以提供许多不同的取舍等关键操作的复杂性将任意SPD转化为更适合的系数。 CofficientSpectrum实现(CofficientSpectrum Implmentation) 本章实现了2种不同的SPD表示方法,RGBSpectrum和SampledSpectrum。前者是计算机图像学中常用的RGBSpectrum,后者则是通过指定波长区间上若干离散的采样点来表示SPD。 //光谱类的声明 template <int nSpectrumSamples> //nSpectrumSamples是采样样本的数量 class CoefficientSpectrum{ public: CoefficientSpectrum(Float v = 0.f){ for(int i=0; i< nSpectrumSamples; i++) c[i] = v; } CoefficientSpectrum &operator+=(const CoefficientSpectrum &s2) { for (int i = 0; i < nSpectrumSamples; ++i) c[i] += s2.c[i]; return *this; } CoefficientSpectrum operator+(const CoefficientSpectrum &s2) const { CoefficientSpectrum ret = *this; for (int i = 0; i < nSpectrumSamples; ++i) ret.c[i] += s2.c[i]; return ret; } //..省略 //... ... //..省略 protected: Float c[nSpectrumSamples];//光谱值 } 5.2 SampleSpectrum实现(RGBSpectrum Implmentation) SampleSpectrum使用基类CoefficientSpectrum 来表示在起始波长和结束波长之间具有均匀间隔采样的SPD。波长范围从400 nm到700 nm,这是人类视觉系统最敏感的视觉光谱范围。样本的数量为60个,通常足够准确地表示用于渲染的复杂spd。因此,第一个样本代表波长范围[400,405),第二个样本代表波长范围[405, 410),以此类推。可以根据需要在这里轻松地更改这些值。 static const int sampledLambdaStart = 400; //采样光谱 static const int sampledLambdaEnd = 700;//采样光谱 static const int nSpectralSamples = 60;//采样个数 我们常常会把光谱数据作为一组(λi,vi)样本来提供第i个样本在波长λi处的值vi。一般来说, 5.2.1 XYZ Color(XYZ Color) 人类视觉系统的一个显著特性使得仅用三个浮点数就可以表示人类感知的颜色成为可能。色彩感知的三刺激理论认为,所有可见的spd都可以用三个值xλ、yλ和zλ准确地表示出来。给定一个自发光SPD S(λ),这些值是通过将其乘积与光谱匹配曲线X(λ)、Y(λ)和Z(λ)进行积分计算得到的: ![]() 这些曲线由国际委员会Éclairage (CIE)标准机构在一系列人体试验对象后确定,并绘制在图5.3中。研究认为,这些匹配曲线与人眼视网膜中三种视锥细胞的反应大致相似。值得注意的是,具有显著不同分布的spd可能具有非常相似的xλ、yλ和zλ值。对于人类观察者来说,这样的spd在视觉上看起来是一样的。这样的光谱对被称为条件等色。 ![]() 这将我们带到了一个关于光谱功率分布表示的微妙点。大多数颜色空间试图模拟人类可见的颜色,因此只使用三个系数,利用颜色感知的三刺激理论。虽然XYZ可以很好地表示为人类观察者显示的给定SPD,但对于光谱计算来说,它并不是一组特别好的基函数。例如,虽然XYZ值可以很好地分别描述柠檬皮肤或荧光灯的感知颜色(回想图5.1),它们各自的XYZ值的乘积可能会给出一个明显不同的XYZ颜色,而XYZ值是通过将它们的spd的更精确表示相乘,然后计算XYZ值来计算的。 pbrt提供了标准的X(λ)、Y(λ)和Z(λ)响应曲线的值,从360 nm到830 nm以1nm的增量采样。下面数组中第i个样本的波长由CIE_lambda的第i个元素给出;用这种方式显式地表示样本的波长,可以很容易地将XYZ样本传递给AverageSpectrumSamples()等函数,这些函数将波长数组作为参数。 static const int nCIESamples = 471; extern const Float CIE_X[nCIESamples]; extern const Float CIE_Y[nCIESamples]; extern const Float CIE_Z[nCIESamples]; extern const Float CIE_lambda[nCIESamples]; SampledSpectrum使用这些样本在其光谱表示中计算XYZ匹配曲线(即,本身为SampledSpectrum)。 给定SampledSpectrum的波长范围和样本数量,计算每个样本的匹配函数值只需要计算样本的波长范围并使AverageSpectrumSamples()例程即可。 //将SPD按照光谱波长按照从小到大排序 void SortSpectrumSamples(Float *lambda, Float *vals, int n){ std::vector<std::pair<Float,Float>> sortVec; sortVec.reserve(n); for(int i = 0; i<n;++i) sortVec.push_back(std::make_pair(lambda[i], vals[i])); std::sort(sortVec.begin(), sortVec.end()); for(int i=0;i<n;++i) { lambda[i] = sortVec[i].first; vals[i] = sortVec[i].second; } } //判断是否从小到达排序 bool SpectrumSamplesSorted(const Float* lambda,const Float *vals, int n) { for(int i=0; i<n-1;++i) { if(lambda[i] > lambda[i + 1]) return false return true; } } // 计算出SPD平均值 Float AverageSpectrumSamples(const Float *lambda,const Float *vals, int n, Float lambdaStart,Float lambdaEnd){ //处理越界和单个SPD采样 if(lambdaEnd <= lambda[0]) return vals[0]; if(lambdaStart >= lambda[n -1]) return vals[n-1]; if(n == 1) return vals[0]; Float sum = 0;
//加入样本之前或之后的采样贡献 if (lambdaStart < lambda[0]) sum += vals[0] * (lambda[0] - lambdaStart); if (lambdaEnd > lambda[n - 1]) sum += vals[n - 1] * (lambdaEnd - lambda[n - 1]);
//循环到第一个采样样本处 int i = 0; while (lambdaStart > lambda[i + 1]) ++i;
//循环添加计算样本的贡献值 auto interp = [lambda, vals](Float w, int i){ return Lerp((w - lambda[i]) / (lambda[i + 1] - lambda[i]), vals[i], vals[i + 1]); } for (; i + 1 < n && lambdaEnd >= lambda[i]; ++i) { Float segLambdaStart = std::max(lambdaStart, lambda[i]); Float segLambdaEnd = std::min(lambdaEnd, lambda[i + 1]); sum += 0.5 * (interp(segLambdaStart, i) + interp(segLambdaEnd, i)) * (segLambdaEnd - segLambdaStart); } return sum / (lambdaEnd - lambdaStart); } class SampledSpectrum : public CoefficientSpectrum<nSpectralSamples>{ public: //给定一定数量的(nSpectralSamples)采样数SPD 计算出SPD平均值 static SampledSpectrum FromSampled(const Float *lambda,const Float *v, int n) { if(!SpectrumSamplesSorted(lambda, v, n)) { std::vector<Float> slambda(&lambda[0], &lambda[n]); std::vector<Float> sv(&v[0], &v[n]); SortSpectrumSamples(&slambda[0],&sv[0], n); return FromSampled(&slambda[0],&sv[0], n); } SampledSpectrum r; for(int i=0; i < nSpectralSamples; ++i) { Float lambda0 = Lerp(Float(i) / Float(nSpectralSamples), sampledLambdaStart, sampledLambdaEnd); Float lambda1 = Lerp(Float(i + 1) / Float(nSpectralSamples), sampledLambdaStart, sampledLambdaEnd); r.c[i] = AverageSpectrumSamples(lambda, v, n, lambda0, lambda1); } } static void Init(){ //计算XYZ匹配函数 for(int i=0; i<nSpectralSamples; ++i){ Float wl0 = Lerp(Float(i) / Float(nSpectralSamples), sampledLambdaStart, sampledLambdaEnd); Float wl1 = Lerp(Float(i + 1) / Float(nSpectralSamples), sampledLambdaStart, sampledLambdaEnd); X.c[i] = AverageSpectrumSamples(CIE_lambda, CIE_X, nCIESamples, wl0, wl1); Y.c[i] = AverageSpectrumSamples(CIE_lambda, CIE_Y, nCIESamples, wl0, wl1); Z.c[i] = AverageSpectrumSamples(CIE_Lambda, CIE_Z, nCIESamples, wl0, wl1); } //计算RGB光谱函数 for(int i= 0; i< nSpectralSamples;++i){ Float wl0 = Lerp(Float(i) / Float(nSpectralSamples), sampledLambdaStart, sampledLambdaEnd); Float wl1 = Lerp(Float(i + 1) / Float(nSpectralSamples), sampledLambdaStart, sampledLambdaEnd); rgbRefl2SpectWhite.c[i] = AverageSpectrumSamples(RGB2SpectLambda, RGBRefl2SpectWhite, nRGB2SpectSamples, wl0, wl1); rgbRefl2SpectCyan.c[i] = AverageSpectrumSamples(RGB2SpectLambda, RGBRefl2SpectCyan, nRGB2SpectSamples, wl0, wl1); rgbRefl2SpectMagenta.c[i] = AverageSpectrumSamples(RGB2SpectLambda, RGBRefl2SpectMagenta, nRGB2SpectSamples, wl0, wl1); rgbRefl2SpectYellow.c[i] = AverageSpectrumSamples(RGB2SpectLambda, RGBRefl2SpectYellow, nRGB2SpectSamples, wl0, wl1); rgbRefl2SpectRed.c[i] = AverageSpectrumSamples(RGB2SpectLambda, RGBRefl2SpectRed, nRGB2SpectSamples, wl0, wl1); rgbRefl2SpectGreen.c[i] = AverageSpectrumSamples(RGB2SpectLambda, RGBRefl2SpectGreen, nRGB2SpectSamples, wl0, wl1); rgbRefl2SpectBlue.c[i] = AverageSpectrumSamples(RGB2SpectLambda, RGBRefl2SpectBlue, nRGB2SpectSamples, wl0, wl1);
rgbIllum2SpectWhite.c[i] = AverageSpectrumSamples(RGB2SpectLambda, RGBIllum2SpectWhite, nRGB2SpectSamples, wl0, wl1); rgbIllum2SpectCyan.c[i] = AverageSpectrumSamples(RGB2SpectLambda, RGBIllum2SpectCyan, nRGB2SpectSamples, wl0, wl1); rgbIllum2SpectMagenta.c[i] = AverageSpectrumSamples(RGB2SpectLambda, RGBIllum2SpectMagenta, nRGB2SpectSamples, wl0, wl1); rgbIllum2SpectYellow.c[i] = AverageSpectrumSamples(RGB2SpectLambda, RGBIllum2SpectYellow, nRGB2SpectSamples, wl0, wl1); rgbIllum2SpectRed.c[i] = AverageSpectrumSamples(RGB2SpectLambda, RGBIllum2SpectRed, nRGB2SpectSamples, wl0, wl1); rgbIllum2SpectGreen.c[i] = AverageSpectrumSamples(RGB2SpectLambda, RGBIllum2SpectGreen, nRGB2SpectSamples, wl0, wl1); rgbIllum2SpectBlue.c[i] = AverageSpectrumSamples(RGB2SpectLambda, RGBIllum2SpectBlue, nRGB2SpectSamples, wl0, wl1); } } private: //计算RGB到频谱函数,它从SampledSpectrum::Init()调用,这里不包括;它通过使用AverageSpectrumSamples() //函数重新采样RGBRefl2Spect和RGBIllum2Spect分布来初始化以下SampledSpectrum值。 static SampledSpectrum X,Y,Z;//X Y Z 三曲线匹配函数(将SPD转换为人类视觉的函数) static SampledSpectrum rgbRefl2SpectWhite,rgbRefl2SpectCyan; static SampledSpectrum rgbRef12SpectMagenta,rgbReflSpectYellow; static SampledSpectrum rgbRefl2SpectRed ,rgbRefl2SpectGreen, static SampledSpectrum rgbRefl2SpectBlue; static SampledSpectrum rgbIllum2SpectWhite, rgbIllum2SpectCyan; static SampledSpectrum rgbIllum2SpectMagenta,rgbIllum2SpectYellow; static SampledSpectrum rgbIllum2SpectRed,rgbIllum2SpectGreen; static SampledSpectrum rgbIllum2SpectBlue; } pbrt中所有的频谱实现都必须提供一种将其SPD转换为(xλ,yλ,zλ)系数。例如在更新图像中的像素过程中调用此方法。当光线通过相机将管埔提供给胶片时,胶片将SPD转换为XYZ系数,作为最终将它们转化为用于存储和显示的RGB的第一步。 为了计算(xλ,yλ,zλ)系数,sampledSpectrum使用黎曼和从公式5.1计算积分。 ![]() void ToXYZ(Float xyz[3])const { xyz[0] = xyz[1] = xyz[2] = 0.f; for(int i=0;i<nSpectralSamples; ++i) { xyz[0] += X.c[i] * c[i]; xyz[1] += Y.c[i] * c[i]; xyz[2] += Z.c[i] * c[i]; } Float scale = Float(sampledLambdaEnd - sampledLambdaStart) / Float(CIE_Y_integral * nSpectralSamples); xyz[0] *= scale; xyz[1] *= scale; xyz[2] *= scale; } 其中:static const Float CIE_Y_integral = 106.856895; CIE_Y_integral是通过图5.3 XYZ曲线取和而得出的积分结果。 XYZ颜色系数与亮度y密切相关,亮度是衡量颜色的感知亮度。亮度在第5.4.3节中有更详细的描述。我们提供了一种单独计算光谱亮度的方法。(例如,第14章、第15章和第16章中的一些光传输算法使用亮度作为贯穿场景的光传输路径的相对重要性的度量。) Float y() const { Float yy =0.f; for(int i = 0; i < nSpectralSamples; i++){ yy += Y.c[i] * c[i]; return yy * Float(sampledLambdaEnd - sampledLambdaStart) / Float(CIE_Y_integral * nSpectralSamples); } } 当我们在显示器上显示RGB颜色时,实际显示的光谱基本上是由三个光谱响应曲线的加权和决定的,分别代表红色,绿色和蓝色,这是由显示器的荧光粉发出的,LED或LCD元件,或等离子电池。图5.4绘制了LED显示器和LCD显示器发出的红、绿、蓝分布;请注意,它们是非常不同的。 ![]() 图5.5显示了在这些显示器上显示RGB颜色(0.6,0.3,0.2)所产生的spd。不足为奇的是,产生的spd也有很大的不同。这个例子说明,使用用户提供的RGB值来描述特定的颜色,实际上只有当用户选择RGB值时,他们所使用的显示的特征是有意义的。 ![]() 给定一个用(xλ,yλ,zλ)表示光谱功率分布,我们可以将其转换为相应的RGB系数,给定一组特定的spd,这些spd定义了红色、绿色和蓝色,用于感兴趣的显示,给定频谱响应曲线R(λ),G(λ)和B(λ)对于特定的显示,RGB系数可以通过将响应曲线与SPD的S(λ)结合,并使用颜色感知的三刺激理论来计算: ![]() 对于给定的R(λ)X(λ)响应曲线,可以预先计算等积的积分,从而可以将完全转换表示为一个矩阵: ![]() XYZ转换为RGB inline void XYZToRGB(const Float xyz[3], Float rgb[3]) { rgb[0] = 3.240479f*xyz[0] - 1.537150f*xyz[1] - 0.498535f*xyz[2]; rgb[1] = -0.969256f*xyz[0] + 1.875991f*xyz[1] + 0.041556f*xyz[2]; rgb[2] = 0.055648f*xyz[0] - 0.204043f*xyz[1] + 1.057311f*xyz[2]; } RGB转换为XYZ inline void RGBToXYZ(const Float rgb[3], Float xyz[3]) { xyz[0] = 0.412453f*rgb[0] + 0.357580f*rgb[1] + 0.180423f*rgb[2]; xyz[1] = 0.212671f*rgb[0] + 0.715160f*rgb[1] + 0.072169f*rgb[2]; xyz[2] = 0.019334f*rgb[0] + 0.119193f*rgb[1] + 0.950227f*rgb[2]; } 从另一种方式将RGB或XYZ值转换为SPD就不那么容易了:这个问题的约束程度非常低。回想一下,有无数种不同的spd具有相同的(因此是RGB)系数 (xλ,yλ,zλ) 。因此,给定RGB或值 (xλ,yλ,zλ) ,可以为其选择无限数量的spd。我们希望一个转换函数具有以下几个理想的标准:
平滑的目标是构建一个SPD的原因之一是显示的加权和 R(λ),G(λ)和B(λ), SPD并不是一个好的解决方案:如图5.4所示,这些函数通常是不规则的,和一个加权和的将不是一个非常光滑的SPD。尽管结果将是给定RGB值的一个metamer,但它可能不是实际对象的SPD的准确表示。 在这里,我们实现了Smits(1999)提出的将rgb转换为spd的方法,以实现上述目标。这种方法是基于这样的观察:一个好的开始是计算单独spd红、绿、蓝,光滑,这样计算它们与给定的RGB系数的加权和,然后回RGB转换给的结果接近于原始RGB系数。他通过一个数值优化程序找到了这样的光谱。 Smits注意到,可以对这种基本方法进行两个额外的改进。首先,与其用计算出来的红、绿、蓝spd的和来表示恒定的光谱(它们的和并不完全是恒定的),不如用恒定的spd来表示恒定的光谱。第二,像黄色(红色和绿色的混合物)这样的两种原色的混合物,用它们自己预先计算的平滑spd比用两种相应的原色spd的总和更好地表示。 以下数组存储符合这些标准的spd,其样品的波长为RGB2SpectLambda[](这些数据由Karl vom Berge生成)。 static const int nRGB2SpectSamples = 32; extern const Float RGB2SpectLambda[nRGB2SpectSamples]; extern const Float RGBRefl2SpectWhite[nRGB2SpectSamples]; extern const Float RGBRefl2SpectCyan[nRGB2SpectSamples]; extern const Float RGBRefl2SpectMagenta[nRGB2SpectSamples]; extern const Float RGBRefl2SpectYellow[nRGB2SpectSamples]; extern const Float RGBRefl2SpectRed[nRGB2SpectSamples]; extern const Float RGBRefl2SpectGreen[nRGB2SpectSamples]; extern const Float RGBRefl2SpectBlue[nRGB2SpectSamples]; 如果一个给定的RGB颜色描述从光源照明,实现更好的效果,如果转换表计算使用的光谱功率分布代表照明源定义“白”,而不是使用一个常数谱作为他们的表用于相对于红光之上。RGBIllum2Spect阵列使用D65光谱功率分布,该分布已被CIE标准化,以代表正午的阳光。(D65光源将在12.1.2节进行更多讨论。) extern const Float RGBIllum2SpectWhite[nRGB2SpectSamples]; extern const Float RGBIllum2SpectCyan[nRGB2SpectSamples]; extern const Float RGBIllum2SpectMagenta[nRGB2SpectSamples]; extern const Float RGBIllum2SpectYellow[nRGB2SpectSamples]; extern const Float RGBIllum2SpectRed[nRGB2SpectSamples]; extern const Float RGBIllum2SpectGreen[nRGB2SpectSamples]; extern const Float RGBIllum2SpectBlue[nRGB2SpectSamples]; SampledSpectrum::FromRGB()方法将给定的RGB值转换为完整的SPD。除了RGB值,它还接受一个枚举值,表示RGB值是代表表面反射率还是光源;对应的rgbIllum2Spect或rgbRefl2Spect值用于转换。 enum class SpectrumType { Reflectance, Illuminant }; SampledSpectrum SampledSpectrum::FromRGB(const Float rgb[3], SpectrumType type){ SampledSpectrum r; if( type == SpectrumType::Reflectance){ //将反射 光谱转换为RGB }else{ //将照明(太阳光) 光谱转换为RGB } } 这里我们将展示反射系数的转换过程。光源的计算是相同的,只是使用不同的转换值。首先,实现确定红色、绿色或蓝色通道是最小的。 //将反射 光谱转换为RGB if (rgb[0] <= rgb[1] && rgb[0] <= rgb[2]) { //以rgb[0]为最小值计算反射SampledSpectrum } else if (rgb[1] <= rgb[0] && rgb[1] <= rgb[2]) { //以rgb[1]为最小值计算反射SampledSpectrum } else { //以rgb[2]为最小值计算反射SampledSpectrum }
下面是红色组件最小情况下的代码。(绿色和蓝色的情况类似,不在本书中。)如果红色最小,我们知道绿色和蓝色比红色有更大的值。因此,我们可以将rgbrefl2spectrewhite中的红色分量乘以白色光谱的值赋给最终的SPD以返回。完成此操作后,剩下的RGB值为(0,g-r,b-r)。代码依次确定剩下的两个组件中哪个最小。这个值,乘以青色(绿色和蓝色)光谱,加到结果中,我们就得到要么(0,g-b,0)要么(0,0,b-g)。根据绿色或蓝色通道是否为非零,将绿色或蓝色SPD按余数进行缩放,转换完成。 数学推导公式:https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.40.9608&rep=rep1&type=pdf //以rgb[0]为最小值计算反射SampledSpectrum r += rgb[0] * rgbRefl2SpectWhite; if (rgb[1] <= rgb[2]) { r += (rgb[1] - rgb[0]) * rgbRefl2SpectCyan; r += (rgb[2] - rgb[1]) * rgbRefl2SpectBlue; } else { r += (rgb[2] - rgb[0]) * rgbRefl2SpectCyan; r += (rgb[1] - rgb[2]) * rgbRefl2SpectGreen; } RGBSpectrum实现(RGBSpectrum Implmentation) 这里的RGBSpectrum的实现是根据红,绿,蓝分量的加权和来表示SPD。回想一下,这种定义是不明确的:给定两个不同的计算机显示器,让它们显示相同的RGB值并不会导致它们发出相同的SPD。因此,为了用一组RGB值来指定实际的SPD,我们必须知道它们是根据什么定义的;此信息通常不与RGB值一起提供。 RGB表示方式还是很方便的:几乎所有的3D建模和设计工具都使用RGB颜色,而且大多数3D内容都是根据RGB来指定的。此外,它的计算和存储效率都很高,只需要表示三个浮点值。RGBSpectrum的实现继承自CoefficientSpectrum,指定存储三个组件。因此,前面定义的所有算术操作都可以自动用于RGBSpectrum。 class RGBSpectrum : public CoefficientSpectrum<3>{ using CoefficientSpectrum<3>::c; //... } 除了基本的算术运算符外,RGBSpectrum还需要提供转换到XYZ和RGB表示的方法。对于RGBSpectrum来说,这些都很简单。注意FromRGB()接受一个SpectrumType参数,就像这个方法的SampledSpectrum实例一样。尽管这里没有使用,但这两个类的FromRGB()方法必须具有匹配的签名,以便系统的其余部分能够一致地调用它们,而不管使用的是哪种谱表示。 static RGBSpectrum FromRGB(const Float rgb[3], SpectrumType type = SpectrumType::Reflectance) { RGBSpectrum s; s.c[0] = rgb[0]; s.c[1] = rgb[1]; s.c[2] = rgb[2]; return s; } 类似地,频谱表示必须能够将自己转换为RGB值。对于RGBSpectrum,实现可以回避使用什么特定的RGB原色来表示光谱分布的问题,只需直接返回RGB系数,并假设原色与已经被用于表示颜色的原色相同。 void ToRGB(Float *rgb) const { rgb[0] = c[0]; rgb[1] = c[1]; rgb[2] = c[2]; } 为了从一个任意采样的SPD创建一个RGB频谱,FromSampled()将频谱转换为XYZ,然后再转换为RGB。它在CIE匹配函数有一个值的每个波长上,使用InterpolateSpectrumSamples()效用函数,在1 nm步处评估分段线性采样光谱。然后用这个值来计算黎曼和来近似XYZ积分。 static RGBSpectrum FromSampled(const Float *lambda, const Float *v, int n) { <<Sort samples if unordered, use sorted for returned spectrum>> Float xyz[3] = { 0, 0, 0 }; for (int i = 0; i < nCIESamples; ++i) { Float val = InterpolateSpectrumSamples(lambda, v, n, CIE_lambda[i]); xyz[0] += val * CIE_X[i]; xyz[1] += val * CIE_Y[i]; xyz[2] += val * CIE_Z[i]; } Float scale = Float(CIE_lambda[nCIESamples-1] - CIE_lambda[0]) / Float(CIE_Y_integral * nCIESamples); xyz[0] *= scale; xyz[1] *= scale; xyz[2] *= scale; return FromXYZ(xyz); } InterpolateSpectrumSamples()取一组可能不规则的波长和SPD值 (λi,vi) ,并返回给定波长 λ 的SPD值,在括号中的 λ 两个样本值之间线性插值。附录A中定义的FindInterval()函数通过已排序的波长数组lambda执行二分查找,以找到包含l的区间。 Float InterpolateSpectrumSamples(const Float *lambda, const Float *vals, int n, Float l) { if (l <= lambda[0]) return vals[0]; if (l >= lambda[n - 1]) return vals[n - 1]; int offset = FindInterval(n, [&](int index) { return lambda[index] <= l; }); Float t = (l - lambda[offset]) / (lambda[offset+1] - lambda[offset]); return Lerp(t, vals[offset], vals[offset + 1]); } Reference: https://www.likecs.com/show-204980301.html https://pbr-book.org/3ed-2018/Color_and_Radiometry/Spectral_Representation |
光谱类的定义与实现细节
最新推荐文章于 2023-03-31 10:33:49 发布