1
0
mirror of https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git synced 2025-08-23 04:35:28 +08:00

Replace all the math equations with latex

This commit is contained in:
Meow J
2016-07-05 18:47:06 +08:00
parent ca728d41d6
commit e6c891f7cc
14 changed files with 171 additions and 657 deletions

View File

@@ -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以内的范围这样的衰减效果。 由于二次项的光会以线性方式减少指导距离足够大的时候就会超过一次项之后光的亮度会减少的更快。最后的效果就是光在近距离时非常量但是距离变远亮度迅速降低最后亮度降低速度再次变慢。下面的图展示了在100以内的范围这样的衰减效果。
![](http://www.learnopengl.com/img/lighting/attenuation.png) ![](http://www.learnopengl.com/img/lighting/attenuation.png)
@@ -123,7 +126,7 @@ glUniform3f(lightDirPos, -0.2f, -1.0f, -0.3f);
但是我们把这三个项设置为什么值呢正确的值的设置由很多因素决定环境、你希望光所覆盖的距离范围、光的类型等。大多数场合这是经验的问题也要适度调整。下面的表格展示一些各项的值它们模拟现实某种类型的光源覆盖特定的半径距离。第一栏定义一个光的距离它覆盖所给定的项。这些值是大多数光的良好开始它是来自Ogre3D的维基的礼物 但是我们把这三个项设置为什么值呢正确的值的设置由很多因素决定环境、你希望光所覆盖的距离范围、光的类型等。大多数场合这是经验的问题也要适度调整。下面的表格展示一些各项的值它们模拟现实某种类型的光源覆盖特定的半径距离。第一栏定义一个光的距离它覆盖所给定的项。这些值是大多数光的良好开始它是来自Ogre3D的维基的礼物
Distance|Constant|Linear|Quadratic 距离|常数项|一次项|二次项
-------|------|-----|------ -------|------|-----|------
7|1.0|0.7|1.8 7|1.0|0.7|1.8
13|1.0|0.35|0.44 13|1.0|0.35|0.44
@@ -138,7 +141,7 @@ Distance|Constant|Linear|Quadratic
600|1.0|0.007|0.0002 600|1.0|0.007|0.0002
3250|1.0|0.0014|0.000007 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`:从片段指向光源的向量。 * `LightDir`:从片段指向光源的向量。
* `SpotDir`:聚光灯所指向的方向。 * `SpotDir`:聚光灯所指向的方向。
* `Phiφ`:定义聚光灯半径的切光角。每个落在这个角度之外的,聚光灯都不会照亮。 * `Phi\(\phi\)`:定义聚光灯半径的切光角。每个落在这个角度之外的,聚光灯都不会照亮。
* `Thetaθ``LightDir`向量和`SpotDir`量之间的角度。θ值应该比φ值小,这样才会在聚光灯内。 * `Theta\(\theta\)``LightDir`向量和`SpotDir`量之间的角度。\(\theta\)值应该比\(\Phi\)值小,这样才会在聚光灯内。
所以我们大致要做的是,计算`LightDir`向量和`SpotDir`向量的点乘(返回两个单位向量的点乘,还记得吗?),然后在和光角φ对比。现在你应该明白聚光灯是我们下面将创建的手电筒的范例。 所以我们大致要做的是,计算`LightDir`向量和`SpotDir`向量的点乘(返回两个单位向量的点乘,还记得吗?),然后在和光角\(\phi\)对比。现在你应该明白聚光灯是我们下面将创建的手电筒的范例。
@@ -216,7 +219,7 @@ OpenGL中的聚光灯用世界空间位置一个方向和一个指定了聚
手电筒是一个坐落在观察者位置的聚光灯,通常瞄准玩家透视图的前面。基本上说,一个手电筒是一个普通的聚光灯,但是根据玩家的位置和方向持续的更新它的位置和方向。 手电筒是一个坐落在观察者位置的聚光灯,通常瞄准玩家透视图的前面。基本上说,一个手电筒是一个普通的聚光灯,但是根据玩家的位置和方向持续的更新它的位置和方向。
所以我们需要为片段着色器提供的值,是聚光灯的位置向量(来计算光的方向坐标),聚光灯的方向向量和光角。我们可以把这些值储存在`Light`结构体中: 所以我们需要为片段着色器提供的值,是聚光灯的位置向量(来计算光的方向坐标),聚光灯的方向向量和光角。我们可以把这些值储存在`Light`结构体中:
```c++ ```c++
struct Light 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))); glUniform1f(lightSpotCutOffLoc, glm::cos(glm::radians(12.5f)));
``` ```
你可以看到,我们为光角设置一个角度,但是我们根据一个角度计算了余弦值,把这个余弦结果传给了片段着色器。这么做的原因是在片段着色器中,我们计算`LightDir`和`SpotDir`向量的点乘,而点乘返回一个余弦值,不是一个角度,所以我们不能直接把一个角度和余弦值对比。为了获得这个角度,我们必须计算点乘结果的反余弦,这个操作开销是很大的。所以为了节约一些性能,我们先计算给定切光角的余弦值,然后把结果传递给片段着色器。由于每个角度都被表示为余弦了,我们可以直接对比它们,而不用进行任何开销高昂的操作。 你可以看到,我们为光角设置一个角度,但是我们根据一个角度计算了余弦值,把这个余弦结果传给了片段着色器。这么做的原因是在片段着色器中,我们计算`LightDir`和`SpotDir`向量的点乘,而点乘返回一个余弦值,不是一个角度,所以我们不能直接把一个角度和余弦值对比。为了获得这个角度,我们必须计算点乘结果的反余弦,这个操作开销是很大的。所以为了节约一些性能,我们先计算给定切光角的余弦值,然后把结果传递给片段着色器。由于每个角度都被表示为余弦了,我们可以直接对比它们,而不用进行任何开销高昂的操作。
现在剩下要做的是计算θ值,用它和φ值对比,以决定我们是否在或不在聚光灯的内部: 现在剩下要做的是计算\(\theta\)值,用它和\(\phi\)值对比,以决定我们是否在或不在聚光灯的内部:
```c++ ```c++
float theta = dot(lightDir, normalize(-light.direction)); float theta = dot(lightDir, normalize(-light.direction));
@@ -254,7 +257,7 @@ color = vec4(light.ambient*vec3(texture(material.diffuse,TexCoords)), 1.0f);
!!! Important !!! 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) ![](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。 为创建外圆锥我们简单定义另一个余弦值它代表聚光灯的方向向量和外圆锥的向量等于它的半径的角度。然后如果片段在内圆锥和外圆锥之间就会给它计算出一个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.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 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]以外。 注意,我们使用了`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://www.learnopengl.com/img/lighting/light_casters_spotlight.png)
看起来好多了。仔细看看内部和外部光角,尝试创建一个符合你求的聚光灯。可以在这里找到应用源码,以及片段的源代码。 看起来好多了。仔细看看内部和外部光角,尝试创建一个符合你求的聚光灯。可以在这里找到应用源码,以及片段的源代码。
这样的一个手电筒/聚光灯类型的灯光非常适合恐怖游戏,结合定向和点光,环境会真的开始被照亮了。[下一个教程](http://learnopengl-cn.readthedocs.org/zh/latest/02%20Lighting/06%20Multiple%20lights/),我们会结合所有我们目前讨论了的光和技巧。 这样的一个手电筒/聚光灯类型的灯光非常适合恐怖游戏,结合定向和点光,环境会真的开始被照亮了。[下一个教程](http://learnopengl-cn.readthedocs.org/zh/latest/02%20Lighting/06%20Multiple%20lights/),我们会结合所有我们目前讨论了的光和技巧。
## 练习 ## 练习
- 试着修改上面的各种不同种类的光源及其片段着色器。试着将部分矢量进行反向并尝试使用 < 来代替 > 。试着解释这些修改导致不同显示效果的原因。 - 试着修改上面的各种不同种类的光源及其片段着色器。试着将部分矢量进行反向并尝试使用 < 来代替 > 。试着解释这些修改导致不同显示效果的原因。

View File

@@ -19,9 +19,9 @@
当导入一个模型文件时即Assimp加载一整个包含所有模型和场景数据的模型文件到一个scene对象时Assimp会为这个模型文件中的所有场景节点、模型节点都生成一个具有对应关系的数据结构且将这些场景中的各种元素与模型数据对应起来。下图展示了一个简化的Assimp生成的模型文件数据结构 当导入一个模型文件时即Assimp加载一整个包含所有模型和场景数据的模型文件到一个scene对象时Assimp会为这个模型文件中的所有场景节点、模型节点都生成一个具有对应关系的数据结构且将这些场景中的各种元素与模型数据对应起来。下图展示了一个简化的Assimp生成的模型文件数据结构
<div class="centerHV">
<img src="http://learnopengl.com/img/model_loading/assimp_structure.png"/> ![](http://learnopengl.com/img/model_loading/assimp_structure.png)
</div>
- 所有的模型、场景数据都包含在scene对象中如所有的材质和Mesh。同样场景的根节点引用也包含在这个scene对象中 - 所有的模型、场景数据都包含在scene对象中如所有的材质和Mesh。同样场景的根节点引用也包含在这个scene对象中
- 场景的根节点可能也会包含很多子节点和一个指向保存模型点云数据mMeshes[]的索引集合。根节点上的mMeshes[]里保存了实际了Mesh对象而每个子节点上的mMesshes[]都只是指向根节点中的mMeshes[]的一个引用(译者注C/C++称为指针Java/C#称为引用) - 场景的根节点可能也会包含很多子节点和一个指向保存模型点云数据mMeshes[]的索引集合。根节点上的mMeshes[]里保存了实际了Mesh对象而每个子节点上的mMesshes[]都只是指向根节点中的mMeshes[]的一个引用(译者注C/C++称为指针Java/C#称为引用)

View File

@@ -83,7 +83,9 @@ glDepthFunc(GL_ALWAYS);
在深度缓冲区中包含深度值介于`0.0`和`1.0`之间,从观察者看到其内容与场景中的所有对象的 z 值进行了比较。这些视图空间中的 z 值可以在投影平头截体的近平面和远平面之间的任何值。我们因此需要一些方法来转换这些视图空间 z 值到 [01] 的范围内,方法之一就是线性将它们转换为 [01] 范围内。下面的 (线性) 方程把 z 值转换为 0.0 和 1.0 之间的值 : 在深度缓冲区中包含深度值介于`0.0`和`1.0`之间,从观察者看到其内容与场景中的所有对象的 z 值进行了比较。这些视图空间中的 z 值可以在投影平头截体的近平面和远平面之间的任何值。我们因此需要一些方法来转换这些视图空间 z 值到 [01] 的范围内,方法之一就是线性将它们转换为 [01] 范围内。下面的 (线性) 方程把 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并将其转换到 [01] 范围。在下面的图给出 z 值和其相应的深度值的关系: 这里far和near是我们用来提供到投影矩阵设置可见视图截锥的远近值 (见[坐标系](http://learnopengl-cn.readthedocs.org/zh/latest/01%20Getting%20started/08%20Coordinate%20Systems/))。方程带内锥截体的深度值 z并将其转换到 [01] 范围。在下面的图给出 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%的浮点数的精度,这正是我们想要的。这类方程,也需要近和远距离考虑,下面给出: 由于非线性函数是和 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 值和产生深度缓冲区的值在下列图中的非线性关系: 如果你不知道这个方程到底怎么回事也不必担心。要记住的重要一点是在深度缓冲区的值不是线性的屏幕空间 (它们在视图空间投影矩阵应用之前是线性)。值为 0.5 在深度缓冲区并不意味着该对象的 z 值是投影平头截体的中间;顶点的 z 值是实际上相当接近近平面!你可以看到 z 值和产生深度缓冲区的值在下列图中的非线性关系:

View File

@@ -130,12 +130,14 @@ glEnable(GL_BLEND);
OpenGL以下面的方程进行混合 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}
$$
* source源颜色向量。这是来自纹理的本来的颜色向量。 * \(\bar{\color{green}C}_{source}\):源颜色向量。这是来自纹理的本来的颜色向量。
* destination目标颜色向量。这是储存在颜色缓冲中当前位置的颜色向量。 * \(\bar{\color{red}C}_{destination}\):目标颜色向量。这是储存在颜色缓冲中当前位置的颜色向量。
* Fsource源因子。设置了对源颜色的alpha值影响。 * \(\color{green}F_{source}\)源因子。设置了对源颜色的alpha值影响。
* Fdestination目标因子。设置了对目标颜色的alpha影响。 * \(\color{red}F_{destination}\)目标因子。设置了对目标颜色的alpha影响。
片段着色器运行完成并且所有的测试都通过以后混合方程才能自由执行片段的颜色输出当前它在颜色缓冲中前面片段的颜色在当前片段之前储存。源和目标颜色会自动被OpenGL设置而源和目标因子可以让我们自由设置。我们来看一个简单的例子 片段着色器运行完成并且所有的测试都通过以后混合方程才能自由执行片段的颜色输出当前它在颜色缓冲中前面片段的颜色在当前片段之前储存。源和目标颜色会自动被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%的红色,得到一种脏兮兮的颜色: 最终方块结合部分包含了60%的绿色和40%的红色,得到一种脏兮兮的颜色:
@@ -155,27 +159,27 @@ C¯result = C¯source Fsource + C¯destination Fdestination
这个方案不错但我们怎样告诉OpenGL来使用这样的因子呢恰好有一个叫做`glBlendFunc`的函数。 这个方案不错但我们怎样告诉OpenGL来使用这样的因子呢恰好有一个叫做`glBlendFunc`的函数。
`void glBlendFunc(GLenum sfactor, GLenum dfactor)`接收两个参数来设置源source和目标destination因子。OpenGL为我们定义了很多选项我们把最常用的列在下面。注意颜色常数向量constant可以用`glBlendColor`函数分开来设置。 `void glBlendFunc(GLenum sfactor, GLenum dfactor)`接收两个参数来设置源source和目标destination因子。OpenGL为我们定义了很多选项我们把最常用的列在下面。注意颜色常数向量\(\bar{\color{blue}C}_{constant}\)可以用`glBlendColor`函数分开来设置。
Option | Value 选项 | 值
---|--- ---|---
GL_ZERO | 0 GL_ZERO | \(0\)
GL_ONE | 1 GL_ONE | \(1\)
GL_SRC_COLOR | 颜色source. GL_SRC_COLOR | 颜色向量\(\bar{\color{green}C}_{source}\)
GL_ONE_MINUS_SRC_COLOR | 1 source. GL_ONE_MINUS_SRC_COLOR | \(1 - \bar{\color{green}C}_{source}\)
GL_DST_COLOR | destination GL_DST_COLOR | 目标颜色向量\(\bar{\color{red}C}_{destination}\)
GL_ONE_MINUS_DST_COLOR | 1 destination. GL_ONE_MINUS_DST_COLOR | \(1 - \bar{\color{red}C}_{destination}\)
GL_SRC_ALPHA | C¯source的alpha值 GL_SRC_ALPHA | \(\bar{\color{green}C}_{source}\)的\(alpha\)
GL_ONE_MINUS_SRC_ALPHA | 1 - C¯source的alpha值 GL_ONE_MINUS_SRC_ALPHA | \(1 -\) \(\bar{\color{green}C}_{source}\)的\(alpha\)
GL_DST_ALPHA | destinationalpha值 GL_DST_ALPHA | \(\bar{\color{red}C}_{destination}\)的\(alpha\)
GL_ONE_MINUS_DST_ALPHA | 1 -destinationalpha值 GL_ONE_MINUS_DST_ALPHA | \(1 -\) \(\bar{\color{red}C}_{destination}\)的\(alpha\)
GL_CONSTANT_COLOR | constant. GL_CONSTANT_COLOR | 常颜色向量\(\bar{\color{blue}C}_{constant}\)
GL_ONE_MINUS_CONSTANT_COLOR | 1 - constant GL_ONE_MINUS_CONSTANT_COLOR | \(1 - \bar{\color{blue}C}_{constant}\)
GL_CONSTANT_ALPHA | constantalpha值 GL_CONSTANT_ALPHA | \(\bar{\color{blue}C}_{constant}\)的\(alpha\)
GL_ONE_MINUS_CONSTANT_ALPHA | 1 constantalpha值 GL_ONE_MINUS_CONSTANT_ALPHA | \(1 -\) \(\bar{\color{blue}C}_{constant}\)的\(alpha\)
为从两个方块获得混合结果我们打算把源颜色的alpha给源因子1-alpha给目标因子。调整到`glBlendFunc`之后就像这样: 为从两个方块获得混合结果,我们打算把源颜色的\(alpha\)给源因子,\(1 - alpha\)给目标因子。调整到`glBlendFunc`之后就像这样:
```c++ ```c++
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
@@ -193,9 +197,9 @@ OpenGL给了我们更多的自由我们可以改变方程源和目标部分
`void glBlendEquation(GLenum mode)`允许我们设置这个操作有3种可行的选项 `void glBlendEquation(GLenum mode)`允许我们设置这个操作有3种可行的选项
* GL_FUNC_ADD默认的彼此元素相加result = Src + Dst. * GL_FUNC_ADD默认的彼此元素相加\(\bar{C}_{result} = \color{green}{Src} + \color{red}{Dst}\)
* GL_FUNC_SUBTRACT彼此元素相减 result = Src Dst. * GL_FUNC_SUBTRACT彼此元素相减 \(\bar{C}_{result} = \color{green}{Src} - \color{red}{Dst}\)
* GL_FUNC_REVERSE_SUBTRACT彼此元素相减但顺序相反result = Dst Src. * GL_FUNC_REVERSE_SUBTRACT彼此元素相减但顺序相反\(\bar{C}_{result} = \color{red}{Dst} - \color{green}{Src}\).
通常我们可以简单地省略`glBlendEquation`因为GL_FUNC_ADD在大多数时候就是我们想要的但是如果你如果你真想尝试努力打破主流常规其他的方程或许符合你的要求。 通常我们可以简单地省略`glBlendEquation`因为GL_FUNC_ADD在大多数时候就是我们想要的但是如果你如果你真想尝试努力打破主流常规其他的方程或许符合你的要求。

View File

@@ -332,7 +332,9 @@ void main()
kernel是一个长得有点像一个小矩阵的数值数组它中间的值中心可以映射到一个像素上这个像素和这个像素周围的值再乘以kernel最后再把结果相加就能得到一个值。所以我们基本上就是给当前纹理坐标加上一个它四周的偏移量然后基于kernel把它们结合起来。下面是一个kernel的例子 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中间像素去乘以一个比较大的负数来进行平衡。 这个kernel表示一个像素周围八个像素乘以2它自己乘以-15。这个例子基本上就是把周围像素乘上2中间像素去乘以一个比较大的负数来进行平衡。
@@ -390,7 +392,7 @@ void main()
创建模糊效果的kernel定义如下 创建模糊效果的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数组会是这样的: 由于所有数值加起来的总和为16,简单返回结合起来的采样颜色是非常亮的,所以我们必须将kernel的每个值除以16.最终的kernel数组会是这样的:
@@ -414,7 +416,9 @@ float kernel[9] = float[](
下面的边检测kernel与锐化kernel类似: 下面的边检测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将所有的边提高亮度,而对其他部分进行暗化处理,当我们值关心一副图像的边缘的时候,它非常有用. 这个kernel将所有的边提高亮度,而对其他部分进行暗化处理,当我们值关心一副图像的边缘的时候,它非常有用.

View File

@@ -34,7 +34,7 @@ Phong光照很棒而且性能较高但是它的镜面反射在某些条件
得到半程向量很容易我们将光的方向向量和视线向量相加然后将结果归一化normalize 得到半程向量很容易我们将光的方向向量和视线向量相加然后将结果归一化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代码如下 翻译成GLSL代码如下

View File

@@ -1,6 +1,6 @@
本文作者JoeyDeVries由Django翻译自[http://learnopengl.com](http://learnopengl.com) 本文作者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的区间那么多少电压就等于多少亮度。对于CRTGamma通常为2.2,因而,输出亮度 = 输入电压的2.2次幂你可以从本节第二张图中看到Gamma2.2实际显示出来的总会比预期暗相反Gamma0.45就会比理想预期亮如果你讲Gamma0.45叠加到Gamma2.2的显示设备上,便会对偏暗的显示效果做到校正,这个简单的思路就是本节的核心)。 当我们计算出场景中所有像素的最终颜色以后我们就必须把它们显示在监视器上。过去大多数监视器是阴极射线管显示器CRT。这些监视器有一个物理特性就是两倍的输入电压产生的不是两倍的亮度。输入电压产生约为输入电压的2.2次幂的亮度这叫做监视器Gamma译注Gamma也叫灰度系数每种显示设备都有自己的Gamma值都不相同有一个公式设备输出亮度 = 电压的Gamma次幂任何设备Gamma基本上都不会等于1等于1是一种理想的线性状态这种理想状态是如果电压和亮度都是在0到1的区间那么多少电压就等于多少亮度。对于CRTGamma通常为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) ![](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但最终还是要把所哟的颜色输出到监视器上所以我们配置的所有颜色和光照变量从物理角度来看都是不正确的在我们的监视器上很少能够正确地显示。出于这个原因我们以及艺术家通常将光照值设置得比本来更亮一些由于监视器会将其亮度显示的更暗一些如果不是这样在线性空间里计算出来的光照就会不正确。同时还要记住监视器所显示出来的图像和线性图像的最小亮度是相同的它们最大的亮度也是相同的只是中间亮度部分会被压暗。 直到现在我们还一直假设我们所有的工作都是在线性空间中进行的译注Gamma为1但最终还是要把所哟的颜色输出到监视器上所以我们配置的所有颜色和光照变量从物理角度来看都是不正确的在我们的监视器上很少能够正确地显示。出于这个原因我们以及艺术家通常将光照值设置得比本来更亮一些由于监视器会将其亮度显示的更暗一些如果不是这样在线性空间里计算出来的光照就会不正确。同时还要记住监视器所显示出来的图像和线性图像的最小亮度是相同的它们最大的亮度也是相同的只是中间亮度部分会被压暗。
@@ -24,22 +24,11 @@
![](http://learnopengl.com/img/advanced-lighting/gamma_correction_example.png) ![](http://learnopengl.com/img/advanced-lighting/gamma_correction_example.png)
Gamma校正 ## Gamma校正
Gamma校正的思路是在最终的颜色输出上应用监视器Gamma的倒数。回头看前面的Gamma曲线图你会有一个短划线它是监视器Gamma曲线的翻转曲线。我们在颜色显示到监视器的时候把每个颜色输出都加上这个翻转的Gamma曲线这样应用了监视器Gamma以后最终的颜色将会变为线性的。我们所得到的中间色调就会更亮所以虽然监视器使它们变暗但是我们又将其平衡回来了。 Gamma校正的思路是在最终的颜色输出上应用监视器Gamma的倒数。回头看前面的Gamma曲线图你会有一个短划线它是监视器Gamma曲线的翻转曲线。我们在颜色显示到监视器的时候把每个颜色输出都加上这个翻转的Gamma曲线这样应用了监视器Gamma以后最终的颜色将会变为线性的。我们所得到的中间色调就会更亮所以虽然监视器使它们变暗但是我们又将其平衡回来了。
我们来看另一个例子。还是那个暗红色0.5, 0.0, 0.0。在将颜色显示到监视器之前我们先对颜色应用Gamma校正曲线。线性的颜色显示在监视器上相当于降低了2.2次幂的亮度所以倒数就是1/2.2次幂。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校正监视器最终会显示出我们在应用中设置的那种线性的颜色。
```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校正监视器最终会显示出我们在应用中设置的那种线性的颜色。
!!! Important !!! Important
@@ -130,17 +119,7 @@ float attenuation = 1.0 / distance;
![](http://learnopengl.com/img/advanced-lighting/gamma_correction_attenuation.png) ![](http://learnopengl.com/img/advanced-lighting/gamma_correction_attenuation.png)
这种差异产生的原因是光的衰减方程改变了亮度值而且屏幕上显示出来的也不是线性空间在监视器上效果最好的衰减方程并不是符合物理的。想想平方衰减方程如果我们使用这个方程而且不进行gamma校正显示在监视器上的衰减方程实际上将变成 这种差异产生的原因是光的衰减方程改变了亮度值而且屏幕上显示出来的也不是线性空间在监视器上效果最好的衰减方程并不是符合物理的。想想平方衰减方程如果我们使用这个方程而且不进行gamma校正显示在监视器上的衰减方程实际上将变成\((1.0 / distance^2)^{2.2}\)。若不进行gamma校正将产生更强烈的衰减。这也解释了为什么双曲线不用gamma校正时看起来更真实因为它实际变成了\((1.0 / distance)^{2.2} = 1.0 / distance^{2.2}\)。这和物理公式是很相似的。
```math
{(1.0 / distance2)}^{2.2}
```
若不进行gamma校正将产生更强烈的衰减。这也解释了为什么双曲线不用gamma校正时看起来更真实因为它实际变成了
```math
{(1.0 / distance)}^{2.2} = 1.0 / distance^{2.2}
```
这和物理公式是很相似的。
!!! Important !!! Important

View File

@@ -34,13 +34,13 @@
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_theory_spaces.png) ![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_theory_spaces.png)
左侧的图片展示了一个定向光源(所有光线都是平行的)在立方体下的表面投射的阴影。通过储存到深度贴图中的深度值,我们就能找到最近点,用以决定片元是否在阴影中。我们使用一个来自光源的视图和投影矩阵来渲染场景就能创建一个深度贴图。这个投影和视图矩阵结合在一起成为一个T变换,它可以将任何三维位置转变到光源的可见坐标空间。 左侧的图片展示了一个定向光源(所有光线都是平行的)在立方体下的表面投射的阴影。通过储存到深度贴图中的深度值,我们就能找到最近点,用以决定片元是否在阴影中。我们使用一个来自光源的视图和投影矩阵来渲染场景就能创建一个深度贴图。这个投影和视图矩阵结合在一起成为一个\(T\)变换,它可以将任何三维位置转变到光源的可见坐标空间。
!!! Important !!! 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; glm::mat4 lightSpaceMatrix = lightProjection * lightView;
``` ```
这个lightSpaceMatrix正是前面我们称为T的那个变换矩阵。有了lightSpaceMatrix只要给shader提供光空间的投影和视图矩阵我们就能像往常那样渲染场景了。然而我们只关心深度值并非所有片元计算都在我们的着色器中进行。为了提升性能我们将使用一个与之不同但更为简单的着色器来渲染出深度贴图。 这个lightSpaceMatrix正是前面我们称为\(T\)的那个变换矩阵。有了lightSpaceMatrix只要给shader提供光空间的投影和视图矩阵我们就能像往常那样渲染场景了。然而我们只关心深度值并非所有片元计算都在我们的着色器中进行。为了提升性能我们将使用一个与之不同但更为简单的着色器来渲染出深度贴图。
### 渲染出深度贴图 ### 渲染出深度贴图

View File

@@ -0,0 +1,7 @@
# CSM
# 未完成
这篇教程暂时还没有完成,您可以经常来刷新看看是否有更新的进展。
<img src="../../../img/development.png" class="clean">

View File

@@ -29,8 +29,10 @@
由于法线向量是个几何工具而纹理通常只用于储存颜色信息用纹理储存法线向量不是非常直接。如果你想一想就会知道纹理中的颜色向量用r、g、b元素代表一个3D向量。类似的我们也可以将法线向量的x、y、z元素储存到纹理中代替颜色的r、g、b元素。法线向量的范围在-1到1之间所以我们先要将其映射到0到1的范围 由于法线向量是个几何工具而纹理通常只用于储存颜色信息用纹理储存法线向量不是非常直接。如果你想一想就会知道纹理中的颜色向量用r、g、b元素代表一个3D向量。类似的我们也可以将法线向量的x、y、z元素储存到纹理中代替颜色的r、g、b元素。法线向量的范围在-1到1之间所以我们先要将其映射到0到1的范围
1 ```c++
vec3 rgb_normal = normal * 0.5 - 0.5; // transforms from [-1,1] to [0,1] vec3 rgb_normal = normal * 0.5 - 0.5; // transforms from [-1,1] to [0,1]
```
将法线向量变换为像这样的RGB颜色元素我们就能把根据表面的形状的fragment的法线保存在2D纹理中。教程开头展示的那个砖块的例子的法线贴图如下所示 将法线向量变换为像这样的RGB颜色元素我们就能把根据表面的形状的fragment的法线保存在2D纹理中。教程开头展示的那个砖块的例子的法线贴图如下所示
![](http://learnopengl.com/img/advanced-lighting/normal_mapping_normal_map.png) ![](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) ![](http://learnopengl.com/img/advanced-lighting/normal_mapping_surface_edges.png)
上图中我们可以看到边<math xmlns="http://www.w3.org/1998/Math/MathML"> 上图中我们可以看到边\(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之间因此可以进行这样的组合
<msub>
<mi>E</mi>
<mn>2</mn>
</msub>
</math>纹理坐标的不同,<math xmlns="http://www.w3.org/1998/Math/MathML">
<msub>
<mi>E</mi>
<mn>2</mn>
</msub>
</math>是一个三角形的边,这个三角形的另外两条边是<math xmlns="http://www.w3.org/1998/Math/MathML">
<mi mathvariant="normal">&#x0394;<!-- Δ --></mi>
<msub>
<mi>U</mi>
<mn>2</mn>
</msub>
</math>和<math xmlns="http://www.w3.org/1998/Math/MathML">
<mi mathvariant="normal">&#x0394;<!-- Δ --></mi>
<msub>
<mi>V</mi>
<mn>2</mn>
</msub>
</math>,它们与切线向量*T*和副切线向量*B*方向相同。这样我们可以把边</math>和<math xmlns="http://www.w3.org/1998/Math/MathML">
<msub>
<mi>E</mi>
<mn>1</mn>
</msub>
</math>和<math xmlns="http://www.w3.org/1998/Math/MathML">
<msub>
<mi>E</mi>
<mn>2</mn>
</msub>
</math>用切线向量 *T* 和副切线向量 *B* 的线性组合表示出来(译注:注意*T*和*B*都是单位长度,在*TB*平面中所有点的*T*、*B*坐标都在0到1之间因此可以进行这样的组合
```math $$
E_1 = \Delta U_1T + \Delta V_1B E_1 = \Delta U_1T + \Delta V_1B
$$
$$
E_2 = \Delta U_2T + \Delta V_2B 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_{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\)。
上面的方程允许我们把它们写成另一种格式:矩阵乘法 上面的方程允许我们把它们写成另一种格式:矩阵乘法
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> $$
<mrow> \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}
<mo>[</mo> $$
<mtable rowspacing="4pt" columnspacing="1em">
<mtr>
<mtd>
<msub>
<mi>E</mi>
<mrow class="MJX-TeXAtom-ORD">
<mn>1</mn>
<mi>x</mi>
</mrow>
</msub>
</mtd>
<mtd>
<msub>
<mi>E</mi>
<mrow class="MJX-TeXAtom-ORD">
<mn>1</mn>
<mi>y</mi>
</mrow>
</msub>
</mtd>
<mtd>
<msub>
<mi>E</mi>
<mrow class="MJX-TeXAtom-ORD">
<mn>1</mn>
<mi>z</mi>
</mrow>
</msub>
</mtd>
</mtr>
<mtr>
<mtd>
<msub>
<mi>E</mi>
<mrow class="MJX-TeXAtom-ORD">
<mn>2</mn>
<mi>x</mi>
</mrow>
</msub>
</mtd>
<mtd>
<msub>
<mi>E</mi>
<mrow class="MJX-TeXAtom-ORD">
<mn>2</mn>
<mi>y</mi>
</mrow>
</msub>
</mtd>
<mtd>
<msub>
<mi>E</mi>
<mrow class="MJX-TeXAtom-ORD">
<mn>2</mn>
<mi>z</mi>
</mrow>
</msub>
</mtd>
</mtr>
</mtable>
<mo>]</mo>
</mrow>
<mo>=</mo>
<mrow>
<mo>[</mo>
<mtable rowspacing="4pt" columnspacing="1em">
<mtr>
<mtd>
<mi mathvariant="normal">&#x0394;<!-- Δ --></mi>
<msub>
<mi>U</mi>
<mn>1</mn>
</msub>
</mtd>
<mtd>
<mi mathvariant="normal">&#x0394;<!-- Δ --></mi>
<msub>
<mi>V</mi>
<mn>1</mn>
</msub>
</mtd>
</mtr>
<mtr>
<mtd>
<mi mathvariant="normal">&#x0394;<!-- Δ --></mi>
<msub>
<mi>U</mi>
<mn>2</mn>
</msub>
</mtd>
<mtd>
<mi mathvariant="normal">&#x0394;<!-- Δ --></mi>
<msub>
<mi>V</mi>
<mn>2</mn>
</msub>
</mtd>
</mtr>
</mtable>
<mo>]</mo>
</mrow>
<mrow>
<mo>[</mo>
<mtable rowspacing="4pt" columnspacing="1em">
<mtr>
<mtd>
<msub>
<mi>T</mi>
<mi>x</mi>
</msub>
</mtd>
<mtd>
<msub>
<mi>T</mi>
<mi>y</mi>
</msub>
</mtd>
<mtd>
<msub>
<mi>T</mi>
<mi>z</mi>
</msub>
</mtd>
</mtr>
<mtr>
<mtd>
<msub>
<mi>B</mi>
<mi>x</mi>
</msub>
</mtd>
<mtd>
<msub>
<mi>B</mi>
<mi>y</mi>
</msub>
</mtd>
<mtd>
<msub>
<mi>B</mi>
<mi>z</mi>
</msub>
</mtd>
</mtr>
</mtable>
<mo>]</mo>
</mrow>
</math>
尝试会一下矩阵乘法,它们确实是同一种等式。把等式写成矩阵形式的好处是,解*T*和*B*会因此变得很容易。两边都乘以<math xmlns="http://www.w3.org/1998/Math/MathML"> 尝试会一下矩阵乘法,它们确实是同一种等式。把等式写成矩阵形式的好处是,解\(T\)和\(B\)会因此变得很容易。两边都乘以\(\Delta U \Delta V\)的逆矩阵等于:
<mi mathvariant="normal">&#x0394;<!-- Δ --></mi>
<mi>U</mi>
<mi mathvariant="normal">&#x0394;<!-- Δ --></mi>
<mi>V</mi>
</math>的反数等于:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> $$
<msup> \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}
<mrow> $$
<mo>[</mo>
<mtable rowspacing="4pt" columnspacing="1em">
<mtr>
<mtd>
<mi mathvariant="normal">&#x0394;<!-- Δ --></mi>
<msub>
<mi>U</mi>
<mn>1</mn>
</msub>
</mtd>
<mtd>
<mi mathvariant="normal">&#x0394;<!-- Δ --></mi>
<msub>
<mi>V</mi>
<mn>1</mn>
</msub>
</mtd>
</mtr>
<mtr>
<mtd>
<mi mathvariant="normal">&#x0394;<!-- Δ --></mi>
<msub>
<mi>U</mi>
<mn>2</mn>
</msub>
</mtd>
<mtd>
<mi mathvariant="normal">&#x0394;<!-- Δ --></mi>
<msub>
<mi>V</mi>
<mn>2</mn>
</msub>
</mtd>
</mtr>
</mtable>
<mo>]</mo>
</mrow>
<mrow class="MJX-TeXAtom-ORD">
<mo>&#x2212;<!-- --></mo>
<mn>1</mn>
</mrow>
</msup>
<mrow>
<mo>[</mo>
<mtable rowspacing="4pt" columnspacing="1em">
<mtr>
<mtd>
<msub>
<mi>E</mi>
<mrow class="MJX-TeXAtom-ORD">
<mn>1</mn>
<mi>x</mi>
</mrow>
</msub>
</mtd>
<mtd>
<msub>
<mi>E</mi>
<mrow class="MJX-TeXAtom-ORD">
<mn>1</mn>
<mi>y</mi>
</mrow>
</msub>
</mtd>
<mtd>
<msub>
<mi>E</mi>
<mrow class="MJX-TeXAtom-ORD">
<mn>1</mn>
<mi>z</mi>
</mrow>
</msub>
</mtd>
</mtr>
<mtr>
<mtd>
<msub>
<mi>E</mi>
<mrow class="MJX-TeXAtom-ORD">
<mn>2</mn>
<mi>x</mi>
</mrow>
</msub>
</mtd>
<mtd>
<msub>
<mi>E</mi>
<mrow class="MJX-TeXAtom-ORD">
<mn>2</mn>
<mi>y</mi>
</mrow>
</msub>
</mtd>
<mtd>
<msub>
<mi>E</mi>
<mrow class="MJX-TeXAtom-ORD">
<mn>2</mn>
<mi>z</mi>
</mrow>
</msub>
</mtd>
</mtr>
</mtable>
<mo>]</mo>
</mrow>
<mo>=</mo>
<mrow>
<mo>[</mo>
<mtable rowspacing="4pt" columnspacing="1em">
<mtr>
<mtd>
<msub>
<mi>T</mi>
<mi>x</mi>
</msub>
</mtd>
<mtd>
<msub>
<mi>T</mi>
<mi>y</mi>
</msub>
</mtd>
<mtd>
<msub>
<mi>T</mi>
<mi>z</mi>
</msub>
</mtd>
</mtr>
<mtr>
<mtd>
<msub>
<mi>B</mi>
<mi>x</mi>
</msub>
</mtd>
<mtd>
<msub>
<mi>B</mi>
<mi>y</mi>
</msub>
</mtd>
<mtd>
<msub>
<mi>B</mi>
<mi>z</mi>
</msub>
</mtd>
</mtr>
</mtable>
<mo>]</mo>
</mrow>
</math>
这样我们就可以解出*T*和*B*了。这需要我们计算出delta纹理坐标矩阵的拟阵。我不打算讲解计算逆矩阵的细节但大致是把它变化为1除以矩阵的行列式再乘以它的共轭矩阵。 这样我们就可以解出\(T\)和\(B\)了。这需要我们计算出delta纹理坐标矩阵的拟阵。我不打算讲解计算逆矩阵的细节但大致是把它变化为1除以矩阵的行列式再乘以它的共轭矩阵。
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> $$
<mrow> \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}
<mo>[</mo> $$
<mtable rowspacing="4pt" columnspacing="1em">
<mtr>
<mtd>
<msub>
<mi>T</mi>
<mi>x</mi>
</msub>
</mtd>
<mtd>
<msub>
<mi>T</mi>
<mi>y</mi>
</msub>
</mtd>
<mtd>
<msub>
<mi>T</mi>
<mi>z</mi>
</msub>
</mtd>
</mtr>
<mtr>
<mtd>
<msub>
<mi>B</mi>
<mi>x</mi>
</msub>
</mtd>
<mtd>
<msub>
<mi>B</mi>
<mi>y</mi>
</msub>
</mtd>
<mtd>
<msub>
<mi>B</mi>
<mi>z</mi>
</msub>
</mtd>
</mtr>
</mtable>
<mo>]</mo>
</mrow>
<mo>=</mo>
<mfrac>
<mn>1</mn>
<mrow>
<mi mathvariant="normal">&#x0394;<!-- Δ --></mi>
<msub>
<mi>U</mi>
<mn>1</mn>
</msub>
<mi mathvariant="normal">&#x0394;<!-- Δ --></mi>
<msub>
<mi>V</mi>
<mn>2</mn>
</msub>
<mo>&#x2013;</mo>
<mi mathvariant="normal">&#x0394;<!-- Δ --></mi>
<msub>
<mi>U</mi>
<mn>2</mn>
</msub>
<mi mathvariant="normal">&#x0394;<!-- Δ --></mi>
<msub>
<mi>V</mi>
<mn>1</mn>
</msub>
</mrow>
</mfrac>
<mrow>
<mo>[</mo>
<mtable rowspacing="4pt" columnspacing="1em">
<mtr>
<mtd>
<mi mathvariant="normal">&#x0394;<!-- Δ --></mi>
<msub>
<mi>V</mi>
<mn>2</mn>
</msub>
</mtd>
<mtd>
<mo>&#x2212;<!-- --></mo>
<mi mathvariant="normal">&#x0394;<!-- Δ --></mi>
<msub>
<mi>V</mi>
<mn>1</mn>
</msub>
</mtd>
</mtr>
<mtr>
<mtd>
<mo>&#x2212;<!-- --></mo>
<mi mathvariant="normal">&#x0394;<!-- Δ --></mi>
<msub>
<mi>U</mi>
<mn>2</mn>
</msub>
</mtd>
<mtd>
<mi mathvariant="normal">&#x0394;<!-- Δ --></mi>
<msub>
<mi>U</mi>
<mn>1</mn>
</msub>
</mtd>
</mtr>
</mtable>
<mo>]</mo>
</mrow>
<mrow>
<mo>[</mo>
<mtable rowspacing="4pt" columnspacing="1em">
<mtr>
<mtd>
<msub>
<mi>E</mi>
<mrow class="MJX-TeXAtom-ORD">
<mn>1</mn>
<mi>x</mi>
</mrow>
</msub>
</mtd>
<mtd>
<msub>
<mi>E</mi>
<mrow class="MJX-TeXAtom-ORD">
<mn>1</mn>
<mi>y</mi>
</mrow>
</msub>
</mtd>
<mtd>
<msub>
<mi>E</mi>
<mrow class="MJX-TeXAtom-ORD">
<mn>1</mn>
<mi>z</mi>
</mrow>
</msub>
</mtd>
</mtr>
<mtr>
<mtd>
<msub>
<mi>E</mi>
<mrow class="MJX-TeXAtom-ORD">
<mn>2</mn>
<mi>x</mi>
</mrow>
</msub>
</mtd>
<mtd>
<msub>
<mi>E</mi>
<mrow class="MJX-TeXAtom-ORD">
<mn>2</mn>
<mi>y</mi>
</mrow>
</msub>
</mtd>
<mtd>
<msub>
<mi>E</mi>
<mrow class="MJX-TeXAtom-ORD">
<mn>2</mn>
<mi>z</mi>
</mrow>
</msub>
</mtd>
</mtr>
</mtable>
<mo>]</mo>
</mrow>
</math>
有了最后这个等式,我们就可以用公式、三角形的两条边以及纹理坐标计算出切线向量*T*和副切线*B* 有了最后这个等式,我们就可以用公式、三角形的两条边以及纹理坐标计算出切线向量\(T\)和副切线\(B\)
如果你对这些数学内容不理解也不用担心。当你知道我们可以用一个三角形的顶点和纹理坐标(因为纹理坐标和切线向量在同一空间中)计算出切线和副切线你就已经部分地达到目的了(译注:上面的推导已经很清楚了,如果你不明白可以参考任意线性代数教材,就像作者所说的记住求得切线空间的公式也行,不过不管怎样都得理解切线空间的含义)。 如果你对这些数学内容不理解也不用担心。当你知道我们可以用一个三角形的顶点和纹理坐标(因为纹理坐标和切线向量在同一空间中)计算出切线和副切线你就已经部分地达到目的了(译注:上面的推导已经很清楚了,如果你不明白可以参考任意线性代数教材,就像作者所说的记住求得切线空间的公式也行,不过不管怎样都得理解切线空间的含义)。
@@ -649,6 +142,8 @@ E_2 = \Delta U_2T + \Delta V_2B
这个教程的demo场景中有一个简单的2D平面它朝向正z方向。这次我们会使用切线空间来实现法线贴图所以我们可以使平面朝向任意方向法线贴图仍然能够工作。使用前面讨论的数学方法我们来手工计算出表面的切线和副切线向量。 这个教程的demo场景中有一个简单的2D平面它朝向正z方向。这次我们会使用切线空间来实现法线贴图所以我们可以使平面朝向任意方向法线贴图仍然能够工作。使用前面讨论的数学方法我们来手工计算出表面的切线和副切线向量。
假设平面使用下面的向量建立起来1、2、3和1、3、4它们是两个三角形 假设平面使用下面的向量建立起来1、2、3和1、3、4它们是两个三角形
```c++
// positions // positions
glm::vec3 pos1(-1.0, 1.0, 0.0); glm::vec3 pos1(-1.0, 1.0, 0.0);
glm::vec3 pos2(-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); glm::vec2 uv4(1.0, 1.0);
// normal vector // normal vector
glm::vec3 nm(0.0, 0.0, 1.0); glm::vec3 nm(0.0, 0.0, 1.0);
```
我们先计算第一个三角形的边和deltaUV坐标 我们先计算第一个三角形的边和deltaUV坐标
1 ```c++
2
3
4
glm::vec3 edge1 = pos2 - pos1; glm::vec3 edge1 = pos2 - pos1;
glm::vec3 edge2 = pos3 - pos1; glm::vec3 edge2 = pos3 - pos1;
glm::vec2 deltaUV1 = uv2 - uv1; glm::vec2 deltaUV1 = uv2 - uv1;
glm::vec2 deltaUV2 = uv3 - uv1; glm::vec2 deltaUV2 = uv3 - uv1;
```
有了计算切线和副切线的必备数据,我们就可以开始写出来自于前面部分中的下列等式: 有了计算切线和副切线的必备数据,我们就可以开始写出来自于前面部分中的下列等式:

View File

@@ -18,21 +18,21 @@
[](http://learnopengl.com/img/advanced-lighting/parallax_mapping_plane_height.png) [](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处HA采样得来的值。下图展示了经缩放得到的向量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) ![](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) ![](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) ![](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++ ```c++
#version 330 core #version 330 core
@@ -142,9 +142,9 @@ vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir)
} }
``` ```
这个相对简单的函数是我们所讨论过的内容的直接表述。我们用本来的纹理坐标texCoords从高度贴图中来采样出当前fragment高度H(A)。然后计算出Px和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。选择哪一个技术是个人偏好问题但我倾向于普通的视差贴图。 有些人更喜欢在等式中不使用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) ![](http://learnopengl.com/img/advanced-lighting/parallax_mapping_issues.png)
问题的原因是这只是一个大致近似的视差映射。还有一些技巧让我们在陡峭的高度上能够获得几乎完美的结果,即使当以一定角度观看的时候。例如,我们不再使用单一样本,取而代之使用多样本来找到最近点B会得到怎样的结果? 问题的原因是这只是一个大致近似的视差映射。还有一些技巧让我们在陡峭的高度上能够获得几乎完美的结果,即使当以一定角度观看的时候。例如,我们不再使用单一样本,取而代之使用多样本来找到最近点\(\color{blue}B\)会得到怎样的结果?
### 陡峭视差映射Steep Parallax Mapping ### 陡峭视差映射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) ![](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函数因为所有需要的变量都有了 为实现这个技术我们只需要改变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个样本砖墙从一个角度看上去就已经很好了但是当有一个强前面展示的木制表面一样陡峭的表面时陡峭的视差映射的威力就显示出来了 有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) ![](http://learnopengl.com/img/advanced-lighting/parallax_mapping_steep_artifact.png)
我们可以通过增加样本的方式减少这个问题,但是很快就会花费很多性能。有些旨在修复这个问题的方法:不适用低于表面的第一个位置,而是在两个接近的深度层进行插值找出更匹配B的。 我们可以通过增加样本的方式减少这个问题,但是很快就会花费很多性能。有些旨在修复这个问题的方法:不适用低于表面的第一个位置,而是在两个接近的深度层进行插值找出更匹配\(\color{blue}B\)的。
两种最流行的解决方法叫做Relief Parallax Mapping和Parallax Occlusion MappingRelief Parallax Mapping更精确一些但是比Parallax Occlusion Mapping性能开销更多。因为Parallax Occlusion Mapping的效果和前者差不多但是效率更高因此这种方式更经常使用所以我们将在下面讨论一下。 两种最流行的解决方法叫做Relief Parallax Mapping和Parallax Occlusion MappingRelief Parallax Mapping更精确一些但是比Parallax Occlusion Mapping性能开销更多。因为Parallax Occlusion Mapping的效果和前者差不多但是效率更高因此这种方式更经常使用所以我们将在下面讨论一下。

View File

@@ -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/)这节中引入的一个更加复杂,但非常灵活的衰减方程: 为了获取一个光源的体积半径,我们需要解一个对于一个我们认为是**黑暗(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 !!! 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++ ```c++
GLfloat constant = 1.0; GLfloat constant = 1.0;

BIN
docs/img/development.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

View File

@@ -16,7 +16,7 @@
除了核心概念之外,我们将会讨论许多有用的技巧,它们都可以用在你的程序中,比如说在你的场景中移动,做出漂亮的光效,加载一些建模软件导出的一些自定义的模型,做一些很酷的后期处理技巧等。最后,我们也将会使用我们已学的知识从头开始做一个小游戏,让你真正体验一把图形编程的魅力。 除了核心概念之外,我们将会讨论许多有用的技巧,它们都可以用在你的程序中,比如说在你的场景中移动,做出漂亮的光效,加载一些建模软件导出的一些自定义的模型,做一些很酷的后期处理技巧等。最后,我们也将会使用我们已学的知识从头开始做一个小游戏,让你真正体验一把图形编程的魅力。
## 中文翻译 ## 关于中文翻译
这里是LearnOpenGL教程的中文翻译英文版的地址为[http://learnopengl.com/](http://learnopengl.com/) 这里是LearnOpenGL教程的中文翻译英文版的地址为[http://learnopengl.com/](http://learnopengl.com/)