mirror of
https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git
synced 2025-08-23 04:35:28 +08:00
Merge pull request #1 from LearnOpenGL-CN/new-theme
keep update with origin branch
This commit is contained in:
@@ -137,7 +137,7 @@ return 0;
|
||||
|
||||
## 输入
|
||||
|
||||
我们同样也希望能够在GLFW中实现一些键盘控制,这可以通过使用GLFW的回调函数(Callback Function)来完成。<def>回调函数</def>事实上是一个函数指针,当我们设置好后,GLWF会在合适的时候调用它。**按键回调**(KeyCallback)是众多回调函数中的一种。当我们设置了按键回调之后,GLFW会在用户有键盘交互时调用它。该回调函数的原型如下所示:
|
||||
我们同样也希望能够在GLFW中实现一些键盘控制,这可以通过使用GLFW的回调函数(Callback Function)来完成。<def>回调函数</def>事实上是一个函数指针,当我们设置好后,GLFW会在合适的时候调用它。**按键回调**(KeyCallback)是众多回调函数中的一种。当我们设置了按键回调之后,GLFW会在用户有键盘交互时调用它。该回调函数的原型如下所示:
|
||||
|
||||
```c++
|
||||
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);
|
||||
|
@@ -45,7 +45,7 @@ glm::vec3 toyColor(1.0f, 0.5f, 0.31f);
|
||||
glm::vec3 result = lightColor * toyColor; // = (0.33f, 0.21f, 0.06f);
|
||||
```
|
||||
|
||||
如你所见,我们可以通过物体对不同颜色光的反射来的得到意想不到的不到的颜色,从此创作颜色已经变得非常简单。
|
||||
如你所见,我们可以通过物体对不同颜色光的反射来得到意想不到的颜色,从此创作颜色已经变得非常简单。
|
||||
|
||||
目前有了这些颜色相关的理论已经足够了,接下来我们将创建一个场景用来做更多的实验。
|
||||
|
||||
|
@@ -6,30 +6,30 @@
|
||||
翻译 | [Django](http://bullteacher.com/)
|
||||
校对 | [Geequlim](http://geequlim.com)
|
||||
|
||||
现在是时候着手启用Assimp,并开始创建实际的加载和转换代码了。本教程的目标是创建另一个类,这个类可以表达模型(Model)的全部。更确切的说,一个模型包含多个网格(Mesh),一个网格可能带有多个对象。一个别墅,包含一个木制阳台,一个尖顶或许也有一个游泳池,它仍然被加载为一个单一模型。我们通过Assimp加载模型,把它们转变为多个`Mesh`对象,这些对象是是上一节教程里创建的。
|
||||
现在是时候着手启用Assimp,并开始创建实际的加载和转换代码了。本教程的目标是创建另一个能够完整表示模型(Model)的类,更确切的说,是一个包含多个网格(Mesh),并且可能带有多个对象的模型。也就是说,一个包含木制阳台、塔楼、甚至游泳池的房子,仍然可以被加载为单独的一个模型。我们将通过Assimp加载模型,把它们转变为多个在[上一节](02 Mesh.md)中创建的<fun>Mesh</fun>对象。
|
||||
|
||||
闲话少说,我把Model类的结构呈现给你:
|
||||
闲话少说,我会先把<fun>Model</fun>类的结构给你:
|
||||
|
||||
```c++
|
||||
class Model
|
||||
{
|
||||
public:
|
||||
/* 成员函数 */
|
||||
/* 函数 */
|
||||
Model(GLchar* path)
|
||||
{
|
||||
this->loadModel(path);
|
||||
}
|
||||
void Draw(Shader shader);
|
||||
void Draw(Shader shader);
|
||||
private:
|
||||
/* 模型数据 */
|
||||
vector<Mesh> meshes;
|
||||
string directory;
|
||||
|
||||
/* 私有成员函数 */
|
||||
/* 函数 */
|
||||
void loadModel(string path);
|
||||
void processNode(aiNode* node, const aiScene* scene);
|
||||
Mesh processMesh(aiMesh* mesh, const aiScene* scene);
|
||||
vector<Texture> loadMaterialTextures(aiMaterial* mat, aiTextureType type, string typeName);
|
||||
vector<Texture> loadMaterialTextures(aiMaterial* mat, aiTextureType type,
|
||||
string typeName);
|
||||
};
|
||||
```
|
||||
|
||||
|
@@ -165,7 +165,7 @@ float LinearizeDepth(float depth)
|
||||
|
||||
void main()
|
||||
{
|
||||
float depth = LinearizeDepth(gl_FragCoord.z);
|
||||
float depth = LinearizeDepth(gl_FragCoord.z) / far; // 为了演示除以far
|
||||
color = vec4(vec3(depth), 1.0f);
|
||||
}
|
||||
```
|
||||
|
@@ -40,7 +40,7 @@ glEnable(GL_STENCIL_TEST);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
```
|
||||
|
||||
同时,和深度测试的`glDepthMask`函数一样,模板缓冲也有一个相似函数。`glStencilMask`允许我们给模板值设置一个**位遮罩(Bitmask)**,它与模板值进行按位与(AND)运算决定缓冲是否可写。默认设置的位遮罩都是1,这样就不会影响输出,但是如果我们设置为0x00,所有写入深度缓冲最后都是0。这和深度缓冲的`glDepthMask(GL_FALSE)`很类似:
|
||||
同时,和深度测试的`glDepthMask`函数一样,模板缓冲也有一个相似函数。`glStencilMask`允许我们给模板值设置一个**位掩码(Bitmask)**,它与模板值进行按位与(AND)运算决定缓冲是否可写。默认设置的位掩码都是1,这样就不会影响输出,但是如果我们设置为0x00,所有写入深度缓冲最后都是0。这和深度缓冲的`glDepthMask(GL_FALSE)`很类似:
|
||||
|
||||
```c++
|
||||
|
||||
@@ -53,7 +53,7 @@ glStencilMask(0xFF);
|
||||
glStencilMask(0x00);
|
||||
```
|
||||
|
||||
大多数情况你的模板遮罩(stencil mask)写为0x00或0xFF就行,但是最好知道有一个选项可以自定义位遮罩。
|
||||
大多数情况你的模板掩码(stencil mask)写为0x00或0xFF就行,但是最好知道有一个选项可以自定义位掩码。
|
||||
|
||||
## 模板函数
|
||||
|
||||
@@ -62,8 +62,8 @@ glStencilMask(0x00);
|
||||
`void glStencilFunc(GLenum func, GLint ref, GLuint mask)`函数有三个参数:
|
||||
|
||||
* **func**:设置模板测试操作。这个测试操作应用到已经储存的模板值和`glStencilFunc`的`ref`值上,可用的选项是:`GL_NEVER`、`GL_LEQUAL`、`GL_GREATER`、`GL_GEQUAL`、`GL_EQUAL`、`GL_NOTEQUAL`、`GL_ALWAYS`。它们的语义和深度缓冲的相似。
|
||||
* **ref**:指定模板测试的引用值。模板缓冲的内容会与这个值对比。
|
||||
* **mask**:指定一个遮罩,在模板测试对比引用值和储存的模板值前,对它们进行按位与(and)操作,初始设置为1。
|
||||
* **ref**:指定模板测试的参考值。模板缓冲的内容会与这个值对比。
|
||||
* **mask**:指定一个掩码值。在模板测试比较参考值和储存的模板值前,会用掩码值对它们分别进行按位与(AND)操作。初始情况下所有位都为1。
|
||||
|
||||
在上面简单模板的例子里,方程应该设置为:
|
||||
|
||||
@@ -71,7 +71,7 @@ glStencilMask(0x00);
|
||||
glStencilFunc(GL_EQUAL, 1, 0xFF)
|
||||
```
|
||||
|
||||
它会告诉OpenGL,无论何时,一个片段模板值等于(`GL_EQUAL`)引用值`1`,片段就能通过测试被绘制了,否则就会被丢弃。
|
||||
它会告诉OpenGL,无论何时,一个片段模板值等于(`GL_EQUAL`)参考值`1`,片段就能通过测试被绘制了,否则就会被丢弃。
|
||||
|
||||
但是`glStencilFunc`只描述了OpenGL对模板缓冲做什么,而不是描述我们如何更新缓冲。这就需要`glStencilOp`登场了。
|
||||
|
||||
@@ -145,7 +145,7 @@ normalShader.Use();
|
||||
DrawTwoContainers();
|
||||
```
|
||||
|
||||
使用`GL_ALWAYS`模板测试函数,我们确保箱子的每个片段用模板值1更新模板缓冲。因为片段总会通过模板测试,在我们绘制它们的地方,模板缓冲用引用值更新。
|
||||
使用`GL_ALWAYS`模板测试函数,我们确保箱子的每个片段用模板值1更新模板缓冲。因为片段总会通过模板测试,在我们绘制它们的地方,模板缓冲用参考值更新。
|
||||
|
||||
现在箱子绘制之处,模板缓冲更新为1了,我们将要绘制放大的箱子,但是这次关闭模板缓冲的写入:
|
||||
|
||||
|
@@ -370,7 +370,7 @@ void main()
|
||||
{
|
||||
sampleTex[i] = vec3(texture(screenTexture, TexCoords.st + offsets[i]));
|
||||
}
|
||||
vec3 col;
|
||||
vec3 col = vec3(0.0);
|
||||
for(int i = 0; i < 9; i++)
|
||||
col += sampleTex[i] * kernel[i];
|
||||
|
||||
|
@@ -4,7 +4,7 @@
|
||||
---|---
|
||||
作者 | JoeyDeVries
|
||||
翻译 | [Django](http://bullteacher.com/)
|
||||
校对 | [Geequlim](http://geequlim.com)
|
||||
校对 | [Geequlim](http://geequlim.com), [KenLee](https://hellokenlee.github.io/)
|
||||
|
||||
|
||||
这章不会向你展示什么新的功能,也不会对你的场景的视觉效果有较大提升。本文多多少少地深入探讨了一些GLSL有趣的知识,它们可能在将来能帮助你。基本来说有些不可不知的内容和功能在你去使用GLSL创建OpenGL应用的时候能让你的生活更轻松。
|
||||
@@ -82,7 +82,7 @@ void main()
|
||||
|
||||
因为窗口的宽是800,当一个像素的x坐标小于400,那么它一定在窗口的左边,这样我们就让物体有个不同的颜色。
|
||||
|
||||

|
||||

|
||||
|
||||
我们现在可以计算出两个完全不同的片段着色器结果,每个显示在窗口的一端。这对于测试不同的光照技术很有好处。
|
||||
|
||||
@@ -306,7 +306,7 @@ layout (std140) uniform ExampleBlock
|
||||
|
||||
尝试自己计算出偏移量,把它们和表格对比,你可以把这件事当作一个练习。使用计算出来的偏移量,根据std140布局规则,我们可以用`glBufferSubData`这样的函数,使用变量数据填充缓冲。虽然不是很高效,但std140布局可以保证在每个程序中声明的这个uniform块的布局保持一致。
|
||||
|
||||
在定义uniform块前面添加layout (std140)声明,我们就能告诉OpenGL这个uniform块使用了std140布局。另外还有两种其他的布局可以选择,它们需要我们在填充缓冲之前查询每个偏移量。我们已经了解了分享布局(shared layout)和其他的布局都将被封装(packed)。当使用封装(packed)布局的时候,不能保证布局在别的程序中能够保持一致,因为它允许编译器从uniform块中优化出去uniform变量,这在每个着色器中都可能不同。
|
||||
通过在uniform块的定义前面添加`layout (std140)`声明,我们告诉OpenGL这个uniform块使用了std140布局。我们还有其它的另外两种布局可以选择,但它们都需要我们在填充缓冲之前查询每个的偏移量。前面我们已经见过的**共享**布局(Shared Layout),还有一种则是**紧凑**布局(Packed Layout)。当我们使用紧凑布局的时候,不能保证这个布局在所有程序间能够保持一致(即并非共享),因为它允许编译器从uniform块中优化出去uniform变量,而具体优化出去的是哪个则根据着色器的不同会有所变化。
|
||||
|
||||
## 使用uniform缓冲
|
||||
|
||||
|
@@ -4,7 +4,7 @@
|
||||
---|---
|
||||
作者 | JoeyDeVries
|
||||
翻译 | [Django](http://bullteacher.com/)
|
||||
校对 | [Geequlim](http://geequlim.com)
|
||||
校对 | [Geequlim](http://geequlim.com), [KenLee](https://hellokenlee.github.io/)
|
||||
|
||||
在顶点和片段着色器之间有一个可选的着色器,叫做几何着色器(Geometry Shader)。几何着色器以一个或多个表示为一个单独基本图形(primitive)的顶点作为输入,比如可以是一个点或者三角形。几何着色器在将这些顶点发送到下一个着色阶段之前,可以将这些顶点转变为它认为合适的内容。几何着色器有意思的地方在于它可以把(一个或多个)顶点转变为完全不同的基本图形(primitive),从而生成比原来多得多的顶点。
|
||||
|
||||
@@ -337,15 +337,15 @@ EndPrimitive();
|
||||
|
||||
# 爆破物体
|
||||
|
||||
绘制房子的确很有趣,但我们不会经常这么用。这就是为什么现在我们将撬起物体缺口,形成爆炸式物体的原因!虽然这个我们也不会经常用到,但是它能向你展示一些几何着色器的强大之处。
|
||||
尽管绘制房子的确很有趣,但我们不会经常这么做。因此我们接下来要更上一层楼,去爆破物体啦!虽然这个我们可能也不会经常用到,但是它能向你展示一些几何着色器的强大之处。
|
||||
|
||||
当我们说对一个物体进行爆破(Explode)的时候并不是说我们将要把之前的那堆顶点炸掉,但是我们打算把每个三角形沿着它们的法线向量移动一小段距离。效果是整个物体上的三角形看起来就像沿着它们的法线向量爆炸了一样。纳米服上的三角形的爆炸式效果看起来是这样的:
|
||||
当我们说对一个物体进行爆破(Explode)的时候并不是说我们将要把之前的那堆顶点炸掉,但是我们打算把每个三角形沿着它们的法线向量移动一小段距离。视觉效果是整个物体上的三角形看起来就像沿着它们的法线向量爆炸了一样。纳米服上的三角形的爆炸式效果看起来是这样的:
|
||||
|
||||

|
||||
|
||||
这样一个几何着色器效果的一大好处是,它可以用到任何物体上,无论它们多复杂。
|
||||
|
||||
因为我们打算沿着三角形的法线向量移动三角形的每个顶点,我们需要先计算它的法线向量。我们要做的是计算出一个向量,它垂直于三角形的表面,使用这三个我们已经的到的顶点就能做到。你可能记得变换教程中,我们可以使用叉乘获取一个垂直于两个其他向量的向量。如果我们有两个向量a和b,它们平行于三角形的表面,我们就可以对这两个向量进行叉乘得到法线向量了。下面的几何着色器函数做的正是这件事,它使用3个输入顶点坐标获取法线向量:
|
||||
因为我们打算沿着三角形的法线向量移动三角形的每个顶点,我们需要先计算它的法线向量。我们要做的是计算出一个向量,它垂直于三角形的表面,使用这三个我们已经的到的顶点就能做到。你可能记得在变换教程中,我们可以使用 **叉乘**获取一个垂直于两个其他向量的向量。如果我们有两个向量a和b,它们平行于三角形的表面,我们就可以对这两个向量进行叉乘得到法线向量了。下面的几何着色器函数做的正是这件事,它使用3个输入顶点坐标获取法线向量:
|
||||
|
||||
```c++
|
||||
vec3 GetNormal()
|
||||
@@ -412,15 +412,15 @@ void main() {
|
||||
|
||||
```c++
|
||||
glUniform1f(glGetUniformLocation(shader.Program, "time"), glfwGetTime());
|
||||
```
|
||||
```
|
||||
|
||||
最后的结果是一个随着时间持续不断地爆炸的3D模型(不断爆炸不断回到正常状态)。尽管没什么大用处,它却向你展示出很多几何着色器的高级用法。你可以用[完整的源码](http://learnopengl.com/code_viewer.php?code=advanced/geometry_shader_explode)和[着色器](http://learnopengl.com/code_viewer.php?code=advanced/geometry_shader_explode_shaders)对比一下你自己的。
|
||||
|
||||
# 显示法向量
|
||||
|
||||
在这部分我们将使用几何着色器写一个例子,非常有用:显示一个法线向量。当编写光照着色器的时候,你最终会遇到奇怪的视频输出问题,你很难决定是什么导致了这个问题。通常导致光照错误的是,不正确的加载顶点数据,以及给它们指定了不合理的顶点属性,又或是在着色器中不合理的管理,导致产生了不正确的法线向量。我们所希望的是有某种方式可以检测出法线向量是否正确。把法线向量显示出来正是这样一种方法,恰好几何着色器能够完美地达成这个目的。
|
||||
在这部分我们将使用几何着色器写一个非常有用的例子:绘制出任意一个物体的全部法向量。当编写光照着色器的时候,你可能会得到一些奇怪的渲染输出,但是你却又很难判断是什么导致了这个问题。通常导致光照错误的是,由不正确地加载顶点数据或者错误地定义定点属性又或者在着色器中错误地处理等原因导致的法向量错误。我们希望有某种方式可以检测出我们使用法向量是否正确。一种比较好的方法去检测我们的法向量是否有错误是直接把物体的法向量全部绘制出来,而恰恰好几何着色器能完美地达成这个目标。
|
||||
|
||||
思路是这样的:我们先不用几何着色器,正常绘制场景,然后我们再次绘制一遍场景,但这次只显示我们通过几何着色器生成的法线向量。几何着色器把一个三角形基本图形作为输入类型,用它们生成3条和法线向量同向的线段,每个顶点一条。伪代码应该是这样的:
|
||||
思路是这样的:我们先不用几何着色器,正常绘制场景。然后我们再次绘制一遍场景,但这次只显示我们通过几何着色器生成的法线向量。几何着色器把一个三角形基本图形作为输入类型,用它们生成3条沿法向量方向的法线——每个顶点一条。伪代码应该类似这个样子:
|
||||
|
||||
```c++
|
||||
shader.Use();
|
||||
@@ -429,7 +429,7 @@ normalDisplayShader.Use();
|
||||
DrawScene();
|
||||
```
|
||||
|
||||
这次我们会创建一个使用模型提供的顶点法线,而不是自己去生成。为了适应缩放和旋转我们会在把它变换到裁切空间坐标前,使用法线矩阵来法线(几何着色器用他的位置向量做为裁切空间坐标,所以我们还要把法线向量变换到同一个空间)。这些都能在顶点着色器中完成:
|
||||
这次我们会在几何着色器中使用模型提供的顶点法线,而不用自己去生成。为了适应(由于 view 和 model 矩阵产生的)缩放和旋转,我们会在把法向量变换到裁切空间坐标系(clip-space)前,先把它变成法向量矩阵(几何着色器接受在裁切空间坐标系下的位置向量,因此我们也需要把法向量转换到同一空间坐标系下)。这些步骤都能在顶点着色器中完成:
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
@@ -452,7 +452,7 @@ void main()
|
||||
}
|
||||
```
|
||||
|
||||
经过变换的裁切空间法线向量接着通过一个接口块被传递到下个着色阶段。几何着色器接收每个顶点(带有位置和法线向量),从每个位置向量绘制出一个法线向量:
|
||||
经过变换的裁切坐标系空间法向量通过一个接口块被传递到下个着色阶段。几何着色器接收每个顶点的信息(包括其位置向量和法向量),从每个定点的位置上沿法向量方向画一条法线:
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
@@ -496,10 +496,10 @@ void main()
|
||||
}
|
||||
```
|
||||
|
||||
现在先使用普通着色器来渲染你的模型,然后使用特制的法线可视着色器,你会看到这样的效果:
|
||||
现在先使用普通着色器来渲染你的模型,然后使用特制的法线可视化着色器,你会看到类似这样的效果:
|
||||
|
||||

|
||||
|
||||
除了我们的纳米服现在看起来有点像一个带着隔热手套的全身多毛的家伙外,它给了我们一种非常有效的检查一个模型的法线向量是否有错误的方式。你可以想象下这样的几何着色器也经常能被用在给物体添加毛发上。
|
||||
除了我们的纳米服现在看起来有点像一个带着隔热手套的全身多毛的家伙外,我们拥有了一种非常有效的检查一个模型的法线向量是否有错误的方式。你可以想象到这样的几何着色器也能被用在给物体添加毛发上。
|
||||
|
||||
你可以从这里找到[源码](http://learnopengl.com/code_viewer.php?code=advanced/geometry_shader_normals)和可显示法线的[着色器](http://learnopengl.com/code_viewer.php?code=advanced/geometry_shader_normals_shaders)。
|
||||
|
@@ -4,67 +4,69 @@
|
||||
---|---
|
||||
作者 | JoeyDeVries
|
||||
翻译 | [Django](http://bullteacher.com/)
|
||||
校对 | [Geequlim](http://geequlim.com)
|
||||
校对 | [Geequlim](http://geequlim.com),[KenLee](https://hellokenlee.github.io/)
|
||||
|
||||
在你的渲染大冒险中,你可能会遇到模型边缘有锯齿的问题。**锯齿边(Jagged Edge)**出现的原因是由顶点数据像素化之后成为片段的方式所引起的。下面是一个简单的立方体,它体现了锯齿边的效果:
|
||||
在你的渲染大冒险中,你可能会遇到模型边缘有锯齿的问题。**锯齿边缘(Jagged Edge)**出现的原因是由顶点数据光栅化成为片段(fragment)的方式所引起的。举个例子,我们随手绘制一个简单的正方体就已经能很清楚地看到锯齿边缘的效果:
|
||||
|
||||

|
||||
|
||||
也许不是立即可见的,如果你更近的看看立方体的边,你就会发现锯齿了。如果我们放大就会看到下面的情境:
|
||||
尽管可能不会被立刻察觉到,如果你更近的看看立方体的边,你就会发现锯齿状的图案。如果我们放大就会看到下面的情境:
|
||||
|
||||

|
||||
|
||||
这当然不是我们在最终版本的应用里想要的效果。这个效果,很明显能看到边是由像素所构成的,这种现象叫做**走样(Aliasing)**。有很多技术能够减少走样,产生更平滑的边缘,这些技术叫做**抗锯齿技术**(Anti-aliasing,也被称为反走样技术)。
|
||||
这当然不是我们在实际应用中想要的效果。这个效果,我们很明显能看到边缘像素的形态,这种现象被称之为**走样(Aliasing)**。有很多技术能够减少走样现象,产生更平滑的边缘,这些技术叫做**抗锯齿技术**(Anti-aliasing,也被称为反走样技术)。
|
||||
|
||||
首先,我们有一个叫做**超级采样抗锯齿技术(Super Sample Anti-aliasing, SSAA)**,它暂时使用一个更高的解析度(以超级采样方式)来渲染场景,当视频输出在帧缓冲中被更新时,解析度便降回原来的普通解析度。这个额外的解析度被用来防止锯齿边。虽然它确实为我们提供了一种解决走样问题的方案,但却由于必须绘制比平时更多的片段而降低了性能。所以这个技术只流行了一段时间。
|
||||
最开始我们有一个叫做**超采样抗锯齿技术(Super Sample Anti-aliasing, SSAA)**,它暂时使用一个更高的分辨率(以超采样方式)来渲染场景,当输出图像在帧缓冲中被更新时,图像的分辨率会被下采样(down sample)回原来的分辨率。它使用额外的分辨率来防止锯齿边缘。虽然它确实为我们提供了一种解决走样问题的方案,但却由于必须绘制比平时更多的片段而降低了性能。所以这个技术只流行了一段时间。
|
||||
|
||||
这个技术的基础上诞生了更为现代的技术,叫做**多采样抗锯齿(Multisample Anti-aliasing)**或叫MSAA,虽然它借用了SSAA的理念,但却以更加高效的方式实现了它。这节教程我们会展开讨论这个MSAA技术,它是OpenGL内建的。
|
||||
这个技术的基础上诞生了更为现代的技术,叫做**多采样抗锯齿(Multisample Anti-aliasing)**或叫**MSAA**,虽然它借用了SSAA的理念,但却以更加高效的方式实现了它。这节教程我们会展开讨论这个OpenGL内建的MSAA技术。
|
||||
|
||||
## 多重采样
|
||||
|
||||
为了理解什么是多重采样(Multisampling),以及它是如何解决锯齿问题的,我们先要更深入了解一个OpenGL光栅化的工作方式。
|
||||
为了理解什么是多重采样(Multisampling),以及它是如何解决锯齿问题的,我们先要更深入了解一下OpenGL光栅化的工作方式。
|
||||
|
||||
光栅化是你的最终的经处理的顶点和片段着色器之间的所有算法和处理的集合。光栅化将属于一个基本图形的所有顶点转化为一系列片段。顶点坐标理论上可以含有任何坐标,但片段却不是这样,这是因为它们与你的窗口的解析度有关。几乎永远都不会有顶点坐标和片段的一对一映射,所以光栅化必须以某种方式决定每个特定顶点最终结束于哪个片段/屏幕坐标上。
|
||||
光栅化表示在我们输出的顶点(Vertices)和片段着色器(fragment shader)中间的所有算法和处理过程的集合(译者注: 在OpenGL中,光栅化步骤在几何着色器后,片段着色器前)。光栅化将属于一个图元的所有顶点转化为一系列片段。顶点坐标理论上可以是任意值,但片段却不是这样,这是因为它们受你的窗口的分辨率限制。几乎永远都不会有顶点坐标和片段的一对一映射,所以光栅化必须以某种方式决定每个顶点最终位于哪个片段/屏幕坐标上。
|
||||
|
||||

|
||||
|
||||
这里我们看到一个屏幕像素网格,每个像素中心包含一个采样点(sample point),它被用来决定一个像素是否被三角形所覆盖。红色的采样点如果被三角形覆盖,那么就会为这个被覆盖像(屏幕)素生成一个片段。即使三角形覆盖了部分屏幕像素,但是采样点没被覆盖,这个像素仍然不会受到任何片段着色器影响到。
|
||||
这里我们看到一个屏幕像素网格,每个像素中心包含一个采样点(sample point),它被用来决定一个像素是否被三角形所覆盖。红色的采样点表示该点被三角形所覆盖,片段着色器会给该屏幕像素着色。不过即使三角形覆盖了某一个屏幕像素的一部分,但是中心的采样点没被覆盖到,这个像素仍然不会受到片段着色器的影响。
|
||||
|
||||
你可能已经明白走样的原因来自何处了。三角形渲染后的版本最后在你的屏幕上是这样的:
|
||||
你可能现在已经明白走样的原因是什么了。上述三角形渲染后在你的屏幕上会是这样的:
|
||||
|
||||

|
||||
|
||||
由于屏幕像素总量的限制,有些边上的像素能被渲染出来,而有些则不会。结果就是我们渲染出的基本图形的非光滑边缘产生了上图的锯齿边。
|
||||
由于屏幕像素总量的限制,有些边上的像素能被渲染出来,而有些则不会。结果就是我们渲染出的图元的边缘产生了上图的锯齿。
|
||||
|
||||
多采样所做的正是不再使用单一采样点来决定三角形的覆盖范围,而是采用多个采样点。我们不再使用每个像素中心的采样点,取而代之的是4个子样本(subsample),用它们来决定像素的覆盖率。这意味着颜色缓冲的大小也由于每个像素的子样本的增加而增加了。
|
||||
多采样所做的正是不再使用单一采样点来决定三角形的覆盖范围,而是采用多个采样点(这大概就是它名字的由来)。我们不再使用每个像素中心的采样点,取而代之的是4个子采样(subsample),用它们来决定像素是否被覆盖。这意味着颜色缓冲的大小也由于每个像素的子样本的增加而增加了。
|
||||
|
||||

|
||||
|
||||
左侧的图显示了我们普通决定一个三角形的覆盖范围的方式。这个像素并不会运行一个片段着色器(这就仍保持空白),因为它的采样点没有被三角形所覆盖。右边的图展示了多采样的版本,每个像素包含4个采样点。这里我们可以看到只有2个采样点被三角形覆盖。
|
||||
上图的左侧显示了我们普通决定一个三角形是否覆盖屏幕像素的方式(译者注: 中心点,单采样)。这个像素并不会被一个片段着色器着色(因此它保持空白),因为它的采样点没有被三角形所覆盖。右边示了多采样的版本,每个像素包含4个采样点。这里我们可以看到只有2个采样点被三角形覆盖。
|
||||
|
||||
!!! Important
|
||||
|
||||
采样点的数量是任意的,更多的采样点能带来更精确的覆盖率。
|
||||
|
||||
多采样开始变得有趣了。2个子样本被三角覆盖,下一步是决定这个像素的颜色。我们原来猜测,我们会为每个被覆盖的子样本运行片段着色器,然后对每个像素的子样本的颜色进行平均化。例子的那种情况,我们在插值的顶点数据的每个子样本上运行片段着色器,然后将这些采样点的最终颜色储存起来。幸好,它不是这么运作的,因为这等于说我们必须运行更多的片段着色器,会明显降低性能。
|
||||
从这儿开始我们的多重采样变得有趣起来了。我们知道了只有2个子采样被三角覆盖,下一步就是决定这个像素的颜色。我们可以猜测一下,我们会为每个被覆盖的子采样运行片段着色器,然后对每个像素的所有子采样的颜色进行平均化。在这种情况下,我们需要为每一个被插值后的顶点数据的每一个子采样运行两次两次片段着色器,然后把采样点的颜色储存起来。幸好,它不是这么运作的,因为这等于说我们必须运行更多的片段着色器,会明显降低性能。
|
||||
|
||||
MSAA的真正工作方式是,每个像素只运行一次片段着色器,无论多少子样本被三角形所覆盖。片段着色器运行着插值到像素中心的顶点数据,最后颜色被储存近每个被覆盖的子样本中,每个像素的所有颜色接着将平均化,每个像素最终有了一个唯一颜色。在前面的图片中4个样本中只有2个被覆盖,像素的颜色将以三角形的颜色进行平均化,颜色同时也被储存到其他2个采样点,最后生成的是一种浅蓝色。
|
||||
MSAA的真正工作方式是,每个像素只运行一次片段着色器,无论该像素有多少子样本被三角形所覆盖。片段着色器接受的顶点数据是被插值到每一个像素的**中心**坐标,而其着色的颜色会被每个被三角形覆盖的子采样储存。一旦一个我们绘制的图元的子采样颜色缓冲被填满了,每个像素对应的所有颜色将会被平均化,这使得每个像素最终有了一个唯一颜色。比如在前面的图片中4个子采样中只有2个被三角形覆盖,像素的颜色事实上是一个均值,该均值由三角形的颜色和其他2个子采样的颜色(aka. 背景色)平均化而成,最后该像素被着色为一种浅蓝色。
|
||||
|
||||
结果是,颜色缓冲中所有基本图形的边都生成了更加平滑的样式。让我们看看当再次决定前面的三角形覆盖范围时多样本看起来是这样的:
|
||||
结果是,颜色缓冲中所有图元的边变得更加平滑了。让我们看看多重采样对前面的一个三角形来说是怎样做的:
|
||||
|
||||

|
||||
|
||||
这里每个像素包含着4个子样本(不相关的已被隐藏)蓝色的子样本是被三角形覆盖了的,灰色的没有被覆盖。三角形内部区域中的所有像素都会运行一次片段着色器,它输出的颜色被储存到所有4个子样本中。三角形的边缘并不是所有的子样本都会被覆盖,所以片段着色器的结果仅储存在部分子样本中。根据被覆盖子样本的数量,最终的像素颜色由三角形颜色和其他子样本所储存的颜色所决定。
|
||||
这里每个像素包含着4个子采样(不相关采样点的没有被标注出来)蓝色的子采样是被三角形覆盖了的,灰色的则没有被覆盖。三角形内部区域中的所有像素都会运行一次片段着色器,它输出的颜色由4个子采样决定。三角形的边缘并不是所有的子采样都会被覆盖,所以片段着色器的结果仅由部分的子采样决定。根据被覆盖子采样的数量,最终的像素颜色由三角形颜色和其他子采样所储存的颜色所决定。(译者注: 其实有点类似于Blending的原理。)
|
||||
|
||||
大致上来说,如果更多的采样点被覆盖,那么像素的颜色就会更接近于三角形。如果我们用早期使用的三角形的颜色填充像素,我们会获得这样的结果:
|
||||
大致上来说,如果更多的采样点被覆盖,那么像素的颜色就会更接近于三角形。如果我们用这种方去给我们前面的三角形的填充像素颜色,我们会获得这样的结果:
|
||||
|
||||

|
||||
|
||||
对于每个像素来说,被三角形覆盖的子样本越少,像素受到三角形的颜色的影响也越少。现在三角形的硬边被比实际颜色浅一些的颜色所包围,因此观察者从远处看上去就比较平滑了。
|
||||
对于每个像素来说,被三角形覆盖的子采样越少,像素受到三角形的颜色的影响也越少。现在原本三角形不平滑的边被比实际颜色浅一些的颜色像素所包围,因此观察者从远处看上去就比较平滑了。
|
||||
|
||||
不仅颜色值被多采样影响,深度和模板测试也同样使用了多采样点。比如深度测试,顶点的深度值在运行深度测试前被插值到每个子样本中,对于模板测试,我们为每个子样本储存模板值,而不是每个像素。这意味着深度和模板缓冲的大小随着像素子样本的增加也增加了。
|
||||
不仅颜色值会被多重采样技术影响,深度测试和模板测试也同样使用了多重采样技术。比如深度测试,顶点的深度值在运行深度测试前被插值到每个子采样中,对于模板测试,我们为每个子采样储存模板值,而不是每个像素。这意味着深度和模板缓冲的大小随着像素子样本的增加也增加了。
|
||||
|
||||
到目前为止我们所讨论的不过是多重采样技术的工作原理。光栅化背后实际的逻辑要比我们讨论的复杂,但你现在可以理解MSAA背后的概念和逻辑了。
|
||||
(译者注: 如果看到这里还是对原理似懂非懂,可以简单看看知乎上[@文刀秋二](https://www.zhihu.com/people/edliu/answers) 对抗锯齿技术的[精彩介绍](https://www.zhihu.com/question/20236638/answer/14438218))
|
||||
|
||||
到目前为止我们所讨论的不过是多采样发走样工作的方式。光栅化背后实际的逻辑要比我们讨论的复杂,但你现在可以理解多采样抗锯齿背后的概念和逻辑了。
|
||||
|
||||
## OpenGL中的MSAA
|
||||
|
||||
|
@@ -108,7 +108,7 @@ RenderScene();
|
||||
GLfloat aspect = (GLfloat)SHADOW_WIDTH/(GLfloat)SHADOW_HEIGHT;
|
||||
GLfloat near = 1.0f;
|
||||
GLfloat far = 25.0f;
|
||||
glm::mat4 shadowProj = glm::perspective(90.0f, aspect, near, far);
|
||||
glm::mat4 shadowProj = glm::perspective(glm::radians(90.0f), aspect, near, far);
|
||||
```
|
||||
|
||||
非常重要的一点是,这里glm::perspective的视野参数,设置为90度。90度我们才能保证视野足够大到可以合适地填满立方体贴图的一个面,立方体贴图的所有面都能与其他面在边缘对齐。
|
||||
@@ -202,7 +202,7 @@ void main()
|
||||
lightDistance = lightDistance / far_plane;
|
||||
|
||||
// Write this as modified depth
|
||||
gl_FragDepth = gl_FragCoord.z;
|
||||
gl_FragDepth = lightDistance;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -476,4 +476,4 @@ PCF算法的结果如果没有变得更好,也是非常不错的,这是柔
|
||||
|
||||
- [Shadow Mapping for point light sources in OpenGL](http://www.sunandblackcat.com/tipFullView.php?l=eng&topicid=36):sunandblackcat的万向阴影映射教程。
|
||||
- [Multipass Shadow Mapping With Point Lights](http://ogldev.atspace.co.uk/www/tutorial43/tutorial43.html):ogldev的万向阴影映射教程。
|
||||
- [Omni-directional Shadows](http://www.cg.tuwien.ac.at/~husky/RTR/OmnidirShadows-whyCaps.pdf):Peter Houska的关于万向阴影映射的一组很好的ppt。
|
||||
- [Omni-directional Shadows](http://www.cg.tuwien.ac.at/~husky/RTR/OmnidirShadows-whyCaps.pdf):Peter Houska的关于万向阴影映射的一组很好的ppt。
|
||||
|
@@ -4,7 +4,7 @@
|
||||
---|---
|
||||
作者 | JoeyDeVries
|
||||
翻译 | [Django](http://bullteacher.com/)
|
||||
校对 | 暂无
|
||||
校对 | [KenLee](https://hellokenlee.github.io/)
|
||||
|
||||
我们的场景中已经充满了多边形物体,其中每个都可能由成百上千平坦的三角形组成。我们以向三角形上附加纹理的方式来增加额外细节,提升真实感,隐藏多边形几何体是由无数三角形组成的事实。纹理确有助益,然而当你近看它们时,这个事实便隐藏不住了。现实中的物体表面并非是平坦的,而是表现出无数(凹凸不平的)细节。
|
||||
|
||||
@@ -182,10 +182,10 @@ bitangent1.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y);
|
||||
bitangent1.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z);
|
||||
bitangent1 = glm::normalize(bitangent1);
|
||||
|
||||
[...] // similar procedure for calculating tangent/bitangent for plane's second triangle
|
||||
[...] // 对平面的第二个三角形采用类似步骤计算切线和副切线
|
||||
```
|
||||
|
||||
我们预先计算出等式的分数部分f,然后把它和每个向量的元素进行相应矩阵乘法。如果你把代码和最终的等式对比你会发现,这就是直接套用。最后我们还要进行标准化,来确保切线/副切线向量最后是单位向量。
|
||||
我们预先计算出等式的分数部分`f`,然后把它和每个向量的元素进行相应矩阵乘法。如果你把代码和最终的等式对比你会发现,这就是直接套用。最后我们还要进行标准化,来确保切线/副切线向量最后是单位向量。
|
||||
|
||||
因为一个三角形永远是平坦的形状,我们只需为每个三角形计算一个切线/副切线,它们对于每个三角形上的顶点都是一样的。要注意的是大多数实现通常三角形和三角形之间都会共享顶点。这种情况下开发者通常将每个顶点的法线和切线/副切线等顶点属性平均化,以获得更加柔和的效果。我们的平面的三角形之间分享了一些顶点,但是因为两个三角形相互并行,因此并不需要将结果平均化,但无论何时只要你遇到这种情况记住它就是件好事。
|
||||
|
||||
@@ -224,12 +224,17 @@ void main()
|
||||
|
||||
我们先将所有TBN向量变换到我们所操作的坐标系中,现在是世界空间,我们可以乘以model矩阵。然后我们创建实际的TBN矩阵,直接把相应的向量应用到mat3构造器就行。注意,如果我们希望更精确的话就不要讲TBN向量乘以model矩阵,而是使用法线矩阵,但我们只关心向量的方向,不会平移也和缩放这个变换。
|
||||
|
||||
从技术上讲,顶点着色器中无需副切线。所有的这三个TBN向量都是相互垂直的所以我们可以在顶点着色器中庸T和N向量的叉乘,自己计算出副切线:vec3 B = cross(T, N);
|
||||
现在我们有了TBN矩阵,如果来使用它呢?基本有两种方式可以使用,我们会把这两种方式都说明一下:
|
||||
!!! Important
|
||||
|
||||
我们可以用TBN矩阵把所有向量从切线空间转到世界空间,传给像素着色器,然后把采样得到的法线用TBN矩阵从切线空间变换到世界空间;法线就处于和其他光照变量一样的空间中了。
|
||||
我们用TBN的逆矩阵把所有世界空间的向量转换到切线空间,使用这个矩阵将除法线以外的所有相关光照变量转换到切线空间中;这样法线也能和其他光照变量处于同一空间之中。
|
||||
我们来看看第一种情况。我们从法线贴图重采样得来的法线向量,是以切线空间表达的,尽管其他光照向量是以世界空间表达的。把TBN传给像素着色器,我们就能将采样得来的切线空间的法线乘以这个TBN矩阵,将法线向量变换到和其他光照向量一样的参考空间中。这种方式随后所有光照计算都可以简单的理解。
|
||||
从技术上讲,顶点着色器中无需副切线。所有的这三个TBN向量都是相互垂直的所以我们可以在顶点着色器中使用T和N向量的叉乘,自己计算出副切线:vec3 B = cross(T, N);
|
||||
|
||||
现在我们有了TBN矩阵,如果来使用它呢?通常来说有两种方式使用它,我们会把这两种方式都说明一下:
|
||||
|
||||
1. 我们直接使用TBN矩阵,这个矩阵可以把切线坐标空间的向量转换到世界坐标空间。因此我们把它传给片段着色器中,把通过采样得到的法线坐标左乘上TBN矩阵,转换到世界坐标空间中,这样所有法线和其他光照变量就在同一个坐标系中了。
|
||||
2. 我们也可以使用TBN矩阵的逆矩阵,这个矩阵可以把世界坐标空间的向量转换到切线坐标空间。因此我们使用这个矩阵左乘其他光照变量,把他们转换到切线空间,这样法线和其他光照变量再一次在一个坐标系中了。
|
||||
|
||||
|
||||
**我们来看看第一种情况。**我们从法线贴图重采样得来的法线向量,是以切线空间表达的,尽管其他光照向量是以世界空间表达的。把TBN传给像素着色器,我们就能将采样得来的切线空间的法线乘以这个TBN矩阵,将法线向量变换到和其他光照向量一样的参考空间中。这种方式随后所有光照计算都可以简单的理解。
|
||||
|
||||
把TBN矩阵发给像素着色器很简单:
|
||||
|
||||
@@ -268,7 +273,7 @@ normal = normalize(fs_in.TBN * normal);
|
||||
|
||||
因为最后的normal现在在世界空间中了,就不用改变其他像素着色器的代码了,因为光照代码就是假设法线向量在世界空间中。
|
||||
|
||||
我们同样看看第二种情况,我们用TBN矩阵的逆矩阵将所有相关的世界空间向量转变到采样所得法线向量的空间:切线空间。TBN的建构还是一样,但我们在将其发送给像素着色器之前先要求逆矩阵:
|
||||
**我们同样看看第二种情况。**我们用TBN矩阵的逆矩阵将所有相关的世界空间向量转变到采样所得法线向量的空间:切线空间。TBN的建构还是一样,但我们在将其发送给像素着色器之前先要求逆矩阵:
|
||||
|
||||
```c++
|
||||
vs_out.TBN = transpose(mat3(T, B, N));
|
||||
|
@@ -6,6 +6,10 @@
|
||||
翻译 | [J.moons](https://github.com/JiangMuWen)
|
||||
校对 | Meow J(初校)
|
||||
|
||||
!!! warning
|
||||
|
||||
注意,作者正在对PBR章节进行大的调整,原文的内容时时可能有更新,建议仍是阅读原文。
|
||||
|
||||
PBR,或者用更通俗一些的称呼是指<def>基于物理的渲染</def>(Physically Based Rendering),它指的是一些在不同程度上都基于与现实世界的物理原理更相符的基本理论所构成的渲染技术的集合。正因为基于物理的渲染目的便是为了使用一种更符合物理学规律的方式来模拟光线,因此这种渲染方式与我们原来的Phong或者Blinn-Phong光照算法相比总体上看起来要更真实一些。除了看起来更好些以外,由于它与物理性质非常接近,因此我们(尤其是美术师们)可以直接以物理参数为依据来编写表面材质,而不必依靠粗劣的修改与调整来让光照效果看上去正常。使用基于物理参数的方法来编写材质还有一个更大的好处,就是不论光照条件如何,这些材质看上去都会是正确的,而在非PBR的渲染管线当中有些东西就不会那么真实了。
|
||||
|
||||
虽然如此,基于物理的渲染仍然只是对基于物理原理的现实世界的一种近似,这也就是为什么它被称为**基于**物理的着色(Physically based Shading) 而非物理着色(Physical Shading)的原因。判断一种PBR光照模型是否是基于物理的,必须满足以下三个条件(不用担心,我们很快就会了解它们的):
|
||||
|
@@ -4,4 +4,6 @@
|
||||
|
||||
这篇教程暂时还没有进行翻译,您可以先阅读[原文](https://learnopengl.com/#!PBR/Lighting),或经常来刷新看看是否有更新的进展。当然,我们更欢迎您在[GitHub上](https://github.com/LearnOpenGL-CN/LearnOpenGL-CN)认领翻译这篇文章,帮助我们完善这个教程系列。
|
||||
|
||||
注意,作者正在对PBR章节进行大的调整,原文的内容时时可能有更新,所以建议暂时不要进行认领。
|
||||
|
||||
<img src="../../img/development.png" class="clean">
|
@@ -1,7 +1,9 @@
|
||||
# 漫反射辐照
|
||||
|
||||
**未完成**
|
||||
**暂无翻译**
|
||||
|
||||
这篇教程暂时还没有完成,您可以经常来刷新看看是否有更新的进展。
|
||||
这篇教程暂时还没有进行翻译,您可以先阅读[原文](https://learnopengl.com/#!PBR/IBL/Diffuse-irradiance),或经常来刷新看看是否有更新的进展。当然,我们更欢迎您在[GitHub上](https://github.com/LearnOpenGL-CN/LearnOpenGL-CN)认领翻译这篇文章,帮助我们完善这个教程系列。
|
||||
|
||||
注意,作者正在对PBR章节进行大的调整,原文的内容时时可能有更新,所以建议目前不要进行认领。
|
||||
|
||||
<img src="../../../img/development.png" class="clean">
|
@@ -1,7 +1,9 @@
|
||||
# 镜面IBL
|
||||
|
||||
**未完成**
|
||||
**暂无翻译**
|
||||
|
||||
这篇教程暂时还没有完成,您可以经常来刷新看看是否有更新的进展。
|
||||
这篇教程暂时还没有进行翻译,您可以先阅读[原文](https://learnopengl.com/#!PBR/IBL/Specular-IBL),或经常来刷新看看是否有更新的进展。当然,我们更欢迎您在[GitHub上](https://github.com/LearnOpenGL-CN/LearnOpenGL-CN)认领翻译这篇文章,帮助我们完善这个教程系列。
|
||||
|
||||
注意,作者正在对PBR章节进行大的调整,原文的内容时时可能有更新,所以建议目前不要进行认领。
|
||||
|
||||
<img src="../../../img/development.png" class="clean">
|
@@ -128,3 +128,10 @@ var {
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
color: #227;
|
||||
}
|
||||
|
||||
img.patreon {
|
||||
border: none;
|
||||
display: inline-block;
|
||||
max-height: 13px;
|
||||
margin-bottom: 2px;
|
||||
}
|
@@ -1,6 +1,8 @@
|
||||
# 欢迎来到OpenGL的世界
|
||||
|
||||
欢迎来到OpenGL的世界。这个工程只是我([JoeyDeVries](http://joeydevries.com/))的一次小小的尝试,希望能够建立起一个完善的OpenGL教学平台。无论你学习OpenGL是为了学业,找工作,或仅仅是因为兴趣,这个网站都将能够教会你**现代**(Core-profile) OpenGL的基础,中级,以及高级知识。LearnOpenGL的目标是使用易于理解的形式,使用清晰的例子,展现现代OpenGL的所有知识点,与此同时为以后的学习提供有用的参考。
|
||||
欢迎来到OpenGL的世界。这个工程只是我([Joey de Vries](http://joeydevries.com/))的一次小小的尝试,希望能够建立起一个完善的OpenGL教学平台。无论你学习OpenGL是为了学业,找工作,或仅仅是因为兴趣,这个网站都将能够教会你**现代**(Core-profile) OpenGL的基础,中级,以及高级知识。LearnOpenGL的目标是使用易于理解的形式,使用清晰的例子,展现现代OpenGL的所有知识点,与此同时为以后的学习提供有用的参考。
|
||||
|
||||
如果您喜欢这个系列教程的话,不妨到Joey de Vries的[Patreon](https://www.patreon.com/learnopengl)上支持一下作者,让这个教程能够持续完善并更新。
|
||||
|
||||
## 为什么要阅读这些教程呢?
|
||||
|
||||
|
BIN
yeti/img/patreon_logo.png
Normal file
BIN
yeti/img/patreon_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
@@ -66,6 +66,12 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a href="https://www.patreon.com/learnopengl">
|
||||
<img class="patreon" src="/img/patreon_logo.png" alt="">
|
||||
Patreon
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user