在前面的文章中我们已经给出了基于物理的渲染方程:

并介绍了直接光照的实现。然而在自然界中,一个物体不会单独存在,光源会照射到其他的物体上,反射的光会有一部分反射到物体上。为了模拟这种环境光照的形式,我们通过环境贴图来保存周围的环境信息,从而实现间接环境光照,因此这种光照也叫基于图像的光照 (Image Based Lighting,简称IBL)。

与直接光照不同,间接光照的入射方向位于法线的半球的各个方向,想要求的出射方向$omega_o$的辐射率,需要对半球方向上所有的入射光线求辐射率的积分。

这要求给定一定入射方向,能获取这个方向的辐射率,并且能在实时渲染中求解积分方程。下面我们来解决这两个问题。

一、蒙特卡洛方法

在实时渲染中求解积分是不太现实的,所以计算机图形学引入了蒙特卡洛方法来获得近似解。蒙特卡洛方法主要运用了概率论和统计学的知识,我们来简单了解一下原理。

概率密度函数

随机变量X用来表示随机事件,它的值用小些字母表示,如x,称为随机数。随机变量的输入集合也可以是另一种随机变量,这时我们将服从一种分布的随机变量转换为服从另一种分布的随机变量,如$Y = f(X)$。

随机变量$X$的每个值$x$都关联着每次样本抽样时这个值出现的概率,随机变量所有可能值组成的概率分布函数称为概率密度函数(probability density function,简称为$pdf$),用$p$表示。

在渲染方程中我们用到的随机变量都是连续型随机变量。随机连续变量$X$期望的期望为:

通常我们求的是与随机变量相关的函数$Y = f(X)$,Y的期望为:

大数定律

我们对随机变量$X$重复N次抽样,形成一系列的独立同分布的随机数,然后通过对这些随机数进行平均来近似上述的期望模型,这就是估计:

随着随机抽象数目N的增大,估计的方差逐渐减小。当N无限大时,随机变量的统计平均值趋近于期望的值:

大数定律是蒙特卡洛方法的基础,用随机抽样和统计试验求出近似解。

蒙特卡洛积分

假设$f(x)$服从概率密度函数$p(x)$,对下面的积分,有:

随意上述问题转化为了求$frac{f(x)}{p(x)}$的期望。根据大数定律,可得估计值。

在实时渲染中,PBR的积分方程主要由这种估计思想来求解。

二、立方体贴图

我们解决了实时渲染中求解积分的问题,接下来我们解决给定一个方向$w_i$,获取环境辐射率的问题。

我们可以将辐射率保存到环境贴图中,通过给定的方向,可以很方便地采样出环境的辐射率:

1
vec3 radiance = textureCube(cubemap, w_i).rgb;

在渲染中美术制作的环境贴图通常是HDR球形贴图,常见的有RGBE格式。我们需要将HDR贴图转换为CubeMap,然后对CubeMap计算积分。

将SphereMap转换为CubeMap

立方体贴图的面展开如下:

将立方体的各个面作为RenderTarget,把SphereMap的内容画到立方体的各个面中,就可以完成从SphereMap到CubeMap的转换。

从原点看向立方体的各个面,并设置相机以保证各个面采集的顺序是按照上图的方式排布的(上图中的X面和Z面的是以Y轴为摄像机的上方向采集的,采集Y面需要以X轴或Z轴为上方向采集,我在代码里选取的是Z轴)。代码如下:

1
2
3
4
5
6
7
8
9
Matrix4 cubeviews[] = 
{
Matrix4::CreateLookAtLH( Vector3::cOrigin, Vector3( 1.0f, 0.0f, 0.0f ), Vector3( 0.0f, 1.0f, 0.0f ) ),
Matrix4::CreateLookAtLH( Vector3::cOrigin, Vector3( -1.0f, 0.0f, 0.0f ), Vector3( 0.0f, 1.0f, 0.0f ) ), // x-
Matrix4::CreateLookAtLH( Vector3::cOrigin, Vector3( 0.0f, 1.0f, 0.0f ), Vector3( 0.0f, 0.0f, -1.0f ) ), // y+
Matrix4::CreateLookAtLH( Vector3::cOrigin, Vector3( 0.0f, -1.0f, 0.0f ), Vector3( 0.0f, 0.0f, 1.0f ) ), // y-
Matrix4::CreateLookAtLH( Vector3::cOrigin, Vector3( 0.0f, 0.0f, 1.0f ), Vector3( 0.0f, 1.0f, 0.0f ) ), // z+
Matrix4::CreateLookAtLH( Vector3::cOrigin, Vector3( 0.0f, 0.0f, -1.0f ), Vector3( 0.0f, 1.0f, 0.0f ) ), // z-
};

