diff --git a/docs/02 Lighting/05 Light casters.md b/docs/02 Lighting/05 Light casters.md index 9ccbca1..fc3d4df 100644 --- a/docs/02 Lighting/05 Light casters.md +++ b/docs/02 Lighting/05 Light casters.md @@ -104,13 +104,16 @@ glUniform3f(lightDirPos, -0.2f, -1.0f, -0.3f); 幸运的是一些聪明人已经早就把它想到了。下面的方程把一个片段的光的亮度除以一个已经计算出来的衰减值,这个值根据光源的远近得到: -![Latex Formula](https://raw.githubusercontent.com/LearnOpenGL-CN/LearnOpenGL-CN/master/img/Light_casters1.png) +$$ +\begin{equation} F_{att} = \frac{1.0}{K_c + K_l * d + K_q * d^2} \end{equation} +$$ -在这里![I](https://raw.githubusercontent.com/LearnOpenGL-CN/LearnOpenGL-CN/master/img/I.png)是当前片段的光的亮度,![d](https://raw.githubusercontent.com/LearnOpenGL-CN/LearnOpenGL-CN/master/img/d.png)代表片段到光源的距离。为了计算衰减值,我们定义3个项:常数项![Kc](https://raw.githubusercontent.com/LearnOpenGL-CN/LearnOpenGL-CN/master/img/Kc.png),一次项![Kl](https://raw.githubusercontent.com/LearnOpenGL-CN/LearnOpenGL-CN/master/img/Kl.png)和二次项![Kq](https://raw.githubusercontent.com/LearnOpenGL-CN/LearnOpenGL-CN/master/img/Kq.png)。 +在这里\(d\)代表片段到光源的距离。为了计算衰减值,我们定义3个(可配置)项:**常数**项\(K_c\),**一次**项\(K_l\)和**二次**项\(K_q\)。 + +- 常数项通常是1.0,它的作用是保证坟墓永远不会比1小,因为它可以利用一定的距离增加亮度,这个结果不会影响到我们所寻找的。 +- 一次项用于与距离值相称,这回以线性的方式减少亮度。 +- 二次项用于与距离的平方相乘,为光源设置一个亮度的二次递减。二次项在距离比较近的时候相比一次项会比一次项更小,但是当距离更远的时候比一次项更大。 -常数项通常是1.0,它的作用是保证坟墓永远不会比1小,因为它可以利用一定的距离增加亮度,这个结果不会影响到我们所寻找的。 -一次项用于与距离值相称,这回以线性的方式减少亮度。 -二次项用于与距离的平方相乘,为光源设置一个亮度的二次递减。二次项在距离比较近的时候相比一次项会比一次项更小,但是当距离更远的时候比一次项更大。 由于二次项的光会以线性方式减少,指导距离足够大的时候,就会超过一次项,之后,光的亮度会减少的更快。最后的效果就是光在近距离时,非常量,但是距离变远亮度迅速降低,最后亮度降低速度再次变慢。下面的图展示了在100以内的范围,这样的衰减效果。 ![](http://www.learnopengl.com/img/lighting/attenuation.png) @@ -123,7 +126,7 @@ glUniform3f(lightDirPos, -0.2f, -1.0f, -0.3f); 但是,我们把这三个项设置为什么值呢?正确的值的设置由很多因素决定:环境、你希望光所覆盖的距离范围、光的类型等。大多数场合,这是经验的问题,也要适度调整。下面的表格展示一些各项的值,它们模拟现实(某种类型的)光源,覆盖特定的半径(距离)。第一栏定义一个光的距离,它覆盖所给定的项。这些值是大多数光的良好开始,它是来自Ogre3D的维基的礼物: -Distance|Constant|Linear|Quadratic +距离|常数项|一次项|二次项 -------|------|-----|------ 7|1.0|0.7|1.8 13|1.0|0.35|0.44 @@ -138,7 +141,7 @@ Distance|Constant|Linear|Quadratic 600|1.0|0.007|0.0002 3250|1.0|0.0014|0.000007 -就像你所看到的,常数项![Kc](https://raw.githubusercontent.com/LearnOpenGL-CN/LearnOpenGL-CN/master/img/Kc.png)一直都是1.0。一次项![Kl](https://raw.githubusercontent.com/LearnOpenGL-CN/LearnOpenGL-CN/master/img/Kl.png)为了覆盖更远的距离通常很小,二次项![Kq](https://raw.githubusercontent.com/LearnOpenGL-CN/LearnOpenGL-CN/master/img/Kq.png)就更小了。尝试用这些值进行实验,看看它们在你的实现中各自的效果。我们的环境中,32到100的距离对大多数光通常就足够了。 +就像你所看到的,常数项\(K_c\)一直都是1.0。一次项\(K_l\)为了覆盖更远的距离通常很小,二次项\(K_q\)就更小了。尝试用这些值进行实验,看看它们在你的实现中各自的效果。我们的环境中,32到100的距离对大多数光通常就足够了。 #### 实现衰减 @@ -205,10 +208,10 @@ OpenGL中的聚光灯用世界空间位置,一个方向和一个指定了聚 * `LightDir`:从片段指向光源的向量。 * `SpotDir`:聚光灯所指向的方向。 -* `Phiφ`:定义聚光灯半径的切光角。每个落在这个角度之外的,聚光灯都不会照亮。 -* `Thetaθ`:`LightDir`向量和`SpotDir向`量之间的角度。θ值应该比φ值小,这样才会在聚光灯内。 +* `Phi\(\phi\)`:定义聚光灯半径的切光角。每个落在这个角度之外的,聚光灯都不会照亮。 +* `Theta\(\theta\)`:`LightDir`向量和`SpotDir`向量之间的角度。\(\theta\)值应该比\(\Phi\)值小,这样才会在聚光灯内。 -所以我们大致要做的是,计算`LightDir`向量和`SpotDir`向量的点乘(返回两个单位向量的点乘,还记得吗?),然后在和遮光角φ对比。现在你应该明白聚光灯是我们下面将创建的手电筒的范例。 +所以我们大致要做的是,计算`LightDir`向量和`SpotDir`向量的点乘(返回两个单位向量的点乘,还记得吗?),然后在和切光角\(\phi\)对比。现在你应该明白聚光灯是我们下面将创建的手电筒的范例。 @@ -216,7 +219,7 @@ OpenGL中的聚光灯用世界空间位置,一个方向和一个指定了聚 手电筒是一个坐落在观察者位置的聚光灯,通常瞄准玩家透视图的前面。基本上说,一个手电筒是一个普通的聚光灯,但是根据玩家的位置和方向持续的更新它的位置和方向。 -所以我们需要为片段着色器提供的值,是聚光灯的位置向量(来计算光的方向坐标),聚光灯的方向向量和遮光角。我们可以把这些值储存在`Light`结构体中: +所以我们需要为片段着色器提供的值,是聚光灯的位置向量(来计算光的方向坐标),聚光灯的方向向量和切光角。我们可以把这些值储存在`Light`结构体中: ```c++ struct Light @@ -236,9 +239,9 @@ glUniform3f(lightSpotdirLoc, camera.Front.x, camera.Front.y, camera.Front.z); glUniform1f(lightSpotCutOffLoc, glm::cos(glm::radians(12.5f))); ``` -你可以看到,我们为遮光角设置一个角度,但是我们根据一个角度计算了余弦值,把这个余弦结果传给了片段着色器。这么做的原因是在片段着色器中,我们计算`LightDir`和`SpotDir`向量的点乘,而点乘返回一个余弦值,不是一个角度,所以我们不能直接把一个角度和余弦值对比。为了获得这个角度,我们必须计算点乘结果的反余弦,这个操作开销是很大的。所以为了节约一些性能,我们先计算给定切光角的余弦值,然后把结果传递给片段着色器。由于每个角度都被表示为余弦了,我们可以直接对比它们,而不用进行任何开销高昂的操作。 +你可以看到,我们为切光角设置一个角度,但是我们根据一个角度计算了余弦值,把这个余弦结果传给了片段着色器。这么做的原因是在片段着色器中,我们计算`LightDir`和`SpotDir`向量的点乘,而点乘返回一个余弦值,不是一个角度,所以我们不能直接把一个角度和余弦值对比。为了获得这个角度,我们必须计算点乘结果的反余弦,这个操作开销是很大的。所以为了节约一些性能,我们先计算给定切光角的余弦值,然后把结果传递给片段着色器。由于每个角度都被表示为余弦了,我们可以直接对比它们,而不用进行任何开销高昂的操作。 -现在剩下要做的是计算θ值,用它和φ值对比,以决定我们是否在或不在聚光灯的内部: +现在剩下要做的是计算\(\theta\)值,用它和\(\phi\)值对比,以决定我们是否在或不在聚光灯的内部: ```c++ float theta = dot(lightDir, normalize(-light.direction)); @@ -254,7 +257,7 @@ color = vec4(light.ambient*vec3(texture(material.diffuse,TexCoords)), 1.0f); !!! Important - 你可能奇怪为什么if条件中使用>符号而不是<符号。为了在聚光灯以内,θ不是应该比光的遮光值更小吗?这没错,但是不要忘了,角度值是以余弦值来表示的,一个0度的角表示为1.0的余弦值,当一个角是90度的时候被表示为0.0的余弦值,你可以在这里看到: + 你可能奇怪为什么if条件中使用>符号而不是<符号。为了在聚光灯以内,`theta`不是应该比光的切光值更小吗?这没错,但是不要忘了,角度值是以余弦值来表示的,一个0度的角表示为1.0的余弦值,当一个角是90度的时候被表示为0.0的余弦值,你可以在这里看到: ![](http://www.learnopengl.com/img/lighting/light_casters_cos.png) @@ -275,13 +278,17 @@ color = vec4(light.ambient*vec3(texture(material.diffuse,TexCoords)), 1.0f); 为创建外圆锥,我们简单定义另一个余弦值,它代表聚光灯的方向向量和外圆锥的向量(等于它的半径)的角度。然后,如果片段在内圆锥和外圆锥之间,就会给它计算出一个0.0到1.0之间的亮度。如果片段在内圆锥以内这个亮度就等于1.0,如果在外面就是0.0。 我们可以使用下面的公式计算这样的值: -![Latex Formula](https://raw.githubusercontent.com/LearnOpenGL-CN/LearnOpenGL-CN/master/img/Light_casters1.png) -这里![Epsilon](https://raw.githubusercontent.com/LearnOpenGL-CN/LearnOpenGL-CN/master/img/epsilon.png)是内部(![Phi](https://raw.githubusercontent.com/LearnOpenGL-CN/LearnOpenGL-CN/master/img/phi.png))和外部圆锥(![Gamma](https://raw.githubusercontent.com/LearnOpenGL-CN/LearnOpenGL-CN/master/img/gamma.png))![Latex Formula](https://raw.githubusercontent.com/LearnOpenGL-CN/LearnOpenGL-CN/master/img/Light_casters3.png)的差。结果I的值是聚光灯在当前片段的亮度。 + +$$ +\begin{equation} I = \frac{\theta - \gamma}{\epsilon} \end{equation} +$$ + +这里\(\epsilon\)是内部(\(\phi\))和外部圆锥(\(\gamma\))(\epsilon = \phi - \gamma)的差。结果\(I\)的值是聚光灯在当前片段的亮度。 很难用图画描述出这个公式是怎样工作的,所以我们尝试使用一个例子: -θ|θ in degrees|φ (inner cutoff)|φ in degrees|γ (outer cutoff)|γ in degrees|ε|l +\(\theta\)|角度制\(\theta\)|\(\phi\)(内切)|角度制\(\phi\)|\(\gamma\)(外切)|角度制\(\gamma\)|\(\epsilon\)|\(I\) --|---|---|---|---|---|---|--- 0.87|30|0.91|25|0.82|35|0.91 - 0.82 = 0.09|0.87 - 0.82 / 0.09 = 0.56 0.9|26|0.91|25|0.82|35|0.91 - 0.82 = 0.09|0.9 - 0.82 / 0.09 = 0.89 @@ -308,14 +315,14 @@ specular* = intensity; 注意,我们使用了`clamp`函数,它把第一个参数固定在0.0和1.0之间。这保证了亮度值不会超出[0, 1]以外。 -确定你把`outerCutOff`值添加到了`Light`结构体,并在应用中设置了它的uniform值。对于下面的图片,内部遮光角`12.5f`,外部遮光角是`17.5f`: +确定你把`outerCutOff`值添加到了`Light`结构体,并在应用中设置了它的uniform值。对于下面的图片,内部切光角`12.5f`,外部切光角是`17.5f`: ![](http://www.learnopengl.com/img/lighting/light_casters_spotlight.png) -看起来好多了。仔细看看内部和外部遮光角,尝试创建一个符合你求的聚光灯。可以在这里找到应用源码,以及片段的源代码。 +看起来好多了。仔细看看内部和外部切光角,尝试创建一个符合你求的聚光灯。可以在这里找到应用源码,以及片段的源代码。 这样的一个手电筒/聚光灯类型的灯光非常适合恐怖游戏,结合定向和点光,环境会真的开始被照亮了。[下一个教程](http://learnopengl-cn.readthedocs.org/zh/latest/02%20Lighting/06%20Multiple%20lights/),我们会结合所有我们目前讨论了的光和技巧。 ## 练习 - - 试着修改上面的各种不同种类的光源及其片段着色器。试着将部分矢量进行反向并尝试使用 < 来代替 > 。试着解释这些修改导致不同显示效果的原因。 +- 试着修改上面的各种不同种类的光源及其片段着色器。试着将部分矢量进行反向并尝试使用 < 来代替 > 。试着解释这些修改导致不同显示效果的原因。 diff --git a/docs/03 Model Loading/01 Assimp.md b/docs/03 Model Loading/01 Assimp.md index cb23cea..693db6c 100644 --- a/docs/03 Model Loading/01 Assimp.md +++ b/docs/03 Model Loading/01 Assimp.md @@ -19,9 +19,9 @@ 当导入一个模型文件时,即Assimp加载一整个包含所有模型和场景数据的模型文件到一个scene对象时,Assimp会为这个模型文件中的所有场景节点、模型节点都生成一个具有对应关系的数据结构,且将这些场景中的各种元素与模型数据对应起来。下图展示了一个简化的Assimp生成的模型文件数据结构: -
- -
+ +![](http://learnopengl.com/img/model_loading/assimp_structure.png) + - 所有的模型、场景数据都包含在scene对象中,如所有的材质和Mesh。同样,场景的根节点引用也包含在这个scene对象中 - 场景的根节点可能也会包含很多子节点和一个指向保存模型点云数据mMeshes[]的索引集合。根节点上的mMeshes[]里保存了实际了Mesh对象,而每个子节点上的mMesshes[]都只是指向根节点中的mMeshes[]的一个引用(译者注:C/C++称为指针,Java/C#称为引用) diff --git a/docs/04 Advanced OpenGL/01 Depth testing.md b/docs/04 Advanced OpenGL/01 Depth testing.md index b56dc4f..77318e6 100644 --- a/docs/04 Advanced OpenGL/01 Depth testing.md +++ b/docs/04 Advanced OpenGL/01 Depth testing.md @@ -83,7 +83,9 @@ glDepthFunc(GL_ALWAYS); 在深度缓冲区中包含深度值介于`0.0`和`1.0`之间,从观察者看到其内容与场景中的所有对象的 z 值进行了比较。这些视图空间中的 z 值可以在投影平头截体的近平面和远平面之间的任何值。我们因此需要一些方法来转换这些视图空间 z 值到 [0,1] 的范围内,方法之一就是线性将它们转换为 [0,1] 范围内。下面的 (线性) 方程把 z 值转换为 0.0 和 1.0 之间的值 : -![](../img/05_01_F_depth.png) +$$ +\begin{equation} F_{depth} = \frac{z - near}{far - near} \end{equation} +$$ 这里far和near是我们用来提供到投影矩阵设置可见视图截锥的远近值 (见[坐标系](http://learnopengl-cn.readthedocs.org/zh/latest/01%20Getting%20started/08%20Coordinate%20Systems/))。方程带内锥截体的深度值 z,并将其转换到 [0,1] 范围。在下面的图给出 z 值和其相应的深度值的关系: @@ -97,7 +99,9 @@ glDepthFunc(GL_ALWAYS); 由于非线性函数是和 1/z 成正比,例如1.0 和 2.0 之间的 z 值,将变为 1.0 到 0.5之间, 这样在z非常小的时候给了我们很高的精度。50.0 和 100.0 之间的 Z 值将只占 2%的浮点数的精度,这正是我们想要的。这类方程,也需要近和远距离考虑,下面给出: -![](../img/05_01_F_depth_nonliner.png) +$$ +\begin{equation} F_{depth} = \frac{1/z - 1/near}{1/far - 1/near} \end{equation} +$$ 如果你不知道这个方程到底怎么回事也不必担心。要记住的重要一点是在深度缓冲区的值不是线性的屏幕空间 (它们在视图空间投影矩阵应用之前是线性)。值为 0.5 在深度缓冲区并不意味着该对象的 z 值是投影平头截体的中间;顶点的 z 值是实际上相当接近近平面!你可以看到 z 值和产生深度缓冲区的值在下列图中的非线性关系: diff --git a/docs/04 Advanced OpenGL/03 Blending.md b/docs/04 Advanced OpenGL/03 Blending.md index a7090ad..773ec89 100644 --- a/docs/04 Advanced OpenGL/03 Blending.md +++ b/docs/04 Advanced OpenGL/03 Blending.md @@ -130,12 +130,14 @@ glEnable(GL_BLEND); OpenGL以下面的方程进行混合: -C¯result = C¯source ∗ Fsource + C¯destination ∗ Fdestination +$$ +\begin{equation}\bar{C}_{result} = \bar{\color{green}C}_{source} * \color{green}F_{source} + \bar{\color{red}C}_{destination} * \color{red}F_{destination}\end{equation} +$$ -* C¯source:源颜色向量。这是来自纹理的本来的颜色向量。 -* C¯destination:目标颜色向量。这是储存在颜色缓冲中当前位置的颜色向量。 -* Fsource:源因子。设置了对源颜色的alpha值影响。 -* Fdestination:目标因子。设置了对目标颜色的alpha影响。 +* \(\bar{\color{green}C}_{source}\):源颜色向量。这是来自纹理的本来的颜色向量。 +* \(\bar{\color{red}C}_{destination}\):目标颜色向量。这是储存在颜色缓冲中当前位置的颜色向量。 +* \(\color{green}F_{source}\):源因子。设置了对源颜色的alpha值影响。 +* \(\color{red}F_{destination}\):目标因子。设置了对目标颜色的alpha影响。 片段着色器运行完成并且所有的测试都通过以后,混合方程才能自由执行片段的颜色输出,当前它在颜色缓冲中(前面片段的颜色在当前片段之前储存)。源和目标颜色会自动被OpenGL设置,而源和目标因子可以让我们自由设置。我们来看一个简单的例子: @@ -143,9 +145,11 @@ C¯result = C¯source ∗ Fsource + C¯destination ∗ Fdestination 我们有两个方块,我们希望在红色方块上绘制绿色方块。红色方块会成为源颜色(它会先进入颜色缓冲),我们将在红色方块上绘制绿色方块。 -那么问题来了:我们怎样来设置因子呢?我们起码要把绿色方块乘以它的alpha值,所以我们打算把Fsource设置为源颜色向量的alpha值:0.6。接着,让目标方块的浓度等于剩下的alpha值。如果最终的颜色中绿色方块的浓度为60%,我们就把红色的浓度设为40%(1.0 – 0.6)。所以我们把Fdestination设置为1减去源颜色向量的alpha值。方程将变成: +那么问题来了:我们怎样来设置因子呢?我们起码要把绿色方块乘以它的alpha值,所以我们打算把\(F_{src}\)设置为源颜色向量的alpha值:0.6。接着,让目标方块的浓度等于剩下的alpha值。如果最终的颜色中绿色方块的浓度为60%,我们就把红色的浓度设为40%(1.0 – 0.6)。所以我们把\(F_{destination}\)设置为1减去源颜色向量的alpha值。方程将变成: -![](../img/blending_C_result.png) +$$ +\begin{equation}\bar{C}_{result} = \begin{pmatrix} \color{red}{0.0} \\ \color{green}{1.0} \\ \color{blue}{0.0} \\ \color{purple}{0.6} \end{pmatrix} * \color{green}{0.6} + \begin{pmatrix} \color{red}{1.0} \\ \color{green}{0.0} \\ \color{blue}{0.0} \\ \color{purple}{1.0} \end{pmatrix} * \color{red}{(1 - 0.6)} \end{equation} +$$ 最终方块结合部分包含了60%的绿色和40%的红色,得到一种脏兮兮的颜色: @@ -155,27 +159,27 @@ C¯result = C¯source ∗ Fsource + C¯destination ∗ Fdestination 这个方案不错,但我们怎样告诉OpenGL来使用这样的因子呢?恰好有一个叫做`glBlendFunc`的函数。 -`void glBlendFunc(GLenum sfactor, GLenum dfactor)`接收两个参数,来设置源(source)和目标(destination)因子。OpenGL为我们定义了很多选项,我们把最常用的列在下面。注意,颜色常数向量C¯constant可以用`glBlendColor`函数分开来设置。 +`void glBlendFunc(GLenum sfactor, GLenum dfactor)`接收两个参数,来设置源(source)和目标(destination)因子。OpenGL为我们定义了很多选项,我们把最常用的列在下面。注意,颜色常数向量\(\bar{\color{blue}C}_{constant}\)可以用`glBlendColor`函数分开来设置。 -Option | Value +选项 | 值 ---|--- -GL_ZERO | 0 -GL_ONE | 1 -GL_SRC_COLOR | 颜色C¯source. -GL_ONE_MINUS_SRC_COLOR | 1 − C¯source. -GL_DST_COLOR | C¯destination -GL_ONE_MINUS_DST_COLOR | 1 − C¯destination. -GL_SRC_ALPHA | C¯source的alpha值 -GL_ONE_MINUS_SRC_ALPHA | 1 - C¯source的alpha值 -GL_DST_ALPHA | C¯destination的alpha值 -GL_ONE_MINUS_DST_ALPHA | 1 - C¯destination的alpha值 -GL_CONSTANT_COLOR | C¯constant. -GL_ONE_MINUS_CONSTANT_COLOR | 1 - C¯constant -GL_CONSTANT_ALPHA | C¯constant的alpha值 -GL_ONE_MINUS_CONSTANT_ALPHA | 1 − C¯constant的alpha值 +GL_ZERO | \(0\) +GL_ONE | \(1\) +GL_SRC_COLOR | 源颜色向量\(\bar{\color{green}C}_{source}\) +GL_ONE_MINUS_SRC_COLOR | \(1 - \bar{\color{green}C}_{source}\) +GL_DST_COLOR | 目标颜色向量\(\bar{\color{red}C}_{destination}\) +GL_ONE_MINUS_DST_COLOR | \(1 - \bar{\color{red}C}_{destination}\) +GL_SRC_ALPHA | \(\bar{\color{green}C}_{source}\)的\(alpha\)值 +GL_ONE_MINUS_SRC_ALPHA | \(1 -\) \(\bar{\color{green}C}_{source}\)的\(alpha\)值 +GL_DST_ALPHA | \(\bar{\color{red}C}_{destination}\)的\(alpha\)值 +GL_ONE_MINUS_DST_ALPHA | \(1 -\) \(\bar{\color{red}C}_{destination}\)的\(alpha\)值 +GL_CONSTANT_COLOR | 常颜色向量\(\bar{\color{blue}C}_{constant}\) +GL_ONE_MINUS_CONSTANT_COLOR | \(1 - \bar{\color{blue}C}_{constant}\) +GL_CONSTANT_ALPHA | \(\bar{\color{blue}C}_{constant}\)的\(alpha\)值 +GL_ONE_MINUS_CONSTANT_ALPHA | \(1 -\) \(\bar{\color{blue}C}_{constant}\)的\(alpha\)值 -为从两个方块获得混合结果,我们打算把源颜色的alpha给源因子,1-alpha给目标因子。调整到`glBlendFunc`之后就像这样: +为从两个方块获得混合结果,我们打算把源颜色的\(alpha\)给源因子,\(1 - alpha\)给目标因子。调整到`glBlendFunc`之后就像这样: ```c++ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); @@ -193,9 +197,9 @@ OpenGL给了我们更多的自由,我们可以改变方程源和目标部分 `void glBlendEquation(GLenum mode)`允许我们设置这个操作,有3种可行的选项: -* GL_FUNC_ADD:默认的,彼此元素相加:C¯result = Src + Dst. -* GL_FUNC_SUBTRACT:彼此元素相减: C¯result = Src – Dst. -* GL_FUNC_REVERSE_SUBTRACT:彼此元素相减,但顺序相反:C¯result = Dst – Src. +* GL_FUNC_ADD:默认的,彼此元素相加:\(\bar{C}_{result} = \color{green}{Src} + \color{red}{Dst}\) +* GL_FUNC_SUBTRACT:彼此元素相减: \(\bar{C}_{result} = \color{green}{Src} - \color{red}{Dst}\) +* GL_FUNC_REVERSE_SUBTRACT:彼此元素相减,但顺序相反:\(\bar{C}_{result} = \color{red}{Dst} - \color{green}{Src}\). 通常我们可以简单地省略`glBlendEquation`因为GL_FUNC_ADD在大多数时候就是我们想要的,但是如果你如果你真想尝试努力打破主流常规,其他的方程或许符合你的要求。 diff --git a/docs/04 Advanced OpenGL/05 Framebuffers.md b/docs/04 Advanced OpenGL/05 Framebuffers.md index 92ca7e7..b113efd 100644 --- a/docs/04 Advanced OpenGL/05 Framebuffers.md +++ b/docs/04 Advanced OpenGL/05 Framebuffers.md @@ -332,7 +332,9 @@ void main() kernel是一个长得有点像一个小矩阵的数值数组,它中间的值中心可以映射到一个像素上,这个像素和这个像素周围的值再乘以kernel,最后再把结果相加就能得到一个值。所以,我们基本上就是给当前纹理坐标加上一个它四周的偏移量,然后基于kernel把它们结合起来。下面是一个kernel的例子: -![](http://learnopengl-cn.readthedocs.org/zh/latest/img/05_05framebuffers_ kernel_sample.png) +$$ +\begin{bmatrix}2 & 2 & 2 \\ 2 & -15 & 2 \\ 2 & 2 & 2 \end{bmatrix} +$$ 这个kernel表示一个像素周围八个像素乘以2,它自己乘以-15。这个例子基本上就是把周围像素乘上2,中间像素去乘以一个比较大的负数来进行平衡。 @@ -390,7 +392,7 @@ void main() 创建模糊效果的kernel定义如下: -![](http://learnopengl-cn.readthedocs.org/zh/latest/img/05_05_blur_sample.png) +\(\begin{bmatrix} 1 & 2 & 1 \\ 2 & 4 & 2 \\ 1 & 2 & 1 \end{bmatrix} / 16\) 由于所有数值加起来的总和为16,简单返回结合起来的采样颜色是非常亮的,所以我们必须将kernel的每个值除以16.最终的kernel数组会是这样的: @@ -414,7 +416,9 @@ float kernel[9] = float[]( 下面的边检测kernel与锐化kernel类似: -![](http://learnopengl-cn.readthedocs.org/zh/latest/img/05_05_Edge_detection.png) +$$ +\begin{bmatrix} 1 & 1 & 1 \\ 1 & -8 & 1 \\ 1 & 1 & 1 \end{bmatrix} +$$ 这个kernel将所有的边提高亮度,而对其他部分进行暗化处理,当我们值关心一副图像的边缘的时候,它非常有用. diff --git a/docs/05 Advanced Lighting/01 Advanced Lighting.md b/docs/05 Advanced Lighting/01 Advanced Lighting.md index 27dc91d..25e0354 100644 --- a/docs/05 Advanced Lighting/01 Advanced Lighting.md +++ b/docs/05 Advanced Lighting/01 Advanced Lighting.md @@ -34,7 +34,7 @@ Phong光照很棒,而且性能较高,但是它的镜面反射在某些条件 得到半程向量很容易,我们将光的方向向量和视线向量相加,然后将结果归一化(normalize); -![](http://learnopengl-cn.readthedocs.org/zh/latest/img/05_01_01.png) +\(\bar{H} = \frac{\bar{L} + \bar{V}}{||\bar{L} + \bar{V}||}\) 翻译成GLSL代码如下: diff --git a/docs/05 Advanced Lighting/02 Gamma Correction.md b/docs/05 Advanced Lighting/02 Gamma Correction.md index 0929393..1a0c5af 100644 --- a/docs/05 Advanced Lighting/02 Gamma Correction.md +++ b/docs/05 Advanced Lighting/02 Gamma Correction.md @@ -1,6 +1,6 @@ 本文作者JoeyDeVries,由Django翻译自[http://learnopengl.com](http://learnopengl.com) -## Gamma校正(Gamma Correction) +# Gamma校正(Gamma Correction) 当我们计算出场景中所有像素的最终颜色以后,我们就必须把它们显示在监视器上。过去,大多数监视器是阴极射线管显示器(CRT)。这些监视器有一个物理特性就是两倍的输入电压产生的不是两倍的亮度。输入电压产生约为输入电压的2.2次幂的亮度,这叫做监视器Gamma(译注:Gamma也叫灰度系数,每种显示设备都有自己的Gamma值,都不相同,有一个公式:设备输出亮度 = 电压的Gamma次幂,任何设备Gamma基本上都不会等于1,等于1是一种理想的线性状态,这种理想状态是:如果电压和亮度都是在0到1的区间,那么多少电压就等于多少亮度。对于CRT,Gamma通常为2.2,因而,输出亮度 = 输入电压的2.2次幂,你可以从本节第二张图中看到Gamma2.2实际显示出来的总会比预期暗,相反Gamma0.45就会比理想预期亮,如果你讲Gamma0.45叠加到Gamma2.2的显示设备上,便会对偏暗的显示效果做到校正,这个简单的思路就是本节的核心)。 @@ -16,7 +16,7 @@ ![](http://learnopengl.com/img/advanced-lighting/gamma_correction_gamma_curves.png) -点线代表线性颜色/亮度值(译注:这表示的是理想状态,Gamma为1),实线代表监视器显示的颜色。如果我们把一个点线线性的颜色翻一倍,结果就是这个值的两倍。比如,光的颜色向量L=(0.5, 0.0, 0.0)代表的是暗红色。如果我们在线性空间中把它翻倍,就会变成(1.0, 0.0, 0.0),就像你在图中看到的那样。然而,由于我们定义的颜色仍然需要输出的监视器上,监视器上显示的实际颜色就会是(0.218, 0.0, 0.0)。在这儿问题就出现了:当我们将理想中直线上的那个暗红色翻一倍时,在监视器上实际上亮度翻了4.5倍以上! +点线代表线性颜色/亮度值(译注:这表示的是理想状态,Gamma为1),实线代表监视器显示的颜色。如果我们把一个点线线性的颜色翻一倍,结果就是这个值的两倍。比如,光的颜色向量\(\bar{L} = (0.5, 0.0, 0.0)\)代表的是暗红色。如果我们在线性空间中把它翻倍,就会变成\((1.0, 0.0, 0.0)\),就像你在图中看到的那样。然而,由于我们定义的颜色仍然需要输出的监视器上,监视器上显示的实际颜色就会是\((0.218, 0.0, 0.0)\)。在这儿问题就出现了:当我们将理想中直线上的那个暗红色翻一倍时,在监视器上实际上亮度翻了4.5倍以上! 直到现在,我们还一直假设我们所有的工作都是在线性空间中进行的(译注:Gamma为1),但最终还是要把所哟的颜色输出到监视器上,所以我们配置的所有颜色和光照变量从物理角度来看都是不正确的,在我们的监视器上很少能够正确地显示。出于这个原因,我们(以及艺术家)通常将光照值设置得比本来更亮一些(由于监视器会将其亮度显示的更暗一些),如果不是这样,在线性空间里计算出来的光照就会不正确。同时,还要记住,监视器所显示出来的图像和线性图像的最小亮度是相同的,它们最大的亮度也是相同的;只是中间亮度部分会被压暗。 @@ -24,22 +24,11 @@ ![](http://learnopengl.com/img/advanced-lighting/gamma_correction_example.png) -Gamma校正 +## Gamma校正 Gamma校正的思路是在最终的颜色输出上应用监视器Gamma的倒数。回头看前面的Gamma曲线图,你会有一个短划线,它是监视器Gamma曲线的翻转曲线。我们在颜色显示到监视器的时候把每个颜色输出都加上这个翻转的Gamma曲线,这样应用了监视器Gamma以后最终的颜色将会变为线性的。我们所得到的中间色调就会更亮,所以虽然监视器使它们变暗,但是我们又将其平衡回来了。 -我们来看另一个例子。还是那个暗红色(0.5, 0.0, 0.0)。在将颜色显示到监视器之前,我们先对颜色应用Gamma校正曲线。线性的颜色显示在监视器上相当于降低了2.2次幂的亮度,所以倒数就是1/2.2次幂。Gamma校正后的暗红色就会成为 - -```math -{(0.5, 0.0, 0.0)}^{1/2.2} = {(0.5, 0.0, 0.0)}^{0.45}={(0.73, 0.0, 0.0)} -``` - -校正后的颜色接着被发送给监视器,最终显示出来的颜色是 - -```math -(0.73, 0.0, 0.0)^{2.2} = (0.5, 0.0, 0.0) -``` -你会发现使用了Gamma校正,监视器最终会显示出我们在应用中设置的那种线性的颜色。 +我们来看另一个例子。还是那个暗红色\((0.5, 0.0, 0.0)\)。在将颜色显示到监视器之前,我们先对颜色应用Gamma校正曲线。线性的颜色显示在监视器上相当于降低了\(2.2\)次幂的亮度,所以倒数就是\(1/2.2\)次幂。Gamma校正后的暗红色就会成为\((0.5, 0.0, 0.0)^{1/2.2} = (0.5, 0.0, 0.0)^{0.45} = (0.73, 0.0, 0.0)\)。校正后的颜色接着被发送给监视器,最终显示出来的颜色是\((0.73, 0.0, 0.0)^{2.2} = (0.5, 0.0, 0.0)\)。你会发现使用了Gamma校正,监视器最终会显示出我们在应用中设置的那种线性的颜色。 !!! Important @@ -130,17 +119,7 @@ float attenuation = 1.0 / distance; ![](http://learnopengl.com/img/advanced-lighting/gamma_correction_attenuation.png) -这种差异产生的原因是,光的衰减方程改变了亮度值,而且屏幕上显示出来的也不是线性空间,在监视器上效果最好的衰减方程,并不是符合物理的。想想平方衰减方程,如果我们使用这个方程,而且不进行gamma校正,显示在监视器上的衰减方程实际上将变成: - -```math -{(1.0 / distance2)}^{2.2} -``` -若不进行gamma校正,将产生更强烈的衰减。这也解释了为什么双曲线不用gamma校正时看起来更真实,因为它实际变成了 - -```math -{(1.0 / distance)}^{2.2} = 1.0 / distance^{2.2} -``` -这和物理公式是很相似的。 +这种差异产生的原因是,光的衰减方程改变了亮度值,而且屏幕上显示出来的也不是线性空间,在监视器上效果最好的衰减方程,并不是符合物理的。想想平方衰减方程,如果我们使用这个方程,而且不进行gamma校正,显示在监视器上的衰减方程实际上将变成\((1.0 / distance^2)^{2.2}\)。若不进行gamma校正,将产生更强烈的衰减。这也解释了为什么双曲线不用gamma校正时看起来更真实,因为它实际变成了\((1.0 / distance)^{2.2} = 1.0 / distance^{2.2}\)。这和物理公式是很相似的。 !!! Important diff --git a/docs/05 Advanced Lighting/03 Shadows/01 Shadow Mapping.md b/docs/05 Advanced Lighting/03 Shadows/01 Shadow Mapping.md index 2f6722f..3296d25 100644 --- a/docs/05 Advanced Lighting/03 Shadows/01 Shadow Mapping.md +++ b/docs/05 Advanced Lighting/03 Shadows/01 Shadow Mapping.md @@ -34,13 +34,13 @@ ![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_theory_spaces.png) -左侧的图片展示了一个定向光源(所有光线都是平行的)在立方体下的表面投射的阴影。通过储存到深度贴图中的深度值,我们就能找到最近点,用以决定片元是否在阴影中。我们使用一个来自光源的视图和投影矩阵来渲染场景就能创建一个深度贴图。这个投影和视图矩阵结合在一起成为一个T变换,它可以将任何三维位置转变到光源的可见坐标空间。 +左侧的图片展示了一个定向光源(所有光线都是平行的)在立方体下的表面投射的阴影。通过储存到深度贴图中的深度值,我们就能找到最近点,用以决定片元是否在阴影中。我们使用一个来自光源的视图和投影矩阵来渲染场景就能创建一个深度贴图。这个投影和视图矩阵结合在一起成为一个\(T\)变换,它可以将任何三维位置转变到光源的可见坐标空间。 !!! Important 定向光并没有位置,因为它被规定为无穷远。然而,为了实现阴影贴图,我们得从一个光的透视图渲染场景,这样就得在光的方向的某一点上渲染场景。 -在右边的图中我们显示出同样的平行光和观察者。我们渲染一个点P处的片元,需要决定它是否在阴影中。我们先得使用T把P变换到光源的坐标空间里。既然点P是从光的透视图中看到的,它的z坐标就对应于它的深度,例子中这个值是0.9。使用点P在光源的坐标空间的坐标,我们可以索引深度贴图,来获得从光的视角中最近的可见深度,结果是点C,最近的深度是0.4。因为索引深度贴图的结果是一个小于点P的深度,我们可以断定P被挡住了,它在阴影中了。 +在右边的图中我们显示出同样的平行光和观察者。我们渲染一个点\(\bar{\color{red}{P}}\)处的片元,需要决定它是否在阴影中。我们先得使用\(T\)把\(\bar{\color{red}{P}}\)变换到光源的坐标空间里。既然点\(\bar{\color{red}{P}}\)是从光的透视图中看到的,它的z坐标就对应于它的深度,例子中这个值是0.9。使用点\(\bar{\color{red}{P}}\)在光源的坐标空间的坐标,我们可以索引深度贴图,来获得从光的视角中最近的可见深度,结果是点\(\bar{\color{green}{C}}\),最近的深度是0.4。因为索引深度贴图的结果是一个小于点\(\bar{\color{red}{P}}\)的深度,我们可以断定\(\bar{\color{red}{P}}\)被挡住了,它在阴影中了。 深度映射由两个步骤组成:首先,我们渲染深度贴图,然后我们像往常一样渲染场景,使用生成的深度贴图来计算片元是否在阴影之中。听起来有点复杂,但随着我们一步一步地讲解这个技术,就能理解了。 @@ -130,7 +130,7 @@ glm::mat4 lightView = glm::lookAt(glm::vec(-2.0f, 4.0f, -1.0f), glm::vec3(0.0f), glm::mat4 lightSpaceMatrix = lightProjection * lightView; ``` -这个lightSpaceMatrix正是前面我们称为T的那个变换矩阵。有了lightSpaceMatrix只要给shader提供光空间的投影和视图矩阵,我们就能像往常那样渲染场景了。然而,我们只关心深度值,并非所有片元计算都在我们的着色器中进行。为了提升性能,我们将使用一个与之不同但更为简单的着色器来渲染出深度贴图。 +这个lightSpaceMatrix正是前面我们称为\(T\)的那个变换矩阵。有了lightSpaceMatrix只要给shader提供光空间的投影和视图矩阵,我们就能像往常那样渲染场景了。然而,我们只关心深度值,并非所有片元计算都在我们的着色器中进行。为了提升性能,我们将使用一个与之不同但更为简单的着色器来渲染出深度贴图。 ### 渲染出深度贴图 diff --git a/docs/05 Advanced Lighting/03 Shadows/03 CSM.md b/docs/05 Advanced Lighting/03 Shadows/03 CSM.md index e69de29..5434205 100644 --- a/docs/05 Advanced Lighting/03 Shadows/03 CSM.md +++ b/docs/05 Advanced Lighting/03 Shadows/03 CSM.md @@ -0,0 +1,7 @@ +# CSM + +# 未完成 + +这篇教程暂时还没有完成,您可以经常来刷新看看是否有更新的进展。 + + \ No newline at end of file diff --git a/docs/05 Advanced Lighting/04 Normal Mapping.md b/docs/05 Advanced Lighting/04 Normal Mapping.md index de09369..f6c5c00 100644 --- a/docs/05 Advanced Lighting/04 Normal Mapping.md +++ b/docs/05 Advanced Lighting/04 Normal Mapping.md @@ -29,8 +29,10 @@ 由于法线向量是个几何工具,而纹理通常只用于储存颜色信息,用纹理储存法线向量不是非常直接。如果你想一想,就会知道纹理中的颜色向量用r、g、b元素代表一个3D向量。类似的我们也可以将法线向量的x、y、z元素储存到纹理中,代替颜色的r、g、b元素。法线向量的范围在-1到1之间,所以我们先要将其映射到0到1的范围: -1 -vec3 rgb_normal = normal * 0.5 - 0.5; // transforms from [-1,1] to [0,1] +```c++ +vec3 rgb_normal = normal * 0.5 - 0.5; // transforms from [-1,1] to [0,1] +``` + 将法线向量变换为像这样的RGB颜色元素,我们就能把根据表面的形状的fragment的法线保存在2D纹理中。教程开头展示的那个砖块的例子的法线贴图如下所示: ![](http://learnopengl.com/img/advanced-lighting/normal_mapping_normal_map.png) @@ -92,555 +94,46 @@ void main() ![](http://learnopengl.com/img/advanced-lighting/normal_mapping_surface_edges.png) -上图中我们可以看到边 - - E - 2 - -纹理坐标的不同, - - E - 2 - -是一个三角形的边,这个三角形的另外两条边是 - Δ - - U - 2 - - - Δ - - V - 2 - -,它们与切线向量*T*和副切线向量*B*方向相同。这样我们可以把边和 - - E - 1 - - - - E - 2 - -用切线向量 *T* 和副切线向量 *B* 的线性组合表示出来(译注:注意*T*和*B*都是单位长度,在*TB*平面中所有点的*T*、*B*坐标都在0到1之间,因此可以进行这样的组合): +上图中我们可以看到边\(E_2\)纹理坐标的不同,\(E_2\)是一个三角形的边,这个三角形的另外两条边是\(\Delta U_2\)和\(\Delta V_2\),它们与切线向量\(T\)和副切线向量\(B\)方向相同。这样我们可以把边\(E_1\)和\(E_2\)用切线向量\(T\)和副切线向量\(B\)的线性组合表示出来(译注:注意\(T\)和\(B\)都是单位长度,在\(TB\)平面中所有点的\(T\)、\(B\)坐标都在0到1之间,因此可以进行这样的组合): -```math +$$ E_1 = \Delta U_1T + \Delta V_1B +$$ +$$ E_2 = \Delta U_2T + \Delta V_2B -``` +$$ 我们也可以写成这样: -```math +$$ (E_{1x}, E_{1y}, E_{1z}) = \Delta U_1(T_x, T_y, T_z) + \Delta V_1(B_x, B_y, B_z) -``` +$$ -*E*是两个向量位置的差,*U*和*V*是纹理坐标的差。然后我们得到两个未知数(切线*T*和副切线*B*)和两个等式。你可能想起你的代数课了,这是让我们去接*T*和*B*。 +$$ +(E_{2x}, E_{2y}, E_{2z}) = \Delta U_2(T_x, T_y, T_z) + \Delta V_2(B_x, B_y, B_z) +$$ + +\(E\)是两个向量位置的差,\(\Delta U\)和\(\Delta V\)是纹理坐标的差。然后我们得到两个未知数(切线*T*和副切线*B*)和两个等式。你可能想起你的代数课了,这是让我们去接\(T\)和\(B\)。 上面的方程允许我们把它们写成另一种格式:矩阵乘法 - - - [ - - - - - E - - 1 - x - - - - - - E - - 1 - y - - - - - - E - - 1 - z - - - - - - - - E - - 2 - x - - - - - - E - - 2 - y - - - - - - E - - 2 - z - - - - - - ] - - = - - [ - - - - Δ - - U - 1 - - - - Δ - - V - 1 - - - - - - Δ - - U - 2 - - - - Δ - - V - 2 - - - - - ] - - - [ - - - - - T - x - - - - - T - y - - - - - T - z - - - - - - - B - x - - - - - B - y - - - - - B - z - - - - - ] - - +$$ +\begin{bmatrix} E_{1x} & E_{1y} & E_{1z} \\ E_{2x} & E_{2y} & E_{2z} \end{bmatrix} = \begin{bmatrix} \Delta U_1 & \Delta V_1 \\ \Delta U_2 & \Delta V_2 \end{bmatrix} \begin{bmatrix} T_x & T_y & T_z \\ B_x & B_y & B_z \end{bmatrix} +$$ -尝试会以一下矩阵乘法,它们确实是同一种等式。把等式写成矩阵形式的好处是,解*T*和*B*会因此变得很容易。两边都乘以 - Δ - U - Δ - V -的反数等于: +尝试会意一下矩阵乘法,它们确实是同一种等式。把等式写成矩阵形式的好处是,解\(T\)和\(B\)会因此变得很容易。两边都乘以\(\Delta U \Delta V\)的逆矩阵等于: - - - - [ - - - - Δ - - U - 1 - - - - Δ - - V - 1 - - - - - - Δ - - U - 2 - - - - Δ - - V - 2 - - - - - ] - - - - 1 - - - - [ - - - - - E - - 1 - x - - - - - - E - - 1 - y - - - - - - E - - 1 - z - - - - - - - - E - - 2 - x - - - - - - E - - 2 - y - - - - - - E - - 2 - z - - - - - - ] - - = - - [ - - - - - T - x - - - - - T - y - - - - - T - z - - - - - - - B - x - - - - - B - y - - - - - B - z - - - - - ] - - +$$ +\begin{bmatrix} \Delta U_1 & \Delta V_1 \\ \Delta U_2 & \Delta V_2 \end{bmatrix}^{-1} \begin{bmatrix} E_{1x} & E_{1y} & E_{1z} \\ E_{2x} & E_{2y} & E_{2z} \end{bmatrix} = \begin{bmatrix} T_x & T_y & T_z \\ B_x & B_y & B_z \end{bmatrix} +$$ -这样我们就可以解出*T*和*B*了。这需要我们计算出delta纹理坐标矩阵的拟阵。我不打算讲解计算逆矩阵的细节,但大致是把它变化为,1除以矩阵的行列式,再乘以它的共轭矩阵。 +这样我们就可以解出\(T\)和\(B\)了。这需要我们计算出delta纹理坐标矩阵的拟阵。我不打算讲解计算逆矩阵的细节,但大致是把它变化为,1除以矩阵的行列式,再乘以它的共轭矩阵。 - - - [ - - - - - T - x - - - - - T - y - - - - - T - z - - - - - - - B - x - - - - - B - y - - - - - B - z - - - - - ] - - = - - 1 - - Δ - - U - 1 - - Δ - - V - 2 - - - Δ - - U - 2 - - Δ - - V - 1 - - - - - [ - - - - Δ - - V - 2 - - - - - Δ - - V - 1 - - - - - - - Δ - - U - 2 - - - - Δ - - U - 1 - - - - - ] - - - [ - - - - - E - - 1 - x - - - - - - E - - 1 - y - - - - - - E - - 1 - z - - - - - - - - E - - 2 - x - - - - - - E - - 2 - y - - - - - - E - - 2 - z - - - - - - ] - - +$$ +\begin{bmatrix} T_x & T_y & T_z \\ B_x & B_y & B_z \end{bmatrix} = \frac{1}{\Delta U_1 \Delta V_2 - \Delta U_2 \Delta V_1} \begin{bmatrix} \Delta V_2 & -\Delta V_1 \\ -\Delta U_2 & \Delta U_1 \end{bmatrix} \begin{bmatrix} E_{1x} & E_{1y} & E_{1z} \\ E_{2x} & E_{2y} & E_{2z} \end{bmatrix} +$$ -有了最后这个等式,我们就可以用公式、三角形的两条边以及纹理坐标计算出切线向量*T*和副切线*B*。 +有了最后这个等式,我们就可以用公式、三角形的两条边以及纹理坐标计算出切线向量\(T\)和副切线\(B\)。 如果你对这些数学内容不理解也不用担心。当你知道我们可以用一个三角形的顶点和纹理坐标(因为纹理坐标和切线向量在同一空间中)计算出切线和副切线你就已经部分地达到目的了(译注:上面的推导已经很清楚了,如果你不明白可以参考任意线性代数教材,就像作者所说的记住求得切线空间的公式也行,不过不管怎样都得理解切线空间的含义)。 @@ -649,6 +142,8 @@ E_2 = \Delta U_2T + \Delta V_2B 这个教程的demo场景中有一个简单的2D平面,它朝向正z方向。这次我们会使用切线空间来实现法线贴图,所以我们可以使平面朝向任意方向,法线贴图仍然能够工作。使用前面讨论的数学方法,我们来手工计算出表面的切线和副切线向量。 假设平面使用下面的向量建立起来(1、2、3和1、3、4,它们是两个三角形): + +```c++ // positions glm::vec3 pos1(-1.0, 1.0, 0.0); glm::vec3 pos2(-1.0, -1.0, 0.0); @@ -661,20 +156,18 @@ glm::vec2 uv3(1.0, 0.0); glm::vec2 uv4(1.0, 1.0); // normal vector glm::vec3 nm(0.0, 0.0, 1.0); - +``` 我们先计算第一个三角形的边和deltaUV坐标: -1 -2 -3 -4 +```c++ glm::vec3 edge1 = pos2 - pos1; glm::vec3 edge2 = pos3 - pos1; glm::vec2 deltaUV1 = uv2 - uv1; glm::vec2 deltaUV2 = uv3 - uv1; - +``` + 有了计算切线和副切线的必备数据,我们就可以开始写出来自于前面部分中的下列等式: diff --git a/docs/05 Advanced Lighting/05 Parallax Mapping.md b/docs/05 Advanced Lighting/05 Parallax Mapping.md index a515005..74c9274 100644 --- a/docs/05 Advanced Lighting/05 Parallax Mapping.md +++ b/docs/05 Advanced Lighting/05 Parallax Mapping.md @@ -18,21 +18,21 @@ [](http://learnopengl.com/img/advanced-lighting/parallax_mapping_plane_height.png) -这里粗糙的红线代表高度贴图中的数值的立体表达,向量V代表观察方向。如果平面进行实际位移,观察者会在点B看到表面。然而我们的平面没有实际上进行位移,观察方向将在点A与平面接触。视差贴图的目的是,在A位置上的fragment不再使用点A的纹理坐标而是使用点B的。随后我们用点B的纹理坐标采样,观察者就像看到了点B一样。 +这里粗糙的红线代表高度贴图中的数值的立体表达,向量\(\color{orange}{\bar{V}}\)代表观察方向。如果平面进行实际位移,观察者会在点\(\color{blue}B\)看到表面。然而我们的平面没有实际上进行位移,观察方向将在点\(\color{green}A\)与平面接触。视差贴图的目的是,在\(\color{green}A\)位置上的fragment不再使用点\(\color{green}A\)的纹理坐标而是使用点\(\color{blue}B\)的。随后我们用点\(\color{blue}B\)的纹理坐标采样,观察者就像看到了点\(\color{blue}B\)一样。 -这个技巧就是描述如何从点A得到点B的纹理坐标。视差贴图尝试通过对从fragment到观察者的方向向量V进行缩放的方式解决这个问题,缩放的大小是A处fragment的高度。所以我们将V的长度缩放为高度贴图在点A处H(A)采样得来的值。下图展示了经缩放得到的向量P: +这个技巧就是描述如何从点\(\color{green}A\)得到点\(\color{blue}B\)的纹理坐标。视差贴图尝试通过对从fragment到观察者的方向向量\(\color{orange}{\bar{V}}\)进行缩放的方式解决这个问题,缩放的大小是\(\color{green}A\)处fragment的高度。所以我们将\(\color{orange}{\bar{V}}\)的长度缩放为高度贴图在点\(\color{green}A\)处\(\color{green}{H(A)}\)采样得来的值。下图展示了经缩放得到的向量\(\color{brown}{\bar{P}}\): ![](http://learnopengl.com/img/advanced-lighting/parallax_mapping_scaled_height.png) -我们随后选出P以及这个向量与平面对齐的坐标作为纹理坐标的偏移量。这能工作是因为向量P是使用从高度贴图得到的高度值计算出来的,所以一个fragment的高度越高位移的量越大。 +我们随后选出\(\color{brown}{\bar{P}}\)以及这个向量与平面对齐的坐标作为纹理坐标的偏移量。这能工作是因为向量\(\color{brown}{\bar{P}}\)是使用从高度贴图得到的高度值计算出来的,所以一个fragment的高度越高位移的量越大。 -这个技巧在大多数时候都没问题,但点B是粗略估算得到的。当表面的高度变化很快的时候,看起来就不会真实,因为向量P最终不会和B接近,就像下图这样: +这个技巧在大多数时候都没问题,但点\(\color{blue}B\)是粗略估算得到的。当表面的高度变化很快的时候,看起来就不会真实,因为向量\(\color{brown}{\bar{P}}\)最终不会和\(\color{blue}B\)接近,就像下图这样: ![](http://learnopengl.com/img/advanced-lighting/parallax_mapping_incorrect_p.png) -视差贴图的另一个问题是,当表面被任意旋转以后很难指出从P获取哪一个坐标。我们在视差贴图中使用了另一个坐标空间,这个空间P向量的x和y元素总是与纹理表面对齐。如果你看了法线贴图教程,你也许猜到了,我们实现它的方法,是的,我们还是在切线空间中实现视差贴图。 +视差贴图的另一个问题是,当表面被任意旋转以后很难指出从\(\color{brown}{\bar{P}}\)获取哪一个坐标。我们在视差贴图中使用了另一个坐标空间,这个空间\(\color{brown}{\bar{P}}\)向量的x和y元素总是与纹理表面对齐。如果你看了法线贴图教程,你也许猜到了,我们实现它的方法,是的,我们还是在切线空间中实现视差贴图。 -将fragment到观察者的向量V转换到切线空间中,经变换的P向量的x和y元素将于表面的切线和副切线向量对齐。由于切线和副切线向量与表面纹理坐标的方向相同,我们可以用P的x和y元素作为纹理坐标的偏移量,这样就不用考虑表面的方向了。 +将fragment到观察者的向量\(\color{orange}{\bar{V}}\)转换到切线空间中,经变换的\(\color{brown}{\bar{P}}\)向量的x和y元素将于表面的切线和副切线向量对齐。由于切线和副切线向量与表面纹理坐标的方向相同,我们可以用\(\color{brown}{\bar{P}}\)的x和y元素作为纹理坐标的偏移量,这样就不用考虑表面的方向了。 理论都有了,下面我们来动手实现视差贴图。 @@ -46,9 +46,9 @@ ![](http://learnopengl.com/img/advanced-lighting/parallax_mapping_depth.png) -我们再次获得A和B,但是这次我们用向量V减去点A的纹理坐标得到P。我们通过在着色器中用1.0减去采样得到的高度贴图中的值来取得深度值,而不再是高度值,或者简单地在图片编辑软件中把这个纹理进行反色操作,就像我们对连接中的那个深度贴图所做的一样。 +我们再次获得\(\color{green}A\)和\(\color{blue}B\),但是这次我们用向量\(\color{orange}{\bar{V}}\)减去点\(\color{green}A\)的纹理坐标得到\(\color{brown}{\bar{P}}\)。我们通过在着色器中用1.0减去采样得到的高度贴图中的值来取得深度值,而不再是高度值,或者简单地在图片编辑软件中把这个纹理进行反色操作,就像我们对连接中的那个深度贴图所做的一样。 -位移贴图是在像素着色器中实现的,因为三角形表面的所有位移效果都不同。在像素着色器中我们将需要计算fragment到观察者到方向向量V所以我们需要观察者位置和在切线空间中的fragment位置。法线贴图教程中我们已经有了一个顶点着色器,它把这些向量发送到切线空间,所以我们可以复制那个顶点着色器: +位移贴图是在像素着色器中实现的,因为三角形表面的所有位移效果都不同。在像素着色器中我们将需要计算fragment到观察者到方向向量\(\color{orange}{\bar{V}}\)所以我们需要观察者位置和在切线空间中的fragment位置。法线贴图教程中我们已经有了一个顶点着色器,它把这些向量发送到切线空间,所以我们可以复制那个顶点着色器: ```c++ #version 330 core @@ -142,9 +142,9 @@ vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir) } ``` -这个相对简单的函数是我们所讨论过的内容的直接表述。我们用本来的纹理坐标texCoords从高度贴图中来采样出当前fragment高度H(A)。然后计算出P,x和y元素在切线空间中,viewDir向量除以它的z元素,用fragment的高度对它进行缩放。我们同时引入额一个height_scale的uniform,来进行一些额外的控制,因为视差效果如果没有一个缩放参数通常会过于强烈。然后我们用P减去纹理坐标来获得最终的经过位移纹理坐标。 +这个相对简单的函数是我们所讨论过的内容的直接表述。我们用本来的纹理坐标texCoords从高度贴图中来采样出当前fragment高度\(\color{green}{H(A)}\)。然后计算出\(\color{brown}{\bar{P}}\),x和y元素在切线空间中,viewDir向量除以它的z元素,用fragment的高度对它进行缩放。我们同时引入额一个height_scale的uniform,来进行一些额外的控制,因为视差效果如果没有一个缩放参数通常会过于强烈。然后我们用\(\color{brown}{\bar{P}}\)减去纹理坐标来获得最终的经过位移纹理坐标。 -有一个地方需要注意,就是viewDir.xy除以viewDir.z那里。因为viewDir向量是经过了标准化的,viewDir.z会在0.0到1.0之间的某处。当viewDir大致平行于表面时,它的z元素接近于0.0,除法会返回比viewDir垂直于表面的时候更大的P向量。所以基本上我们增加了P的大小,当以一个角度朝向一个表面相比朝向顶部时它对纹理坐标会进行更大程度的缩放;这回在角上获得更大的真实度。 +有一个地方需要注意,就是viewDir.xy除以viewDir.z那里。因为viewDir向量是经过了标准化的,viewDir.z会在0.0到1.0之间的某处。当viewDir大致平行于表面时,它的z元素接近于0.0,除法会返回比viewDir垂直于表面的时候更大的\(\color{brown}{\bar{P}}\)向量。所以基本上我们增加了\(\color{brown}{\bar{P}}\)的大小,当以一个角度朝向一个表面相比朝向顶部时它对纹理坐标会进行更大程度的缩放;这回在角上获得更大的真实度。 有些人更喜欢在等式中不使用viewDir.z,因为普通的视差贴图会在角上产生不想要的结果;这个技术叫做有偏移量限制的视差贴图(Parallax Mapping with Offset Limiting)。选择哪一个技术是个人偏好问题,但我倾向于普通的视差贴图。 @@ -172,19 +172,19 @@ if(texCoords.x > 1.0 || texCoords.y > 1.0 || texCoords.x < 0.0 || texCoords.y < ![](http://learnopengl.com/img/advanced-lighting/parallax_mapping_issues.png) -问题的原因是这只是一个大致近似的视差映射。还有一些技巧让我们在陡峭的高度上能够获得几乎完美的结果,即使当以一定角度观看的时候。例如,我们不再使用单一样本,取而代之使用多样本来找到最近点B会得到怎样的结果? +问题的原因是这只是一个大致近似的视差映射。还有一些技巧让我们在陡峭的高度上能够获得几乎完美的结果,即使当以一定角度观看的时候。例如,我们不再使用单一样本,取而代之使用多样本来找到最近点\(\color{blue}B\)会得到怎样的结果? ### 陡峭视差映射(Steep Parallax Mapping) -陡峭视差映射是视差映射的扩展,原则是一样的,但不是使用一个样本而是多个样本来确定向量P到B。它能得到更好的结果,它将总深度范围分布到同一个深度/高度的多个层中。从每个层中我们沿着P方向移动采样纹理坐标,直到我们找到了一个采样得到的低于当前层的深度值的深度值。看看下面的图片: +陡峭视差映射是视差映射的扩展,原则是一样的,但不是使用一个样本而是多个样本来确定向量\(\color{brown}{\bar{P}}\)到\(\color{blue}B\)。它能得到更好的结果,它将总深度范围分布到同一个深度/高度的多个层中。从每个层中我们沿着\(\color{brown}{\bar{P}}\)方向移动采样纹理坐标,直到我们找到了一个采样得到的低于当前层的深度值的深度值。看看下面的图片: ![](http://learnopengl.com/img/advanced-lighting/parallax_mapping_steep_parallax_mapping_diagram.png) -我们从上到下遍历深度层,我们把每个深度层和储存在深度贴图中的它的深度值进行对比。如果这个层的深度值小于深度贴图的值,就意味着这一层的P向量部分在表面之下。我们继续这个处理过程直到有一层的深度高于储存在深度贴图中的值:这个点就在(经过位移的)表面下方。 +我们从上到下遍历深度层,我们把每个深度层和储存在深度贴图中的它的深度值进行对比。如果这个层的深度值小于深度贴图的值,就意味着这一层的\(\color{brown}{\bar{P}}\)向量部分在表面之下。我们继续这个处理过程直到有一层的深度高于储存在深度贴图中的值:这个点就在(经过位移的)表面下方。 -这个例子中我们可以看到第二层(D(2) = 0.73)的深度贴图的值仍低于第二层的深度值0.4,所以我们继续。下一次迭代,这一层的深度值0.6大于深度贴图中采样的深度值(D(3) = 0.37)。我们便可以假设第三层向量P是可用的位移几何位置。我们可以用从向量P3的纹理坐标偏移T3来对fragment的纹理坐标进行位移。你可以看到随着深度曾的增加精确度也在提高。 +这个例子中我们可以看到第二层(D(2) = 0.73)的深度贴图的值仍低于第二层的深度值0.4,所以我们继续。下一次迭代,这一层的深度值0.6大于深度贴图中采样的深度值(D(3) = 0.37)。我们便可以假设第三层向量\(\color{brown}{\bar{P}}\)是可用的位移几何位置。我们可以用从向量\(\color{brown}{\bar{P_3}}\)的纹理坐标偏移\(T_3\)来对fragment的纹理坐标进行位移。你可以看到随着深度曾的增加精确度也在提高。 为实现这个技术,我们只需要改变ParallaxMapping函数,因为所有需要的变量都有了: @@ -205,7 +205,7 @@ vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir) } ``` -我们先定义层的数量,计算每一层的深度,最后计算纹理坐标偏移,每一层我们必须沿着P的方向进行移动。 +我们先定义层的数量,计算每一层的深度,最后计算纹理坐标偏移,每一层我们必须沿着\(\color{brown}{\bar{P}}\)的方向进行移动。 然后我们遍历所有层,从上开始,知道找到小于这一层的深度值的深度贴图值: @@ -228,7 +228,7 @@ return texCoords - currentTexCoords; ``` -这里我们循环每一层深度,直到沿着P向量找到第一个返回低于(位移)表面的深度的纹理坐标偏移量。从fragment的纹理坐标减去最后的偏移量,来得到最终的经过位移的纹理坐标向量,这次就比传统的视差映射更精确了。 +这里我们循环每一层深度,直到沿着\(\color{brown}{\bar{P}}\)向量找到第一个返回低于(位移)表面的深度的纹理坐标偏移量。从fragment的纹理坐标减去最后的偏移量,来得到最终的经过位移的纹理坐标向量,这次就比传统的视差映射更精确了。 有10个样本砖墙从一个角度看上去就已经很好了,但是当有一个强前面展示的木制表面一样陡峭的表面时,陡峭的视差映射的威力就显示出来了: @@ -250,7 +250,7 @@ float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0.0, 0.0, 1.0), viewDir ![](http://learnopengl.com/img/advanced-lighting/parallax_mapping_steep_artifact.png) -我们可以通过增加样本的方式减少这个问题,但是很快就会花费很多性能。有些旨在修复这个问题的方法:不适用低于表面的第一个位置,而是在两个接近的深度层进行插值找出更匹配B的。 +我们可以通过增加样本的方式减少这个问题,但是很快就会花费很多性能。有些旨在修复这个问题的方法:不适用低于表面的第一个位置,而是在两个接近的深度层进行插值找出更匹配\(\color{blue}B\)的。 两种最流行的解决方法叫做Relief Parallax Mapping和Parallax Occlusion Mapping,Relief Parallax Mapping更精确一些,但是比Parallax Occlusion Mapping性能开销更多。因为Parallax Occlusion Mapping的效果和前者差不多但是效率更高,因此这种方式更经常使用,所以我们将在下面讨论一下。 diff --git a/docs/05 Advanced Lighting/08 Deferred Shading.md b/docs/05 Advanced Lighting/08 Deferred Shading.md index 11b41c1..236b978 100644 --- a/docs/05 Advanced Lighting/08 Deferred Shading.md +++ b/docs/05 Advanced Lighting/08 Deferred Shading.md @@ -293,37 +293,53 @@ glBindFramebuffer(GL_FRAMEBUFFER, 0); 为了获取一个光源的体积半径,我们需要解一个对于一个我们认为是**黑暗(Dark)**的亮度(Brightness)的衰减方程,它可以是0.0,或者是更亮一点的但仍被认为黑暗的值,像是0.03。为了展示我们如何计算光源的体积半径,我们将会使用一个在[投光物](http://learnopengl-cn.readthedocs.org/zh/latest/02%20Lighting/05%20Light%20casters/)这节中引入的一个更加复杂,但非常灵活的衰减方程: -![](../img/deferred_shading_1.png) +$$ +F_{light} = \frac{I}{K_c + K_l * d + K_q * d^2} +$$ -我们现在想要在![](../img/F_light.png)等于0的前提下解这个方程,也就是说光在该距离完全是黑暗的。然而这个方程永远不会真正等于0.0,所以它没有解。所以,我们不会求表达式等于0.0时候的解,相反我们会求当亮度值靠近于0.0的解,这时候它还是能被看做是黑暗的。在这个教程的演示场景中,我们选择![](../img/5256.png)作为一个合适的光照值;除以256是因为默认的8-bit帧缓冲可以每个分量显示这么多强度值(Intensity)。 +我们现在想要在\(F_{light}\)等于0的前提下解这个方程,也就是说光在该距离完全是黑暗的。然而这个方程永远不会真正等于0.0,所以它没有解。所以,我们不会求表达式等于0.0时候的解,相反我们会求当亮度值靠近于0.0的解,这时候它还是能被看做是黑暗的。在这个教程的演示场景中,我们选择\(5/256\)作为一个合适的光照值;除以256是因为默认的8-bit帧缓冲可以每个分量显示这么多强度值(Intensity)。 !!! Important - 我们使用的衰减方程在它的可视范围内基本都是黑暗的,所以如果我们想要限制它为一个比![](../img/5256.png)更加黑暗的亮度,光体积就会变得太大从而变得低效。只要是用户不能在光体积边缘看到一个突兀的截断,这个参数就没事了。当然它还是依赖于场景的类型,一个高的亮度阀值会产生更小的光体积,从而获得更高的效率,然而它同样会产生一个很容易发现的副作用,那就是光会在光体积边界看起来突然断掉。 + 我们使用的衰减方程在它的可视范围内基本都是黑暗的,所以如果我们想要限制它为一个比\(5/256\)更加黑暗的亮度,光体积就会变得太大从而变得低效。只要是用户不能在光体积边缘看到一个突兀的截断,这个参数就没事了。当然它还是依赖于场景的类型,一个高的亮度阀值会产生更小的光体积,从而获得更高的效率,然而它同样会产生一个很容易发现的副作用,那就是光会在光体积边界看起来突然断掉。 我们要求的衰减方程会是这样: -![](../img/deferred_shading_2.png) +$$ +\frac{5}{256} = \frac{I_{max}}{Attenuation} +$$ -在这里,![](../img/I_max.png)是光源最亮的颜色分量。我们之所以使用光源最亮的颜色分量是因为解光源最亮的强度值方程最好地反映了理想光体积半径。 +在这里,\(I_{max}\)是光源最亮的颜色分量。我们之所以使用光源最亮的颜色分量是因为解光源最亮的强度值方程最好地反映了理想光体积半径。 从这里我们继续解方程: -![](../img/deferred_shading_3.png) +$$ +\frac{5}{256} * Attenuation = I_{max} +$$ -![](../img/deferred_shading_4.png) +$$ +5 * Attenuation = I_{max} * 256 +$$ -![](../img/deferred_shading_5.png) +$$ +Attenuation = I_{max} * \frac{256}{5} +$$ -![](../img/deferred_shading_6.png) +$$ +K_c + K_l * d + K_q * d^2 = I_{max} * \frac{256}{5} +$$ -![](../img/deferred_shading_7.png) +$$ +K_q * d^2 + K_l * d + K_c - I_{max} * \frac{256}{5} = 0 +$$ -最后的方程形成了![](../img/quad_formula.png)的形式,我们可以用求根公式来解这个二次方程: +最后的方程形成了\(ax^2 + bx + c = 0\)的形式,我们可以用求根公式来解这个二次方程: -![](../img/deferred_shading_8.png) +$$ +x = \frac{-K_l + \sqrt{K_l^2 - 4 * K_q * (K_c - I_{max} * \frac{256}{5})}}{2 * K_q} +$$ -它给我们了一个通用公式从而允许我们计算x的值,即光源的光体积半径,只要我们提供了一个常量,线性和二次项参数: +它给我们了一个通用公式从而允许我们计算\(x\)的值,即光源的光体积半径,只要我们提供了一个常量,线性和二次项参数: ```c++ GLfloat constant = 1.0; diff --git a/docs/img/development.png b/docs/img/development.png new file mode 100644 index 0000000..9376b5f Binary files /dev/null and b/docs/img/development.png differ diff --git a/docs/index.md b/docs/index.md index 8956584..7ff01a0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -16,7 +16,7 @@ 除了核心概念之外,我们将会讨论许多有用的技巧,它们都可以用在你的程序中,比如说在你的场景中移动,做出漂亮的光效,加载一些建模软件导出的一些自定义的模型,做一些很酷的后期处理技巧等。最后,我们也将会使用我们已学的知识从头开始做一个小游戏,让你真正体验一把图形编程的魅力。 -## 中文翻译 +## 关于中文翻译 这里是LearnOpenGL教程的中文翻译,英文版的地址为:[http://learnopengl.com/](http://learnopengl.com/)