1
0
mirror of https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git synced 2025-08-22 20:25:28 +08:00

Merge pull request #3 from LearnOpenGL-CN/new-theme

update with master
This commit is contained in:
Aillieo
2017-06-21 21:19:36 +08:00
committed by GitHub
30 changed files with 1461 additions and 708 deletions

View File

@@ -6,11 +6,11 @@ learnopengl.com系列教程的中文翻译目前正在翻译中。
**英文原版**[learnopengl.com](http://learnopengl.com/)
**当前翻译进度**52/63
**目前状态**
**校对进度**13/48
**目前状态**请认领一下In Practice下的章节
- 原文大部分代码都有改变(使用的新的库),需要从头开始重新校对
- PBL 章节和 In Practice 章节下还有几篇教程没有翻译
- 1-8节之后都没有按照新版的格式来排版需要进行整体的修改
## 阅读地址

View File

@@ -11,7 +11,7 @@
<img alt="OpenGL Logo" src="../../img/01/01/opengl.jpg" class="right" />
OpenGL规范严格规定了每个函数该如何执行以及它们的输出值。至于内部具体每个函数是如何实现(Implement)的将由OpenGL库的开发者自行决定这里开发者是指编写OpenGL库的人。因为OpenGL规范并没有规定实现的细节具体的OpenGL库允许使用不同的实现只要其功能和结果与规范相匹配亦即作为用户不会感受到功能上的差异
OpenGL规范严格规定了每个函数该如何执行以及它们的输出值。至于内部具体每个函数是如何实现(Implement)的将由OpenGL库的开发者自行决定这里开发者是指编写OpenGL库的人。因为OpenGL规范并没有规定实现的细节具体的OpenGL库允许使用不同的实现只要其功能和结果与规范相匹配亦即作为用户不会感受到功能上的差异
实际的OpenGL库的开发者通常是显卡的生产商。你购买的显卡所支持的OpenGL版本都为这个系列的显卡专门开发的。当你使用Apple系统的时候OpenGL库是由Apple自身维护的。在Linux下有显卡生产商提供的OpenGL库也有一些爱好者改编的版本。这也意味着任何时候OpenGL库表现的行为与规范规定的不一致时基本都是库的开发者留下的bug。
@@ -23,9 +23,9 @@ OpenGL规范严格规定了每个函数该如何执行以及它们的输出
## 核心模式与立即渲染模式
早期的OpenGL使用<def>立即渲染模式</def>Immediate mode也就是<def>固定渲染管线</def>这个模式下绘制图形很方便。OpenGL的大多数功能都被库隐藏起来开发者很少能控制OpenGL如何进行计算的自由。而开发者迫切希望能有更多的灵活性。随着时间推移规范越来越灵活开发者对绘图细节有了更多的掌控。立即渲染模式确实容易使用和理解但是效率太低。因此从OpenGL3.2开始,规范文档开始废弃立即渲染模式,推出<def>核心模式</def>(Core-profile),这个模式完全移除了旧的特性。
早期的OpenGL使用<def>立即渲染模式</def>Immediate mode也就是<def>固定渲染管线</def>这个模式下绘制图形很方便。OpenGL的大多数功能都被库隐藏起来开发者很少能控制OpenGL如何进行计算的自由。而开发者迫切希望能有更多的灵活性。随着时间推移规范越来越灵活开发者对绘图细节有了更多的掌控。立即渲染模式确实容易使用和理解但是效率太低。因此从OpenGL3.2开始,规范文档开始废弃立即渲染模式,并鼓励开发者在OpenGL的<def>核心模式</def>(Core-profile)下进行开发,这个分支的规范完全移除了旧的特性。
当使用OpenGL的核心模式时OpenGL迫使我们使用现代的函数。当我们试图使用一个已废弃的函数时OpenGL会抛出一个错误并终止绘图。现代函数的优势是更高的灵活性和效率然而也更难于学习。立即渲染模式从OpenGL**实际**运作中抽象掉了很多细节,因而它易于学习的同时也很难去把握OpenGL具体是如何运作的。现代函数要求使用者真正理解OpenGL和图形编程它有一些难度然而提供了更多的灵活性更高的效率更重要的是可以更深入的理解图形编程。
当使用OpenGL的核心模式时OpenGL迫使我们使用现代的函数。当我们试图使用一个已废弃的函数时OpenGL会抛出一个错误并终止绘图。现代函数的优势是更高的灵活性和效率然而也更难于学习。立即渲染模式从OpenGL**实际**运作中抽象掉了很多细节,因此它在易于学习的同时,也很难让人去把握OpenGL具体是如何运作的。现代函数要求使用者真正理解OpenGL和图形编程它有一些难度然而提供了更多的灵活性更高的效率更重要的是可以更深入的理解图形编程。
这也是为什么我们的教程面向OpenGL3.3的核心模式。虽然上手更困难,但这份努力是值得的。
@@ -46,7 +46,7 @@ OpenGL的一大特性就是对扩展(Extension)的支持,当一个显卡公司
```c++
if(GL_ARB_extension_name)
{
// 使用一些新的特性
// 使用硬件支持的全新的现代特性
}
else
{
@@ -62,7 +62,7 @@ OpenGL自身是一个巨大的状态机(State Machine):一系列的变量描
假设当我们想告诉OpenGL去画线段而不是三角形的时候我们通过改变一些上下文变量来改变OpenGL状态从而告诉OpenGL如何去绘图。一旦我们改变了OpenGL的状态为绘制线段下一个绘制命令就会画出线段而不是三角形。
当使用OpenGL的时候我们会遇到一些<def>状态设置</def>函数(State-changing Function),这类函数将会改变上下文。以及<def>状态用</def>函数(State-using Function)这类函数会根据当前OpenGL的状态执行一些操作。只要你记住OpenGL本质上是个大状态机就能更容易理解它的大部分特性。
当使用OpenGL的时候我们会遇到一些<def>状态设置</def>函数(State-changing Function),这类函数将会改变上下文。以及<def>状态使用</def>函数(State-using Function)这类函数会根据当前OpenGL的状态执行一些操作。只要你记住OpenGL本质上是个大状态机就能更容易理解它的大部分特性。
## 对象
@@ -72,33 +72,30 @@ OpenGL库是用C语言写的同时也支持多种语言的派生但其内
```c++
struct object_name {
GLfloat option1;
GLuint option2;
GLchar[] name;
float option1;
int option2;
char[] name;
};
```
!!! important
**基元类型(Primitive Type)**
使用OpenGL时建议使用OpenGL定义的基元类型。比如使用`float`时我们加上前缀`GL`(因此写作`GLfloat`)。`int`、`uint`、`char`、`bool`等等也类似。OpenGL定义的这些GL基元类型的内存布局是与平台无关的而int等基元类型在不同操作系统上可能有不同的内存布局。使用GL基元类型可以保证你的程序在不同的平台上工作一致。
!!! note "译注"
在更新前的教程中一直使用的都是OpenGL的基本类型但由于作者觉得在本教程系列中并没有一个必须使用它们的原因所有的类型都改为了自带类型。但是请仍然记住使用OpenGL的类型的好处是保证了在各平台中每一种类型的大小都是统一的。你也可以使用其它的定宽类型(Fixed-width Type)来实现这一点。
当我们使用一个对象时通常看起来像如下一样把OpenGL上下文看作一个大的结构体
```c++
// OpenGL的状态
struct OpenGL_Context
{
...
object* object_Window_Target;
...
struct OpenGL_Context {
...
object* object_Window_Target;
...
};
```
```c++
// 创建对象
GLuint objectId = 0;
unsigned int objectId = 0;
glGenObject(1, &objectId);
// 绑定对象至上下文
glBindObject(GL_WINDOW_TARGET, objectId);

View File

@@ -4,7 +4,11 @@
---|---
作者 | JoeyDeVries
翻译 | gjy_1992
校对 | Geequlim
校对 | 未校对
!!! note "译注"
注意由于作者对教程做出了更新之前本节使用的是GLEW库但现在改为了使用GLAD库关于GLEW配置的部分现在已经被修改但我仍决定将这部分教程保留起来放到一个历史存档中如果有需要的话可以到[这里](../legacy.md)来查看。
在我们画出出色的效果之前首先要做的就是创建一个OpenGL上下文(Context)和一个用于显示的窗口。然而这些操作在每个系统上都是不一样的OpenGL有目的地从这些操作抽象(Abstract)出去。这意味着我们不得不自己处理创建窗口定义OpenGL上下文以及处理用户输入。
@@ -16,37 +20,37 @@ GLFW是一个专门针对OpenGL的C语言库它提供了一些渲染物体所
<img alt="GLFW Logo" src="../../img/01/02/glfw.png" class="right" />
本节和下一节的目标是建立GLFW环境并保证它恰当地创建OpenGL上下文并显示窗口。这篇教程会一步步从获取、编译、链接GLFW库讲起。我们使用的是Microsoft Visual Studio 2012 IDE操作过程在新版的Visual Studio都是相同的。如果你用的不是Visual Studio或者用的是它的旧版本请不要担心大多数IDE上的操作都是类似的。
本节和下一节的目标是建立GLFW环境并保证它恰当地创建OpenGL上下文并显示窗口。这篇教程会一步步从获取、编译、链接GLFW库讲起。我们使用的是Microsoft Visual Studio 2015 IDE操作过程在新版的Visual Studio都是相同的。如果你用的不是Visual Studio或者用的是它的旧版本请不要担心大多数IDE上的操作都是类似的。
## 构建GLFW
GLFW可以从它官方网站的[下载页](http://www.glfw.org/download.html)上获取。GLFW已经有针对Visual Studio 2012/2013的预编译的二进制版本和相应的头文件,但是为了完整性我们将从编译源代码开始。所以我们需要下载**源代码包**。
GLFW可以从它官方网站的[下载页](http://www.glfw.org/download.html)上获取。GLFW已经有针对Visual Studio 2013/2015的预编译的二进制版本和相应的头文件,但是为了完整性我们将从编译源代码开始。所以我们需要下载**源代码包**。
!!! Attention
当你下载二进制版本请下载32位的版本而不是64位的除非你清楚你在做什么。大部分读者反映64位版本会出现很多奇怪的问题。
如果你要使用预编译的二进制版本的话请下载32位的版本而不是64位的除非你清楚你在做什么。大部分读者反映64位版本会出现很多奇怪的问题。
下载源码包之后,将其解压并打开。我们只需要里面的这些内容:
- 编译生成的库
- **include**文件夹
从源代码编译库可以保证生成的库是兼容你的操作系统和CPU的而预编译的二进制文件可能会出现兼容问题甚至有时候没提供支持你系统的文件。提供源代码所产生的一个问题在于不是每个人都用相同的IDE开发程序因而提供的工程文件可能和一些人的IDE不兼容。所以人们只能从.cpp和.h文件来自己建立工程这是一项笨重的工作。因此诞生了一个叫做CMake的工具。
从源代码编译库可以保证生成的库是兼容你的操作系统和CPU的而预编译的二进制文件可能会出现兼容问题甚至有时候没提供支持你系统的文件。提供源代码所产生的一个问题在于不是每个人都用相同的IDE开发程序因而提供的工程/解决方案文件可能和一些人的IDE不兼容。所以人们只能从.c/.cpp和.h/.hpp文件来自己建立工程/解决方案,这是一项枯燥的工作。因此诞生了一个叫做CMake的工具。
### CMake
CMake是一个工程文件生成工具。用户可以使用预定义好的CMake脚本根据自己的选择像是Visual Studio, Code::Blocks, Eclipse生成不同IDE的工程文件。这允许我们从GLFW源码里创建一个Visual Studio 2012工程文件,之后进行编译。首先,我们需要从[这里](http://www.cmake.org/cmake/resources/software.html)下载安装CMake。我选择的是Win32安装程序。
CMake是一个工程文件生成工具。用户可以使用预定义好的CMake脚本根据自己的选择像是Visual Studio, Code::Blocks, Eclipse生成不同IDE的工程文件。这允许我们从GLFW源码里创建一个Visual Studio 2015工程文件,之后进行编译。首先,我们需要从[这里](http://www.cmake.org/cmake/resources/software.html)下载安装CMake。我选择的是Win32安装程序。
当CMake安装成功后你可以选择从命令行或者GUI启动CMake由于我不想让事情变得太过复杂我们选择用GUI。CMake需要一个源代码目录和一个存放编译结果的目标文件目录。源代码目录我们选择GLFW的源代码的根目录然后我们新建一个 *build* 文件夹,选中作为目标目录。
![](../img/01/02/cmake.png)
之后,点击**Configure(设置)**按钮让CMake读取设置和源代码。接下来,我们选择工程的生成器**Visual Studio 11**因为Visual Studio 2012的内部版本号是11.0。CMake会显示可选的编译选项这里我们使用默认设置,再次点击**Configure(设置)**按钮保存设置。保存之后,点击**Generate(生成)**按钮,生成的工程文件会在你的**build**文件夹中。
在设置完源代码目录和目标目录之后,点击**Configure(设置)**按钮让CMake读取设置和源代码。我们接下来需要选择工程的生成器由于我们使用的是Visual Studio 2015我们选择 **Visual Studio 14** 选项因为Visual Studio 2015的内部版本号是14。CMake会显示可选的编译选项用来配置最终生成的库。这里我们使用默认设置,再次点击**Configure(设置)**按钮保存设置。保存之后,点击**Generate(生成)**按钮,生成的工程文件会在你的**build**文件夹中。
### 编译
在**build**文件夹里可以找到**GLFW.sln**文件用Visual Studio 2012打开。因为CMake已经配置好了项目所以我们直接点击**Build Solution(生成解决方案)**按钮,然后编译的库**glfw3.lib**注意我们用的是第3版就会出现在**src/Debug**文件夹内。
在**build**文件夹里可以找到**GLFW.sln**文件用Visual Studio 2015打开。因为CMake已经配置好了项目所以我们直接点击**Build Solution(生成解决方案)**按钮,然后编译的库**glfw3.lib**注意我们用的是第3版就会出现在**src/Debug**文件夹内。
库生成完毕之后我们需要让IDE知道库和头文件的位置。有两种方法
@@ -57,7 +61,7 @@ CMake是一个工程文件生成工具。用户可以使用预定义好的CMake
## 我们的第一个工程
首先打开Visual Studio创建一个新的项目。如果VS提供了多个选项选择Visual C++,然后选择**Empty Project(空项目)**别忘了给你的项目起一个合适的名字。现在我们终于有一个空的工作空间了开始创建我们第一个OpenGL程序吧
首先打开Visual Studio创建一个新的项目。如果VS提供了多个选项选择Visual C++,然后选择**Empty Project(空项目)**别忘了给你的项目起一个合适的名字。现在我们终于有一个空的工作空间了开始创建我们第一个OpenGL程序吧
## 链接
@@ -93,11 +97,15 @@ CMake是一个工程文件生成工具。用户可以使用预定义好的CMake
#include <GLFW\glfw3.h>
```
!!! Important
对于用GCC编译的Linux用户建议使用这个命令行选项`-lGLEW -lglfw3 -lGL -lX11 -lpthread -lXrandr -lXi`。没有正确链接相应的库会产生 *undefined reference* (未定义的引用) 这个错误。
GLFW的安装与配置就到此为止。
## GLEW
## GLAD
到这里我们仍然有一件事要做。因为OpenGL只是一个标准/规范具体的实现是由驱动开发商针对特定显卡实现的。由于OpenGL驱动版本众多它大多数函数的位置都无法在编译时确定下来需要在运行时查询。任务就落在了开发者身上开发者需要在运行时获取函数地址并将其保存在一个函数指针中供以后使用。取得地址的方法因平台而异在Windows上会是类似这样
到这里还没有结束,我们仍然有一件事要做。因为OpenGL只是一个标准/规范具体的实现是由驱动开发商针对特定显卡实现的。由于OpenGL驱动版本众多它大多数函数的位置都无法在编译时确定下来需要在运行时查询。所以任务就落在了开发者身上,开发者需要在运行时获取函数地址并将其保存在一个函数指针中供以后使用。取得地址的方法[因平台而异](https://www.khronos.org/opengl/wiki/Load_OpenGL_Functions)在Windows上会是类似这样
```c++
// 定义函数原型
@@ -109,40 +117,29 @@ GLuint buffer;
glGenBuffers(1, &buffer);
```
你可以看到代码非常复杂,而且很繁琐,我们需要对每个可能使用的函数都要重复这个过程。幸运的是,有些库能简化此过程,其中**GLEW**是目前最新,也是最流行的库。
你可以看到代码非常复杂,而且很繁琐,我们需要对每个可能使用的函数都要重复这个过程。幸运的是,有些库能简化此过程,其中**GLAD**是目前最新,也是最流行的库。
### 编译和链接GLEW
### 配置GLAD
GLEW是OpenGL Extension Wrangler Library的缩写它能解决我们上面提到的那个繁琐的问题。因为GLEW也是一个库我们同样需要构建并将其链接进工程。GLEW可以从[这里](http://glew.sourceforge.net/index.html)下载你同样可以选择下载二进制版本如果你的目标平台列在上面的话或者下载源码编译步骤和编译GLFW时差不多。记住如果不确定的话选择32位的二进制版本
GLAD是一个[开源](https://github.com/Dav1dde/glad)的库它能解决我们上面提到的那个繁琐的问题。GLAD的配置与大多数的开源库有些许的不同GLAD使用了一个[在线服务](http://glad.dav1d.de/)。在这里我们能够告诉GLAD需要定义的OpenGL版本并且根据这个版本加载所有相关的OpenGL函数
我们使用GLEW的**静态**版本**glew32s.lib**注意这里的“s”将库文件添加到你的库目录将**include**内容添加到你的include目录。接下来在VS的链接器选项里加上**glew32s.lib**。注意GLFW3默认也是编译成了一个静态库
打开GLAD的[在线服务](http://glad.dav1d.de/),将语言(Language)设置为**C/C++**在API选项中选择**3.3**以上的OpenGL(gl)版本我们的教程中将使用3.3版本,但更新的版本也能正常工作)。之后将模式(Profile)设置为**Core**,并且保证**生成加载器**(Generate a loader)的选项是选中的。现在可以先(暂时)忽略拓展(Extensions)中的内容。都选择完之后,点击**生成**(Generate)按钮来生成库文件
GLAD现在应该提供给你了一个zip压缩文件包含两个头文件目录和一个**glad.c**文件。将两个头文件目录(**glad**和**KHR**)复制到你的**Include**文件夹中(或者增加一个额外的项目指向这些目录),并添加**glad.c**文件到你的工程中。
!!! Important
**静态**(Static)链接是指编译时就将库代码里的内容整合进你的二进制文件。优点就是你不需要管理额外的文件了,只需要发布你单独的一个二进制文件就行了。缺点就是你的可执行文件会变得更大,另外当库有升级版本时,你必须重新进行编译整个程序。
**动态**(Dynamic)链接是指一个库通过`.dll`或`.so`的方式存在它的代码与你的二进制文件的代码是分离的。优点是使你的二进制文件大小变小并且更容易升级缺点是你最终发布程序时必须带上这些DLL。
如果你希望静态链接GLEW必须在包含GLEW头文件之前定义预处理器宏`GLEW_STATIC`
经过前面的这些步骤之后,你就应该可以将以下的指令加到你的文件顶部了:
```c++
#define GLEW_STATIC
#include <GL/glew.h>
#include <glad/glad.h>
```
如果你希望动态链接,那么你可以省略这个宏。但是记住使用动态链接的话你需要拷贝一份.DLL文件到你的应用程序目录
!!! Important
对于用GCC编译的Linux用户建议使用这个命令行选项`-lGLEW -lglfw3 -lGL -lX11 -lpthread -lXrandr -lXi`。没有正确链接相应的库会产生 *undefined reference*(未定义的引用) 这个错误。
我们现在成功编译了GLFW和GLEW库我们已经准备好将进入[下一节](03 Hello Window.md)去真正使用GLFW和GLEW来设置OpenGL上下文并创建窗口。记得确保你的头文件和库文件的目录设置正确以及链接器里引用的库文件名正确。如果仍然遇到错误可以先看一下评论有没有人遇到类似的问题请参考额外资源中的例子或者在下面的评论区提问。
点击编译按钮应该不会给你提示任何的错误,到这里我们就已经准备好继续学习[下一节](03 Hello Window.md)去真正使用GLFW和GLAD来设置OpenGL上下文并创建一个窗口了。记得确保你的头文件和库文件的目录设置正确以及链接器里引用的库文件名正确。如果仍然遇到错误可以先看一下评论有没有人遇到类似的问题请参考额外资源中的例子或者在下面的评论区提问
## 附加资源
- [Building applications](http://www.opengl-tutorial.org/miscellaneous/building-your-own-c-application/): 提供了很多编译或链接相关的信息和一大列错误及对应的解决方案
- [GLFW with Code::Blocks](http://wiki.codeblocks.org/index.php?title=Using_GLFW_with_Code::Blocks):使用Code::Blocks IDE编译GLFW
- [Running CMake](http://www.cmake.org/runningcmake/): 简要的介绍如何在Windows和Linux上使用CMake
- [Writing a build system under Linux](http://learnopengl.com/demo/autotools_tutorial.txt): Wouter Verholst写的一个autotools的教程讲的是如何在Linux上编写构建系统尤其是针对这些教程
- [Polytonic/Glitter](https://github.com/Polytonic/Glitter): 一个简单的样板项目它已经提前配置了所有相关的库如果你想要很方便地搞到一个LearnOpenGL教程的范例工程这也是很不错的
- [GLFW: Window Guide](http://www.glfw.org/docs/latest/window_guide.html)GLFW官方的配置GLFW窗口的指南
- [Building applications](http://www.opengl-tutorial.org/miscellaneous/building-your-own-c-application/):提供了很多编译或链接相关的信息和一大列错误及对应的解决方案
- [GLFW with Code::Blocks](http://wiki.codeblocks.org/index.php?title=Using_GLFW_with_Code::Blocks)使用Code::Blocks IDE编译GLFW
- [Running CMake](http://www.cmake.org/runningcmake/)简要的介绍如何在Windows和Linux上使用CMake
- [Writing a build system under Linux](http://learnopengl.com/demo/autotools_tutorial.txt)Wouter Verholst写的一个autotools的教程讲的是如何在Linux上编写构建系统尤其是针对这些教程
- [Polytonic/Glitter](https://github.com/Polytonic/Glitter)一个简单的样板项目它已经提前配置了所有相关的库如果你想要很方便地搞到一个LearnOpenGL教程的范例工程这也是很不错的。

View File

@@ -4,21 +4,18 @@
---|---
作者 | JoeyDeVries
翻译 | Geequlim
校对 | Geequlim
校对 | 未校对
让我们试试能不能让GLFW正常工作。首先新建一个`.cpp`文件,然后把下面的代码粘贴到该文件的最前面。注意,之所以定义`GLEW_STATIC`是因为我们使用的是GLEW静态的链接库。
让我们试试能不能让GLFW正常工作。首先新建一个`.cpp`文件,然后把下面的代码粘贴到该文件的最前面。
```c++
// GLEW
#define GLEW_STATIC
#include <GL/glew.h>
// GLFW
#include <glad/glad.h>
#include <GLFW/glfw3.h>
```
!!! Attention
请确认在包含GLFW的头文件之前包含了GLEW的头文件。在包含glew.h头文件时会引入许多OpenGL必要的头文件(例如`GL/gl.h`),所以需要在包含其它依赖于OpenGL的头文件之前包含GLEW
请确认在包含GLFW的头文件之前包含了GLAD的头文件。GLAD的头文件包含了正确的OpenGL头文件例如`GL/gl.h`所以需要在其它依赖于OpenGL的头文件之前包含GLAD
接下来我们创建<fun>main</fun>函数在这个函数中我们将会实例化GLFW窗口
@@ -29,15 +26,15 @@ int main()
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
//glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
return 0;
}
```
首先我们在main函数中调用<fun>glfwInit</fun>函数来初始化GLFW然后我们可以使用<fun>glfwWindowHint</fun>函数来配置GLFW。<fun>glfwWindowHint</fun>函数的第一个参数代表选项的名称,我们可以从很多以`GLFW_`开头的枚举值中选择;第二个参数接受一个整形,用来设置这个选项的值。该函数的所有的选项以及对应的值都可以在 [GLFW's window handling](http://www.glfw.org/docs/latest/window.html#window_hints) 这篇文档中找到。如果你现在编译你的cpp文件会得到大量的 *undefined reference* (未定义的引用)错误也就是说你并未顺利地链接GLFW库。
由于本站的教程都是基于OpenGL 3.3版本展开讨论的所以我们需要告诉GLFW我们要使用的OpenGL版本是3.3这样GLFW会在创建OpenGL上下文时做出适当的调整。这也可以确保用户在没有适当的OpenGL版本支持的情况下无法运行。我们将主版本号(Major)和次版本号(Minor)都设为3。我们同样明确告诉GLFW我们使用的是核心模式(Core-profile),并且不允许用户调整窗口的大小。在明确告诉GLFW使用核心模式的情况下,使用旧版函数将会导致**invalid operation**(无效操作)的错误,而这不正是一个很好的提醒吗?在我们不小心用了旧函数时报错,就能避免使用一些被废弃的用法了。如果使用的是Mac OS X系统你还需要加下面这行代码到你的初始化代码中这些配置才能起作用
由于本站的教程都是基于OpenGL 3.3版本展开讨论的所以我们需要告诉GLFW我们要使用的OpenGL版本是3.3这样GLFW会在创建OpenGL上下文时做出适当的调整。这也可以确保用户在没有适当的OpenGL版本支持的情况下无法运行。我们将主版本号(Major)和次版本号(Minor)都设为3。我们同样明确告诉GLFW我们使用的是核心模式(Core-profile)明确告诉GLFW我们需要使用核心模式意味着我们只能使用OpenGL功能的一个子集没有我们已不再需要的向后兼容特性。如果使用的是Mac OS X系统你还需要加下面这行代码到你的初始化代码中这些配置才能起作用(将上面的代码解除注释)
```c++
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
@@ -50,8 +47,8 @@ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
接下来我们创建一个窗口对象这个窗口对象存放了所有和窗口相关的数据而且会被GLFW的其他函数频繁地用到。
```c++
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", nullptr, nullptr);
if (window == nullptr)
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
@@ -60,35 +57,32 @@ if (window == nullptr)
glfwMakeContextCurrent(window);
```
<fun>glfwCreateWindow</fun>函数需要窗口的宽和高作为它的前两个参数第三个参数表示这个窗口的名称(标题),这里我们使用`"LearnOpenGL"`,当然你也可以使用你喜欢的名称最后两个参数我们暂时忽略,先设置为空指针就行。它的返回值<fun>GLFWwindow</fun>对象的指针会在其的GLFW操作中使用到。创建完窗口我们就可以通知GLFW将我们窗口的上下文设置为当前线程的主上下文了。
<fun>glfwCreateWindow</fun>函数需要窗口的宽和高作为它的前两个参数第三个参数表示这个窗口的名称(标题),这里我们使用`"LearnOpenGL"`,当然你也可以使用你喜欢的名称最后两个参数我们暂时忽略。这个函数将会返回一个<fun>GLFWwindow</fun>对象,我们会在其的GLFW操作中使用到。创建完窗口我们就可以通知GLFW将我们窗口的上下文设置为当前线程的主上下文了。
## GLEW
## GLAD
在之前的教程中已经提到过GLEW是用来管理OpenGL的函数指针的所以在调用任何OpenGL的函数之前我们需要初始化GLEW
在之前的教程中已经提到过GLAD是用来管理OpenGL的函数指针的所以在调用任何OpenGL的函数之前我们需要初始化GLAD
```c++
glewExperimental = GL_TRUE;
if (glewInit() != GLEW_OK)
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLEW" << std::endl;
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
```
请注意我们在初始化GLEW之前设置<var>glewExperimental</var>变量的值为`GL_TRUE`这样做能让GLEW在管理OpenGL函数指针时更多地使用现代化的技术,如果把它设置为`GL_FALSE`的话可能会在使用OpenGL的核心模式时出现一些问题
我们给GLAD传入了用来加载系统相关的OpenGL函数指针地址的函数。GLFW给我们的是`glfwGetProcAddress`,它根据我们编译的系统定义了正确的函数
## 视口(Viewport)
在我们开始渲染之前还有一件重要的事情要做我们必须告诉OpenGL渲染窗口的尺寸大小这样OpenGL才只能知道怎样相对于窗口大小显示数据和坐标。我们可以通过调用<fun>glViewport</fun>函数来设置窗口的**维度**(Dimension)
## 视口
在我们开始渲染之前还有一件重要的事情要做我们必须告诉OpenGL渲染窗口的尺寸大小即视口(Viewport)这样OpenGL才只能知道怎样根据窗口大小显示数据和坐标。我们可以通过调用<fun>glViewport</fun>函数来设置窗口的**维度**(Dimension)
```c++
int width, height;
glfwGetFramebufferSize(window, &width, &height);
glViewport(0, 0, width, height);
glViewport(0, 0, 800, 600);
```
<fun>glViewport</fun>函数前两个参数控制窗口左下角的位置。第三个和第四个参数控制渲染窗口的宽度和高度(像素)这里我们是直接从GLFW中获取的。我们从GLFW中获取视口的维度而不设置为800*600是为了让它在高DPI的屏幕上比如说Apple的视网膜显示屏也能[正常工作](http://www.glfw.org/docs/latest/window.html#window_size)
<fun>glViewport</fun>函数前两个参数控制窗口左下角的位置。第三个和第四个参数控制渲染窗口的宽度和高度(像素)。
我们实际上也可以将视口的维度设置为比GLFW的维度小这样子之后所有的OpenGL渲染将会在一个更小的窗口中显示这样子的话我们也可以将一些其它元素显示在OpenGL视口之外。
@@ -96,21 +90,46 @@ glViewport(0, 0, width, height);
OpenGL幕后使用<fun>glViewport</fun>中定义的位置和宽高进行2D坐标的转换将OpenGL中的位置坐标转换为你的屏幕坐标。例如OpenGL中的坐标(-0.5, 0.5)有可能(最终)被映射为屏幕中的坐标(200,450)。注意处理过的OpenGL坐标范围只为-1到1因此我们事实上将(-1到1)范围内的坐标映射到(0, 800)和(0, 600)。
然而,当用户改变窗口的大小的时候,视口也应该被调整。我们可以对窗口注册一个回调函数(Callback Function),它会在每次窗口大小被调整的时候被调用。这个回调函数的原型如下:
```c++
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
```
这个帧缓冲大小函数需要一个<fun>GLFWwindow</fun>作为它的第一个参数以及两个整数表示窗口的新维度。每当窗口改变大小GLFW会调用这个函数并填充相应的参数供你处理。
```c++
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
```
我们还需要注册这个函数告诉GLFW我们希望每当窗口调整大小的时候调用这个函数
```c++
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
```
当窗口被第一次显示的时候<fun>framebuffer_size_callback</fun>也会被调用。对于视网膜(Retina)显示屏,<var>width</var>和<var>height</var>都会明显比原输入值更高一点。
我们还可以将我们的函数注册到其它很多的回调函数中。比如说,我们可以创建一个回调函数来处理手柄输入变化,处理错误消息等。我们会在创建窗口之后,渲染循环初始化之前注册这些回调函数。
# 准备好你的引擎
我们可不希望只绘制一个图像之后我们的应用程序就立即退出并关闭窗口。我们希望程序在我们明确地关闭它之前不断绘制图像并能够接受用户输入。因此我们需要在程序中添加一个while循环我们可以把它称之为<def>游戏循环</def>(Game Loop)它能在我们让GLFW退出前一直保持运行。下面几行的代码就实现了一个简单的游戏循环:
我们可不希望只绘制一个图像之后我们的应用程序就立即退出并关闭窗口。我们希望程序在我们主动关闭它之前不断绘制图像并能够接受用户输入。因此我们需要在程序中添加一个while循环我们可以把它称之为<def>渲染循环</def>(Render Loop)它能在我们让GLFW退出前一直保持运行。下面几行的代码就实现了一个简单的渲染循环:
```c++
while(!glfwWindowShouldClose(window))
{
glfwPollEvents();
glfwSwapBuffers(window);
glfwPollEvents();
}
```
- <fun>glfwWindowShouldClose</fun>函数在我们每次循环的开始前检查一次GLFW是否被要求退出如果是的话该函数返回`true`然后游戏循环便结束了,之后为我们就可以关闭应用程序了。
- <fun>glfwPollEvents</fun>函数检查有没有触发什么事件(比如键盘输入、鼠标移动等),然后调用对应的回调函数(可以通过回调方法手动设置)。我们一般在游戏循环的开始调用事件处理函数。
- <fun>glfwSwapBuffers</fun>函数会交换颜色缓冲它是一个储存着GLFW窗口每一个像素颜色的大缓冲它在这一迭代中被用来绘制并且将会作为输出显示在屏幕上。
- <fun>glfwWindowShouldClose</fun>函数在我们每次循环的开始前检查一次GLFW是否被要求退出如果是的话该函数返回`true`然后渲染循环便结束了,之后为我们就可以关闭应用程序了。
- <fun>glfwPollEvents</fun>函数检查有没有触发什么事件(比如键盘输入、鼠标移动等)、更新窗口状态,并调用对应的回调函数(可以通过回调方法手动设置)。
- <fun>glfwSwapBuffers</fun>函数会交换颜色缓冲它是一个储存着GLFW窗口每一个像素颜色的大缓冲),它在这一迭代中被用来绘制,并且将会作为输出显示在屏幕上。
!!! Important
@@ -120,7 +139,7 @@ while(!glfwWindowShouldClose(window))
## 最后一件事
游戏循环结束后我们需要正确释放/删除之前的分配的所有资源。我们可以在<fun>main</fun>函数的最后调用<fun>glfwTerminate</fun>函数来释放GLFW分配的内存
渲染循环结束后我们需要正确释放/删除之前的分配的所有资源。我们可以在<fun>main</fun>函数的最后调用<fun>glfwTerminate</fun>函数来完成
```c++
glfwTerminate();
@@ -131,57 +150,54 @@ return 0;
![](../img/01/03/hellowindow.png)
如果你看见了一个非常无聊的黑色窗口,那么就对了!如果你没得到正确的结果,或者你不知道怎么把所有东西放到一起,请到[这里](http://learnopengl.com/code_viewer.php?code=getting-started/hellowindow)参考源代码。
如果你看见了一个非常无聊的黑色窗口,那么就对了!如果你没得到正确的结果,或者你不知道怎么把所有东西放到一起,请到[这里](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/1.1.hello_window/hello_window.cpp)参考源代码。
如果程序编译有问题请先检查连接器选项是否正确IDE中是否导入了正确的目录前面教程解释过。并且请确认你的代码是否正确直接对照上面提供的源代码就行了。如果还是有问题欢迎评论我或者其他人可能会帮助你的。
## 输入
我们同样也希望能够在GLFW中实现一些键盘控制这可以通过使用GLFW的回调函数(Callback Function)来完成。<def>回调函数</def>事实上是一个函数指针当我们设置好后GLFW会在合适的时候调用它。**按键回调**(KeyCallback)是众多回调函数中的一种。当我们设置了按键回调之后GLFW会在用户有键盘交互时调用它。该回调函数的原型如下所示
我们同样也希望能够在GLFW中实现一些输入控制这可以通过使用GLFW的几个输入函数来完成。我们将会使用GLFW的<fun>glfwGetKey</fun>函数,它需要一个窗口以及一个按键作为输入。这个函数将会返回这个按键是否正在被按下。我们将创建一个<fun>processInput</fun>函数来让所有的输入代码保持整洁。
```c++
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);
```
按键回调函数接受一个<fun>GLFWwindow</fun>指针作为它的第一个参数;第二个整形参数用来表示按下的按键;`action`参数表示这个按键是被按下还是释放最后一个整形参数表示是否有Ctrl、Shift、Alt、Super等按钮的操作。GLFW会在合适的时候调用它并为各个参数传入适当的值。
```c++
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
void processInput(GLFWwindow *window)
{
// 当用户按下ESC键,我们设置window窗口的WindowShouldClose属性为true
// 关闭应用程序
if(key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, GL_TRUE);
}
if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
```
在我们(新创建的)<fun>key_callback</fun>函数中我们检测了键盘是否按下了Escape键。如果的确按下了(不释放),我们使用<fun>glfwSetwindowShouldClose</fun>函数设定`WindowShouldClose`属性`true`从而关闭GLFW。main函数的`while`循环下一次的检测将失败,程序关闭
这里我们检查用户是否按下了返回键(Esc)(如果没有按下,<fun>glfwGetKey</fun>将会返回<var>GLFW_RELEASE</var>。如果用户的确按下了返回键,我们将通过<fun>glfwSetwindowShouldClose</fun>使用把`WindowShouldClose`属性设置为 `true`的方法关闭GLFW。下一次while循环的条件检测将失败,程序将会关闭。
最后一件事就是通过GLFW注册我们的函数至合适的回调代码是这样的:
我们接下来在渲染循环的每一个迭代中调用<fun>processInput</fun>
```c++
glfwSetKeyCallback(window, key_callback);
while (!glfwWindowShouldClose(window))
{
processInput(window);
glfwSwapBuffers(window);
glfwPollEvents();
}
```
除了按键回调函数之外,我们还能我们自己的函数注册其它的回调。例如,我们可以注册一个回调函数来处理窗口尺寸变化、处理一些错误信息等。我们可以在创建窗口之后,开始游戏循环之前注册各种回调函数
这就给我们一个非常简单的方式来检测特定的键是否被按下,并在每一帧做出处理
## 渲染
我们要把所有的渲染(Rendering)操作放到游戏循环中,因为我们想让这些渲染指令在每次游戏循环迭代的时候都能被执行。代码将会是这样的:
我们要把所有的渲染(Rendering)操作放到渲染循环中,因为我们想让这些渲染指令在每次渲染循环迭代的时候都能被执行。代码将会是这样的:
```c++
// 程序循环
// 渲染循环
while(!glfwWindowShouldClose(window))
{
// 检查事件
glfwPollEvents();
// 输入
processInput(window);
// 渲染指令
...
// 交换缓冲
// 检查并调用事件,交换缓冲
glfwPollEvents();
glfwSwapBuffers(window);
}
```
@@ -197,10 +213,10 @@ glClear(GL_COLOR_BUFFER_BIT);
!!! Important
你应该能够回忆起来我们在 *OpenGL* 这节教程的内容,<fun>glClearColor</fun>函数是一个状态设置函数,而<fun>glClear</fun>函数则是一个状态应用的函数
你应该能够回忆起来我们在 *OpenGL* 这节教程的内容,<fun>glClearColor</fun>函数是一个**状态设置**函数,而<fun>glClear</fun>函数则是一个**状态使用**的函数,它使用了当前的状态来获取应该清除为的颜色
![](../img/01/03/hellowindow2.png)
这个程序的完整源代码可以在[这里](http://learnopengl.com/code_viewer.php?code=getting-started/hellowindow2)找到。
这个程序的完整源代码可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/1.2.hello_window_clear/hello_window_clear.cpp)找到。
好了,现在我们已经做好开始在游戏循环中添加许多渲染调用的准备了,但这是[下一节](04 Hello Triangle.md)教程了,这一节的内容已经太多了。
好了,现在我们已经做好开始在渲染循环中添加许多渲染调用的准备了,但这是[下一节](04 Hello Triangle.md)教程了,这一节的内容已经太多了。

View File

@@ -66,10 +66,10 @@
开始绘制图形之前我们必须先给OpenGL输入一些顶点数据。OpenGL是一个3D图形库所以我们在OpenGL中指定的所有坐标都是3D坐标x、y和z。OpenGL不是简单地把**所有的**3D坐标变换为屏幕上的2D像素OpenGL仅当3D坐标在3个轴x、y和z上都为-1.0到1.0的范围内时才处理它。所有在所谓的<def>标准化设备坐标</def>(Normalized Device Coordinates)范围内的坐标才会最终呈现在屏幕上(在这个范围以外的坐标都不会显示)。
由于我们希望渲染一个三角形我们一共要指定三个顶点每个顶点都有一个3D位置。我们会将它们以标准化设备坐标的形式OpenGL的可见区域定义为一个`GLfloat`数组。
由于我们希望渲染一个三角形我们一共要指定三个顶点每个顶点都有一个3D位置。我们会将它们以标准化设备坐标的形式OpenGL的可见区域定义为一个`float`数组。
```c++
GLfloat vertices[] = {
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
@@ -96,13 +96,13 @@ GLfloat vertices[] = {
定义这样的顶点数据以后我们会把它作为输入发送给图形渲染管线的第一个处理阶段顶点着色器。它会在GPU上创建内存用于储存我们的顶点数据还要配置OpenGL如何解释这些内存并且指定其如何发送给显卡。顶点着色器接着会处理我们在内存中指定数量的顶点。
我们通过<def>顶点缓冲对象</def>(Vertex Buffer Objects, VBO)管理这个内存它会在GPU内存(通常被称为显存)中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢所以只要可能我们都要尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后顶点着色器几乎能立即访问顶点这是个非常快的过程。
我们通过<def>顶点缓冲对象</def>(Vertex Buffer Objects, VBO)管理这个内存它会在GPU内存通常被称为显存中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢所以只要可能我们都要尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后顶点着色器几乎能立即访问顶点这是个非常快的过程。
顶点缓冲对象是我们在[OpenGL](01 OpenGL.md)教程中第一个出现的OpenGL对象。就像OpenGL中的其它对象一样这个缓冲有一个独一无二的ID所以我们可以使用<fun>glGenBuffers</fun>函数和一个缓冲ID生成一个VBO对象
```c++
GLuint VBO;
glGenBuffers(1, &VBO);
unsigned int VBO;
glGenBuffers(1, &VBO);
```
OpenGL有很多缓冲对象类型顶点缓冲对象的缓冲类型是<var>GL_ARRAY_BUFFER</var>。OpenGL允许我们同时绑定多个缓冲只要它们是不同的缓冲类型。我们可以使用<fun>glBindBuffer</fun>函数把新创建的缓冲绑定到<var>GL_ARRAY_BUFFER</var>目标上:
@@ -137,24 +137,23 @@ glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
```c++
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 0) in vec3 aPos;
void main()
{
gl_Position = vec4(position.x, position.y, position.z, 1.0);
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
```
可以看到GLSL看起来很像C语言。每个着色器都起始于一个版本声明。OpenGL 3.3以及和更高版本中GLSL版本号和OpenGL的版本是匹配的比如说GLSL 420版本对应于OpenGL 4.2)。我们同样明确表示我们会使用核心模式。
下一步,使用`in`关键字,在顶点着色器中声明所有的输入顶点属性(Input Vertex Attribute)。现在我们只关心位置(Position)数据所以我们只需要一个顶点属性。GLSL有一个向量数据类型它包含1到4个`float`分量包含的数量可以从它的后缀数字看出来。由于每个顶点都有一个3D坐标我们就创建一个`vec3`输入变量<var>position</var>。我们同样也通过`layout (location = 0)`设定了输入变量的位置值(Location)你后面会看到为什么我们会需要这个位置值。
下一步,使用`in`关键字,在顶点着色器中声明所有的输入顶点属性(Input Vertex Attribute)。现在我们只关心位置(Position)数据所以我们只需要一个顶点属性。GLSL有一个向量数据类型它包含1到4个`float`分量包含的数量可以从它的后缀数字看出来。由于每个顶点都有一个3D坐标我们就创建一个`vec3`输入变量<var>aPos</var>。我们同样也通过`layout (location = 0)`设定了输入变量的位置值(Location)你后面会看到为什么我们会需要这个位置值。
!!! Important
**向量(Vector)**
在图形编程中我们经常会使用向量这个数学概念因为它简明地表达了任意空间中的位置和方向并且它有非常有用的数学属性。在GLSL中一个向量有最多4个分量每个分量值都代表空间中的一个坐标它们可以通过`vec.x`、`vec.y`、`vec.z`和`vec.w`来获取。注意`vec.w`分量不是用作表达空间中的位置的我们处理的是3D不是4D而是用在所谓透视划分(Perspective Division)上。我们会在后面的教程中更详细地讨论向量。
在图形编程中我们经常会使用向量这个数学概念因为它简明地表达了任意空间中的位置和方向并且它有非常有用的数学属性。在GLSL中一个向量有最多4个分量每个分量值都代表空间中的一个坐标它们可以通过`vec.x`、`vec.y`、`vec.z`和`vec.w`来获取。注意`vec.w`分量不是用作表达空间中的位置的我们处理的是3D不是4D而是用在所谓透视除法(Perspective Division)上。我们会在后面的教程中更详细地讨论向量。
为了设置顶点着色器的输出,我们必须把位置数据赋值给预定义的<var>gl_Position</var>变量,它在幕后是`vec4`类型的。在<fun>main</fun>函数的最后,我们将<var>gl_Position</var>设置的值会成为该顶点着色器的输出。由于我们的输入是一个3分量的向量我们必须把它转换为4分量的。我们可以把`vec3`的数据作为`vec4`构造器的参数,同时把`w`分量设置为`1.0f`(我们会在后面解释为什么)来完成这一任务。
@@ -164,10 +163,10 @@ void main()
我们已经写了一个顶点着色器源码储存在一个C的字符串中但是为了能够让OpenGL使用它我们必须在运行时动态编译它的源码。
我们首先要做的是创建一个着色器对象注意还是用ID来引用的。所以我们储存这个顶点着色器为`GLuint`,然后用<fun>glCreateShader</fun>创建这个着色器:
我们首先要做的是创建一个着色器对象注意还是用ID来引用的。所以我们储存这个顶点着色器为`unsigned int`,然后用<fun>glCreateShader</fun>创建这个着色器:
```c++
GLuint vertexShader;
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
```
@@ -186,23 +185,23 @@ glCompileShader(vertexShader);
你可能会希望检测在调用<fun>glCompileShader</fun>后编译是否成功了,如果没成功的话,你还会希望知道错误是什么,这样你才能修复它们。检测编译时错误可以通过以下代码来实现:
GLint success;
GLchar infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
首先我们定义一个整型变量来表示是否成功编译,还定义了一个储存错误消息(如果有的话)的容器。然后我们用<fun>glGetShaderiv</fun>检查是否编译成功。如果编译失败,我们会用<fun>glGetShaderInfoLog</fun>获取错误消息,然后打印它。
if(!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
如果编译的时候没有检测到任何错误,顶点着色器就被编译成功了。
## 片段着色器
片段着色器(Fragment Shader)是第二个也是最后一个我们打算创建的用于渲染三角形的着色器。片段着色器全是关于计算你的像素最后的颜色输出。为了让事情更简单,我们的片段着色器将会一直输出橘黄色。
片段着色器(Fragment Shader)是第二个也是最后一个我们打算创建的用于渲染三角形的着色器。片段着色器所做的是计算像素最后的颜色输出。为了让事情更简单,我们的片段着色器将会一直输出橘黄色。
!!! Important
@@ -210,38 +209,37 @@ glCompileShader(vertexShader);
```c++
#version 330 core
out vec4 color;
out vec4 FragColor;
void main()
{
color = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
```
片段着色器只需要一个输出变量这个变量是一个4分量向量它表示的是最终的输出颜色我们应该自己将其计算出来。我们可以用`out`关键字声明输出变量,这里我们命名为<var>color</var>。下面我们将一个alpha值为1.0(1.0代表完全不透明)的橘黄色的`vec4`赋值给颜色输出。
片段着色器只需要一个输出变量这个变量是一个4分量向量它表示的是最终的输出颜色我们应该自己将其计算出来。我们可以用`out`关键字声明输出变量,这里我们命名为<var>FragColor</var>。下面我们将一个alpha值为1.0(1.0代表完全不透明)的橘黄色的`vec4`赋值给颜色输出。
编译片段着色器的过程与顶点着色器类似,只不过我们使用<var>GL_FRAGMENT_SHADER</var>常量作为着色器类型:
```c++
GLuint fragmentShader;
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, null);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
```
两个着色器现在都编译了,剩下的事情是把两个着色器对象链接到一个用来渲染的着色器程序(Shader Program)中。
两个着色器现在都编译了,剩下的事情是把两个着色器对象链接到一个用来渲染的<def>着色器程序</def>(Shader Program)中。
### 着色器程序
着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们<def>链接</def>为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们<def>链接</def>(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。
创建一个程序对象很简单:
```c++
GLuint shaderProgram;
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
```
@@ -261,8 +259,8 @@ glLinkProgram(shaderProgram);
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
...
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
...
}
得到的结果就是一个程序对象,我们可以调用<fun>glUseProgram</fun>函数,用刚创建的程序对象作为它的参数,以激活这个程序对象:
@@ -290,15 +288,15 @@ glDeleteShader(fragmentShader);
![](../img/01/04/vertex_attribute_pointer.png)
- 位置数据被储存为32-bit4字节浮点值。
- 位置数据被储存为324字节浮点值。
- 每个位置包含3个这样的值。
- 在这3个值之间没有空隙或其他值。这几个值在数组中<def>紧密排列</def>。
- 在这3个值之间没有空隙或其他值。这几个值在数组中<def>紧密排列</def>(Tightly Packed)
- 数据中第一个值在缓冲开始的位置。
有了这些信息我们就可以使用<fun>glVertexAttribPointer</fun>函数告诉OpenGL该如何解析顶点数据应用到逐个顶点属性上
```c++
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
```
@@ -308,8 +306,8 @@ glEnableVertexAttribArray(0);
- 第二个参数指定顶点属性的大小。顶点属性是一个`vec3`它由3个值组成所以大小是3。
- 第三个参数指定数据的类型,这里是<var>GL_FLOAT</var>(GLSL中`vec*`都是由浮点数值组成的)。
- 下个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为<var>GL_TRUE</var>所有数据都会被映射到0对于有符号型signed数据是-1到1之间。我们把它设置为<var>GL_FALSE</var>。
- 第五个参数叫做<def>步长</def>(Stride)它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个`GLfloat`之后,我们把步长设置为`3 * sizeof(GLfloat)`。要注意的是由于我们知道这个数组是紧密排列的在两个顶点属性之间没有空隙我们也可以设置为0来让OpenGL决定具体步长是多少只有当数值是紧密排列时才可用。一旦我们有更多的顶点属性我们就必须更小心地定义每个顶点属性之间的间隔我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)
- 最后一个参数的类型是`GLvoid*`,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的<def>偏移量</def>(Offset)。由于位置数据在数组的开头所以这里是0。我们会在后面详细解释这个参数。
- 第五个参数叫做<def>步长</def>(Stride)它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个`float`之后,我们把步长设置为`3 * sizeof(float)`。要注意的是由于我们知道这个数组是紧密排列的在两个顶点属性之间没有空隙我们也可以设置为0来让OpenGL决定具体步长是多少只有当数值是紧密排列时才可用。一旦我们有更多的顶点属性我们就必须更小心地定义每个顶点属性之间的间隔我们在后面会看到更多的例子译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节
- 最后一个参数的类型是`void*`,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的<def>偏移量</def>(Offset)。由于位置数据在数组的开头所以这里是0。我们会在后面详细解释这个参数。
!!! Important
@@ -322,7 +320,7 @@ glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 2. 当我们渲染一个物体时要使用着色器程序
glUseProgram(shaderProgram);
@@ -351,8 +349,8 @@ someOpenGLFunctionThatDrawsOurTriangle();
创建一个VAO和创建一个VBO很类似
```c++
GLuint VAO;
glGenVertexArrays(1, &VAO);
unsigned int VAO;
glGenVertexArrays(1, &VAO);
```
要想使用VAO要做的只是使用<fun>glBindVertexArray</fun>绑定VAO。从绑定之后起我们应该绑定和配置对应的VBO和属性指针之后解绑VAO供之后使用。当我们打算绘制一个物体的时候我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了。这段代码应该看起来像这样
@@ -361,29 +359,22 @@ glGenVertexArrays(1, &VAO);
// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变) :: ..
// 1. 绑定VAO
glBindVertexArray(VAO);
// 2. 把顶点数组复制到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
//4. 解绑VAO
glBindVertexArray(0);
// 2. 把顶点数组复制到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
[...]
// ..:: 绘制代(游戏循环中) :: ..
// 5. 绘制物体
// ..:: 绘制代(渲染循环中) :: ..
// 4. 绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();
glBindVertexArray(0);
```
!!! Attention
通常情况下当我们配置好OpenGL对象以后要解绑它们这样我们才不会在其它地方错误地配置它们。
就这么多了前面做的一切都是等待这一刻一个储存了我们顶点属性配置和应使用的VBO的顶点数组对象。一般当你打算绘制多个物体时你首先要生成/配置所有的VAO和必须的VBO及属性指针)然后储存它们供后面使用。当我们打算绘制物体的时候就拿出相应的VAO绑定它绘制完物体后再解绑VAO。
### 我们一直期待的三角形
@@ -394,16 +385,15 @@ glBindVertexArray(0);
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
```
<fun>glDrawArrays</fun>函数第一个参数是我们打算绘制的OpenGL图元的类型。由于我们在一开始时说过我们希望绘制的是一个三角形这里传递<var>GL_TRIANGLES</var>给它。第二个参数指定了顶点数组的起始索引,我们这里填`0`。最后一个参数指定我们打算绘制多少个顶点,这里是3我们只从我们的数据中渲染一个三角形它只有3个顶点长
<fun>glDrawArrays</fun>函数第一个参数是我们打算绘制的OpenGL图元的类型。由于我们在一开始时说过我们希望绘制的是一个三角形这里传递<var>GL_TRIANGLES</var>给它。第二个参数指定了顶点数组的起始索引,我们这里填`0`。最后一个参数指定我们打算绘制多少个顶点,这里是`3`我们只从我们的数据中渲染一个三角形它只有3个顶点长
现在尝试编译代码,如果弹出了任何错误,回头检查你的代码。如果你编译通过了,你应该看到下面的结果:
![](../img/01/04/hellotriangle.png)
完整的程序源码可以在[这里](http://learnopengl.com/code_viewer.php?code=getting-started/hellotriangle)找到。
完整的程序源码可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/2.1.hello_triangle/hello_triangle.cpp)找到。
如果你的输出和这个看起来不一样,你可能做错了什么。去查看一下源码,检查你是否遗漏了什么东西,或者你也可以在评论区提问。
@@ -412,7 +402,7 @@ glBindVertexArray(0);
在渲染顶点这一话题上我们还有最有一个需要讨论的东西——索引缓冲对象(Element Buffer ObjectEBO也叫Index Buffer ObjectIBO)。要解释索引缓冲对象的工作方式最好还是举个例子假设我们不再绘制一个三角形而是绘制一个矩形。我们可以绘制两个三角形来组成一个矩形OpenGL主要处理三角形。这会生成下面的顶点的集合
```c++
GLfloat vertices[] = {
float vertices[] = {
// 第一个三角形
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
@@ -426,17 +416,17 @@ GLfloat vertices[] = {
可以看到,有几个顶点叠加了。我们指定了`右下角`和`左上角`两次一个矩形只有4个而不是6个顶点这样就产生50%的额外开销。当我们有包括上千个三角形的模型之后这个问题会更糟糕这会产生一大堆浪费。更好的解决方案是只储存不同的顶点并设定绘制这些顶点的顺序。这样子我们只要储存4个顶点就能绘制矩形了之后只要指定绘制的顺序就行了。如果OpenGL提供这个功能就好了对吧
很幸运索引缓冲对象的工作方式正是这样的。和顶点缓冲对象一样EBO也是一个缓冲它专门储存索引OpenGL调用这些顶点的索引来决定该绘制哪个顶点。所谓的<def>索引绘制</def>(Indexed Drawing)正是我们问题的解决方案。首先,我们先要定义(独一无二的)顶点,和绘制出矩形所需的索引:
很幸运索引缓冲对象的工作方式正是这样的。和顶点缓冲对象一样EBO也是一个缓冲它专门储存索引OpenGL调用这些顶点的索引来决定该绘制哪个顶点。所谓的<def>索引绘制</def>(Indexed Drawing)正是我们问题的解决方案。首先,我们先要定义(不重复的)顶点,和绘制出矩形所需的索引:
```c++
GLfloat vertices[] = {
float vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
GLuint indices[] = { // 注意索引从0开始!
unsigned int indices[] = { // 注意索引从0开始!
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
@@ -445,7 +435,7 @@ GLuint indices[] = { // 注意索引从0开始!
你可以看到当时用索引的时候我们只定义了4个顶点而不是6个。下一步我们需要创建索引缓冲对象
```c++
GLuint EBO;
unsigned int EBO;
glGenBuffers(1, &EBO);
```
@@ -453,7 +443,7 @@ glGenBuffers(1, &EBO);
```c++
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
```
要注意的是,我们传递了<var>GL_ELEMENT_ARRAY_BUFFER</var>当作缓冲目标。最后一件要做的事是用<fun>glDrawElements</fun>来替换<fun>glDrawArrays</fun>函数,来指明我们从索引缓冲渲染。使用<fun>glDrawElements</fun>时,我们会使用当前绑定的索引缓冲对象中的索引进行绘制:
@@ -479,22 +469,19 @@ glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
// ..:: 初始化代码 :: ..
// 1. 绑定顶点数组对象
glBindVertexArray(VAO);
// 2. 把我们的顶点数组复制到一个顶点缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 复制我们的索引数组到一个索引缓冲中供OpenGL使用
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 3. 设定顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
// 4. 解绑VAO不是EBO
glBindVertexArray(0);
// 2. 把我们的顶点数组复制到一个顶点缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 复制我们的索引数组到一个索引缓冲中供OpenGL使用
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. 设定顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
[...]
// ..:: 绘制代码(游戏循环中) :: ..
// ..:: 绘制代码(渲染循环中) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
@@ -511,20 +498,21 @@ glBindVertexArray(0);
要想用线框模式绘制你的三角形,你可以通过`glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)`函数配置OpenGL如何绘制图元。第一个参数表示我们打算将其应用到所有的三角形的正面和背面第二个参数告诉我们用线来绘制。之后的绘制调用会一直以线框模式绘制三角形直到我们用`glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)`将其设置回默认模式。
如果你遇到任何错误,回头检查代码,看看是否遗漏了什么。同时,你可以在[这里](http://learnopengl.com/code_viewer.php?code=getting-started/hellotriangle2)找到全部源码,你也可以在评论区自由提问。
如果你遇到任何错误,回头检查代码,看看是否遗漏了什么。同时,你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/2.2.hello_triangle_indexed/hello_triangle_indexed.cpp)找到全部源码,你也可以在评论区自由提问。
如果你像我这样成功绘制出了这个三角形或矩形那么恭喜你你成功地通过了现代OpenGL最难部分之一绘制你自己的第一个三角形。这部分很难因为在可以绘制第一个三角形之前你需要了解很多知识。幸运的是我们现在已经越过了这个障碍接下来的教程会比较容易理解一些。
## 附加资源
- [antongerdelan.net/hellotriangle](http://antongerdelan.net/opengl/hellotriangle.html)Anton Gerdelan的渲染第一个三角形教程。
- [open.gl/drawing](https://open.gl/drawing): Alexander Overvoorde的渲染第一个三角形教程。
- [antongerdelan.net/vertexbuffers](http://antongerdelan.net/opengl/vertexbuffers.html): 顶点缓冲对象的一些深入探讨。
- [open.gl/drawing](https://open.gl/drawing)Alexander Overvoorde的渲染第一个三角形教程。
- [antongerdelan.net/vertexbuffers](http://antongerdelan.net/opengl/vertexbuffers.html)顶点缓冲对象的一些深入探讨。
- [调试](https://learnopengl.com/#!In-Practice/Debugging):这个教程中涉及到了很多步骤,如果你在哪卡住了,阅读一点调试的教程是非常值得的(只需要阅读到调试输出部分)。
# 练习
为了更好的掌握上述概念,我准备了一些练习。建议在继续下一节的学习之前先做完这些练习,确保你对这些知识有比较好的理解。
1. 添加更多顶点到数据中,使用<fun>glDrawArrays</fun>,尝试绘制两个彼此相连的三角形:[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/hello-triangle-exercise1)
2. 创建相同的两个三角形但对它们的数据使用不同的VAO和VBO[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/hello-triangle-exercise2)
3. 创建两个着色器程序,第二个程序使用与第一个不同的片段着色器,输出黄色;再次绘制这两个三角形,其中一个输出为黄色:[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/hello-triangle-exercise3)
1. 添加更多顶点到数据中,使用<fun>glDrawArrays</fun>,尝试绘制两个彼此相连的三角形:[参考解答](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/2.3.hello_triangle_exercise1/hello_triangle_exercise1.cpp)
2. 创建相同的两个三角形但对它们的数据使用不同的VAO和VBO[参考解答](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/2.4.hello_triangle_exercise2/hello_triangle_exercise2.cpp)
3. 创建两个着色器程序,第二个程序使用一个不同的片段着色器,输出黄色;再次绘制这两个三角形,其中一个输出为黄色:[参考解答](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/2.5.hello_triangle_exercise3/hello_triangle_exercise3.cpp)

View File

@@ -20,7 +20,6 @@
```c++
#version version_number
in type in_variable_name;
in type in_variable_name;
@@ -40,7 +39,7 @@ int main()
当我们特别谈论到顶点着色器的时候,每个输入变量也叫<def>顶点属性</def>(Vertex Attribute)。我们能声明的顶点属性是有上限的它一般由硬件来决定。OpenGL确保至少有16个包含4分量的顶点属性可用但是有些硬件或许允许更多的顶点属性你可以查询<var>GL_MAX_VERTEX_ATTRIBS</var>来获取具体的上限:
```c++
GLint nrAttributes;
unsigned int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
```
@@ -79,9 +78,9 @@ vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
你可以使用上面4个字母任意组合来创建一个和原来向量一样长的同类型新向量只要原来向量有那些分量即可然而你不允许在一个`vec2`向量中去获取`.z`元素。我们也可以把一个向量作为一个参数传给不同的向量构造函数,以减少需求参数的数量:
```c++
vec2 vect = vec2(0.5f, 0.7f);
vec4 result = vec4(vect, 0.0f, 0.0f);
vec4 otherResult = vec4(result.xyz, 1.0f);
vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);
```
向量是一种灵活的数据类型,我们可以把用在各种输入和输出上。学完教程你会看到很多新颖的管理向量的例子。
@@ -104,27 +103,27 @@ vec4 otherResult = vec4(result.xyz, 1.0f);
```c++
#version 330 core
layout (location = 0) in vec3 position; // position变量的属性位置值为0
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为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(aPos, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数
vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把输出变量设置为暗红色
}
```
**片段着色器**
```c++
#version 330 core
out vec4 FragColor;
in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)
out vec4 color; // 片段着色器输出的变量名可以任意命名类型必须是vec4
void main()
{
color = vertexColor;
FragColor = vertexColor;
}
```
@@ -142,14 +141,14 @@ void main()
```c++
#version 330 core
out vec4 color;
out vec4 FragColor;
uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量
void main()
{
color = ourColor;
}
FragColor = ourColor;
}
```
我们在片段着色器中声明了一个uniform `vec4`的<var>ourColor</var>并把片段着色器的输出颜色设置为uniform值的内容。因为uniform是全局变量我们可以在任何着色器中定义它们而无需通过顶点着色器作为中介。顶点着色器中不需要这个uniform所以我们不用在那里定义它。
@@ -161,16 +160,16 @@ void main()
这个uniform现在还是空的我们还没有给它添加任何数据所以下面我们就做这件事。我们首先需要找到着色器中uniform属性的索引/位置值。当我们得到uniform的索引/位置值后,我们就可以更新它的值了。这次我们不去给像素传递单独一个颜色,而是让它随着时间改变颜色:
```c++
GLfloat timeValue = glfwGetTime();
GLfloat greenValue = (sin(timeValue) / 2) + 0.5;
GLint vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
float timeValue = glfwGetTime();
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
```
首先我们通过<fun>glfwGetTime()</fun>获取运行的秒数。然后我们使用<fun>sin</fun>函数让颜色在0.0到1.0之间改变,最后将结果储存到<var>greenValue</var>里。
接着,我们用<fun>glGetUniformLocation</fun>查询uniform `ourColor`的位置值。我们为查询函数提供着色器程序和uniform的名字这是我们希望获得的位置值的来源。如果<fun>glGetUniformLocation</fun>返回`-1`就代表没有找到这个位置值。最后,我们可以通过<fun>glUniform4f</fun>函数设置uniform值。注意查询uniform地址不要求你之前使用过着色器程序但是更新一个uniform之前你**必须**先使用程序(调用<fun>glUseProgram</fun>)因为它是在当前激活的着色器程序中设置uniform的。
接着,我们用<fun>glGetUniformLocation</fun>查询uniform <var>ourColor</var>的位置值。我们为查询函数提供着色器程序和uniform的名字这是我们希望获得的位置值的来源。如果<fun>glGetUniformLocation</fun>返回`-1`就代表没有找到这个位置值。最后,我们可以通过<fun>glUniform4f</fun>函数设置uniform值。注意查询uniform地址不要求你之前使用过着色器程序但是更新一个uniform之前你**必须**先使用程序(调用<fun>glUseProgram</fun>)因为它是在当前激活的着色器程序中设置uniform的。
!!! Important
@@ -191,27 +190,30 @@ glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
```c++
while(!glfwWindowShouldClose(window))
{
// 检测并调用事件
glfwPollEvents();
// 输入
processInput(window);
// 渲染
// 清颜色缓冲
// 清颜色缓冲
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 记得激活着色器
glUseProgram(shaderProgram);
// 更新uniform颜色
GLfloat timeValue = glfwGetTime();
GLfloat greenValue = (sin(timeValue) / 2) + 0.5;
GLint vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
float timeValue = glfwGetTime();
float greenValue = sin(timeValue) / 2.0f + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
// 绘制三角形
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
// 交换缓冲并查询IO事件
glfwSwapBuffers(window);
glfwPollEvents();
}
```
@@ -219,7 +221,7 @@ while(!glfwWindowShouldClose(window))
<video src="../../img/01/05/shaders.mp4" controls="controls"/></video>
如果你在哪儿卡住了,可以到[这里](http://www.learnopengl.com/code_viewer.php?code=getting-started/shaders-uniform)查看源码。
如果你在哪儿卡住了,可以到[这里](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/3.1.shaders_uniform/shaders_uniform.cpp)查看源码。
可以看到uniform对于设置一个在渲染迭代中会改变的属性是一个非常有用的工具它也是一个在程序和着色器间数据交互的很好工具但假如我们打算为每个顶点设置一个颜色的时候该怎么办这种情况下我们就不得不声明和顶点数目一样多的uniform了。在这一问题上更好的解决方案是在顶点属性中包含更多的数据这是我们接下来要做的事情。
@@ -228,7 +230,7 @@ while(!glfwWindowShouldClose(window))
在前面的教程中我们了解了如何填充VBO、配置顶点属性指针以及如何把它们都储存到一个VAO里。这次我们同样打算把颜色数据加进顶点数据中。我们将把颜色数据添加为3个float值至<var>vertices</var>数组。我们将把三角形的三个角分别指定为红色、绿色和蓝色:
```c++
GLfloat vertices[] = {
float 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, // 左下
@@ -236,19 +238,19 @@ GLfloat vertices[] = {
};
```
由于现在有更多的数据要发送到顶点着色器,我们有必要去调整一下顶点着色器,使它能够接收颜色值作为一个顶点属性输入。需要注意的是我们用`layout`标识符来把<var>color</var>属性的位置值设置为1
由于现在有更多的数据要发送到顶点着色器,我们有必要去调整一下顶点着色器,使它能够接收颜色值作为一个顶点属性输入。需要注意的是我们用`layout`标识符来把<var>aColor</var>属性的位置值设置为1
```c++
#version 330 core
layout (location = 0) in vec3 position; // 位置变量的属性位置值为 0
layout (location = 1) in vec3 color; // 颜色变量的属性位置值为 1
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为 0
layout (location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1
out vec3 ourColor; // 向片段着色器输出一个颜色
void main()
{
gl_Position = vec4(position, 1.0);
ourColor = color; // 将ourColor设置为我们从顶点数据那里得到的输入颜色
gl_Position = vec4(aPos, 1.0);
ourColor = aColor; // 将ourColor设置为我们从顶点数据那里得到的输入颜色
}
```
@@ -256,12 +258,12 @@ void main()
```c++
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
out vec4 color;
void main()
{
color = vec4(ourColor, 1.0f);
FragColor = vec4(ourColor, 1.0);
}
```
@@ -273,23 +275,23 @@ void main()
```c++
// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)(3* sizeof(GLfloat)));
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float)));
glEnableVertexAttribArray(1);
```
<fun>glVertexAttribPointer</fun>函数的前几个参数比较明了。这次我们配置属性位置值为1的顶点属性。颜色值有3个float那么大我们不去标准化这些值。
由于我们现在有了两个顶点属性,我们不得不重新计算**步长**值。为获得数据队列中下一个属性值(比如位置向量的下个`x`分量我们必须向右移动6个float其中3个是位置值另外3个是颜色值。这使我们的步长值为6乘以float的字节数=24字节
同样这次我们必须指定一个偏移量。对于每个顶点来说位置顶点属性在前所以它的偏移量是0。颜色属性紧随位置数据之后所以偏移量就是`3 * sizeof(GLfloat)`用字节来计算就是12字节。
同样这次我们必须指定一个偏移量。对于每个顶点来说位置顶点属性在前所以它的偏移量是0。颜色属性紧随位置数据之后所以偏移量就是`3 * sizeof(float)`用字节来计算就是12字节。
运行程序你应该会看到如下结果:
![](../img/01/05/shaders3.png)
如果你在哪卡住了,可以在[这里](http://learnopengl.com/code_viewer.php?code=getting-started/shaders-interpolated)查看源码。
如果你在哪卡住了,可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/3.2.shaders_interpolation/shaders_interpolation.cpp)查看源码。
这个图片可能不是你所期望的那种因为我们只提供了3个颜色而不是我们现在看到的大调色板。这是在片段着色器中进行的所谓<def>片段插值</def>(Fragment Interpolation)的结果。当渲染一个三角形时,光栅化(Rasterization)阶段通常会造成比原指定顶点更多的片段。光栅会根据每个片段在三角形形状上所处相对位置决定这些片段的位置。
基于这些位置,它会<def>插值</def>(Interpolate)所有片段着色器的输入变量。比如说我们有一个线段上面的端点是绿色的下面的端点是蓝色的。如果一个片段着色器在线段的70%的位置运行它的颜色输入属性就会是一个绿色和蓝色的线性结合更精确地说就是30%蓝 + 70%绿。
@@ -306,24 +308,30 @@ glEnableVertexAttribArray(1);
#ifndef SHADER_H
#define SHADER_H
#include <glad/glad.h>; // 包含glad来获取所有的必须OpenGL头文件
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
#include <GL/glew.h>; // 包含glew来获取所有的必须OpenGL头文件
class Shader
{
public:
// 程序ID
GLuint Program;
// 构造器读取并构建着色器
Shader(const GLchar* vertexPath, const GLchar* fragmentPath);
// 使用程序
void Use();
// 程序ID
unsigned int ID;
// 构造器读取并构建着色器
Shader(const GLchar* vertexPath, const GLchar* fragmentPath);
// 使用/激活程序
void use();
// uniform工具函数
void setBool(const std::string &name, bool value) const;
void setInt(const std::string &name, int value) const;
void setFloat(const std::string &name, float value) const;
};
#endif
```
@@ -331,14 +339,14 @@ public:
在上面,我们在头文件顶部使用了几个<def>预处理指令</def>(Preprocessor Directives)。这些预处理指令会告知你的编译器只在它没被包含过的情况下才包含和编译这个头文件,即使多个文件都包含了这个着色器头文件。它是用来防止链接冲突的。
着色器类储存了着色器程序的ID。它的构造器需要顶点和片段着色器源代码的文件路径这样我们就可以把源码的文本文件储存在硬盘上了。我们还添加了一个<fun>Use</fun>函数,它其实不那么重要,但是能够显示这个自造类如何让我们的生活变得轻松(虽然只有一点)
着色器类储存了着色器程序的ID。它的构造器需要顶点和片段着色器源代码的文件路径这样我们就可以把源码的文本文件储存在硬盘上了。除此之外,为了让我们的生活更轻松一点,还加入了一些工具函数:<fun>use</fun>用来激活着色器程序,所有的<fun>set...</fun>函数能够查询一个unform的位置值并设置它的值
## 从文件读取
我们使用C++文件流读取着色器内容,储存到几个`string`对象里:
```c++
Shader(const GLchar* vertexPath, const GLchar* fragmentPath)
Shader(const char* vertexPath, const char* fragmentPath)
{
// 1. 从文件路径中获取顶点/片段着色器
std::string vertexCode;
@@ -346,30 +354,30 @@ Shader(const GLchar* vertexPath, const GLchar* fragmentPath)
std::ifstream vShaderFile;
std::ifstream fShaderFile;
// 保证ifstream对象可以抛出异常
vShaderFile.exceptions(std::ifstream::badbit);
fShaderFile.exceptions(std::ifstream::badbit);
vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
try
{
// 打开文件
vShaderFile.open(vertexPath);
fShaderFile.open(fragmentPath);
std::stringstream vShaderStream, fShaderStream;
// 读取文件的缓冲内容到流中
// 读取文件的缓冲内容到数据流中
vShaderStream << vShaderFile.rdbuf();
fShaderStream << fShaderFile.rdbuf();
// 关闭文件
// 关闭文件处理器
vShaderFile.close();
fShaderFile.close();
// 转换流至GLchar数组
vertexCode = vShaderStream.str();
// 转换数据流到string
vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
}
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();
const char* vShaderCode = vertexCode.c_str();
const char* fShaderCode = fragmentCode.c_str();
[...]
```
@@ -377,10 +385,10 @@ Shader(const GLchar* vertexPath, const GLchar* fragmentPath)
```c++
// 2. 编译着色器
GLuint vertex, fragment;
GLint success;
GLchar infoLog[512];
unsigned int vertex, fragment;
int success;
char infoLog[512];
// 顶点着色器
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
@@ -414,7 +422,7 @@ glDeleteShader(vertex);
glDeleteShader(fragment);
```
最后我们也会实现<fun>Use</fun>函数:
<fun>use</fun>函数非常简单
```c++
void Use()
@@ -423,25 +431,42 @@ void Use()
}
```
现在我们就写完了一个完整的着色器类。使用这个着色器类很简单;只要创建一个着色器对象,从那一点开始我们就可以开始使用了
uniform的setter函数也很类似
```c++
Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.frag");
void setBool(const std::string &name, bool value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
}
void setInt(const std::string &name, int value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
}
void setFloat(const std::string &name, float value) const
{
glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
}
```
现在我们就写完了一个完整的[着色器类](https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/shader_s.h)。使用这个着色器类很简单;只要创建一个着色器对象,从那一点开始我们就可以开始使用了:
```c++
Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.fs");
...
while(...)
{
ourShader.Use();
glUniform1f(glGetUniformLocation(ourShader.Program, "someUniform"), 1.0f);
ourShader.use();
ourShader.setFloat("someUniform", 1.0f);
DrawStuff();
}
```
我们把顶点和片段着色器储存为两个叫做`shader.vs`和`shader.frag`的文件。你可以使用自己喜欢的名字命名着色器文件;我自己觉得用`.vs`和`.frag`作为扩展名很直观。
我们把顶点和片段着色器储存为两个叫做`shader.vs`和`shader.fs`的文件。你可以使用自己喜欢的名字命名着色器文件;我自己觉得用`.vs`和`.fs`作为扩展名很直观。
源码:[使用新着色器类的程序](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)
你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/3.3.shaders_class/shaders_class.cpp)找到使用[新着色器](https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/shader_s.h)的源代码。注意你可以点击源码中的着色器文件路径来查看每一个着色器的源代码
# 练习
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)
1. 修改顶点着色器让三角形上下颠倒:[参考解答](https://learnopengl.com/code_viewer.php?code=getting-started/shaders-exercise1)
2. 使用uniform定义一个水平偏移量在顶点着色器中使用这个偏移量把三角形移动到屏幕右侧[参考解答](https://learnopengl.com/code_viewer.php?code=getting-started/shaders-exercise2)
3. 使用`out`关键字把顶点位置输出到片段着色器,并将片段的颜色设置为与顶点位置相等(来看看连顶点位置值都在三角形中被插值的结果)。做完这些后,尝试回答下面的问题:为什么在三角形的左下角是黑的?[参考解答](https://learnopengl.com/code_viewer.php?code=getting-started/shaders-exercise3)

View File

@@ -6,6 +6,10 @@
翻译 | [Django](http://bullteacher.com/)
校对 | Geequlim, [BLumia](https://github.com/blumia/)
!!! note "译注"
注意由于作者对教程做出了更新之前本节使用的是SOIL库但现在改为了使用`stb_image.h`关于SOIL配置的部分现在已经被修改但我仍决定将这部分教程保留起来放到一个历史存档中如果有需要的话可以到[这里](../legacy.md)来查看。
我们已经了解到,我们可以为每个顶点添加颜色来增加图形的细节,从而创建出有趣的图像。但是,如果想让图形看起来更真实,我们就必须有足够多的顶点,从而指定足够多的颜色。这将会产生很多额外开销,因为每个模型都会需求更多的顶点,每个顶点又需求一个颜色属性。
艺术家和程序员更喜欢使用<def>纹理</def>(Texture)。纹理是一个2D图片甚至也有1D和3D的纹理它可以用来添加物体的细节你可以想象纹理是一张绘有砖块的纸无缝折叠贴合到你的3D的房子上这样你的房子看起来就像有砖墙外表了。因为我们可以在一张图片上插入非常多的细节这样就可以让物体非常精细而不用指定额外的顶点。
@@ -29,7 +33,7 @@
纹理坐标看起来就像这样:
```c++
GLfloat texCoords[] = {
float texCoords[] = {
0.0f, 0.0f, // 左下角
1.0f, 0.0f, // 右下角
0.5f, 1.0f // 上中
@@ -131,31 +135,38 @@ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
使用纹理之前要做的第一件事是把它们加载到我们的应用中。纹理图像可能被储存为各种各样的格式,每种都有自己的数据结构和排列,所以我们如何才能把这些图像加载到应用中呢?一个解决方案是选一个需要的文件格式,比如`.PNG`,然后自己写一个图像加载器,把图像转化为字节序列。写自己的图像加载器虽然不难,但仍然挺麻烦的,而且如果要支持更多文件格式呢?你就不得不为每种你希望支持的格式写加载器了。
另一个解决方案也许是一种更好的选择,使用一个支持多种流行格式的图像加载库来为我们解决这个问题。比如说我们要用的SOIL库。
另一个解决方案也许是一种更好的选择,使用一个支持多种流行格式的图像加载库来为我们解决这个问题。比如说我们要用的`stb_image.h`库。
## SOIL
## stb_image.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>`。
下面的教程中,我们会使用一张[木箱](../img/01/06/container.jpg)的图片。要使用SOIL加载图片我们需要使用它的<fun>SOIL_load_image</fun>函数:
`stb_image.h`是[Sean Barrett](https://github.com/nothings)的一个非常流行的单头文件图像加载库,它能够加载大部分流行的文件格式,并且能够很简单得整合到你的工程之中。`stb_image.h`可以在[这里](https://github.com/nothings/stb/blob/master/stb_image.h)下载。下载这一个头文件,将它以`stb_image.h`的名字加入你的工程并另创建一个新的C++文件,输入以下代码:
```c++
int width, height;
unsigned char* image = SOIL_load_image("container.jpg", &width, &height, 0, SOIL_LOAD_RGB);
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
```
函数首先需要输入图片文件的路径。然后需要两个`int`指针作为第二个和第三个参数SOIL会分别返回图片的**宽度**和**高度**到其中。后面我们在生成纹理的时候会用图像的宽度和高度。第四个参数指定图片的**通道**(Channel)数量,但是这里我们只需留为`0`。最后一个参数告诉SOIL如何来加载图片我们只关注图片的`RGB`值。结果会储存为一个很大的char/byte数组
通过定义<var>STB_IMAGE_IMPLEMENTATION</var>,预处理器会修改头文件,让其只包含相关的函数定义源码,等于是将这个头文件变为一个 `.cpp` 文件了。现在只需要在你的程序中包含`stb_image.h`并编译就可以了
下面的教程中,我们会使用一张[木箱](../img/01/06/container.jpg)的图片。要使用`stb_image.h`加载图片,我们需要使用它的<fun>stbi_load</fun>函数:
```c++
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
```
这个函数首先接受一个图像文件的位置作为输入。接下来它需要三个`int`作为它的第二、第三和第四个参数,`stb_image.h`将会用图像的**宽度**、**高度**和**颜色通道的个数**填充这三个变量。我们之后生成纹理的时候会用到的图像的宽度和高度的。
## 生成纹理
和之前生成的OpenGL对象一样纹理也是使用ID引用的。让我们来创建一个
```c++
GLuint texture;
unsigned int texture;
glGenTextures(1, &texture);
```
<fun>glGenTextures</fun>函数首先需要输入生成纹理的数量,然后把它们储存在第二个参数的`GLuint`数组中(我们的例子中只是一个单独的`GLuint`),就像其他对象一样,我们需要绑定它,让之后任何的纹理指令都可以配置当前绑定的纹理:
<fun>glGenTextures</fun>函数首先需要输入生成纹理的数量,然后把它们储存在第二个参数的`unsigned int`数组中(我们的例子中只是单独的一个`unsigned int`),就像其他对象一样,我们需要绑定它,让之后任何的纹理指令都可以配置当前绑定的纹理:
```c++
glBindTexture(GL_TEXTURE_2D, texture);
@@ -164,7 +175,7 @@ glBindTexture(GL_TEXTURE_2D, texture);
现在纹理已经绑定了,我们可以使用前面载入的图片数据生成一个纹理了。纹理可以通过<fun>glTexImage2D</fun>来生成:
```c++
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
```
@@ -174,34 +185,42 @@ glGenerateMipmap(GL_TEXTURE_2D);
- 第二个参数为纹理指定多级渐远纹理的级别如果你希望单独手动设置每个多级渐远纹理的级别的话。这里我们填0也就是基本级别。
- 第三个参数告诉OpenGL我们希望把纹理储存为何种格式。我们的图像只有`RGB`值,因此我们也把纹理储存为`RGB`值。
- 第四个和第五个参数设置最终的纹理的宽度和高度。我们之前加载图像的时候储存了它们,所以我们使用对应的变量。
- 下个参数应该总是被设为`0`(历史遗留问题)。
- 下个参数应该总是被设为`0`(历史遗留问题)。
- 第七第八个参数定义了源图的格式和数据类型。我们使用RGB值加载这个图像并把它们储存为`char`(byte)数组,我们将会传入对应值。
- 最后一个参数是真正的图像数据。
当调用<fun>glTexImage2D</fun>时,当前绑定的纹理对象就会被附加上纹理图像。然而,目前只有基本级别(Base-level)的纹理图像被加载了,如果要使用多级渐远纹理,我们必须手动设置所有不同的图像(不断递增第二个参数)。或者,直接在生成纹理之后调用<fun>glGenerateMipmap</fun>。这会为当前绑定的纹理自动生成所有需要的多级渐远纹理。
生成了纹理和相应的多级渐远纹理后,释放图像的内存并解绑纹理对象是一个很好的习惯。
生成了纹理和相应的多级渐远纹理后,释放图像的内存是一个很好的习惯。
```c++
SOIL_free_image_data(image);
glBindTexture(GL_TEXTURE_2D, 0);
stbi_image_free(data);
```
生成一个纹理的过程应该看起来像这样:
```c++
GLuint texture;
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// 为当前绑定的纹理对象设置环绕、过滤方式
...
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 加载并生成纹理
int width, height;
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);
glBindTexture(GL_TEXTURE_2D, 0);
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
if (data)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);
```
## 应用纹理
@@ -209,7 +228,7 @@ glBindTexture(GL_TEXTURE_2D, 0);
后面的这部分我们会使用<fun>glDrawElements</fun>绘制[「你好,三角形」](04 Hello Triangle.md)教程最后一部分的矩形。我们需要告知OpenGL如何采样纹理所以我们必须使用纹理坐标更新顶点数据
```c++
GLfloat vertices[] = {
float 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, // 右下
@@ -223,46 +242,46 @@ GLfloat vertices[] = {
![](../img/01/06/vertex_attribute_pointer_interleaved_textures.png)
```c++
glVertexAttribPointer(2, 2, GL_FLOAT,GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat)));
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
```
注意,我们同样需要调整前面两个顶点属性的步长参数为`8 * sizeof(GLfloat)`。
注意,我们同样需要调整前面两个顶点属性的步长参数为`8 * sizeof(float)`。
接着我们需要调整顶点着色器使其能够接受顶点坐标为一个顶点属性,并把坐标传给片段着色器:
```c++
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 color;
layout (location = 2) in vec2 texCoord;
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;
out vec3 ourColor;
out vec2 TexCoord;
void main()
{
gl_Position = vec4(position, 1.0f);
ourColor = color;
TexCoord = texCoord;
gl_Position = vec4(aPos, 1.0);
ourColor = aColor;
TexCoord = aTexCoord;
}
```
片段着色器应该把输出变量`TexCoord`作为输入变量。
片段着色器应该接下来会把输出变量`TexCoord`作为输入变量。
片段着色器也应该能访问纹理对象但是我们怎样能把纹理对象传给片段着色器呢GLSL有一个供纹理对象使用的内建数据类型叫做<def>采样器</def>(Sampler),它以纹理类型作为后缀,比如`sampler1D`、`sampler3D`,或在我们的例子中的`sampler2D`。我们可以简单声明一个`uniform sampler2D`把一个纹理添加到片段着色器中稍后我们会把纹理赋值给这个uniform。
```c++
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
out vec4 color;
uniform sampler2D ourTexture;
void main()
{
color = texture(ourTexture, TexCoord);
FragColor = texture(ourTexture, TexCoord);
}
```
@@ -274,19 +293,22 @@ void main()
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
```
如果你跟着这个教程正确地做完了,你会看到下面的图像:
![](../img/01/06/textures2.png)
如果你的矩形是全黑或全白的你可能在哪儿做错了什么。检查你的着色器日志,并尝试对比一下[源码](http://learnopengl.com/code_viewer.php?code=getting-started/textures)。
如果你的矩形是全黑或全白的你可能在哪儿做错了什么。检查你的着色器日志,并尝试对比一下[源码](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/4.1.textures/textures.cpp)。
!!! attention
如果你的纹理代码不能正常工作或者显示是全黑请继续阅读并一直跟进我们的代码到最后的例子它是应该能够工作的。在一些驱动中必须要对每个采样器uniform都附加上纹理单元才可以这个会在下面介绍。
我们还可以把得到的纹理颜色与顶点颜色混合,来获得更有趣的效果。我们只需把纹理颜色与顶点颜色在片段着色器中相乘来混合二者的颜色:
```c++
color = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0f);
FragColor = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0);
```
最终的效果应该是顶点颜色和纹理颜色的混合色:
@@ -302,7 +324,7 @@ color = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0f);
纹理单元的主要目的是让我们在着色器中可以使用多于一个的纹理。通过把纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们首先激活对应的纹理单元。就像<fun>glBindTexture</fun>一样,我们可以使用<fun>glActiveTexture</fun>激活纹理单元,传入我们需要使用的纹理单元:
```c++
glActiveTexture(GL_TEXTURE0); //在绑定纹理之前先激活纹理单元
glActiveTexture(GL_TEXTURE0); // 在绑定纹理之前先激活纹理单元
glBindTexture(GL_TEXTURE_2D, texture);
```
@@ -323,7 +345,7 @@ uniform sampler2D ourTexture2;
void main()
{
color = mix(texture(ourTexture1, TexCoord), texture(ourTexture2, TexCoord), 0.2);
FragColor = mix(texture(ourTexture1, TexCoord), texture(ourTexture2, TexCoord), 0.2);
}
```
@@ -336,42 +358,47 @@ void main()
```c++
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glUniform1i(glGetUniformLocation(ourShader.Program, "ourTexture1"), 0);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
glUniform1i(glGetUniformLocation(ourShader.Program, "ourTexture2"), 1);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
```
注意,我们使用<fun>glUniform1i</fun>设置uniform采样器的位置值或者说纹理单元。通过<fun>glUniform1i</fun>的设置我们保证每个uniform采样器对应着正确的纹理单元。你应该能得到下面的结果
我们还要通过使用<fun>glUniform1i</fun>设置每个采样器的方式告诉OpenGL每个着色器采样器属于哪个纹理单元。我们只需要设置一次即可所以这个会放在渲染循环的前面
```c++
ourShader.use(); // 别忘记在激活着色器前先设置uniform
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0); // 手动设置
ourShader.setInt("texture2", 1); // 或者使用着色器类设置
while(...)
{
[...]
}
```
通过使用<fun>glUniform1i</fun>设置采样器我们保证了每个uniform采样器对应着正确的纹理单元。你应该能得到下面的结果
![](../img/01/06/textures_combined.png)
你可能注意到纹理上下颠倒了这是因为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>,自己做纹理的生成。
你可能注意到纹理上下颠倒了这是因为OpenGL要求y轴`0.0`坐标是在图片的底部的但是图片的y轴`0.0`坐标通常在顶部。很幸运,`stb_image.h`能够在图像加载时帮助我们翻转y轴只需要在加载任何图像前加入以下语句即可
所以修复我们的小问题,有两个选择:
```c++
stbi_set_flip_vertically_on_load(true);
```
1. 我们可以改变顶点数据的纹理坐标,翻转`y`值用1减去y坐标
2. 我们可以编辑顶点着色器来自动翻转`y`坐标,替换`TexCoord`的值为`TexCoord = vec2(texCoord.x, 1.0f - texCoord.y);`。
!!! Attention
上面提供的解决方案仅仅通过一些黑科技让图片翻转。它们在大多数情况下都能正常工作然而实际上这种方案的效果取决于你的实现和纹理所以最好的解决方案是调整你的图片加载器或者以一种y原点符合OpenGL需求的方式编辑你的纹理图像。
如果你编辑了顶点数据,在顶点着色器中翻转了纵坐标,你会得到下面的结果:
在让`stb_image.h`在加载图片时翻转y轴之后你就应该能够获得下面的结果了
![](../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)着色器
如果你看到了一个开心的箱子,你就做对了。你可以对比一下[源代码](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/4.2.textures_combined/textures_combined.cpp)
## 练习
为了更熟练地使用纹理,建议在继续之后的学习之前做完这些练习:
- 修改片段着色器,**仅**让笑脸图案朝另一个方向看,[参考解答](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)。
- 修改片段着色器,**仅**让笑脸图案朝另一个方向看,[参考解答](https://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise1)
- 尝试用不同的纹理环绕方式,设定一个从`0.0f`到`2.0f`范围内的(而不是原来的`0.0f`到`1.0f`纹理坐标。试试看能不能在箱子的角落放置4个笑脸[参考解答](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/4.3.textures_exercise2/textures_exercise2.cpp)[结果](../img/01/06/textures_exercise2.png)。记得一定要试试其的环绕方式。
- 尝试在矩形上只显示纹理图像的中间一部分,修改纹理坐标,达到能看见单个的像素的效果。尝试使用<var>GL_NEAREST</var>的纹理过滤方式让像素显示得更清晰:[参考解答](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/4.4.textures_exercise3/textures_exercise3.cpp)
- 使用一个uniform变量作为<fun>mix</fun>函数的第三个参数来改变两个纹理可见度,使用上和下键来改变箱子或笑脸的可见度:[参考解答](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/4.5.textures_exercise4/textures_exercise4.cpp)。

View File

@@ -402,34 +402,27 @@ std::cout << vec.x << vec.y << vec.z << std::endl;
```c++
glm::mat4 trans;
trans = glm::rotate(trans, 90.0f, glm::vec3(0.0, 0.0, 1.0));
trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));
trans = glm::rotate(trans, glm::radians(90.0f), glm::vec3(0.0, 0.0, 1.0));
trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));
```
首先我们把箱子在每个轴都缩放到0.5倍然后沿z轴旋转90度。注意有纹理的那面矩形是在XY平面上的所以我们需要把它绕着z轴旋转。因为我们把这个矩阵传递给了GLM的每个函数GLM会自动将矩阵相乘返回的结果是一个包括了多个变换的变换矩阵。
!!! Attention
有些GLM版本接收的是弧度而不是角度这种情况下你可以用`glm::radians(90.0f)`将角度转换为弧度。
首先我们把箱子在每个轴都缩放到0.5倍然后沿z轴旋转90度。GLM希望它的角度是弧度制的(Radian),所以我们使用`glm::radians`将角度转化为弧度。注意有纹理的那面矩形是在XY平面上的所以我们需要把它绕着z轴旋转。因为我们把这个矩阵传递给了GLM的每个函数GLM会自动将矩阵相乘返回的结果是一个包括了多个变换的变换矩阵。
下一个大问题是如何把矩阵传递给着色器我们在前面简单提到过GLSL里也有一个`mat4`类型。所以我们将修改顶点着色器让其接收一个`mat4`的uniform变量然后再用矩阵uniform乘以位置向量
```c++
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 color;
layout (location = 2) in vec2 texCoord;
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec3 ourColor;
out vec2 TexCoord;
uniform mat4 transform;
void main()
{
gl_Position = transform * vec4(position, 1.0f);
ourColor = color;
TexCoord = vec2(texCoord.x, 1.0 - texCoord.y);
gl_Position = transform * vec4(aPos, 1.0f);
TexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y);
}
```
@@ -440,7 +433,7 @@ void main()
在把位置向量传给<var>gl_Position</var>之前我们先添加一个uniform并且将其与变换矩阵相乘。我们的箱子现在应该是原来的二分之一大小并向左旋转了90度。当然我们仍需要把变换矩阵传递给着色器
```c++
GLuint transformLoc = glGetUniformLocation(ourShader.Program, "transform");
unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));
```
@@ -455,7 +448,7 @@ glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));
```c++
glm::mat4 trans;
trans = glm::translate(trans, glm::vec3(0.5f, -0.5f, 0.0f));
trans = glm::rotate(trans,(GLfloat)glfwGetTime() * 50.0f, glm::vec3(0.0f, 0.0f, 1.0f));
trans = glm::rotate(trans, (float)glfwGetTime(), glm::vec3(0.0f, 0.0f, 1.0f));
```
要记住的是前面的例子中我们可以在任何地方声明变换矩阵,但是现在我们必须在每一次迭代中创建它,从而保证我们能够不断更新旋转角度。这也就意味着我们不得不在每次游戏循环的迭代中重新创建变换矩阵。通常在渲染场景的时候,我们也会有多个需要在每次渲染迭代中都用新值重新创建的变换矩阵
@@ -466,13 +459,17 @@ trans = glm::rotate(trans,(GLfloat)glfwGetTime() * 50.0f, glm::vec3(0.0f, 0.0f,
<video src="../../img/01/07/transformations.mp4" controls="controls"></video>
这就是我们刚刚做到的!一个位移过的箱子,它会一直转,一个变换矩阵就做到了!现在你可以明白为什么矩阵在图形领域是一个如此重要的工具了。我们可以定义一个无限数量的变换,把它们组合为仅仅一个矩阵,如果愿意的话我们还可以重复使用它。在着色器中使用矩阵可以省去重新定义顶点数据的功夫,它也能够节省处理时间,因为我们没有一直重新发送我们的数据(这是个非常慢的过程)。
这就是我们刚刚做到的!一个位移过的箱子,它会一直转,一个变换矩阵就做到了!现在你可以明白为什么矩阵在图形领域是一个如此重要的工具了。我们可以定义无限数量的变换,把它们组合为仅仅一个矩阵,如果愿意的话我们还可以重复使用它。在着色器中使用矩阵可以省去重新定义顶点数据的功夫,它也能够节省处理时间,因为我们没有一直重新发送我们的数据(这是个非常慢的过程)。
如果你没有得到正确的结果,或者你有哪儿不清楚的地方。可以看[源码](http://learnopengl.com/code_viewer.php?code=getting-started/transformations)和[顶点](http://learnopengl.com/code_viewer.php?code=getting-started/transformations&type=vertex)、[片段](http://learnopengl.com/code_viewer.php?code=getting-started/transformations&type=fragment)着色器
如果你没有得到正确的结果,或者你有哪儿不清楚的地方。可以看[源码](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/5.1.transformations/transformations.cpp)
下一节中我们会讨论怎样使用矩阵为顶点定义不同的坐标空间。这将是我们进入实时3D图像的第一步
## 拓展阅读
- [线性代数的本质](https://www.youtube.com/playlist?list=PLZHQObOWTQDPD3MizzM2xVFitgF8hE_ab)Grant Sanderson制作的非常棒的视频教程系列它讨论了变换和线性代数内在的数学本质[中文字幕版本](http://space.bilibili.com/88461692#!/channel/detail?cid=9450))。
## 练习
- 使用应用在箱子上的最后一个变换,尝试将其改变为先旋转,后位移。看看发生了什么,试着想想为什么会发生这样的事情:[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/transformations-exercise1)
- 尝试再次调用<fun>glDrawElements</fun>画出第二个箱子,**只**使用变换将其摆放在不同的位置。让这个箱子被摆放在窗口的左上角,并且会不断的缩放(而不是旋转)。`sin`函数在这里会很有用,不过注意使用`sin`函数时应用负值会导致物体被翻转:[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/transformations-exercise2)
- 使用应用在箱子上的最后一个变换,尝试将其改变为先旋转,后位移。看看发生了什么,试着想想为什么会发生这样的事情:[参考解答](https://learnopengl.com/code_viewer.php?code=getting-started/transformations-exercise1)
- 尝试再次调用<fun>glDrawElements</fun>画出第二个箱子,**只**使用变换将其摆放在不同的位置。让这个箱子被摆放在窗口的左上角,并且会不断的缩放(而不是旋转)。`sin`函数在这里会很有用,不过注意使用`sin`函数时应用负值会导致物体被翻转[参考解答](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/5.2.transformations_exercise2/transformations_exercise2.cpp)

View File

@@ -103,12 +103,12 @@ $$
out = \begin{pmatrix} x /w \\ y / w \\ z / w \end{pmatrix}
$$
顶点坐标的每个分量都会除以它的w分量距离观察者越远顶点坐标就会越小。这是也是w分量非常重要的另一个原因它能够帮助我们进行透视投影。最后的结果坐标就是处于标准化设备空间中的。如果你对正射投影矩阵和透视投影矩阵是如何计算的很感兴趣且不会对数学感到恐惧的话我推荐这篇由Songho写的[]文章](http://www.songho.ca/opengl/gl_projectionmatrix.html)。
顶点坐标的每个分量都会除以它的w分量距离观察者越远顶点坐标就会越小。这是也是w分量非常重要的另一个原因它能够帮助我们进行透视投影。最后的结果坐标就是处于标准化设备空间中的。如果你对正射投影矩阵和透视投影矩阵是如何计算的很感兴趣且不会对数学感到恐惧的话我推荐这篇由Songho写的[文章](http://www.songho.ca/opengl/gl_projectionmatrix.html)。
在GLM中可以这样创建一个透视投影矩阵
```c++
glm::mat4 proj = glm::perspective(45.0f, (float)width/(float)height, 0.1f, 100.0f);
glm::mat4 proj = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);
```
同样,`glm::perspective`所做的其实就是创建了一个定义了可视空间的大**平截头体**,任何在这个平截头体以外的东西最后都不会出现在裁剪空间体积内,并且将会受到裁剪。一个透视平截头体可以被看作一个不均匀形状的箱子,在这个箱子内部的每个坐标都会被映射到裁剪空间上的一个点。下面是一张透视平截头体的图片:
@@ -153,12 +153,12 @@ $$
```c++
glm::mat4 model;
model = glm::rotate(model, -55.0f, glm::vec3(1.0f, 0.0f, 0.0f));
model = glm::rotate(model, glm::radians(-55.0f), glm::vec3(1.0f, 0.0f, 0.0f));
```
通过将顶点坐标乘以这个模型矩阵,我们将该顶点坐标变换到世界坐标。我们的平面看起来就是在地板上,代表全局世界里的平面。
接下来我们需要创建一个观察矩阵。我们想要在场景里面稍微往后移动,以使得物体变成可见的(当在世界空间时,我们位于原点(0,0,0))。要想在场景里面移动,先想一想下面这个问题
接下来我们需要创建一个观察矩阵。我们想要在场景里面稍微往后移动,以使得物体变成可见的(当在世界空间时,我们位于原点(0,0,0))。要想在场景里面移动,先仔细想一想下面这个句子
- 将摄像机向后移动,和将整个场景向前移动是一样的。
@@ -186,25 +186,21 @@ model = glm::rotate(model, -55.0f, glm::vec3(1.0f, 0.0f, 0.0f));
```c++
glm::mat4 view;
// 注意,我们将矩阵向我们要进行移动场景的反方向移动。
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
```
最后我们需要做的是定义一个投影矩阵。我们希望在场景中使用透视投影,所以像这样声明一个投影矩阵:
```c++
glm::mat4 projection;
projection = glm::perspective(45.0f, screenWidth / screenHeight, 0.1f, 100.0f);
projection = glm::perspective(glm::radians(45.0f), screenWidth / screenHeight, 0.1f, 100.0f);
```
!!! attention
再重复一遍在glm中指定角度的时候要注意。这里我们将参数fov设置为**45**度但有些GLM的实现是将fov当成弧度在这种情况你需要使用`glm::radians(45.0)`来设置。
既然我们已经创建了变换矩阵我们应该将它们传入着色器。首先让我们在顶点着色器中声明一个uniform变换矩阵然后将它乘以顶点坐标
```c++
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 0) in vec3 aPos;
...
uniform mat4 model;
uniform mat4 view;
@@ -212,8 +208,8 @@ uniform mat4 projection;
void main()
{
// 注意从右向左读
gl_Position = projection * view * model * vec4(position, 1.0f);
// 注意乘法要从右向左读
gl_Position = projection * view * model * vec4(aPos, 1.0);
...
}
```
@@ -221,7 +217,7 @@ void main()
我们还应该将矩阵传入着色器(这通常在每次的渲染迭代中进行,因为变换矩阵会经常变动):
```c++
GLint modelLoc = glGetUniformLocation(ourShader.Program, "model");
int modelLoc = glGetUniformLocation(ourShader.ID, "model"));
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
... // 观察矩阵和投影矩阵与之类似
```
@@ -236,16 +232,16 @@ glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
![coordinate_systems_result](../img/01/08/coordinate_systems_result.png)
它看起来就像是一个3D的平面静止在一个虚构的地板上。如果你得到的不是相同的结果请检查下完整的[源代码](http://learnopengl.com/code_viewer.php?code=getting-started/coordinate_systems) 以及[顶点](http://learnopengl.com/code_viewer.php?code=getting-started/transform&type=vertex)和[片段](http://learnopengl.com/code_viewer.php?code=getting-started/transform&type=fragment)着色器
它看起来就像是一个3D的平面静止在一个虚构的地板上。如果你得到的不是相同的结果请检查下完整的[源代码](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/6.1.coordinate_systems/coordinate_systems.cpp)
## 更多的3D
到目前为止我们一直都在使用一个2D平面而且甚至是在3D空间里所以让我们大胆地拓展我们的2D平面为一个3D立方体。要想渲染一个立方体我们一共需要36个顶点6个面 x 每个面有2个三角形组成 x 每个三角形有3个顶点这36个顶点的位置你可以从[这里](http://learnopengl.com/code_viewer.php?code=getting-started/cube_vertices)获取。注意,这一次我们省略了颜色值,因为我们只通过纹理来获取最终的颜色值。
到目前为止我们一直都在使用一个2D平面而且甚至是在3D空间里所以让我们大胆地拓展我们的2D平面为一个3D立方体。要想渲染一个立方体我们一共需要36个顶点6个面 x 每个面有2个三角形组成 x 每个三角形有3个顶点这36个顶点的位置你可以从[这里](https://learnopengl.com/code_viewer.php?code=getting-started/cube_vertices)获取。
为了有趣一点,我们将让立方体随着时间旋转:
```c++
model = glm::rotate(model, (GLfloat)glfwGetTime() * 50.0f, glm::vec3(0.5f, 1.0f, 0.0f));
model = glm::rotate(model, (float)glfwGetTime() * glm::radians(50.0f), glm::vec3(0.5f, 1.0f, 0.0f));
```
然后我们使用<fun>glDrawArrays</fun>来绘制立方体但这一次总共有36个顶点。
@@ -282,7 +278,7 @@ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
<video src="../../img/01/08/coordinate_system_depth.mp4" controls="controls"></video>
就是这样!一个开启了深度测试,各个面都是纹理,并且还在旋转的立方体!如果你的程序有问题可以到[这里](http://learnopengl.com/code_viewer.php?code=getting-started/coordinate_systems_with_depth)下载源码进行比对。
就是这样!一个开启了深度测试,各个面都是纹理,并且还在旋转的立方体!如果你的程序有问题可以到[这里](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/6.2.coordinate_systems_depth/coordinate_systems_depth.cpp)下载源码进行比对。
### 更多的立方体!
@@ -309,28 +305,27 @@ glm::vec3 cubePositions[] = {
```c++
glBindVertexArray(VAO);
for(GLuint i = 0; i < 10; i++)
for(unsigned int i = 0; i < 10; i++)
{
glm::mat4 model;
model = glm::translate(model, cubePositions[i]);
GLfloat angle = 20.0f * i;
model = glm::rotate(model, angle, glm::vec3(1.0f, 0.3f, 0.5f));
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
float angle = 20.0f * i;
model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
ourShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
glBindVertexArray(0);
```
这段代码将会在每次新立方体绘制出来的时候更新模型矩阵如此总共重复10次。然后我们应该就能看到一个拥有10个正在奇葩地旋转着的立方体的世界。
![coordinate_systems_multiple_objects](../img/01/08/coordinate_systems_multiple_objects.png)
完美!这就像我们的箱子找到了志同道合的小伙伴一样。如果你在这里卡住了,你可以对照一下[源代码](http://learnopengl.com/code_viewer.php?code=getting-started/coordinate_systems_multiple_objects) 以及[顶点](http://learnopengl.com/code_viewer.php?code=getting-started/coordinate_systems&type=vertex)和[片段](http://learnopengl.com/code_viewer.php?code=getting-started/coordinate_systems&type=fragment) 着色器
完美!这就像我们的箱子找到了志同道合的小伙伴一样。如果你在这里卡住了,你可以对照一下[源代码](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/6.3.coordinate_systems_multiple/coordinate_systems_multiple.cpp)
## 练习
- 对GLM的`projection`函数中的`FoV`和`aspect-ratio`参数进行实验。看能否搞懂它们是如何影响透视平截头体的。
- 将观察矩阵在各个方向上进行位移,来看看场景是如何改变的。注意把观察矩阵当成摄像机对象。
- 使用模型矩阵只让是3倍数的箱子旋转以及第1个箱子而让剩下的箱子保持静止。[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/coordinate_systems-exercise3)。
- 使用模型矩阵只让是3倍数的箱子旋转以及第1个箱子而让剩下的箱子保持静止。[参考解答](https://learnopengl.com/code_viewer.php?code=getting-started/coordinate_systems-exercise3)。

View File

@@ -12,7 +12,7 @@
## 摄像机/观察空间
当我们讨论摄像机/观察空间(Camera/View Space)的时候,是在讨论以摄像机的视角作为场景原点时场景中所有顶点坐标:观察矩阵把所有的世界坐标变换为相对于摄像机位置与方向的观察坐标。要定义一个摄像机,我们需要它在世界空间中的位置、观察的方向、一个指向它右测的向量以及一个指向它上方的向量。细心的读者可能已经注意到我们实际上创建了一个三个单位轴相互垂直的、以摄像机的位置为原点的坐标系。
当我们讨论摄像机/观察空间(Camera/View Space)的时候,是在讨论以摄像机的视角作为场景原点时场景中所有顶点坐标:观察矩阵把所有的世界坐标变换为相对于摄像机位置与方向的观察坐标。要定义一个摄像机,我们需要它在世界空间中的位置、观察的方向、一个指向它右测的向量以及一个指向它上方的向量。细心的读者可能已经注意到我们实际上创建了一个三个单位轴相互垂直的、以摄像机的位置为原点的坐标系。
![](../img/01/09/camera_axes.png)
@@ -30,7 +30,7 @@ glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
### 2. 摄像机方向
下一个需要的向量是摄像机的方向,比如它指向哪个方向。现在我们让摄像机指向场景原点:(0, 0, 0)。还记得两个矢量相减我们能得到这两个矢量的差吗用场景原点向量减去摄像机位置向量的结果就是摄像机的指向向量。由于我们知道摄像机指向z轴负方向但我们希望方向向量(Direction Vector)指向摄像机的z轴正方向。如果我们交换相减的顺序我们就会获得一个指向摄像机正z轴方向的向量
下一个需要的向量是摄像机的方向,这里指的是摄像机指向哪个方向。现在我们让摄像机指向场景原点:(0, 0, 0)。还记得如果将两个矢量相减,我们能得到这两个矢量的差吗用场景原点向量减去摄像机位置向量的结果就是摄像机的指向向量。由于我们知道摄像机指向z轴负方向但我们希望方向向量(Direction Vector)指向摄像机的z轴正方向。如果我们交换相减的顺序我们就会获得一个指向摄像机正z轴方向的向量
```c++
glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);
@@ -39,7 +39,7 @@ glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);
!!! Attention
**方向向量**(Direction Vector)并不是最好的名字,因为它正好指向从它到目标向量的相反方向(译注:注意看前面的那个图,所说的“方向向量”是指向z轴正方向的,而不是摄像机所注视的那个方向)。
**方向**向量(Direction Vector)并不是最好的名字,因为它实际上指向从它到目标向量的相反方向(译注:注意看前面的那个图,蓝色的方向向量大概指向z轴正方向,与摄像机实际指向的方向是正好相反的)。
### 3. 右轴
@@ -79,18 +79,18 @@ view = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f),
glm::vec3(0.0f, 1.0f, 0.0f));
```
<fun>glm::LookAt</fun>函数需要一个位置、目标和上向量。它会创建一个和上一节一样的观察矩阵。
<fun>glm::LookAt</fun>函数需要一个位置、目标和上向量。它会创建一个和上一节使用的一样的观察矩阵。
在讨论用户输入之前,我们先来做些有意思的事,把我们的摄像机在场景中旋转。我们会将摄像机的注视点保持在(0, 0, 0)。
我们需要用到一点三角学的知识来在每一帧创建一个x和z坐标它会代表圆上的一点我们将会使用它作为摄像机的位置。通过重新计算x和y坐标我们会遍历圆上的所有点这样摄像机就会绕着场景旋转了。我们预先定义这个圆的半径<var>radius</var>在每次渲染迭代中使用GLFW的<fun>glfwGetTime</fun>函数重新创建观察矩阵,来扩大这个圆。
```c++
GLfloat radius = 10.0f;
GLfloat camX = sin(glfwGetTime()) * radius;
GLfloat camZ = cos(glfwGetTime()) * radius;
float radius = 10.0f;
float camX = sin(glfwGetTime()) * radius;
float camZ = cos(glfwGetTime()) * radius;
glm::mat4 view;
view = glm::lookAt(glm::vec3(camX, 0.0, camZ), glm::vec3(0.0, 0.0, 0.0), glm::vec3(0.0, 1.0, 0.0));
view = glm::lookAt(glm::vec3(camX, 0.0, camZ), glm::vec3(0.0, 0.0, 0.0), glm::vec3(0.0, 1.0, 0.0));
```
如果你运行代码,应该会得到下面的结果:
@@ -98,7 +98,7 @@ view = glm::lookAt(glm::vec3(camX, 0.0, camZ), glm::vec3(0.0, 0.0, 0.0), glm::ve
<video src="../../img/01/09/camera_circle.mp4" controls="controls">
</video>
通过这一小段代码,摄像机现在会随着时间流逝围绕场景转动了。自己试试改变半径和位置/方向参数,看看**LookAt**矩阵是如何工作的。同时,如果你需要的话,这里有[源码](http://learnopengl.com/code_viewer.php?code=getting-started/camera_circle)、[顶点](http://learnopengl.com/code_viewer.php?code=getting-started/coordinate_systems&type=vertex)和[片段](http://learnopengl.com/code_viewer.php?code=getting-started/coordinate_systems&type=fragment)着色器
通过这一小段代码,摄像机现在会随着时间流逝围绕场景转动了。自己试试改变半径和位置/方向参数,看看**LookAt**矩阵是如何工作的。同时,如果你在哪卡住的话,这里有[源码](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/7.1.camera_circle/camera_circle.cpp)
# 自由移动
@@ -118,115 +118,59 @@ view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
我们首先将摄像机位置设置为之前定义的<var>cameraPos</var>。方向是当前的位置加上我们刚刚定义的方向向量。这样能保证无论我们怎么移动,摄像机都会注视着目标方向。让我们摆弄一下这些向量,在按下某些按钮时更新<var>cameraPos</var>向量。
我们已经为GLFW的键盘输入定义一个<fun>key_callback</fun>函数,我们来新添加几个需要检查的按键命令:
我们已经为GLFW的键盘输入定义一个<fun>processInput</fun>函数,我们来新添加几个需要检查的按键命令:
```c++
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
void processInput(GLFWwindow *window)
{
...
GLfloat cameraSpeed = 0.05f;
if(key == GLFW_KEY_W)
float cameraSpeed = 0.05f; // adjust accordingly
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
cameraPos += cameraSpeed * cameraFront;
if(key == GLFW_KEY_S)
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
cameraPos -= cameraSpeed * cameraFront;
if(key == GLFW_KEY_A)
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
if(key == GLFW_KEY_D)
cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
}
```
当我们按下**WASD**键的任意一个,摄像机的位置都会相应更新。如果我们希望向前或向后移动,我们就把位置向量加上或减去方向向量。如果我们希望向左右移动,我们使用叉乘来创建一个**右向量**(Right Vector),并沿着它相应移动就可以了。这样就创建了使用摄像机时熟悉的<def>扫射</def>(Strafe)效果。
当我们按下**WASD**键的任意一个,摄像机的位置都会相应更新。如果我们希望向前或向后移动,我们就把位置向量加上或减去方向向量。如果我们希望向左右移动,我们使用叉乘来创建一个**右向量**(Right Vector),并沿着它相应移动就可以了。这样就创建了使用摄像机时熟悉的<def>横移</def>(Strafe)效果。
!!! important
注意,我们对**右向量**进行了标准化。如果我们没对这个向量进行标准化,最后的叉乘结果会根据<var>cameraFront</var>变量返回大小不同的向量。如果我们不对向量进行标准化,我们就得根据摄像机的朝向不同加速或减速移动了,但如进行了标准化移动就是匀速的。
注意,我们对**右向量**进行了标准化。如果我们没对这个向量进行标准化,最后的叉乘结果会根据<var>cameraFront</var>变量返回大小不同的向量。如果我们不对向量进行标准化,我们就得根据摄像机的朝向不同加速或减速移动了,但如进行了标准化移动就是匀速的。
如果你用这段代码更新<fun>key_callback</fun>函数,你就可以在场景中自由的前后左右移动了
<video src="../../img/01/09/camera_inside.mp4" controls="controls">
</video>
在摆弄这个基础的摄像机系统之后你可能会注意到这个摄像机系统不能同时朝两个方向移动对角线移动而且当你按下一个按键时它会先顿一下才开始移动。这是因为大多数事件输入系统一次只能处理一个键盘输入它们的函数只有当我们激活了一个按键时才被调用。虽然这对大多数GUI系统都没什么问题它对摄像机来说并不合理。我们可以用一些小技巧解决这个问题。
这个技巧是在回调函数中只跟踪哪个按键被按下/释放。在游戏循环中我们读取这些值,检查哪个按键是**激活的**,然后做出相应反应。我们只储存哪个按键被按下/释放的状态信息并在游戏循环中对状态做出反应。首先我们来创建一个boolean数组代表被按下/释放的按键:
```c++
bool keys[1024];
```
然后我们需要在<fun>key_callback</fun>函数中设置被按下/释放的按键为`true`或`false`
```c++
if(action == GLFW_PRESS)
keys[key] = true;
else if(action == GLFW_RELEASE)
keys[key] = false;
```
并且我们创建一个新的叫做<fun>do_movement</fun>的函数,在这个函数中,我们将根据正在被按下的按键更新摄像机的值。:
```c++
void do_movement()
{
// 摄像机控制
GLfloat cameraSpeed = 0.01f;
if(keys[GLFW_KEY_W])
cameraPos += cameraSpeed * cameraFront;
if(keys[GLFW_KEY_S])
cameraPos -= cameraSpeed * cameraFront;
if(keys[GLFW_KEY_A])
cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
if(keys[GLFW_KEY_D])
cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
}
```
之前的代码现在被移动到<fun>do_movement</fun>函数中。由于所有GLFW的按键枚举值本质上都是整数我们可以把它们当数组索引使用。
最后,我们需要在游戏循环中添加新函数的调用:
```c++
while(!glfwWindowShouldClose(window))
{
// 检测并调用事件
glfwPollEvents();
do_movement();
// 渲染
...
}
```
至此,你应该可以同时向多个方向移动了,并且当你按下按钮时也会立刻移动了。如遇困难,可以查看下[源代码](http://learnopengl.com/code_viewer.php?code=getting-started/camera_keyboard)。
现在你就应该能够移动摄像机了,虽然移动速度和系统有关,你可能会需要调整一下<var>cameraSpeed</var>
## 移动速度
目前我们的移动速度是个常量。理论上没什么问题,但是实际情况下根据处理器的能力不同,有些人可能会比其他人每秒绘制更多帧,也就是以更高的频率调用<fun>do_movement</fun>函数。结果就是,根据配置的不同,有些人可能移动很快,而有些人会移动很慢。当你发布你的程序的时候,你必须确保它在所有硬件上移动速度都一样。
目前我们的移动速度是个常量。理论上没什么问题,但是实际情况下根据处理器的能力不同,有些人可能会比其他人每秒绘制更多帧,也就是以更高的频率调用<fun>processInput</fun>函数。结果就是,根据配置的不同,有些人可能移动很快,而有些人会移动很慢。当你发布你的程序的时候,你必须确保它在所有硬件上移动速度都一样。
图形程序和游戏通常会跟踪一个<def>时间差</def>(Deltatime)变量,它储存了渲染上一帧所用的时间。我们把所有速度都去乘以<var>deltaTime</var>值。结果就是,如果我们的<var>deltaTime</var>很大,就意味着上一帧的渲染花费了更多时间,所以这一帧的速度需要变得更高来平衡渲染所花去的时间。使用这种方法时,无论你的电脑快还是慢,摄像机的速度都会相应平衡,这样每个用户的体验就都一样了。
图形程序和游戏通常会跟踪一个<def>时间差</def>(Deltatime)变量,它储存了渲染上一帧所用的时间。我们把所有速度都去乘以<var>deltaTime</var>值。结果就是,如果我们的<var>deltaTime</var>很大,就意味着上一帧的渲染花费了更多时间,所以这一帧的速度需要变得更高来平衡渲染所花去的时间。使用这种方法时,无论你的电脑快还是慢,摄像机的速度都会相应平衡,这样每个用户的体验就都一样了。
我们跟踪两个全局变量来计算出<var>deltaTime</var>值:
```c++
GLfloat deltaTime = 0.0f; // 当前帧与上一帧的时间差
GLfloat lastFrame = 0.0f; // 上一帧的时间
float deltaTime = 0.0f; // 当前帧与上一帧的时间差
float lastFrame = 0.0f; // 上一帧的时间
```
在每一帧中我们计算出新的<var>deltaTime</var>以备后用。
```c++
GLfloat currentFrame = glfwGetTime();
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
lastFrame = currentFrame;
```
现在我们有了<var>deltaTime</var>,在计算速度的时候可以将其考虑进去了:
```c++
void do_movement()
void processInput(GLFWwindow *window)
{
GLfloat cameraSpeed = 5.0f * deltaTime;
float cameraSpeed = 2.5f * deltaTime;
...
}
```
@@ -237,7 +181,7 @@ void do_movement()
<video src="../../img/01/09/camera_smooth.mp4" controls="controls">
</video>
现在我们有了一个在任何系统上移动速度都一样的摄像机。同样,如果你卡住了,查看一下[源码](http://learnopengl.com/code_viewer.php?code=getting-started/camera_keyboard_dt)。我们可以看到任何移动都会影响返回的<var>deltaTime</var>值。
现在我们有了一个在任何系统上移动速度都一样的摄像机。同样,如果你卡住了,查看一下[源码](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/7.2.camera_keyboard_dt/camera_keyboard_dt.cpp)。我们可以看到任何移动都会影响返回的<var>deltaTime</var>值。
# 视角移动
@@ -323,18 +267,18 @@ glfwSetCursorPosCallback(window, mouse_callback);
第一步是计算鼠标自上一帧的偏移量。我们必须先在程序中储存上一帧的鼠标位置我们把它的初始值设置为屏幕的中心屏幕的尺寸是800x600
```c++
GLfloat lastX = 400, lastY = 300;
float lastX = 400, lastY = 300;
```
然后在鼠标的回调函数中我们计算当前帧和上一帧鼠标位置的偏移量:
```c++
GLfloat xoffset = xpos - lastX;
GLfloat yoffset = lastY - ypos; // 注意这里是相反的因为y坐标是从底部往顶部依次增大的
float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // 注意这里是相反的因为y坐标是从底部往顶部依次增大的
lastX = xpos;
lastY = ypos;
GLfloat sensitivity = 0.05f;
float sensitivity = 0.05f;
xoffset *= sensitivity;
yoffset *= sensitivity;
```
@@ -345,7 +289,7 @@ yoffset *= sensitivity;
```c++
yaw += xoffset;
pitch += yoffset;
pitch += yoffset;
```
第三步我们需要给摄像机添加一些限制这样摄像机就不会发生奇怪的移动了这样也会避免一些奇怪的问题。对于俯仰角要让用户不能看向高于89度的地方在90度时视角会发生逆转所以我们把89度作为极限同样也不允许小于-89度。这样能够保证用户只能看到天空或脚下但是不能超越这个限制。我们可以在值超过限制的时候将其改为极限值来实现
@@ -376,9 +320,9 @@ cameraFront = glm::normalize(front);
```c++
if(firstMouse) // 这个bool变量初始时是设定为true的
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
```
@@ -394,12 +338,12 @@ void mouse_callback(GLFWwindow* window, double xpos, double ypos)
firstMouse = false;
}
GLfloat xoffset = xpos - lastX;
GLfloat yoffset = lastY - ypos;
float xoffset = xpos - lastX;
float yoffset = lastY - ypos;
lastX = xpos;
lastY = ypos;
GLfloat sensitivity = 0.05;
float sensitivity = 0.05;
xoffset *= sensitivity;
yoffset *= sensitivity;
@@ -419,7 +363,7 @@ void mouse_callback(GLFWwindow* window, double xpos, double ypos)
}
```
现在我们就可以自由地在3D场景中移动了如果你遇到困难,可以来看一下[源代码](http://www.learnopengl.com/code_viewer.php?code=getting-started/camera_mouse)。
现在我们就可以自由地在3D场景中移动了
## 缩放
@@ -442,7 +386,7 @@ void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
我们现在在每一帧都必须把透视投影矩阵上传到GPU但现在使用<var>fov</var>变量作为它的视野:
```c++
projection = glm::perspective(fov, (GLfloat)WIDTH/(GLfloat)HEIGHT, 0.1f, 100.0f);
projection = glm::perspective(glm::radians(fov), 800.0f / 600.0f, 0.1f, 100.0f);
```
最后不要忘记注册鼠标滚轮的回调函数:
@@ -456,29 +400,26 @@ glfwSetScrollCallback(window, scroll_callback);
<video src="../../img/01/09/camera_mouse.mp4" controls="controls">
</video>
你可以去自由地实验,如果遇到困难,可以对比[源代码](http://learnopengl.com/code_viewer.php?code=getting-started/camera_zoom)。
你可以去自由地实验,如果遇到困难,可以对比[源代码](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/7.3.camera_mouse_zoom/camera_mouse_zoom.cpp)。
!!! Important
注意,使用欧拉角的摄像机系统并不完美。根据你的视角限制或者是配置,你仍然可能引入[万向节死锁](http://en.wikipedia.org/wiki/Gimbal_lock)问题。最好的摄像机系统是使用四元数(Quaternions)的,但我们将会把这个留到后面讨论。
注意,使用欧拉角的摄像机系统并不完美。根据你的视角限制或者是配置,你仍然可能引入[万向节死锁](http://en.wikipedia.org/wiki/Gimbal_lock)问题。最好的摄像机系统是使用四元数(Quaternions)的,但我们将会把这个留到后面讨论。(译注:[这里](https://github.com/cybercser/OpenGL_3_3_Tutorial_Translation/blob/master/Tutorial%2017%20Rotations.md)可以查看四元数摄像机的实现)
# 摄像机类
接下来的教程中,我们将会一直使用一个摄像机来浏览场景,从各个角度观察结果。然而,由于一个摄像机会占用每篇教程很大的篇幅,我们将会从细节抽象出来,创建我们自己的摄像机对象,它会完成大多数的工作,而且还会提供一些附加的功能。与着色器教程不同,我们不会带你一步一步创建摄像机类,我们只会提供你一份(有完整注释的)代码,如果你想知道它的内部构造的话可以自己去阅读。
和着色器对象一样,我们把摄像机类写在一个单独的头文件中。你可以在[这里](http://learnopengl.com/code_viewer.php?type=header&code=camera)找到它,你现在应该能够理解所有的代码了。我们建议您至少看一看这个类,看看如何创建一个自己的摄像机类。
和着色器对象一样,我们把摄像机类写在一个单独的头文件中。你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/camera.h)找到它,你现在应该能够理解所有的代码了。我们建议您至少看一看这个类,看看如何创建一个自己的摄像机类。
!!! Attention
我们介绍的摄像机系统是一个FPS风格的摄像机它能够满足大多数情况需要而且与欧拉角兼容但是在创建不同的摄像机系统比如飞行模拟摄像机时就要当心。每个摄像机系统都有自己的优点和不足所以确保对它们进行了详细研究。比如这个FPS摄像机不允许俯仰角大于90度而且我们使用了一个固定的上向量(0, 1, 0),这在需要考虑滚转角的时候就不能用了。
使用新摄像机对象,更新后版本的源码可以在[这里](http://learnopengl.com/code_viewer.php?code=getting-started/camera_with_class)找到。
(译注:总而言之这个摄像机实现并不十分完美,你可以看看最终的源码。建议先看[这篇文章](https://github.com/cybercser/OpenGL_3_3_Tutorial_Translation/blob/master/Tutorial%2017%20Rotations.md)对旋转有更深的理解后你就能做出更好的摄像机类不过本文有些内容比如如何防止按键停顿和GLFW鼠标事件实现摄像机的注意事项比较重要其它的就要做一定的取舍了
使用新摄像机对象,更新后版本的源码可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/7.4.camera_class/camera_class.cpp)找到。
## 练习
- 看看你是否能够修改摄像机类,使得其能够变成一个**真正的**FPS摄像机也就是说不能够随意飞行你只能够呆在xz平面上[参考解答](http://www.learnopengl.com/code_viewer.php?code=getting-started/camera-exercise1)
- 看看你是否能够修改摄像机类,使得其能够变成一个**真正的**FPS摄像机也就是说不能够随意飞行你只能够呆在xz平面上[参考解答](https://learnopengl.com/code_viewer.php?code=getting-started/camera-exercise1)
- 试着创建你自己的LookAt函数其中你需要手动创建一个我们在一开始讨论的观察矩阵。用你的函数实现来替换GLM的LookAt函数看看它是否还能一样地工作[参考解答](http://www.learnopengl.com/code_viewer.php?code=getting-started/camera-exercise2)
- 试着创建你自己的LookAt函数其中你需要手动创建一个我们在一开始讨论的观察矩阵。用你的函数实现来替换GLM的LookAt函数看看它是否还能一样地工作[参考解答](https://learnopengl.com/code_viewer.php?code=getting-started/camera-exercise2)

View File

@@ -1,6 +1,6 @@
# 复习
原文 | [Review](http//learnopengl.com/#!Getting-started/Review)
原文 | [Review](https://learnopengl.com/#!Getting-started/Review)
---|---
作者 | JoeyDeVries
翻译 | Meow J
@@ -13,7 +13,7 @@
## 词汇表
- **OpenGL** 一个定义了函数布局和输出的图形API的正式规范。
- **GLEW** 一个拓展加载库用来为我们加载并设定所有OpenGL函数指针从而让我们能够使用所有现代OpenGL函数。
- **GLAD** 一个拓展加载库用来为我们加载并设定所有OpenGL函数指针从而让我们能够使用所有现代OpenGL函数。
- **视口(Viewport)** 我们需要渲染的窗口。
- **图形管线(Graphics Pipeline)** 一个顶点在呈现为像素之前通过的过程。
- **着色器(Shader)** 一个运行在显卡上的小型程序。很多阶段的图形管道都可以使用自定义的着色器来代替原来的功能。
@@ -26,7 +26,7 @@
- **纹理缠绕(Texture Wrapping)** 定义了一种当纹理顶点超出范围(0, 1)时指定OpenGL如何采样纹理的模式。
- **纹理过滤(Texture Filtering)** 定义了一种当有多种纹素选择时指定OpenGL如何采样纹理的模式。这通常在纹理被放大情况下发生。
- **多级渐远纹理(Mipmaps)** 被存储的材质的一些缩小版本,根据距观察者的距离会使用材质的合适大小。
- **SOIL** 图像加载库。
- **stb_image.h** 图像加载库。
- **纹理单元(Texture Units)** 通过绑定纹理到不同纹理单元从而允许多个纹理在同一对象上渲染。
- **向量(Vector)** 一个定义了在空间中方向和/或位置的数学实体。
- **矩阵(Matrix)** 一个矩形阵列的数学表达式。

View File

@@ -6,22 +6,21 @@
翻译 | [Geequlim](http://geequlim.com/)
校对 | [Geequlim](http://geequlim.com/)
在前面的教程中我们已经简要提到过该如何在OpenGL中使用颜色(Color),但是我们至今所接触到的都是很浅层的知识。本节我们将会更广泛地讨论颜色,并且还会为接下来的光照(Lighting)教程创建一个场景。
在前面的教程中我们已经简要提到过该如何在OpenGL中使用颜色(Color),但是我们至今所接触到的都是很浅层的知识。本节我们将会更深入地讨论什么是颜色,并且还会为接下来的光照(Lighting)教程创建一个场景。
现实世界中有无数种颜色,每一个物体都有它们自己的颜色。我们要做的工作是使用(有限的)数字来模拟真实世界中(无限)的颜色,因此并不是所有的现实世界中的颜色都可以用数字来表示。然而我们依然可以用数字来代表许多种颜色,并且你甚至可能根本感觉不到他们与真实颜色之间的差异。颜色可以数字化的由红色(Red)、绿色(Green)和蓝色(Blue)三个分量组成它们通常被缩写为RGB。这三个不同的分量组合在一起几乎可以表示存在的任何一种颜色。例如,要获取一个**珊瑚红(Coral)**颜色我们可以这样定义一个颜色向量:
现实世界中有无数种颜色,每一个物体都有它们自己的颜色。我们需要使用(有限的)数值来模拟真实世界中(无限)的颜色,所以并不是所有现实世界中的颜色都可以用数值来表示的。然而我们仍能通过数值来表现出非常多的颜色,甚至你可能都不会注意到与现实的颜色有任何的差异。颜色可以数字化的由红色(Red)、绿色(Green)和蓝色(Blue)三个分量组成它们通常被缩写为RGB。仅仅用这三个值就可以组合出任意一种颜色。例如要获取一个**珊瑚红(Coral)**色的话,我们可以定义这样的一个颜色向量:
```c++
glm::vec3 coral(1.0f, 0.5f, 0.31f);
```
我们在现实生活中看到某一物体的颜色并不是这个物体的真实颜色,而是它所反射(Reflected)颜色。换句话说,那些不能被物体吸收(Absorb)的颜色(被反射的颜色)就是我们能够感知到的物体的颜色。例如,太阳光被认为是由许多不同的颜色组合成的白色光(如下图所示)。如果我们将白光照在一个蓝色的玩具上,这个蓝色的玩具会吸收白光中除了蓝色以外的所有颜色,不被吸收的蓝色光被反射到我们的眼中,使我们看到了一个蓝色的玩具。下图显示的是一个珊瑚红的玩具,它以不同强度的方式反射了几种不同的颜色。
我们在现实生活中看到某一物体的颜色并不是这个物体真正拥有的颜色,而是它所<def>反射的</def>(Reflected)颜色。换句话说,那些不能被物体吸收(Absorb)的颜色(被拒绝的颜色就是我们能够感知到的物体的颜色。例如太阳光能被看见的白光其实是由许多不同的颜色组合成的如下图所示。如果我们将白光照在一个蓝色的玩具上,这个蓝色的玩具会吸收白光中除了蓝色以外的所有颜色,不被吸收的蓝色光被反射到我们的眼中,让这个玩具看起来是蓝色的。下图显示的是一个珊瑚红的玩具,它以不同强度反射了多个颜色。
![](../img/02/01/light_reflection.png)
正如你所见,白色的阳光是一种所有可见颜色的集合,上面的物体吸收了其中的大部分颜色它仅反射了那些代表这个物体颜色的部分,这些被反射颜色的组合就是我们感知到的颜色(此例中为珊瑚红)
你可以看到,白色的阳光实际上是所有可见颜色的集合,物体吸收了其中的大部分颜色它仅反射了代表物体颜色的部分,被反射颜色的组合就是我们感知到的颜色此例中为珊瑚红
这些颜色反射的律被直接地运用在图形领域。我们在OpenGL中创建一个光源时都会为它定义一个颜色。在前面的段落中所提到光源的颜色都是白色的,那我们就继续来创建一个白色的光源吧。当我们把光源的颜色与物体的颜色相乘,所得到的就是这个物体所反射该光源的颜色(也就是我们感知到的颜色)。让我们再次审视我们的玩具(这一次它还是珊瑚红)并看看如何计算出的反射颜色。我们通过检索结果颜色的每一个分量来看一下光源色和物体颜色的反射运算
这些颜色反射的律被直接地运用在图形领域。我们在OpenGL中创建一个光源时,我们希望给光源一个颜色。在上一段中我们有一个白色的太阳,所以我们也将光源设置为白色。当我们把光源的颜色与物体的颜色相乘,所得到的就是这个物体所反射的颜色也就是我们感知到的颜色。让我们再次审视我们的玩具这一次它还是珊瑚红看看如何在图形学中计算出的反射颜色。我们将这两个颜色向量作分量相乘,结果就是最终的颜色向量了
```c++
glm::vec3 lightColor(1.0f, 1.0f, 1.0f);
@@ -29,7 +28,7 @@ glm::vec3 toyColor(1.0f, 0.5f, 0.31f);
glm::vec3 result = lightColor * toyColor; // = (1.0f, 0.5f, 0.31f);
```
我们可以看到玩具在进行反射时**吸收**了白色光源颜色中的大部分颜色,但它对红、绿、蓝三个分量都一定的反射,反射量是由物体本身的颜色所决定的。这也代表着现实中的光线原理。由此,我们可以定义物体的颜色为**这个物体从一个光源反射各个颜色分量的多少**。现在,如果我们使用一束绿色的光又会发生什么呢?
我们可以看到玩具的颜色**吸收**了白色光源中很大一部分颜色,但它根据自身的颜色值对红、绿、蓝三个分量都做出了一定的反射。这也表现了现实中颜色的工作原理。由此,我们可以定义物体的颜色为**物体从一个光源反射各个颜色分量的大小**。现在,如果我们使用绿色的光又会发生什么呢?
```c++
glm::vec3 lightColor(0.0f, 1.0f, 0.0f);
@@ -37,7 +36,7 @@ glm::vec3 toyColor(1.0f, 0.5f, 0.31f);
glm::vec3 result = lightColor * toyColor; // = (0.0f, 0.5f, 0.0f);
```
可以看到,我们的玩具没有红色和蓝色的光让来吸收或反射这个玩具吸收了光线中一半的绿色,当然它仍然反射了光的一半绿色。它现在看上去是深绿色(Dark-greenish)的。我们可以看到,如果我们用一束绿色的光线照来照射玩具,那么只有绿色能被反射和感知到,没有红色和蓝色能被反射和感知。这样做的结果是,一个珊瑚红的玩具突然变成了深绿色物体。现在我们来看另一个例子,使用深橄榄绿色(Dark olive-green)的光线
可以看到,没有红色和蓝色的光让我们的玩具来吸收或反射这个玩具吸收了光线中一半的绿色值,但仍然反射了一半绿色值。玩具现在看上去是深绿色(Dark-greenish)的。我们可以看到,如果我们用绿色光源来照射玩具,那么只有绿色分量能被反射和感知到,红色和蓝色都不能被我们所感知。这样做的结果是,一个珊瑚红的玩具突然变成了深绿色物体。现在我们来看另一个例子,使用深橄榄绿色(Dark olive-green)的光
```c++
glm::vec3 lightColor(0.33f, 0.42f, 0.18f);
@@ -45,23 +44,23 @@ glm::vec3 toyColor(1.0f, 0.5f, 0.31f);
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)是吧?)
首先我们需要一个物体来作为被投光(Cast the light)的对象,我们将使用前面教程中的那个著名的立方体箱子。我们还需要一个物体来代表光源在3D场景中的位置。简单起见,我们依然使用一个立方体来代表光源我们已拥有立方体的[顶点数据](https://learnopengl.com/code_viewer.php?code=getting-started/cube_vertices)是吧?
当然,填一个顶点缓冲对象(VBO),设定一下顶点属性指针和其一些乱七八糟的东西现在对你来说应该很容易了,所以我们就不再赘述那些步骤了。如果你仍然觉得这很困难,我建议你复习[之前的教程](../01 Getting started/04 Hello Triangle.md),并且在继续学习之前先把练习过一遍。
填一个顶点缓冲对象(VBO),设定一下顶点属性指针和其一些乱七八糟的东西现在对你来说应该很容易了,所以我们就不再赘述那些步骤了。如果你仍然觉得这很困难,我建议你复习[之前的教程](../01 Getting started/04 Hello Triangle.md),并且在继续学习之前先把练习过一遍。
所以,我们首先需要一个顶点着色器来绘制箱子。与上一个教程的顶点着色器相比,容器的顶点位置保持不变(虽然这一次我们不需要纹理坐标),因此顶点着色器中没有新的代码。我们将会使用上一篇教程顶点着色器的精简版:
我们首先需要一个顶点着色器来绘制箱子。与之前的顶点着色器相比,容器的顶点位置保持不变的(虽然这一次我们不需要纹理坐标了),因此顶点着色器中没有新的代码。我们将会使用之前教程顶点着色器的精简版:
```c++
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 0) in vec3 aPos;
uniform mat4 model;
uniform mat4 view;
@@ -69,68 +68,66 @@ uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(position, 1.0f);
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
```
请确认更新你的顶点数据和属性对应的指针与新的顶点着色器一致(当然你可以继续留纹理数据并保持属性对应的指针有效。在这一节中我们不使用纹理,但如果你想要一个全新的开始也不是什么坏主意)
记得更新你的顶点数据和属性指针使其与新的顶点着色器保持一致当然你可以继续留纹理数据和属性指针。在这一节中我们将不会用到它们,但有一个全新的开始也不是什么坏主意
因为我们还要创建一个表示灯(光源)的立方体,所以我们还要为这个灯创建一个特殊的VAO。当然我们也可以让这个灯和其物体使用同一个VAO然后对他的`model`(模型)矩阵做一些变换,然而接下来的教程中我们会频繁地对顶点数据做一些改变并且需要改变属性对应指针设置,我们并不想因此影响到灯(我们只在乎灯的位置)因此我们有必要为灯创建一个新的VAO。
因为我们还要创建一个表示灯光源的立方体,所以我们还要为这个灯创建一个专门的VAO。当然我们也可以让这个灯和其物体使用同一个VAO,简单地对它的<var>model</var>模型矩阵做一些变换就好了,然而接下来的教程中我们会频繁地对顶点数据和属性指针做出修改,我们并不想让这些修改影响到灯我们只关心灯的顶点位置因此我们有必要为灯创建一个新的VAO。
```c++
GLuint lightVAO;
unsigned int lightVAO;
glGenVertexArrays(1, &lightVAO);
glBindVertexArray(lightVAO);
// 只需要绑定VBO不用再次设置VBO的数据因为容器(物体)的VBO数据中已经包含了正确的立方体顶点数据
// 只需要绑定VBO不用再次设置VBO的数据因为箱子的VBO数据中已经包含了正确的立方体顶点数据
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 设置灯立方体的顶点属性指针(仅设置灯的顶点数据)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
// 设置灯立方体的顶点属性(对我们的灯来说仅仅只有位置数据
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glBindVertexArray(0);
```
这段代码对你来说应该非常直观。既然我们已经创建了表示灯和被照物体的立方体,我们只需要再定义一个东西就行了了,那就是片段着色器
这段代码对你来说应该非常直观。现在我们已经创建了表示灯和被照物体箱子,我们只需要再定义一个片段着色器就行了:
```c++
#version 330 core
out vec4 color;
out vec4 FragColor;
uniform vec3 objectColor;
uniform vec3 lightColor;
void main()
{
color = vec4(lightColor * objectColor, 1.0f);
FragColor = vec4(lightColor * objectColor, 1.0);
}
```
这个片段着色器接受两个分别表示物体颜色和光源颜色的uniform变量。正如本篇教程一开始所讨论的样,我们将光源的颜色物体(能反射)的颜色相乘。这个着色器应该很容易理解。接下来让我们把物体的颜色设置为上一节中所提到的珊瑚红并把光源设置为白色
这个片段着色器从uniform变量中接受物体颜色和光源颜色。正如本一开始所讨论的样,我们将光源的颜色物体(反射的)颜色相乘。这个着色器理解起来应该很容易我们把物体的颜色设置为之前提到的珊瑚红色,并把光源设置为白色
```c++
// 在此之前不要忘记首先'使用'对应的着色器程序(来设定uniform)
GLint objectColorLoc = glGetUniformLocation(lightingShader.Program, "objectColor");
GLint lightColorLoc = glGetUniformLocation(lightingShader.Program, "lightColor");
glUniform3f(objectColorLoc, 1.0f, 0.5f, 0.31f);// 我们所熟悉的珊瑚红
glUniform3f(lightColorLoc, 1.0f, 1.0f, 1.0f); // 依旧把光源设置为白色
// 在此之前不要忘记首先 use 对应的着色器程序来设定uniform
lightingShader.use();
lightingShader.setVec3("objectColor", 1.0f, 0.5f, 0.31f);
lightingShader.setVec3("lightColor", 1.0f, 1.0f, 1.0f);
```
要注意的是,当我们修改顶点或者片段着色器后,灯的位置或颜色也会随之改变,这并不是我们想要的效果。我们不希望灯对象的颜色在接下来的教程中因光照计算的结果而受到影响,而希望它能够独立。希望表示灯不受其他光照的影响而一直保持明亮(这样它才更像是一个真实的光源)
要注意的是,当我们修改顶点或者片段着色器后,灯的位置或颜色也会随之改变,这并不是我们想要的效果。我们不希望灯的颜色在接下来的教程中因光照计算的结果而受到影响,而希望它能够与其它的计算分离。我们希望灯一直保持明亮,不受其它颜色变化的影响(这样它才更像是一个真实的光源
为了实现这个目,我们需要为灯创建另外的一套着色器程序,从而能保证它能够在其光照着色器变化的时候保持不变。顶点着色器我们当前的顶点着色器是一样的,所以你可以直接把的顶点着色器复制过来。片段着色器保证了灯的颜色一直是亮的,我们通过给灯定义一个常量的白色来实现
为了实现这个目,我们需要为灯的绘制创建另外的一套着色器,从而能保证它能够在其光照着色器发生改变的时候不受影响。顶点着色器我们当前的顶点着色器是一样的,所以你可以直接把现在的顶点着色器用在灯上。灯的片段着色器给灯定义了一个不变的常量白色,保证了灯的颜色一直是亮的
```c++
#version 330 core
out vec4 color;
out vec4 FragColor;
void main()
{
color = vec4(1.0f); //设置四维向量的所有元素为 1.0f
FragColor = vec4(1.0); // 将向量的四个分量全部设置为1.0
}
```
当我们想要绘制我们的物体的时候,我们需要使用刚刚定义的光照着色器绘制箱子(或者可能是其它的一些物体),让我们想要绘制灯的时候,我们会使用灯的着色器。在之后的教程里我们会逐步升级这个光照着色器从而能够缓慢的实现更真实的效果。
当我们想要绘制我们的物体的时候,我们需要使用刚刚定义的光照着色器绘制箱子或者可能是其它的物体)。当我们想要绘制灯的时候,我们会使用灯的着色器。在之后的教程里我们会逐步更新这个光照着色器从而能够慢慢地实现更真实的效果。
使用这个灯立方体的主要目的是为了让我们知道光源在场景中的具体位置。我们通常在场景中定义一个光源的位置,但这只是一个位置,它并没有视觉意义。为了显示真正的灯,我们将表示光源的立方体绘制在与光源同的位置。我们将使用我们为它新建的片段着色器让它保持它一直处于白色状态,不受场景中的光照影响。
使用这个灯立方体的主要目的是为了让我们知道光源在场景中的具体位置。我们通常在场景中定义一个光源的位置,但这只是一个位置,它并没有视觉意义。为了显示真正的灯,我们将表示光源的立方体绘制在与光源同的位置。我们将使用我们为它新建的片段着色器来绘制它,让它一直处于白色状态,不受场景中的光照影响。
我们声明一个全局`vec3`变量来表示光源在场景的世界空间坐标中的位置:
@@ -138,7 +135,7 @@ void main()
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);
```
然后我们把灯移到这儿,当然我们需要对它进行缩放,让它不那么明显:
然后我们把灯移到这里,然后将它缩小一点,让它不那么明显:
```c++
model = glm::mat4();
@@ -149,21 +146,20 @@ model = glm::scale(model, glm::vec3(0.2f));
绘制灯立方体的代码应该与下面的类似:
```c++
lampShader.Use();
lampShader.use();
// 设置模型、视图和投影矩阵uniform
...
// 绘制灯立方体对象
glBindVertexArray(lightVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
```
请把上述的所有代码片段放在你程序中合适的位置,这样我们就能有一个干净的光照实验场地了。如果一切顺利,运行效果将会如下图所示:
![](../img/02/01/colors_scene.png)
没什么好看的是吗?但我保证在接下来的教程中它会给你有趣的视觉效果
没什么好看的是吗?但我保证在接下来的教程中它会变得更加有趣
如果你在把上述代码片段到一起编译遇到困难,可以去认真地看看我的[源代码](http://learnopengl.com/code_viewer.php?code=lighting/colors_scene)。你好最自己实现一遍这些操作
如果你觉得将上面的代码片段整合到一起非常困难,可以来[这里](https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/1.colors/colors.cpp)看一下源代码,并仔细研究我的代码和注释
现在我们有了一些关于颜色的知识,并且创建了一个基本的场景能够绘制一些漂亮的光线。你现在可以阅读[下一节](02 Basic Lighting.md),真正的魔法即将开始!
我们现在已经对颜色有一定的了解了,并且已经创建了一个简单的场景供我们之后绘制动感的光照,我们可以进入[下一节](02 Basic Lighting.md)进行学习,真正的魔法即将开始!

View File

@@ -1,4 +1,4 @@
# 光照基础
# 基础光照
原文 | [Basic Lighting](http://learnopengl.com/#!Lighting/Basic-Lighting)
---|---
@@ -6,97 +6,99 @@
翻译 | [Django](http://bullteacher.com/)
校对 | Geequlim, [BLumia](https://github.com/blumia/)
现实世界的光照是极其复杂的,而且会受到诸多因素的影响,这是以目前我们所拥有的处理能力无法模拟的。因此OpenGL的光照仅仅使用了简化的模型并基于对现实的估计来进行模拟,这样处理起来会更容易一些,而且看起来也差不多一样。这些光照模型都是基于我们对光的物理特性的理解。其中一个模型被称为冯氏光照模型(Phong Lighting Model)。冯氏光照模型的主要结构由3个元素组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。这些光照元素看起来像下面这样
现实世界的光照是极其复杂的,而且会受到诸多因素的影响,这是我们有限的计算能力无法模拟的。因此OpenGL的光照使用的是简化的模型对现实的情况进行近似,这样处理起来会更容易一些,而且看起来也差不多一样。这些光照模型都是基于我们对光的物理特性的理解。其中一个模型被称为<def>冯氏光照模型</def>(Phong Lighting Model)。冯氏光照模型的主要结构由3个分量组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。下面这张图展示了这些光照分量看起来的样子
![](../img/02/02/basic_lighting_phong.png)
- 环境光照(Ambient Lighting):即使在黑暗的情况下,世界上也仍然有一些光亮(月亮、一个来自远处的光),所以物体永远不会是完全黑暗的。我们使用环境光照来模拟这种情况,也就是无论如何永远给物体一些颜色。
- 漫反射光照(Diffuse Lighting):模拟一个发光物对物体的方向性影响(Directional Impact)。它是冯氏光照模型最显著的组成部分。面向光源的一面比其他面会更亮。
- 镜面光照(Specular Lighting):模拟有光泽物体上面出现的亮点。镜面光照的颜色相比于物体的颜色更倾向于光的颜色。
- <def>环境光照</def>(Ambient Lighting):即使在黑暗的情况下,世界上通常也仍然有一些光亮月亮、远处的光,所以物体几乎永远不会是完全黑暗的。为了模拟这个,我们使用一个环境光照常量,它永远给物体一些颜色。
- <def>漫反射光照</def>(Diffuse Lighting):模拟光源对物体的方向性影响(Directional Impact)。它是冯氏光照模型中视觉上最显著的分量。物体的某一部分越是正对着光源,它就会越亮。
- <def>镜面光照</def>(Specular Lighting):模拟有光泽物体上面出现的亮点。镜面光照的颜色相比于物体的颜色更倾向于光的颜色。
为了创建有趣的视觉场景,我们希望模拟至少这三种光照元素。我们将以最简单的一个开始:**环境光照**。
为了创建有趣的视觉场景,我们希望模拟至少这三种光照分量。我们将以最简单的一个开始:**环境光照**。
# 环境光照
光通常都不是来自于同一光源,而是来自散落于我们周围的很多光源,即使它们可能并不是那么显而易见。光的一个属性是,它可以向很多方向发散反弹,所以光最后到达的地点可能并不是它所临近的直射方向;光能够像这样**反射(Reflect)**到其他表面,一个物体的光照可能受到来自一个非直射的光源影响。考虑到这种情况的算法叫做**全局照明(Global Illumination)**算法,但是这种算法既开销高昂又极其复杂。
光通常都不是来自于同一光源,而是来自于我们周围分散的很多光源,即使它们可能并不是那么显而易见。光的一个属性是,它可以向很多方向发散反弹,从而能够到达不是非常直接临近的点。所以,光能够在其它的表面上**反射**,对一个物体产生间接的影响。考虑到这种情况的算法叫做<def>全局照明</def>(Global Illumination)算法,但是这种算法既开销高昂又极其复杂。
因为我们不是复杂和昂贵算法的死忠粉丝,所以我们将会使用一简化的全局照明模型,叫做环境光照(Ambient Lighting)。如你在前面章节所见,我们使用一个(数值)很小的常量(光)颜色添加物体**片段**(Fragment指当前讨论的光线在物体上的照射点)的最终颜色,这看起来就像即使没有直光源也始终存在一些发散的光。
由于我们现在对那种又复杂又开销高昂的算法不是很感兴趣,所以我们将会使用一简化的全局照明模型,<def>环境光照</def>。正如你在上一节所学到的,我们使用一个很小的常量(光照)颜色添加物体片段的最终颜色,这样子的话即便场景中没有直接的光源也能看起来存在一些发散的光。
把环境光照添加到场景里非常简单。我们用光的颜色乘以一个(数值)很小常量环境因子,再乘以物体的颜色,然后使用它作为片段的颜色:
把环境光照添加到场景里非常简单。我们用光的颜色乘以一个很小常量环境因子,再乘以物体的颜色,然后将最终结果作为片段的颜色:
```c++
void main()
{
float ambientStrength = 0.1f;
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
vec3 result = ambient * objectColor;
color = vec4(result, 1.0f);
FragColor = vec4(result, 1.0);
}
```
如果你现在运行你的程序,你会注意到冯氏光照的第一个阶段已经应用到你的物体上了。这个物体非常暗,但不是完全的黑暗,因为我们应用了环境光照(注意光立方体没被环境光照影响是因为我们对它使用了另一个着色器)。它看起来应该像这样:
如果你现在运行你的程序,你会注意到冯氏光照的第一个阶段已经应用到你的物体上了。这个物体非常暗,但由于应用了环境光照注意光立方体没影响是因为我们对它使用了另一个着色器),也不是完全黑的。它看起来应该像这样:
![](../img/02/02/ambient_lighting.png)
# 漫反射光照
环境光本身不提供最明显的光照效果,但是漫反射光照(Diffuse Lighting)会对物体产生显著的视觉影响。漫反射光使物体上与光线排布越近的片段能从光源处获得更多的亮度。为了更好的理解漫反射光照,请看下图:
环境光本身不提供最有趣的结果,但是漫反射光照就能开始对物体产生显著的视觉影响。漫反射光使物体上与光线方向越接近的片段能从光源处获得更多的亮度。为了能够更好的理解漫反射光照,请看下图:
![](../img/02/02/diffuse_light.png)
图左上方有一个光源,它所发出的光线落在物体的一个片段上。我们需要测量这个光线与它所接触片段之间的角度。如果光线垂直于物体表面,这束光对物体的影响会最大化(译注:更亮)。为了测量光线和片段的角度,我们使用一个叫做法向量(Normal Vector)的东西,它是垂直于片段表面的一向量(这里以黄色箭头表示),我们在后面再讲这个东西。两个向量之间的角度就能够根据点乘计算出来。
图左上方有一个光源,它所发出的光线落在物体的一个片段上。我们需要测量这个光线是以什么角度接触到这个片段的。如果光线垂直于物体表面,这束光对物体的影响会最大化译注:更亮。为了测量光线和片段的角度,我们使用一个叫做<def>法向量</def>(Normal Vector)的东西,它是垂直于片段表面的一向量这里以黄色箭头表示,我们在后面再讲这个东西。两个向量之间的角度很容易就能够通过点乘计算出来。
你可能记得在[变换](../01 Getting started/07 Transformations.md)那一节教程里,我们知道两个单位向量的角越小它们点乘的结果越倾向于1。当两个向量的角度是90度的时候点乘会变为0。这同样适用于θ,θ越大,光对片段颜色的影响越小。
你可能记得在[变换](../01 Getting started/07 Transformations.md)那一节教程里,我们知道两个单位向量的角越小它们点乘的结果越倾向于1。当两个向量的夹角为90度的时候点乘会变为0。这同样适用于\(\theta\)\(\theta\)越大,光对片段颜色的影响就应该越小。
!!! Important
注意,我们使用的是单位向量(Unit Vector长度1的向量)取得两个向量夹角的余弦值,所以我们需要确保所有的向量都标准化,否则点乘返回的就不仅仅是余弦值了(如果你不明白,可以复习[变换](../01 Getting started/07 Transformations.md)那一节的点乘部分)
注意,为了(只)得到两个向量夹角的余弦值,我们使用的是单位向量(长度1的向量,所以我们需要确保所有的向量都标准化,否则点乘返回的就不仅仅是余弦值了(见[变换](../01 Getting started/07 Transformations.md)
点乘返回一个标量,我们可以用它计算光线对片段颜色的影响,基于不同片段朝向光源的方向的不同,这些片段被照亮的情况也不同。
点乘返回一个标量,我们可以用它计算光线对片段颜色的影响不同片段朝向光源的方向的不同,这些片段被照亮的情况也不同。
所以,我们需要些什么来计算漫反射光照?
所以,计算漫反射光照需要什么
- 法向量:一个垂直于顶点表面的向量。
- 定向的光线:作为光的位置片段的位置之间向量差的方向向量。为了计算这个光线,我们需要光的位置向量和片段的位置向量。
- 定向的光线:作为光的位置片段的位置之间向量差的方向向量。为了计算这个光线,我们需要光的位置向量和片段的位置向量。
## 法向量
法向量(Normal Vector)是垂直于顶点表面的(单位)向量。由于顶点身并没有表面(它只是空间中一个独立的点),我们利用顶点周围的顶点计算出这个顶点的表面。我们能够使用叉乘这个技巧为立方体所有的顶点计算出法线但是由于3D立方体不是一个复杂的形状所以我们可以简单把法线数据手工添加到顶点数据中。更新的顶点数据数组可以在[这里](http://learnopengl.com/code_viewer.php?code=lighting/basic_lighting_vertex_data)找到。试着去想象一下,这些法向量真的是垂直于立方体各个面的表面的(一个立方体由6个面组成)
法向量是一个垂直于顶点表面的单位向量。由于顶点身并没有表面它只是空间中一个独立的点,我们利用周围的顶点计算出这个顶点的表面。我们能够使用一个小技巧,使用叉乘对立方体所有的顶点计算法向量但是由于3D立方体不是一个复杂的形状所以我们可以简单把法线数据手工添加到顶点数据中。更新的顶点数据数组可以在[这里](https://learnopengl.com/code_viewer.php?code=lighting/basic_lighting_vertex_data)找到。试着去想象一下,这些法向量真的是垂直于立方体各个面的表面的一个立方体由6个面组成
因为我们向顶点数组添加了额外的数据,所以我们应该更新光照的顶点着色器:
由于我们向顶点数组添加了额外的数据,所以我们应该更新光照的顶点着色器:
```c++
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
...
```
现在我们已经向每个顶点添加了一个法向量,已经更新了顶点着色器,我们还要更新顶点属性指针(Vertex Attibute Pointer)。注意,发光物使用同样的顶点数组作为它的顶点数据,然而发光物的着色器没有使用新添加的法向量。我们不会更新发光物的着色器或者属性配置,但是我们必须至少修改一下顶点属性指针来适应新的顶点数组的大小:
现在我们已经向每个顶点添加了一个法向量更新了顶点着色器,我们还要更新顶点属性指针。注意,使用同样的顶点数组作为它的顶点数据,然而的着色器没有使用新添加的法向量。我们不需要更新灯的着色器或者属性配置,但是我们必须至少修改一下顶点属性指针来适应新的顶点数组的大小:
```c++
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid * )0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
```
我们只想使用每个顶点的前三个浮点数,并且我们忽略后三个浮点数,所以我们只需要把**步长**参数改成`GLfloat`尺寸的6倍就行了。
我们只想使用每个顶点的前三个float,并且忽略后三个float,所以我们只需要把**步长**参数改成`float`大小的6倍就行了。
!!! Important
发光物着色器顶点数据的不完全使用看起来有点低效,但这些顶点数据已经从立方体对象载入GPU的内存里了所以GPU内存不是必须再储存新数据。相对于重新给发光物分配VBO实际上却是更高效了。
虽然对灯的着色器使用不能完全利用的顶点数据看起来不是那么高效,但这些顶点数据已经从箱子对象载入后开始就储存在GPU的内存里了所以我们并不需要储存新数据到GPU内存中。这实际上比给灯专门分配一个新的VBO更高效了。
所有光照的计算需要在片段着色器里进行,所以我们需要法向量由顶点着色器传递到片段着色器。我们这么做:
所有光照的计算都是在片段着色器里进行,所以我们需要法向量由顶点着色器传递到片段着色器。我们这么做:
```c++
out vec3 Normal;
void main()
{
gl_Position = projection * view * model * vec4(position, 1.0f);
Normal = normal;
gl_Position = projection * view * model * vec4(aPos, 1.0);
Normal = aNormal;
}
```
剩下要做的事情是,在片段着色器中定义相应的输入
接下来,在片段着色器中定义相应的输入变量
```c++
in vec3 Normal;
@@ -104,30 +106,29 @@ in vec3 Normal;
## 计算漫反射光照
每个顶点现在都有了法向量,但是我们仍然需要光的位置向量和片段的位置向量。由于光的位置是一个静态变量,我们可以简单在片段着色器中把它声明为uniform
我们现在对每个顶点都有了法向量,但是我们仍然需要光的位置向量和片段的位置向量。由于光的位置是一个静态变量,我们可以简单在片段着色器中把它声明为uniform
```c++
uniform vec3 lightPos;
```
然后再游戏循环中(外面也可以,因为它不会变)更新uniform。我们使用在前面教程中声明的`lightPos`向量作为光源位置:
然后在渲染循环中(渲染循环的外面也可以,因为它不会改变)更新uniform。我们使用在前面声明的<def>lightPos</def>向量作为光源位置:
```c++
GLint lightPosLoc = glGetUniformLocation(lightingShader.Program, "lightPos");
glUniform3f(lightPosLoc, lightPos.x, lightPos.y, lightPos.z);
lightingShader.setVec3("lightPos", lightPos);
```
最后,我们还需要片段的位置(Position)。我们会在世界空间中进行所有的光照计算,因此我们需要一个在世界空间中的顶点位置。我们可以通过把顶点位置属性乘以模型矩阵(Model Matrix,只用模型矩阵不需要用观察和投影矩阵)来把它变换到世界空间坐标。这个在顶点着色器中很容易完成,所以我们声明一个输出(out)变量,然后计算它的世界空间坐标:
最后,我们还需要片段的位置。我们会在世界空间中进行所有的光照计算,因此我们需要一个在世界空间中的顶点位置。我们可以通过把顶点位置属性乘以模型矩阵(不是观察和投影矩阵来把它变换到世界空间坐标。这个在顶点着色器中很容易完成,所以我们声明一个输出变量,计算它的世界空间坐标:
```c++
out vec3 FragPos;
out vec3 FragPos;
out vec3 Normal;
void main()
{
gl_Position = projection * view * model * vec4(position, 1.0f);
FragPos = vec3(model * vec4(position, 1.0f));
Normal = normal;
gl_Position = projection * view * model * vec4(aPos, 1.0);
FragPos = vec3(model * vec4(aPos, 1.0));
Normal = aNormal;
}
```
@@ -137,9 +138,9 @@ void main()
in vec3 FragPos;
```
现在,所有需要的变量都设置好了,我们可以在片段着色器中开始光照计算了。
现在,所有需要的变量都设置好了,我们可以在片段着色器中添加光照计算了。
我们需要做的第一件事是计算光源和片段位置之间的方向向量。前面提到,光的方向向量是光位置向量与片段位置向量之间的向量差。你可能记得,在变换教程中,我们简单通过两个向量相减的方式计算向量差。我们同样希望确保所有相关向量最后都转换为单位向量,所以我们把法线和方向向量这个结果都进行标准化:
我们需要做的第一件事是计算光源和片段位置之间的方向向量。前面提到,光的方向向量是光位置向量与片段位置向量之间的向量差。你可能记得在[变换](../01 Getting started/07 Transformations.md)教程中,我们能够简单通过两个向量相减的方式计算向量差。我们同样希望确保所有相关向量最后都转换为单位向量,所以我们把法线和最终的方向向量都进行标准化:
```c++
vec3 norm = normalize(Normal);
@@ -148,137 +149,138 @@ vec3 lightDir = normalize(lightPos - FragPos);
!!! Important
当计算光照时我们通常不关心一个向量的“量”或它的位置,我们只关心它们的方向。所有的计算都使用单位向量完成,因为这简化了大多数计算(比如点乘)。所以当进行光照计算时,确保你总是对相关向量进行标准化,这样它们才会保证自身为单位向量。忘记对向量进行标准化是一个十分常见的错误。
当计算光照时我们通常不关心一个向量的模长或它的位置,我们只关心它们的方向。所以,几乎所有的计算都使用单位向量完成,因为这简化了大部分的计算比如点乘。所以当进行光照计算时,确保你总是对相关向量进行标准化,来保证它们是真正地单位向量。忘记对向量进行标准化是一个十分常见的错误。
下一步,我们对`norm`和`lightDir`向量进行点乘,计算光对当前片段实际的射影响。结果值再乘以光的颜色,得到散射因子。两个向量之间的角度越大,散射因子就会越小:
下一步,我们对<var>norm</var>和<var>lightDir</var>向量进行点乘,计算光对当前片段实际的漫发射影响。结果值再乘以光的颜色,得到漫反射分量。两个向量之间的角度越大,漫反射分量就会越小:
```c++
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
```
如果两个向量之间的角度大于90度点乘的结果就会变成负数这样会导致散射因子变为负数。为此,我们使用`max`函数返回两个参数之间较大的参数,从而保证散射因子不会变成负数。负数颜色是没有实际定义的,所以最好避免它,除非你是那种古怪的艺术家。
如果两个向量之间的角度大于90度点乘的结果就会变成负数这样会导致漫反射分量变为负数。为此,我们使用<fun>max</fun>函数返回两个参数之间较大的参数,从而保证漫反射分量不会变成负数。负数颜色的光照是没有定义的,所以最好避免它,除非你是那种古怪的艺术家。
既然我们有了一个环境光照颜色和一个散射光颜色,我们把它们相加,然后把结果乘以物体的颜色,来获得片段最后的输出颜色。
现在我们有了环境光分量和漫反射分量,我们把它们相加,然后把结果乘以物体的颜色,来获得片段最后的输出颜色。
```c++
vec3 result = (ambient + diffuse) * objectColor;
color = vec4(result, 1.0f);
FragColor = vec4(result, 1.0);
```
如果你的应用(和着色器)编译成功了,你可能看到类似的输出:
![](../img/02/02/basic_lighting_diffuse.png)
你可以看到使用了射光照,立方体看起来就真的像个立方体了。尝试在你的脑中想象,通过移动正方体,法向量和光的方向向量之间的夹角大,片段变得更暗。
你可以看到使用了漫反射光照,立方体看起来就真的像个立方体了。尝试在你的脑中想象一下法向量,并在立方体周围移动,注意观察法向量和光的方向向量之间的夹角大,片段就会越暗。
如果你遇到很多困难,可以对比[完整的源代码](http://learnopengl.com/code_viewer.php?code=lighting/basic_lighting_diffuse)以及[片段着色器](http://learnopengl.com/code_viewer.php?code=lighting/basic_lighting_diffuse&type=fragment)代码。
如果你在哪卡住了,可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/2.1.basic_lighting_diffuse/basic_lighting_diffuse.cpp)对比一下完整的源代码。
## 最后一件事
现在我们已经把法向量从顶点着色器传到了片段着色器。可是,目前片段着色器里,我们都是在世界空间坐标中进行计算的,所以,我们不是应该把法向量转换为世界空间坐标?基本正确,但是这不是简单地把它乘以一个模型矩阵就能搞定的。
现在我们已经把法向量从顶点着色器传到了片段着色器。可是,目前片段着色器里的计算都是在世界空间坐标中进行的。所以,我们不是应该把法向量转换为世界空间坐标?基本正确,但是这不是简单地把它乘以一个模型矩阵就能搞定的。
首先,法向量只是一个方向向量,不能表达空间中的特定位置。同时,法向量没有齐次坐标(顶点位置中的w分量)。这意味着,移不应该影响到法向量。因此,如果我们打算把法向量乘以一个模型矩阵,我们就要把模型矩阵左上角3×3矩阵从模型矩阵移除(译注所谓移除就是设置为0),它是模型矩阵的平移部分(注意我们也可以把法向量的w分量设置为0再乘以4×4矩阵同样可以移除平移)。对于法向量,我们只能对它应用缩放(Scale)和旋转(Rotation)变换。
首先,法向量只是一个方向向量,不能表达空间中的特定位置。同时,法向量没有齐次坐标顶点位置中的w分量。这意味着,移不应该影响到法向量。因此如果我们打算把法向量乘以一个模型矩阵我们就要把模型矩阵左上角3×3的位移部分矩阵移除译注所谓移除就是设置为0注意我们也可以把法向量的w分量设置为0再乘以4×4矩阵同样可以移除位移)。对于法向量,我们只希望对它实施缩放和旋转变换。
其次,如果模型矩阵执行了不等比缩放,法向量不再垂直于表面了,顶点就会以这种方式被改变了。因此,我们不能用这样的模型矩阵去乘以法向量。下面的图展示了应用了不等比缩放的矩阵对法向量的影响:
其次,如果模型矩阵执行了不等比缩放,顶点的改变会导致法向量不再垂直于表面了。因此,我们不能用这样的模型矩阵来变换法向量。下面的图展示了应用了不等比缩放的模型矩阵对法向量的影响:
![](../img/02/02/basic_lighting_normal_transformation.png)
无论何时当我们提交一个不等比缩放(注意:等比缩放不会破坏法线,因为法线的方向没被改变,法线的长度很容易通过标准化进行修复),法向量就不会再垂直于它们的表面了,这样光照会被扭曲
当我们应用一个不等比缩放时(注意:等比缩放不会破坏法线,因为法线的方向没被改变,仅仅改变了法线的长度,而这很容易通过标准化修复,法向量就不会再垂直于对应的表面了,这样光照会被破坏
修复这个行为的诀窍是使用一个为法向量专门定制的模型矩阵。这个矩阵称之为正规矩阵(Normal Matrix),它是进行了一线性代数操作移除对法向量错误缩放效果。如果你想知道这个矩阵是如何计算出来的,建议看[这个文章](http://www.lighthouse3d.com/tutorials/glsl-tutorial/the-normal-matrix/)。
修复这个行为的诀窍是使用一个为法向量专门定制的模型矩阵。这个矩阵称之为<def>法线矩阵</def>(Normal Matrix),它使用了一线性代数操作移除对法向量错误缩放的影响。如果你想知道这个矩阵是如何计算出来的,建议去阅读这个[文章](http://www.lighthouse3d.com/tutorials/glsl-tutorial/the-normal-matrix/)。
正规矩阵被定义为模型矩阵左上角的逆矩阵的转置矩阵。真拗口,如果你不明白这是什么意思,别担心我们还没有讨论逆矩阵(Inverse Matrix)和转置矩阵(Transpose Matrix)。注意,定义正规矩阵的大多资源就像应用到模型观察矩阵(Model-view Matrix)上的操作一样,但是由于我们只在世界空间工作(而不是在观察空间),我们只使用模型矩阵。
法线矩阵被定义为模型矩阵左上角的逆矩阵的转置矩阵。真拗口,如果你不明白这是什么意思,别担心我们还没有讨论逆矩阵(Inverse Matrix)和转置矩阵(Transpose Matrix)。注意,大部分的资源都会将法线矩阵定义为应用到模型-观察矩阵(Model-view Matrix)上的操作,但是由于我们只在世界空间中进行操作(不是在观察空间,我们只使用模型矩阵。
在顶点着色器中,我们可以使用`inverse`和`transpose`函数自己生成正规矩阵,`inverse`和`transpose`函数对所有类型矩阵都有效。注意我们要把这个被处理过的矩阵强制转换为3×3矩阵这是为了保证它失去了移属性,之后它才能乘以法向量。
在顶点着色器中,我们可以使用<fun>inverse</fun>和<fun>transpose</fun>函数自己生成这个法线矩阵,这两个函数对所有类型矩阵都有效。注意我们要把被处理过的矩阵强制转换为3×3矩阵保证它失去了移属性以及能够乘以`vec3`的法向量。
```c++
Normal = mat3(transpose(inverse(model))) * normal;
Normal = mat3(transpose(inverse(model))) * aNormal;
```
环境光照部分,光照表现问题,这是因为我们没有对物体本身执行任何缩放操作,因而不是非得使用正规矩阵不可,用模型矩阵乘以法线也没错。可是,如果你进行了不等比缩放,使用正规矩阵去乘以法向量就是必不可少的了。
漫反射光照部分,光照表现并没有问题,这是因为我们没有对物体本身执行任何缩放操作,所以并不是必须要使用一个法线矩阵,仅仅让模型矩阵乘以法线也可以。可是,如果你进行了不等比缩放,使用法线矩阵去乘以法向量就是必不可少的了。
!!! Attention
对于着色器来说,逆矩阵也是一开销比较大的操作,因此,无论何时,在着色器中只要可能就应该尽量避免逆操作,因为它们必须为你场景中的每个顶点进行这样的处理。以学习的目的这样做很好,但是对于一个对效率有要求的应用来说,在绘制之前你最好用CPU计算出正规矩阵然后通过uniform把值传递给着色器(和模型矩阵一样)
即使是对于着色器来说,逆矩阵也是一开销比较大的运算,因此,只要可能就应该避免在着色器中进行逆矩阵运算,它们必须为你场景中的每个顶点进行这样的处理。用作学习目这样做是可以的但是对于一个对效率有要求的应用来说在绘制之前你最好用CPU计算出法线矩阵然后通过uniform把值传递给着色器(像模型矩阵一样
# 镜面光照
如果你还没被这些光照计算搞得精疲力尽,我们就再把镜面高光(Specular Highlight)加进来,这样冯氏光照才算完整。
环境光照一样,镜面光照(Specular Lighting)同样依据光的方向向量和物体的法向量,但是这次它也会依据观察方向,例如玩家是从什么方向看着这个片段的。镜面光照根据光的反射特性。如果我们想象物体表面像一面镜子一样,那么,无论我们从哪里去看那个表面所反射的光,镜面光照都会达到最大化。你可以从下面的图片看到效果:
漫反射光照一样,镜面光照也是依据光的方向向量和物体的法向量来决定的,但是它也依赖于观察方向,例如玩家是从什么方向看着这个片段的。镜面光照是基于光的反射特性。如果我们想象物体表面像一面镜子一样,那么,无论我们从哪里去看那个表面所反射的光,镜面光照都会达到最大化。你可以从下面的图片看到效果:
![](../img/02/02/basic_lighting_specular_theory.png)
我们通过反射法向量周围光的方向计算反射向量。然后我们计算反射向量和视线方向的角度,如果之间的角度越小,那么镜面光的作用就会越大。它的作用效果就是,当我们去看光被物体所反射的那个方向的时候,我们会看到一个高光。
我们通过反射法向量周围光的方向计算反射向量。然后我们计算反射向量和视线方向的角度,如果夹角越小,那么镜面光的影响就会越大。它的作用效果就是,当我们去看光被物体所反射的那个方向的时候,我们会看到一个高光。
观察向量是镜面光照的一个附加变量,我们可以使用观察者世界空间位置(Viewers World Space Position)和片段的位置来计算。之后,我们计算镜面光度,用它乘以光的颜色,在用它加上作为之前计算的光照颜色
观察向量是镜面光照附加的一个变量,我们可以使用观察者世界空间位置和片段的位置来计算。之后,我们计算镜面光度,用它乘以光的颜色,再将它加上环境光和漫反射分量
!!! Important
我们选择在世界空间(World Space)进行光照计算,但是大多数人趋向于在观察空间(View Space)进行光照计算。在观察空间计算的好处是,观察者的位置总是(0, 0, 0),所以这样你直接就获得了观察者位置。可是我发现出于学习的目的,在世界空间计算光照更符合直觉。如果你仍然希望在视野空间计算光照的话,那就使用观察矩阵应用到所有相关的需要变换的向量(不要忘记,也要改变正规矩阵)
我们选择在世界空间进行光照计算,但是大多数人趋向于在观察空间进行光照计算。在观察空间计算的好处是,观察者的位置总是(0, 0, 0),所以这样你直接就获得了观察者位置。可是我发现学习的时候在世界空间计算光照更符合直觉。如果你仍然希望在观察空间计算光照的话,你需要将所有相关的向量都用观察矩阵进行变换(记得也要改变法线矩阵
为了得到观察者的世界空间坐标,我们简单地使用摄像机对象的位置坐标代替(它就是观察者)。所以我们把另一个uniform添加到片段着色器把相应的摄像机位置坐标传给片段着色器
为了得到观察者的世界空间坐标,我们简单地使用摄像机对象的位置坐标代替(它当然就是观察者。所以我们把另一个uniform添加到片段着色器把相应的摄像机位置坐标传给片段着色器
```c++
uniform vec3 viewPos;
GLint viewPosLoc = glGetUniformLocation(lightingShader.Program, "viewPos");
glUniform3f(viewPosLoc, camera.Position.x, camera.Position.y, camera.Position.z);
```
现在我们已经获得所有需要的变量,可以计算高光亮度了。首先,我们定义一个镜面强度(Specular Intensity)变量`specularStrength`,给镜面高光一个中等亮度颜色,这样就不会产生过度的影响了。
```c++
float specularStrength = 0.5f;
lightingShader.setVec3("viewPos", camera.Position);
```
如果我们把它设置为1.0f,我们会得到一个对于珊瑚色立方体来说过度明亮的镜面亮度因子。下一节教程,我们会讨论所有这些光照亮度的合理设置,以及它们是如何影响物体的。下一步,我们计算视线方向坐标,和沿法线轴的对应的反射坐标:
现在我们已经获得所有需要的变量,可以计算高光强度了。首先,我们定义一个镜面强度(Specular Intensity)变量,给镜面高光一个中等亮度颜色,让它不要产生过度的影响。
```c++
float specularStrength = 0.5;
```
如果我们把它设置为1.0f,我们会得到一个非常亮的镜面光分量,这对于一个珊瑚色的立方体来说有点太多了。下一节教程中我们会讨论如何合理设置这些光照强度,以及它们是如何影响物体的。下一步,我们计算视线方向向量,和对应的沿着法线轴的反射向量:
```
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
```
需要注意的是我们使用了`lightDir`向量的相反数。`reflect`函数要求第一个是从光源指向片段位置的向量,但是`lightDir`当前是从片段指向光源的向量(由先前我们计算`lightDir`向量时,(减数和被减数)减法的顺序决定)。为了保证我们得到正确的`reflect`坐标,我们通过`lightDir`向量的相反数获得它的方向的反向。第二个参数要求是一个法向量,所以我们提供的是已标准化的`norm`向量。
需要注意的是我们`lightDir`向量进行了取反。`reflect`函数要求第一个向量是**从**光源指向片段位置的向量,但是`lightDir`当前正好相反,是从片段**指向**光源(由先前我们计算`lightDir`向量时,减法的顺序决定。为了保证我们得到正确的`reflect`向量,我们通过`lightDir`向量取反来获得相反的方向。第二个参数要求是一个法向量,所以我们提供的是已标准化的`norm`向量。
剩下要做的是计算镜面亮度分量。下面的代码完成了这件事:
剩下要做的是计算镜面分量。下面的代码完成了这件事:
```c++
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;
```
我们先计算视线方向与反射方向的点乘(确保它不是负值),然后得到它的32次幂。这个32是高光的**发光值(Shininess)**。一个物体的发光值越高,反射光的能力越强,散射得越少,高光点越小。在下面的图片里,你会看到不同发光值对视觉(效果)的影响:
我们先计算视线方向与反射方向的点乘(并确保它不是负值,然后它的32次幂。这个32是高光的<def>反光度</def>(Shininess)。一个物体的反光度越高,反射光的能力越强,散射得越少,高光点就会越小。在下面的图片里,你会看到不同反光度的视觉效果影响:
![](../img/02/02/basic_lighting_specular_shininess.png)
我们不希望镜面成分过于显眼,所以我们把指数设置为32。剩下的最后一件事情是把它加到环境光颜色和散射光颜色里,然后再乘以物体颜色:
我们不希望镜面成分过于显眼,所以我们把指数保持为32。剩下的最后一件事情是把它加到环境光分量和漫反射分量里,再用结果乘以物体颜色:
```c++
vec3 result = (ambient + diffuse + specular) * objectColor;
color = vec4(result, 1.0f);
FragColor = vec4(result, 1.0);
```
我们现在为冯氏光照计算了全部的光照元素。根据你的观察点,你可以看到类似下面的画面:
我们现在为冯氏光照计算了全部的光照分量。根据你的视角,你可以看到类似下面的画面:
![](../img/02/02/basic_lighting_specular.png)
你可以[在这里找到完整源码](http://learnopengl.com/code_viewer.php?code=lighting/basic_lighting_specular),在这里有[顶点](http://learnopengl.com/code_viewer.php?code=lighting/basic_lighting&type=vertex)和[片段](http://learnopengl.com/code_viewer.php?code=lighting/basic_lighting&type=fragment)着色器
你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/2.2.basic_lighting_specular/basic_lighting_specular.cpp)找到完整源码
!!! Important
早期的光照着色器,开发者在顶点着色器中实现冯氏光照。在顶点着色器中做这件事的优势是,相比片段来说,顶点要少得多,因此会更高效,所以(开销大的)光照计算频率会更低。然而,顶点着色器中的颜色值是只是顶点的颜色值,片段的颜色值是它与周围的颜色值的插值。结果就是这种光照看起来不会非常真实,除非使用了大量顶点。
光照着色器的早期,开发者曾经在顶点着色器中实现冯氏光照模型。在顶点着色器中做光照的优势是,相比片段来说,顶点要少得多,因此会更高效,所以开销大的光照计算频率会更低。然而,顶点着色器中的最终颜色值是仅仅只是那个顶点的颜色值,片段的颜色值是由插值光照颜色所得来的。结果就是这种光照看起来不会非常真实,除非使用了大量顶点。
![](../img/02/02/basic_lighting_gouruad.png)
在顶点着色器中实现的冯氏光照模型叫做Gouraud着色而不是冯氏着色。记住由于插值,这种光照起来有点逊色。冯氏着色能产生更平滑的光照效果。
在顶点着色器中实现的冯氏光照模型叫做<def>Gouraud着色</def>(Gouraud Shading),而不是<def>冯氏着色</def>(Phong Shading)。记住由于插值,这种光照起来有点逊色。冯氏着色能产生更平滑的光照效果。
现在你可以看到着色器的强大之处了。只用很少的信息,着色器就能计算出光照影响到为我们所有物体的片段颜色。[下一节](03 Materials.md)中,我们会更深入的研究光照模型,看看我们还能做些什么。
现在你应该能够看到着色器的强大之处了。只用很少的信息,着色器就能计算出光照如何影响到所有物体的片段颜色。[下一节](03 Materials.md)中,我们会更深入的研究光照模型,看看我们还能做些什么。
## 练习
- 目前,我们的光源时静止的,你可以尝试使用`sin`和`cos`函数让光源在场景中来回移动,此时再观察光照效果能让你更容易理解冯氏光照模型。[参考解答](http://learnopengl.com/code_viewer.php?code=lighting/basic_lighting-exercise1)。
- 尝试使用不同的环境光、散射镜面强度,观察光照效果。改变镜面光照的`shininess`因子试试
- 在观察空间(而不是世界空间)中计算冯氏光照:[参考解答](http://learnopengl.com/code_viewer.php?code=lighting/basic_lighting-exercise2)。
- 尝试实现一个Gouraud光照来模拟冯氏光照,[参考解答](http://learnopengl.com/code_viewer.php?code=lighting/basic_lighting-exercise3)。
- 目前,我们的光源时静止的,你可以尝试使用<fun>sin</fun><fun>cos</fun>函数让光源在场景中来回移动。观察光照随时间的改变能让你更容易理解冯氏光照模型。[参考解答](https://learnopengl.com/code_viewer.php?code=lighting/basic_lighting-exercise1)。
- 尝试使用不同的环境光、漫反射和镜面强度,观察它们怎么是影响光照效果的。同样,尝试实验一下镜面光照的反光度因子。尝试理解为什么某一个值能够有着某一种视觉输出
- 在观察空间而不是世界空间中计算冯氏光照:[参考解答](https://learnopengl.com/code_viewer.php?code=lighting/basic_lighting-exercise2)。
- 尝试实现一个Gouraud着色(而不是冯氏着色)。如果你做对了话,立方体的光照应该会[看起来有些奇怪](../img/02/02/basic_lighting_exercise3.png),尝试推理为什么它会看起来这么奇怪:[参考解答](https://learnopengl.com/code_viewer.php?code=lighting/basic_lighting-exercise3)。

View File

@@ -3,176 +3,170 @@
原文 | [Materials](http://learnopengl.com/#!Lighting/Materials)
---|---
作者 | JoeyDeVries
翻译 | [Django](http://bullteacher.com/)
校对 | [Geequlim](http://geequlim.com)
翻译 | Meow J
校对 | 未校对
实世界里,每个物体会对光产生不同的反应。钢看起来比陶瓷花瓶更闪闪发光,一个木头箱子不会像钢箱子样对光产生很强的反射。每个物体对镜面高光也有不同的反应。有些物体不会散射(Scatter)很多光却会反射(Reflect)很多光,结果看起来就有一个较小的高光点(Highlight)有些物体散射很多,它们就会产生一个半径更大的高光。如果我们想要在OpenGL中模拟多种类型的物体我们必须为每个物体分别定义材质(Material)属性。
实世界里,每个物体会对光产生不同的反应。比如说,钢看起来通常会比陶瓷花瓶更闪闪发光,木头箱子不会像钢箱子样对光产生很强的反射。每个物体对镜面高光也有不同的反应。有些物体反射光的时候不会有太多的散射(Scatter),因而产生一个较小的高光点,而有些物体则会散射很多,产生一个有着更大半径的高光。如果我们想要在OpenGL中模拟多种类型的物体我们必须为每个物体分别定义一个<def>材质</def>(Material)属性。
前面的教程中,我们指定一个物体和一个光的颜色来定义物体的图像输出,并使之结合环境(Ambient)和镜面强度(Specular Intensity)元素。当描述物体的时候,我们可以使用3种光照元素:环境光照(Ambient Lighting)、漫反射光照(Diffuse Lighting)镜面光照(Specular Lighting)定义一个材质颜色。通过为每个元素指定一个颜色,我们已经对物体的颜色输出有了精密的控制。现在把一个镜面高光元素添加到这三个颜色,这我们需要的所有材质属性:
上一节中,我们指定一个物体和光的颜色,以及结合环境光和镜面强度分量,来定义物体的视觉输出。当描述一个物体的时候,我们可以用这三个分量来定义一个材质颜色(Material Color):环境光照(Ambient Lighting)、漫反射光照(Diffuse Lighting)镜面光照(Specular Lighting)。通过为每个分量指定一个颜色,我们就能够对物体的颜色输出有着精细的控制。现在,我们再添加反光度(Shininess)这个分量到上述的三个颜色,这就有我们需要的所有材质属性
```c++
#version 330 core
struct Material
{
struct Material {
vec3 ambient;
vec3 diffuse;
vec3 specular;
float shininess;
};
};
uniform Material material;
```
在片段着色器中,我们创建一个结构体(Struct)来储存物体的材质属性。我们也可以把它们储存为独立的uniform值但是作为一个结构体来储存可以更有条理。我们首先定义结构体的布局,然后简单声明一个uniform变量,以新创建的结构体作为它的类型
在片段着色器中,我们创建一个结构体(Struct)来储存物体的材质属性。我们也可以把它们储存为独立的uniform值但是作为一个结构体来储存更有条理一些。我们首先定义结构体的布局(Layout),然后使用刚创建的结构体为类型,简单声明一个uniform变量。
就像你所看到,我们为每个冯氏光照模型的元素都定义一个颜色向量。`ambient`材质向量定义了在环境光照下这个物体反射是什么颜色通常这是和物体颜色相同的颜色。`diffuse`材质向量定义了在漫反射光照下物体的颜色。漫反射颜色设置为(和环境光照一样)我们需要的物体颜色。`specular`材质向量设置的是物体受到的镜面光照的影响的颜色(或者可能反射一个物体特定的镜面高光颜色)。最后,`shininess`影响镜面高光的散射/半径。
你可以看到,我们为每个冯氏光照模型的分量都定义一个颜色向量。<var>ambient</var>材质向量定义了在环境光照下这个物体反射是什么颜色通常这是和物体颜色相同的颜色。<var>diffuse</var>材质向量定义了在漫反射光照下物体的颜色。(和环境光照一样)漫反射颜色也要设置为我们需要的物体颜色。<var>specular</var>材质向量设置的是镜面光照对物体的颜色影响(或者甚至可能反射一个物体特定的镜面高光颜色。最后,<var>shininess</var>影响镜面高光的散射/半径。
这四个元素定义了一个物体的材质,通过它们我们能够模拟很多实世界的材质。这里有一个列表[devernay.free.fr](http://devernay.free.fr/cours/opengl/materials.html)展示了几种材质属性,这些材质属性模拟外部世界的真实材质。下面的图片展示了几种实世界材质对我们的立方体的影响:
这四个元素定义了一个物体的材质,通过它们我们能够模拟很多实世界的材质。[devernay.free.fr](http://devernay.free.fr/cours/opengl/materials.html)上的一个表格展示了几种材质属性,它们模拟了现实世界的真实材质。下面的图片展示了几种实世界材质对我们的立方体的影响:
![](../img/02/03/materials_real_world.png)
如你所见,正确地指定一个物体的材质属性,似乎就是改变我们物体的相关属性的比例。效果显然很引人注目,但是对于大多数真实效果,我们最终需要更加复杂的形状,而不单单是一个立方体。在[后面的教程](../03 Model Loading/01 Assimp.md)中,我们会讨论更复杂的形状。
可以看到,通过正确地指定一个物体的材质属性,我们对这个物体的感知也就不同了。效果非常明显,但是要想获得更真实效果,我们最终需要更加复杂的形状,而不单单是一个立方体。在[后面的教程](../03 Model Loading/01 Assimp.md)中,我们会讨论更复杂的形状。
为一个物体赋予一款正确的材质是非常困难的,这需要大量实验和丰富的经验,所以由于错误的设置材质而毁了物体的画面质量是件经常发生的事。
为一个物体赋予一款合适的材质是非常困难的,这需要大量实验和丰富的经验,所以由于不合适的材质而毁了物体的视觉质量是件经常发生的事。
让我们试试在着色器中实现这样的一个材质系统。
让我们在着色器中实现这样的一个材质系统。
# 设置材质
我们在片段着色器中创建了一个uniform材质结构体,所以下面我们希望改变光照计算来顺应新的材质属性。由于所有材质元素都储存在结构体中我们可以从uniform变量`material`取得它们:
我们在片段着色器中创建了一个材质结构体的uniform所以下面我们希望修改一下光照计算来顺应新的材质属性。由于所有材质变量都储存在结构体中我们可以从uniform变量<var>material</var>中访问它们:
```c++
void main()
{
{
// 环境光
vec3 ambient = lightColor * material.ambient;
// 漫反射
// 漫反射
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = lightColor * (diff * material.diffuse);
// 镜面
// 镜面光
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
vec3 specular = lightColor * (spec * material.specular);
vec3 result = ambient + diffuse + specular;
color = vec4(result, 1.0f);
FragColor = vec4(result, 1.0);
}
```
可以看到,我们现在获得所有材质结构体的属性,无论在哪儿我们都需要它们,这次通过材质颜色的帮助,计算结果输出颜色。物体的每个材质属性都乘它们对应的光照元素
可以看到,我们现在在需要的地方访问了材质结构体中的所有属性,并且这次是根据材质颜色来计算最终的输出颜色。物体的每个材质属性都乘上了它们对应的光照分量
通过设置适当的uniform我们可以在应用中设置物体的材质。当设置uniform时GLSL中的一个结构体并不会被认为有什么特别之处。一个结构体值扮演uniform变量的封装,所以如果我们希望填充这个结构体,我们就仍然必须设置结构体中的各个元素的uniform,但这次带有结构体名字作为前缀:
我们现在可以在程序中设置适当的uniform对物体设置材质了。GLSL中的结构体在设置uniform时并没有什么特别之处。结构体只是作为uniform变量的一个封装,所以如果填充这个结构体的话,我们仍需要对每个单独的uniform进行设置,但这次要带上结构体名前缀:
```c++
GLint matAmbientLoc = glGetUniformLocation(lightingShader.Program, "material.ambient");
GLint matDiffuseLoc = glGetUniformLocation(lightingShader.Program, "material.diffuse");
GLint matSpecularLoc = glGetUniformLocation(lightingShader.Program, "material.specular");
GLint matShineLoc = glGetUniformLocation(lightingShader.Program, "material.shininess");
glUniform3f(matAmbientLoc, 1.0f, 0.5f, 0.31f);
glUniform3f(matDiffuseLoc, 1.0f, 0.5f, 0.31f);
glUniform3f(matSpecularLoc, 0.5f, 0.5f, 0.5f);
glUniform1f(matShineLoc, 32.0f);
lightingShader.setVec3("material.ambient", 1.0f, 0.5f, 0.31f);
lightingShader.setVec3("material.diffuse", 1.0f, 0.5f, 0.31f);
lightingShader.setVec3("material.specular", 0.5f, 0.5f, 0.5f);
lightingShader.setFloat("material.shininess", 32.0f);
```
我们将`ambient`和`diffuse`元素设置成我们想要让物体所呈现的颜色,设置物体的`specular`元素为中等亮度颜色我们不希望`specular`元素对这个指定物体产生过于强烈的影响。我们同样设置`shininess`为32。我们现在可以简单的在应用中影响物体的材质。
我们将环境光和漫反射分量设置成我们想要让物体所拥有的颜色,而将镜面分量设置为一个中等亮度颜色我们不希望镜面分量在这个物体上过于强烈。我们将反光度保持为32。现在我们能够程序中非常容易地修改物体的材质
运行程序,会得到下面这样的结果:
运行程序,你应该会得到下面这样的结果:
![](../img/02/03/materials_with_material.png)
看起来很奇怪不是吗?
但它看起来很奇怪不是吗?
## 光的属性
这个物体太亮了。物体过亮的原因是环境、漫反射和镜面三个颜色任何一个光源都会去全力反射。光源对环境、漫反射和镜面元素同时具有不同的强度。前面的教程,我们通过使用一个强度值改变环境和镜面强度的方式解决了这个问题。我们想做一个相同的系统,但是这次为每个光照元素指定了强度向量。如果我们想象`lightColor`是`vec3(1.0)`,代码看起来像这样:
这个物体太亮了。物体过亮的原因是环境、漫反射和镜面光这三个颜色任何一个光源都会去全力反射。光源对环境、漫反射和镜面光分量也具有不同的强度。前面的教程,我们通过使用一个强度值改变环境和镜面强度的方式解决了这个问题。我们想做一个类似的系统,但是这次是要为每个光照分量都指定一个强度向量。如果我们假设<var>lightColor</var>是`vec3(1.0)`,代码看起来像这样:
```c++
vec3 ambient = vec3(1.0f) * material.ambient;
vec3 diffuse = vec3(1.0f) * (diff * material.diffuse);
vec3 specular = vec3(1.0f) * (spec * material.specular);
vec3 ambient = vec3(1.0) * material.ambient;
vec3 diffuse = vec3(1.0) * (diff * material.diffuse);
vec3 specular = vec3(1.0) * (spec * material.specular);
```
所以物体的每个材质属性返回了每个光照元素的全强度。这些vec3(1.0)值可以各自独立的影响各个光源,这通常就是我们想要的。现在物体的`ambient`元素完全地展示了立方体的颜色,可是环境元素不应该对最终颜色有这么大的影响,所以我们要设置光的`ambient`亮度为一个小一点的值,从而限制环境色:
所以物体的每个材质属性对每一个光照分量都返回了最大的强度。对单个光源来说,这些`vec3(1.0)`值同样可以分别改变,而这通常就是我们想要的。现在物体的环境光分量完全地影响了立方体的颜色,可是环境光分量实际上不应该对最终颜色有这么大的影响,所以我们会将光源的环境光强度设置为一个小一点的值,从而限制环境光颜色:
```c++
vec3 result = vec3(0.1f) * material.ambient;
vec3 ambient = vec3(0.1) * material.ambient;
```
我们可以用同样的方式影响光源`diffuse`和`specular`的强度。这和我们[前面教程](http://learnopengl-cn.readthedocs.org/zh/latest/02%20Lighting/02%20Basic%20Lighting/)所做的极为相似你可以说我们已经创建了一些光属性来各自独立地影响每个光照元素。我们希望为光属性创建一与材质结构体似的东西
我们可以用同样的方式修改光源的漫反射和镜面光强度。这和我们在[上一节](02 Basic Lighting.md)中所做的极为相似你可以说我们已经创建了一些光属性来影响每个单独的光照分量。我们希望为光属性创建一与材质结构体似的结构体
```c++
struct Light
{
struct Light {
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
uniform Light light;
```
一个光源的`ambient`、`diffuse`和`specular`光都有不同的度。环境光通常设置为一个比较低的度,因为我们不希望环境色太过显眼。光源的`diffuse`元素通常设置为我们希望光所具有的颜色;经常是一个明亮的白色。`specular`元素通常被设置为`vec3(1.0f)`类型的全强度发光。要记住的是我们同样把光的位置添加到结构体中。
一个光源对它的<var>ambient</var>、<var>diffuse</var>和<var>specular</var>光照有着不同的度。环境光通常设置为一个比较低的度,因为我们不希望环境光颜色太过显眼。光源的漫反射分量通常设置为光所具有的颜色,通常是一个比较明亮的白色。镜面光分量通常会保持为`vec3(1.0)`,以最大强度发光。注意我们也将光源的位置添加到结构体中。
就像材质uniform一样需要更新片段着色器
材质uniform一样我们需要更新片段着色器:
```c++
vec3 ambient = light.ambient * material.ambient;
vec3 diffuse = light.diffuse * (diff * material.diffuse);
vec3 ambient = light.ambient * material.ambient;
vec3 diffuse = light.diffuse * (diff * material.diffuse);
vec3 specular = light.specular * (spec * material.specular);
```
然后我们要在应用里设置光的亮度:
我们接下来在程序中设置光照强度:
```c++
GLint lightAmbientLoc = glGetUniformLocation(lightingShader.Program, "light.ambient");
GLint lightDiffuseLoc = glGetUniformLocation(lightingShader.Program, "light.diffuse");
GLint lightSpecularLoc = glGetUniformLocation(lightingShader.Program, "light.specular");
glUniform3f(lightAmbientLoc, 0.2f, 0.2f, 0.2f);
glUniform3f(lightDiffuseLoc, 0.5f, 0.5f, 0.5f);// 让我们把这个光调暗一点,这样会看起来更自然
glUniform3f(lightSpecularLoc, 1.0f, 1.0f, 1.0f);
lightingShader.setVec3("light.ambient", 0.2f, 0.2f, 0.2f);
lightingShader.setVec3("light.diffuse", 0.5f, 0.5f, 0.5f); // 将光照调暗了一些以搭配场景
lightingShader.setVec3("light.specular", 1.0f, 1.0f, 1.0f);
```
现在我们调整了光是如何影响物体所有的材质的,我们得到一个更像前面教程的视觉输出。这次我们完全控制了物体光照和材质
现在我们调整了光照对物体材质的影响,我们应该能得到一个更类似于上一节的视觉效果。但这次我们有了对光照和物体材质的完全掌控
![](../img/02/03/materials_light.png)
现在改变物体的外观相对简单了些。我们做点更有趣的事!
改变物体的视觉效果现在变得相对容易了。让我们做点更有趣的事!
## 不同的光源颜色
目前为止,我们使用光源的颜色仅仅去改变物体各个元素的强度(通过选用从白到灰到黑范围内的颜色),并没有影响物体的真颜色(只是强度)。由于现在能够非常容易地访问光的属性了,我们可以随着时间改变它们的颜色获得一些有意思的效果。由于所有东西都已经在片段着色器做好了,改变光的颜色简单,我们可以立即创建出一些有趣的效果:
目前为止,我们都只对光源设置了从白到灰到黑范围内的颜色,这样只会改变物体各个分量的强度,而不是它的真颜色。由于现在能够非常容易地访问光的属性了,我们可以随着时间改变它们的颜色,从而获得一些非常有意思的效果。由于所有东西都在片段着色器中配置好了,修改光源的颜色非常简单,我们能够立刻创造一些有趣的效果:
<video src="../../img/02/03/materials.mp4" controls="controls">
</video>
如你所见,不同光的颜色极大地影响物体的颜色输出。由于光颜色直接影响物体反射的颜色(你可能想起在颜色教程中有讨论过),它对视觉输出有显著的影响。
你可以看到,不同的光照颜色能够极大地影响物体的最终颜色输出。由于光颜色能够直接影响物体能够反射的颜色(回想[颜色](01 Colors.md)这一节),这对视觉输出有显著的影响。
利用`sin`和`glfwGetTime`改变光的环境和漫反射颜色,我们可以随着时间流逝简单的改变光源颜色
我们可以利用<fun>sin</fun>和<fun>glfwGetTime</fun>函数改变光的环境和漫反射颜色,从而很容易地让光源的颜色随着时间变化
```c++
glm::vec3 lightColor; lightColor.x = sin(glfwGetTime() * 2.0f);
glm::vec3 lightColor;
lightColor.x = sin(glfwGetTime() * 2.0f);
lightColor.y = sin(glfwGetTime() * 0.7f);
lightColor.z = sin(glfwGetTime() * 1.3f);
glm::vec3 diffuseColor = lightColor * glm::vec3(0.5f);
glm::vec3 ambientColor = diffuseColor * glm::vec3(0.2f);
glUniform3f(lightAmbientLoc, ambientColor.x, ambientColor.y, ambientColor.z);
glUniform3f(lightDiffuseLoc, diffuseColor.x, diffuseColor.y, diffuseColor.z);
glm::vec3 diffuseColor = lightColor * glm::vec3(0.5f); // 降低影响
glm::vec3 ambientColor = diffuseColor * glm::vec3(0.2f); // 很低的影响
lightingShader.setVec3("light.ambient", ambientColor);
lightingShader.setVec3("light.diffuse", diffuseColor);
```
尝试实验使用这些光照和材质值,看看它们怎样影响图像输出的。你可以从这里找到[程序的源码](http://learnopengl.com/code_viewer.php?code=lighting/materials)[片段着色器](http://learnopengl.com/code_viewer.php?code=lighting/materials&type=fragment)的源码。
尝试实验些光照和材质值,看看它们怎样影响视觉输出的。你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/3.1.materials/materials.cpp)找到程序的源码。
## 练习
- 你能像我们教程一开始那样根据一些材质的属性来模拟一个真实世界的物体吗?
注意[材质表](http://devernay.free.fr/cours/opengl/materials.html)中的环境光颜色与漫反射光的颜色可能不一样,因为他们并没有把光照强度考虑进去来模拟,你需要将光照颜色的强度改为`vec(1.0f)`来输出正确的结果:[参考解答](http://learnopengl.com/code_viewer.php?code=lighting/materials-exercise1),我做了一个青色(Cyan)的塑料箱子
- 你能像教程一开始那样,定义相应的材质来模拟现实世界的物体吗?注意[材质表格](http://devernay.free.fr/cours/opengl/materials.html)中的环境光值可能与漫反射值不一样,它们没有考虑光照的强度。要想纠正这一问题,你需要将所有的光照强度都设置为`vec3(1.0)`,这样才能得到正确的输出:[参考解答](https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/3.2.materials_exercise1/materials_exercise1.cpp),我做的是青色塑料(Cyan Plastic)的箱子
- 你能像我们教程一开始那样根据一些材质的属性来模拟一个真实世界的物体吗?注意[材质表](http://devernay.free.fr/cours/opengl/materials.html)中的环境光颜色与漫反射光的颜色可能不一样,因为他们并没有把光照强度考虑进去来模拟,你需要将光照颜色的强度改为`vec(1.0f)`来输出正确的结果:[参考解答](http://learnopengl.com/code_viewer.php?code=lighting/materials-exercise1),我做了一个青色(Cyan)的塑料箱子

View File

@@ -4,7 +4,7 @@
---|---
作者 | JoeyDeVries
翻译 | Meow J
校对 | 未校对
校对 | [KenLee](https://hellokenlee.github.io/)
我们现在一直使用的光照方式叫做**正向渲染(Forward Rendering)**或者**正向着色法(Forward Shading)**,它是我们渲染物体的一种非常直接的方式,在场景中我们根据所有光源照亮一个物体,之后再渲染下一个物体,以此类推。它非常容易理解,也很容易实现,但是同时它对程序性能的影响也很大,因为对于每一个需要渲染的物体,程序都要对每一个光源每一个需要渲染的片段进行迭代,这是**非常**多的!因为大部分片段着色器的输出都会被之后的输出覆盖,正向渲染还会在场景中因为高深的复杂度(多个物体重合在一个像素上)浪费大量的片段着色器运行时间。
@@ -24,6 +24,8 @@
这种渲染方法一个很大的好处就是能保证在G缓冲中的片段和在屏幕上呈现的像素所包含的片段信息是一样的因为深度测试已经最终将这里的片段信息作为最顶层的片段。这样保证了对于在光照处理阶段中处理的每一个像素都只处理一次所以我们能够省下很多无用的渲染调用。除此之外延迟渲染还允许我们做更多的优化从而渲染更多的光源。
当然这种方法也带来几个缺陷, 由于G缓冲要求我们在纹理颜色缓冲中存储相对比较大的场景数据这会消耗比较多的显存尤其是类似位置向量之类的需要高精度的场景数据。 另外一个缺点就是他不支持混色(因为我们只有最前面的片段信息) 因此也不能使用MSAA了。针对这几个问题我们可以做一些变通来克服这些缺点这些我们留会在教程的最后讨论。
在几何处理阶段中填充G缓冲非常高效因为我们直接储存像是位置颜色或者是法线等对象信息到帧缓冲中而这几乎不会消耗处理时间。在此基础上使用多渲染目标(Multiple Render Targets, MRT)技术,我们甚至可以在一个渲染处理之内完成这所有的工作。
## G缓冲

View File

@@ -41,11 +41,12 @@ Crytek公司开发的SSAO技术会产生一种特殊的视觉风格。因为使
## 样本缓冲
SSAO需要获取几何体的信息因为我们需要一些方式来确定一个片段的遮蔽因子。对于每一个片段我们将需要这些数据
- 逐片段**位置**向量
- 逐片段的**法线**向量
- **线性深度**纹理
- 逐片段的**反射颜色**
- **采样核心**
- 用来旋转采样核心的逐片段随机旋转矢量
- 用来旋转采样核心的随机旋转矢量
通过使用一个逐片段观察空间位置,我们可以将一个采样半球核心对准片段的观察空间表面法线。对于每一个核心样本我们会采样线性深度纹理来比较结果。采样核心会根据旋转矢量稍微偏转一点;我们所获得的遮蔽因子将会之后用来限制最终的环境光照分量。

View File

@@ -6,9 +6,9 @@
翻译 | [J.moons](https://github.com/JiangMuWen)
校对 | Meow J初校
!!! warning
!!! Important
注意作者正在对PBR章节进行大的调整原文的内容时时可能有更新建议仍是阅读原文。
注意 作者正在对PBR章节进行大的调整原文的内容时时可能有更新建议仍是阅读原文。
PBR或者用更通俗一些的称呼是指<def>基于物理的渲染</def>(Physically Based Rendering)它指的是一些在不同程度上都基于与现实世界的物理原理更相符的基本理论所构成的渲染技术的集合。正因为基于物理的渲染目的便是为了使用一种更符合物理学规律的方式来模拟光线因此这种渲染方式与我们原来的Phong或者Blinn-Phong光照算法相比总体上看起来要更真实一些。除了看起来更好些以外由于它与物理性质非常接近因此我们尤其是美术师们可以直接以物理参数为依据来编写表面材质而不必依靠粗劣的修改与调整来让光照效果看上去正常。使用基于物理参数的方法来编写材质还有一个更大的好处就是不论光照条件如何这些材质看上去都会是正确的而在非PBR的渲染管线当中有些东西就不会那么真实了。

372
docs/07 PBR/02 Lighting.md Normal file → Executable file
View File

@@ -1,9 +1,373 @@
# 光照
**暂无翻译**
原文 | [Lighting](https://learnopengl.com/#!PBR/Lighting)
---|---
作者 | JoeyDeVries
翻译 | [KenLee](https://hellokenlee.github.io/)
校对 | 暂无
这篇教程暂时还没有进行翻译,您可以先阅读[原文](https://learnopengl.com/#!PBR/Lighting),或经常来刷新看看是否有更新的进展。当然,我们更欢迎您在[GitHub上](https://github.com/LearnOpenGL-CN/LearnOpenGL-CN)认领翻译这篇文章,帮助我们完善这个教程系列。
!!! Important
注意作者正在对PBR章节进行大的调整原文的内容时时可能有更新所以建议暂时不要进行认领
注意 作者正在对PBR章节进行大的调整原文的内容时时可能有更新建议仍是阅读原文
!!! Important
<img src="../../img/development.png" class="clean">
译者注:
<br>
阅读本节请熟悉上一节提到的几个名词:
<br>
- 辐射通量(Radiant flux)<br>
- 辐射率(Radiance)<br>
- 辐照度(Irradiance)<br>
- 辐射强度(Radiant Intensity)
在[上一个教程](https://learnopengl-cn.github.io/07%20PBR/01%20Theory/)中我们讨论了一些PBR渲染的基础知识。 在本章节中,我们将重点放在把以前讨论过的理论转化为实际的渲染器,这个渲染器将使用直接的(或解析的)光源:比如点光源,定向灯或聚光灯。
我们先来看看上一个章提到的反射方程的最终版:
$$
L_o(p,\omega_o) = \int\limits_{\Omega}
(k_d\frac{c}{\pi} + k_s\frac{DFG}{4(\omega_o \cdot n)(\omega_i \cdot n)})
L_i(p,\omega_i) n \cdot \omega_i d\omega_i
$$
我们大致上清楚这个反射方程在干什么,但我们仍然留有一些迷雾尚未揭开。比如说我们究竟将怎样表示场景上的辐照度(Irradiance), 辐射率(Radiance) \(L\)? 我们知道辐射率\(L\)(在计算机图形领域中)表示在给定立体角\(\omega\)的情况下光源的辐射通量(Radiant flux)\(\phi\)或光源在角度\(\omega\)下发送出来的光能。 在我们的情况下,不妨假设立体角\(\omega\)无限小,这样辐射度就表示光源在一条光线或单个方向向量上的辐射通量。
基于以上的知识,我们如何将其转化为以前的教程中积累的一些光照知识呢? 那么想象一下我们有一个点光源一个光源在所有方向具有相同的亮度它的辐射通量为用RBG表示为**23.47,21.31,20.79**。该光源的辐射强度(Radiant Intensity)等于其在所有出射光线的辐射通量。 然而,当我们为一个表面上的特定的点\(p\)着色时,在其半球领域\(\Omega\)的所有可能的入射方向上,只有一个入射方向向量\(\omega_i\)直接来自于该点光源。 假设我们在场景中只有一个光源,位于空间中的某一个点,因而对于\(p\)点的其他可能的入射光线方向上的辐射率为0
![](../img/07/02/lighting_radiance_direct.png)
如果从一开始,我们就假设点光源不受光线衰减(光照强度会随着距离变暗)的影响,那么无论我们把光源放在哪,入射光线的辐射率总是一样的(除去入射角\(cos\theta\)对辐射率的影响之外)。 正正是因为无论我们从哪个角度观察它,点光源总具有相同的辐射强度,我们可以有效地将其辐射强度建模为其辐射通量: 一个常量向量**23.47,21.31,20.79**。
然而,辐射率也需要将位置\(p\)作为输入,正如所有现实的点光源都会受光线衰减影响一样,点光源的辐射强度应该根据点\(p\)所在的位置和光源的位置以及他们之间的距离而做一些缩放。 因此,根据原始的辐射方程,我们会根据表面法向量\(n\)和入射角度\(w_i\)来缩放光源的辐射强度。
在实现上来说:对于直接点光源的情况,辐射率函数\(L\)先获取光源的颜色值, 然后光源和某点\(p\)的距离衰减,接着按照\(n \cdot w_i\)缩放,但是仅仅有一条入射角为\(w_i\)的光线打在点\(p\)上, 这个\(w_i\)同时也等于在\(p\)点光源的方向向量。写成代码的话会是这样:
```glsl
vec3 lightColor = vec3(23.47, 21.31, 20.79);
vec3 wi = normalize(lightPos - fragPos);
float cosTheta = max(dot(N, Wi), 0.0);
float attenuation = calculateAttenuation(fragPos, lightPos);
float radiance = lightColor * attenuation * cosTheta;
```
除了一些叫法上的差异以外这段代码对你们来说应该很TM熟悉这正是我们一直以来怎么计算(漫反射(diffuse))光照的!当涉及到直接照明(direct lighting)时,辐射率的计算方式和我们之前计算当只有一个光源照射在物体表面的时候非常相似。
!!! Important
请注意,这个假设是成立的条件是点光源体积无限小,相当于在空间中的一个点。如果我们认为该光源是具有体积的,它的辐射会在一个以上的入射光的方向不等于零。
对于其它类型的从单点发出来的光源我们类似地计算出辐射率。比如,定向光(directional light)拥有恒定的\(w_i\)而不会有衰减因子;而一个聚光灯光源则没有恒定的辐射强度,其辐射强度是根据聚光灯的方向向量来缩放的。
这也让我们回到了对于表面的半球领域(hemisphere)\(\Omega\)的积分\(\int\)上。由于我们事先知道的所有贡献光源的位置因此对物体表面上的一个点着色并不需要我们尝试去求解积分。我们可以直接拿光源的已知的数目去计算它们的总辐照度因为每个光源仅仅只有一个方向上的光线会影响物体表面的辐射率。这使得PBR对直接光源的计算相对简单因为我们只需要有效地遍历所有有贡献的光源。而当我们后来把环境照明也考虑在内的[IBL]()教程中,我们就必须采取积分去计算了,这是因为光线可能会在任何一个方向入射。
# 一个PBR表面模型
现在让我们开始写片段着色器来实现上述的PBR模型吧~ 首先我们需要把PBR相关的输入放进片段着色器。
```glsl
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
in vec3 WorldPos;
in vec3 Normal;
uniform vec3 camPos;
uniform vec3 albedo;
uniform float metallic;
uniform float roughness;
uniform float ao;
```
我们把通用的顶点着色器的输出作为输入的一部分。另一部分输入则是物体表面模型的一些材质参数。
然后再片段着色器的开始部分我们做一下任何光照算法都需要做的计算:
```glsl
void main()
{
vec3 N = normalize(Normal);
vec3 V = normalize(camPos - WorldPos);
[...]
}
```
## 直接光照明
在本教程的例子中我们会采用总共4个点光源来直接表示场景的辐照度。为了满足反射率方程我们循环遍历每一个光源计算他们独立的辐射率然后求和接着根据BRDF和光源的入射角来缩放该辐射率。我们可以把循环当作在对物体的半球领域对所以直接光源求积分。首先我们来计算一些可以预计算的光照变量
```glsl
vec3 Lo = vec3(0.0);
for(int i = 0; i < 4; ++i)
{
vec3 L = normalize(lightPositions[i] - WorldPos);
vec3 H = normalize(V + L);
float distance = length(lightPositions[i] - WorldPos);
float attenuation = 1.0 / (distance * distance);
vec3 radiance = lightColors[i] * attenuation;
[...]
```
由于我们线性空间内计算光照我们会在着色器的尾部进行Gamma校正我们使用在物理上更为准确的平方倒数作为衰减。
!!! Important
相对于物理上正确来说,你可能仍然想使用常量,线性或者二次衰减方程(他们在物理上相对不准确),却可以为您提供在光的能量衰减更多的控制。
然后,对于每一个光源我们都想计算完整的 Cook-Torrance specular BRDF项
$$
\frac{DFG}{4(\omega_o \cdot n)(\omega_i \cdot n)}
$$
首先我们想计算的是镜面反射和漫反射的系数, 或者说发生表面反射和折射的光线的比值。 我们从[上一个教程]()知道可以使用菲涅尔方程计算:
```glsl
vec3 fresnelSchlick(float cosTheta, vec3 F0)
{
return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}
```
菲涅尔方程返回的是一个物体表面光线被反射的百分比, 也就是我们反射方程中的参数\(k_s\)。Fresnel-Schlick近似接受一个参数`F0`被称为0°入射角的反射(surface reflection at zero incidence)表示如果直接(垂直)观察表面的时候有多少光线会被反射。 这个参数`F0`会因为材料不同而不同而且会因为材质是金属而发生变色。在PBR金属流中我们简单地认为大多数的绝缘体在`F0`为0.04的时候看起来视觉上是正确的,我们同时会特别指定`F0`当我们遇到金属表面并且给定反射率的时候。 因此代码上看起来会像是这样:
```glsl
vec3 F0 = vec3(0.04);
F0 = mix(F0, albedo, metallic);
vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0);
```
你可以看到,对于非金属材质来说`F0`永远保持0.04这个值,我们会根据表面的金属性来改变`F0`这个值, 并且在原来的`F0`和反射率中插值计算`F0`
我们已经算出\(F\) 剩下的项就是计算正态分布函数\(D\)和几何遮蔽函数\(G\)了。
因此一个直接PBR光照着色器中\(D\)和\(G\)的计算代码类似于:
```glsl
float DistributionGGX(vec3 N, vec3 H, float roughness)
{
float a = roughness*roughness;
float a2 = a*a;
float NdotH = max(dot(N, H), 0.0);
float NdotH2 = NdotH*NdotH;
float nom = a2;
float denom = (NdotH2 * (a2 - 1.0) + 1.0);
denom = PI * denom * denom;
return nom / denom;
}
float GeometrySchlickGGX(float NdotV, float roughness)
{
float r = (roughness + 1.0);
float k = (r*r) / 8.0;
float nom = NdotV;
float denom = NdotV * (1.0 - k) + k;
return nom / denom;
}
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness)
{
float NdotV = max(dot(N, V), 0.0);
float NdotL = max(dot(N, L), 0.0);
float ggx2 = GeometrySchlickGGX(NdotV, roughness);
float ggx1 = GeometrySchlickGGX(NdotL, roughness);
return ggx1 * ggx2;
}
```
这里比较重要的是和[上一个教程]()不同的是,我们直接传了粗糙度(roughness)参数给上述的函数通过这种方式我们可以针对每一个不同的项对粗糙度做一些修改。根据迪士尼公司给出的观察以及后来被Epic Games公司采用的光照模型光照在几何遮蔽函数和正太分布函数中采用粗糙度的平方会让光照看起来更加自然。
现在两个函数都给出了定义在计算反射的循环中计算NDF和G项变得非常自然
```glsl
float NDF = DistributionGGX(N, H, roughness);
float G = GeometrySmith(N, V, L, roughness);
```
这样我们就凑够了足够的项来计算Cook-Torrance BRDF:
```glsl
vec3 nominator = NDF * G * F;
float denominator = 4 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.001;
vec3 specular = nominator / denominator;
```
注意我们在分母项中加了一个0.001为了避免出现除零错误。
现在我们终于可以计算每个光源在反射率方程中的贡献值了!因为菲涅尔方程直接给出了\(k_S\) 我们可以使用`F`表示镜面反射在所有打在物体表面上的光线的贡献。 从\(k_S\)我们很容易计算折射的比值\(k_D\)
```glsl
vec3 kS = F;
vec3 kD = vec3(1.0) - kS;
kD *= 1.0 - metallic;
```
我们可以看作\(k_S\)表示光能中被反射的能量的比例, 而剩下的光能会被折射, 比值即为\(k_D\)。更进一步来说,因为金属不会折射光线,因此不会有漫反射。所以如果表面是金属的,我们会把系数\(k_D\)变为0。 这样,我们终于集齐所有变量来计算我们出射光线的值:
```glsl
const float PI = 3.14159265359;
float NdotL = max(dot(N, L), 0.0);
Lo += (kD * albedo / PI + specular) * radiance * NdotL;
}
```
最终的结果`Lo`,或者说是出射光线的辐射率,实际上是反射率方程的在半球领域\(\Omega\)的积分的结果。但是我们实际上不需要去求积因为对于所有可能的入射光线方向我们知道只有4个方向的入射光线会影响片段(像素)的着色。因为这样我们可以直接循环N次计算这些入射光线的方向(N也就是场景中光源的数目)。
比较重要的是我们没有把`kS`乘进去我们的反射率方程中这是因为我们已经在specualr BRDF中乘了菲涅尔系数`F`了,因为`kS`等于`F`,因此我们不需要再乘一次。
剩下的工作就是加一个环境光照项给`Lo`,然后我们就拥有了片段的最后颜色:
```glsl
vec3 ambient = vec3(0.03) * albedo * ao;
vec3 color = ambient + Lo;
```
## 线性空间和HDR渲染
直到现在,我们假设的所有计算都在线性的颜色空间中进行的,因此我们需要在着色器最后做[伽马矫正]()。 在线性空间中计算光照是非常重要的因为PBR要求所有输入都是线性的如果不是这样我们就会得到不正常的光照。另外我们希望所有光照的输入都尽可能的接近他们在物理上的取值这样他们的反射率或者说颜色值就会在色谱上有比较大的变化空间。`Lo`作为结果可能会变大得很快(超过1)但是因为默认的LDR输入而取值被截断。所以在伽马矫正之前我们采用色调映射使`Lo`从LDR的值映射为HDR的值。
```glsl
color = color / (color + vec3(1.0));
color = pow(color, vec3(1.0/2.2));
```
这里我们采用的色调映射方法为Reinhard 操作,使得我们在伽马矫正后可以保留尽可能多的辐照度变化。 我们没有使用一个独立的帧缓冲或者采用后期处理,所以我们需要直接在每一步光照计算后采用色调映射和伽马矫正。
![](../img/07/02/lighting_linear_vs_non_linear_and_hdr.png)
采用线性颜色空间和HDR在PBR渲染管线中非常重要。如果没有这些操作几乎是不可能正确地捕获到因光照强度变化的细节这最终会导致你的计算变得不正确在视觉上看上去非常不自然。
## 完整的直接光照PBR着色器
现在剩下的事情就是把做好色调映射和伽马矫正的颜色值传给片段着色器的输出然后我们就拥有了自己的直接光照PBR着色器。 为了完整性,这里给出了完整的代码:
```glsl
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
in vec3 WorldPos;
in vec3 Normal;
// material parameters
uniform vec3 albedo;
uniform float metallic;
uniform float roughness;
uniform float ao;
// lights
uniform vec3 lightPositions[4];
uniform vec3 lightColors[4];
uniform vec3 camPos;
const float PI = 3.14159265359;
float DistributionGGX(vec3 N, vec3 H, float roughness);
float GeometrySchlickGGX(float NdotV, float roughness);
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness);
vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness);
void main()
{
vec3 N = normalize(Normal);
vec3 V = normalize(camPos - WorldPos);
vec3 F0 = vec3(0.04);
F0 = mix(F0, albedo, metallic);
// reflectance equation
vec3 Lo = vec3(0.0);
for(int i = 0; i < 4; ++i)
{
// calculate per-light radiance
vec3 L = normalize(lightPositions[i] - WorldPos);
vec3 H = normalize(V + L);
float distance = length(lightPositions[i] - WorldPos);
float attenuation = 1.0 / (distance * distance);
vec3 radiance = lightColors[i] * attenuation;
// cook-torrance brdf
float NDF = DistributionGGX(N, H, roughness);
float G = GeometrySmith(N, V, L, roughness);
vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0);
vec3 kS = F;
vec3 kD = vec3(1.0) - kS;
kD *= 1.0 - metallic;
vec3 nominator = NDF * G * F;
float denominator = 4 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.001;
vec3 specular = nominator / denominator;
// add to outgoing radiance Lo
float NdotL = max(dot(N, L), 0.0);
Lo += (kD * albedo / PI + specular) * radiance * NdotL;
}
vec3 ambient = vec3(0.03) * albedo * ao;
vec3 color = ambient + Lo;
color = color / (color + vec3(1.0));
color = pow(color, vec3(1.0/2.2));
FragColor = vec4(color, 1.0);
}
```
希望经过上一个教程的[理论知识]()以及学习过关于渲染方程的一些知识后这个着色器看起来不会太可怕。如果我们采用这个着色器加上4个点光源和一些球体同时我们令这些球体的金属性(metallic)和粗糙度(roughness)沿垂直方向和水平方向分别变化,我们会得到这样的结果:
![](../img/07/02/lighting_result.png)
(上述图片)从下往上球体的金属性从0.0变到1.0 从左到右球体的粗糙度从0.0变到1.0。你可以看到仅仅改变这两个值,显示的效果会发生巨大的改变!
你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/6.pbr/1.1.lighting/lighting.cpp)找到整个demo的完整代码。
# 带贴图的PBR
把我们系统扩展成可以接受纹理作为参数可以让我们对物体的材质有更多的自定义空间:
```glsl
[...]
uniform sampler2D albedoMap;
uniform sampler2D normalMap;
uniform sampler2D metallicMap;
uniform sampler2D roughnessMap;
uniform sampler2D aoMap;
void main()
{
vec3 albedo = pow(texture(albedoMap, TexCoords).rgb, 2.2);
vec3 normal = getNormalFromNormalMap();
float metallic = texture(metallicMap, TexCoords).r;
float roughness = texture(roughnessMap, TexCoords).r;
float ao = texture(aoMap, TexCoords).r;
[...]
}
```
不过需要注意的是一般来说反射率(albedo)纹理在美术人员创建的时候就已经在sRGB空间了因此我们需要在光照计算之前先把他们转换到线性空间。一般来说环境光遮蔽贴图(ambient occlusion maps)也需要我们转换到线性空间。不过金属性(Metallic)和粗糙度(Roughness)贴图大多数时间都会保证在线性空间中。
只是把之前的球体的材质性质换成纹理属性,就在视觉上有巨大的提升:
![](../img/07/02/lighting_textured.png)
你可以在这里找到纹理贴图过的全部[代码](https://learnopengl.com/code_viewer_gh.php?code=src/6.pbr/1.2.lighting_textured/lighting_textured.cpp) 以及我用的[纹理](http://freepbr.com/materials/rusted-iron-pbr-metal-material-alt/)(记得加上一张全白色的ao Map)。注意金属表面会在场景中看起来有点黑,因为他们没有漫反射。它们会在考虑环境镜面光照的时候看起来更加自然,不过这是我们下一个教程的事情了。
相比起在网上找到的其他PBR渲染结果来说尽管在视觉上不算是非常震撼因为我们还没考虑到[基于图片的关照,IBL]()。我们现在也算是有了一个基于物理的渲染器了(虽然还没考虑IBL)!你会发现你的光照看起来更加真实了。
!!! Important
译者注:
<br>
本章教程有几个小坑原作者没有说清楚,可能是希望读者自己思考,在这译者稍稍提醒一下:
<br>
- 首先是球体的生成主流的球体顶点生成有两种方法作者源码采用的是UVSphere方法 IcoSpher方法可以参考[这里](http://blog.andreaskahler.com/2009/06/creating-icosphere-mesh-in-code.html)
<br>
- 对于贴图的PBR来说我们需要TBN矩阵做坐标转换(切线空间-> 世界空间 或者 世界空间 -> 切线空间,参考 法线贴图 章节。)。这有两种方法一种是在片段着色器中使用叉乘计算TBN矩阵(作者采用的方法)另外一种是在根据顶点预计算TBN然后VAO中传入TBN矩阵理论上来说后者会比较快(但是比较麻烦),不过在译者的实际测试中两者速度差距不大。

9
docs/code_repo.md Normal file
View File

@@ -0,0 +1,9 @@
# 代码仓库
你可以在每一篇教程中找到在线的代码范例但如果你想自己运行教程的Demo或者将正常工作的范例代码与你的代码进行比较你可以在[这里](https://github.com/JoeyDeVries/LearnOpenGL)找到在线的GitHub代码仓库。
目前,`CMakeLists.txt`文件能够正常生成Visual Studio的工程文件和make文件它能够在Windows和Linux上运行。但是它在Apple的macOS和其它的IDE上还没有进行非常完全的测试所以如果出现问题你可以留言或者帮忙通过Pull Request来更新一下`CMakeLists.txt`文件让它支持不同的系统。
我非常感谢Zwookie在制作Linux的CMake脚本时提供的巨大帮助。感谢Zwookie对CMakeLists的更新现在它能够在Windows和Linux上生成工程文件了。
你也可以查看Polytonic的[Glitter](https://github.com/Polytonic/Glitter),它是一个非常简单的样板工程,它提供了已经预配置好的相关依赖项。

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

View File

@@ -22,4 +22,6 @@
这里是LearnOpenGL教程的中文翻译英文版的地址为[http://learnopengl.com/](http://learnopengl.com/)
由于翻译可能无法做到精确表达原文意思,我们推荐您在对问题有疑惑的时候去阅读一下英文版的教程。如果您对翻译有更好的建议,可以去我们的[GitHub工程](https://github.com/LearnOpenGL-CN/LearnOpenGL-CN)上提交Issue或者Pull Request。如果是对教程的内容有问题请先查看原文如果不是翻译错误的话请直接在原网站评论区向作者JoeyDeVries反馈。
由于翻译可能无法做到精确表达原文意思,我们推荐您在对问题有疑惑的时候去阅读一下英文版的教程。如果您对翻译有更好的建议,可以去我们的[GitHub工程](https://github.com/LearnOpenGL-CN/LearnOpenGL-CN)上提交Issue或者Pull Request。如果是对教程的内容有问题请先查看原文如果不是翻译错误的话请直接在原网站评论区向作者JoeyDeVries反馈。
如果教程中的源码无法打开的话,可以到教程的[GitHub页面](https://github.com/JoeyDeVries/LearnOpenGL)上去寻找所需的代码,每一节教程的源码以及练习都位于 `src` 目录下的对应章节目录中。

358
docs/legacy.md Normal file
View File

@@ -0,0 +1,358 @@
# 历史存档
由于作者在对教程不断地更新,一些比较旧的内容就从教程中删除或者修改了,但是这部分内容仍然对在更新以前就开始学习的读者可能还是会有点用处的。所以,我会将被大段删除或修改的内容留到这里以供大家参考。
注意的是,一些比较小的改动将不会出现在这里。
## 01-01 OpenGL
!!! important
**基元类型(Primitive Type)**
使用OpenGL时建议使用OpenGL定义的基元类型。比如使用`float`时我们加上前缀`GL`(因此写作`GLfloat`)。`int``uint``char``bool`等等也类似。OpenGL定义的这些GL基元类型的内存布局是与平台无关的而int等基元类型在不同操作系统上可能有不同的内存布局。使用GL基元类型可以保证你的程序在不同的平台上工作一致。
## 01-02 创建窗口
### 编译和链接GLEW
GLEW是OpenGL Extension Wrangler Library的缩写它能解决我们上面提到的那个繁琐的问题。因为GLEW也是一个库我们同样需要构建并将其链接进工程。GLEW可以从[这里](http://glew.sourceforge.net/index.html)下载你同样可以选择下载二进制版本如果你的目标平台列在上面的话或者下载源码编译步骤和编译GLFW时差不多。记住如果不确定的话选择32位的二进制版本。
我们使用GLEW的**静态**版本**glew32s.lib**注意这里的“s”将库文件添加到你的库目录将**include**内容添加到你的include目录。接下来在VS的链接器选项里加上**glew32s.lib**。注意GLFW3默认也是编译成了一个静态库。
!!! Important
**静态**(Static)链接是指编译时就将库代码里的内容整合进你的二进制文件。优点就是你不需要管理额外的文件了,只需要发布你单独的一个二进制文件就行了。缺点就是你的可执行文件会变得更大,另外当库有升级版本时,你必须重新进行编译整个程序。
**动态**(Dynamic)链接是指一个库通过`.dll``.so`的方式存在它的代码与你的二进制文件的代码是分离的。优点是使你的二进制文件大小变小并且更容易升级缺点是你最终发布程序时必须带上这些DLL。
如果你希望静态链接GLEW必须在包含GLEW头文件之前定义预处理器宏`GLEW_STATIC`
```c++
#define GLEW_STATIC
#include <GL/glew.h>
```
如果你希望动态链接,那么你可以省略这个宏。但是记住使用动态链接的话你需要拷贝一份.DLL文件到你的应用程序目录。
!!! Important
对于用GCC编译的Linux用户建议使用这个命令行选项`-lGLEW -lglfw3 -lGL -lX11 -lpthread -lXrandr -lXi`。没有正确链接相应的库会产生 *undefined reference*(未定义的引用) 这个错误。
我们现在成功编译了GLFW和GLEW库我们已经准备好将进入[下一节](01 Getting started/03 Hello Window.md)去真正使用GLFW和GLEW来设置OpenGL上下文并创建窗口。记得确保你的头文件和库文件的目录设置正确以及链接器里引用的库文件名正确。如果仍然遇到错误可以先看一下评论有没有人遇到类似的问题请参考额外资源中的例子或者在下面的评论区提问。
## 01-03 你好,窗口
### GLEW
在之前的教程中已经提到过GLEW是用来管理OpenGL的函数指针的所以在调用任何OpenGL的函数之前我们需要初始化GLEW。
```c++
glewExperimental = GL_TRUE;
if (glewInit() != GLEW_OK)
{
std::cout << "Failed to initialize GLEW" << std::endl;
return -1;
}
```
请注意我们在初始化GLEW之前设置<var>glewExperimental</var>变量的值为`GL_TRUE`这样做能让GLEW在管理OpenGL的函数指针时更多地使用现代化的技术如果把它设置为`GL_FALSE`的话可能会在使用OpenGL的核心模式时出现一些问题。
### 视口(Viewport)
在我们开始渲染之前还有一件重要的事情要做我们必须告诉OpenGL渲染窗口的尺寸大小这样OpenGL才只能知道怎样相对于窗口大小显示数据和坐标。我们可以通过调用<fun>glViewport</fun>函数来设置窗口的**维度**(Dimension)
```c++
int width, height;
glfwGetFramebufferSize(window, &width, &height);
glViewport(0, 0, width, height);
```
<fun>glViewport</fun>函数前两个参数控制窗口左下角的位置。第三个和第四个参数控制渲染窗口的宽度和高度像素这里我们是直接从GLFW中获取的。我们从GLFW中获取视口的维度而不设置为800*600是为了让它在高DPI的屏幕上比如说Apple的视网膜显示屏也能[正常工作](http://www.glfw.org/docs/latest/window.html#window_size)。
我们实际上也可以将视口的维度设置为比GLFW的维度小这样子之后所有的OpenGL渲染将会在一个更小的窗口中显示这样子的话我们也可以将一些其它元素显示在OpenGL视口之外。
!!! Important
OpenGL幕后使用<fun>glViewport</fun>中定义的位置和宽高进行2D坐标的转换将OpenGL中的位置坐标转换为你的屏幕坐标。例如OpenGL中的坐标(-0.5, 0.5)有可能(最终)被映射为屏幕中的坐标(200,450)。注意处理过的OpenGL坐标范围只为-1到1因此我们事实上将(-1到1)范围内的坐标映射到(0, 800)和(0, 600)。
### 输入
我们同样也希望能够在GLFW中实现一些键盘控制这可以通过使用GLFW的回调函数(Callback Function)来完成。<def>回调函数</def>事实上是一个函数指针当我们设置好后GLFW会在合适的时候调用它。**按键回调**(KeyCallback)是众多回调函数中的一种。当我们设置了按键回调之后GLFW会在用户有键盘交互时调用它。该回调函数的原型如下所示
```c++
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);
```
按键回调函数接受一个<fun>GLFWwindow</fun>指针作为它的第一个参数;第二个整形参数用来表示按下的按键;`action`参数表示这个按键是被按下还是释放最后一个整形参数表示是否有Ctrl、Shift、Alt、Super等按钮的操作。GLFW会在合适的时候调用它并为各个参数传入适当的值。
```c++
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
// 当用户按下ESC键,我们设置window窗口的WindowShouldClose属性为true
// 关闭应用程序
if(key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, GL_TRUE);
}
```
在我们(新创建的)<fun>key_callback</fun>函数中我们检测了键盘是否按下了Escape键。如果键的确按下了(不释放),我们使用<fun>glfwSetwindowShouldClose</fun>函数设定`WindowShouldClose`属性为`true`从而关闭GLFW。main函数的`while`循环下一次的检测将为失败,程序就关闭了。
最后一件事就是通过GLFW注册我们的函数至合适的回调代码是这样的:
```c++
glfwSetKeyCallback(window, key_callback);
```
除了按键回调函数之外,我们还能我们自己的函数注册其它的回调。例如,我们可以注册一个回调函数来处理窗口尺寸变化、处理一些错误信息等。我们可以在创建窗口之后,开始游戏循环之前注册各种回调函数。
## 01-06 纹理
### SOIL
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>`。
下面的教程中,我们会使用一张[木箱](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数组。
### 生成纹理
和之前生成的OpenGL对象一样纹理也是使用ID引用的。让我们来创建一个
```c++
GLuint texture;
glGenTextures(1, &texture);
```
<fun>glGenTextures</fun>函数首先需要输入生成纹理的数量,然后把它们储存在第二个参数的`GLuint`数组中(我们的例子中只是一个单独的`GLuint`),就像其他对象一样,我们需要绑定它,让之后任何的纹理指令都可以配置当前绑定的纹理:
```c++
glBindTexture(GL_TEXTURE_2D, texture);
```
现在纹理已经绑定了,我们可以使用前面载入的图片数据生成一个纹理了。纹理可以通过<fun>glTexImage2D</fun>来生成:
```c++
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
glGenerateMipmap(GL_TEXTURE_2D);
```
函数很长,参数也不少,所以我们一个一个地讲解:
- 第一个参数指定了纹理目标(Target)。设置为<var>GL_TEXTURE_2D</var>意味着会生成与当前绑定的纹理对象在同一个目标上的纹理(任何绑定到<var>GL_TEXTURE_1D</var>和<var>GL_TEXTURE_3D</var>的纹理不会受到影响)。
- 第二个参数为纹理指定多级渐远纹理的级别如果你希望单独手动设置每个多级渐远纹理的级别的话。这里我们填0也就是基本级别。
- 第三个参数告诉OpenGL我们希望把纹理储存为何种格式。我们的图像只有`RGB`值,因此我们也把纹理储存为`RGB`值。
- 第四个和第五个参数设置最终的纹理的宽度和高度。我们之前加载图像的时候储存了它们,所以我们使用对应的变量。
- 下个参数应该总是被设为`0`(历史遗留问题)。
- 第七第八个参数定义了源图的格式和数据类型。我们使用RGB值加载这个图像并把它们储存为`char`(byte)数组,我们将会传入对应值。
- 最后一个参数是真正的图像数据。
当调用<fun>glTexImage2D</fun>时,当前绑定的纹理对象就会被附加上纹理图像。然而,目前只有基本级别(Base-level)的纹理图像被加载了,如果要使用多级渐远纹理,我们必须手动设置所有不同的图像(不断递增第二个参数)。或者,直接在生成纹理之后调用<fun>glGenerateMipmap</fun>。这会为当前绑定的纹理自动生成所有需要的多级渐远纹理。
生成了纹理和相应的多级渐远纹理后,释放图像的内存并解绑纹理对象是一个很好的习惯。
```c++
SOIL_free_image_data(image);
glBindTexture(GL_TEXTURE_2D, 0);
```
生成一个纹理的过程应该看起来像这样:
```c++
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);
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);
glBindTexture(GL_TEXTURE_2D, 0);
```
### 纹理单元
你可能会奇怪为什么`sampler2D`变量是个uniform我们却不用<fun>glUniform</fun>给它赋值。使用<fun>glUniform1i</fun>,我们可以给纹理采样器分配一个位置值,这样的话我们能够在一个片段着色器中设置多个纹理。一个纹理的位置值通常称为一个<def>纹理单元</def>(Texture Unit)。一个纹理的默认纹理单元是0它是默认的激活纹理单元所以教程前面部分我们没有分配一个位置值。
纹理单元的主要目的是让我们在着色器中可以使用多于一个的纹理。通过把纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们首先激活对应的纹理单元。就像<fun>glBindTexture</fun>一样,我们可以使用<fun>glActiveTexture</fun>激活纹理单元,传入我们需要使用的纹理单元:
```c++
glActiveTexture(GL_TEXTURE0); //在绑定纹理之前先激活纹理单元
glBindTexture(GL_TEXTURE_2D, texture);
```
激活纹理单元之后,接下来的<fun>glBindTexture</fun>函数调用会绑定这个纹理到当前激活的纹理单元,纹理单元<var>GL_TEXTURE0</var>默认总是被激活,所以我们在前面的例子里当我们使用`glBindTexture`的时候,无需激活任何纹理单元。
!!! Important
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内建的<fun>mix</fun>函数需要接受两个值作为参数,并对它们根据第三个参数进行线性插值。。如果第三个值是`0.0`,它会返回第一个输入;如果是`1.0`,会返回第二个输入值。`0.2`会返回`80%`的第一个输入颜色和`20%`的第二个输入颜色,即返回两个纹理的混合色。
我们现在需要载入并创建另一个纹理;你应该对这些步骤很熟悉了。记得创建另一个纹理对象,载入图片,使用<fun>glTexImage2D</fun>生成最终纹理。对于第二个纹理我们使用一张[你学习OpenGL时的面部表情](img/01/06/awesomeface.png)图片。
为了使用第二个纹理以及第一个我们必须改变一点渲染流程先绑定两个纹理到对应的纹理单元然后定义哪个uniform采样器对应哪个纹理单元
```c++
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glUniform1i(glGetUniformLocation(ourShader.Program, "ourTexture1"), 0);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
glUniform1i(glGetUniformLocation(ourShader.Program, "ourTexture2"), 1);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
```
注意,我们使用<fun>glUniform1i</fun>设置uniform采样器的位置值或者说纹理单元。通过<fun>glUniform1i</fun>的设置我们保证每个uniform采样器对应着正确的纹理单元。你应该能得到下面的结果
![](img/01/06/textures_combined.png)
你可能注意到纹理上下颠倒了这是因为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.0f - texCoord.y);`。
!!! Attention
上面提供的解决方案仅仅通过一些黑科技让图片翻转。它们在大多数情况下都能正常工作然而实际上这种方案的效果取决于你的实现和纹理所以最好的解决方案是调整你的图片加载器或者以一种y原点符合OpenGL需求的方式编辑你的纹理图像。
如果你编辑了顶点数据,在顶点着色器中翻转了纵坐标,你会得到下面的结果:
![](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)着色器。
## 01-09 摄像机
### 自由移动
让摄像机绕着场景转的确很有趣,但是让我们自己移动摄像机会更有趣!首先我们必须设置一个摄像机系统,所以在我们的程序前面定义一些摄像机变量很有用:
```c++
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
```
`LookAt`函数现在成了:
```c++
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
```
我们首先将摄像机位置设置为之前定义的<var>cameraPos</var>。方向是当前的位置加上我们刚刚定义的方向向量。这样能保证无论我们怎么移动,摄像机都会注视着目标方向。让我们摆弄一下这些向量,在按下某些按钮时更新<var>cameraPos</var>向量。
我们已经为GLFW的键盘输入定义了一个<fun>key_callback</fun>函数,我们来新添加几个需要检查的按键命令:
```c++
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
...
GLfloat cameraSpeed = 0.05f;
if(key == GLFW_KEY_W)
cameraPos += cameraSpeed * cameraFront;
if(key == GLFW_KEY_S)
cameraPos -= cameraSpeed * cameraFront;
if(key == GLFW_KEY_A)
cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
if(key == GLFW_KEY_D)
cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
}
```
当我们按下**WASD**键的任意一个,摄像机的位置都会相应更新。如果我们希望向前或向后移动,我们就把位置向量加上或减去方向向量。如果我们希望向左右移动,我们使用叉乘来创建一个**右向量**(Right Vector),并沿着它相应移动就可以了。这样就创建了使用摄像机时熟悉的<def>扫射</def>(Strafe)效果。
!!! important
注意,我们对**右向量**进行了标准化。如果我们没对这个向量进行标准化,最后的叉乘结果会根据<var>cameraFront</var>变量返回大小不同的向量。如果我们不对向量进行标准化,我们就得根据摄像机的朝向不同加速或减速移动了,但假如进行了标准化移动就是匀速的。
如果你用这段代码更新<fun>key_callback</fun>函数,你就可以在场景中自由的前后左右移动了。
<video src="../img/01/09/camera_inside.mp4" controls="controls">
</video>
在摆弄这个基础的摄像机系统之后你可能会注意到这个摄像机系统不能同时朝两个方向移动对角线移动而且当你按下一个按键时它会先顿一下才开始移动。这是因为大多数事件输入系统一次只能处理一个键盘输入它们的函数只有当我们激活了一个按键时才被调用。虽然这对大多数GUI系统都没什么问题它对摄像机来说并不合理。我们可以用一些小技巧解决这个问题。
这个技巧是在回调函数中只跟踪哪个按键被按下/释放。在游戏循环中我们读取这些值,检查哪个按键是**激活的**,然后做出相应反应。我们只储存哪个按键被按下/释放的状态信息并在游戏循环中对状态做出反应。首先我们来创建一个boolean数组代表被按下/释放的按键:
```c++
bool keys[1024];
```
然后我们需要在<fun>key_callback</fun>函数中设置被按下/释放的按键为`true`或`false`
```c++
if(action == GLFW_PRESS)
keys[key] = true;
else if(action == GLFW_RELEASE)
keys[key] = false;
```
并且我们创建一个新的叫做<fun>do_movement</fun>的函数,在这个函数中,我们将根据正在被按下的按键更新摄像机的值。:
```c++
void do_movement()
{
// 摄像机控制
GLfloat cameraSpeed = 0.01f;
if(keys[GLFW_KEY_W])
cameraPos += cameraSpeed * cameraFront;
if(keys[GLFW_KEY_S])
cameraPos -= cameraSpeed * cameraFront;
if(keys[GLFW_KEY_A])
cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
if(keys[GLFW_KEY_D])
cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
}
```
之前的代码现在被移动到<fun>do_movement</fun>函数中。由于所有GLFW的按键枚举值本质上都是整数我们可以把它们当数组索引使用。
最后,我们需要在游戏循环中添加新函数的调用:
```c++
while(!glfwWindowShouldClose(window))
{
// 检测并调用事件
glfwPollEvents();
do_movement();
// 渲染
...
}
```
至此,你应该可以同时向多个方向移动了,并且当你按下按钮时也会立刻移动了。如遇困难,可以查看下[源代码](http://learnopengl.com/code_viewer.php?code=getting-started/camera_keyboard)。

View File

@@ -14,7 +14,7 @@
- State Machine状态机
- Context上下文
- State-changing Function状态设置函数
- State-using Function状态用函数
- State-using Function状态使用函数
- Object对象
- Primitive Type基元类型
- Option选项
@@ -35,6 +35,7 @@
- Viewport视口
- Dimension窗口维度
- Game Loop游戏循环
- Render Loop渲染循环
- Color Buffer颜色缓冲
- Double Buffer双缓冲
- Front Buffer前缓冲
@@ -165,7 +166,7 @@
- Camera Space摄像机空间
- Gram-Schmidt Process格拉姆—施密特正交化
- LookAt MatrixLookAt矩阵
- Strafe(?)扫射
- Strafe横移
- Deltatime时间差
- Euler Angles欧拉角
- Pitch俯仰角
@@ -173,6 +174,45 @@
- Roll滚转角
- Field of View(FoV):视野
## 02-01
- Lighting光照
- Reflect反射
- Absorb吸收
- Container第一章创建的那个物体箱子
- Lamp
## 02-02
- Phong Lighting Model冯氏光照模型
- Ambient Lighting环境光照
- Diffuse Lighting漫反射光照
- Specular Lighting镜面光照
- Directional Impact方向性影响
- Global Illumination全局性照明
- Factor因子
- Normal Vector法向量
- Normal Matrix法线矩阵
- Ambient Component环境光分量
- Diffuse Component漫反射分量
- Specular Component镜面光分量
- Inverse Matrix逆矩阵
- Transpose Matrix转置矩阵
- Specular Highlight镜面高光
- Intensity强度
- Specular Intensity镜面强度
- Shininess反光度
- Gouraud ShadingGouraud着色
- Phong Shading冯氏着色
## 02-03
- Material材质
- Struct结构体
- Layout布局
- Light Property光照属性
- Scatter散射
## 06-01
- Debugging调试

View File

@@ -15,7 +15,7 @@ pages:
- 复习: '01 Getting started/10 Review.md'
- 光照:
- 颜色: '02 Lighting/01 Colors.md'
- 光照基础: '02 Lighting/02 Basic Lighting.md'
- 基础光照: '02 Lighting/02 Basic Lighting.md'
- 材质: '02 Lighting/03 Materials.md'
- 光照贴图: '02 Lighting/04 Lighting maps.md'
- 投光物: '02 Lighting/05 Light casters.md'
@@ -74,10 +74,12 @@ pages:
- 音效: '06 In Practice/2D-Game/09 Audio.md'
- 渲染文本: '06 In Practice/2D-Game/10 Render Text.md'
- 结语: '06 In Practice/2D-Game/11 Final thoughts.md'
- 历史存档: 'legacy.md'
- 代码仓库: 'code_repo.md'
site_name: 'LearnOpenGL CN'
site_description: 'http://learnopengl.com 系列教程的中文翻译'
site_description: 'http://learnopengl.com 系列教程的简体中文翻译'
site_url: https://learnopengl-cn.github.io/