mirror of
https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git
synced 2025-08-23 20:55:27 +08:00
校对 04/02
This commit is contained in:
@@ -1,10 +1,14 @@
|
|||||||
##模板测试(Stencil testing)
|
# 模板测试(Stencil testing)
|
||||||
|
|
||||||
本文作者JoeyDeVries,由Django翻译自http://learnopengl.com
|
原文 | [Stencil testing](http://learnopengl.com/#!Advanced-OpenGL/Stencil-testing)
|
||||||
|
---|---
|
||||||
|
作者 | JoeyDeVries
|
||||||
|
翻译 | [Django](http://bullteacher.com/)
|
||||||
|
校对 | [Geequlim](http://geequlim.com)
|
||||||
|
|
||||||
当像素着色器处理完fragment以后,模板测试(stencil test)就开始执行了,和深度测试一样,它有能力丢弃fragment。仍然保留下来的fragment进入深度测试阶段,深度测试可能丢弃更多的fragment。模板测试基于另一个缓冲,这个缓冲叫做模板缓冲(stencil buffer),我们被允许在渲染时更新它来获取有意思的效果。
|
当片段着色器处理完片段之后,**模板测试(stencil test)** 就开始执行了,和深度测试一样,它能丢弃一些片段。仍然保留下来的片段进入深度测试阶段,深度测试可能丢弃更多。模板测试基于另一个缓冲,这个缓冲叫做**模板缓冲(stencil buffer)**,我们被允许在渲染时更新它来获取有意思的效果。
|
||||||
|
|
||||||
模板缓冲中一个模板值(stencil value)通常含有8位大小,因此每个像素/fragment总共有256个不同的模板值(译注:8位就是1字节大小,因此和char的容量一样是256个不同值)。这样我们就能将这些模板值设置为我们链接的,然后当一个特定fragment有一个特定的模板值,我们就可以丢弃或保留fragment了。
|
模板缓冲中的模板值(stencil value)通常是8位的,因此每个片段(像素)共有256种不同的模板值(译注:8位就是1字节大小,因此和char的容量一样是256个不同值)。这样我们就能将这些模板值设置为我们链接的,然后在模板测试时根据这个模板值,我们就可以决定丢弃或保留它了。
|
||||||
|
|
||||||
!!! Important
|
!!! Important
|
||||||
|
|
||||||
@@ -14,18 +18,18 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
模板缓冲先清空模板缓冲中设置的零,然后开启矩形一。场景中的fragment值渲染模板值包含1的那些fragment(其他的都被丢弃)。
|
模板缓冲先清空模板缓冲设置所有片段的模板值为0,然后开启矩形片段用1填充。场景中的模板值为1的那些片段才会被渲染(其他的都被丢弃)。
|
||||||
|
|
||||||
无论我们在渲染哪里的fragment,模板缓冲操作都允许我们把模板缓冲设置为一个特定值。改变模板缓冲的内容实际上就是对模板缓冲进行写入。在同一次(或接下来的)渲染迭代我们可以读取这些值来决定丢弃还是保留这些fragment。当使用模板缓冲的时候,你可以随心所欲,但是需要遵守下面的原则:
|
无论我们在渲染哪里的片段,模板缓冲操作都允许我们把模板缓冲设置为一个特定值。改变模板缓冲的内容实际上就是对模板缓冲进行写入。在同一次(或接下来的)渲染迭代我们可以读取这些值来决定丢弃还是保留这些片段。当使用模板缓冲的时候,你可以随心所欲,但是需要遵守下面的原则:
|
||||||
|
|
||||||
* 开启模板缓冲写入。
|
* 开启模板缓冲写入。
|
||||||
* 渲染物体,更新模板缓冲。
|
* 渲染物体,更新模板缓冲。
|
||||||
* 关闭模板缓冲写入。
|
* 关闭模板缓冲写入。
|
||||||
* 渲染(其他)物体,这次基于模板缓冲内容丢弃特定fragment。
|
* 渲染(其他)物体,这次基于模板缓冲内容丢弃特定片段。
|
||||||
|
|
||||||
使用模板缓冲我们可以基于场景中已经绘制的fragment,来决定是否丢弃特定的fragment。
|
使用模板缓冲我们可以基于场景中已经绘制的片段,来决定是否丢弃特定的片段。
|
||||||
|
|
||||||
你可以开启GL_STENCIL_TEST来开启模板测试。接着所有渲染函数调用都会以这样或那样的方式影响到模板缓冲。
|
你可以开启`GL_STENCIL_TEST`来开启模板测试。接着所有渲染函数调用都会以这样或那样的方式影响到模板缓冲。
|
||||||
|
|
||||||
```c
|
```c
|
||||||
glEnable(GL_STENCIL_TEST);
|
glEnable(GL_STENCIL_TEST);
|
||||||
@@ -36,24 +40,30 @@ glEnable(GL_STENCIL_TEST);
|
|||||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
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++
|
```c++
|
||||||
glStencilMask(0xFF); // Each bit is written to the stencil buffer as is
|
|
||||||
glStencilMask(0x00); // Each bit ends up as 0 in the stencil buffer (disabling writes)
|
// 0xFF == 0b11111111
|
||||||
|
//此时,模板值与它进行按位与运算结果是模板值,模板缓冲可写
|
||||||
|
glStencilMask(0xFF);
|
||||||
|
|
||||||
|
// 0x00 == 0b00000000 == 0
|
||||||
|
//此时,模板值与它进行按位与运算结果是0,模板缓冲不可写
|
||||||
|
glStencilMask(0x00);
|
||||||
```
|
```
|
||||||
|
|
||||||
大多数情况你的模板遮罩(stencil mask)写为0x00或0xFF就行,但是最好知道有一个选项可以自定义位遮罩。
|
大多数情况你的模板遮罩(stencil mask)写为0x00或0xFF就行,但是最好知道有一个选项可以自定义位遮罩。
|
||||||
|
|
||||||
###模板方程(stencil functions)
|
## 模板函数(stencil functions)
|
||||||
|
|
||||||
和深度测试一样,我们也有几个不同控制权,决定何时模板测试通过或失败以及它怎样影响模板缓冲。一共有两种方程可供我们使用去配置模板测试:glStencilFunc和glStencilOp。
|
和深度测试一样,我们也有几个不同控制权,决定何时模板测试通过或失败以及它怎样影响模板缓冲。一共有两种函数可供我们使用去配置模板测试:`glStencilFunc`和`glStencilOp`。
|
||||||
|
|
||||||
glStencilFunc(GLenum func, GLint ref, GLuint mask)有三个参数:
|
`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。它们的语义和深度缓冲的方程相似。
|
* **func**:设置模板测试操作。这个测试操作应用到已经储存的模板值和`glStencilFunc`的`ref`值上,可用的选项是:`GL_NEVER`、`GL_LEQUAL`、`GL_GREATER`、`GL_GEQUAL`、`GL_EQUAL`、`GL_NOTEQUAL`、`GL_ALWAYS`。它们的语义和深度缓冲的相似。
|
||||||
* ref:指定模板测试的引用值。模板缓冲的内容会与这个值对比。
|
* **ref**:指定模板测试的引用值。模板缓冲的内容会与这个值对比。
|
||||||
* mask:指定一个遮罩,在模板测试对比引用值和储存的模板值前,对它们进行与(and)操作。初始设置为1。
|
* **mask**:指定一个遮罩,在模板测试对比引用值和储存的模板值前,对它们进行按位与(and)操作,初始设置为1。
|
||||||
|
|
||||||
在上面简单模板的例子里,方程应该设置为:
|
在上面简单模板的例子里,方程应该设置为:
|
||||||
|
|
||||||
@@ -61,52 +71,52 @@ glStencilFunc(GLenum func, GLint ref, GLuint mask)有三个参数:
|
|||||||
glStencilFunc(GL_EQUAL, 1, 0xFF)
|
glStencilFunc(GL_EQUAL, 1, 0xFF)
|
||||||
```
|
```
|
||||||
|
|
||||||
它会告诉OpenGL,无论何时,一个fragment模板值等于(GL_EQUAL)引用值1,fragment就能通过测试被绘制了,否则就会被丢弃。
|
它会告诉OpenGL,无论何时,一个片段模板值等于(`GL_EQUAL`)引用值`1`,片段就能通过测试被绘制了,否则就会被丢弃。
|
||||||
|
|
||||||
但是glStencilFunc只描述了OpenGL对模板缓冲做什么,而不是我们如何更新缓冲。这就需要glStencilOp登场了。
|
但是`glStencilFunc`只描述了OpenGL对模板缓冲做什么,而不是描述我们如何更新缓冲。这就需要`glStencilOp`登场了。
|
||||||
|
|
||||||
glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)包含三个选项,我们可以指定每个选项的动作:
|
`void glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)`函数包含三个选项,我们可以指定每个选项的动作:
|
||||||
|
|
||||||
* sfail:如果模板测试失败将采取的动作。
|
* **sfail**: 如果模板测试失败将采取的动作。
|
||||||
* dpfail:如果模板测试通过,但是深度测试失败时采取的动作。
|
* **dpfail**: 如果模板测试通过,但是深度测试失败时采取的动作。
|
||||||
* dppass:如果深度测试和模板测试都通过,将采取的动作。
|
* **dppass**: 如果深度测试和模板测试都通过,将采取的动作。
|
||||||
|
|
||||||
每个选项都可以使用下列任何一个动作。
|
每个选项都可以使用下列任何一个动作。
|
||||||
|
|
||||||
操作 | 描述
|
操作 | 描述
|
||||||
---|---
|
---|---
|
||||||
GL_KEEP |The currently stored stencil value is kept.
|
GL_KEEP | 保持现有的模板值
|
||||||
GL_ZERO |The stencil value is set to 0.
|
GL_ZERO | 将模板值置为0
|
||||||
GL_REPLACE| The stencil value is replaced with the reference value set with glStencilFunc.
|
GL_REPLACE | 将模板值设置为用`glStencilFunc`函数设置的**ref**值
|
||||||
GL_INCR |The stencil value is increased by 1 if it is lower than the maximum value.
|
GL_INCR | 如果模板值不是最大值就将模板值+1
|
||||||
GL_INCR_WRAP| Same as GL_INCR, but wraps it back to 0 as soon as the maximum value is exceeded.
|
GL_INCR_WRAP| 与`GL_INCR`一样将模板值+1,如果模板值已经是最大值则设为0
|
||||||
GL_DECR |The stencil value is decreased by 1 if it is higher than the minimum value.
|
GL_DECR | 如果模板值不是最小值就将模板值-1
|
||||||
GL_DECR_WRAP| Same as GL_DECR, but wraps it to the maximum value if it ends up lower than 0.
|
GL_DECR_WRAP| 与`GL_DECR`一样将模板值-1,如果模板值已经是最小值则设为最大值
|
||||||
GL_INVERT | Bitwise inverts the current stencil buffer value.
|
GL_INVERT | Bitwise inverts the current stencil buffer value.
|
||||||
|
|
||||||
glStencilOp函数默认设置为 (GL_KEEP, GL_KEEP, GL_KEEP) ,所以任何测试的任何结果,模板缓冲都会保留它的值。默认行为不会更新模板缓冲,所以如果你想写入模板缓冲的话,你必须像任意选项指定至少一个不同的动作。
|
`glStencilOp`函数默认设置为 (GL_KEEP, GL_KEEP, GL_KEEP) ,所以任何测试的任何结果,模板缓冲都会保留它的值。默认行为不会更新模板缓冲,所以如果你想写入模板缓冲的话,你必须像任意选项指定至少一个不同的动作。
|
||||||
|
|
||||||
使用glStencilFunc和glStencilOp,我们就可以指定在什么时候以及我们打算怎么样去更新模板缓冲了,我们也可以指定何时让测试通过或不通过。什么时候fragment会被抛弃。
|
使用`glStencilFunc`和`glStencilOp`,我们就可以指定在什么时候以及我们打算怎么样去更新模板缓冲了,我们也可以指定何时让测试通过或不通过。什么时候片段会被抛弃。
|
||||||
|
|
||||||
###物体轮廓
|
## 物体轮廓
|
||||||
|
|
||||||
看了前面的部分你未必能理解模板测试是如何工作的,所以我们会展示一个模板测试可以实现的一个特别的和有用的功能,叫做物体轮廓(object outlining)。
|
看了前面的部分你未必能理解模板测试是如何工作的,所以我们会展示一个用模板测试实现的一个特别的和有用的功能,叫做物体轮廓(object outlining)。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
物体轮廓就像它的名字所描述的那样,它能够给每个(或一个)物体创建一个有颜色的边。在策略游戏中当你打算选择一个单位的时候它特别有用。给物体加上轮廓的步骤如下:
|
物体轮廓就像它的名字所描述的那样,它能够给每个(或一个)物体创建一个有颜色的边。在策略游戏中当你打算选择一个单位的时候它特别有用。给物体加上轮廓的步骤如下:
|
||||||
|
|
||||||
1. 在绘制(有轮廓的)物体前,把模板方程设置为GL_ALWAYS,用1更新物体的将被渲染的fragment。
|
1. 在绘制物体前,把模板方程设置为`GL_ALWAYS`,用1更新物体将被渲染的片段。
|
||||||
2. 渲染物体。
|
2. 渲染物体,写入模板缓冲。
|
||||||
3. 关闭模板写入和深度测试。
|
3. 关闭模板写入和深度测试。
|
||||||
4. 每个物体放大一点点。
|
4. 每个物体放大一点点。
|
||||||
5. 使用一个不同的像素着色器输出一个纯颜色。
|
5. 使用一个不同的片段着色器用来输出一个纯颜色。
|
||||||
6. 再次绘制物体,但只是当它们的fragment的模板值不为1时才进行。
|
6. 再次绘制物体,但只是当它们的片段的模板值不为1时才进行。
|
||||||
7. 开启模板写入和深度测试。
|
7. 开启模板写入和深度测试。
|
||||||
|
|
||||||
这个过程将每个物体的fragment模板缓冲设置为1,当我们绘制边框的时候,我们基本上绘制的是放大版本的物体和通过测试的地方,放大的版本绘制后物体就会有一个边。我们基本会使用模板缓冲丢弃所有的不是原来物体的fragment的放大的版本内容。
|
这个过程将每个物体的片段模板缓冲设置为1,当我们绘制边框的时候,我们基本上绘制的是放大版本的物体的通过测试的地方,放大的版本绘制后物体就会有一个边。我们基本会使用模板缓冲丢弃所有的不是原来物体的片段的放大的版本内容。
|
||||||
|
|
||||||
我们先来创建一个非常基本的像素着色器,它输出一个边框颜色。我们简单地设置一个固定的颜色值,把这个着色器命名为shaderSingleColor:
|
我们先来创建一个非常基本的片段着色器,它输出一个边框颜色。我们简单地设置一个固定的颜色值,把这个着色器命名为shaderSingleColor:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
void main()
|
void main()
|
||||||
@@ -115,39 +125,39 @@ void main()
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
我们只打算给两个箱子加上边框,所以我们不会对地面做什么。这样我们要先绘制地面,然后再绘制两个箱子(同时写入模板缓冲),接着我们绘制放大的箱子(同时丢弃前面已经绘制的箱子的那部分fragment)。
|
我们只打算给两个箱子加上边框,所以我们不会对地面做什么。这样我们要先绘制地面,然后再绘制两个箱子(同时写入模板缓冲),接着我们绘制放大的箱子(同时丢弃前面已经绘制的箱子的那部分片段)。
|
||||||
|
|
||||||
我们先开启模板测试,把将设置为无论何时、任何测试、通过或失败时才采取动作:
|
我们先开启模板测试,设置模板、深度测试通过或失败时才采取动作:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
glEnable(GL_DEPTH_TEST);
|
glEnable(GL_DEPTH_TEST);
|
||||||
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
|
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
|
||||||
```
|
```
|
||||||
|
|
||||||
如果任何测试失败我们都什么也不做,我们简单地保持深度缓冲中当前所储存着的值。如果模板测试和深度测试都成功了,我们就将储存着的模板值替换为1,我们要用glStencilFunc来做这件事。
|
如果任何测试失败我们都什么也不做,我们简单地保持深度缓冲中当前所储存着的值。如果模板测试和深度测试都成功了,我们就将储存着的模板值替换为`1`,我们要用`glStencilFunc`来做这件事。
|
||||||
|
|
||||||
我们清空模板缓冲为0,为箱子的所有绘制的fragment的模板缓冲更新为1:
|
我们清空模板缓冲为0,为箱子的所有绘制的片段的模板缓冲更新为1:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
glStencilFunc(GL_ALWAYS, 1, 0xFF); // All fragments should update the stencil buffer
|
glStencilFunc(GL_ALWAYS, 1, 0xFF); //所有片段都要写入模板缓冲
|
||||||
glStencilMask(0xFF); // Enable writing to the stencil buffer
|
glStencilMask(0xFF); // 设置模板缓冲为可写状态
|
||||||
normalShader.Use();
|
normalShader.Use();
|
||||||
DrawTwoContainers();
|
DrawTwoContainers();
|
||||||
```
|
```
|
||||||
|
|
||||||
使用GL_ALWAYS模板测试方程,我们确保箱子的每个fragment用模板值1更新模板缓冲。因为fragment总会通过模板测试,在我们绘制它们的地方,模板缓冲用引用值更新。
|
使用`GL_ALWAYS`模板测试函数,我们确保箱子的每个片段用模板值1更新模板缓冲。因为片段总会通过模板测试,在我们绘制它们的地方,模板缓冲用引用值更新。
|
||||||
|
|
||||||
现在箱子绘制之处,模板缓冲更新为1了,我们将要绘制放大的箱子,但是这次关闭模板缓冲的写入:
|
现在箱子绘制之处,模板缓冲更新为1了,我们将要绘制放大的箱子,但是这次关闭模板缓冲的写入:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
|
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
|
||||||
glStencilMask(0x00); // Disable writing to the stencil buffer
|
glStencilMask(0x00); // 禁止修改模板缓冲
|
||||||
glDisable(GL_DEPTH_TEST);
|
glDisable(GL_DEPTH_TEST);
|
||||||
shaderSingleColor.Use();
|
shaderSingleColor.Use();
|
||||||
DrawTwoScaledUpContainers();
|
DrawTwoScaledUpContainers();
|
||||||
```
|
```
|
||||||
|
|
||||||
我们把模板方程设置为GL_NOTEQUAL,它保证我们只箱子上不等于1的部分,这样只绘制前面绘制的箱子外围的那部分。注意,我们也要关闭深度测试,这样放大的的箱子也就是边框才不会被地面覆盖。
|
我们把模板方程设置为`GL_NOTEQUAL`,它保证我们只箱子上不等于1的部分,这样只绘制前面绘制的箱子外围的那部分。注意,我们也要关闭深度测试,这样放大的的箱子也就是边框才不会被地面覆盖。
|
||||||
|
|
||||||
做完之后还要保证再次开启深度缓冲。
|
做完之后还要保证再次开启深度缓冲。
|
||||||
|
|
||||||
@@ -159,7 +169,7 @@ glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
|
|||||||
|
|
||||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||||
|
|
||||||
glStencilMask(0x00); // Make sure we don't update the stencil buffer while drawing the floor
|
glStencilMask(0x00); // 绘制地板时确保关闭模板缓冲的写入
|
||||||
normalShader.Use();
|
normalShader.Use();
|
||||||
DrawFloor()
|
DrawFloor()
|
||||||
|
|
||||||
@@ -179,9 +189,10 @@ glEnable(GL_DEPTH_TEST);
|
|||||||
理解这段代码后面的模板测试的思路并不难以理解。如果还不明白尝试再仔细阅读上面的部分,尝试理解每个函数的作用,现在你已经看到了它的使用方法的例子。
|
理解这段代码后面的模板测试的思路并不难以理解。如果还不明白尝试再仔细阅读上面的部分,尝试理解每个函数的作用,现在你已经看到了它的使用方法的例子。
|
||||||
|
|
||||||
这个边框的算法的结果在深度测试教程的那个场景中,看起来像这样:
|
这个边框的算法的结果在深度测试教程的那个场景中,看起来像这样:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
在这里查看源码和着色器,看看完整的物体边框算法是怎样的。
|
在这里[查看源码](http://learnopengl.com/code_viewer.php?code=advanced/stencil_testing)和[着色器](http://learnopengl.com/code_viewer.php?code=advanced/depth_testing_func_shaders),看看完整的物体边框算法是怎样的。
|
||||||
|
|
||||||
!!! Important
|
!!! Important
|
||||||
|
|
||||||
|
@@ -36,7 +36,7 @@ unsigned char * image = SOIL_load_image(path, &width, &height, 0, SOIL_LOAD_RGBA
|
|||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image);
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image);
|
||||||
```
|
```
|
||||||
|
|
||||||
保证你在像素着色器中获取了纹理的所有4个颜色元素,而不仅仅是RGB元素:
|
保证你在片段着色器中获取了纹理的所有4个颜色元素,而不仅仅是RGB元素:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
void main()
|
void main()
|
||||||
@@ -79,7 +79,7 @@ glBindVertexArray(0);
|
|||||||
运行程序你将看到:
|
运行程序你将看到:
|
||||||

|

|
||||||
|
|
||||||
出现这种情况是因为OpenGL默认是不知道如何处理alpha值的,不知道何时丢弃它们。我们不得不手动做这件事。幸运的是这很简单,感谢着色器吧。GLSL为我们提供了discard命令,它保证了fragment不会被进一步处理,这样就不会进入颜色缓冲。有了这个命令我们就可以在像素着色器中检查一个fragment是否有在一定的阈限下的alpha值,如果有,那么丢弃这个fragment,就好像它从来都没被处理过一样:
|
出现这种情况是因为OpenGL默认是不知道如何处理alpha值的,不知道何时丢弃它们。我们不得不手动做这件事。幸运的是这很简单,感谢着色器吧。GLSL为我们提供了discard命令,它保证了fragment不会被进一步处理,这样就不会进入颜色缓冲。有了这个命令我们就可以在片段着色器中检查一个fragment是否有在一定的阈限下的alpha值,如果有,那么丢弃这个fragment,就好像它从来都没被处理过一样:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
#version 330 core
|
#version 330 core
|
||||||
@@ -98,7 +98,7 @@ void main()
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
在这儿我们检查被采样纹理颜色包含着一个低于0.1这个阈限的alpha值,如果有,就丢弃fragment。这个像素着色器能够保证我们只渲染哪些不是完全透明的fragment。现在我们来看看效果:
|
在这儿我们检查被采样纹理颜色包含着一个低于0.1这个阈限的alpha值,如果有,就丢弃fragment。这个片段着色器能够保证我们只渲染哪些不是完全透明的fragment。现在我们来看看效果:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ C¯result = C¯source ∗ Fsource + C¯destination ∗ Fdestination
|
|||||||
* Fsource:源因子。设置了对源颜色的alpha值影响。
|
* Fsource:源因子。设置了对源颜色的alpha值影响。
|
||||||
* Fdestination:目标因子。设置了对目标颜色的alpha影响。
|
* Fdestination:目标因子。设置了对目标颜色的alpha影响。
|
||||||
|
|
||||||
像素着色器运行和所有的测试就通过了以后,混合方程才能自由执行fragment的颜色输出,当前它在颜色缓冲中(前面的fragment的颜色在当前fragment之前储存)。源和目标颜色会自动被OpenGL设置,但是源和目标因子可以让我们自由设置。我们来看一个简单的例子:
|
片段着色器运行和所有的测试就通过了以后,混合方程才能自由执行fragment的颜色输出,当前它在颜色缓冲中(前面的fragment的颜色在当前fragment之前储存)。源和目标颜色会自动被OpenGL设置,但是源和目标因子可以让我们自由设置。我们来看一个简单的例子:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -204,7 +204,7 @@ glEnable(GL_BLEND);
|
|||||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
```
|
```
|
||||||
|
|
||||||
由于我们开启了混合,就不需要丢弃fragment了,所以我们把像素着色器设置为原来的那个版本:
|
由于我们开启了混合,就不需要丢弃fragment了,所以我们把片段着色器设置为原来的那个版本:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
#version 330 core
|
#version 330 core
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
本文作者JoeyDeVries,由Django翻译自http://learnopengl.com
|
本文作者JoeyDeVries,由Django翻译自http://learnopengl.com
|
||||||
|
|
||||||
尝试在头脑中想象一下有一个3D立方体,你从任何一个方向去看他,你最多可以一次看到多少个面。如果你的想象力不是过于丰富,你最终最多能数出来的面是3个。你可以从一个立方体的任意位置和方向上去看它,但是你永远不能看到多于3个面。所以我们为何还要去绘制那三个不会显示出来的3个面呢。如果我们可以以某种方式丢弃它们,我们会提高像素着色器50%的性能!
|
尝试在头脑中想象一下有一个3D立方体,你从任何一个方向去看他,你最多可以一次看到多少个面。如果你的想象力不是过于丰富,你最终最多能数出来的面是3个。你可以从一个立方体的任意位置和方向上去看它,但是你永远不能看到多于3个面。所以我们为何还要去绘制那三个不会显示出来的3个面呢。如果我们可以以某种方式丢弃它们,我们会提高片段着色器50%的性能!
|
||||||
|
|
||||||
!!! Important
|
!!! Important
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
这的确是个好主意,但是有个问题需要我们去解决:我们如何知道某个面在观察者的视野中不会出现呢?如果我们去想象任何封闭的形状,它每个面有两面。一面面向用户,另一面背对用户。假如我们只渲染面向观察者的面会怎样?
|
这的确是个好主意,但是有个问题需要我们去解决:我们如何知道某个面在观察者的视野中不会出现呢?如果我们去想象任何封闭的形状,它每个面有两面。一面面向用户,另一面背对用户。假如我们只渲染面向观察者的面会怎样?
|
||||||
|
|
||||||
这正是面剔除所做的(face culling)。OpenGL检查所有正面朝向(front facing)观察者的面,并渲染它们,而丢弃所有背面朝向(back facing)的面,这样就节约了我们很多像素着色器的命令(它们很昂贵!)。我们必须告诉OpenGL我们使用的哪个面是正面,哪个面是反面。OpenGL使用一种聪明的手段解决这个问题——分析顶点数据的链接顺序(winding order)。
|
这正是面剔除所做的(face culling)。OpenGL检查所有正面朝向(front facing)观察者的面,并渲染它们,而丢弃所有背面朝向(back facing)的面,这样就节约了我们很多片段着色器的命令(它们很昂贵!)。我们必须告诉OpenGL我们使用的哪个面是正面,哪个面是反面。OpenGL使用一种聪明的手段解决这个问题——分析顶点数据的链接顺序(winding order)。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@@ -224,7 +224,7 @@ void main()
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
没有花哨的地方。像素着色器更简洁,因为我们做的唯一一件事是从纹理采样:
|
没有花哨的地方。片段着色器更简洁,因为我们做的唯一一件事是从纹理采样:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
#version 330 core
|
#version 330 core
|
||||||
@@ -272,7 +272,7 @@ glBindVertexArray(0);
|
|||||||
|
|
||||||
你可以从这里得到应用的源码。
|
你可以从这里得到应用的源码。
|
||||||
|
|
||||||
然而这有什么好处呢?好处就是我们现在可以自由的获取已经渲染场景中的任何像素,然后把它当作一个纹理图像了,我们可以在像素着色器中创建一些有意思的效果。所有这些有意思的效果统称为后处理特效。
|
然而这有什么好处呢?好处就是我们现在可以自由的获取已经渲染场景中的任何像素,然后把它当作一个纹理图像了,我们可以在片段着色器中创建一些有意思的效果。所有这些有意思的效果统称为后处理特效。
|
||||||
|
|
||||||
|
|
||||||
###后处理
|
###后处理
|
||||||
@@ -281,7 +281,7 @@ glBindVertexArray(0);
|
|||||||
|
|
||||||
###反相
|
###反相
|
||||||
|
|
||||||
我们已经取得了渲染输出的每个颜色,所以在像素着色器里返回这些颜色的反色并不难。我们得到屏幕纹理的颜色,然后用1.0减去它:
|
我们已经取得了渲染输出的每个颜色,所以在片段着色器里返回这些颜色的反色并不难。我们得到屏幕纹理的颜色,然后用1.0减去它:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
void main()
|
void main()
|
||||||
@@ -335,7 +335,7 @@ kernel是一个长得有点像一个小矩阵的数值数组,它中间的值
|
|||||||
|
|
||||||
你在网上能找到的kernel的例子大多数都是所有值加起来等于1,如果加起来不等于1就意味着这个纹理值比原来更大或者更小了。
|
你在网上能找到的kernel的例子大多数都是所有值加起来等于1,如果加起来不等于1就意味着这个纹理值比原来更大或者更小了。
|
||||||
|
|
||||||
kernel对于后处理来说非常管用,因为用起来简单。网上能找到有很多实例,为了能用上kernel我们还得改改像素着色器。这里假设每个kernel都是3×3(实际上大多数都是3×3):
|
kernel对于后处理来说非常管用,因为用起来简单。网上能找到有很多实例,为了能用上kernel我们还得改改片段着色器。这里假设每个kernel都是3×3(实际上大多数都是3×3):
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
const float offset = 1.0 / 300;
|
const float offset = 1.0 / 300;
|
||||||
@@ -373,7 +373,7 @@ void main()
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
在像素着色器中我们先为每个四周的纹理坐标创建一个9个vec2偏移量的数组。偏移量是一个简单的常数,你可以设置为自己喜欢的。接着我们定义kernel,这里应该是一个锐化kernel,它通过一种有趣的方式从所有周边的像素采样,对每个颜色值进行锐化。最后,在采样的时候我们把每个偏移量加到当前纹理坐标上,然后用加在一起的kernel的值乘以这些纹理值。
|
在片段着色器中我们先为每个四周的纹理坐标创建一个9个vec2偏移量的数组。偏移量是一个简单的常数,你可以设置为自己喜欢的。接着我们定义kernel,这里应该是一个锐化kernel,它通过一种有趣的方式从所有周边的像素采样,对每个颜色值进行锐化。最后,在采样的时候我们把每个偏移量加到当前纹理坐标上,然后用加在一起的kernel的值乘以这些纹理值。
|
||||||
|
|
||||||
这个锐化的kernel看起来像这样:
|
这个锐化的kernel看起来像这样:
|
||||||
|
|
||||||
|
@@ -71,7 +71,7 @@ glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
在像素着色器中,我们也必须使用一个不同的采样器——samplerCube,用它来从texture函数中采样,但是这次使用的是一个vec3方向向量,取代vec2。下面是一个像素着色器使用了cubemap的例子:
|
在片段着色器中,我们也必须使用一个不同的采样器——samplerCube,用它来从texture函数中采样,但是这次使用的是一个vec3方向向量,取代vec2。下面是一个片段着色器使用了cubemap的例子:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
|
|
||||||
@@ -182,7 +182,7 @@ void main()
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
注意,顶点着色器有意思的地方在于我们把输入的位置向量作为输出给像素着色器的纹理坐标。像素着色器就会把它们作为输入去采样samplerCube:
|
注意,顶点着色器有意思的地方在于我们把输入的位置向量作为输出给片段着色器的纹理坐标。片段着色器就会把它们作为输入去采样samplerCube:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
#version 330 core
|
#version 330 core
|
||||||
@@ -197,7 +197,7 @@ void main()
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
像素着色器比较明了。我们把顶点属性中的位置向量作为纹理的方向向量,使用它们从cubemap采样纹理值。渲染天空盒现在很简单,我们有了一个cubemap纹理,我们简单绑定cubemap纹理,天空盒就自动地用天空盒的cubemap填充了。为了绘制天空盒,我们将把它作为场景中第一个绘制的物体并且关闭深度写入。这样天空盒才能成为所有其他物体的背景来绘制出来。
|
片段着色器比较明了。我们把顶点属性中的位置向量作为纹理的方向向量,使用它们从cubemap采样纹理值。渲染天空盒现在很简单,我们有了一个cubemap纹理,我们简单绑定cubemap纹理,天空盒就自动地用天空盒的cubemap填充了。为了绘制天空盒,我们将把它作为场景中第一个绘制的物体并且关闭深度写入。这样天空盒才能成为所有其他物体的背景来绘制出来。
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
|
|
||||||
@@ -228,9 +228,9 @@ glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix()));
|
|||||||
|
|
||||||
#### 优化
|
#### 优化
|
||||||
|
|
||||||
现在我们在渲染场景中的其他物体之前渲染了天空盒。这么做没错,但是不怎么高效。如果我们先渲染了天空盒,那么我们就是在为每一个屏幕上的像素运行像素着色器,即使天空盒只有部分在显示着;fragment可以使用前置深度测试(early depth testing)简单地被丢弃,这样就节省了我们宝贵的带宽。
|
现在我们在渲染场景中的其他物体之前渲染了天空盒。这么做没错,但是不怎么高效。如果我们先渲染了天空盒,那么我们就是在为每一个屏幕上的像素运行片段着色器,即使天空盒只有部分在显示着;fragment可以使用前置深度测试(early depth testing)简单地被丢弃,这样就节省了我们宝贵的带宽。
|
||||||
|
|
||||||
所以最后渲染天空盒就能够给我们带来轻微的性能提升。采用这种方式,深度缓冲被全部物体的深度值完全填充,所以我们只需要渲染通过前置深度测试的那部分天空的fragment就行了,而且能显著减少像素着色器的调用。问题是天空盒是个1×1×1的立方体,极有可能会渲染失败,因为极有可能通不过深度测试。简单地不用深度测试渲染它也不是解决方案,这是因为天空盒会在之后覆盖所有的场景中其他物体。我们需要耍个花招让深度缓冲相信天空盒的深度缓冲有着最大深度值1.0,如此只要有个物体存在深度测试就会失败,看似物体就在它前面了。
|
所以最后渲染天空盒就能够给我们带来轻微的性能提升。采用这种方式,深度缓冲被全部物体的深度值完全填充,所以我们只需要渲染通过前置深度测试的那部分天空的fragment就行了,而且能显著减少片段着色器的调用。问题是天空盒是个1×1×1的立方体,极有可能会渲染失败,因为极有可能通不过深度测试。简单地不用深度测试渲染它也不是解决方案,这是因为天空盒会在之后覆盖所有的场景中其他物体。我们需要耍个花招让深度缓冲相信天空盒的深度缓冲有着最大深度值1.0,如此只要有个物体存在深度测试就会失败,看似物体就在它前面了。
|
||||||
|
|
||||||
在坐标系教程中我们说过,透视除法(perspective division)是在顶点着色器运行之后执行的,把gl_Position的xyz坐标除以w元素。我们从深度测试教程了解到除法结果的z元素等于顶点的深度值。利用这个信息,我们可以把输出位置的z元素设置为它的w元素,这样就会导致z元素等于1.0了,因为,当透视除法应用后,它的z元素转换为w/w = 1.0:
|
在坐标系教程中我们说过,透视除法(perspective division)是在顶点着色器运行之后执行的,把gl_Position的xyz坐标除以w元素。我们从深度测试教程了解到除法结果的z元素等于顶点的深度值。利用这个信息,我们可以把输出位置的z元素设置为它的w元素,这样就会导致z元素等于1.0了,因为,当透视除法应用后,它的z元素转换为w/w = 1.0:
|
||||||
|
|
||||||
@@ -263,7 +263,7 @@ void main()
|
|||||||
|
|
||||||
我们基于观察方向向量I和物体的法线向量N计算出反射向量R。我们可以使用GLSL的内建函数reflect来计算这个反射向量。最后向量R作为一个方向向量对cubemap进行索引/采样,返回一个环境的颜色值。最后的效果看起来就像物体反射了天空盒。
|
我们基于观察方向向量I和物体的法线向量N计算出反射向量R。我们可以使用GLSL的内建函数reflect来计算这个反射向量。最后向量R作为一个方向向量对cubemap进行索引/采样,返回一个环境的颜色值。最后的效果看起来就像物体反射了天空盒。
|
||||||
|
|
||||||
因为我们在场景中已经设置了一个天空盒,创建反射就不难了。我们改变一下箱子使用的那个像素着色器,给箱子一个反射属性:
|
因为我们在场景中已经设置了一个天空盒,创建反射就不难了。我们改变一下箱子使用的那个片段着色器,给箱子一个反射属性:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
#version 330 core
|
#version 330 core
|
||||||
@@ -304,7 +304,7 @@ void main()
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
我们用了法线向量,所以我们打算使用一个法线矩阵(normal matrix)变换它们。Position输出的向量是一个世界空间位置向量。顶点着色器输出的Position用来在像素着色器计算观察方向向量。
|
我们用了法线向量,所以我们打算使用一个法线矩阵(normal matrix)变换它们。Position输出的向量是一个世界空间位置向量。顶点着色器输出的Position用来在片段着色器计算观察方向向量。
|
||||||
|
|
||||||
因为我们使用法线,你还得更新顶点数据,更新属性指针。还要确保设置cameraPos的uniform。
|
因为我们使用法线,你还得更新顶点数据,更新属性指针。还要确保设置cameraPos的uniform。
|
||||||
|
|
||||||
@@ -353,7 +353,7 @@ glBindVertexArray(0);
|
|||||||
|
|
||||||
我们使用这些折射指数来计算光线通过两个材质的比率。在我们的例子中,光线/视线从空气进入玻璃(如果我们假设箱子是玻璃做的)所以比率是1.001.52 = 0.658。
|
我们使用这些折射指数来计算光线通过两个材质的比率。在我们的例子中,光线/视线从空气进入玻璃(如果我们假设箱子是玻璃做的)所以比率是1.001.52 = 0.658。
|
||||||
|
|
||||||
我们已经绑定了cubemap,提供了定点数据,设置了摄像机位置的uniform。现在只需要改变像素着色器:
|
我们已经绑定了cubemap,提供了定点数据,设置了摄像机位置的uniform。现在只需要改变片段着色器:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
void main()
|
void main()
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
### GLSL的内建变量
|
### GLSL的内建变量
|
||||||
|
|
||||||
着色器是很小的,如果我们需要从当前着色器以外的别的资源里的数据,那么我们就不得不穿给它。我们学过了使用顶点属性、uniform和采样器可以实现这个目标。GLSL有几个以gl_为前缀的变量,使我们有一个额外的手段来获取和写入数据。我们已经看到了两个:gl_Position和gl_FragCoord,前一个是顶点着色器的输出向量,后一个是像素着色器的变量。
|
着色器是很小的,如果我们需要从当前着色器以外的别的资源里的数据,那么我们就不得不穿给它。我们学过了使用顶点属性、uniform和采样器可以实现这个目标。GLSL有几个以gl_为前缀的变量,使我们有一个额外的手段来获取和写入数据。我们已经看到了两个:gl_Position和gl_FragCoord,前一个是顶点着色器的输出向量,后一个是片段着色器的变量。
|
||||||
|
|
||||||
我们会讨论几个有趣的GLSL内建变量,并向你解释为什么它们对我们来说很有好处。注意,我们不会讨论到GLSL中所有的内建变量,因此如果你想看到所有的内建变量还是最好去看[OpenGL的wiki](http://www.opengl.org/wiki/Built-in_Variable_(GLSL)。
|
我们会讨论几个有趣的GLSL内建变量,并向你解释为什么它们对我们来说很有好处。注意,我们不会讨论到GLSL中所有的内建变量,因此如果你想看到所有的内建变量还是最好去看[OpenGL的wiki](http://www.opengl.org/wiki/Built-in_Variable_(GLSL)。
|
||||||
|
|
||||||
@@ -52,9 +52,9 @@ gl_VertexID是个整型变量,它储存着我们绘制的当前顶点的ID。
|
|||||||
|
|
||||||
尽管目前看似没用,但是我们最好知道我们能获取这样的信息。
|
尽管目前看似没用,但是我们最好知道我们能获取这样的信息。
|
||||||
|
|
||||||
#### 像素着色器的变量
|
#### 片段着色器的变量
|
||||||
|
|
||||||
在像素着色器中也有一些有趣的变量。GLSL给我们提供了两个有意思的输入变量,它们是gl_FragCoord和gl_FrontFacing。
|
在片段着色器中也有一些有趣的变量。GLSL给我们提供了两个有意思的输入变量,它们是gl_FragCoord和gl_FrontFacing。
|
||||||
|
|
||||||
##### gl_FragCoord
|
##### gl_FragCoord
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ gl_VertexID是个整型变量,它储存着我们绘制的当前顶点的ID。
|
|||||||
|
|
||||||
gl_FragCoord的x和y元素是这个fragment窗口空间坐标(window-space coordinate)。它们的起始处是窗口的左下角。如果我们的窗口是800×600的,那么一个fragment的窗口空间坐标x的范围就在0到800之间,y在0到600之间。
|
gl_FragCoord的x和y元素是这个fragment窗口空间坐标(window-space coordinate)。它们的起始处是窗口的左下角。如果我们的窗口是800×600的,那么一个fragment的窗口空间坐标x的范围就在0到800之间,y在0到600之间。
|
||||||
|
|
||||||
我们可以使用像素着色器基于fragment的窗口坐标计算出一个不同的颜色。gl_FragCoord变量的一个常用的方式是与一个不同的fragment计算出来的视频输出进行对比,通常在技术演示中常见。比如我们可以把屏幕分为两个部分,窗口的左侧渲染一个输出,窗口的右边渲染另一个输出。下面是一个基于fragment的窗口坐标的位置的不同输出不同的颜色的像素着色器:
|
我们可以使用片段着色器基于fragment的窗口坐标计算出一个不同的颜色。gl_FragCoord变量的一个常用的方式是与一个不同的fragment计算出来的视频输出进行对比,通常在技术演示中常见。比如我们可以把屏幕分为两个部分,窗口的左侧渲染一个输出,窗口的右边渲染另一个输出。下面是一个基于fragment的窗口坐标的位置的不同输出不同的颜色的片段着色器:
|
||||||
|
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
@@ -79,13 +79,13 @@ void main()
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
我们现在可以计算出两个完全不同的像素着色器结果,每个显示在窗口的一端。这对于测试不同的光照技术很有好处。
|
我们现在可以计算出两个完全不同的片段着色器结果,每个显示在窗口的一端。这对于测试不同的光照技术很有好处。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
gl_FrontFacing
|
gl_FrontFacing
|
||||||
|
|
||||||
像素着色器另一个有意思的输入变量是gl_FrontFacing变量。在面剔除教程中,我们提到过OpenGL可以根据顶点绘制顺序弄清楚一个面是正面还是背面。如果我们不适用面剔除,那么gl_FrontFacing变量能告诉我们当前fragment是一个正面的一部分还是背面的一部分。然后我们可以决定做一些事情,比如为正面计算出不同的颜色。
|
片段着色器另一个有意思的输入变量是gl_FrontFacing变量。在面剔除教程中,我们提到过OpenGL可以根据顶点绘制顺序弄清楚一个面是正面还是背面。如果我们不适用面剔除,那么gl_FrontFacing变量能告诉我们当前fragment是一个正面的一部分还是背面的一部分。然后我们可以决定做一些事情,比如为正面计算出不同的颜色。
|
||||||
|
|
||||||
gl_FrontFacing变量是一个布尔值,如果fragment是正面的一部分那么就是true,否则就是false。这样我们可以创建一个立方体,里面和外面使用不同的纹理:
|
gl_FrontFacing变量是一个布尔值,如果fragment是正面的一部分那么就是true,否则就是false。这样我们可以创建一个立方体,里面和外面使用不同的纹理:
|
||||||
|
|
||||||
@@ -124,9 +124,9 @@ gl_FragDepth = 0.0f; // This fragment now has a depth value of 0.0f
|
|||||||
|
|
||||||
如果着色器中没有像gl_FragDepth变量写入什么,它就会自动采用gl_FragCoord.z的值。
|
如果着色器中没有像gl_FragDepth变量写入什么,它就会自动采用gl_FragCoord.z的值。
|
||||||
|
|
||||||
我们自己设置深度值有一个显著缺点,因为只要我们在像素着色器中对gl_FragDepth写入什么,OpenGL就会关闭所有的前置深度测试。它被关闭的原因是,在我们运行像素着色器之前OpenGL搞不清像素的深度值,因为像素着色器可能会完全改变这个深度值。
|
我们自己设置深度值有一个显著缺点,因为只要我们在片段着色器中对gl_FragDepth写入什么,OpenGL就会关闭所有的前置深度测试。它被关闭的原因是,在我们运行片段着色器之前OpenGL搞不清像素的深度值,因为片段着色器可能会完全改变这个深度值。
|
||||||
|
|
||||||
你也需要考虑到gl_FragDepth写入所带来的性能的下降。然而从OpenGL4.2起,我们仍然可以对二者进行一定的调和,这需要在像素着色器的顶部使用深度条件(depth condition)来重新声明gl_FragDepth:
|
你也需要考虑到gl_FragDepth写入所带来的性能的下降。然而从OpenGL4.2起,我们仍然可以对二者进行一定的调和,这需要在片段着色器的顶部使用深度条件(depth condition)来重新声明gl_FragDepth:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
layout (depth_<condition>) out float gl_FragDepth;
|
layout (depth_<condition>) out float gl_FragDepth;
|
||||||
@@ -143,7 +143,7 @@ unchanged |如果写入gl_FragDepth, 你就会写gl_FragCoord.z
|
|||||||
|
|
||||||
如果把深度条件定义为greater或less,OpenGL会假定你只写入比当前的像素深度值的深度值大或小的。
|
如果把深度条件定义为greater或less,OpenGL会假定你只写入比当前的像素深度值的深度值大或小的。
|
||||||
|
|
||||||
下面是一个在像素着色器里增加深度值的例子,不过仍可开启前置深度测试:
|
下面是一个在片段着色器里增加深度值的例子,不过仍可开启前置深度测试:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
#version 330 core
|
#version 330 core
|
||||||
@@ -163,7 +163,7 @@ void main()
|
|||||||
|
|
||||||
### Interface blocks(接口块)
|
### Interface blocks(接口块)
|
||||||
|
|
||||||
到目前位置,每次我们打算从顶点向像素着色器发送数据,我们都会声明一个相互匹配的输入/输入变量。从一个着色器向另一个着色器发送数据,一次将它们声明好是最简单的方式,但是随着应用变得越来越大,你也许会打算发送的不仅仅是变量,最好还可以包括数组和结构体。
|
到目前位置,每次我们打算从顶点向片段着色器发送数据,我们都会声明一个相互匹配的输入/输入变量。从一个着色器向另一个着色器发送数据,一次将它们声明好是最简单的方式,但是随着应用变得越来越大,你也许会打算发送的不仅仅是变量,最好还可以包括数组和结构体。
|
||||||
|
|
||||||
为了帮助我们组织这些变量,GLSL为我们提供了一些叫做interface blocks的东西,好让我们能够组织这些变量。声明interface block和声明struct有点像,不同之处是它现在基于块(block),使用in和out关键字来声明,最后它将成为一个输入或输出块(block)。
|
为了帮助我们组织这些变量,GLSL为我们提供了一些叫做interface blocks的东西,好让我们能够组织这些变量。声明interface block和声明struct有点像,不同之处是它现在基于块(block),使用in和out关键字来声明,最后它将成为一个输入或输出块(block)。
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ void main()
|
|||||||
|
|
||||||
这次我们声明一个叫做vs_out的interface block,它把我们需要发送给下个阶段着色器的所有输出变量组合起来。虽然这是一个微不足道的例子,但是你可以想象一下,它的确能够帮助我们组织着色器的输入和输出。当我们希望把着色器的输入和输出组织成数组的时候它就变得很有用,我们会在下节几何着色器(geometry)中见到。
|
这次我们声明一个叫做vs_out的interface block,它把我们需要发送给下个阶段着色器的所有输出变量组合起来。虽然这是一个微不足道的例子,但是你可以想象一下,它的确能够帮助我们组织着色器的输入和输出。当我们希望把着色器的输入和输出组织成数组的时候它就变得很有用,我们会在下节几何着色器(geometry)中见到。
|
||||||
|
|
||||||
然后,我们还需要在下一个着色器——像素着色器中声明一个输入interface block。块名(block name)应该是一样的,但是实例名可以是任意的。
|
然后,我们还需要在下一个着色器——片段着色器中声明一个输入interface block。块名(block name)应该是一样的,但是实例名可以是任意的。
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
#version 330 core
|
#version 330 core
|
||||||
@@ -388,7 +388,7 @@ void main()
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
这儿没什么特别的,除了我们现在使用了一个带有std140布局的uniform block。我们在例程中将显示4个立方体,每个立方体都使用一个不同的着色器程序。4个着色器程序使用同样的顶点着色器,但是它们将使用各自的像素着色器,每个像素着色器输出一个单色。
|
这儿没什么特别的,除了我们现在使用了一个带有std140布局的uniform block。我们在例程中将显示4个立方体,每个立方体都使用一个不同的着色器程序。4个着色器程序使用同样的顶点着色器,但是它们将使用各自的片段着色器,每个片段着色器输出一个单色。
|
||||||
|
|
||||||
首先,我们把顶点着色器的uniform block设置为绑定点0。注意,我们必须为每个着色器做这件事。
|
首先,我们把顶点着色器的uniform block设置为绑定点0。注意,我们必须为每个着色器做这件事。
|
||||||
|
|
||||||
@@ -457,7 +457,7 @@ glBindVertexArray(0);
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
通过改变模型矩阵,每个立方体都移动到窗口的一边,由于像素着色器不同,物体的颜色也不同。这是一个相对简单的场景,我们可以使用uniform缓冲对象,但是任何大型渲染程序有成百上千的活动着色程序;彼时uniform缓冲对象就会闪闪发光了。
|
通过改变模型矩阵,每个立方体都移动到窗口的一边,由于片段着色器不同,物体的颜色也不同。这是一个相对简单的场景,我们可以使用uniform缓冲对象,但是任何大型渲染程序有成百上千的活动着色程序;彼时uniform缓冲对象就会闪闪发光了。
|
||||||
|
|
||||||
你可以[在这里获得例程的完整源码](http://www.learnopengl.com/code_viewer.php?code=advanced/advanced_glsl_uniform_buffer_objects)。
|
你可以[在这里获得例程的完整源码](http://www.learnopengl.com/code_viewer.php?code=advanced/advanced_glsl_uniform_buffer_objects)。
|
||||||
|
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
本文作者JoeyDeVries,由Django翻译自http://learnopengl.com
|
本文作者JoeyDeVries,由Django翻译自http://learnopengl.com
|
||||||
|
|
||||||
在顶点和像素着色器张志坚有一个可选的着色器阶段,叫做几何着色器(geometry shader)。几何着色器以一个或多个表示为一个单独基本图形(primitive)的顶点作为输入,比如可以是一个点或者三角形。几何着色器在将这些顶点发送到下一个着色阶段之前,可以将这些顶点转变为它认为合适的内容。几何着色器有意思的地方在于它可以把(一个或多个)顶点转变为完全不同的基本图形(primitive),从而生成比原来多得多的顶点。
|
在顶点和片段着色器张志坚有一个可选的着色器阶段,叫做几何着色器(geometry shader)。几何着色器以一个或多个表示为一个单独基本图形(primitive)的顶点作为输入,比如可以是一个点或者三角形。几何着色器在将这些顶点发送到下一个着色阶段之前,可以将这些顶点转变为它认为合适的内容。几何着色器有意思的地方在于它可以把(一个或多个)顶点转变为完全不同的基本图形(primitive),从而生成比原来多得多的顶点。
|
||||||
|
|
||||||
我们直接用一个例子深入了解一下:
|
我们直接用一个例子深入了解一下:
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ void main()
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
我们会简单地为所有点输出绿色,我们直接在像素着色器里进行硬编码:
|
我们会简单地为所有点输出绿色,我们直接在片段着色器里进行硬编码:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
#version 330 core
|
#version 330 core
|
||||||
@@ -163,7 +163,7 @@ void main() {
|
|||||||
|
|
||||||
现在这个几何着色器应该很容易理解了。它简单地将它接收到的输入的无修改的顶点位置发射出去,然后生成一个point基本图形。
|
现在这个几何着色器应该很容易理解了。它简单地将它接收到的输入的无修改的顶点位置发射出去,然后生成一个point基本图形。
|
||||||
|
|
||||||
一个几何着色器需要像顶点和像素着色器一样被编译和链接,但是这次我们将使用GL_GEOMETRY_SHADER作为着色器的类型来创建这个着色器:
|
一个几何着色器需要像顶点和片段着色器一样被编译和链接,但是这次我们将使用GL_GEOMETRY_SHADER作为着色器的类型来创建这个着色器:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
geometryShader = glCreateShader(GL_GEOMETRY_SHADER);
|
geometryShader = glCreateShader(GL_GEOMETRY_SHADER);
|
||||||
@@ -174,7 +174,7 @@ glAttachShader(program, geometryShader);
|
|||||||
glLinkProgram(program);
|
glLinkProgram(program);
|
||||||
```
|
```
|
||||||
|
|
||||||
编译着色器的代码和顶点、像素着色器的基本一样。要记得检查编译和链接错误!
|
编译着色器的代码和顶点、片段着色器的基本一样。要记得检查编译和链接错误!
|
||||||
|
|
||||||
如果你现在编译和运行,就会看到和下面相似的结果:
|
如果你现在编译和运行,就会看到和下面相似的结果:
|
||||||
|
|
||||||
@@ -225,7 +225,7 @@ void main()
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
这个几何着色器生成5个顶点,每个顶点是点(point)的位置加上一个偏移量,来组成一个大triangle strip。接着最后的基本图形被像素化,像素着色器处理整个triangle strip,结果是为我们绘制的每个点生成一个绿房子:
|
这个几何着色器生成5个顶点,每个顶点是点(point)的位置加上一个偏移量,来组成一个大triangle strip。接着最后的基本图形被像素化,片段着色器处理整个triangle strip,结果是为我们绘制的每个点生成一个绿房子:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -285,7 +285,7 @@ in VS_OUT {
|
|||||||
out vec3 fColor;
|
out vec3 fColor;
|
||||||
```
|
```
|
||||||
|
|
||||||
因为像素着色器只需要一个(已进行了插值的)颜色,传送多个颜色没有意义。fColor向量这样就不是一个数组,而是一个单一的向量。当发射一个顶点时,为了它的像素着色器运行,每个顶点都会储存最后在fColor中储存的值。对于这些房子来说,我们可以在第一个顶点被发射,对整个房子上色前,只使用来自顶点着色器的颜色填充fColor一次:
|
因为片段着色器只需要一个(已进行了插值的)颜色,传送多个颜色没有意义。fColor向量这样就不是一个数组,而是一个单一的向量。当发射一个顶点时,为了它的片段着色器运行,每个顶点都会储存最后在fColor中储存的值。对于这些房子来说,我们可以在第一个顶点被发射,对整个房子上色前,只使用来自顶点着色器的颜色填充fColor一次:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
fColor = gs_in[0].color; // gs_in[0] since there's only one input vertex
|
fColor = gs_in[0].color; // gs_in[0] since there's only one input vertex
|
||||||
@@ -482,7 +482,7 @@ void main()
|
|||||||
|
|
||||||
到现在为止,像这样的几何着色器的内容就不言自明了。需要注意的是我们我们把法线向量乘以一个MAGNITUDE向量来限制显示出的法线向量的大小(否则它们就太大了)。
|
到现在为止,像这样的几何着色器的内容就不言自明了。需要注意的是我们我们把法线向量乘以一个MAGNITUDE向量来限制显示出的法线向量的大小(否则它们就太大了)。
|
||||||
|
|
||||||
由于把法线显示出来通常用于调试的目的,我们可以在像素着色器的帮助下把它们显示为单色的线(如果你愿意也可以更炫一点)。
|
由于把法线显示出来通常用于调试的目的,我们可以在片段着色器的帮助下把它们显示为单色的线(如果你愿意也可以更炫一点)。
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
#version 330 core
|
#version 330 core
|
||||||
|
@@ -44,7 +44,7 @@ GLfloat quadVertices[] = {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
像素着色器接收从顶点着色器发送来的颜色向量,设置为它的颜色输出,从而为四边形上色:
|
片段着色器接收从顶点着色器发送来的颜色向量,设置为它的颜色输出,从而为四边形上色:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
#version 330 core
|
#version 330 core
|
||||||
|
@@ -20,11 +20,11 @@
|
|||||||
|
|
||||||
为了理解什么是多采样,以及它是如何解决走样的问题的,我们先要更深入了解一个OpenGL像素器的工作方式。
|
为了理解什么是多采样,以及它是如何解决走样的问题的,我们先要更深入了解一个OpenGL像素器的工作方式。
|
||||||
|
|
||||||
像素器是是你的最终的经处理的顶点和像素着色器之间的所有算法和处理的集合。像素器将属于一个基本图形的所有顶点转化为一系列fragment。顶点坐标理论上可以含有任何坐标,但fragment却不是这样,这是因为它们与你的窗口的解析度有关。几乎永远都不会有顶点坐标和fragment的一对一映射,所以像素器必须以某种方式决定每个特定顶点最终结束于哪个fragment/屏幕坐标上。
|
像素器是是你的最终的经处理的顶点和片段着色器之间的所有算法和处理的集合。像素器将属于一个基本图形的所有顶点转化为一系列fragment。顶点坐标理论上可以含有任何坐标,但fragment却不是这样,这是因为它们与你的窗口的解析度有关。几乎永远都不会有顶点坐标和fragment的一对一映射,所以像素器必须以某种方式决定每个特定顶点最终结束于哪个fragment/屏幕坐标上。
|
||||||
|
|
||||||

|

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

|

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

|

|
||||||
|
|
||||||
这里每个像素包含着4个子样本(不相关的已被隐藏)蓝色的子样本是被三角形覆盖了的,灰色的没有被覆盖。三角形内部区域中的所有像素都会运行一次像素着色器,它输出的颜色被储存到所有4个子样本中。三角形的边缘并不是所有的子样本都会被覆盖,所以像素着色器的结果仅储存在部分子样本中。根据被覆盖子样本的数量,最终的像素颜色由三角形颜色和其他子样本所储存的颜色所决定。
|
这里每个像素包含着4个子样本(不相关的已被隐藏)蓝色的子样本是被三角形覆盖了的,灰色的没有被覆盖。三角形内部区域中的所有像素都会运行一次片段着色器,它输出的颜色被储存到所有4个子样本中。三角形的边缘并不是所有的子样本都会被覆盖,所以片段着色器的结果仅储存在部分子样本中。根据被覆盖子样本的数量,最终的像素颜色由三角形颜色和其他子样本所储存的颜色所决定。
|
||||||
|
|
||||||
大致上来说,如果更多的采样点被覆盖,那么像素的颜色就会更接近于三角形。如果我们用早期使用的三角形的颜色填充像素,我们会获得这样的结果:
|
大致上来说,如果更多的采样点被覆盖,那么像素的颜色就会更接近于三角形。如果我们用早期使用的三角形的颜色填充像素,我们会获得这样的结果:
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT,
|
|||||||
|
|
||||||
你可以[在这里找到源代码](http://learnopengl.com/code_viewer.php?code=advanced/anti_aliasing_framebuffers)。
|
你可以[在这里找到源代码](http://learnopengl.com/code_viewer.php?code=advanced/anti_aliasing_framebuffers)。
|
||||||
|
|
||||||
但是如果我们打算使用一个多采样帧缓冲的纹理结果来做这件事,就像后处理一样会怎样?我们不能在像素着色器中直接使用多采样纹理。我们可以做的事情是把多缓冲位块传送(Blit)到另一个带有非多采样纹理附件的FBO中。之后我们使用这个普通的颜色附件纹理进行后处理,通过多采样来对一个图像渲染进行后处理效率很高。这意味着我们必须生成一个新的FBO,它仅作为一个将多采样缓冲还原为一个我们可以在像素着色器中使用的普通2D纹理中介。伪代码是这样的:
|
但是如果我们打算使用一个多采样帧缓冲的纹理结果来做这件事,就像后处理一样会怎样?我们不能在片段着色器中直接使用多采样纹理。我们可以做的事情是把多缓冲位块传送(Blit)到另一个带有非多采样纹理附件的FBO中。之后我们使用这个普通的颜色附件纹理进行后处理,通过多采样来对一个图像渲染进行后处理效率很高。这意味着我们必须生成一个新的FBO,它仅作为一个将多采样缓冲还原为一个我们可以在片段着色器中使用的普通2D纹理中介。伪代码是这样的:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
GLuint msFBO = CreateFBOWithMultiSampledAttachments();
|
GLuint msFBO = CreateFBOWithMultiSampledAttachments();
|
||||||
|
Reference in New Issue
Block a user