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

Irregular update

This commit is contained in:
Meow J
2016-07-26 22:41:19 +08:00
parent 6553fce46e
commit 628478b528
38 changed files with 300 additions and 295 deletions

View File

@@ -16,7 +16,7 @@ learnopengl.com系列教程的中文翻译目前正在翻译中。
目前Host在GitHub上可以[点击这里](https://learnopengl-cn.github.io/)进行阅读。
旧版本的Host在Read The Docs内可能会放弃更新),可以[点击这里](http://learnopengl-cn.readthedocs.io/)进行阅读。
旧版本的Host在Read The Docs内不定时更新),可以[点击这里](http://learnopengl-cn.readthedocs.io/)进行阅读。
## 认领翻译

View File

@@ -7,7 +7,7 @@
校对 | Geequlim
在OpenGL中任何事物都在3D空间中而屏幕和窗口却是2D像素数组这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的<def>图形渲染管线</def>(Graphics Pipeline大多译为管线实际上指的是一堆原始图形数据途经一个输送管道期间经过各种变化处理最终出现在屏幕的过程)管理的。图形渲染管线可以被划分为两个主要部分第一部分把你的3D坐标转换为2D坐标第二部分是把2D坐标转变为实际的有颜色的像素。这个教程里我们会简单地讨论一下图形渲染管线以及如何利用它创建一些漂亮的像素。
在OpenGL中任何事物都在3D空间中而屏幕和窗口却是2D像素数组这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的<def>图形渲染管线</def>Graphics Pipeline大多译为管线实际上指的是一堆原始图形数据途经一个输送管道期间经过各种变化处理最终出现在屏幕的过程管理的。图形渲染管线可以被划分为两个主要部分第一部分把你的3D坐标转换为2D坐标第二部分是把2D坐标转变为实际的有颜色的像素。这个教程里我们会简单地讨论一下图形渲染管线以及如何利用它创建一些漂亮的像素。
!!! Important

View File

@@ -6,15 +6,15 @@
翻译 | [Django](http://bullteacher.com/)
校对 | Geequlim
在[Hello Triangle](http://learnopengl-cn.readthedocs.org/zh/latest/01%20Getting%20started/04%20Hello%20Triangle/)教程中提到,着色器(Shader)是运行在GPU上的小程序。这些小程序为图形渲染管线的个特定部分而运行。从基本意义上来说,着色器不是别的,只是一种把输入转化为输出的程序。着色器也是一种相当独立的程序,它们不能相互通信;只能通过输入和输出的方式来进行沟通
在[Hello Triangle](04 Hello Triangle.md)教程中提到,着色器(Shader)是运行在GPU上的小程序。这些小程序为图形渲染管线的个特定部分而运行。从基本意义上来说,着色器只是一种把输入转化为输出的程序。着色器也是一种非常独立的程序,因为它们之间不能相互通信;它们之间唯一的沟通只有通过输入和输出
前面的教程里我们简要地触及了一点着色器的皮毛了解了如何恰当地使用它们。现在我们会用一种更加通用的方式详细解释着色器特别是OpenGL着色器语言。
前面的教程里我们简要地触及了一点着色器的皮毛,并了解了如何恰当地使用它们。现在我们会用一种更加广泛的形式详细解释着色器特别是OpenGL着色器语言(GLSL)
# GLSL
着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的它包含针对向量和矩阵操作的有用特性。
着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的它包含一些针对向量和矩阵操作的有用特性。
着色器的开头总是要声明版本接着是输入和输出变量、uniform和`main`函数。每个着色器的入口都是`main`函数,在这我们处理所有输入变量,用输出变量输出结果。如果你不知道什么是uniform也不用担心我们后面会进行讲解。
着色器的开头总是要声明版本接着是输入和输出变量、uniform和<fun>main</fun>函数。每个着色器的入口都是<fun>main</fun>函数,在这个函数中我们处理所有输入变量,并将结果输出到输出变量中。如果你不知道什么是uniform也不用担心我们后面会进行讲解。
一个典型的着色器有下面的结构:
@@ -30,14 +30,14 @@ uniform type uniform_name;
int main()
{
// 处理输入
// 处理输入并进行一些图形操作
...
// 输出
// 输出处理过的结果到输出变量
out_variable_name = weird_stuff_we_processed;
}
```
当我们谈论特别是谈到顶点着色器的时候,每个输入变量也叫顶点属性(Vertex Attribute)。能声明多少个顶点属性是由硬件决定。OpenGL确保至少有16个包含4个元素的顶点属性可用,但是有些硬件或许可用更多,你可以查询`GL_MAX_VERTEX_ATTRIB`S来获取这个数目。
当我们特别谈论到顶点着色器的时候,每个输入变量也叫<def>顶点属性</def>(Vertex Attribute)。我们能声明顶点属性是有上限的,它一般由硬件决定。OpenGL确保至少有16个包含4分量的顶点属性可用,但是有些硬件或许允许更多的顶点属性,你可以查询<var>GL_MAX_VERTEX_ATTRIBS</var>来获取具体的上限:
```c++
GLint nrAttributes;
@@ -45,29 +45,29 @@ glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
```
通常情况下它会返回至少16个大部分情况下是够用了。
通常情况下它至少会返回16个大部分情况下是够用了。
## 数据类型
GLSL有像其他编程语言相似的数据类型。GLSL有C风格的默认基础数据类型:`int`、`float`、`double`、`uint`和`bool`。GLSL也有两种容器类型教程中我们会使用很多,它们是向量(Vector)和矩阵(Matrix),其中矩阵我们会在之后的教程里再讨论。
和其他编程语言一样GLSL有数据类型可以来指定变量的种类。GLSL中包含C等其它语言大部分的默认基础数据类型:`int`、`float`、`double`、`uint`和`bool`。GLSL也有两种容器类型它们会在这个教程中使用很多,分别是向量(Vector)和矩阵(Matrix),其中矩阵我们会在之后的教程里再讨论。
### 向量
GLSL中的向量(Vector)可以包含有1、2、3或者4个分量分量类型可以是前面默认基础类型的任意一个。它们可以是下面的形式(n代表元素数量)
GLSL中的向量是一个可以包含有1、2、3或者4个分量的容器,分量类型可以是前面默认基础类型的任意一个。它们可以是下面的形式`n`代表分量的数量
类型|含义
---|---
vecn | 包含n个默认为float元素的向量
bvecn| 包含n个布尔元素向量
ivecn| 包含n个int元素的向量
uvecn| 包含n个unsigned int元素的向量
dvecn| 包含n个double元素的向量
`vecn` | 包含`n`个float分量的默认向量
`bvecn`| 包含`n`个bool分量的向量
`ivecn`| 包含`n`个int分量的向量
`uvecn`| 包含`n`个unsigned int分量的向量
`dvecn`| 包含`n`个double分量的向量
大多数时候我们使用vecn因为float足够满足大多数要求。
大多数时候我们使用`vecn`因为float足够满足大多数要求
一个向量的元素可以通过`vec.x`这种方式获取,这里`x`是指这个向量的第一个元素。你可以分别使用`.x`、`.y`、`.z`和`.w`来获取它们的第1、2、3、4号元素。GLSL也允许你使用**rgba**来获取颜色的元素,或是**stpq**获取纹理坐标元素
一个向量的分量可以通过`vec.x`这种方式获取,这里`x`是指这个向量的第一个分量。你可以分别使用`.x`、`.y`、`.z`和`.w`来获取它们的第1、2、3、4个分量。GLSL也允许你对颜色使用`rgba`,或是对纹理坐标使用`stpq`访问相同的分量
向量数据类型也允许一些有趣而灵活的元素选择方式,叫做重组(Swizzling)。重组允许这样的语法:
向量这一数据类型也允许一些有趣而灵活的分量选择方式,叫做<def>重组</def>(Swizzling)。重组允许这样的语法:
```c++
vec2 someVec;
@@ -76,7 +76,7 @@ vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
```
你可以使用上面任何4个字母组合来创建一个新的和原来向量一样长的向量(但4个元素需要是同一种类型)不允许在一个vec2向量中去获取.z元素。我们可以把一个向量作为一个参数传给不同的向量构造函数,以减少参数需求的数量:
你可以使用上面4个字母任意组合来创建一个和原来向量一样长的(同类型)新向量,只要原来向量有那些分量即可;然而,你不允许在一个`vec2`向量中去获取`.z`元素。我们可以把一个向量作为一个参数传给不同的向量构造函数,以减少需求参数的数量:
```c++
vec2 vect = vec2(0.5f, 0.7f);
@@ -84,41 +84,41 @@ vec4 result = vec4(vect, 0.0f, 0.0f);
vec4 otherResult = vec4(result.xyz, 1.0f);
```
向量是一种灵活的数据类型,我们可以把用在所有输入和输出上。学完教程你会看到很多如何创造性地管理向量的例子。
向量是一种灵活的数据类型,我们可以把用在各种输入和输出上。学完教程你会看到很多新颖的管理向量的例子。
## 输入与输出
着色器是各自独立的小程序,但是它们都是一个整体的局部出于这样的原因我们希望每个着色器都有输入和输出这样才能进行数据交流和传递。GLSL定义了`in`和`out`关键字来实现这个目的。每个着色器使用这关键字定输入和输出,无论在哪儿,一个输出变量就能与一个下一个阶段的输入变量相匹配。他们在顶点和片段着色器之间有点不同。
虽然着色器是各自独立的小程序,但是它们都是一个整体的一部分出于这样的原因我们希望每个着色器都有输入和输出这样才能进行数据交流和传递。GLSL定义了`in`和`out`关键字专门来实现这个目的。每个着色器使用这两个关键字定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。但在顶点和片段着色器中会有点不同。
顶点着色器应该接收的输入是一种特形式,否则就会效率低下。顶点着色器的输入特殊,它所接受的是从顶点数据直接输入。为了定义顶点数据如何组织,我们使用`location`元数据指定输入变量这样我们才可以在CPU上配置顶点属性。我们已经在前面的教程看过`layout (location = 0)`。顶点着色器需要为它的输入提供一个额外的`layout`定义,这样我们才能把它链接到顶点数据。
顶点着色器应该接收的是一种特形式的输入,否则就会效率低下。顶点着色器的输入特殊,它从顶点数据直接接收输入。为了定义顶点数据如何管理,我们使用`location`这一元数据指定输入变量这样我们才可以在CPU上配置顶点属性。我们已经在前面的教程看过这个了,`layout (location = 0)`。顶点着色器需要为它的输入提供一个额外的`layout`标识,这样我们才能把它链接到顶点数据。
!!! Important
也可以移除`layout (location = 0)`通过在OpenGL代码中使用`glGetAttribLocation`请求属性地址(Location),但是我更喜欢在着色器中设置它们,理解容易而且节省时间
也可以忽略`layout (location = 0)`标识符通过在OpenGL代码中使用<fun>glGetAttribLocation</fun>查询属性位置值(Location),但是我更喜欢在着色器中设置它们,这样会更容易理解而且节省你和OpenGL的工作量
另一个例外是片段着色器需要一个`vec4`颜色输出变量因为片段着色器需要生成一个最终输出的颜色。如果你在片段着色器没有定义输出颜色OpenGL会把你的物体渲染为黑色(或白色)
另一个例外是片段着色器,它需要一个`vec4`颜色输出变量因为片段着色器需要生成一个最终输出的颜色。如果你在片段着色器没有定义输出颜色OpenGL会把你的物体渲染为黑色或白色
所以,如果我们打算从一个着色器向另一个着色器发送数据,我们必须**在发送方着色器中声明一个输出,在接收方着色器中声明一个同名输入**。当名字和类型都一样的时候OpenGL就会把两个变量链接到一起它们之间就能发送数据了(这是在链接程序(Program)对象时完成的)。为了展示这是这么工作的,我们会改变前面教程里的那个着色器,让顶点着色器为片段着色器决定颜色。
所以,如果我们打算从一个着色器向另一个着色器发送数据,我们必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。当类型和名字都一样的时候OpenGL就会把两个变量链接到一起它们之间就能发送数据了这是在链接程序对象时完成的。为了展示这是如何工作的,我们会稍微改动一下之前教程里的那个着色器,让顶点着色器为片段着色器决定颜色。
**顶点着色器**
```c++
#version 330 core
layout (location = 0) in vec3 position; // 位置变量的属性为0
layout (location = 0) in vec3 position; // position变量的属性位置值为0
out vec4 vertexColor; // 为片段着色器指定一个颜色输出
void main()
{
gl_Position = vec4(position, 1.0); // 把一个vec3作为vec4的构造器的参数
vertexColor = vec4(0.5f, 0.0f, 0.0f, 1.0f); // 把输出颜色设置为暗红色
gl_Position = vec4(position, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数
vertexColor = vec4(0.5f, 0.0f, 0.0f, 1.0f); // 把输出变量设置为暗红色
}
```
**片段着色器**
```c++
#version 330 core
in vec4 vertexColor; // 顶点着色器的vertexColor变量类型相同、名称相同
in vec4 vertexColor; // 顶点着色器传来的输入变量(名称相同、类型相同
out vec4 color; // 片段着色器输出的变量名可以任意命名类型必须是vec4
@@ -128,23 +128,23 @@ void main()
}
```
你可以看到我们在顶点着色器中声明了一个`vertexColor`变量作为`vec4`输出,在片段着色器声明了一个一样的`vertexColor`。由于它们**类型相同并且名字也相同**,片段着色器中的`vertexColor`就和顶点着色器中的`vertexColor`链接了。因为我们在顶点着色器中设置的颜色是深红色的,片段着色器输出的结果也是深红色的。下面的图片展示了输出结果:
你可以看到我们在顶点着色器中声明了一个<var>vertexColor</var>变量作为`vec4`输出,在片段着色器声明了一个类似的<var>vertexColor</var>。由于它们名字相同且类型相同,片段着色器中的<var>vertexColor</var>就和顶点着色器中的<var>vertexColor</var>链接了。由于我们在顶点着色器中将颜色设置为深红色,最终的片段也是深红色的。下面的图片展示了输出结果:
![](http://learnopengl.com/img/getting-started/shaders.png)
![](../img/01/05/shaders.png)
我们完成了从顶点着色器向片段着色器发送数据。让我们更上一层楼,看看能否从应用程序中直接给片段着色器发送一个颜色!
完成了!我们成功地从顶点着色器向片段着色器发送数据。让我们更上一层楼,看看能否从应用程序中直接给片段着色器发送一个颜色!
## Uniform
uniform是另一种从CPU应用向GPU着色器发送数据的方式但uniform和顶点属性有不同。首先uniform是**全局的(Global)**。这里全局的意思是uniform变量必须在所有着色器程序对象中都是独一无二的,它可以着色器程序的任着色器任何阶段使用。第二无论你把uniform值设置成什么uniform会一直保存它们的数据直到它们被重置或更新。
<def>Uniform</def>是一种从CPU中的应用向GPU中的着色器发送数据的方式但uniform和顶点属性有不同。首先uniform是<def>全局的</def>(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以着色器程序的任着色器在任意阶段访问。第二无论你把uniform值设置成什么uniform会一直保存它们的数据直到它们被重置或更新。
我们可以简单地通过在片段着色器中设置uniform关键字类型和变量名来声明一个GLSL的uniform。之后,我们可以在着色器中使用新声明的uniform了。我们来看看这次是否能通过uniform设置三角形的颜色
我们可以在一个着色器中添加`uniform`关键字类型和变量名来声明一个GLSL的uniform。从此处开始我们可以在着色器中使用新声明的uniform了。我们来看看这次是否能通过uniform设置三角形的颜色
```c++
#version 330 core
out vec4 color;
uniform vec4 ourColor; //程序代码中设
uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量
void main()
{
@@ -152,13 +152,13 @@ void main()
}
```
我们在片段着色器中声明了一个uniform vec4的`ourColor`并把片段着色器的输出颜色设置为uniform值。因为uniform是全局变量我们我们可以在任何着色器中定义它们而无需通过顶点着色器作为中介。顶点着色器中不需要这个uniform所以不用在那里定义它。
我们在片段着色器中声明了一个uniform `vec4`的<var>ourColor</var>并把片段着色器的输出颜色设置为uniform值的内容。因为uniform是全局变量我们我们可以在任何着色器中定义它们而无需通过顶点着色器作为中介。顶点着色器中不需要这个uniform所以我们不用在那里定义它。
!!! Attention
如果你声明了一个uniform却在GLSL代码中没用过编译器会静默移除这个变量从而最后编译出的版本中并不会包含它,如果有一个从没用过的uniform出现在已编译版本中会出现几个错误,记住这点!
如果你声明了一个uniform却在GLSL代码中没用过编译器会静默移除这个变量导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误,记住这点!
uniform现在还是空的我们没有给它添加任何数据所以下面就做这件事。我们首先需要找到着色器中uniform的索引/地址。当我们得到uniform的索引/地址后,我们就可以更新它的值了。这我们不去给像素传递一个颜色,而是随着时间让它改变颜色:
这个uniform现在还是空的我们没有给它添加任何数据,所以下面我们就做这件事。我们首先需要找到着色器中uniform属性的索引/位置值。当我们得到uniform的索引/位置值后,我们就可以更新它的值了。这我们不去给像素传递单独一个颜色,而是让它随着时间改变颜色:
```c++
GLfloat timeValue = glfwGetTime();
@@ -168,30 +168,30 @@ glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
```
首先我们通过`glfwGetTime()`获取运行的秒数。然后我们使用余弦函数在0.0到-1.0之间改变颜色,最后储存到`greenValue`里。
首先我们通过<fun>glfwGetTime()</fun>获取运行的秒数。然后我们使用<fun>sin</fun>函数让颜色在0.0到1.0之间改变,最后将结果储存到<var>greenValue</var>里。
接着,我们用`glGetUniformLocation`请求`uniform ourColor`的地址。我们为请求函数提供着色器程序和uniform的名字(这是我们希望获得的地址的来源)。如果`glGetUniformLocation`返回`-1`就代表没有找到这个地址。最后,我们可以通过`glUniform4f`函数设置uniform值。注意查询uniform地址不需要在之前使用着色器程序但是更新一个unform之前**必须**使用程序(调用`glUseProgram`)因为它是在当前激活的着色器程序中设置unform的。
接着,我们用<fun>glGetUniformLocation</fun>查询uniform `ourColor`的位置值。我们为查询函数提供着色器程序和uniform的名字这是我们希望获得的位置值的来源。如果<fun>glGetUniformLocation</fun>返回`-1`就代表没有找到这个位置值。最后,我们可以通过<fun>glUniform4f</fun>函数设置uniform值。注意查询uniform地址不要求你之前使用着色器程序但是更新一个unform之前**必须**使用程序调用<fun>glUseProgram</fun>)因为它是在当前激活的着色器程序中设置unform的。
!!! Important
因为OpenGL是C库内核,所以它不支持函数重载在函数参数不同的时候就要定义新的函数glUniform是一个典型例子。这个函数有一个特定的作为类型的后缀。有几种可用的后缀:
因为OpenGL在其核心是一个C库,所以它不支持类型重载,在函数参数不同的时候就要为其定义新的函数;<fun>glUniform</fun>是一个典型例子。这个函数有一个特定的后缀标识设定的uniform的类型。可能的后缀
后缀|含义
---|--
f | 函数需要一个float作为它的值
i | 函数需要一个int作为它的值
ui| 函数需要一个unsigned int作为它的值
3f| 函数需要3个float作为它的值
fv| 函数需要一个float向量/数组作为它的值
`f` | 函数需要一个float作为它的值
`i` | 函数需要一个int作为它的值
`ui`| 函数需要一个unsigned int作为它的值
`3f`| 函数需要3个float作为它的值
`fv`| 函数需要一个float向量/数组作为它的值
每当你打算配置一个OpenGL的选项时就可以简单地根据这些规则选择适合你的数据类型的重载函数。在我们的例子里,我们使用uniform的4float,所以我们通过`glUniform4f`传递我们的数据(注意,我们也可以使用fv版本)。
每当你打算配置一个OpenGL的选项时就可以简单地根据这些规则选择适合你的数据类型的重载函数。在我们的例子里我们希望分别设定uniform的4float,所以我们通过<fun>glUniform4f</fun>传递我们的数据(注意,我们也可以使用`fv`版本)。
现在你知道如何设置uniform变量的值了我们可以使用它们来渲染了。如果我们打算让颜色慢慢变化我们就要在游戏循环的每一更新这个uniform否则三角形就不会改变颜色。下面我们就计算greenValue然后每个渲染迭代都更新这个uniform
现在你知道如何设置uniform变量的值了我们可以使用它们来渲染了。如果我们打算让颜色慢慢变化我们就要在游戏循环的每一次迭代中(所以他会逐帧改变)更新这个uniform否则三角形就不会改变颜色。下面我们就计算<var>greenValue</var>然后每个渲染迭代都更新这个uniform
```c++
while(!glfwWindowShouldClose(window))
{
// 检测事件
// 检测并调用事件
glfwPollEvents();
// 渲染
@@ -199,7 +199,7 @@ while(!glfwWindowShouldClose(window))
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 激活着色器
// 记得激活着色器
glUseProgram(shaderProgram);
// 更新uniform颜色
@@ -215,49 +215,50 @@ while(!glfwWindowShouldClose(window))
}
```
新代码和上一节的很相似。这次,我们在每个循环绘制三角形前先更新uniform值。如果你成功更新uniform,你会看到你的三角形逐渐由绿变黑再变绿
这里的代码对之前代码是一次非常直接的修改。这次,我们在每次迭代绘制三角形前先更新uniform值。如果你正确更新uniform你会看到你的三角形逐渐由绿变黑再变回绿色
<video src="http://learnopengl.com/video/getting-started/shaders.mp4" controls="controls"/></video>
<video src="../../img/01/05/shaders.mp4" controls="controls"/></video>
如果你在哪儿卡住了,[这里有源码](http://www.learnopengl.com/code_viewer.php?code=getting-started/shaders-uniform)。
如果你在哪儿卡住了,可以到[这里](http://www.learnopengl.com/code_viewer.php?code=getting-started/shaders-uniform)查看源码
就像你所看到的那样uniform是个设置属性的很有用的工具它可以在渲染循环中改变也可以在你的应用和着色器之间进行数据交互但假如我们打算为每个顶点设置一个颜色的时候该怎么办这种情况下我们就不得不声明和顶点数目一样多的uniform了。在顶点属性问题上一个更好的解决方案一定要能包含足够多的数据,这是我们接下来要讲的内容
可以看到uniform对于设置一个在渲染迭代中会改变的属性是一个非常有用的工具它也是一个在程序和着色器间数据交互的很好工具但假如我们打算为每个顶点设置一个颜色的时候该怎么办这种情况下我们就不得不声明和顶点数目一样多的uniform了。在这一问题上更好的解决方案是在顶点属性中包含更多的数据,这是我们接下来要做的事情
## 更多属性
## 更多属性
前面的教程中我们了解了如何填充VBO、配置顶点属性指针以及如何把它们都储存到VAO里。这次我们同样打算把颜色数据加进顶点数据中。我们将把颜色数据表示为3个float的**顶点数组(Vertex Array)**。我们三角形的个角分别指定为红色、绿色和蓝色:
前面的教程中我们了解了如何填充VBO、配置顶点属性指针以及如何把它们都储存到一个VAO里。这次我们同样打算把颜色数据加进顶点数据中。我们将把颜色数据添加为3个float值至<var>vertices</var>数组。我们将把三角形的个角分别指定为红色、绿色和蓝色:
```c++
GLfloat vertices[] = {
// 位置 // 颜色
// 位置 // 颜色
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下
0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // 顶部
};
```
由于我们现在发送到顶点着色器的数据更多了,有必要调整顶点着色器,使它能够颜色值作为一个顶点属性输入。需要注意的是我们用`layout`标识符来吧`color`属性的`location`设置为1
由于现在有更多的数据要发送到顶点着色器,我们有必要调整一下顶点着色器,使它能够接收颜色值作为一个顶点属性输入。需要注意的是我们用`layout`标识符来把<var>color</var>属性的位置值设置为1
```c++
#version 330 core
layout (location = 0) in vec3 position; // 位置变量的属性position为 0
layout (location = 1) in vec3 color; // 颜色变量的属性position为 1
layout (location = 0) in vec3 position; // 位置变量的属性位置值为 0
layout (location = 1) in vec3 color; // 颜色变量的属性位置值为 1
out vec3 ourColor; // 向片段着色器输出一个颜色
void main()
{
gl_Position = vec4(position, 1.0);
ourColor = color; // ourColor设置为我们从顶点数据那里得到的输入颜色
ourColor = color; // ourColor设置为我们从顶点数据那里得到的输入颜色
}
```
由于我们不再使用uniform来传递片段的颜色了现在使用`ourColor`输出变量要求必须也去改变片段着色器:
由于我们不再使用uniform来传递片段的颜色了现在使用`ourColor`输出变量,我们必须再修改一下片段着色器:
```c++
#version 330 core
in vec3 ourColor
in vec3 ourColor;
out vec4 color;
void main()
{
color = vec4(ourColor, 1.0f);
@@ -266,12 +267,12 @@ void main()
因为我们添加了另一个顶点属性并且更新了VBO的内存我们就必须重新配置顶点属性指针。更新后的VBO内存中的数据现在看起来像这样
![](http://learnopengl.com/img/getting-started/vertex_attribute_pointer_interleaved.png)
![](../img/01/05/vertex_attribute_pointer_interleaved.png)
知道了当前使用的layout,我们就可以使用`glVertexAttribPointer`函数更新顶点格式,
知道了现在使用的布局,我们就可以使用<fun>glVertexAttribPointer</fun>函数更新顶点格式,
```c++
// 顶点属性
// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
// 颜色属性
@@ -279,28 +280,27 @@ glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)(3
glEnableVertexAttribArray(1);
```
`glVertexAttribPointer`函数的前几个参数比较明了。这次我们配置属性location为1的顶点属性。颜色值有3个float那么大我们不去标准化这些值。
<fun>glVertexAttribPointer</fun>函数的前几个参数比较明了。这次我们配置属性位置值为1的顶点属性。颜色值有3个float那么大我们不去标准化这些值。
由于我们现在有了两个顶点属性,我们不得不重新计算步长值(Stride)。为获得数据队列中下一个属性值(比如位置向量的下个x元素)我们必须向右移动6个float其中3个是位置值另外个是颜色值。这给了我们6个步长的大小每个步长都是float的字节数(=24字节)。
由于我们现在有了两个顶点属性,我们不得不重新计算**步长**值。为获得数据队列中下一个属性值比如位置向量的下个`x`分量)我们必须向右移动6个float其中3个是位置值另外3个是颜色值。这使我们的步长值为6乘以float的字节数=24字节)。
同样这次我们必须指定一个偏移量。对于每个顶点来说位置顶点属性在前所以它的偏移量是0。颜色属性紧随位置数据之后所以偏移量就是`3 * sizeof(GLfloat)`用字节来计算就是12字节。
同样,这次我们必须指定一个偏移量(Offset)。对于每个顶点来说,位置(Position)顶点属性是先声明的所以它的偏移量是0。颜色属性紧随位置数据之后所以偏移量就是`3*sizeof(GLfloat)`用字节来计算就是12字节。
运行程序你应该会看到如下结果:
运行应用你会看到如下结果:
![](http://learnopengl.com/img/getting-started/shaders3.png)
![](../img/01/05/shaders3.png)
如果你有困惑,可以[在这里获得源码](http://learnopengl.com/code_viewer.php?code=getting-started/shaders-interpolated)。
如果你在哪卡住了,可以在[这里](http://learnopengl.com/code_viewer.php?code=getting-started/shaders-interpolated)查看源码
这个图片可能不是你所期望的那种因为我们只提供3个颜色而不是我们现在看到的大调色板。这是所谓片段着色器进行**片段插值(Fragment Interpolation)**的结果。当渲染一个三角形在像素化(Rasterization 也译为光栅化)阶段通常成比原来的顶点更多的像素。像素器就会基于每个像素在三角形所处相对位置决定像素的位置。
这个图片可能不是你所期望的那种,因为我们只提供3个颜色而不是我们现在看到的大调色板。这是片段着色器进行的所谓<def>片段插值</def>(Fragment Interpolation)的结果。当渲染一个三角形时,光栅化(Rasterization)阶段通常会造成比原指定顶点更多的片段。光栅会根据每个片段在三角形形状上所处相对位置决定这些片段的位置。
基于这些位置,它会<def>插值</def>(Interpolate)所有片段着色器的输入变量。比如说我们有一个线段上面的端点是绿色的下面的端点是蓝色的。如果一个片段着色器在线段的70%的位置运行它的颜色输入属性就会是一个绿色和蓝色的线性结合更精确地说就是30%蓝 + 70%绿。
基于这些位置,它**插入(Interpolate)**所有片段着色器的输入变量。比如说,我们有一个线段,上面的那个点是绿色的,下面的点是蓝色的。如果一个片段着色器正在处理的那个片段(实际上就是像素)是在线段的70%的位置它的颜色输入属性就会是一个绿色和蓝色的线性结合更精确地说就是30%蓝+70%绿
这正是这个三角形里发生的事。我们有3个顶点和相应的3个颜色从这个三角形的像素来看它可能包含50,000左右的像素片段着色器为这些像素进行插值。如果你仔细看这些颜色你会发现其中的奥秘红到紫再到蓝。像素插值会应用到所有片段着色器的输入属性上。
这正是在这个三角形中发生了什么。我们有3个顶点和相应的3个颜色从这个三角形的像素来看它可能包含50000左右的片段片段着色器为这些像素进行插值颜色。如果你仔细看这些颜色就应该能明白了红首先变成到紫再变为蓝色。片段插值会被应用到片段着色器的所有输入属性上
# 我们自己的着色器类
编写、编译、管理着色器是件麻烦事。在着色器的最后主题里,我们会写一个类来让我们的生活轻松一点,这个类从硬盘读着色器,然后编译链接它们,对它们进行错误检测,这就变得很好用了。这也会给你一些关于如何把我们目前所学的知识封装到一个抽象对象里的灵感
编写、编译、管理着色器是件麻烦事。在着色器主题的最后,我们会写一个类来让我们的生活轻松一点,它可以从硬盘读着色器,然后编译链接它们,对它们进行错误检测,这就变得很好用了。这也会让你了解该如何封装目前所学的知识到一个抽象对象
我们会在头文件里创建整个类,主要为了学习,也可以方便移植。我们先来添加必要的include定义类结构
我们会把着色器类全部放在在头文件里,主要为了学习用途,当然也方便移植。我们先来添加必要的include定义类结构:
```c++
#ifndef SHADER_H
@@ -311,19 +311,17 @@ glEnableVertexAttribArray(1);
#include <sstream>
#include <iostream>
using namespace std;
#include <GL/glew.h>; // 包含glew获取所有的OpenGL必要headers
#include <GL/glew.h>; // 包含glew来获取所有的必须OpenGL头文件
class Shader
{
public:
// 程序ID
GLuint Program;
// 构造器读取并创建Shader
Shader(const GLchar * vertexSourcePath, const GLchar * fragmentSourcePath);
// 使用Program
void Use();
// 程序ID
GLuint Program;
// 构造器读取并构建着色器
Shader(const GLchar* vertexPath, const GLchar* fragmentPath);
// 使用程序
void Use();
};
#endif
@@ -331,50 +329,55 @@ public:
!!! Important
在上面,我们用了几个预处理指令(Preprocessor Directives)。这些预处理指令告知你的编译器只在没被包含过的情况下才包含和编译这个头文件,即使多个文件都包含了这个shader头文件,它是用来防止链接冲突的。
在上面,我们在头文件顶部使用了几个<def>预处理指令</def>(Preprocessor Directives)。这些预处理指令告知你的编译器只在没被包含过的情况下才包含和编译这个头文件,即使多个文件都包含了这个着色器头文件它是用来防止链接冲突的。
shader类保留了着色器程序的ID。它的构造器需要顶点和片段着色器源代码的文件路径我们可以把各自的文本文件储存在硬盘上。`Use`函数看似平常,但是能够显示这个自造类如何让我们的生活变轻松(虽然只有一点)
着色器类储存了着色器程序的ID。它的构造器需要顶点和片段着色器源代码的文件路径这样我们可以把源码的文本文件储存在硬盘上了。我们还添加了一个<fun>Use</fun>函数,它其实不那么重要,但是能够显示这个自造类如何让我们的生活变轻松虽然只有一点
## 从文件读取
我们使用C++文件流读取着色器内容储存到几个string对象里(译注1)
我们使用C++文件流读取着色器内容,储存到几个`string`对象里译注1
!!! note "译注1"
实际上把着色器代码保存在文件中适合学习OpenGL的时候实际开发中最好把一个着色器直接储存为多个字符串这样具有更高的灵活度。
```c++
Shader(const GLchar * vertexPath, const GLchar * fragmentPath)
Shader(const GLchar* vertexPath, const GLchar* fragmentPath)
{
// 1. 从文件路径获得vertex/fragment源码
// 1. 从文件路径中获取顶点/片段着色器
std::string vertexCode;
std::string fragmentCode;
try {
std::ifstream vShaderFile;
std::ifstream fShaderFile;
// 保证ifstream对象可以抛出异常
vShaderFile.exceptions(std::ifstream::badbit);
fShaderFile.exceptions(std::ifstream::badbit);
try
{
// 打开文件
std::ifstream vShaderFile(vertexPath);
std::ifstream fShaderFile(fragmentPath);
vShaderFile.open(vertexPath);
fShaderFile.open(fragmentPath);
std::stringstream vShaderStream, fShaderStream;
// 读取文件缓冲到流
// 读取文件缓冲内容到流
vShaderStream << vShaderFile.rdbuf();
fShaderStream << fShaderFile.rdbuf();
// 关闭文件句柄
// 关闭文件
vShaderFile.close();
fShaderFile.close();
// 将流转为GLchar数组
// 转换流至GLchar数组
vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
}
catch(std::exception e)
catch(std::ifstream::failure e)
{
std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;
}
const GLchar* vShaderCode = vertexCode.c_str();
const GLchar* fShaderCode = fragmentCode.c_str();
[...]
```
下一步,我们需要编译和链接着色器。注意,我们也检查编译/链接是否失败,如果失败打印编译错误,调试的时候这及其重要(这些错误日志你总会需要的)
下一步,我们需要编译和链接着色器。注意,我们也检查编译/链接是否失败,如果失败打印编译错误,调试的时候这些错误输出会及其重要(你总会需要这些错误日志的)
```c++
// 2. 编译着色器
@@ -386,8 +389,7 @@ GLchar infoLog[512];
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
// 打印编译时错误
// 打印编译错误(如果有的话)
glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
if(!success)
{
@@ -395,7 +397,7 @@ if(!success)
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
};
// 片段着色器进行类似处理
// 片段着色器也类似
[...]
// 着色器程序
@@ -403,8 +405,7 @@ this->Program = glCreateProgram();
glAttachShader(this->Program, vertex);
glAttachShader(this->Program, fragment);
glLinkProgram(this->Program);
// 打印连接错误
// 打印连接错误(如果有的话)
glGetProgramiv(this->Program, GL_LINK_STATUS, &success);
if(!success)
{
@@ -412,12 +413,12 @@ if(!success)
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
// 删除着色器
// 删除着色器,它们已经链接到我们的程序中了,已经不再需要了
glDeleteShader(vertex);
glDeleteShader(fragment);
```
最后我们也实现Use函数:
最后我们也实现<fun>Use</fun>函数:
```c++
void Use()
@@ -426,7 +427,7 @@ void Use()
}
```
现在我们写完了一个完整的着色器类。使用着色器类很简单;我们创建一个着色器对象以后,就可以简单的使用了:
现在我们写完了一个完整的着色器类。使用这个着色器类很简单;只要创建一个着色器对象,从那一点开始我们就可以开始使用了:
```c++
Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.frag");
@@ -441,10 +442,10 @@ while(...)
我们把顶点和片段着色器储存为两个叫做`shader.vs`和`shader.frag`的文件。你可以使用自己喜欢的名字命名着色器文件;我自己觉得用`.vs`和`.frag`作为扩展名很直观。
使用新着色器类的[程序](http://learnopengl.com/code_viewer.php?code=getting-started/shaders-using-object)[着色器类](http://learnopengl.com/code_viewer.php?type=header&code=shader)[顶点着色器](http://learnopengl.com/code_viewer.php?type=vertex&code=getting-started/basic)[片段着色器](http://learnopengl.com/code_viewer.php?type=fragment&code=getting-started/basic)。
源码:[使用新着色器类的程序](http://learnopengl.com/code_viewer.php?code=getting-started/shaders-using-object)[着色器类](http://learnopengl.com/code_viewer.php?type=header&code=shader)[顶点着色器](http://learnopengl.com/code_viewer.php?type=vertex&code=getting-started/basic)[片段着色器](http://learnopengl.com/code_viewer.php?type=fragment&code=getting-started/basic)。
# 练习
1. 修改顶点着色器让三角形上下颠倒:[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/shaders-exercise1)
2. 通过使用uniform定义一个水平偏移在顶点着色器中使用这个偏移量把三角形移动到屏幕右侧[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/shaders-exercise2)
3. 使用`out`关键字把顶点位置输出到片段着色器,把像素的颜色设置为与顶点位置相等(看看顶点位置值是如何在三角形中进行插值的)。做完这些后,尝试回答下面的问题:为什么在三角形的左下角是黑的?[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/shaders-exercise3)
2. 使用uniform定义一个水平偏移,在顶点着色器中使用这个偏移量把三角形移动到屏幕右侧:[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/shaders-exercise2)
3. 使用`out`关键字把顶点位置输出到片段着色器,并将片段的颜色设置为与顶点位置相等(来看看顶点位置值在三角形中插值的结果)。做完这些后,尝试回答下面的问题:为什么在三角形的左下角是黑的?[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/shaders-exercise3)

View File

@@ -6,25 +6,25 @@
翻译 | [Django](http://bullteacher.com/)
校对 | Geequlim, [BLumia](https://github.com/blumia/)
我们已经了解到,我们可以为每个顶点使用颜色来增加图形的细节,从而创建出有趣的图像。但是,如果想让图形看起来更真实我们就必须有足够多的顶点,从而指定足够多的颜色。这将会产生很多额外开销,因为每个模型都会需求更多的顶点和顶点颜色
我们已经了解到,我们可以为每个顶点添加颜色来增加图形的细节,从而创建出有趣的图像。但是,如果想让图形看起来更真实我们就必须有足够多的顶点,从而指定足够多的颜色。这将会产生很多额外开销,因为每个模型都会需求更多的顶点,每个顶点又需求一个颜色属性
艺术家和程序员更喜欢使用**纹理(Texture)**。纹理是一个2D图片(也有1D和3D),它用来添加物体的细节;这就像有一张绘有砖块的图片贴到你的3D的房子上你的房子看起来就像一堵砖墙。因为我们可以在一张图片上插入足够多的细节,这样物体就会拥有很多细节而不用增加额外的顶点。
艺术家和程序员更喜欢使用<def>纹理</def>(Texture)。纹理是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节;你可以想象纹理是一张绘有砖块的纸,无缝折叠贴合到你的3D的房子上这样你的房子看起来就像有砖墙外表了。因为我们可以在一张图片上插入非常多的细节,这样就可以让物体非常精细而不用指定额外的顶点。
!!! Important
除了图像以外,纹理也可以储存大量的数据,这些数据用来发送到着色器上,但是这不是我们现在的主题。
除了图像以外,纹理也可以被用来储存大量的数据,这些数据可以发送到着色器上,但是这不是我们现在的主题。
下面你会看到之前教程的那个三角形贴上了一张[砖墙](http://learnopengl.com/img/textures/wall.jpg)图片。
下面你会看到之前教程的那个三角形贴上了一张[砖墙](../img/01/06/wall.jpg)图片。
![](http://learnopengl.com/img/getting-started/textures.png)
![](../img/01/06/textures.png)
为了能够把纹理映射到三角形上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会有一个**纹理坐标(Texture Coordinate)**,它指明从纹理图像的哪个地方采样(采集像素颜色)。之后在所有的其他的片段上进行片段插值(Fragment Interpolation)。
为了能够把纹理映射(Map)到三角形上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个<def>纹理坐标</def>(Texture Coordinate),用来标明该从纹理图像的哪个部分采样(译注:采集片段颜色。之后在图形的其它片段上进行片段插值(Fragment Interpolation)。
纹理坐标x和y轴上0到1之间的范围(注意我们使用的是2D纹理图片)。使用纹理坐标获取纹理颜色叫做**采样(Sampling)**。纹理坐标起始于(0,0)也就是纹理图片的左下角,终结于纹理图片的右上角(1,1)。下面的图片展示了我们是如何把纹理坐标映射到三角形上的。
纹理坐标x和y轴上,范围为0到1之间注意我们使用的是2D纹理图像)。使用纹理坐标获取纹理颜色叫做<def>采样</def>(Sampling)。纹理坐标起始于(0, 0)也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。下面的图片展示了我们是如何把纹理坐标映射到三角形上的。
![](http://learnopengl.com/img/getting-started/tex_coords.png)
![](../img/01/06/tex_coords.png)
我们为三角形准备了3个纹理坐标点。如上图所示我们希望三角形的左下角对应纹理的左下角因此我们把三角左下角顶点的纹理坐标设置为(0,0);三角形的上顶点对应于图片的中间所以我们把它的纹理坐标设置为(0.5,1.0);同理右下方的顶点设置为(1.0,0)。我们只要传递这三个纹理坐标给顶点着色器就行了,接着片段着色器会为每个片段生成纹理坐标的插值。
我们为三角形指定了3个纹理坐标点。如上图所示我们希望三角形的左下角对应纹理的左下角因此我们把三角左下角顶点的纹理坐标设置为(0, 0);三角形的上顶点对应于图片的上中位置所以我们把它的纹理坐标设置为(0.5, 1.0);同理右下方的顶点设置为(1, 0)。我们只要给顶点着色器传递这三个纹理坐标就行了,接下来它们会被传片段着色器中,它会为每个片段进行纹理坐标的插值。
纹理坐标看起来就像这样:
@@ -32,39 +32,37 @@
GLfloat texCoords[] = {
0.0f, 0.0f, // 左下角
1.0f, 0.0f, // 右下角
0.5f, 1.0f // 顶部位置
0.5f, 1.0f // 上中
};
```
纹理采样几种不同的插值方式。我们需要自己告诉OpenGL在纹理中采用哪种采样方式
纹理采样的解释非常宽松,它可以采用几种不同的插值方式。所以我们需要自己告诉OpenGL该怎样对纹理**采样**
## 纹理环绕方式
纹理坐标通常的范围是从(0, 0)到(1, 1),如果我们把纹理坐标设置范围外会发生什么OpenGL默认的行为是重复这个纹理图像(我们简单地忽略浮点纹理坐标的整数部分)但OpenGL提供了更多的选择
纹理坐标的范围通常是从(0, 0)到(1, 1)如果我们把纹理坐标设置范围外会发生什么OpenGL默认的行为是重复这个纹理图像我们基本上忽略浮点纹理坐标的整数部分但OpenGL提供了更多的选择
环绕方式(Wrapping) | 描述
---|---
GL_REPEAT | 纹理的默认行为重复纹理图像
GL_MIRRORED_REPEAT | 和`GL_REPEAT`一样,除了重复图片是镜像放置的
GL_CLAMP_TO_EDGE | 纹理坐标会在0到1之间超出的部分会重复纹理坐标的边缘就是边缘被拉伸
GL_CLAMP_TO_BORDER | 超出的部分是用户指定的边缘颜色
环绕方式(Wrapping) | 描述
---|---
<var>GL_REPEAT</var> | 纹理的默认行为重复纹理图像
<var>GL_MIRRORED_REPEAT</var> | 和<var>GL_REPEAT</var>一样,但每次重复图片是镜像放置的
<var>GL_CLAMP_TO_EDGE</var> | 纹理坐标会被约束在0到1之间超出的部分会重复纹理坐标的边缘产生一种边缘被拉伸的效果。
<var>GL_CLAMP_TO_BORDER</var> | 超出的坐标为用户指定的边缘颜色
当纹理坐标超出默认范围时,每个都有不同的视觉效果输出。我们来看看这些纹理图像的例子:
当纹理坐标超出默认范围时,每个选项都有不同的视觉效果输出。我们来看看这些纹理图像的例子:
![](http://learnopengl.com/img/getting-started/texture_wrapping.png)
![](../img/01/06/texture_wrapping.png)
前面提到的选项都可以使用`glTexParameter`函数单独设置每个坐标轴`s`、`t`(如果是使用3D纹理那么还有一个`r`)它们和`x`、`y`(`z`)是相等的
前面提到的每个选项都可以使用<fun>glTexParameter*</fun>函数单独的一个坐标轴设置(`s`、`t`如果是使用3D纹理那么还有一个`r`它们和`x`、`y``z`是等价的)
`glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);`
`glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);`
```c++
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
```
第一个参数指定了纹理目标我们使用的是2D纹理因此纹理目标是`GL_TEXTURE_2D`
第一个参数指定了纹理目标我们使用的是2D纹理因此纹理目标是<var>GL_TEXTURE_2D</var>。第二个参数需要我们指定设置的选项与应用的纹理轴。我们打算配置的是`WRAP`选项,并且指定`S`和`T`轴。最后一个参数需要我们传递一个环绕方式在这个例子中OpenGL会给当前激活的纹理设定纹理环绕方式为<var>GL_MIRRORED_REPEAT</var>
第二个参数需要我们去告知我们希望去设置哪个纹理轴。
我们打算设置的是`WRAP`选项并且指定S和T轴。最后一个参数需要我们传递放置方式在这个例子里我们在当前激活纹理上应用`GL_MIRRORED_REPEAT`。
如果我们选择`GL_CLAMP_TO_BORDER`选项,我们还要指定一个边缘的颜色。这次使用`glTexParameter`函数的`fv`后缀形式,加上`GL_TEXTURE_BORDER_COLOR`作为选项这个函数需要我们传递一个边缘颜色的float数组作为颜色值
如果我们选择<var>GL_CLAMP_TO_BORDER</var>选项,我们还需要指定一个边缘的颜色。这需要使用<fun>glTexParameter</fun>函数的`fv`后缀形式,用<var>GL_TEXTURE_BORDER_COLOR</var>作为它的选项并且传递一个float数组作为边缘的颜色值
```c++
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
@@ -73,27 +71,27 @@ glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
## 纹理过滤
纹理坐标不依赖于解析度,它可以是任浮点值,这样OpenGL需要描述出哪个纹理像素对应哪个纹理坐标(Texture Pixel也叫Texel译注1)。当你有一个很大的物体但是纹理解析度很低的时候这就变得很重要了。你可能已经猜到了OpenGL也有一个叫做纹理过滤(Texture Filtering)的选项。有多种不同的选项可用,但是现在我们只讨论最重要的两种:`GL_NEAREST`和`GL_LINEAR`
纹理坐标不依赖于分辨率(Resolution),它可以是任浮点值,所以OpenGL需要知道怎样将纹理像素(Texture Pixel也叫Texel译注1)映射到纹理坐标。当你有一个很大的物体但是纹理的分辨率很低的时候这就变得很重要了。你可能已经猜到了OpenGL也有对于<def>纹理过滤</def>(Texture Filtering)的选项。纹理过滤有很多个选项,但是现在我们只讨论最重要的两种:<var>GL_NEAREST</var>和<var>GL_LINEAR</var>
!!! note "译注1"
Texture Pixel也叫Texel你可以想象你打开一张.jpg格式图片不断放大你会发现它是由无数像素点组成的这个点就是纹理像素注意不要和纹理坐标搞混纹理坐标是你给模型顶点设置的那个数组OpenGL以这个顶点的纹理坐标数据去查找纹理图像上的像素然后进行采样提取纹理像素的颜色
Texture Pixel也叫Texel你可以想象你打开一张`.jpg`格式图片不断放大你会发现它是由无数像素点组成的这个点就是纹理像素注意不要和纹理坐标搞混纹理坐标是你给模型顶点设置的那个数组OpenGL以这个顶点的纹理坐标数据去查找纹理图像上的像素然后进行采样提取纹理像素的颜色
**GL_NEAREST(Nearest Neighbor Filtering,邻近过滤)** 是一种OpenGL默认的纹理过滤方式。当设置为`GL_NEAREST`的时候OpenGL选择最接近纹理坐标中心点的那个像素。下图你会看到四个像素,加号代表纹理坐标。左上角纹理像素距离纹理坐标最近的那个,这样它就会选择这个作为采样颜色:
<var>GL_NEAREST</var>(也叫<def>邻近过滤</def>Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。当设置为<var>GL_NEAREST</var>的时候OpenGL选择中心点最接近纹理坐标的那个像素。下图中你可以看到四个像素,加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色:
![](http://learnopengl.com/img/getting-started/filter_nearest.png)
![](../img/01/06/filter_nearest.png)
**GL_LINEAR((Bi)linear Filtering,线性过滤)** 它会从纹理坐标的临近纹理像素进行计算,返回一个多个纹理像素的近似值。一个纹理像素距离纹理坐标越近,那么这个纹理像素对最终的样颜色的影响越大。下面你会看到临近像素返回的混合色:
<var>GL_LINEAR</var>(也叫<def>线性过滤</def>(Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样颜色的贡献越大。下图中你可以看到返回的颜色是邻近像素的混合色:
![](http://learnopengl.com/img/getting-started/filter_linear.png)
![](../img/01/06/filter_linear.png)
不同的纹理过滤方式有怎样的视觉效果呢?让我们看看在一个很大的物体上应用一张地解析度的纹理会发生什么吧(纹理被放大了,纹理像素能看到)
那么这两种纹理过滤方式有怎样的视觉效果呢?让我们看看在一个很大的物体上应用一张低分辨率的纹理会发生什么吧纹理被放大了,每个纹理像素能看到
![](http://learnopengl.com/img/getting-started/texture_filtering.png)
![](../img/01/06/texture_filtering.png)
如上面两张图片所示,`GL_NEAREST`返回了格子一样的样式,我们能够清晰看到纹理像素组成,而`GL_LINEAR`产生更平滑的样式,看不出纹理像素。`GL_LINEAR`是一种更真实的输出但有些开发者更喜欢8-bit风格所以他们还是用`GL_NEAREST`选项。
<var>GL_NEAREST</var>产生了颗粒状的图案,我们能够清晰看到组成纹理像素,而<var>GL_LINEAR</var>能够产生更平滑的图案,很难看出单个的纹理像素。<var>GL_LINEAR</var>可以产生更真实的输出但有些开发者更喜欢8-bit风格所以他们会用<var>GL_NEAREST</var>选项。
纹理过滤可以为放大和缩小设置不同的选项,这样你可以在纹理被缩小的时候使用最临近过滤,被放大时使用线性过滤。我们必须通过`glTexParameter`为放大和缩小指定过滤方式。这段代码看起来和纹理环绕方式(Texture Wrapping)的设置相似:
当进行<def>放大</def>(Magnify)和<def>缩小</def>(Minify)操作的时候可以设置纹理过滤的选项,比如你可以在纹理被缩小的时候使用近过滤,被放大时使用线性过滤。我们需要使用<fun>glTexParameter*</fun>函数为放大和缩小指定过滤方式。这段代码看起来和纹理环绕方式的设置相似:
```c++
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
@@ -102,68 +100,68 @@ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
### 多级渐远纹理
想象一下,如果我们一个着上千物体的大房间,每个物体上都有纹理。距离观察者远的与距离近的物体的纹理的解析度是相同的。由于远处的物体可能只产生很少的片段OpenGL从高解析度纹理中为这些片段获取正确的颜色值就很困难。这是因为它不得不拾为一个纹理跨度很大的片段取纹理颜色。在小物体上这会产生人工感,更不用说在小物体上使用高解析度纹理浪费内存的问题了。
想象一下,假设我们一个包含着上千物体的大房间,每个物体上都有纹理。有些物体会很远,但其纹理会拥有与近处物体同样高的分辨率。由于远处的物体可能只产生很少的片段OpenGL从高分辨率纹理中为这些片段获取正确的颜色值就很困难,因为它需要对一个跨过纹理很大部分的片段只拾取一个纹理颜色。在小物体上这会产生不真实的感觉,更不用说对它们使用高分辨率纹理浪费内存的问题了。
OpenGL使用一种叫做 **多级渐远纹理(Mipmap)** 的概念解决这个问题,大概来说就是一系列纹理,每个后面的一个纹理是前一个的二分之一。多级渐远纹理背后的思想很简单:距观察者更远的距离的一段确定的阈值OpenGL会把最适合这个距离的物体的不同的多级渐远纹理纹理应用其上。由于距离远,解析度不高也不会被使用者注意到。同时,多级渐远纹理另一加分之处是,执行效率不错。让我们近距离看一多级渐远纹理纹理
OpenGL使用一种叫做<def>多级渐远纹理</def>(Mipmap)的概念解决这个问题,它简单来说就是一系列纹理图像,后一个纹理图像是前一个的二分之一。多级渐远纹理背后的理念很简单:距观察者的距离超过一定的阈值OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。由于距离远,解析度不高也不会被用户注意到。同时,多级渐远纹理另一加分之处是它的性能非常好。让我们看一多级渐远纹理是什么样子的
![](http://learnopengl.com/img/getting-started/mipmaps.png)
![](../img/01/06/mipmaps.png)
手工为每个纹理图像创建一系列多级渐远纹理很麻烦幸好OpenGL有一个`glGenerateMipmaps`函数,它可以在我们创建完一个纹理后帮我们做所有的多级渐远纹理创建工作。后面的纹理教程中你会看到如何使用它。
手工为每个纹理图像创建一系列多级渐远纹理很麻烦幸好OpenGL有一个<fun>glGenerateMipmaps</fun>函数在创建完一个纹理后调用它OpenGL就会承担接下来的所有工作。后面的教程中你会看到如何使用它。
OpenGL渲染的时候两个不同级别的多级渐远纹理之间会产生不真实的生硬边界。就像普通的纹理过滤一样,也可以在两个不同多级渐远纹理级别之间使用`NEAREST`和`LINEAR`过滤。指定不同多级渐远纹理级别之间的过滤方式可以使用下面四选项代替原的过滤方式:
在渲染中切换多级渐远纹理级别(Level)时OpenGL在两个不同级别的多级渐远纹理之间会产生不真实的生硬边界。就像普通的纹理过滤一样,切换多级渐远纹理级别时你也可以在两个不同多级渐远纹理级别之间使用<var>NEAREST</var>和<var>LINEAR</var>过滤。为了指定不同多级渐远纹理级别之间的过滤方式,你可以使用下面四选项中的一个代替原的过滤方式:
过滤方式 | 描述
---|---
GL_NEAREST_MIPMAP_NEAREST | 接收最近的多级渐远纹理来匹配像素大小,并使用最临近插值进行纹理采样
GL_LINEAR_MIPMAP_NEAREST | 接收最近的多级渐远纹理级别,并使用线性插值采样
GL_NEAREST_MIPMAP_LINEAR | 在两个多级渐远纹理之间进行线性插值,通过最邻近插值采样
GL_LINEAR_MIPMAP_LINEAR | 在两个邻的多级渐远纹理进行线性插值,并通过线性插值进行采样
过滤方式 | 描述
---|---
<var>GL_NEAREST_MIPMAP_NEAREST</var> | 使用最邻近的多级渐远纹理来匹配像素大小,并使用近插值进行纹理采样
<var>GL_LINEAR_MIPMAP_NEAREST</var> | 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样
<var>GL_NEAREST_MIPMAP_LINEAR</var> | 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样
<var>GL_LINEAR_MIPMAP_LINEAR</var> | 在两个邻的多级渐远纹理之间使用线性插值,并使用线性插值进行采样
就像纹理过滤一样,前面提到的四种方法也可以使用`glTexParameteri`设置过滤方式
就像纹理过滤一样,我们可以使用<fun>glTexParameteri</fun>将过滤方式设置为前面四种提到的方法之一
```c++
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
```
常见的错误是,为多级渐远纹理过滤选项设置放大过滤。这样没有任何效果,因为多级渐远纹理主要使用在纹理被缩小的情况下的:纹理放大不会使用多级渐远纹理,为多级渐远纹理设置放大过滤选项会产生一个`GL_INVALID_ENUM`错误
一个常见的错误是,将放大过滤的选项设置为多级渐远纹理过滤选项之一。这样没有任何效果,因为多级渐远纹理主要使用在纹理被缩小的情况下的:纹理放大不会使用多级渐远纹理,为放大过滤设置多级渐远纹理的选项会产生一个<var>GL_INVALID_ENUM</var>错误代码
# 加载创建纹理
# 加载创建纹理
使用纹理之前要做的第一件事是把它们加载到应用中。纹理图像可能储存为各种各样的格式,每种都有自己的数据结构和排列,所以我们如何才能把这些图像加载到应用中呢?一个解决方案是一个我们自己的某种图像格式加载器比如.PNG用它来把图像转化为byte序列。写自己的图像加载器虽然不难,但仍然挺烦的,而且如果要支持更多文件格式呢?你就不得不为每种你希望支持的格式写加载器了。
使用纹理之前要做的第一件事是把它们加载到我们的应用中。纹理图像可能储存为各种各样的格式,每种都有自己的数据结构和排列,所以我们如何才能把这些图像加载到应用中呢?一个解决方案是一个需要的文件格式,比如`.PNG`,然后自己写一个图像加载器,把图像转化为字节序列。写自己的图像加载器虽然不难,但仍然挺烦的,而且如果要支持更多文件格式呢?你就不得不为每种你希望支持的格式写加载器了。
另一个解决方案是,也许是更好的一种选择,就是使用一个支持多种流行格式的图像加载库来为我们解决这个问题。就像SOIL这种库①
另一个解决方案也许是一种更好的选择,使用一个支持多种流行格式的图像加载库来为我们解决这个问题。比如说我们要用的SOIL库
## SOIL
SOIL是Simple OpenGL Image Library(简易OpenGL图像库)的缩写,它支持大多数流行的图像格式,使用起来也很简单,你可以从他们的主页下载。像大多数其他库一样,你必须自己生成**.lib**。你可以使用**/projects**文件夹里的解决方案(Solution)文件之一(不用担心他们的Visual Studio版本太老你可以把它们转变为新的版本;这总是可行的。译注用VS2010的时候你要用VC8而不是VC9的解决方案想必更高版本的情况亦是如此)你也可以使用CMake自己生成。你还要添加**src**文件夹里面的文件到你的**includes**文件夹;对了,不要忘记添加**SOIL.lib**到你的接器选项,并在你代码文件的开头加上`#include <SOIL.h>`。
SOIL是简易OpenGL图像库(Simple OpenGL Image Library)的缩写,它支持大多数流行的图像格式,使用起来也很简单,你可以从他们的[主页](http://www.lonesock.net/soil.html)下载。像其它库一样,你必须自己生成**.lib**。你可以使用**/projects**文件夹内的任意一个解决方案(Solution)文件不用担心他们的Visual Studio版本太老你可以把它们转变为新的版本,这一般是没问题的。译注用VS2010的时候你要用VC8而不是VC9的解决方案想必更高版本的情况亦是如此)来生成你自己的**.lib**文件。你还要添加**src**文件夹里面的文件到你的**includes**文件夹;对了,不要忘记添加**SOIL.lib**到你的接器选项,并在你代码文件的开头加上`#include <SOIL.h>`。
下面的纹理部分我们会使用一张木箱的图片。使用SOIL加载图片我们使用它的`SOIL_load_image`函数:
下面的教程中,我们会使用一张[木箱](../img/01/06/container.jpg)的图片。使用SOIL加载图片我们需要使用它的<fun>SOIL_load_image</fun>函数:
```c++
int width, height;
unsigned char* image = SOIL_load_image("container.jpg", &width, &height, 0, SOIL_LOAD_RGB);
```
函数首先需要输入图片文件的路径。然后需要两个int指针作为第二个和第三个参数SOIL会返回图片的宽度和高度到其中。之后,我们需要图片的宽度和高度来生成纹理。第四个参数指定图片的通道(Channel)数量,但是这里我们只需留`0`。最后一个参数告诉SOIL如何来加载图片我们只图片的RGB感兴趣。结果储存为一个大的char/byte数组。
函数首先需要输入图片文件的路径。然后需要两个`int`指针作为第二个和第三个参数SOIL会分别返回图片的**宽度**和**高度**到其中。后面我们在生成纹理的时候会用图像的宽度和高度。第四个参数指定图片的**通道**(Channel)数量,但是这里我们只需留`0`。最后一个参数告诉SOIL如何来加载图片我们只关注图片的`RGB`值。结果储存为一个大的char/byte数组。
## 生成纹理
和之前生成的OpenGL对象一样纹理也是使用ID引用的。
和之前生成的OpenGL对象一样纹理也是使用ID引用的。让我们来创建一个:
```c++
GLuint texture;
glGenTextures(1, &texture);
```
`glGenTextures`函数首先需要输入纹理生成的数量,然后把它们储存在第二个参数的`GLuint`数组中(我们的例子里只有一个`GLuint`),就像其他对象一样,我们需要绑定它,所以下面的纹理命令会配置当前绑定的纹理:
<fun>glGenTextures</fun>函数首先需要输入生成纹理的数量,然后把它们储存在第二个参数的`GLuint`数组中我们的例子中只是一个单独的`GLuint`,就像其他对象一样,我们需要绑定它,让之后任何的纹理指令都可以配置当前绑定的纹理:
```c++
glBindTexture(GL_TEXTURE_2D, texture);
```
现在纹理绑定了,我们可以使用前面载入的图片数据生成纹理了纹理通过`glTexImage2D`来生成:
现在纹理已经绑定了,我们可以使用前面载入的图片数据生成一个纹理了纹理可以通过<fun>glTexImage2D</fun>来生成:
```c++
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
@@ -172,17 +170,17 @@ glGenerateMipmap(GL_TEXTURE_2D);
函数很长,参数也不少,所以我们一个一个地讲解:
- 第一个参数指定纹理目标(环境);设置为`GL_TEXTURE_2D`意味着会生成与当前绑定的纹理对象在同一个目标(Target)上的纹理(任何绑定到`GL_TEXTURE_1D`和`GL_TEXTURE_3D`的纹理不会受到影响)
- 第二个参数为我们打算创建的纹理指定多级渐远纹理的级,如果你希望单独手设置每个多级渐远纹理的级的话。这里我们填0基本级。
- 第三个参数告诉OpenGL我们希望把纹理储存为何种格式。我们的图像只有RGB值因此我们把纹理储存为`GL_RGB`值。
- 第四个和第五个参数设置最终的纹理的宽度和高度。我们加载图像的时候提前储存它们这样我们就能使用相应变量
下个参数应该一直被设为`0`(遗留问题)
- 第七第八个参数定义了源图的格式和数据类型。我们使用RGB值加载这个图像并把它们储存char(byte),我们将会传入应值。
- 最后一个参数是真的图像数据。
- 第一个参数指定纹理目标(Target)。设置为<var>GL_TEXTURE_2D</var>意味着会生成与当前绑定的纹理对象在同一个目标上的纹理任何绑定到<var>GL_TEXTURE_1D</var>和<var>GL_TEXTURE_3D</var>的纹理不会受到影响
- 第二个参数为纹理指定多级渐远纹理的级,如果你希望单独手设置每个多级渐远纹理的级的话。这里我们填0,也就是基本级
- 第三个参数告诉OpenGL我们希望把纹理储存为何种格式。我们的图像只有`RGB`值,因此我们把纹理储存为`RGB`值。
- 第四个和第五个参数设置最终的纹理的宽度和高度。我们之前加载图像的时候储存它们,所以我们使用对应的变量。
- 下个参数应该总是被设为`0`(历史遗留问题
- 第七第八个参数定义了源图的格式和数据类型。我们使用RGB值加载这个图像并把它们储存为`char`(byte)数组,我们将会传入应值。
- 最后一个参数是真的图像数据。
当调用`glTexImage2D`,当前绑定的纹理对象就会被附加上纹理图像。然而,前只有基本级别(Base-level)纹理图像加载了,如果要使用多级渐远纹理,我们必须手设置不同的图像(通过不断把第二个参数增加的方式)或者,在生成纹理之后调用`glGenerateMipmap`。这会为当前绑定的纹理自动生成所有需要的多级渐远纹理。
当调用<fun>glTexImage2D</fun>时,当前绑定的纹理对象就会被附加上纹理图像。然而,前只有基本级别(Base-level)纹理图像加载了,如果要使用多级渐远纹理,我们必须手设置所有不同的图像(不断递增第二个参数)。或者,直接在生成纹理之后调用<fun>glGenerateMipmap</fun>。这会为当前绑定的纹理自动生成所有需要的多级渐远纹理。
生成了纹理和相应的多级渐远纹理后,解绑纹理对象、释放图像的内存很重要
生成了纹理和相应的多级渐远纹理后,释放图像的内存并解绑纹理对象是一个很好的习惯
```c++
SOIL_free_image_data(image);
@@ -195,11 +193,11 @@ glBindTexture(GL_TEXTURE_2D, 0);
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
//为当前绑定的纹理对象设置环绕、过滤方式
// 为当前绑定的纹理对象设置环绕、过滤方式
...
//加载并生成纹理
// 加载并生成纹理
int width, height;
unsigned char * image = SOIL_load_image("container.jpg", &width, &height, 0, SOIL_LOAD_RGB);
unsigned char* image = SOIL_load_image("container.jpg", &width, &height, 0, SOIL_LOAD_RGB);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
glGenerateMipmap(GL_TEXTURE_2D);
SOIL_free_image_data(image);
@@ -208,38 +206,40 @@ glBindTexture(GL_TEXTURE_2D, 0);
## 应用纹理
后面的部分我们会使用`glDrawElements`绘制[Hello Triangle](http://learnopengl-cn.readthedocs.org/zh/latest/01%20Getting%20started/04%20Hello%20Triangle/)教程最后一部分的矩形。我们需要告知OpenGL如何采样纹理这样我们必须更新顶点纹理坐标数据:
后面的部分我们会使用<fun>glDrawElements</fun>绘制[「你好,三角形」](04 Hello Triangle.md)教程最后一部分的矩形。我们需要告知OpenGL如何采样纹理所以我们必须使用纹理坐标更新顶点数据:
```c++
GLfloat vertices[] = {
// ---- 位置 ---- ---- 颜色 ---- ---- 纹理坐标 ----
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,// 左下
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // 左上
// ---- 位置 ---- ---- 颜色 ---- - 纹理坐标 -
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // 左上
};
```
由于我们添加了一个额外的顶点属性,我们必须通知OpenGL新的顶点格式
由于我们添加了一个额外的顶点属性,我们必须告诉OpenGL我们新的顶点格式:
![](http://learnopengl.com/img/getting-started/vertex_attribute_pointer_interleaved_textures.png)
![](../img/01/06/vertex_attribute_pointer_interleaved_textures.png)
```c++
glVertexAttribPointer(2, 2, GL_FLOAT,GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat)));
glEnableVertexAttribArray(2);
```
注意,我们必须修正前面两个顶点属性的步长参数为`8 * sizeof(GLfloat)`。
注意,我们同样需要调整前面两个顶点属性的步长参数为`8 * sizeof(GLfloat)`。
接着我们需要顶点着色器把纹理坐标为一个顶点属性,把坐标传给片段着色器:
接着我们需要调整顶点着色器使其能够接受顶点坐标为一个顶点属性,把坐标传给片段着色器:
```c++
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 color;
layout (location = 2) in vec2 texCoord;
out vec3 ourColor;
out vec2 TexCoord;
void main()
{
gl_Position = vec4(position, 1.0f);
@@ -249,23 +249,26 @@ void main()
```
片段着色器应该把输出变量`TexCoord`作为输入变量。
片段着色器应该也获取纹理对象但是我们怎样把纹理对象传给片段着色器GLSL有一个内建数据类型,供纹理对象使用,叫做采样器(Sampler),它以纹理类型作为后缀,比如`sampler1D`、`sampler3D`,在我们的例子中它是`sampler2D`。我们可以简单声明一个`uniform sampler2D`把一个纹理到片段着色器中,稍后我们把我们的纹理赋值给这个uniform。
片段着色器应该能访问纹理对象,但是我们怎样把纹理对象传给片段着色器GLSL有一个供纹理对象使用的内建数据类型,叫做<def>采样器</def>(Sampler),它以纹理类型作为后缀,比如`sampler1D`、`sampler3D`在我们的例子中`sampler2D`。我们可以简单声明一个`uniform sampler2D`把一个纹理添加到片段着色器中,稍后我们把纹理赋值给这个uniform。
```c++
#version 330 core
in vec3 ourColor;
in vec2 TexCoord;
out vec4 color;
uniform sampler2D ourTexture;
void main()
{
color = texture(ourTexture, TexCoord);
}
```
我们使用GLSL内建`texture`函数来采样纹理的颜色,它第一个参数是纹理采样器,第二个参数是应的纹理坐标。`texture`函数使用前设置的纹理参数对相应颜色值进行采样。这个片段着色器的输出就是纹理的(插值)纹理坐标上的(过滤)颜色。
我们使用GLSL内建的<fun>texture</fun>函数来采样纹理的颜色,它第一个参数是纹理采样器,第二个参数是应的纹理坐标。<fun>texture</fun>函数使用前设置的纹理参数对相应颜色值进行采样。这个片段着色器的输出就是纹理的插值纹理坐标上的(过滤后的)颜色。
现在要做的就是在调用`glDrawElements`之前绑定纹理,它会自动把纹理赋值给片段着色器的采样器:
现在只剩下在调用<fun>glDrawElements</fun>之前绑定纹理,它会自动把纹理赋值给片段着色器的采样器:
```c++
glBindTexture(GL_TEXTURE_2D, texture);
@@ -274,13 +277,13 @@ glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
```
如果你跟着这个教程正确做完了,你会看到下面的图像:
如果你跟着这个教程正确做完了,你会看到下面的图像:
![](http://learnopengl.com/img/getting-started/textures2.png)
![](../img/01/06/textures2.png)
如果你的矩形是全黑或全白的你可能在哪儿做错了什么。检查你的着色器日志,或者尝试对比一下[源码](http://learnopengl.com/code_viewer.php?code=getting-started/textures)。
如果你的矩形是全黑或全白的你可能在哪儿做错了什么。检查你的着色器日志,尝试对比一下[源码](http://learnopengl.com/code_viewer.php?code=getting-started/textures)。
我们还可以把纹理颜色顶点颜色混合,来获得有趣的效果。我们简单的把纹理颜色与顶点颜色在片段着色器中相乘来混合二者的颜色:
我们还可以把得到的纹理颜色顶点颜色混合,来获得有趣的效果。我们只需把纹理颜色与顶点颜色在片段着色器中相乘来混合二者的颜色:
```c++
color = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0f);
@@ -288,45 +291,47 @@ color = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0f);
最终的效果应该是顶点颜色和纹理颜色的混合色:
![](http://learnopengl.com/img/getting-started/textures_funky.png)
![](../img/01/06/textures_funky.png)
这个箱子看起来有点70年代迪斯科风格
我猜你会说我们的箱子喜欢跳70年代迪斯科。
## 纹理单元
你可能感到奇怪为什么`sampler2D`是个uniform变量,你却不用`glUniform`给它赋值使用`glUniform1i`我们可以给纹理采样器确定一个位置,这样的话我们能够一次在一个片段着色器中设置多纹理。一个纹理的位置通常称为一个纹理单元(Texture Units)。一个纹理的默认纹理单元是0它是默认激活纹理单元,所以教程前面部分我们不用给它确定一个位置。
你可能奇怪为什么`sampler2D`变量是个uniform,我们却不用<fun>glUniform</fun>给它赋值使用<fun>glUniform1i</fun>我们可以给纹理采样器分配一个位置,这样的话我们能够在一个片段着色器中设置多纹理。一个纹理的位置通常称为一个<def>纹理单元</def>(Texture Unit)。一个纹理的默认纹理单元是0它是默认激活纹理单元,所以教程前面部分我们没有分配一个位置
纹理单元的主要目的是让我们在着色器中可以使用多于一个的纹理。通过把纹理单元赋值给采样器,我们可以一次绑定多纹理,只要我们首先激活应的纹理单元。就像`glBindTexture`一样,我们可以使用`glActiveTexture`激活纹理单元,传入我们需要使用的纹理单元:
纹理单元的主要目的是让我们在着色器中可以使用多于一个的纹理。通过把纹理单元赋值给采样器,我们可以一次绑定多纹理,只要我们首先激活应的纹理单元。就像<fun>glBindTexture</fun>一样,我们可以使用<fun>glActiveTexture</fun>激活纹理单元,传入我们需要使用的纹理单元:
```c++
glActiveTexture(GL_TEXTURE0); //在绑定纹理之前先激活纹理单元
glActiveTexture(GL_TEXTURE0); //在绑定纹理之前先激活纹理单元
glBindTexture(GL_TEXTURE_2D, texture);
```
激活纹理单元之后,接下来`glBindTexture`调用函数,会绑定这个纹理到当前激活的纹理单元,纹理单元`GL_TEXTURE0`总是默认被激活,所以我们在前面的例子里当我们使用`glBindTexture`的时候,无需激活任何纹理单元。
激活纹理单元之后,接下来的<fun>glBindTexture</fun>函数调用会绑定这个纹理到当前激活的纹理单元,纹理单元<var>GL_TEXTURE0</var>默认总是被激活,所以我们在前面的例子里当我们使用`glBindTexture`的时候,无需激活任何纹理单元。
!!! Important
OpenGL至少提供16个纹理单元供你使用也就是说你可以激活`GL_TEXTURE0`到`GL_TEXTRUE15`。它们都是顺序定义的,所以我们也可以通过`GL_TEXTURE0+8`的方式获得`GL_TEXTURE8`,这个例子在当我们不得不循环几个纹理的时候变得很有用。
OpenGL至少保证有16个纹理单元供你使用也就是说你可以激活从<var>GL_TEXTURE0</var>到<var>GL_TEXTRUE15</var>。它们都是顺序定义的,所以我们也可以通过<var>GL_TEXTURE0 + 8</var>的方式获得<var>GL_TEXTURE8</var>,这在当我们需要循环一些纹理单元的时候很有用。
我们仍然要编辑片段着色器来接收另一个采样器。方法现在相对简单了:
我们仍然要编辑片段着色器来接收另一个采样器。这应该相对来说非常直接了:
```c++
#version 330 core
...
uniform sampler2D ourTexture1;
uniform sampler2D ourTexture2;
void main()
{
color = mix(texture(ourTexture1, TexCoord), texture(ourTexture2, TexCoord), 0.2);
}
```
最终输出颜色现在结合了两个纹理查找。GLSL内建`mix`函数需要两个参数将根据第三个参数为前两者作为输入,并在之间进行线性插值。如果第三个值是0.0它返回第一个输入如果是1.0,就返回第二个输入值。0.2返回80%的第一个输入颜色和20%的第二个输入颜色,返回两个纹理的混合。
最终输出颜色现在两个纹理的结合。GLSL内建的<fun>mix</fun>函数需要接受两个值作为参数,并对它们根据第三个参数进行线性插值。如果第三个值是`0.0`,它返回第一个输入;如果是`1.0`,会返回第二个输入值。`0.2`会返回`80%`的第一个输入颜色和`20%`的第二个输入颜色,返回两个纹理的混合
我们现在需要载入创建另一个纹理;我们应该对这些步骤感到熟悉了。确保创建另一个纹理对象,载入图片,使用`glTexImage2D`生成最终纹理。对于第二个纹理我们使用一张你学习OpenGL时的表情图片。
我们现在需要载入创建另一个纹理;应该对这些步骤熟悉了。记得创建另一个纹理对象,载入图片,使用<fun>glTexImage2D</fun>生成最终纹理。对于第二个纹理我们使用一张[你学习OpenGL时的面部表情](../img/01/06/awesomeface.png)图片。
为了使用第二个纹理(也包括第一个),我们必须改变一点渲染流程,先绑定两个纹理到应的纹理单元然后定义哪个uniform采样器对应哪个纹理单元
为了使用第二个纹理(以及第一个,我们必须改变一点渲染流程,先绑定两个纹理到应的纹理单元然后定义哪个uniform采样器对应哪个纹理单元
```c++
glActiveTexture(GL_TEXTURE0);
@@ -337,36 +342,36 @@ glBindTexture(GL_TEXTURE_2D, texture2);
glUniform1i(glGetUniformLocation(ourShader.Program, "ourTexture2"), 1);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_IN, 0);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
```
注意,我们使用了`glUniform1i`设置uniform采样器的位置或曰纹理单元。通过`glUniform1i`的设置,我们保证每个uniform采样器对应于合适的纹理单元。可以获得下面的结果:
注意,我们使用<fun>glUniform1i</fun>设置uniform采样器的位置值,或者说纹理单元。通过<fun>glUniform1i</fun>的设置我们保证每个uniform采样器对应着正确的纹理单元。你应该能得到下面的结果:
![](http://learnopengl.com/img/getting-started/textures_combined.png)
![](../img/01/06/textures_combined.png)
你可能注意到纹理上下颠倒了这是因为OpenGL要求y轴0.0坐标是在图片的下面的,但是图片通常y轴0.0坐标在上面。一些图片加载器比如DevIL在加载的时候有选项重置y原点但是SOIL没有。SOIL有一个叫做`SOIL_load_OGL_texture`函数可以使用一个叫做`SOIL_FLAG_INVERT_Y`的标记加载和生成纹理,它用来解决我们的问题。不过这个函数在现代OpenGL中的这个特性失效了,所以现在我们必须坚持使用`SOIL_load_image`,自己做纹理生成。
你可能注意到纹理上下颠倒了这是因为OpenGL要求y轴`0.0`坐标是在图片的底部的,但是图片的y轴`0.0`坐标通常在顶部。一些图片加载器比如[DevIL](http://openil.sourceforge.net/tuts/tut_10/index.htm)在加载的时候有选项重置y原点但是SOIL没有。SOIL有一个叫做<fun>SOIL_load_OGL_texture</fun>函数可以使用一个叫做<var>SOIL_FLAG_INVERT_Y</var>的标记加载**并**生成纹理,这可以解决我们的问题。不过这个函数用了一些在现代OpenGL中失效的特性,所以现在我们仍需坚持使用<fun>SOIL_load_image</fun>,自己做纹理生成。
所以修复我们的小问题,有两个选择:
1. 我们切换顶点数据的纹理坐标,翻转`y`值(用1减去y坐标)
2. 我们可以编辑顶点着色器来翻转`y`坐标,自动替换`TexCoord`赋值:`TexCoord = vec2(texCoord.x, 1 - texCoord.y);`
1. 我们可以改变顶点数据的纹理坐标,翻转`y`值用1减去y坐标
2. 我们可以编辑顶点着色器来自动翻转`y`坐标,替换`TexCoord`的值为`TexCoord = vec2(texCoord.x, 1.0f - texCoord.y);`
!!! Attention
上面提供的解决方案仅仅通过一些hacks让图片翻转。它们在大多数情况下都能正常工作然而实际上这种方案的效果取决于你的实现和纹理所以最好的解决方案是调整你的图片加载器或者以一种y原点符合OpenGL需求的方式编辑你的纹理图像。
上面提供的解决方案仅仅通过一些黑科技让图片翻转。它们在大多数情况下都能正常工作然而实际上这种方案的效果取决于你的实现和纹理所以最好的解决方案是调整你的图片加载器或者以一种y原点符合OpenGL需求的方式编辑你的纹理图像。
如果你编辑了顶点数据,在顶点着色器中翻转了纵坐标,你会得到下面的结果:
![](http://learnopengl.com/img/getting-started/textures_combined2.png)
![](../img/01/06/textures_combined2.png)
如果你看到了图片上的笑脸容器,你就做对了。你可以对比[程序源代码](http://learnopengl.com/code_viewer.php?code=getting-started/textures_combined),以及[顶点着色器](http://learnopengl.com/code_viewer.php?type=vertex&code=getting-started/texture)和[片段着色器](http://learnopengl.com/code_viewer.php?type=fragment&code=getting-started/texture)。
如果你看到了一个开心的箱子,你就做对了。你可以对比一下[源代码](http://learnopengl.com/code_viewer.php?code=getting-started/textures_combined),以及[顶点着](http://learnopengl.com/code_viewer.php?type=vertex&code=getting-started/texture)和[片段](http://learnopengl.com/code_viewer.php?type=fragment&code=getting-started/texture)着色器
## 练习
为了更熟练地使用纹理,建议在继续之后的学习之前做完这些练习:
- 使用片段着色器**仅**对笑脸图案进行翻转,[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise1)
- 尝试用不同的纹理环绕方式,并将纹理坐标的范围设定为从`0.0f`到`2.0f`而不是原来的`0.0f`到`1.0f`,在木箱子的角落放置4个笑脸[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise2)[结果](http://learnopengl.com/img/getting-started/textures_exercise2.png)。记得一定要试试其他的环绕方式。
- 尝试在矩形范围内只显示纹理图的中间一部分,并通过修改纹理坐标来设置显示效果。尝试使用`GL_NEAREST`的纹理过滤方式让像显示得更清晰:[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise3)
- 使用一个uniform变量作为`mix`函数的第三个参数来改变两个纹理可见度,使用上和下键来改变容器的大小和笑脸是否可见:[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise4)[片段着色器](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise4_fragment)。
- 修改片段着色器**仅**对笑脸图案进行翻转,[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise1)
- 尝试用不同的纹理环绕方式,设定一个从`0.0f`到`2.0f`范围内的(而不是原来的`0.0f`到`1.0f`)纹理坐标。试试看能不能在箱子的角落放置4个笑脸[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise2)[结果](http://learnopengl.com/img/getting-started/textures_exercise2.png)。记得一定要试试其他的环绕方式。
- 尝试在矩形只显示纹理图的中间一部分,修改纹理坐标,达到能看见单个的像素的效果。尝试使用<var>GL_NEAREST</var>的纹理过滤方式让像显示得更清晰:[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise3)
- 使用一个uniform变量作为<fun>mix</fun>函数的第三个参数来改变两个纹理可见度,使用上和下键来改变箱子或笑脸的可见[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise4)[片段着色器](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise4_fragment)。

View File

@@ -6,35 +6,35 @@
翻译 | Meow J
校对 | Geequlim
恭喜您完成了本章的学习至此为止你应该能够创建一个窗口创建并且编译着色器通过缓冲对象或者uniform发送顶点数据绘制物体使用纹理理解向量和矩阵并且可以综合上述知识创建一个3D场景并可以通过摄像机来移动.
恭喜您完成了本章的学习至此为止你应该能够创建一个窗口创建并且编译着色器通过缓冲对象或者uniform发送顶点数据绘制物体使用纹理理解向量和矩阵并且可以综合上述知识创建一个3D场景并可以通过摄像机来移动
这些就是我们在前几章学习的内容,尝试在教程的基础上进行改动程序,或者实验自己的想法并解决问题. 一旦你认为你真正熟悉了我们讨论的所有的东西,你就可以进行[下一节](http://learnopengl-cn.readthedocs.org/zh/latest/02%20Lighting/01%20Colors/)的学习.
最后这几章我们学了太多的东西了。你可以尝试在教程的基础上改动程序,或者实验一下,有一点自己的想法并解决问题一旦你认为你真正熟悉了我们讨论的所有的东西,你就可以进行[下一节](../02 Lighting/01 Colors.md)的学习
## 词汇表
- **OpenGL**: 一个定义了函数布局和输出的图形API的正式规范.
- **GLEW**: 一个拓展加载库用来为我们加载并设定所有OpenGL函数指针从而让我们能够使用所有(现代)OpenGL函数.
- **视口(Viewport)**: 我们需要渲染的窗口.
- **图形管(Graphics Pipeline)**: 一个顶点在呈现为像素之前通过的过程.
- **着色器(Shader)**: 一个运行在显卡上的小型程序.很多阶段的图形管道都可以使用自定义的着色器来代替原来的功能.
- **标准化设备坐标(Normalized Device Coordinates)**: 顶点在通过在剪裁坐标系中剪裁与透视划分后最终呈现在的坐标系. 所有位置在NDC下-1.0到1.0的顶点将不会被丢弃并且可见.
- **顶点缓冲对象(Vertex Buffer Object)**: 一个调用显存并存储所有顶点数据供显卡使用的缓冲对象.
- **顶点数组对象(Vertex Array Object)**: 存储缓冲区和顶点属性状态.
- **元素缓冲对象(Element Buffer Object)**: 一个存储索引供索引化绘制使用的缓冲对象.
- **Uniform**: 一个特殊类型的GLSL变量.它是全局的(在一个着色器程序中每一个着色器都能够访问uniform变量)并且只能被设定一次.
- **纹理(Texture)**: 一种缠绕物体的特殊类型图片,给物体精细的视觉效果.
- **纹理缠绕(Texture Wrapping)**: 定义了一种当纹理顶点超出范围(0,1)时指定OpenGL如何采样纹理的模式
- **纹理过滤(Texture Filtering)**: 定义了一种当有多种纹素选择时指定OpenGL如何采样纹理的模式. 这通常在纹理被放大情况下发生.
- **多级渐远纹理(Mipmaps)**: 被存储的材质一些的缩小版本,根据距观察者的距离会使用材质的合适大小.
- **SOIL**: 图像加载库
- **纹理单元(Texture Units)**: 通过绑定纹理到不同纹理单元从而允许多个纹理在同一对象上渲染.
- **向量(Vector)**: 一个定义了在空间中方向和/或位置数学实体.
- **矩阵(Matrix)**: 一个矩形阵列的数学表达式.
- **GLM**: 一个为OpenGL打造的数学库.
- **局部空间(Local Space)**: 一个对象的初始空间. 所有的坐标都是相对于对象的原点的.
- **世界空间(World Space)**: 所有的坐标都相对于全局原点.
- **观察空间(View Space)**: 所有的坐标都是从摄像机的视角观察的.
- **裁剪空间(Clip Space)**: 所有的坐标都是从摄像机视角观察的,但是该空间应用了投影.这个空间应该是一个顶点坐标最终的空间,作为顶点着色器的输出. OpenGL负责处理剩下的事情(裁剪/透视划分).
- **屏幕空间(Screen Space)**: 所有的坐标都由屏幕视角来观察. 坐标的范围是从0到屏幕的宽/高.
- **LookAt矩阵**: 一种特殊类型的观察矩阵,它创建了一个坐标系,其中所有坐标都根据从一个位置正在观察目标的用户旋转或者平移.
- **欧拉角(Euler Angles)**: 被定义为偏航角(yaw),俯仰角(pitch),和滚动角(roll)从而允许我们通过这三个值构造任何3D方向.
- **OpenGL**: 一个定义了函数布局和输出的图形API的正式规范
- **GLEW**: 一个拓展加载库用来为我们加载并设定所有OpenGL函数指针从而让我们能够使用所有现代OpenGL函数
- **视口(Viewport)**: 我们需要渲染的窗口
- **图形管线(Graphics Pipeline)**: 一个顶点在呈现为像素之前通过的过程
- **着色器(Shader)**: 一个运行在显卡上的小型程序很多阶段的图形管道都可以使用自定义的着色器来代替原来的功能
- **标准化设备坐标(Normalized Device Coordinates)**: 顶点在通过在剪裁坐标系中剪裁与透视划分后最终呈现在的坐标系所有位置在NDC下-1.0到1.0的顶点将不会被丢弃并且可见
- **顶点缓冲对象(Vertex Buffer Object)**: 一个调用显存并存储所有顶点数据供显卡使用的缓冲对象
- **顶点数组对象(Vertex Array Object)**: 存储缓冲区和顶点属性状态
- **索引缓冲对象(Element Buffer Object)**: 一个存储索引供索引化绘制使用的缓冲对象
- **Uniform**: 一个特殊类型的GLSL变量它是全局的(在一个着色器程序中每一个着色器都能够访问uniform变量)并且只能被设定一次
- **纹理(Texture)**: 一种缠绕物体的特殊类型图片,给物体精细的视觉效果
- **纹理缠绕(Texture Wrapping)**: 定义了一种当纹理顶点超出范围(0, 1)时指定OpenGL如何采样纹理的模式
- **纹理过滤(Texture Filtering)**: 定义了一种当有多种纹素选择时指定OpenGL如何采样纹理的模式这通常在纹理被放大情况下发生
- **多级渐远纹理(Mipmaps)**: 被存储的材质一些的缩小版本,根据距观察者的距离会使用材质的合适大小
- **SOIL**: 图像加载库
- **纹理单元(Texture Units)**: 通过绑定纹理到不同纹理单元从而允许多个纹理在同一对象上渲染
- **向量(Vector)**: 一个定义了在空间中方向和/或位置数学实体
- **矩阵(Matrix)**: 一个矩形阵列的数学表达式
- **GLM**: 一个为OpenGL打造的数学库
- **局部空间(Local Space)**: 一个对象的初始空间所有的坐标都是相对于对象的原点的
- **世界空间(World Space)**: 所有的坐标都相对于全局原点
- **观察空间(View Space)**: 所有的坐标都是从摄像机的视角观察的
- **裁剪空间(Clip Space)**: 所有的坐标都是从摄像机视角观察的,但是该空间应用了投影这个空间应该是一个顶点坐标最终的空间,作为顶点着色器的输出OpenGL负责处理剩下的事情裁剪/透视划分)。
- **屏幕空间(Screen Space)**: 所有的坐标都由屏幕视角来观察坐标的范围是从0到屏幕的宽/高
- **LookAt矩阵**: 一种特殊类型的观察矩阵,它创建了一个坐标系,其中所有坐标都根据从一个位置正在观察目标的用户旋转或者平移
- **欧拉角(Euler Angles)**: 被定义为偏航角(yaw),俯仰角(pitch),和滚动角(roll)从而允许我们通过这三个值构造任何3D方向

View File

@@ -55,7 +55,7 @@ glm::vec3 result = lightColor * toyColor; // = (0.33f, 0.21f, 0.06f);
首先我们需要一个物体来投光(Cast the light)我们将无耻地使用前面教程中的立方体箱子。我们还需要一个物体来代表光源它代表光源在这个3D空间中的确切位置。简单起见我们依然使用一个立方体来代表光源(我们已拥有立方体的[顶点数据](http://www.learnopengl.com/code_viewer.php?code=getting-started/cube_vertices)是吧?)。
当然,填一个顶点缓冲对象(VBO),设定一下顶点属性指针和其他一些乱七八糟的东西现在对你来说应该很容易了,所以我们就不再赘述那些步骤了。如果你仍然觉得这很困难,我建议你复习[之前的教程](http://learnopengl-cn.readthedocs.org/zh/latest/01%20Getting%20started/04%20Hello%20Triangle/),并且在继续学习之前先把练习过一遍。
当然,填一个顶点缓冲对象(VBO),设定一下顶点属性指针和其他一些乱七八糟的东西现在对你来说应该很容易了,所以我们就不再赘述那些步骤了。如果你仍然觉得这很困难,我建议你复习[之前的教程](../01 Getting started/04 Hello Triangle.md),并且在继续学习之前先把练习过一遍。
所以,我们首先需要一个顶点着色器来绘制箱子。与上一个教程的顶点着色器相比,容器的顶点位置保持不变(虽然这一次我们不需要纹理坐标),因此顶点着色器中没有新的代码。我们将会使用上一篇教程顶点着色器的精简版:
@@ -166,4 +166,4 @@ glBindVertexArray(0);
如果你在把上述代码片段放到一起编译遇到困难,可以去认真地看看我的[源代码](http://learnopengl.com/code_viewer.php?code=lighting/colors_scene)。你好最自己实现一遍这些操作。
现在我们有了一些关于颜色的知识,并且创建了一个基本的场景能够绘制一些漂亮的光线。你现在可以阅读[下一节](http://learnopengl-cn.readthedocs.org/zh/latest/02%20Lighting/02%20Basic%20Lighting/),真正的魔法即将开始!
现在我们有了一些关于颜色的知识,并且创建了一个基本的场景能够绘制一些漂亮的光线。你现在可以阅读[下一节](02 Basic Lighting.md),真正的魔法即将开始!

View File

@@ -46,11 +46,11 @@ void main()
图左上方有一个光源,它所发出的光线落在物体的一个片段上。我们需要测量这个光线与它所接触片段之间的角度。如果光线垂直于物体表面,这束光对物体的影响会最大化(译注:更亮)。为了测量光线和片段的角度,我们使用一个叫做法向量(Normal Vector)的东西,它是垂直于片段表面的一种向量(这里以黄色箭头表示),我们在后面再讲这个东西。两个向量之间的角度就能够根据点乘计算出来。
你可能记得在[变换](http://learnopengl-cn.readthedocs.org/zh/latest/01%20Getting%20started/07%20Transformations/)那一节教程里我们知道两个单位向量的角度越小它们点乘的结果越倾向于1。当两个向量的角度是90度的时候点乘会变为0。这同样适用于θθ越大光对片段颜色的影响越小。
你可能记得在[变换](../01 Getting started/07 Transformations.md)那一节教程里我们知道两个单位向量的角度越小它们点乘的结果越倾向于1。当两个向量的角度是90度的时候点乘会变为0。这同样适用于θθ越大光对片段颜色的影响越小。
!!! Important
注意,我们使用的是单位向量(Unit Vector长度是1的向量)取得两个向量夹角的余弦值,所以我们需要确保所有的向量都被标准化,否则点乘返回的值就不仅仅是余弦值了(如果你不明白,可以复习[变换](http://learnopengl-cn.readthedocs.org/zh/latest/01%20Getting%20started/07%20Transformations/)那一节的点乘部分)。
注意,我们使用的是单位向量(Unit Vector长度是1的向量)取得两个向量夹角的余弦值,所以我们需要确保所有的向量都被标准化,否则点乘返回的值就不仅仅是余弦值了(如果你不明白,可以复习[变换](../01 Getting started/07 Transformations.md)那一节的点乘部分)。
点乘返回一个标量,我们可以用它计算光线对片段颜色的影响,基于不同片段所朝向光源的方向的不同,这些片段被照亮的情况也不同。
@@ -274,7 +274,7 @@ color = vec4(result, 1.0f);
在顶点着色器中实现的冯氏光照模型叫做Gouraud着色而不是冯氏着色。记住由于插值这种光照连起来有点逊色。冯氏着色能产生更平滑的光照效果。
现在你可以看到着色器的强大之处了。只用很少的信息,着色器就能计算出光照,影响到为我们所有物体的片段颜色。[下一个教程](http://learnopengl-cn.readthedocs.org/zh/latest/02%20Lighting/03%20Materials/),我们会更深入的研究光照模型,看看我们还能做些什么。
现在你可以看到着色器的强大之处了。只用很少的信息,着色器就能计算出光照,影响到为我们所有物体的片段颜色。[下一节](03 Materials.md)中,我们会更深入的研究光照模型,看看我们还能做些什么。
## 练习

View File

@@ -30,7 +30,7 @@ uniform Material material;
![](http://www.learnopengl.com/img/lighting/materials_real_world.png)
如你所见,正确地指定一个物体的材质属性,似乎就是改变我们物体的相关属性的比例。效果显然很引人注目,但是对于大多数真实效果,我们最终需要更加复杂的形状,而不单单是一个立方体。在[面的教程](http://learnopengl-cn.readthedocs.org/zh/latest/03%20Model%20Loading/01%20Assimp/)中,我们会讨论更复杂的形状。
如你所见,正确地指定一个物体的材质属性,似乎就是改变我们物体的相关属性的比例。效果显然很引人注目,但是对于大多数真实效果,我们最终需要更加复杂的形状,而不单单是一个立方体。在[面的教程](../03 Model Loading/01 Assimp.md)中,我们会讨论更复杂的形状。
为一个物体赋予一款正确的材质是非常困难的,这需要大量实验和丰富的经验,所以由于错误的设置材质而毁了物体的画面质量是件经常发生的事。

View File

@@ -16,7 +16,7 @@
我们希望通过某种方式对每个原始像素独立设置diffuse颜色。有可以让我们基于物体原始像素的位置来获取颜色值的系统吗
这可能听起来极其相似,坦白来讲我们使用这样的系统已经有一段时间了。听起来很像在一个[之前的教程](https://learnopengl-cn.readthedocs.org/zh/latest/01%20Getting%20started/06%20Textures/)中谈论的**纹理**它基本就是一个纹理。我们其实是使用同一个潜在原则下的不同名称使用一张图片覆盖住物体以便我们为每个原始像素索引独立颜色值。在光照场景中通过纹理来呈现一个物体的diffuse颜色这个做法被称做**漫反射贴图(Diffuse texture)**(因为3D建模师就是这么称呼这个做法的)。
这可能听起来极其相似,坦白来讲我们使用这样的系统已经有一段时间了。听起来很像在一个[之前的教程](../01 Getting started/06 Textures.md)中谈论的**纹理**它基本就是一个纹理。我们其实是使用同一个潜在原则下的不同名称使用一张图片覆盖住物体以便我们为每个原始像素索引独立颜色值。在光照场景中通过纹理来呈现一个物体的diffuse颜色这个做法被称做**漫反射贴图(Diffuse texture)**(因为3D建模师就是这么称呼这个做法的)。
为了演示漫反射贴图,我们将会使用[下面的图片](http://learnopengl.com/img/textures/container2.png),它是一个有一圈钢边的木箱:
@@ -109,7 +109,7 @@ glBindTexture(GL_TEXTURE_2D, diffuseMap);
## 镜面贴图采样
一个specular贴图和其他纹理一样所以代码和diffuse贴图的代码也相似。确保合理的加载了图片生成一个纹理对象。由于我们在同样的片段着色器中使用另一个纹理采样器我们必须为specular贴图使用一个不同的纹理单元(参见[纹理](http://learnopengl-cn.readthedocs.org/zh/latest/01%20Getting%20started/06%20Textures/)),所以在渲染前让我们把它绑定到合适的纹理单元
一个specular贴图和其他纹理一样所以代码和diffuse贴图的代码也相似。确保合理的加载了图片生成一个纹理对象。由于我们在同样的片段着色器中使用另一个纹理采样器我们必须为specular贴图使用一个不同的纹理单元(参见[纹理](../01 Getting started/06 Textures.md)),所以在渲染前让我们把它绑定到合适的纹理单元
```c++
glUniform1i(glGetUniformLocation(lightingShader.Program, "material.specular"), 1);

View File

@@ -43,7 +43,7 @@ void main()
作为结果的`lightDir`向量被使用在`diffuse`和`specular`计算之前。
为了清晰地强调一个定向光对所有物体都有同样的影响,我们再次访问[坐标系教程](http://learnopengl-cn.readthedocs.org/zh/latest/01%20Getting%20started/08%20Coordinate%20Systems/)结尾部分的箱子场景。例子里我们先定义10个不同的箱子位置为每个箱子生成不同的模型矩阵每个模型矩阵包含相应的本地到世界变换
为了清晰地强调一个定向光对所有物体都有同样的影响,我们再次访问[坐标系教程](../01 Getting started/08 Coordinate Systems.md)结尾部分的箱子场景。例子里我们先定义10个不同的箱子位置为每个箱子生成不同的模型矩阵每个模型矩阵包含相应的本地到世界变换
```c++
for(GLuint i = 0; i < 10; i++)
@@ -317,7 +317,7 @@ specular* = intensity;
看起来好多了。仔细看看内部和外部切光角,尝试创建一个符合你求的聚光。可以在这里找到应用源码,以及片段的源代码。
这样的一个手电筒/聚光类型的灯光非常适合恐怖游戏,结合定向和点光,环境会真的开始被照亮了。[下一个教程](http://learnopengl-cn.readthedocs.org/zh/latest/02%20Lighting/06%20Multiple%20lights/),我们会结合所有我们目前讨论了的光和技巧。
这样的一个手电筒/聚光类型的灯光非常适合恐怖游戏,结合定向和点光,环境会真的开始被照亮了。[下一](06 Multiple lights.md),我们会结合所有我们目前讨论了的光和技巧。
## 练习

View File

@@ -16,23 +16,22 @@
记得去实验一下不同的光照,材质颜色,光照属性,并且试着利用你无穷的创造力创建自己的环境。
在[下一个教程](http://learnopengl-cn.readthedocs.org/zh/latest/03%20Model%20Loading/01%20Assimp/)当中,我们将加入更高级的形状到我们的场景中,这些形状将会在我们之前讨论过的光照模型中非常好看。
在[下一个教程](../03 Model Loading/01 Assimp.md)当中,我们将加入更高级的形状到我们的场景中,这些形状将会在我们之前讨论过的光照模型中非常好看。
## 词汇表
- **颜色向量(Color Vector)**:一个通过红绿蓝(RGB)分量的组合描绘大部分真实颜色的向量. 一个对象的颜色实际上是该对象不能吸收的反射颜色分量。
- **冯氏光照模型(Phong Lighting Model)**:一个通过计算环境,漫反射,和镜面反射分量的值来估计真实光照的模型。
- **环境光照(Ambient Lighting)**:通过给每个没有被光照的物体很小的亮度,使其不是完全黑暗的,从而对全局光照的估计。
- **漫反射着色法(Diffuse Shading)**:光照随着更多的顶点/片段排列在光源上变强. 该方法使用了法向量来计算角度。
- **法向量(Normal Vector)**:一个垂直于平面的单位向量。
- **正规矩阵(Normal Matrix)**一个3x3矩阵, 或者说是没有平移的模型(或者模型观察)矩阵.它也被以某种方式修改(逆转置)从而当应用非统一缩放时保持法向量朝向正确的方向. 否则法向量会在使用非统一缩放时失真。
- **镜面光照(Specular Lighting)**(sets a specular highlight the closer the viewer is looking at the reflection of a light source on a surface.待翻译). 镜面光照是由观察者的方向,光源的方向和设定高光分散量的反光度值三个量共同决定的。
- **镜面光照(Specular Lighting)**当观察者视线靠近光源在表面的反射线时会显示的镜面高光。镜面光照是由观察者的方向,光源的方向和设定高光分散量的反光度值三个量共同决定的。
- **冯氏着色法(Phong Shading)**:冯氏光照模型应用在片段着色器。
- **高氏着色法(Gouraud shading)**:冯氏光照模型应用在顶点着色器上. 在使用很少树木的顶点时会产生明显的瑕疵. 会得到效率提升但是损失了视觉质量。
- **GLSL结构体(GLSL struct)**一个类似于C的结构体用作着色器变量的容器. 大部分时间用来管理输入/输出/uniform。
- **材质(Material)**:一个物体反射的环境,漫反射,镜面反射光照. 这些东西设定了物体的颜色。
- **光照(性质)(Light(properties)**:一个光的环境,漫反射,镜面反射的强度. 可以应用任何颜色值并对每一个冯氏分量(Phong Component)都定义一个光源闪烁的颜色/强度。
- **光照(性质)(Light(properties))**:一个光的环境,漫反射,镜面反射的强度. 可以应用任何颜色值并对每一个冯氏分量(Phong Component)都定义一个光源闪烁的颜色/强度。
- **漫反射贴图(Diffuse Map)**:一个设定了每个片段中漫反射颜色的纹理图片。
- **镜面贴图(Specular Map)**:一个设定了每一个片段的镜面强度/颜色的纹理贴图. 仅在物体的特定区域允许镜面高光。
- **平行光(Directional Light)**:只有一个方向的光源. 它被建模为不管距离有多长所有光束都是平行而且其方向向量在整个场景中保持不变。

View File

@@ -67,4 +67,4 @@ Required library DirectX not found! Install the library (including dev packages)
如果你想要让Assimp使用多线程支持来提高性能你可以使用<b>Boost</b>库来编译 Assimp。在[Boost安装页面](http://assimp.sourceforge.net/lib_html/install.html)你能找到关于Boost的完整安装介绍。
现在你应该已经能够编译Assimp库并链接Assimp到你的工程里去了。下一步[导入完美的3D物件](http://learnopengl-cn.readthedocs.org/zh/latest/03%20Model%20Loading/02%20Mesh/)
现在你应该已经能够编译Assimp库并链接Assimp到你的工程里去了。下一步[导入完美的3D物件](02 Mesh.md)

View File

@@ -6,7 +6,7 @@
翻译 | [Django](http://bullteacher.com/)
校对 | [Geequlim](http://geequlim.com)
在[坐标系的教程](http://learnopengl-cn.readthedocs.org/zh/latest/01%20Getting%20started/08%20Coordinate%20Systems/)中我们呈现了一个3D容器,使用**深度缓冲(Depth Buffer)**,以防止被其他面遮挡的面渲染到前面。在本教程中我们将细致地讨论被深度缓冲(或z-buffer)所存储的**深度值**以及它是如何确定一个片段是否被其他片段遮挡。
在[坐标系的教程](../01 Getting started/08 Coordinate Systems.md)中我们呈现了一个3D容器,使用**深度缓冲(Depth Buffer)**,以防止被其他面遮挡的面渲染到前面。在本教程中我们将细致地讨论被深度缓冲(或z-buffer)所存储的**深度值**以及它是如何确定一个片段是否被其他片段遮挡。
**深度缓冲**就像**颜色缓冲(Color Buffer)**(存储所有的片段颜色:视觉输出)那样存储每个片段的信息,(通常) 和颜色缓冲区有相同的宽度和高度。深度缓冲由窗口系统自动创建并将其深度值存储为 16、 24 或 32 位浮点数。在大多数系统中深度缓冲区为24位。
@@ -87,7 +87,7 @@ $$
\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是我们用来提供到投影矩阵设置可见视图截锥的远近值 (见[坐标系](../01 Getting started/08 Coordinate Systems.md))。方程带内锥截体的深度值 z并将其转换到 [01] 范围。在下面的图给出 z 值和其相应的深度值的关系:
![](http://learnopengl.com/img/advanced/depth_linear_graph.png)

View File

@@ -143,7 +143,7 @@ $$
![](http://learnopengl.com/img/advanced/blending_equation.png)
我们有两个方块,我们希望在红色方块上绘制绿色方块。红色方块会成为颜色(它会先进入颜色缓冲),我们将在红色方块上绘制绿色方块。
我们有两个方块,我们希望在红色方块上绘制绿色方块。红色方块会成为目标颜色(它会先进入颜色缓冲),我们将在红色方块上绘制绿色方块。
那么问题来了我们怎样来设置因子呢我们起码要把绿色方块乘以它的alpha值所以我们打算把\(F_{src}\)设置为源颜色向量的alpha值0.6。接着让目标方块的浓度等于剩下的alpha值。如果最终的颜色中绿色方块的浓度为60%我们就把红色的浓度设为40%1.0 0.6)。所以我们把\(F_{destination}\)设置为1减去源颜色向量的alpha值。方程将变成

View File

@@ -323,7 +323,7 @@ glBindVertexArray(0);
你可以[从这里找到全部源代码](http://learnopengl.com/code_viewer.php?code=advanced/cubemaps_reflection)。
当反射应用于整个物体之上的时候,物体看上去就像有一个像钢和铬这种高反射材质。如果我们加载[模型教程](http://learnopengl-cn.readthedocs.org/zh/latest/03%20Model%20Loading/03%20Model/)中的纳米铠甲模型,我们就会获得一个铬金属制铠甲:
当反射应用于整个物体之上的时候,物体看上去就像有一个像钢和铬这种高反射材质。如果我们加载[模型教程](../03 Model Loading/03 Model.md)中的纳米铠甲模型,我们就会获得一个铬金属制铠甲:
![](http://learnopengl.com/img/advanced/cubemaps_reflection_nanosuit.png)

View File

@@ -94,7 +94,7 @@ glEnable(GL_MULTISAMPLE);
因为GLFW负责创建多采样缓冲开启MSAA非常简单。如果我们打算使用我们自己的帧缓冲来进行离屏渲染那么我们就必须自己生成多采样缓冲了现在我们需要自己负责创建多采样缓冲。
有两种方式可以创建多采样缓冲,并使其成为帧缓冲的附件:纹理附件和渲染缓冲附件,和[帧缓冲教程](http://learnopengl-cn.readthedocs.org/zh/latest/04%20Advanced%20OpenGL/05%20Framebuffers/)里讨论过的普通的附件很相似。
有两种方式可以创建多采样缓冲,并使其成为帧缓冲的附件:纹理附件和渲染缓冲附件,和[帧缓冲教程](05 Framebuffers.md)里讨论过的普通的附件很相似。
### 多采样纹理附件

View File

@@ -84,7 +84,7 @@ void main()
我们可以说上个部分那个朝向正y的法线贴图错误的贴到了表面上。法线贴图被定义在切线空间中所以一种解决问题的方式是计算出一种矩阵把法线从切线空间变换到一个不同的空间这样它们就能和表面法线方向对齐了法线向量都会指向正y方向。切线空间的一大好处是我们可以为任何类型的表面计算出一个这样的矩阵由此我们可以把切线空间的z方向和表面的法线方向对齐。
这种矩阵叫做TBN矩阵这三个字母分别代表tangent、bitangent和normal向量。这是建构这个矩阵所需的向量。要建构这样一个把切线空间转变为不同空间的变异矩阵我们需要三个相互垂直的向量它们沿一个表面的法线贴图对齐于上、右、前这和我们在[摄像机教程](http://learnopengl-cn.readthedocs.org/zh/latest/01%20Getting%20started/09%20Camera/)中做的类似。
这种矩阵叫做TBN矩阵这三个字母分别代表tangent、bitangent和normal向量。这是建构这个矩阵所需的向量。要建构这样一个把切线空间转变为不同空间的变异矩阵我们需要三个相互垂直的向量它们沿一个表面的法线贴图对齐于上、右、前这和我们在[摄像机教程](../01 Getting started/09 Camera.md)中做的类似。
已知上向量是表面的法线向量。右和前向量是切线(Tagent)和副切线(Bitangent)向量。下面的图片展示了一个表面的三个向量:

View File

@@ -65,7 +65,7 @@ while(...) // 游戏循环
}
```
对于每一个片段我们需要储存的数据有:一个**位置**向量、一个**法**向量,一个**颜色**向量一个镜面强度值。所以我们在几何处理阶段中需要渲染场景中所有的对象并储存这些数据分量到G缓冲中。我们可以再次使用**多渲染目标(Multiple Render Targets)**来在一个渲染处理之内渲染多个颜色缓冲,在之前的[泛光教程](http://learnopengl-cn.readthedocs.org/zh/latest/05%20Advanced%20Lighting/07%20Bloom/)中我们也简单地提及了它。
对于每一个片段我们需要储存的数据有:一个**位置**向量、一个**法**向量,一个**颜色**向量一个镜面强度值。所以我们在几何处理阶段中需要渲染场景中所有的对象并储存这些数据分量到G缓冲中。我们可以再次使用**多渲染目标(Multiple Render Targets)**来在一个渲染处理之内渲染多个颜色缓冲,在之前的[泛光教程](07 Bloom.md)中我们也简单地提及了它。
对于几何渲染处理阶段,我们首先需要初始化一个帧缓冲对象,我们很直观的称它为`gBuffer`,它包含了多个颜色缓冲和一个单独的深度渲染缓冲对象(Depth Renderbuffer Object)。对于位置和法向量的纹理,我们希望使用高精度的纹理(每分量16或32位的浮点数),而对于反照率和镜面值,使用默认的纹理(每分量8位浮点数)就够了。
@@ -224,7 +224,7 @@ void main()
你可以在以下位置找到Demo的完整[源代码](http://learnopengl.com/code_viewer.php?code=advanced-lighting/deferred),和几何渲染阶段的[顶点](http://learnopengl.com/code_viewer.php?code=advanced-lighting/deferred_geometry&type=vertex)和[片段](http://learnopengl.com/code_viewer.php?code=advanced-lighting/deferred_geometry&type=fragment)着色器,还有光照渲染阶段的[顶点](http://learnopengl.com/code_viewer.php?code=advanced-lighting/deferred&type=vertex)和[片段](http://learnopengl.com/code_viewer.php?code=advanced-lighting/deferred&type=vertex)着色器。
延迟着色法的其中一个缺点就是它不能进行[混合](http://learnopengl-cn.readthedocs.org/zh/latest/04%20Advanced%20OpenGL/03%20Blending/)(Blending)因为G缓冲中所有的数据都是从一个单独的片段中来的而混合需要对多个片段的组合进行操作。延迟着色法另外一个缺点就是它迫使你对大部分场景的光照使用相同的光照算法你可以通过包含更多关于材质的数据到G缓冲中来减轻这一缺点。
延迟着色法的其中一个缺点就是它不能进行[混合](../04 Advanced OpenGL/03 Blending.md)(Blending)因为G缓冲中所有的数据都是从一个单独的片段中来的而混合需要对多个片段的组合进行操作。延迟着色法另外一个缺点就是它迫使你对大部分场景的光照使用相同的光照算法你可以通过包含更多关于材质的数据到G缓冲中来减轻这一缺点。
为了克服这些缺点(特别是混合),我们通常分割我们的渲染器为两个部分:一个是延迟渲染的部分,另一个是专门为了混合或者其他不适合延迟渲染管线的着色器效果而设计的的正向渲染的部分。为了展示这是如何工作的,我们将会使用正向渲染器渲染光源为一个小立方体,因为光照立方体会需要一个特殊的着色器(会输出一个光照颜色)。

View File

@@ -55,7 +55,7 @@ SSAO需要获取几何体的信息因为我们需要一些方式来确定一
!!! Important
在这个教程中,我们将会在一个简化版本的延迟渲染器([延迟着色法](http://learnopengl-cn.readthedocs.org/zh/latest/05%20Advanced%20Lighting/08%20Deferred%20Shading/)教程中)的基础上实现SSAO所以如果你不知道什么是延迟着色法请先读完那篇教程。
在这个教程中,我们将会在一个简化版本的延迟渲染器([延迟着色法](08 Deferred Shading.md)教程中)的基础上实现SSAO所以如果你不知道什么是延迟着色法请先读完那篇教程。
由于我们已经有了逐片段位置和法线数据(G缓冲中),我们只需要更新一下几何着色器,让它包含片段的线性深度就行了。回忆我们在深度测试那一节学过的知识,我们可以从`gl_FragCoord.z`中提取线性深度:
@@ -114,7 +114,7 @@ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
## 法向半球
我们需要沿着表面法线方向生成大量的样本。就像我们在这个教程的开始介绍的那样,我们想要生成形成半球形的样本。由于对每个表面法线方向生成采样核心非常困难,也不合实际,我们将在[切线空间](http://learnopengl-cn.readthedocs.org/zh/latest/05%20Advanced%20Lighting/04%20Normal%20Mapping/)(Tangent Space)内生成采样核心法向量将指向正z方向。
我们需要沿着表面法线方向生成大量的样本。就像我们在这个教程的开始介绍的那样,我们想要生成形成半球形的样本。由于对每个表面法线方向生成采样核心非常困难,也不合实际,我们将在[切线空间](04 Normal Mapping.md)(Tangent Space)内生成采样核心法向量将指向正z方向。
![](http://learnopengl.com/img/advanced-lighting/ssao_hemisphere.png)

BIN
docs/img/01/05/shaders.mp4 Normal file

Binary file not shown.

BIN
docs/img/01/05/shaders.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
docs/img/01/05/shaders3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
docs/img/01/06/mipmaps.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
docs/img/01/06/textures.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
docs/img/01/06/wall.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB