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

校对 01/01 到 01/04

This commit is contained in:
Geequlim
2015-07-26 21:31:06 +08:00
parent 71c6e392c4
commit ed925f7f7f
5 changed files with 179 additions and 155 deletions

View File

@@ -1,71 +1,75 @@
本文作者JoeyDeVries由gjy_1992翻译自[http://learnopengl.com](http://learnopengl.com/#!Getting-started/OpenGL) ## OpenGL (Open Graphics Library)
##OpenGL 原文 | [OpenGL](http://learnopengl.com/#!Getting-started/OpenGL)
---|---
作者 | JoeyDeVries
翻译 | gjy_1992
校对 | Geequlim
在进行这段旅程之前我们先解释下OpenGL到底是什么。它一般被认为是一个**应用程序编程接口**API包含了一系列可以操作图形、画像的方法。然而OpenGL本身并不是一个API仅仅是一个规范由[Khronos组织](http://www.khronos.org/)制定并维护。
OpenGL规范严格规定了每个函数该如何执行以及它们该如何返回。至于内部具体每个函数是如何实现的将由openGL库的开发者自行决定这里开发者是指编写OpenGL库的人。因为OpenGL规范并没有规定实现的细节具体的OpenGL库允许使用不同的实现只要其功能和结果与规范相匹配亦即作为用户不会感受到功能上的差异 在开始这段旅程之前我们先了解一下OpenGL到底是什么。一般它被认为是一个**应用程序编程接口**API包含了一系列可以操作图形、画像的方法。然而OpenGL本身并不是一个API仅仅是一个规范由[Khronos组织](http://www.khronos.org/)制定并维护
OpenGL严格规定了每个函数该如何执行以及它们该如何返回。至于内部具体每个函数是如何实现的将由OpenGL库的开发者自行决定这里开发者是指编写OpenGL库的人。因为OpenGL规范并没有规定实现的细节具体的OpenGL库允许使用不同的实现只要其功能和结果与规范相匹配亦即作为用户不会感受到功能上的差异
实际的OpenGL库的开发者通常是显卡的生产商。每个你购买的显卡都会支持特定版本的OpenGL通常是为一个系列的显卡专门开发的。当你使用苹果系统的时候OpenGL库是由苹果自身维护的。在Linux下有显卡生产商提供的OpenGL库也有一些爱好者改编的版本。这也意味着任何时候OpenGL库表现的行为与规范规定的不一致时基本都是库的开发者留下的bug。快甩锅 实际的OpenGL库的开发者通常是显卡的生产商。每个你购买的显卡都会支持特定版本的OpenGL通常是为一个系列的显卡专门开发的。当你使用苹果系统的时候OpenGL库是由苹果自身维护的。在Linux下有显卡生产商提供的OpenGL库也有一些爱好者改编的版本。这也意味着任何时候OpenGL库表现的行为与规范规定的不一致时基本都是库的开发者留下的bug。快甩锅
!!! Important !!! Important
由于大多数实现都是由显卡厂商编写的当产生一个bug时通常可以通过升级显卡驱动来解决。这些驱动会包括你的显卡能支持的最新版本的OpenGL这也是为什么总是建议你偶尔更新一下显卡驱动。 由于OpenGL的大多数实现都是由显卡厂商编写的当产生一个bug时通常可以通过升级显卡驱动来解决。这些驱动会包括你的显卡能支持的最新版本的OpenGL这也是为什么总是建议你偶尔更新一下显卡驱动。
Khronos公开主持所有版本的OpenGL规范书的制定。有兴趣的读者可以找到OpenGL3.3(我们将要提到的版本)的[规范书](https://www.opengl.org/registry/doc/glspec33.core.20100311.withchanges.pdf)。如果你想深入到OpenGL的细节注意只描述函数的功能而不管实现这是个很好的选择。该规范还提供一个强大的可以寻找到每个函数具体功能的参考。 Khronos公开主持所有版本的OpenGL规范书的制定。有兴趣的读者可以找到OpenGL3.3(我们将要提到的版本)的[规范书](https://www.opengl.org/registry/doc/glspec33.core.20100311.withchanges.pdf)。如果你想深入到OpenGL的细节注意只描述函数的功能而不管实现这是个很好的选择。该规范还提供一个强大的可以寻找到每个函数具体功能的参考。
##对比核心模式和立即渲染模式 ### 核心模式Core-profile mode与立即渲染模式Immediate mode
早期的OpenGL使用立即渲染模式固定渲染管线这个模式下绘制图形很方便。OpenGl的大多数功能都被隐藏起来开发者很少能控制OpenGL如何进行计算。开发者最终希望更多的灵活性。随着时间推移规范越来越灵活开发者也能更多的控制绘图细节。立即渲染模式确实容易使用和理解但是效率太低。因此从OpenGL3.2开始,规范书开始废弃立即渲染模式,推核心模式,这个模式完全移除了旧的特性。 早期的OpenGL使用立即渲染模式固定渲染管线这个模式下绘制图形很方便。OpenGl的大多数功能都被隐藏起来开发者很少能控制OpenGL如何进行计算。开发者希望更多的灵活性。随着时间推移规范越来越灵活开发者也能更多的控制绘图细节。立即渲染模式确实容易使用和理解但是效率太低。因此从OpenGL3.2开始,规范书开始废弃立即渲染模式,推核心模式,这个模式完全移除了旧的特性。
当使用核心模式时OpenGL迫使我们使用现代的做法。当我们试图使用一个废弃的函数时OpenGL会抛出一个错误并终止绘图。现代做法的优势是更高的灵活性和效率然而也更难于学习。立即渲染模式从OpenGL实际操作中抽象掉了很多细节因而它易于学习的同时也很难去把握OpenGL具体是如何操作的。现代做法要求使用者真正理解OpenGL和图形编程它有一些难度然而提供了更多的灵活性更高的效率更重要的可以更深入的理解图形编程。 当使用核心模式时OpenGL迫使我们使用现代的做法。当我们试图使用一个废弃的函数时OpenGL会抛出一个错误并终止绘图。现代做法的优势是更高的灵活性和效率然而也更难于学习。立即渲染模式从OpenGL实际操作中抽象掉了很多细节因而它易于学习的同时也很难去把握OpenGL具体是如何操作的。现代做法要求使用者真正理解OpenGL和图形编程它有一些难度然而提供了更多的灵活性更高的效率更重要的可以更深入的理解图形编程。
这也是为什么我们的教程面向OpenGL3.3的核心模式。虽然上手更困难,但是值得去努力。 这也是为什么我们的教程面向OpenGL3.3的核心模式。虽然上手更困难,但是值得去努力。
现今更高版本的OpenGL已经发布目前最新是4.5你可能会问为什么我们还要学习3.3答案很简单所有OpenGL的更高的版本都是在3.3的基础上添加了额外的功能并不更改核心架构。新版本只是引入了一些更有效率或更有用的方式去完成同样的功能。因此所有的概念和技术在现代OpenGL版本里都保持一致。当你的经验足够你可以轻松使用来自更高版本OpenGL的新特性。 现今更高版本的OpenGL已经发布目前最新是4.5你可能会问为什么我们还要学习3.3答案很简单所有OpenGL的更高的版本都是在3.3的基础上,添加了额外的功能,并不更改核心架构。新版本只是引入了一些更有效率或更有用的方式去完成同样的功能。因此所有的概念和技术在现代OpenGL版本里都保持一致。当你的经验足够你可以轻松使用来自更高版本OpenGL的新特性。
!!! Attention !!! Attention
当使用新版本OpenGL的新特性时只有新一代的显卡能够支持你的应用程序。这也是为什么大多数开发者基于较低版本的OpenGL编写程序并有选择的启用新特性。
当使用新版本的OpenGL特性时只有新一代的显卡能够支持你的应用程序。这也是为什么大多数开发者基于较低版本的OpenGL编写程序并有选择的启用新特性。
在有些教程里你会发现像如下方式注明的新特性。 在有些教程里你会发现像如下方式注明的新特性。
##扩展 ### 扩展
OpenGL的一大特性就是对扩展的支持当一个显卡公司提出一个新特性或者渲染上的大优化通常会以扩展的方式在驱动中实现。如果一个程序在支持这个扩展的显卡上运行开发者可以使用这个扩展提供的一些更先进更有效的图形功能。通过这种方式开发者不必等待一个新的OpenGL规范面世就可以方便的检查显卡是否支持此扩展。通常当一个扩展非常流行或有用的时候它将最终成为未来的OpenGL规范的一部分。 OpenGL的一大特性就是对扩展的支持当一个显卡公司提出一个新特性或者渲染上的大优化通常会以扩展的方式在驱动中实现。如果一个程序在支持这个扩展的显卡上运行开发者可以使用这个扩展提供的一些更先进更有效的图形功能。通过这种方式开发者不必等待一个新的OpenGL规范面世就可以方便的检查显卡是否支持此扩展。通常当一个扩展非常流行或有用的时候它将最终成为未来的OpenGL规范的一部分。
使用扩展的代码大多看上去如下: 使用扩展的代码大多看上去如下:
```c++ ```c++
if(GL_ARB_extension_name) if(GL_ARB_extension_name)
{ {
// 使用一些新的特性 // 使用一些新的特性
} }
else else
{ {
// 不支持此扩展: 用旧的方式去做 // 不支持此扩展: 用旧的方式去做
} }
``` ```
使用OpenGL3.3时,我们很少需要使用扩展来完成大多数功能,但是掌握这种方式是必须的。 使用OpenGL3.3时,我们很少需要使用扩展来完成大多数功能,但是掌握这种方式是必须的。
##状态机 ### 状态机
OpenGL自身是一个巨大的状态机一个描述OpenGL该如何操作的所有变量的大集合。OpenGL的状态通常被称为OpenGL**上下文**context。我们通常使用如下途径去更改OpenGL状态设置一些选项操作一些缓存。最后我们使用当前OpenGl上下文来渲染。 **OpenGL自身是一个巨大的状态机**一个描述OpenGL该如何操作的所有变量的大集合。OpenGL的状态通常被称为**OpenGL上下文OpenGL context**。我们通常使用如下途径去更改OpenGL状态设置一些选项操作一些缓存。最后我们使用当前OpenGL上下文来渲染。
假设当我们想告诉OpenGL去画线而不是三角形的时候我们通过改变一些上下文变量来改变OpenGL状态从而告诉OpenGL如何去绘图。一旦我们改变了OpenGL的状态为绘制线段下一个绘制命令就会画出线段而不是三角形。 假设当我们想告诉OpenGL去画线而不是三角形的时候我们通过改变一些上下文变量来改变OpenGL状态从而告诉OpenGL如何去绘图。一旦我们改变了OpenGL的状态为绘制线段下一个绘制命令就会画出线段而不是三角形。
用OpenGL工作时我们会遇到一些**改变OpenGL工作状态的函数**state-changing function以及一些在这些状态的基础上**执行操作的函数**state-using function。只要你记住OpenGL本质上是个大状态机就能更容易理解它的大部分特性。 用OpenGL工作时我们会遇到一些**状态设置函数**state-changing function以及一些在这些状态的基础上**状态应用的函数**state-using function。只要你记住OpenGL本质上是个大状态机就能更容易理解它的大部分特性。
##对象Object ### 对象Object
OpenGL库是用C写的同时也支持多种语言的派生但是核心是一个C库。一些C语言的结构不易被翻译到其他高层语言因此OpenGL设计的时候引入了一些抽象概念。“对象”就是其中一个。 OpenGL库是用C语言写的同时也支持多种语言的派生但是核心是一个C库。一些C语言的结构不易被翻译到其他高层语言因此OpenGL设计的时候引入了一些抽象概念。“对象”就是其中一个。
在OpenGL中一个对象是指一些选项的集合代表OpenGL状态的一个子集。比如我们可以一个对象来代表绘图窗口的设置可以设置它的大小、支持的颜色位数等等。可以把对象看做一个C风格的结构体 在OpenGL中一个对象是指一些选项的集合代表OpenGL状态的一个子集。比如我们可以一个对象来代表绘图窗口的设置可以设置它的大小、支持的颜色位数等等。可以把对象看做一个C风格的结构体
```c++ ```c++
struct object_name { struct object_name {
@@ -79,48 +83,47 @@ OpenGL库是用C写的同时也支持多种语言的派生但是核心是
!!! Important !!! Important
**原始类型** **原始类型**
使用OpenGL时建议使用OpenGL定义的原始类型。比如使用float时我们加上前缀GL因此写作GLfloatintuintcharbool等等类似。OpenGL定义这些GL原始类型平台无关的内存排列方式。而int等原始类型在不同平台上可能有不同的内存排列方式。使用GL原始类型可以保证你的程序在不同的平台上工作一致。 使用OpenGL时建议使用OpenGL定义的原始类型。比如使用float时我们加上前缀GL因此写作GLfloatintuintcharbool等等类似。OpenGL定义这些GL原始类型平台无关的内存排列方式。而int等原始类型在不同平台上可能有不同的内存排列方式。使用GL原始类型可以保证你的程序在不同的平台上工作一致。
当我们使用一个对象时通常看起来像如下一样把OpenGL上下文比作一个大的结构体 当我们使用一个对象时通常看起来像如下一样把OpenGL上下文比作一个大的结构体
```c++ ```c++
// OpenGL状态 // OpenGL状态
struct OpenGL_Context { struct OpenGL_Context
... {
object* object_Window_Target; ...
... object* object_Window_Target;
}; ...
};
``` ```
```c++ ```c++
// 创建一个对象 // 创建一个对象
GLuint objectId = 0; GLuint objectId = 0;
glGenObject(1, &objectId); glGenObject(1, &objectId);
// 绑定对象至上下文 // 绑定对象至上下文
glBindObject(GL_WINDOW_TARGET, objectId); glBindObject(GL_WINDOW_TARGET, objectId);
// 设置GL_WINDOW_TARGET对象的一些选项 // 设置GL_WINDOW_TARGET对象的一些选项
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_WIDTH, 800); glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_WIDTH, 800);
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_HEIGHT, 600); glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_HEIGHT, 600);
// 将上下文的GL_WINDOW_TARGET对象设回默认 // 将上下文的GL_WINDOW_TARGET对象设回默认
glBindObject(GL_WINDOW_TARGET, 0); glBindObject(GL_WINDOW_TARGET, 0);
``` ```
这一小片代码将会是以后使用OpenGl时常见的工作流。我们首先创建一个对象然后用一个id保存它的引用实际数据被储存在后台。然后我们将对象绑定至上下文的目标位置例子里窗口对象的目标位置被定义成GL\_WINDOW\_TARGET。接下来我们设置窗口的选项最后我们解绑这个对象,通过将目标位置的对象id设回0。我们设置的选项被保存在objectId代表的对象中一旦我们重新绑定这个对象到GL\_WINDOW\_TARGET位置这些选项就会重新生效。 这一小片代码将会是以后使用OpenGL时常见的工作流。我们首先创建一个对象然后用一个id保存它的引用实际数据被储存在后台。然后我们将对象绑定至上下文的目标位置例子里窗口对象的目标位置被定义成GL\_WINDOW\_TARGET。接下来我们设置窗口的选项最后我们通过将目标位置的对象id设回0的方式解绑这个对象。设置的选项被保存在objectId代表的对象中一旦我们重新绑定这个对象到GL\_WINDOW\_TARGET位置这些选项就会重新生效。
!!! Attention !!! Attention
目前提供的示例代码只是OpenGL如何操作的一个大致描述通过教程你会遇到很多实际的例子。 目前提供的示例代码只是OpenGL如何操作的一个大致描述通过阅读以后的教程你会遇到很多实际的例子。
使用对象的一个好处是我们在程序中不止可以定义一个对象并且设置他们的状态在我们需要进行一个操作的时候只需要绑定预设了需要设置的对象即可。比如有一个作为3D模型的数据一栋房子或一个人物由多个子模型构成容器对象在我们想绘制其中任何一个3D模型的时候只需绑定相应的子模型数据的对象我们预先创建并设置好了它们的选项就可以了。拥有数个这样的对象允许我们指定多个模型在想画其中任何一个的时候简单的将相应的对象绑定上去便不需要再进行重复的设置选项的操作了。
使用对象的一个好事是我们在程序中可以定义不止一个对象设置他们的选项当我们需要进行一个操作的时候只需要绑定预设了需要的状态的对象。比如有作为3D模型数据一栋房子或一个人物容器对象的对象任何时候我们想绘制其中一个3D模型的时候只需绑定相应的含模型数据的对象我们预先创建并设置好了它们的选项。拥有数个这样的对象允许我们指定多个模型在想画其中任何一个的时候简单的将相应的对象绑定上去不需要进行重复的设置选项的操作。 ### 让我们开始吧
##让我们开始吧 你现在已经知道一些OpenGL的相关知识了包括OpenGL作为规范和库OpenGL大致的操作流程以及一些使用扩展的小技巧。不要担心没有你还完全消化它们通过后面的教程我们会仔细地讲解每一步你会通过足够的例子来把握OpenGL。如果你已经做好了开始下一步的准备我们可以开始建立OpenGL上下文以及我们的第一个窗口了。[点击这里](http://learnopengl-cn.readthedocs.org/zh/latest/01%20Getting%20started/02%20Creating%20a%20window/)(第一章第二节)
你现在已经知道一些OpenGL的相关知识了包括OpenGL作为规范和库OpenGL大致的操作流程以及一些使用扩展的小技巧。不要担心没有完全消化它们通过这个教程我们会仔细讲解每一步你会通过足够的例子来把握OpenGL。如果你已经做好了下一步的准备我们可以开始建立OpenGL上下文以及我们的第一个窗口了。[点击这里](http://www.learnopengl.com/#!Getting-started/Creating-a-window)(第一章第二节) ### 额外的资源
##额外的资源 - [opengl.org](https://www.opengl.org/): OpenGL官方网站。
- [opengl.org](https://www.opengl.org/): OpenGL官方网站.
- [OpenGL registry](https://www.opengl.org/registry/): OpenGL各版本的规范和扩展的主站。 - [OpenGL registry](https://www.opengl.org/registry/): OpenGL各版本的规范和扩展的主站。

View File

@@ -1,18 +1,22 @@
本文作者JoeyDeVries由gjy_1992翻译自[http://learnopengl.com](http://learnopengl.com/#!Getting-started/Creating-a-window) ## 创建窗口
##创建一个窗口 原文 | [Creating a window](http://learnopengl.com/#!Getting-started/Creating-a-window)
---|---
作者 | JoeyDeVries
翻译 | gjy_1992
校对 | Geequlim
在我们画出出色的效果之前首先要做的就是创建一个OpenGL上下文和一个用于显示的窗口。然而这些操作是操作系统相关的OpenGL的目的是从这些操作中抽象出公共的部分。这意味着我们只得自己处理创建窗口定义OpenGL上下文以及处理用户输入。 在我们画出出色的效果之前首先要做的就是创建一个OpenGL上下文和一个用于显示的窗口。然而这些操作是操作系统相关的OpenGL的目的是从这些操作中抽象出公共的部分。这意味着我们不得不自己处理创建窗口定义OpenGL上下文以及处理用户输入。
幸运的是有一些库已经提供了我们所需的功能一部分是特别针对OpenGL的。这些库节省了我们书写平台相关代码的时间提供给我们一个窗口和上下文用来渲染。最流行的几个库有GLUTSDLSFML和GLFW。在我们的教程里使用GLFW。 幸运的是有一些库已经提供了我们所需的功能一部分是特别针对OpenGL的。这些库节省了我们书写平台相关代码的时间提供给我们一个窗口和上下文用来渲染。最流行的几个库有GLUTSDLSFML和GLFW。在我们的教程里使用GLFW。
##GLFW ### GLFW
GLFW是一个C库专门针对OpenGL提供了一些渲染物件所需的最低限度的接口。它允许用户创建OpenGL上下文定义窗口参数以及处理用户输入。 GLFW是一个C库专门针对OpenGL提供了一些渲染物件所需的最低限度的接口。它允许用户创建OpenGL上下文定义窗口参数以及处理用户输入。
这一节和下一节的内容是建立GLFW环境并成功用它来创建窗口和OpenGL上下文。本教程会一步步从获取编译链接GLFW库讲起。我们使用Microsoft Visual Studio 2012 **IDE**集成开发环境如果你用的不是它请不要担心大多数IDE上的步骤都是类似的。Visual Studio 2012或其他版本可以从微软网站上免费下载选择express版本或community版本 这一节和下一节的内容是建立GLFW环境并成功用它来创建窗口和OpenGL上下文。本教程会一步步从获取编译链接GLFW库讲起。我们使用Microsoft Visual Studio 2012 **IDE**集成开发环境如果你用的不是它请不要担心大多数IDE上的操作都是类似的。Visual Studio 2012或其他版本可以从微软网站上免费下载选择express版本或community版本
##生成GLFW ### 生成GLFW
GLFW可以从它们网站的[下载页](http://www.glfw.org/download.html)上获取。GLFW已经有针对Visual Studio 2012/2013的预编译的二进制版本和相应的头文件但是为了完整性我们将从编译源代码开始所以需要下载**源代码包**。 GLFW可以从它们网站的[下载页](http://www.glfw.org/download.html)上获取。GLFW已经有针对Visual Studio 2012/2013的预编译的二进制版本和相应的头文件但是为了完整性我们将从编译源代码开始所以需要下载**源代码包**。
@@ -22,7 +26,6 @@ GLFW可以从它们网站的[下载页](http://www.glfw.org/download.html)上获
当你下载二进制版本时请下载32位的版本而不是64位的除非你清楚你在做什么。64位版本被报告出现很多奇怪的问题。 当你下载二进制版本时请下载32位的版本而不是64位的除非你清楚你在做什么。64位版本被报告出现很多奇怪的问题。
一旦下载完了源码包,解压到某处。我们只关心里面的这些内容: 一旦下载完了源码包,解压到某处。我们只关心里面的这些内容:
- 编译生成的库 - 编译生成的库
@@ -30,7 +33,7 @@ GLFW可以从它们网站的[下载页](http://www.glfw.org/download.html)上获
从源代码编译库可以保证生成的目标代码是针对你的操作系统和CPU的而一个预编译的二进制代码并不保证总是适合。提供源代码的一个问题是不是每个人都用相同的IDE来编译因而提供的工程文件可能和一些人的IDE不兼容。所以人们只能从.cpp和.h文件来自己建立工程这是一项笨重的工作。因此诞生了一个叫做CMake的工具。 从源代码编译库可以保证生成的目标代码是针对你的操作系统和CPU的而一个预编译的二进制代码并不保证总是适合。提供源代码的一个问题是不是每个人都用相同的IDE来编译因而提供的工程文件可能和一些人的IDE不兼容。所以人们只能从.cpp和.h文件来自己建立工程这是一项笨重的工作。因此诞生了一个叫做CMake的工具。
##CMake #### CMake
CMake是一个工程文件生成工具可以使用预定义好的CMake脚本根据用户的选择生成不同IDE的工程文件。这允许我们从GLFW源码里创建一个Visual Studio 2012工程文件。首先我们需要从[这里](http://www.cmake.org/cmake/resources/software.html)下载安装CMake。我们选择Win32安装程序。 CMake是一个工程文件生成工具可以使用预定义好的CMake脚本根据用户的选择生成不同IDE的工程文件。这允许我们从GLFW源码里创建一个Visual Studio 2012工程文件。首先我们需要从[这里](http://www.cmake.org/cmake/resources/software.html)下载安装CMake。我们选择Win32安装程序。
@@ -40,22 +43,22 @@ CMake是一个工程文件生成工具可以使用预定义好的CMake脚本
之后点击Configure设置按钮我们选择生成的目标平台为Visual Studio 11因为Visual Studio 2012的内部版本号是11.0。CMake会显示可选的编译选项这里我们使用默认设置再次点击Configure设置按钮保存这些设置。保存之后我们可以点击Generate生成按钮生成的工程文件就会出现在你的_build_文件夹中。 之后点击Configure设置按钮我们选择生成的目标平台为Visual Studio 11因为Visual Studio 2012的内部版本号是11.0。CMake会显示可选的编译选项这里我们使用默认设置再次点击Configure设置按钮保存这些设置。保存之后我们可以点击Generate生成按钮生成的工程文件就会出现在你的_build_文件夹中。
##编译 #### 编译
在_build_文件夹里可以找到GLFW.sln文件用Visual Studio 2012打开。因为CMake已经配置好了项目所以我们直接点击**生成解决方案**然后编译的结果glfw3.lib就会出现在src/Debug文件夹内。注意我们用的glfw的版本3 在_build_文件夹里可以找到GLFW.sln文件用Visual Studio 2012打开。因为CMake已经配置好了项目所以我们直接点击**生成解决方案**然后编译的结果glfw3.lib就会出现在src/Debug文件夹内。注意我们现在使用的glfw的版本h号为3.1
生成库之后我们需要让IDE知道库和include头文件的正确放置位置。有两种方法去做这件事: 生成库之后我们需要让IDE知道库和头文件的正确放置位置。有两种方法去做这件事
1. 找到IDE/**Compiler**(编译器)的/lib和/include文件夹然后把glfw3.lib所在目录和include文件夹复制进去。这不是推荐的方式因为很难去追踪library/include文件夹而且重新安装IDE/Compiler可能会导致文件丢失。 1. 找到IDE/**Compiler**(编译器)的/lib和/include文件夹然后把glfw3.lib所在目录和include文件夹复制进去。这不是推荐的方式因为很难去追踪library/include文件夹而且重新安装IDE/Compiler可能会导致文件丢失。
2. 推荐的方式是建立一个新的目录包含所有的第三方库文件和头文件并且在你的IDE/Compiler中指定这些文件夹。我个人使用一个单独的文件夹包含Libs和Include文件夹在这里存放OpenGL工程用到的所有第三方库和头文件。这样我的所有第三方库都在同一个路径并且应该在你的多台电脑间共享然而要求是每次新建一个工程我们都需要告诉IDE/Compiler这些目录是什么。 2. 推荐的方式是建立一个新的目录包含所有的第三方库文件和头文件并且在你的IDE/Compiler中指定这些文件夹。我个人使用一个单独的文件夹包含Libs和Include文件夹在这里存放OpenGL工程用到的所有第三方库和头文件。这样我的所有第三方库都在同一个路径并且应该在你的多台电脑间共享然而要求是每次新建一个工程我们都需要告诉IDE/Compiler这些目录是什么。
完成上面步骤后我们就可以使用GLFW创建我们的第一个OpenGL工程了 完成上面步骤后我们就可以使用GLFW创建我们的第一个OpenGL工程了
##我们的第一个工程 ### 我们的第一个工程
现在让我们打开Visual Studio创建一个新的工程。如果提供了多个选项选择Visual C++,然后选择**空工程**Empty Project别忘了给你的工程起一个合适的名字。现在我们有了一个空的工程去创建我们的OpenGL程序。 现在让我们打开Visual Studio创建一个新的工程。如果提供了多个选项选择Visual C++,然后选择**空工程**Empty Project别忘了给你的工程起一个合适的名字。现在我们有了一个空的工程去创建我们的OpenGL程序。
##链接 ### 链接
为了使我们的程序使用GLFW我们需要把GLFW库链接进工程。于是我们需要在链接器的设置里写上glfw3.lib。但是我们的工程还不知道在哪寻找这个文件于是我们首先需要将我们放第三方库的目录添加进设置。 为了使我们的程序使用GLFW我们需要把GLFW库链接进工程。于是我们需要在链接器的设置里写上glfw3.lib。但是我们的工程还不知道在哪寻找这个文件于是我们首先需要将我们放第三方库的目录添加进设置。
@@ -75,13 +78,13 @@ CMake是一个工程文件生成工具可以使用预定义好的CMake脚本
要链接一个库必须指定它的文件名。于是我们在Additional Dependencies域添加这个文件。这样GLFW就会被链接进我们的工程。除了GLFW根据系统的不同可能还要链接OpenGL库。 要链接一个库必须指定它的文件名。于是我们在Additional Dependencies域添加这个文件。这样GLFW就会被链接进我们的工程。除了GLFW根据系统的不同可能还要链接OpenGL库。
###Windows上的OpenGL库 ### Windows上的OpenGL库
如果你是Windows平台opengl32.lib已经随着Microsoft SDK装进了Visual Studio的默认目录所以Windows上我们只需将opengl32.lib添加进Additional Dependencies。 如果你是Windows平台opengl32.lib已经随着Microsoft SDK装进了Visual Studio的默认目录所以Windows上我们只需将opengl32.lib添加进Additional Dependencies。
###Linux上的OpenGL库 ### Linux上的OpenGL库
在Linux下你需要链接libGl.so所以要添加-lGL到你的链接器设置里。如果找不到这个库你可能需要安装MesaNVidia 或AMD的开发包这部分因平台而异就不仔细讲解了。 在Linux下你需要链接libGl.so所以要添加-lGL到你的链接器设置里。如果找不到这个库你可能需要安装MesaNVidia或AMD的开发包这部分因平台而异就不仔细讲解了。
现在如果你添加好了GLFW和OpenGL库你可以用如下方式添加GLFW头文件 现在如果你添加好了GLFW和OpenGL库你可以用如下方式添加GLFW头文件
@@ -91,7 +94,7 @@ CMake是一个工程文件生成工具可以使用预定义好的CMake脚本
这个头文件包含了GLFW的设置。 这个头文件包含了GLFW的设置。
##GLEW ### GLEW
到这里我们仍然有一件事要做。OpenGL只是一个规范具体的实现由显卡生产商提供。由于显卡驱动版本众多大多数函数都无法在编译时确定下来需要在运行时获取。开发者需要运行时获取函数地址并保存下来供以后使用。取得地址的方法因平台而异Windows下看起来类似这样 到这里我们仍然有一件事要做。OpenGL只是一个规范具体的实现由显卡生产商提供。由于显卡驱动版本众多大多数函数都无法在编译时确定下来需要在运行时获取。开发者需要运行时获取函数地址并保存下来供以后使用。取得地址的方法因平台而异Windows下看起来类似这样
@@ -107,7 +110,7 @@ glGenBuffers(1, &buffer);
你可以看到代码复杂而笨重因为我们对于每个函数都必须这样。幸运的是有一个针对此目的的库GLEW是目前最流行的做这件事的方式。 你可以看到代码复杂而笨重因为我们对于每个函数都必须这样。幸运的是有一个针对此目的的库GLEW是目前最流行的做这件事的方式。
###编译和链接GLEW ### 编译和链接GLEW
GLEW代表OpenGL Extension Wrangler Library管理我们上面提到的繁琐的任务。因为GLEW也是一个库我们同样需要链接进工程。GLEW可以从[这里](http://glew.sourceforge.net/index.html)下载你可以选择下载二进制版本或者下载源码编译。记住优先选择32位的二进制版本。 GLEW代表OpenGL Extension Wrangler Library管理我们上面提到的繁琐的任务。因为GLEW也是一个库我们同样需要链接进工程。GLEW可以从[这里](http://glew.sourceforge.net/index.html)下载你可以选择下载二进制版本或者下载源码编译。记住优先选择32位的二进制版本。
@@ -123,20 +126,17 @@ GLEW代表OpenGL Extension Wrangler Library管理我们上面提到的繁琐
如果你希望静态链接GLEW必须在包含GLEW头文件之前定义预编译宏GLEW\_STATIC 如果你希望静态链接GLEW必须在包含GLEW头文件之前定义预编译宏GLEW\_STATIC
```c++ ```c++
#define GLEW_STATIC #define GLEW_STATIC
#include <GL/glew.h> #include <GL/glew.h>
``` ```
如果你希望动态链接那么不要定义这个宏。几乎动态链接的话你需要拷贝一份dll文件到你的应用程序目录。 如果你希望动态链接那么不要定义这个宏。但是使用动态链接的话你需要拷贝一份dll文件到你的应用程序目录。
!!! Important !!! Important
对于Linux用户建议使用这个命令行`-lGLEW -lglfw3 -lGL -lX11 -lpthread -lXrandr -lXi`。没有正确链接相应的库会产生*undefined reference* errors.(未定义的引用) 对于Linux用户建议使用这个命令行`-lGLEW -lglfw3 -lGL -lX11 -lpthread -lXrandr -lXi`。没有正确链接相应的库会产生*undefined reference* errors.(未定义的引用)
我们现在成功编译了GLFW和GLEW库我们将进入[下一节](http://learnopengl-cn.readthedocs.org/zh/latest/01%20Getting%20started/03%20Hello%20Window/)去使用GLFW和GLEW来设置OpenGL上下文并创建窗口。记住确保你的头文件和库文件的目录设置正确以及链接器里引用的库文件名正确。如果仍然遇到错误请参考额外资源中的例子。
我们现在成功编译了GLFW和GLEW库我们将进入[下一节](http://www.learnopengl.com/#!Getting-Started/Hello-Window)第一章第三节去使用GLFW和GLEW来设置OpenGL上下文并创建窗口。记住确保你的头文件和库文件的目录设置正确以及链接器里引用的库文件名正确。如果仍然遇到错误请参考额外资源中的例子。
##额外的资源 ##额外的资源

View File

@@ -1,8 +1,13 @@
本文作者JoeyDeVries由Geequlim翻译自[http://learnopengl.com](http://learnopengl.com/#!Getting-started/Hello-Window) ## 你好,窗口
##你好,窗口 原文 | [Hello Window](http://learnopengl.com/#!Getting-started/Hello-Window)
---|---
作者 | JoeyDeVries
翻译 | Geequlim
校对 | Geequlim
上一节中我们获取到了GLFW和GLEW这两个开源库现在我们就可以使用它们来创建一个OpenGL绘图窗口了。首先新建一个 .cpp文件然后把下面的代码粘贴到该文件的最前面。注意之所以定义GLEW_STATIC宏是因为我们使用GLEW的静态链接库。
上一节中我们获取并编译了GLFW和GLEW这两个开源库现在我们就可以使用它们来创建一个OpenGL绘图窗口了。首先新建一个 .cpp文件然后把下面的代码粘贴到该文件的最前面。注意之所以定义GLEW_STATIC宏是因为我们使用GLEW的静态链接库。
```c++ ```c++
// GLEW // GLEW
@@ -11,6 +16,7 @@
// GLFW // GLFW
#include <GLFW/glfw3.h> #include <GLFW/glfw3.h>
``` ```
!!! Attention !!! Attention
请确认在包含GLFW的头文件之前包含了GLEW的头文件。在包含glew.h头文件时会引入许多OpenGL必要的头文件例如GL/gl.h所以#include <GL/glew.h>应放在引入其他头文件的代码之前。 请确认在包含GLFW的头文件之前包含了GLEW的头文件。在包含glew.h头文件时会引入许多OpenGL必要的头文件例如GL/gl.h所以#include <GL/glew.h>应放在引入其他头文件的代码之前。
@@ -30,19 +36,19 @@ int main()
} }
``` ```
首先我们在main函数中调用glfwInit函数来初始化GLFW然后我们可以使用glfwWindowHint函数来配置GLFW。glfwWindowHint函数的第一个参数表示我们要进行什么样的配置我们可以选择大量以“GLFW\_”开头的枚举值第二个参数接受一个整形用来设置这个配置的值。该函数的所有的选项以及对应的值都可以在 [GLFW's window handling](http://www.glfw.org/docs/latest/window.html#window_hints) 这篇文档中找到。如果你现在编译你的cpp文件会得到大量的连接错误这是因为你还需要设置GLFW的链接库 首先我们在main函数中调用glfwInit函数来初始化GLFW然后我们可以使用glfwWindowHint函数来配置GLFW。glfwWindowHint函数的第一个参数表示我们要进行什么样的配置我们可以选择大量以“GLFW\_”开头的枚举值第二个参数接受一个整形用来设置这个配置的值。该函数的所有的选项以及对应的值都可以在 [GLFW's window handling](http://www.glfw.org/docs/latest/window.html#window_hints) 这篇文档中找到。如果你现在编译你的cpp文件会得到大量的连接错误这是因为你还需要进一步设置GLFW。
由于本站的教程都是基于OpenGL3.3以后的版本展开讨论的所以我们需要告诉GLFW我们要使用的OpenGL版本是3.3这样GLFW会在创建OpenGL上下文时做出适当的调整。这也可以确保用户在没有适当的OpenGL版本支持的情况下无法运行。在这里我们告诉GLFW想要的OpenGL版本号是3.3并且不允许用户调整窗口的大小。我们明确地告诉GLFW我们想要使用核心模式core-profile这将导致我们无法使用那些已经废弃的API而这不正是一个很好的提醒吗当我们不小心用了旧功能时报错就能避免使用一些被废弃的用法了。如果你使用的是Mac OSX系统你还需要加下面这行代码这些配置才能起作用 由于本站的教程都是基于OpenGL3.3以后的版本展开讨论的所以我们需要告诉GLFW我们要使用的OpenGL版本是3.3这样GLFW会在创建OpenGL上下文时做出适当的调整。这也可以确保用户在没有适当的OpenGL版本支持的情况下无法运行。在这里我们告诉GLFW想要的OpenGL版本号是3.3并且不允许用户调整窗口的大小。我们明确地告诉GLFW我们想要使用核心模式core-profile这将导致我们无法使用那些已经废弃的API而这不正是一个很好的提醒吗当我们不小心用了旧功能时报错就能避免使用一些被废弃的用法了。如果你使用的是Mac OSX系统你还需要加下面这行代码这些配置才能起作用
```c++ ```c++
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
``` ```
!!! Important !!! Important
请确认您的系统支持OpenGL3.3或更高版本否则此应用有可能会崩溃或者出现不可预知的错误。可以通过运行glew附带的glxinfo程序或者其他的工具例如[OpenGL Extension Viewer ](http://download.cnet.com/OpenGL-Extensions-Viewer/3000-18487_4-34442.html)来查看的OpenGL版本。如果的OpenGL版本低于3.3请更新的驱动程序或者有必要的话更新您的设备。 请确认您的系统支持OpenGL3.3或更高版本否则此应用有可能会崩溃或者出现不可预知的错误。可以通过运行glew附带的glxinfo程序或者其他的工具例如[OpenGL Extension Viewer ](http://download.cnet.com/OpenGL-Extensions-Viewer/3000-18487_4-34442.html)来查看的OpenGL版本。如果的OpenGL版本低于3.3请更新的驱动程序或者有必要的话更新设备。
接下来我们创建一个窗口对象,这个窗口对象中具有和窗口相关的许多数据而且会被GLFW的其他函数频繁地用到。 接下来我们创建一个窗口对象这个窗口对象中具有和窗口相关的许多数据而且会被GLFW的其他函数频繁地用到。
```c++ ```c++
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", nullptr, nullptr); GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", nullptr, nullptr);
@@ -58,7 +64,7 @@ glfwMakeContextCurrent(window);
glfwCreateWindow函数需要窗口的宽和高作为它的前两个参数第三个参数表示只这个窗口的名称标题这里我们使用"LearnOpenGL"当然你可以使用你喜欢的名称最后两个参数我们暂时忽略先置为空指针就行。它的返回值GLFWwindow对象的指针会在其他的GLFW操作中使用到。创建完窗口我们就可以通知GLFW给我们的窗口在当前的线程中创建我们等待已久的OpenGL上下文了。 glfwCreateWindow函数需要窗口的宽和高作为它的前两个参数第三个参数表示只这个窗口的名称标题这里我们使用"LearnOpenGL"当然你可以使用你喜欢的名称最后两个参数我们暂时忽略先置为空指针就行。它的返回值GLFWwindow对象的指针会在其他的GLFW操作中使用到。创建完窗口我们就可以通知GLFW给我们的窗口在当前的线程中创建我们等待已久的OpenGL上下文了。
###GLEW ### 初始化GLEW
在之前的教程中已经提到过GLEW是用来管理OpenGL的函数指针的所以在调用任何OpenGL的函数之前我们需要初始化GLEW。 在之前的教程中已经提到过GLEW是用来管理OpenGL的函数指针的所以在调用任何OpenGL的函数之前我们需要初始化GLEW。
@@ -70,9 +76,10 @@ if (glewInit() != GLEW_OK)
return -1; return -1;
} }
``` ```
请注意我们在初始化GLEW之前设置glewExperimental变量的值为GL_TRUE这样做能让GLEW在管理OpenGL的函数指针时更多地使用现代化的技术如果把它设置为GL_FALSE的话可能会在使用OpenGL的核心模式core-profile时出现一些问题。
###视口Viewport 请注意我们在初始化GLEW之前设置glewExperimental变量的值为GL\_TRUE这样做能让GLEW在管理OpenGL的函数指针时更多地使用现代化的技术如果把它设置为GL\_FALSE的话可能会在使用OpenGL的核心模式core-profile时出现一些问题。
### 视口Viewport
在我们绘制之前还有一件重要的事情要做我们必须告诉OpenGL渲染窗口的尺寸大小这样OpenGL才只能知道要显示数据的窗口坐标。我们可以通过调用glViewport函数来设置这些维度 在我们绘制之前还有一件重要的事情要做我们必须告诉OpenGL渲染窗口的尺寸大小这样OpenGL才只能知道要显示数据的窗口坐标。我们可以通过调用glViewport函数来设置这些维度
@@ -85,7 +92,7 @@ glViewport(0, 0, 800, 600);
OpenGL 使用glViewport定义的位置和宽高进行位置坐标的转换将OpenGL中的位置坐标转换为你的屏幕坐标。例如OpenGL中的坐标(0.5,0.5)有可能被转换为屏幕中的坐标(200,450)。注意OpenGL只会把-1到1之间的坐标转换为屏幕坐标因此在此例中(-11)转换为屏幕坐标是0,600 OpenGL 使用glViewport定义的位置和宽高进行位置坐标的转换将OpenGL中的位置坐标转换为你的屏幕坐标。例如OpenGL中的坐标(0.5,0.5)有可能被转换为屏幕中的坐标(200,450)。注意OpenGL只会把-1到1之间的坐标转换为屏幕坐标因此在此例中(-11)转换为屏幕坐标是0,600
###准备好你的引擎 ### 准备好你的引擎
我们可不希望只绘制一个图像之后我们的应用程序就关闭窗口并立即退出。我们希望程序在我们明确地关闭它之前一直保持运行状态并能够接受用户输入。因此我们需要在程序中添加一个while循环我们可以把它称之为游戏循环game loop这样我们的程序就能在我们让GLFW退出前保持运行了。下面几行的代码就实现了一个简单的游戏循环 我们可不希望只绘制一个图像之后我们的应用程序就关闭窗口并立即退出。我们希望程序在我们明确地关闭它之前一直保持运行状态并能够接受用户输入。因此我们需要在程序中添加一个while循环我们可以把它称之为游戏循环game loop这样我们的程序就能在我们让GLFW退出前保持运行了。下面几行的代码就实现了一个简单的游戏循环
@@ -106,9 +113,9 @@ while(!glfwWindowShouldClose(window))
**双缓冲区** **双缓冲区**
应用程序使用单缓冲区绘图可能会存在图像闪烁的问题。 这是因为生成的图像不是一下子被绘制出来的,而是按照从左到右,由上而下逐像素地绘制而成的。因为这些不是在瞬间显示给用户,而是通过一步一步地计算结果绘制的,这可能会花费一些时间。为了规避这些问题,我们应用双缓冲区渲染窗口应用程序。前面的缓冲区保存着计算后可显示给用户的图像,被显示到屏幕上;所有的的渲染命令被传递到后台的缓冲区进行计算。当所有的渲染命令执行结束后,我们交换前台缓冲和后台缓冲,这样图像就立即呈显出来,之后清空缓冲区。 应用程序使用单缓冲区绘图可能会存在图像闪烁的问题。 这是因为生成的图像不是一下子被绘制出来的,而是按照从左到右,由上而下逐像素地绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步地计算结果绘制的,这可能会花费一些时间。为了规避这些问题,我们应用双缓冲区渲染窗口应用程序。前面的缓冲区保存着计算后可显示给用户的图像,被显示到屏幕上;所有的的渲染命令被传递到后台的缓冲区进行计算。当所有的渲染命令执行结束后,我们交换前台缓冲和后台缓冲,这样图像就立即呈显出来,之后清空缓冲区。
###最后一件事 ### 最后一件事
当游戏循环结束后我们需要释放之前的操作分配的资源我们可以在main函数的最后调用glfwTerminate函数来释放GLFW分配的内存。 当游戏循环结束后我们需要释放之前的操作分配的资源我们可以在main函数的最后调用glfwTerminate函数来释放GLFW分配的内存。
@@ -123,15 +130,17 @@ return 0;
如果你没有编译通过或者有什么问题的话,首先请检查你程序的的链接选项是否正确 如果你没有编译通过或者有什么问题的话,首先请检查你程序的的链接选项是否正确
。然后对比本教程的代码,检查你的代码是不是哪里写错了,你也可以[点击这里](http://learnopengl.com/code_viewer.php?code=getting-started/hellowindow)获取我的完整代码。 。然后对比本教程的代码,检查你的代码是不是哪里写错了,你也可以[点击这里](http://learnopengl.com/code_viewer.php?code=getting-started/hellowindow)获取我的完整代码。
###输入 ### 输入
我们可以通过多种用户输入来控制GLFW这是通过设置GLFW的回调函数来实现的。回调函数事实上是一个函数指针当我们为GLFW设置回调函数后GLWF会在恰当的时候调用它。按键回调KeyCallback是众多回调函数中的一种当我们为GLFW设置按键回调之后GLFW会在用户有键盘交互时调用它。该回调函数的原型如下所示 我们可以通过多种用户输入来控制GLFW这是通过设置GLFW的回调函数来实现的。回调函数事实上是一个函数指针当我们为GLFW设置回调函数后GLWF会在恰当的时候调用它。按键回调KeyCallback是众多回调函数中的一种当我们为GLFW设置按键回调之后GLFW会在用户有键盘交互时调用它。该回调函数的原型如下所示
```c++ ```c++
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode); void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);
``` ```
按键回调函数接受一个GLFWwindow指针作为它的第一个参数第二个整形参数用来表示事件的按键第三个整形参数描述用户是否有第二个键按下或释放第四个整形参数表示事件类型如按下或释放最后一个参数是表示是否有Ctrl、Shift等按钮的操作。GLFW会在恰当的时候调用它并为各个参数传入适当的值。 按键回调函数接受一个GLFWwindow指针作为它的第一个参数第二个整形参数用来表示事件的按键第三个整形参数描述用户是否有第二个键按下或释放第四个整形参数表示事件类型如按下或释放最后一个参数是表示是否有Ctrl、Shift等按钮的操作。GLFW会在恰当的时候调用它并为各个参数传入适当的值。
```c++ ```c++
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode) void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{ {
@@ -141,6 +150,7 @@ void key_callback(GLFWwindow* window, int key, int scancode, int action, int mod
glfwSetWindowShouldClose(window, GL_TRUE); glfwSetWindowShouldClose(window, GL_TRUE);
} }
``` ```
当用户按下ESC键时我们设置窗口的WindowShouldClose(窗口是否应该被关闭)属性的值为真。当执行下一次游戏循环时glfwWindowShouldClose(window)返回真,此时游戏循环便不再继续,然后释放资源结束程序。 当用户按下ESC键时我们设置窗口的WindowShouldClose(窗口是否应该被关闭)属性的值为真。当执行下一次游戏循环时glfwWindowShouldClose(window)返回真,此时游戏循环便不再继续,然后释放资源结束程序。
最后我们还要为GLFW注册这个按键回调函数这样它才能被GLFW在触发按键事件时被调用。 最后我们还要为GLFW注册这个按键回调函数这样它才能被GLFW在触发按键事件时被调用。
@@ -148,9 +158,11 @@ void key_callback(GLFWwindow* window, int key, int scancode, int action, int mod
```c++ ```c++
glfwSetKeyCallback(window, key_callback); glfwSetKeyCallback(window, key_callback);
``` ```
除了按键回调函数之外我们还能为GLFW注册其他的回调函数。例如我们可以注册一个函数来处理窗口尺寸变化、处理一些错误信息等。我们可以在创建窗口之后到开始游戏循环之前注册各种回调函数。 除了按键回调函数之外我们还能为GLFW注册其他的回调函数。例如我们可以注册一个函数来处理窗口尺寸变化、处理一些错误信息等。我们可以在创建窗口之后到开始游戏循环之前注册各种回调函数。
###渲染
### 渲染
我们要把所有的渲染操作放到游戏循环中,因为我们想让这些渲染操作在每次游戏循环迭代的时候都能被执行。我们将做如下的操作: 我们要把所有的渲染操作放到游戏循环中,因为我们想让这些渲染操作在每次游戏循环迭代的时候都能被执行。我们将做如下的操作:
@@ -168,6 +180,7 @@ while(!glfwWindowShouldClose(window))
glfwSwapBuffers(window); glfwSwapBuffers(window);
} }
``` ```
为了测试一切都正常我们想让屏幕清空为一种我们选择的颜色。在每次执行新的渲染之前我们都希望清除上一次循环的渲染结果除非我们想要看到上一次的结果。我们可以通过调用glClear函数来清空屏幕缓冲区的颜色他接受一个整形常量参数来指定要清空的缓冲区这个常量可以是GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT和 GL_STENCIL_BUFFER_BIT。由于现在我们只关心颜色值所以我们只清空颜色缓冲区。 为了测试一切都正常我们想让屏幕清空为一种我们选择的颜色。在每次执行新的渲染之前我们都希望清除上一次循环的渲染结果除非我们想要看到上一次的结果。我们可以通过调用glClear函数来清空屏幕缓冲区的颜色他接受一个整形常量参数来指定要清空的缓冲区这个常量可以是GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT和 GL_STENCIL_BUFFER_BIT。由于现在我们只关心颜色值所以我们只清空颜色缓冲区。
```c++ ```c++
@@ -178,10 +191,10 @@ glClear(GL_COLOR_BUFFER_BIT);
!!! Important !!! Important
你应该能够想起来我们在 [OpenGL]() 教程的内容, glClearColor函数是一个状态设置函数而glClear函数则是一个状态应用的函数。 你应该能够想起来我们在 [OpenGL](http://learnopengl-cn.readthedocs.org/zh/latest/01%20Getting%20started/01%20OpenGL/) 教程的内容, glClearColor函数是一个状态设置函数而glClear函数则是一个状态应用的函数。
![](http://learnopengl.com/img/getting-started/hellowindow2.png) ![](http://learnopengl.com/img/getting-started/hellowindow2.png)
此程序的完整源代码可以在[这里](http://learnopengl.com/code_viewer.php?code=getting-started/hellowindow2)找到。 此程序的完整源代码可以在[这里](http://learnopengl.com/code_viewer.php?code=getting-started/hellowindow2)找到。
好了,到目前为止我们已经做好开始在游戏循环中添加许多渲染操作的准备了,我认为我们的闲扯已经够多了,从[设置:::::下一篇教程](http://www.learnopengl.com/#!Getting-started/Hello-Triangle)开始我们将真正的征程 好了,到目前为止我们已经做好开始在游戏循环中添加许多渲染操作的准备了,我认为我们的闲扯已经够多了,从下一篇教程开始我们将真正的征程

View File

@@ -1,42 +1,47 @@
本文作者JoeyDeVries由[Django](http://bullteacher.com/5-hello-triangle.html)翻译自[http://learnopengl.com](http://www.learnopengl.com/#!Getting-started/Hello-Triangle) ## 你好三角形
# Hello Triangle 原文 | [Creating a window](http://www.learnopengl.com/#!Getting-started/Hello-Triangle)
---|---
作者 | JoeyDeVries
翻译 | [Django](http://bullteacher.com/)
校对 | Geequlim
在OpenGL中任何事物都在3D空间中但是屏幕和窗口是一个2D像素阵列所以OpenGL的大部分工作都是关于如何把3D坐标转变为适应你的屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的**图形输送管道**(pipeline大多译为管线实际上指的是一堆原始图形数据途经一个输送管道期间经过各种变化处理最终出现在屏幕的过程)管理的。图形输送管道可以被划分为两个主要部分第一个部分把你的3D坐标转换为2D坐标第二部分是把2D坐标转变为实际的有颜色的像素。这个教程里我们会简单地讨论一下图形输送管道以及如何使用它创建一些像素这对我们来说是有好处的。 ### 图形渲染管线
在OpenGL中任何事物都在3D空间中但是屏幕和窗口是一个2D像素阵列所以OpenGL的大部分工作都是关于如何把3D坐标转变为适应你屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的**图形渲染管线**(pipeline大多译为管线实际上指的是一堆原始图形数据途经一个输送管道期间经过各种变化处理最终出现在屏幕的过程)管理的。图形渲染管线可以被划分为两个主要部分第一个部分把你的3D坐标转换为2D坐标第二部分是把2D坐标转变为实际的有颜色的像素。这个教程里我们会简单地讨论一下图形渲染管线以及如何使用它创建一些像素这对我们来说是有好处的。
!!! Important !!! Important
2D坐标和像素也是不同的2D坐标是在2D空间中的一个点的非常精确的表达2D像素是这个点的近似它受到你的屏幕/窗口解析度的限制。 2D坐标和像素也是不同的2D坐标是在2D空间中的一个点的非常精确的表达2D像素是这个点的近似,它受到你的屏幕/窗口解析度的限制。
图形输送管道接收一组3D坐标然后把它们转变为你屏幕上的有色2D像素。图形输送管道可以被划分为几个阶段,每个阶段需要把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们有一个特定的函数),它们能简单地并行执行。由于它们的并行执行的特征当今大多数显卡都有成千上万的小处理核心在GPU上为每一个输送管道的)阶段运行各自的小程序,从而在图形输送管道中快速处理你的数据。这些小程序叫做**着色器**(Shader)。 图形渲染管线接收一组3D坐标然后把它们转变为你屏幕上的有色2D像素。图形渲染管线可以被划分为几个阶段,每个阶段需要把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们有一个特定的函数),它们能简单地并行执行。由于它们的并行执行特性当今大多数显卡都有成千上万的小处理核心在GPU上为每一个渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做 **着色器**(Shader)。
有些着色器允许开发者自己配置,我们可以用自己写的着色器替换默认的。这样我们就可以更细致地控制输送管道的特定部分了因为它们运行在GPU上它们会节约宝贵的CPU时间。着色器是用**OpenGL着色器语言**(OpenGL Shading Language, GLSL)写成的,我们在下节会花更多时间研究它。 有些着色器允许开发者自己配置,我们可以用自己写的着色器替换默认的。这样我们就可以更细致地控制图形渲染管线中的特定部分了因为它们运行在GPU上所以它们会节约宝贵的CPU时间。OpenGL着色器是用**OpenGL着色器语言**(OpenGL Shading Language, GLSL)写成的,我们在下节会花更多时间研究它。
在下面,你会看到一个图形输送管道的每个阶段的抽象表达。要注意蓝色部分代表的是我们可以自定义的着色器。 在下面,你会看到一个图形渲染管线的每个阶段的抽象表达。要注意蓝色部分代表的是我们可以自定义的着色器。
![](http://bullteacher.com/wp-content/uploads/2015/05/pipeline.gif) ![](http://geequlim.com/assets/img/blog/LearnOpenGL/01 Getting started/OpenGL_pipline_cn.png)
如你所见,图形输入管道包含很多部分,每个都是将你的顶点数据转变为最后渲染出来的像素这个大过程中的一个特定阶段。我们会概括性地解释输送管道的每个部分,从而使你对输送管道的工作方式有个大概了解。 如你所见,图形渲染管线包含很多部分,每个都是将你的顶点数据转变为最后渲染出来的像素这个大过程中的一个特定阶段。我们会概括性地解释渲染管线的每个部分,从而使你对图形渲染管线的工作方式有个大概了解。
我们以数组的形式传递3个3D坐标作为图形输送管道的输入它用来表示一个三角形这个数组叫做顶点数据Vertex Data这里顶点数据是一些顶点的集合。一个**顶点**是一个3D坐标的集合也就是x、y、z数据。而顶点数据是用**顶点属性**vertex attributes表示的它可以包含任何我们希望用的数据但是简单起见我们还是假定每个顶点只由一个3D位置①和几个颜色值组成的吧。 我们以数组的形式传递3个3D坐标作为图形渲染管线的输入它用来表示一个三角形这个数组叫做顶点数据Vertex Data这里顶点数据是一些顶点的集合。一个**顶点**是一个3D坐标的集合也就是x、y、z数据。而顶点数据是用**顶点属性**vertex attributes表示的它可以包含任何我们希望用的数据但是简单起见我们还是假定每个顶点只由一个3D位置①和几个颜色值组成的吧。
①译注:当我们谈论一个“位置”的时候它代表在一个“空间”中所处地点的这个特殊属性同时“空间”代表着任何一种坐标系比如x、y、z三维坐标系x、y二维坐标系或者一条直线上的x和y的线性关系只不过二维坐标系是一个扁扁的平面空间而一条直线是一个很瘦的长长的空间。 ①译注:当我们谈论一个“位置”的时候它代表在一个“空间”中所处地点的这个特殊属性同时“空间”代表着任何一种坐标系比如x、y、z三维坐标系x、y二维坐标系或者一条直线上的x和y的线性关系只不过二维坐标系是一个扁扁的平面空间而一条直线是一个很瘦的长长的空间。
!!! Important !!! Important
为了让OpenGL知道我们的坐标和颜色值构成的到底是什么OpenGL需要你去提示你希望这些数据所表示的是什么类型。我们是希望把这些数据渲染成一系列的点一系列的三角形还是仅仅是一个长长的线做出的这些提示叫做**基本图形**(Primitives)任何一个绘制命令的调用都必须把基本图形类型传递给OpenGL。这是其中的几个`GL_POINTS``GL_TRIANGLES``GL_LINE_STRIP` 为了让OpenGL知道我们的坐标和颜色值构成的到底是什么OpenGL需要你去提示你希望这些数据所表示的是什么类型。我们是希望把这些数据渲染成一系列的点一系列的三角形还是仅仅是一个长长的线做出的这些提示叫做**基本图形**(Primitives)任何一个绘制命令的调用都必须把基本图形类型传递给OpenGL。这是其中的几个**GL_POINTS**、**GL_TRIANGLES**、**GL_LINE_STRIP**
输送管道的第一个部分是**顶点着色器**(Vertex Shader)它把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标后面会解释同时顶点着色器允许我们对顶点属性进行一些基本处理。 图形渲染管线的第一个部分是**顶点着色器**(Vertex Shader)它把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标后面会解释同时顶点着色器允许我们对顶点属性进行一些基本处理。
**基本图形装**Primitive Assembly阶段把顶点着色器的表示为基本图形的所有顶点作为输入如果选择的是`GL_POINTS`,那么就是一个单独顶点),把所有点组装为特定的基本图形的形状;本节例子是一个三角形。 **基本图形装**Primitive Assembly阶段把顶点着色器的表示为基本图形的所有顶点作为输入如果选择的是`GL_POINTS`,那么就是一个单独顶点),把所有点组装为特定的基本图形的形状;本节例子是一个三角形。
基本图形装阶段的输出会传递给**几何着色器**Geometry Shader。几何着色器把基本图形形式的一系列顶点的集合作为输入它可以通过产生新顶点构造出新的或是其他的基本图形来生成其他形状。例子中它生成了另一个三角形。 基本图形装阶段的输出会传递给**几何着色器**Geometry Shader。几何着色器把基本图形形式的一系列顶点的集合作为输入它可以通过产生新顶点构造出新的或是其他的基本图形来生成其他形状。例子中它生成了另一个三角形。
**细分着色器**Tessellation Shaders拥有把给定基本图形**细分**为更多小基本图形的能力。这样我们就能在物体更接近玩家的时候通过创建更多的三角形的方式创建出更加平滑的视觉效果。 **细分着色器**Tessellation Shaders拥有把给定基本图形**细分**为更多小基本图形的能力。这样我们就能在物体更接近玩家的时候通过创建更多的三角形的方式创建出更加平滑的视觉效果。
细分着色器的输出会进入**像素化**Rasterization也译为光栅阶段这里它会把基本图形映射为屏幕上相应的像素生成供片段着色器Fragment Shader使用的片段(Fragment)。在片段着色器运行之前,会执行**裁切**clipping。裁切会丢弃超出你的视图以外的那些像素来提升执行效率。 细分着色器的输出会进入**光栅化**Rasterization也译为像素阶段这里它会把基本图形映射为屏幕上相应的像素生成供片段着色器Fragment Shader使用的片段(Fragment)。在片段着色器运行之前,会执行**裁切**clipping。裁切会丢弃超出你的视图以外的那些像素来提升执行效率。
②译注Fragment通常译为片段但从根本上来说它就是带有一些额外信息的像素由于带有额外信息OpenGL就没有给它取名叫“像素”所以“片段”的中文译法比较不准确听起来就像一个fragment可能包含多个像素一样实际上一个fragment只包含一个像素现在你只要简单记住它是像素就行了。后面你会知道除了像素信息以外它还包含了什么。
!!! Important !!! Important
@@ -44,15 +49,15 @@
**片段着色器**的主要目的是计算一个像素的最终颜色这也是OpenGL高级效果产生的地方。通常片段着色器包含用来计算像素最终颜色的3D场景的一些数据比如光照、阴影、光的颜色等等 **片段着色器**的主要目的是计算一个像素的最终颜色这也是OpenGL高级效果产生的地方。通常片段着色器包含用来计算像素最终颜色的3D场景的一些数据比如光照、阴影、光的颜色等等
在所有相应颜色值确定以后,最终它会传到另一个阶段,我们叫做**alpha测试**和**混合**Blending阶段。这个阶段检测像素的相应的深度和Stencil后面会讲使用这些来检查这个像素是否在另一个物体的前面或后面如此做到相应取舍。这个阶段也会查**alpha值**alpha值是一个物体的透明度值和物体之间的**混合**Blend。所以即使在片段着色器中计算出来了一个像素所输出的颜色最后的像素颜色在渲染多个三角形的时候也可能完全不同。 在所有相应颜色值确定以后,最终它会传到另一个阶段,我们叫做**alpha测试**和**混合**Blending阶段。这个阶段检测像素的相应的深度和Stencil后面会讲使用这些来检查这个像素是否在另一个物体的前面或后面如此做到相应取舍。这个阶段也会查**alpha值**alpha值是一个物体的透明度值和物体之间的**混合**Blend。所以即使在片段着色器中计算出来了一个像素所输出的颜色,最后的像素颜色在渲染多个三角形的时候也可能完全不同。
正如你所见的那样,图形输送管道非常复杂,它包含很多要配置的部分。然而,对于大多数场合,我们必须做的只是顶点和片段着色器。几何着色器和细分着色器是可选的,通常使用默认的着色器就行了。 正如你所见的那样,图形渲染管线非常复杂,它包含很多要配置的部分。然而,对于大多数场合,我们必须做的只是顶点和片段着色器。几何着色器和细分着色器是可选的,通常使用默认的着色器就行了。
在现代OpenGL中我们**必须**定义至少一个顶点着色器和一个片段着色器因为GPU中没有默认的顶点/片段着色器。出于这个原因开始学习现代OpenGL的时候非常困难因为在你能够渲染自己的第一个三角形之前需要一大堆知识。这章结束就是你可以最终渲染出你的三角形的时候,你也会了解到很多图形编程知识。 在现代OpenGL中我们**必须**定义至少一个顶点着色器和一个片段着色器因为GPU中没有默认的顶点/片段着色器。出于这个原因开始学习现代OpenGL的时候非常困难因为在你能够渲染自己的第一个三角形之前需要一大堆知识。本节结束就是你可以最终渲染出你的三角形的时候,你也会了解到很多图形编程知识。
## 顶点输入 ### 顶点输入
开始绘制一些东西之前我们必须给OpenGL输入一些顶点数据。OpenGL是一个3D图形库所以我们在OpenGL中指定的所有坐标都是在3D坐标里x、y和z。OpenGL不是简单的把你所有的3D坐标变换为你屏幕上的2D像素OpenGL只是在当它们的3个轴x、y和z在特定的-1.0到1.0的范围内时才处理3D坐标。所有在这个范围内的坐标叫做**标准化设备坐标**Normalized Device Coordinates会最终显示在你的屏幕上所有出了这个范围的都不会显示 开始绘制一些东西之前我们必须给OpenGL输入一些顶点数据。OpenGL是一个3D图形库所以我们在OpenGL中指定的所有坐标都是在3D坐标里x、y和z。OpenGL不是简单的把你所有的3D坐标变换为你屏幕上的2D像素OpenGL只是在当它们的3个轴x、y和z在特定的-1.0到1.0的范围内时才处理3D坐标。所有在这个范围内的坐标叫做**标准化设备坐标**Normalized Device CoordinatesNDC)会最终显示在你的屏幕上(所有出了这个范围的都不会显示)。
由于我们希望渲染一个三角形我们指定所有的这三个顶点都有一个3D位置。我们把它们以`GLfloat`数组的方式定义为标准化设备坐标也就是在OpenGL的可见区域中。 由于我们希望渲染一个三角形我们指定所有的这三个顶点都有一个3D位置。我们把它们以`GLfloat`数组的方式定义为标准化设备坐标也就是在OpenGL的可见区域中。
@@ -64,9 +69,11 @@ GLfloat vertices[] = {
}; };
``` ```
由于OpenGL是在3D空间中工作的我们渲染一个2D三角形它的每个顶点都要有一个z坐标0.0。在这样的方式中三角形的每一处的深度depth都一样从而使它看上去就像2D的 由于OpenGL是在3D空间中工作的我们渲染一个2D三角形它的每个顶点都要有一个z坐标0.0。在这样的方式中三角形的每一处的深度depth都一样从而使它看上去就像2D的。
③译注通常深度可以理解为z坐标它代表一个像素在空间中和你的距离如果离你远就可能被别的像素遮挡你就看不到它了它会被丢弃以节省资源。 !!! Important
译注通常深度可以理解为z坐标它代表一个像素在空间中和你的距离如果离你远就可能被别的像素遮挡你就看不到它了它会被丢弃以节省资源。
!!! Important !!! Important
@@ -74,17 +81,17 @@ GLfloat vertices[] = {
一旦你的顶点坐标已经在顶点着色器中处理过,它们就应该是**标准化设备坐标**了标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪不会显示在你的屏幕上。下面你会看到我们定义的在标准化设备坐标中的三角形忽略z轴 一旦你的顶点坐标已经在顶点着色器中处理过,它们就应该是**标准化设备坐标**了标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪不会显示在你的屏幕上。下面你会看到我们定义的在标准化设备坐标中的三角形忽略z轴
![](http://www.learnopengl.com/img/getting-started/ndc.png) ![](http://www.learnopengl.com/img/getting-started/ndc.png)
与通常的屏幕坐标不同y轴正方向上的点和0, 0坐标是这个图像的中心而不是左上角。最后你希望所有变换过的坐标都在这个坐标空间中否则它们就不可见了。 与通常的屏幕坐标不同y轴正方向上的点和0,0坐标是这个图像的中心而不是左上角。最后你希望所有变换过的坐标都在这个坐标空间中否则它们就不可见了。
你的标准化设备坐标接着会变换为**屏幕空间坐标**Screen-space Coordinates这是使用你通过`glViewport`函数提供的数据,进行**视口变换**Viewport Transform完成的。最后的屏幕空间坐标被变换为像素输入到片段着色器。 你的标准化设备坐标接着会变换为**屏幕空间坐标**Screen-space Coordinates这是使用你通过`glViewport`函数提供的数据,进行**视口变换**Viewport Transform完成的。最后的屏幕空间坐标被变换为像素输入到片段着色器。
有了这样的顶点数据,我们会把它作为输入发送给图形输送管道的第一个处理阶段顶点着色器。它会在GPU上创建储存空间用于储存我们的顶点数据还要配置OpenGL如何解释这些内存并且指定如何发送给显卡。顶点着色器接着会处理我们告诉它要处理内存中的顶点的数量。 有了这样的顶点数据,我们会把它作为输入数据发送给图形渲染管线的第一个处理阶段顶点着色器。它会在GPU上创建储存空间用于储存我们的顶点数据还要配置OpenGL如何解释这些内存并且指定如何发送给显卡。顶点着色器接着会处理我们告诉它要处理内存中的顶点的数量。
我们通过**顶点缓冲对象**Vertex Buffer Objects, VBO管理这个内存它会在GPU内存储存大批顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢所以无论何处我们都要尝试尽量一次性发送尽可能多的数据。当数据到了显卡内存中时顶点着色器几乎立即就能获得顶点这非常快。 我们通过**顶点缓冲对象**Vertex Buffer Objects, VBO管理这个内存它会在GPU内存(通常被称为显存)中储存大批顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢所以无论何处我们都要尝试尽量一次性发送尽可能多的数据。当数据到了显卡内存中时顶点着色器几乎立即就能获得顶点这非常快。
顶点缓冲对象VBO是我们在[OpenGL教程](http://www.learnopengl.com/#!Getting-Started/OpenGL)中第一个出现的OpenGL对象。就像OpenGL中的其他对象一样这个缓冲有一个独一无二的ID所以我们可以使用`glGenBuffers`函数生成一个缓冲ID 顶点缓冲对象VBO是我们在OpenGL教程中第一个出现的OpenGL对象。就像OpenGL中的其他对象一样这个缓冲有一个独一无二的ID所以我们可以使用`glGenBuffers`函数生成一个缓冲ID
```c++ ```c++
GLuint VBO; GLuint VBO;
@@ -103,19 +110,19 @@ glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
``` ```
`glBufferData`是一个用来把用户定义数据复制到当前绑定缓冲的函数。它的第一个参数是我们希望把数据复制到上面的缓冲类型:顶点缓冲对象当前绑定到`GL_ARRAY_BUFFER`目标上。第二个参数指定我们希望传递给缓冲的数据的大小(字节);用一个简单的`sizeof`计算出顶点数据就行。第三个参数是我们希望发送的真实数据。 `glBufferData`是一个用来把用户定义数据复制到当前绑定缓冲的函数。它的第一个参数是我们希望把数据复制到上面的缓冲类型:顶点缓冲对象当前绑定到`GL_ARRAY_BUFFER`目标上。第二个参数指定我们希望传递给缓冲的数据的大小(字节为单位);用一个简单的`sizeof`计算出顶点数据就行。第三个参数是我们希望发送的真实数据。
第四个参数指定了我们希望显卡如何管理给定的数据。有三种形式: 第四个参数指定了我们希望显卡如何管理给定的数据。有三种形式:
- `GL_STATIC_DRAW`:数据不会或几乎不会改变。 - `GL_STATIC_DRAW` :数据不会或几乎不会改变。
- `GL_DYNAMIC_DRAW`:数据会被改变很多。 - `GL_DYNAMIC_DRAW`:数据会被改变很多。
- `GL_STREAM_DRAW`:数据每次绘制时都会改变。 - `GL_STREAM_DRAW` :数据每次绘制时都会改变。
三角形的位置数据不会改变,每次渲染调用时都保持原样,所以它使用的类型最好是`GL_STATIC_DRAW`。如果,比如,一个缓冲中的数据将频繁被改变,那么使用的类型就是`GL_DYNAMIC_DRAW`或`GL_STREAM_DRAW`。这样就能确保图形卡把数据放在高速写入的内存部分。 三角形的位置数据不会改变,每次渲染调用时都保持原样,所以它使用的类型最好是`GL_STATIC_DRAW`。如果,比如,一个缓冲中的数据将频繁被改变,那么使用的类型就是`GL_DYNAMIC_DRAW`或`GL_STREAM_DRAW`。这样就能确保图形卡把数据放在高速写入的内存部分。
现在我们把顶点数据储存在显卡的内存中用VBO顶点缓冲对象管理。下面我们会创建一个顶点和片段着色器来处理这些数据,所以我们开始创建它们吧。 现在我们把顶点数据储存在显卡的内存中用VBO顶点缓冲对象管理。下面我们会创建一个顶点和片段着色器来处理这些数据。现在我们开始着手创建它们吧。
## 顶点着色器 ### 顶点着色器
顶点着色器是几个着色器中的一个它是可编程的。现代OpenGL需要我们至少设置一个顶点和一个片段着色器如果我们打算做渲染的话。我们会简要介绍一下着色器以及配置两个非常简单的着色器来绘制我们第一个三角形。下个教程里我们会更详细的讨论着色器。 顶点着色器是几个着色器中的一个它是可编程的。现代OpenGL需要我们至少设置一个顶点和一个片段着色器如果我们打算做渲染的话。我们会简要介绍一下着色器以及配置两个非常简单的着色器来绘制我们第一个三角形。下个教程里我们会更详细的讨论着色器。
@@ -132,7 +139,7 @@ void main()
} }
``` ```
就像你所看到的那样GLSL看起来很像C。每个着色器都起始于一个版本声明。这是因为OpenGL3.3和更高的GLSL版本号要去匹配OpenGL的版本GLSL420版本对应于OpenGL4.2)。我们同样显式地表示我们会用核心模式(Core Profile)功能 就像你所看到的那样GLSL看起来很像C。每个着色器都起始于一个版本声明。这是因为OpenGL3.3和更高的GLSL版本号要去匹配OpenGL的版本GLSL420版本对应于OpenGL4.2)。我们同样显式地表示我们会用核心模式(Core Profile)。
下一步我们在顶点着色器中声明所有的输入顶点属性使用in关键字。现在我们只关心位置Position数据所以我们只需要一个顶点属性Attribute。GLSL有一个向量数据类型它包含1到4个`float`元素包含的数量可以从它的后缀看出来。由于每个顶点都有一个3D坐标我们就创建一个`vec3`输入变量来表示位置Position。我们同样也指定输入变量的位置值(Location),这是用`layout (location = 0)`来完成的,你后面会看到为什么我们会需要这个位置值。 下一步我们在顶点着色器中声明所有的输入顶点属性使用in关键字。现在我们只关心位置Position数据所以我们只需要一个顶点属性Attribute。GLSL有一个向量数据类型它包含1到4个`float`元素包含的数量可以从它的后缀看出来。由于每个顶点都有一个3D坐标我们就创建一个`vec3`输入变量来表示位置Position。我们同样也指定输入变量的位置值(Location),这是用`layout (location = 0)`来完成的,你后面会看到为什么我们会需要这个位置值。
@@ -146,7 +153,7 @@ void main()
这个顶点着色器可能是能想到的最简单的了因为我们什么都没有处理就把输入数据输出了。在真实的应用里输入数据通常都没有在标准化设备坐标中所以我们首先就必须把它们放进OpenGL的可视区域内。 这个顶点着色器可能是能想到的最简单的了因为我们什么都没有处理就把输入数据输出了。在真实的应用里输入数据通常都没有在标准化设备坐标中所以我们首先就必须把它们放进OpenGL的可视区域内。
## 编译一个着色器 ### 编译一个着色器
我们已经写了一个顶点着色器源码但是为了OpenGL能够使用它我们必须在运行时动态编译它的源码。 我们已经写了一个顶点着色器源码但是为了OpenGL能够使用它我们必须在运行时动态编译它的源码。
@@ -166,7 +173,7 @@ glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader); glCompileShader(vertexShader);
``` ```
`lShaderSource`函数把着色器对象作为第一个参数来编译它。第二参数指定了源码中有多少个字符串,这里只有一个。第三个参数是顶点着色器真正的源码,我们可以把第四个参数设置为`NULL`。 `glShaderSource`函数把着色器对象作为第一个参数来编译它。第二参数指定了源码中有多少个**字符串**,这里只有一个。第三个参数是顶点着色器真正的源码,我们可以把第四个参数设置为`NULL`。
!!! Important !!! Important
@@ -184,9 +191,9 @@ glCompileShader(vertexShader);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl; std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
} }
如果编译的时候没有任何错误,顶点着色器就被编译了。 如果编译的时候没有任何错误,顶点着色器就被编译成功了。
## 片段着色器(Fragment Shader) ### 片段着色器(Fragment Shader)
片段着色器是第二个也是最终我们打算创建的用于渲染三角形的着色器。片段着色器的全部,都是用来计算你的像素的最后颜色输出。为了让事情比较简单,我们的片段着色器只输出橘黄色。 片段着色器是第二个也是最终我们打算创建的用于渲染三角形的着色器。片段着色器的全部,都是用来计算你的像素的最后颜色输出。为了让事情比较简单,我们的片段着色器只输出橘黄色。
@@ -207,7 +214,7 @@ void main()
片段着色器只需要一个输出变量这个变量是一个4元素表示的最终输出颜色的向量我们可以自己计算出来。我们可以用`out`关键字声明输出变量,这里我们命名为`color`。下面我们简单的把一个带有alpha值为1.01.0代表完全不透明)的橘黄的`vec4`赋值给`color`作为输出。 片段着色器只需要一个输出变量这个变量是一个4元素表示的最终输出颜色的向量我们可以自己计算出来。我们可以用`out`关键字声明输出变量,这里我们命名为`color`。下面我们简单的把一个带有alpha值为1.01.0代表完全不透明)的橘黄的`vec4`赋值给`color`作为输出。
编译片段着色器的过程与顶点着色器相似尽管这次我们使用GL_FRAGMENT_SHADER作为着色器类型 编译片段着色器的过程与顶点着色器相似,尽管这次我们使用`GL_FRAGMENT_SHADER`作为着色器类型:
```c++ ```c++
GLuint fragmentShader; GLuint fragmentShader;
@@ -218,7 +225,7 @@ glCompileShader(fragmentShader);
每个着色器现在都编译了剩下的事情是把两个着色器对象链接到一个着色器程序中Shader Program它是用来渲染的。 每个着色器现在都编译了剩下的事情是把两个着色器对象链接到一个着色器程序中Shader Program它是用来渲染的。
## 着色器程序 ### 着色器程序
着色器程序对象Shader Program Object是多个着色器最后链接的版本。如果要使用刚才编译的着色器我们必须把它们链接为一个着色器程序对象然后当渲染物体的时候激活这个着色器程序。激活了的着色器程序的着色器在调用渲染函数时才可用。 着色器程序对象Shader Program Object是多个着色器最后链接的版本。如果要使用刚才编译的着色器我们必须把它们链接为一个着色器程序对象然后当渲染物体的时候激活这个着色器程序。激活了的着色器程序的着色器在调用渲染函数时才可用。
@@ -266,9 +273,9 @@ glDeleteShader(vertexShader);
glDeleteShader(fragmentShader); glDeleteShader(fragmentShader);
``` ```
现在我们把输入顶点数据发送给GPU指示GPU如何在顶点和片段着色器中处理它。还没结束OpenGL还不知道如何解释内存中的顶点数据以及怎样把顶点数据链接到顶点着色器的属性上。我们告诉OpenGL怎么做。 现在我们把输入顶点数据发送给GPU指示GPU如何在顶点和片段着色器中处理它。还没结束OpenGL还不知道如何解释内存中的顶点数据以及怎样把顶点数据链接到顶点着色器的属性上。我们需要告诉OpenGL怎么做。
## 链接顶点属性 ### 链接顶点属性
顶点着色器允许我们以任何我们想要的形式作为顶点属性的输入同样它也具有很强的灵活性这意味着我们必须手动指定我们的输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。这意味着我们必须在渲染前指定OpenGL如何解释顶点数据。 顶点着色器允许我们以任何我们想要的形式作为顶点属性的输入同样它也具有很强的灵活性这意味着我们必须手动指定我们的输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。这意味着我们必须在渲染前指定OpenGL如何解释顶点数据。
@@ -281,7 +288,7 @@ glDeleteShader(fragmentShader);
- 在这3个值之间没有空隙或其他值。这几个值紧密排列为一个数组。 - 在这3个值之间没有空隙或其他值。这几个值紧密排列为一个数组。
- 数据中第一个值是缓冲的开始位置。 - 数据中第一个值是缓冲的开始位置。
有了这些知识我们就可以告诉OpenGL如何解释顶点数据了每一个顶点属性我们使用`glVertexAttribPointer`这个函数: 有了这些信息我们就可以告诉OpenGL如何解释顶点数据了每一个顶点属性我们使用`glVertexAttribPointer`这个函数:
```c++ ```c++
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
@@ -291,11 +298,11 @@ glEnableVertexAttribArray(0);
`glVertexAttribPointer`函数有很多参数,所以我们仔细来了解它们: `glVertexAttribPointer`函数有很多参数,所以我们仔细来了解它们:
- 第一个参数指定我们要配置哪一个顶点属性。记住,我们在顶点着色器中使用`layout(location = 0)`定义了顶点属性——位置Position的位置值(Location)。这样要把顶点属性的位置值(Location)设置为0因为我们希望把数据传递到这个顶点属性中所以我们在这里填0。 - 第一个参数指定我们要配置哪一个顶点属性。记住,我们在顶点着色器中使用`layout(location = 0)`定义了顶点属性——位置Position的位置值(Location)。这样要把顶点属性的位置值(Location)设置为0因为我们希望把数据传递到这个顶点属性中所以我们在这里填0。
- 下一个参数指定顶点属性的大小。顶点属性是`vec3`类型它由3个数值组成。 - 第二个参数指定顶点属性的大小。顶点属性是`vec3`类型它由3个数值组成。
- 第三个参数指定数据的类型,这里是`GL_FLOAT`GLSL中`vec*`是由浮点数组成的)。 - 第三个参数指定数据的类型,这里是`GL_FLOAT`GLSL中`vec*`是由浮点数组成的)。
- 下个参数定义我们是否希望数据被标准化。如果我们设置为`GL_TRUE`所有数据都会被映射到0对于有符号型signed数据是-1到1之间。我们把它设置为`GL_FALSE`。 - 下个参数定义我们是否希望数据被标准化。如果我们设置为`GL_TRUE`所有数据都会被映射到0对于有符号型signed数据是-1到1之间。我们把它设置为`GL_FALSE`。
- 第五个参数叫做步长Stride它告诉我们在连续的顶点属性之间间隔有多少。由于下个位置数据在3个`GLfloat`后面的位置,我们把步长设置为`3 * sizeof(GLfloat)`。要注意的是由于我们知道这个数组是紧密排列的在两个顶点属性之间没有空隙我们也可以设置为0来让OpenGL决定具体步长是多少只有当数值是紧密排列时才可用。每当我们有更多的顶点属性我们就必须小心地定义每个顶点属性之间的空间我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。 - 第五个参数叫做步长Stride它告诉我们在连续的顶点属性之间间隔有多少。由于下个位置数据在3个`GLfloat`后面的位置,我们把步长设置为`3 * sizeof(GLfloat)`。要注意的是由于我们知道这个数组是紧密排列的在两个顶点属性之间没有空隙我们也可以设置为0来让OpenGL决定具体步长是多少只有当数值是紧密排列时才可用。每当我们有更多的顶点属性我们就必须小心地定义每个顶点属性之间的空间我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。
- 最后一个参数有古怪的`GLvoid*`的强制类型转换。它我们的位置数据在缓冲中起始位置的偏移量。由于位置数据是数组的开始所以这里是0。我们会在后面详细解释这个参数。 - 最后一个参数有古怪的`GLvoid*`的强制类型转换。它表示我们的位置数据在缓冲中起始位置的偏移量。由于位置数据是数组的开始所以这里是0。我们会在后面详细解释这个参数。
!!! Important !!! Important
@@ -318,9 +325,9 @@ someOpenGLFunctionThatDrawsOurTriangle();
我们绘制一个物体的时候必须重复这件事。这看起来也不多但是如果有超过5个顶点属性100多个不同物体这并不罕见呢。绑定合适的缓冲对象为每个物体配置所有顶点属性很快就变成一件麻烦事。有没有一些方法可以使我们把所有的配置储存在一个对象中并且可以通过绑定这个对象来恢复状态 我们绘制一个物体的时候必须重复这件事。这看起来也不多但是如果有超过5个顶点属性100多个不同物体这并不罕见呢。绑定合适的缓冲对象为每个物体配置所有顶点属性很快就变成一件麻烦事。有没有一些方法可以使我们把所有的配置储存在一个对象中并且可以通过绑定这个对象来恢复状态
### 顶点数组对象(Vertex Array Object) ### 顶点数组对象(Vertex Array Object,VAO)
顶点数组对象Vertex Array Object, VAO可以像顶点缓冲对象一样绑定任何随后的顶点属性调用都会储存在这个VAO中。这有一个好处当配置顶点属性指针时你只用做一次每次绘制一个物体的时候我们绑定相应VAO就行了。切换不同顶点数据和属性配置就像绑定一个不同的VAO一样简单。所有状态我们都放到了VAO里。 **顶点数组对象Vertex Array Object, VAO**可以像顶点缓冲对象一样绑定任何随后的顶点属性调用都会储存在这个VAO中。这有一个好处当配置顶点属性指针时你只用做一次每次绘制一个物体的时候我们绑定相应VAO就行了。切换不同顶点数据和属性配置就像绑定一个不同的VAO一样简单。所有状态我们都放到了VAO里。
!!! Attention !!! Attention
@@ -328,13 +335,13 @@ someOpenGLFunctionThatDrawsOurTriangle();
一个顶点数组对象储存下面的内容: 一个顶点数组对象储存下面的内容:
- 调用`glEnableVertexAttribArray`和`glDisableVertexAttribArray` - 调用`glEnableVertexAttribArray`和`glDisableVertexAttribArray`。
- 使用`glVertexAttribPointer`的顶点属性配置。 - 使用`glVertexAttribPointer`的顶点属性配置。
- 使用`glVertexAttribPointer`进行的顶点缓冲对象与顶点属性链接。 - 使用`glVertexAttribPointer`进行的顶点缓冲对象与顶点属性链接。
![](http://learnopengl.com/img/getting-started/vertex_array_objects.png) ![](http://learnopengl.com/img/getting-started/vertex_array_objects.png)
生成一个VAO和生成VBO很像 生成一个VAO和生成VBO类似
```c++ ```c++
GLuint VAO; GLuint VAO;
@@ -375,7 +382,7 @@ glBindVertexArray(0);
通常情况下当我们配置好它们以后要解绑OpenGL对象这样我们才不会在某处错误地配置它们。 通常情况下当我们配置好它们以后要解绑OpenGL对象这样我们才不会在某处错误地配置它们。
就是这样前面做的一切都是等待这一刻我们已经把我们的顶点属性配置和打算使用的VBO储存到一个VAO中。一般当你有多个物体打算绘制时你首先要生成/配置所有的VAO它需要VBO和属性指针然后储存它们准备后面使用。当我们打算绘制物体的时候吗我们就拿出相应的VAO绑定它绘制完物体后再解绑VAO。 就是现在前面做的一切都是等待这一刻我们已经把我们的顶点属性配置和打算使用的VBO储存到一个VAO中。一般当你有多个物体打算绘制时你首先要生成/配置所有的VAO它需要VBO和属性指针然后储存它们准备后面使用。当我们打算绘制物体的时候就拿出相应的VAO绑定它绘制完物体后再解绑VAO。
### 我们一直期待的三角形 ### 我们一直期待的三角形
@@ -398,7 +405,7 @@ glBindVertexArray(0);
如果你的输出和这个不一样,你可能做错了什么,去看源码,看看是否遗漏了什么东西或者在评论部分提问。 如果你的输出和这个不一样,你可能做错了什么,去看源码,看看是否遗漏了什么东西或者在评论部分提问。
## 索引缓冲对象(Element Buffer Objects) ### 索引缓冲对象(Element Buffer ObjectsEBO)
这是我们最后一件在渲染顶点这个问题上要讨论的事——索引缓冲对象简称EBO或IBO。解释索引缓冲对象的工作方式最好是举例子假设我们不再绘制一个三角形而是矩形。我们就可以绘制两个三角形来组成一个矩形OpenGL主要就是绘制三角形。这会生成下面的顶点的集合 这是我们最后一件在渲染顶点这个问题上要讨论的事——索引缓冲对象简称EBO或IBO。解释索引缓冲对象的工作方式最好是举例子假设我们不再绘制一个三角形而是矩形。我们就可以绘制两个三角形来组成一个矩形OpenGL主要就是绘制三角形。这会生成下面的顶点的集合
@@ -419,7 +426,7 @@ GLfloat vertices[] = {
就像你所看到的那样有几个顶点叠加了。我们指定右下角和左上角两次一个矩形只有4个而不是6个顶点这样就产生50%的额外开销。当我们有超过1000个三角的模型这个问题会更糟糕这会产生一大堆浪费。最好的解决方案就是每个顶点只储存一次当我们打算绘制这些顶点的时候只调用顶点的索引。这种情况我们只要储存4个顶点就能绘制矩形了我们只要指定我们打算绘制的那个顶点的索引就行了。如果OpenGL提供这个功能就好了对吧 就像你所看到的那样有几个顶点叠加了。我们指定右下角和左上角两次一个矩形只有4个而不是6个顶点这样就产生50%的额外开销。当我们有超过1000个三角的模型这个问题会更糟糕这会产生一大堆浪费。最好的解决方案就是每个顶点只储存一次当我们打算绘制这些顶点的时候只调用顶点的索引。这种情况我们只要储存4个顶点就能绘制矩形了我们只要指定我们打算绘制的那个顶点的索引就行了。如果OpenGL提供这个功能就好了对吧
很幸运索引缓冲的工作方式正是这样的。一个EBO是一个顶点缓冲对象一样的缓冲它专门储存索引OpenGL调用这些顶点的索引来绘制。索引绘制正是这个问题的解决方案。我们先要定义独一无二的顶点和绘制出矩形的索引 很幸运索引缓冲的工作方式正是这样的。一个EBO是一个顶点缓冲对象VBO一样的缓冲它专门储存索引OpenGL调用这些顶点的索引来绘制。索引绘制正是这个问题的解决方案。我们先要定义独一无二的顶点和绘制出矩形的索引
```c++ ```c++
GLfloat vertices[] = { GLfloat vertices[] = {
@@ -510,15 +517,15 @@ glBindVertexArray(0);
如果用线框模式绘制你的三角你可以配置OpenGL绘制用的基本图形调用`glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)`。第一个参数说:我们打算应用到所有的三角形的前面和背面,第二个参数告诉我们用线来绘制。在随后的绘制函数调用后会一直以线框模式绘制三角形,直到我们用`glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)`设置回了默认模式。 如果用线框模式绘制你的三角你可以配置OpenGL绘制用的基本图形调用`glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)`。第一个参数说:我们打算应用到所有的三角形的前面和背面,第二个参数告诉我们用线来绘制。在随后的绘制函数调用后会一直以线框模式绘制三角形,直到我们用`glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)`设置回了默认模式。
如果你遇到任何错误,回头检查代码,看看是否遗漏了什么。同时,你可以在这里获得全部源码可以在评论区自由提问。 如果你遇到任何错误,回头检查代码,看看是否遗漏了什么。同时,你可以[在这里获得全部源码](http://learnopengl.com/code_viewer.php?code=getting-started/hellotriangle2),也可以在评论区自由提问。
如果你绘制出了这个三角形或矩形那么恭喜你你成功地通过了现代OpenGL最难部分之一绘制你自己的第一个三角形。这部分很难因为在可以绘制第一个三角形之前需要很多知识。幸运的是我们现在已经越过了这个障碍接下来的教程会比较容易理解一些。 如果你绘制出了这个三角形或矩形那么恭喜你你成功地通过了现代OpenGL最难部分之一绘制你自己的第一个三角形。这部分很难因为在可以绘制第一个三角形之前需要很多知识。幸运的是我们现在已经越过了这个障碍接下来的教程会比较容易理解一些。
## 附加资源 ## 附加资源
[antongerdelan.net/hellotriangle](http://antongerdelan.net/opengl/hellotriangle.html): 一个渲染第一个三角形的教程。 - [antongerdelan.net/hellotriangle](http://antongerdelan.net/opengl/hellotriangle.html): 一个渲染第一个三角形的教程。
[open.gl/drawing](https://open.gl/drawing): Alexander Overvoorde的关于渲染第一个三角形的教程。 - [open.gl/drawing](https://open.gl/drawing): Alexander Overvoorde的关于渲染第一个三角形的教程。
[antongerdelan.net/vertexbuffers](http://antongerdelan.net/opengl/vertexbuffers.html): 顶点缓冲对象的一些深入探讨。 - [antongerdelan.net/vertexbuffers](http://antongerdelan.net/opengl/vertexbuffers.html): 顶点缓冲对象的一些深入探讨。
# 练习 # 练习

View File

@@ -2,7 +2,7 @@
learnopengl.com系列教程的中文翻译目前正在翻译中。 learnopengl.com系列教程的中文翻译目前正在翻译中。
**当前翻译进度:** **当前翻译进度:**
![Progress](http://progressed.io/bar/56?title=32/57) ![Progress](http://progressed.io/bar/56?title=32/57)
**翻译组成员请注意Advanced Lighting之前的教程已经翻译完成请认领Advanced Lighting及其以后的教程。** **翻译组成员请注意Advanced Lighting之前的教程已经翻译完成请认领Advanced Lighting及其以后的教程。**
@@ -10,17 +10,17 @@ learnopengl.com系列教程的中文翻译目前正在翻译中。
##如何认领翻译? ##如何认领翻译?
由于我们的志愿者来自五湖四海为了避免冲突。请志愿者们先clone这个Repository 。同步到本地后找到要翻译的文章创建一个如下所示的只包含作者、翻译者和原文链接信息的Markdown文件 由于我们的志愿者来自五湖四海为了避免冲突。请志愿者们先clone这个Repository 。同步到本地后找到要翻译的文章创建一个如下所示的只包含作者、翻译者和原文链接信息的Markdown文件
本文作者JoeyDeVries由Geequlim翻译自http://learnopengl.com 本文作者JoeyDeVries由Geequlim翻译自http://learnopengl.com
译文的文件命名统一规范为: 译文的文件命名统一规范为:
<两位数的章序列> <章名称>/<两位数节序列> 节名称/<两位数小节序列> 小节名称.md <两位数的章序列> <章名称>/<两位数节序列> 节名称/<两位数小节序列> 小节名称.md
例如: 例如:
05 Advanced Lighting/03 Shadows/02 Point Shadows.md 05 Advanced Lighting/03 Shadows/02 Point Shadows.md
加入LearnOpenGL-CN组织然后提交并push您的文章。 加入LearnOpenGL-CN组织然后提交并push您的文章。
###欢迎志愿者们加入翻译组交流QQ群383745868 ###欢迎志愿者们加入翻译组交流QQ群383745868
@@ -92,3 +92,4 @@ Vertex|顶点
Viewing Volume|可视区域 Viewing Volume|可视区域
Viewport|视口 Viewport|视口
Wireframe|线框 Wireframe|线框
pipeline | 渲染管线