相机设置好之后,立方体的顶点归一化便可作为立方体贴图的采样方向。对每个CubeMap的面而言,在像素着色器中,将这个采样方向转换为SphereMap的UV,采样SphereMap,变可以把CubeMap转换为SphereMap。
Wiki给出了将3D向量转换为UV的方法:

对这样的一张球形HDR贴图:

在我使用的引擎中,上方向为$Z$轴,需要在shader中将$Y$轴和$Z$轴做了置换,转换后的CubeMap如下图:

GLSL代码如下。
vs:

1
2
3
4
5
6
7
8
attribute vec4 position;
varying vec4 opos;
uniform mat4 wvp;
void main()
{
gl_Position = wvp * vec4(position.xyz, 1.0);
opos = position;
}

ps:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
varying vec4 opos;
uniform sampler2D layer0;
const float PI = 3.14159265359;
const float doublePI = 2 * PI;
vec2 SampleSphereMap(vec3 v)
{
v = normalize(v);
vec2 uv = vec2(atan2(v.y, v.x), acos(v.z));
uv /= vec2(doublePI, PI);
uv.x += 0.5;
return uv;
}
void main()
{
vec2 uv = SampleSphereMap(opos.xyz);
vec3 color = texture2D(layer0, uv).rgb;
gl_FragColor = vec4(color, 1.0);
}

将SphereMap转换为SphereMap后,我们再看在直接光照中使用的PBR渲染公式:

现在我们已经知道了求解积分需要的蒙特卡洛方法,但是在像素着色器里求解是不切实际的。所以我们会把这个积分预处理,将结果保存到贴图中。渲染方程分为两个部分:diffuse和specular,接下来对它们分别进行计算。

三、Diffuse IBL

Diffuse部分的渲染方程:

其中$L_i$是环境贴图的采样,$theta_i$是入射光线与法线的夹角。

将这个积分方程的计算结果保存到一张辐射率图中,由于$L_o$是在法线半球上的积分,所以在求环境光照的时候,用某个点的法线采样这个辐射率贴图得到的结果就是diffuse渲染方程的积分结果。

1
vec3 envdiffuse = textureCube(radianceMap, N).rgb;

下面我们来解这个积分方程。

立体角的微分$domega = sintheta dtheta dvarphi$,带入到渲染方程:

根据蒙特卡洛方法,将$phi$和$theta$离散化,分别分成$n1$和$n2$个采样。

同转换SphereMap一样,利用立方体的顶点当做法线,计算CubeMap在法线半球上的积分。结果如下图:

GLSL代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
varying vec4 opos;
uniform samplerCube layer0;
const float PI = 3.14159265359;
void main()
{
vec3 N = normalize(opos.xyz);
vec3 irradiance = vec3(0.0);
vec3 up = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
vec3 T = normalize(cross(up, N));
vec3 B = normalize(cross(N, T));
const float sampleDelta = 0.025;
int samplenum = 0;
for (float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta)
{
for (float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta)
{
vec3 tagentnormal = vec3(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta));
vec3 worldnormal = tagentnormal.x * T + tagentnormal.y * B + tagentnormal.z * N;
irradiance += textureCube(layer0, worldnormal).rgb * cos(theta) * sin(theta);
samplenum ++;
}
}
irradiance = PI * irradiance / float(samplenum);
gl_FragColor = vec4(irradiance, 1.0);
}

同直接光照中一样,diffuse部分的着色要考虑金属度和菲涅尔效果。由于环境光照来自于法线半球的各个方向,不像直接光照一样有一个确定的半向量$h$来确定菲涅尔效果。还记得粗糙度roughness会影响微平面的半向量$h$吗?同样地,我们在菲涅尔公式里引入粗糙度,表面粗糙,菲涅尔效果要弱一点。下面是引入roughness的菲涅尔公式:

1
2
3
4
vec3 fresnelSchlickRoughness(vec3 F0, float cosa, float roughness)
{
return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - cosa, 5.0);
}

结合金属度,可以计算出环境光照的diffuse分量:

1
2
3
4
5
vec3 IBLF0 = fresnelSchlickRoughness(F0, NDotV, roughness);
vec3 ambientKS = IBLF0;
vec3 ambientKD = (vec3(1.0) - ambientKS) * vec3(1.0 - metallic);
vec3 diffuseradiance = textureCube(layer9, normal).rgb;
vec3 envdiffuse = ambientKD * diffuseradiance * albedo;

与直接光照的部分相加得到的效果如下,可以看到,比直接光照相比有了非常大的提升。

四、Specular IBL

specular部分的渲染方程:

其中$f_{cook-torrance}frac{D(h)G(omega_i, omega_o,h)F(omega_o,h)}{4(ncdot omega_i)(ncdot omega_o)}$。

由于diffuse的辐射率积分只与入射立体角$omega_i$有关,可以把diffuse的常量部分移到积分外求解。但对specular而言,BDRF还依赖于出射立体角$omega_o$。我们已经知道只有光线方向在视线方向的反射方向的附近的光,才能够被人眼看到,但是环境光来自法线半球的各个方向, 无法确定入射方向$L$,但是我们可以通过得到半向量$h$来求入射光的方向$L$。接下来掉微平面模型详细分析推导出$h$。

法线分布函数


我们假设微平面(蓝色部分)是光滑平面(黑色部分)沿法线$n$做随机扰动后形成的不平整表面,其中红色部分是朝向半向量$h$附近的微面元集合。假设扰动前光滑平面的表面积和为$A$,扰动之后的表面积和为$A’$。定义$D(omega_h)domega_h$为所有朝向$omega_h$附近的微面元面积之和占A的比例。于是有$Acdot D(omega_h)domega_h$为朝向$omega_h$附近的所有微面元的面积之和。可以得到:

$D(omega_h)$有以下性质:

  1. $D(omega_h) ge 0$。
  2. 由于$A’ = int_{Omega} A cdot D(omega_h)domega_h ge A$,得 $$int_{Omega} D(omega_h)domega_h ge 1$。
  3. 光滑表面在任意方向$bf{v}$的投影面积,等于微平面在该方向的投影面积。于是有即$int_{Omega} D(omega_h)(mathbf{v}cdot mathbf{h}) domega_h = mathbf{v}cdot mathbf{n}$。特殊地取$mathbf{v} = mathbf{n} $,则有其中$theta$是$mathbf{h}$与$mathbf{n}$的夹角,这就满足了法线分布函数$D(omega_h)$的归一化条件,可以得到其概率密度函数为$p(omega_h) = D(omega_h)costheta$。

我们知道了在立体角下的概率密度函数,将其转换为球坐标系下基于$theta$和$phi$的$pdf$。由于$domega = sintheta dtheta dphi$,有

将我们采用的Trowbridge-Reitz GGX发现分布函数代入,得

由于$p(theta, phi)$式子中没有$phi$,很容易得到$p(theta)$:

再求$p(phi|theta)$,有

对$p(theta)$积分,得到其概率分布函数:

对$p(phi|theta)$求其概率分布函数:

这时我们可以假设随机数 $xi_0 = cdf_h(theta)$,$xi_1 = cdf_h(phi|theta)$,分别求得$theta$和$phi$。

得到$theta$和$phi$后,就可以得到笛卡尔坐标系下的$h$,之后便可以进行渲染方程的计算。

Hammersley Sequence

我们还有随机数$xi_0$和$xi_1$没有解决。蒙特卡洛传统的抽样方式是采用电脑的伪随机数来进行的,这种随机的特征导致每个随机数并不知道其他随机数的任何信息,所以其分布可能会出现丛聚,减慢了收敛速度。但如果使用某些 大专栏  基于物理的渲染——间接光照方法使随机数均匀地分布,那么蒙特卡洛方法的模拟过程不会受到影响,并且收敛速度可以加快。

产生较均匀分布的随机数生成方法叫低差异序列(Low Discrepancy Sequence),而我们使用的是其中的一种——Hammersley。伪随机数和Hammersley序列生成的点集对比如下图:

有兴趣了解原理的可以阅读这篇文章,在这里我们直接给出Hammersley序列的生成代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
float RadicalInverse_VdC(uint bits) 
{
bits = (bits << 16u) | (bits >> 16u);
bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
return float(bits) * 2.3283064365386963e-10; // / 0x100000000
}
// ----------------------------------------------------------------------------
vec2 Hammersley(uint i, uint N)
{
return vec2(float(i)/float(N), RadicalInverse_VdC(i));
}

到这里随机数$xi_0$和$xi_1$的生成也搞定了。

GGX重要度采样

基于蒙特卡洛积分的求解方法,我们在半球$Omega$上生成一系列偏向半向量$h$的采样向量。与diffuse部分的求解类似,在一个循环内生成一系列低差异化的随机数,

1
2
3
4
5
const uint SAMPLE_COUNT = 1024u;
for(uint i = 0u; i < SAMPLE_COUNT; ++i)
{
vec2 Xi = Hammersley(i, SAMPLE_COUNT);
}

用这些随机数生成切线空间的向量,再将其转换到世界空间,就得到了半向量$h$。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness)
{
float a = roughness * roughness;
float phi = 2.0 * PI * Xi.x;
float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a * a - 1.0) * Xi.y));
float sinTheta = sqrt(1.0 - cosTheta * cosTheta);
vec3
H.x = cos(phi) * sinTheta;
H.y = sin(phi) * sinTheta;
H.z = cosTheta;
vec3 up = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
vec3 T = normalize(cross(up, N));
vec3 B = normalize(cross(N, T));
vec3 normal = H.x * T + H.y * B + H.z * N;
return normalize(normal);
}

至此,半向量h的求解方法已经给出。再来看specular渲染方程:

用模特卡洛方法估计这个积分:

UE4用Split Sum Approximation将估计分为两个部分:

第一部分的计算结果存储到一张叫做Pre-Filter Envionment Map的CubeMap上。
第二部分的计算结果存储到一张叫做Environment BRDF的2D贴图上。

Pre-Filter Envionment Map

Split Sum的第一部分

可以简单地根据不同的粗糙度对CubeMap取均值,然后将不同粗糙度的结果存储到不同的MipMap中,也能取得不错的效果。这里我们采用UE4的方案,使用重要度采样的法线分布函数对CubeMap卷积。由于我们不知道视线方向,UE4假设其与采样的方向相同。

1
2
vec3 N = R;
vec3 V = R;

对CubeMap做不同roughness的卷积得到Pre-Filter Environment Map,glsl代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void main()
{
vec3 R = normalize(opos.xyz);
vec3 N = R;
vec3 V = R;
vec3 preFilter = vec3(0.0);
const uint sampleNum = 1024;
float totalWeight = 0.0;
for (uint i = 0u; i < sampleNum; i ++)
{
vec2 Xi = Hammersley(i, sampleNum);
vec3 H = ImportanceSampleGGX(Xi, N, weight.x);
vec3 L = normalize(2 * dot(V, H) * H - V);
float NDotL = max(dot(N, L), 0.0); if (NDotL > 0.0)
{
preFilter += textureCube(layer0, L).rgb * NDotL;
totalWeight += NDotL;
}
} preFilter /= totalWeight;
gl_FragColor = vec4(preFilter, 1.0);
}

由于在工具里没法看全Mipmap的CubeMap,用Learn OpenGL里的Pre-Filter Environment Map展示一下效果。

Environment BRDF

Split Sum的第二部分

式子里包含了一个概率密度函数,在前面我们已经给出$p(omega_h) = D(omega_h)(n cdot h)$,引用自Surface Reflection: Physical and Geometrical Perspectives

可以得到

将Cook-Torrance BRDF:$ f_{cook-torrance} = frac{D(h)G(omega_i, omega_o,h)F(omega_o,h)}{4(ncdot omega_i)(ncdot omega_o)}$ ,菲涅尔函数:$F(n, v, F_0) = F_0 + (1 - F_0) ( 1 - (n cdot v))^5$以及$p(omega_i,omega_o)$代入,有

所以蒙特卡洛估计被转换为与粗糙度和$omega_o cdot h$相关的式子,由于二者都在[0, 1]范围内,可以用这两个值作为UV,将上式的求和结果保存到一张2D的LUT里,也就是Environment BRDF。
UE中,IBL中计算几何遮蔽因子$G$的系数与直接光照的不同,在IBL中有$k = frac{alpha^2}{2}$,

1
2
3
4
5
6
7
8
float GeometrySchlickGGX(float NDotV, float roughness)
{
float a = roughness;
float k = (a * a) / 2.0;
float nom = NDotV;
float denom = NDotV * (1.0 - k) + k;
return nom / denom;
}

生成Environment BRDF的GLSL代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
varying vec2 otex0;
const float PI = 3.14159265359;
varying vec2 otex0;
const float PI = 3.14159265359; vec2 IntegrateBRDF(float NDotV, float roughness)
{
vec3 V;
V.x = sqrt(1.0 - NDotV * NDotV);
V.y = 0.0;
V.z = NDotV;
float A = 0.0;
float B = 0.0;
vec3 N = vec3(0.0, 0.0, 1.0);
const uint SAMPLE_COUNT = 1024u;
for (uint i = 0u; i < SAMPLE_COUNT; i ++)
{
vec2 Xi = Hammersley(i, SAMPLE_COUNT);
vec3 H = ImportanceSampleGGX(Xi, N, roughness);
vec3 L = normalize(2.0 * dot(V, H) * H - V);
float NDotL = max(L.z, 0.0);
float NDotH = max(H.z, 0.0);
float VDotH = max(dot(V, H), 0.0); if (NDotL > 0.0);
{
float G = GeometrySmith(NDotL, NDotV, roughness);
float G_Vis = G * VDotH / (NDotH * NDotV + 0.001);
float Fc = pow(1.0 - VDotH + 0.001, 5.0);
A += (1.0 - Fc) * G_Vis;
B += Fc * G_Vis;
}
} A /= float(SAMPLE_COUNT);
B /= float(SAMPLE_COUNT);
return vec2(A, B);
}
void main()
{
vec2 brdf = IntegrateBRDF(otex0.x, otex0.y);
gl_FragColor = vec4(brdf.x, brdf.y, 0.0, 1.0);
}

计算出来的LUT如下图。

我们已经将IBL的diffuse部分的预计算结果加到材质shader里,现在处理specular部分的预计算结果Pre-Filter Environment Map和Environment BRDF。先根据roughness取出Pre-Filter部分,再与Environment BRDF部分结合。写成GLSL代码如下:

1
2
3
4
5
6
vec3 IBLF0 = fresnelSchlickRoughness(F0, NDotV, roughness);
const float MAX_REFLECTION_LOD = 4.0; vec3 preFilter = textureCubeLod(layerpreFilter, R, roughness * MAX_REFLECTION_LOD).rgb;
vec2 brdf = texture2D(layerbrdf, vec2(NDotV, roughness)).rg;
vec3 envspecular = preFilter * (IBLF0 * brdf.x + brdf.y);

最后将IBL的diffuse部分和specular部分相加就完全地实现了PBR,效果如下:

五、结语

完整地实现PBR之后,可以看出材质很真实。我写的PBR的实现主要参考了Learn OpenGL的PBR教程,这是一个很棒的系列教程,能够帮助新手对渲染方面有较为全面的认识。PBR实现中的公式我都有尽量去理解,去推导,但是能感觉到还有很多知识如辐射度量学、概率论、统计学、光学等知识需要补足,希望以后有机会深入了解。
没有在引擎里为项目实现PBR,很多坑没有踩到,在这里就从网上摘一些PBR的优点作为结尾吧。

  • 渲染效果很真实。
  • 金属工作流,能够很好的区分金属和非金属,去除浓浓的塑料感
  • 基于物理世界的真实参数,方便美术制作,不用依靠经验调参数
  • 同样的材质能够在不同的环境下表现。下雨天,黄昏等。

引用

  1. https://learnopengl.com/PBR/IBL/Diffuse-irradiance
  2. https://learnopengl.com/PBR/IBL/Specular-IBL
  3. https://interplayoflight.wordpress.com/2013/12/30/readings-on-physically-based-rendering/
  4. http://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/monte-carlo-methods-in-practice/monte-carlo-integration
  5. https://blog.csdn.net/baimafujinji/article/details/53869358
  6. http://www.codinglabs.net/article_physically_based_rendering_cook_torrance.aspx
  7. https://schuttejoe.github.io/post/ggximportancesamplingpart1/
  8. 全局光照技术
  9. https://www.tobias-franke.eu/log/2014/03/30/notes_on_importance_sampling.html
  10. https://agraphicsguy.wordpress.com/2015/11/01/sampling-microfacet-brdf/
  11. https://www.cnblogs.com/time-flow1024/p/10209093.html

基于物理的渲染——间接光照的更多相关文章

  1. PBR(基于物理的渲染)学习笔记

    PBR基本介绍 PBR代表基于物理的渲染,本质上还是 gl_FragColor = Emssive + Ambient + Diffuse + Specular 可能高级一些在考虑下AO也就是环境光遮 ...

  2. PBR:基于物理的渲染(Physically Based Rendering)+理论相关

    一: 关于能量守恒 出射光线的能量永远不能超过入射光线的能量(发光面除外).如图示我们可以看到,随着粗糙度的上升镜面反射区域的会增加,但是镜面反射的亮度却会下降.如果不管反射轮廓的大小而让每个像素的镜 ...

  3. PBR(基于物理的渲染)学习笔记2

    相关资料 https://www.cnblogs.com/dojo-lzz/p/13237686.html 文档:PBR学习笔记.note 链接:http://note.youdao.com/note ...

  4. 基于物理的渲染—HDR Tone Mapping

    在游戏引擎渲染管线中,我们对于R.G.B通道颜色信息的数值范围通常设置在[0,1]之间(或者是[0,255]).其中,0代表没有光亮度,1代表显示器能够显示的最大光亮度.这个表示方式虽然直接易懂,但它 ...

  5. Unity3d 基于物理渲染Physically-Based Rendering之specular BRDF

    在实时渲染中Physically-Based Rendering(PBR)中文为基于物理的渲染它能为渲染的物体带来更真实的效果,而且能量守恒 稍微解释一下字母的意思,为对后文的理解有帮助,从右到左L为 ...

  6. Unity3d 基于物理渲染Physically-Based Rendering之实现

    根据前文的例子http://blog.csdn.net/wolf96/article/details/44172243(不弄超链接了审核太慢)弄一下真正的基于物理的渲染逃了节课= =,弄了一下.公式和 ...

  7. 基于预计算的全局光照(Global Illumination Based On Precomputation)

    目录 基于图像的光照(Image Based Lighting,IBL) The Split Sum Approximation 过滤环境贴图 预计算BRDF积分 预计算辐射度传输(Precomput ...

  8. 个人翻译的cedec2010基于物理的光照

    作为自己介绍基于物理渲染计划的一部分,在自己总结和发布的同时,也会翻译一些国外的优秀资料做推广    本文是Tri Ace 在 cedec2010上发布的文章,主要描述了他们基于物理光照的实现方法,这 ...

  9. unity3d 基于物理渲染的问题解决

    最近1个月做了unity 次世代开发的一些程序方面的支持工作,当然也是基于物理渲染相关的,主要还是skyshop marmoset的使用吧,他算是unity4.x版本 PBR的优秀方案之一了但在使用以 ...

随机推荐

  1. Python判断一个字符串是否包含子串的几种方法

    转自---http://blog.csdn.net/yl2isoft/article/details/52079960 1.使用成员操作符 in >>> s='nihao,shiji ...

  2. 简单总结------redis

    一.Redis 是一个基于内存的高性能key-value数据库. 二.端口 6379 三.特点: Redis本质上是一个Key-Value类型的内存数据库,很像memcached,整个数据库统统加载在 ...

  3. Java集合详解(全)

    Java的集合主要有List , Set, Map List , Set继承至Collection接口,Map为独立接口 List下有ArrayList,LinkedList,Vector Set下有 ...

  4. goweb-文本处理

    文本处理 Web开发中对于文本处理是非常重要的一部分,我们往往需要对输出或者输入的内容进行处理,这里的文本包括字符串.数字.Json.XML等等.Go语言作为一门高性能的语言,对这些文本的处理都有官方 ...

  5. 0.3W微功率放大器

    电路结构 电路摘自<晶体管电路设计(上)>. 电路采用+5V单电源供电,两级结构.Tr1构成共射极放大电路作为电压放大级:Tr3,Tr4构成推挽的射极跟随器作为输出级:Tr2作为射极跟随器 ...

  6. stopping service [tomcat],服务未启动

    1. 在主类中添加日志打印,查看错误: 2. 我的错误是因为两个controller含有相同的路径

  7. TreeviewEditor软件的安装和使用

    TreeviewEditor是用VB6开发的一款Windows桌面程序,用户可以快速搭建树形结构,可以导出为Word文档. 支持节点的复制粘贴.节点的拖放. 下载地址:TreeviewEditor.r ...

  8. Nginx_安全1

    Nginx 安全 nginx隐藏版本号 # 在Nginx的配置文件中进行修改,增加下面这个. server_tokens on; nginx对IP和目录限速 # Nginx可以通过HTTPLimitZ ...

  9. 使用java获取手机号归属地等信息httpClient实现

    java获取手机号归属地 一般想获取手机号归属地等信息个人是无法获取的,但是可以通过调用第三方接口获取,具体百度搜索很多这里例子提供一个淘宝的接口 ,该功能已经发布到网站作为一个在线小工具,拿走不谢: ...

  10. 48)PHP,工厂模式

    为啥需要工厂模式啊: (原来是生产类的工具——————————) 工厂类的代码格式: <?php class factory{ //Instance表示“实例”,“对象” static func ...