计算机图形学与opengl C++版 学习笔记 第4章 管理3D图形数据


使用OpenGL渲染3D图形通常需要将若干数据集发送给OpenGL着色器管线。举个例子,想要绘制一个简单的3D对象,比如一个立方体, 你至少需要发送以下项目:

  1. 立方体模型的顶点。
  2. 控制立方体在3D空间中朝向表现的变换矩阵。

把数据发送给OpenGL管线还要更加复杂一点,有两种方式:

  1. 通过顶点属性的缓冲区。
  2. 直接发送给统一变量。

理解这两种机制具体如何工作非常重要,这样我们才能为每个要发送的项目选取合适的方法。让我们从渲染一个简单的立方体开始。

4.1 缓冲区和顶点属性

想要绘制一个对象,它的顶点数据需要被发送给顶点着色器。通常会把顶点数据在C++端放入一个缓冲区,并把这个缓冲区和着色器中声明的顶点属性相关联。要完成这件事,有好几个步骤,有些步骤只需要做一次,而如果是动画场景的话,有些步骤需要每帧都做一次:
只做一次的步骤—— 一般是在init()中。

  1. 创建一个缓冲区。
  2. 将顶点数据复制到缓冲区。

每帧都要做的步骤—— 一般是在display()中。

  1. 启用包含了顶点数据的缓冲区
  2. 将这个缓冲区和一个顶点属性相关联
  3. 启用这个顶点属性。
  4. 使用glDrawArrays(…)绘制对象。

所有缓冲区通常在程序开始的时候统一创建,可以在init()中,或者在被init()调用的函数中。

在OpenGL中,缓冲区被包含在顶点缓冲对象(Vertex Buffer Object,VBO)(VBO存储的是三角形的顶点)中,VBO在C++/OpenGL应用程序中被声明和实例化。一个场景可能需要很多VBO,所以常常会在 init()中生成并填充若干个VBO,这样在你的程序需要绘制一个或多个 VBO的时候就可以直接使用。

缓冲区使用特定的方式和顶点属性交互。当glDrawArrays()被执行时,缓冲区中的数据开始流动,从缓冲区的开头开始,按顺序流过顶点着色器。像第2章中介绍的一样,顶点着色器对每个顶点执行一次。3D空间中的顶点需要3个数值,所以着色器中的顶点属性常常会以vec3类型接收到这3个数值。然后,对缓冲区中的每组这3个数值,着色器会被调用,如图4.1所示。
在这里插入图片描述

图4.1 在VBO和顶点属性之间的数据传递

OpenGL中还有一种相关的结构,叫作顶点数组对象(Vertex Array Object,VAO)。OpenGL的3.0版本引入了VAO,作为一种组织缓冲区的方法,让缓冲区在复杂场景中更容易操控。OpenGL要求至少创建一个VAO,对我们现在来说一个就够了。

举个例子,假设我们想要显示两个对象。在C++端,我们可以声明 一个VAO和两个相关的VBO(每个对象一个),就像这样:

GLuint vao[1];// OpenGL 要求这些数值以数组的形式指定
GLuint vbo[2];
......
glGenVertexArrays(1, vao);
glBindVertexArray(vao[0]); 
glGenBuffers(2, vbo);

glGenVertexArrays()glGenBuffers()这两个OpenGL命令分别创建VAO和VBO,并返回它们的整数型ID。我们把这些ID存进整数型数组 vao和vbo中。这两个命令各自有两个参数,第一个是要创建多少个 ID,第二个是用来保存返回的ID的数组。

glBindVertexArrays()命令的目的是将指定的VAO标记为“活跃”,这样生成的缓冲区就会和这个VAO相关联。

每个缓冲区需要有在顶点着色器中声明的相应的顶点属性变量。 顶点属性通常是着色器中首先被声明的变量。在我们的立方体例子中,用来接收立方体顶点的顶点属性可以在顶点着色器中这样声明:

//layout (location = 0) 顶点属性标识 
//in 指顶点着色器从vbo读取数据
//vec3 指顶点着色器从vbo每次读取3个数值为1个数据 叫做position
layout (location = 0) in vec3 position;

我们把一个模型的顶点加载到缓冲区(VBO)的方式取决于模型的顶点数值存储在哪里。在第6章中,我们将会看到通常如何使用建模工具(比如Blender 或者Maya )创建模型、导出成标准文件格式(比如.obj,在第6章会介绍)并导入到C++/OpenGL应用程序。我们还会看到模型的顶点如何被临时计算出来,或者在管线中使用细分着色器生成出来。

现在,假设我们想要绘制一个立方体,并且假定我们的立方体的顶点数据在C++/OpenGL应用程序中的数组中直接指定。在这种情况下,我们需要
(a)将这些值复制到我们之前生成的两个缓冲区中的一个之中。为此,我们需要使用OpenGL的glBindBuffer()命令将缓冲区(例如,第0个缓冲区)标记为“活跃”;并且
(b)使用 glBufferData()命令将包含顶点数据的数组复制进活跃缓冲区(这里 应该是第0个VBO)。假设顶点存储在名为vPositions的浮点类型数组 中,以下C++代码[2]会将这些值复制到第0个VBO中:
在这里插入图片描述

接下来,我们向display()中添加代码,将缓冲区中的值发送到着色器中的顶点属性。我们通过以下3个步骤来实现:
(a)使用glBindBuffer()命令标记这个缓冲区为“活跃”,正如上所述;
(b)将活跃缓冲区与着色器中的顶点属性相关联;
(c)启用顶点属性。以下代码行实现了这些步骤:
在这里插入图片描述

现在,当我们执行glDrawArrays()时,第0个VBO中的数据将被传输给拥有位置0的layout修饰符的顶点属性中。这会将立方体的顶点数据发送到着色器

4.2 统一变量

要想渲染一个场景以使它看起来是3D的,需要构建适当的变换矩阵,例如第3章中描述的那些,并将它们应用于模型的每个顶点。在顶点着色器中应用所需的矩阵运算是最有效的,并且习惯上会将这些矩阵从C++/OpenGL应用程序发送给着色器中的统一变量。

使用uniform关键字在着色器中声明统一变量。以下示例声明了用于存储模型-视图和投影矩阵的变量,足够我们的立方体程序使用:
在这里插入图片描述

关键字“mat4”表示这些是4×4矩阵。这里我们将用来保存模型- 视图矩阵的变量命名为mv_matrix,并将用来保存投影矩阵的变量命名为proj_matrix。因为3D变换是4×4的,因此mat4是GLSL着色器统一中常用的数据类型。

将数据从C++/OpenGL应用程序发送到统一变量需要执行以下步骤:
(a)获取统一变量的引用;
(b)将指向所需数值的指针与获取的统一引用相关联。
在我们的立方体例子中,假设链接的渲染程序保存在名为renderingProgram的变量中,以下代码行表示,我们要把模型-视图和投影矩阵发送到两个统一变量mv_matrix和proj_matrix中去:
在这里插入图片描述

在上面的例子中,我们假设已经利用GLM工具来构建模型-视图和投影矩阵变换mvMat和pMat,稍后将会更详细地讨论。它们是mat4类型(GLM的一个类)的。GLM函数调用value_ptr()返回对矩阵数据的引用,glUniformMatrix4fv()需要将这些矩阵值传递给统一变量。

4.3 顶点属性插值

相较于如何处理统一变量,了解如何在OpenGL管线中处理顶点属性非常重要。回想一下,在片段着色器光栅化之前,由顶点定义的图元(例如,三角形)被转换为片段。光栅化过程会线性插值顶点属性值,以便显示的像素能无缝连接建模的曲面。

相比之下,统一变量的行为类似于初始化过的常量,并且在每次顶点着色器调用(即从缓冲区发送的每个顶点)中保持不变。统一变量本身不是插值的;无论有多少顶点,它始终包含相同的值。

光栅着色器对顶点属性进行的插值在很多方面都很有用。稍后,我们将使用光栅化来插值颜色、纹理坐标和曲面法向量。重要的是要理解通过缓冲区发送到顶点属性的所有值都将在管线中被进一步插值。

我们在顶点着色器中看到顶点属性被声明为“in”,表示它们从缓冲区接收值。顶点属性还可以改为声明为“out”,这意味着它们会将值发送到管线中的下一个阶段。例如,顶点着色器中的以下声明将指定一个名为“color”的顶点属性,该属性输出vec4类型的值:

out vec4 color;

没有必要为顶点位置声明一个“out”变量,因为OpenGL有一个内置的vec4变量用于此目的,它的名字叫作gl_Position。在顶点着色器中,我们将矩阵变换应用于传入的顶点(之前声明为位置的顶点), 并将结果赋值给gl_Position:

gl_Position = proj_matrix * mv_matrix *  position;

然后,变换后的顶点将自动输出到光栅着色器,最终将相应的像素位置发送到片段着色器。

光栅化过程如图4.2所示。在glDrawArrays()函数中指定 GL_TRIANGLES时,光栅化是逐个三角形完成的。首先沿着连接顶点的线开始插值,其精度级别和像素显示密度相关。然后通过沿连接边缘像素的水平线插值来填充三角形的内部空间中的像素。(参考这个
在这里插入图片描述

图4.2 顶点的光栅化

4.4 模型-视图和透视矩阵

渲染3D对象的一个基础步骤是创建适当的变换矩阵并将它们发送到统一变量,就像我们在4.2节中所做的那样。我们首先定义3个矩阵:

  • 一个模型矩阵
  • 一个视图矩阵
  • 一个透视矩阵

模型矩阵在世界坐标空间中表示对象的位置和朝向。每个模型都有自己的模型矩阵,如果模型移动,则需要不断重建该矩阵。

视图矩阵移动并旋转世界中的模型,以模拟相机在所需位置的效果。回忆一下第2章,OpenGL相机存在于位置(0,0,0)并且面向Z负轴。为了模拟以某种方式移动的相机的表现,我们需要向相反的方向移动物体本身。例如,将摄像机向右移动会导致场景中的物体看起来像是向左移动;虽然OpenGL相机是固定的,但我们可以通过把对象向左移动的方式,让摄像机看起来向右移动了。

透视矩阵是一种变换,它根据所需的视锥提供3D效果,如前面第3 章所述。

了解何时计算每种类型的矩阵也很重要。永远不会改变的矩阵可以在init()中构建,但那些会改变的矩阵需要在display()中构建,以便为每个帧重建它们。我们假设模型是动画的,相机是可移动的,那么需要为每个模型和每个帧都创建模型矩阵; 视图矩阵需要每帧创建一次(因为相机可以移动),但是对于在 这一帧期间渲染的所有对象,它都是一样的; 透视矩阵只需要创建一次[在init()中],它需要使用屏幕窗口的宽度和高度(以及所需的视锥体参数),除非调整窗口大小, 否则它通常保持不变。

然后在display()函数中生成模型和视图转换矩阵,如下所示。
(1)根据所需的摄像机位置和朝向构建视图矩阵。
(2)对于每个模型,进行以下操作。

	i.根据模型的位置和朝向构建模型矩阵。 
	ii.将模型和视图矩阵结合成单个“MV”矩阵。 
	iii.将MV和投影矩阵发送到相应的着色器统一变量。

从技术上讲,没有必要将模型和视图矩阵合并成一个矩阵。也就是说,它们也可以用单独分开的矩阵的形式发送给顶点着色器。

然而,将它们合并,并保持透视矩阵分离,有一些优点。例如,在顶点着色器中,模型中的每个顶点都需要乘以矩阵。由于复杂的模型可能 有数百甚至数千个顶点,因此可以通过在将模型和视图矩阵发送到顶点着色器之前预先相乘一次来提高性能。稍后,我们将看到为什么需要将透视矩阵分开以用于光照的目的。

4.5 我们的第一个3D程序—— 一个3D立方体

在给定了Y轴的指定视场角、屏幕纵横比以及所需的近、远剪裁平面(在4.9节中讨论了如何为近剪裁平面和远剪裁平面选择适当的值)的情况下,我们还需要一个构建透视矩阵的工具函数。虽然我们可以自己轻松编写这样的函数,但GLM已经包含了一个:

我们现在可以构建完整的3D立方体程序了,如下面的程序4.1所示。

vertShader.glsl

#version 430

//声明的相应的顶点属性变量 
layout (location=0) in vec3 position;
//in	代表顶点着色器从缓冲区(VBO)读取数值
//vec3	指顶点着色器每次从缓冲区拿取3个数值,这三个数值命名为position 
//layout (location=0)	把顶点属性和特定缓冲区关联起来 说明这个顶点属性的识别号是0

uniform mat4 mv_matrix;//模型-视图矩阵
uniform mat4 proj_matrix;//投影矩阵

void main(void)
{
    
    
	gl_Position = proj_matrix * mv_matrix * vec4(position,1.0);//内置输出变量,将矩阵变换应用于传入的顶点后得到的结果
} 

fragShader.glsl

#version 430

out vec4 color;

uniform mat4 mv_matrix;
uniform mat4 proj_matrix;

void main(void)
{
    
    	color = vec4(1.0, 0.0, 0.0, 1.0);
}

程序4.1 简单的红色立方体

#include <GL\glew.h>
#include <GLFW\glfw3.h>
#include <string>
#include <iostream>
#include <fstream>
#include <cmath>
#include <glm\glm.hpp>
#include <glm\gtc\type_ptr.hpp> // glm::value_ptr
#include <glm\gtc\matrix_transform.hpp> // glm::translate, glm::rotate, glm::scale, glm::perspective
#include "Utils.h"
using namespace std;

#define numVAOs 1
#define numVBOs 2

Utils util = Utils();
float cameraX, cameraY, cameraZ;
float cubeLocX, cubeLocY, cubeLocZ;
GLuint renderingProgram;
GLuint vao[numVAOs];//为了显示两个对象 声明一个VAO 两个VBO(一个对象一个)
GLuint vbo[numVBOs];

// 分配在 display() 函数中使用的变量空间,这样它们就不必在渲染过程中分配
GLuint mvLoc, projLoc;
int width, height;
float aspect;
glm::mat4 pMat, vMat, mMat, mvMat;

void setupVertices(void) {
    
    
	//顶点数值: 正方体有6个面,划分为12个三角形,共有36个顶点,每个顶点由3个数值代表空间位置,所以一个正方体需要108个数值
	float vertexPositions[108] = {
    
    
		-1.0f,  1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f,
		1.0f, -1.0f, -1.0f, 1.0f,  1.0f, -1.0f, -1.0f,  1.0f, -1.0f,
		1.0f, -1.0f, -1.0f, 1.0f, -1.0f,  1.0f, 1.0f,  1.0f, -1.0f,
		1.0f, -1.0f,  1.0f, 1.0f,  1.0f,  1.0f, 1.0f,  1.0f, -1.0f,
		1.0f, -1.0f,  1.0f, -1.0f, -1.0f,  1.0f, 1.0f,  1.0f,  1.0f,
		-1.0f, -1.0f,  1.0f, -1.0f,  1.0f,  1.0f, 1.0f,  1.0f,  1.0f,
		-1.0f, -1.0f,  1.0f, -1.0f, -1.0f, -1.0f, -1.0f,  1.0f,  1.0f,
		-1.0f, -1.0f, -1.0f, -1.0f,  1.0f, -1.0f, -1.0f,  1.0f,  1.0f,
		-1.0f, -1.0f,  1.0f,  1.0f, -1.0f,  1.0f,  1.0f, -1.0f, -1.0f,
		1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f,  1.0f,
		-1.0f,  1.0f, -1.0f, 1.0f,  1.0f, -1.0f, 1.0f,  1.0f,  1.0f,
		1.0f,  1.0f,  1.0f, -1.0f,  1.0f,  1.0f, -1.0f,  1.0f, -1.0f
	};
	//创建VAO与VBO
	glGenVertexArrays(1, vao);//创建VAO 创建的1个VAO ID(vao[0])保存在vao数组中
	glBindVertexArray(vao[0]);//将指定的VAO标记为“活跃”,这样生成的缓冲区就会和这个VAO相关联
	glGenBuffers(numVBOs, vbo);//创建VBO 创建的2个VBO ID(vbo[0] vbo[1])保存在vbo数组中

	//顶点数值传送到缓冲区vbo[0]
	glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);//将第0个VBO缓冲区 vbo[0]标记为“活跃”
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertexPositions), vertexPositions, GL_STATIC_DRAW);//将包含顶点数据的数组复制进活跃缓冲区 vbo[0]
}

void init(GLFWwindow* window) {
    
    
	renderingProgram = Utils::createShaderProgram("vertShader.glsl", "fragShader.glsl");
	cameraX = 0.0f; cameraY = 0.0f; cameraZ = 8.0f;//我眼睛的位置
	cubeLocX = 0.0f; cubeLocY = -2.0f; cubeLocZ = 0.0f;//物体的位置
	setupVertices();
}

void display(GLFWwindow* window, double currentTime) {
    
    
	glClear(GL_DEPTH_BUFFER_BIT);

	glUseProgram(renderingProgram);

	//获取着色器程序renderingProgram中统一变量mv_matrix proj_matrix的位置
	mvLoc = glGetUniformLocation(renderingProgram, "mv_matrix");
	projLoc = glGetUniformLocation(renderingProgram, "proj_matrix");

	// 构建透视矩阵
	glfwGetFramebufferSize(window, &width, &height);
	aspect = (float)width / (float)height;
	pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);

	// 构建视图矩阵、模型矩阵和视图 - 模型矩阵
	vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, -cameraY, -cameraZ));
	mMat = glm::translate(glm::mat4(1.0f), glm::vec3(cubeLocX, cubeLocY, cubeLocZ));
	mvMat = vMat * mMat;

	//将模型-视图和投影矩阵发送到两个统一变量mv_matrix和proj_matrix中去
	glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat));
	glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat));

	//VBO缓冲区中的数值发送到顶点着色器
	glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);//将第0个VBO缓冲区 vbo[0]标记为“活跃”
	glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);//将活跃缓冲区vbo[0]与着色器中的顶点属性location=0相关联
	glEnableVertexAttribArray(0);//启用顶点属性

	//调整opengl设置,绘制模型
	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LEQUAL);
	glDrawArrays(GL_TRIANGLES, 0, 36);
}

int main(void) {
    
    
	if (!glfwInit()) {
    
     exit(EXIT_FAILURE); }
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	GLFWwindow* window = glfwCreateWindow(600, 600, "Chapter 4 - program 1", NULL, NULL);
	glfwMakeContextCurrent(window);
	if (glewInit() != GLEW_OK) {
    
     exit(EXIT_FAILURE); }
	glfwSwapInterval(1);

	init(window);

	while (!glfwWindowShouldClose(window)) {
    
    
		display(window, glfwGetTime());
		glfwSwapBuffers(window);
		glfwPollEvents();
	}

	glfwDestroyWindow(window);
	glfwTerminate();
	exit(EXIT_SUCCESS);
}

图4.3 程序4.1的输出。从(0,0,8)看位于(0,−2,0)的红色立方体
在这里插入图片描述

图4.3 程序4.1的输出。从(0,0,8)看位于(0,−2,0)的红色立方体

为什么这个立方体有36个顶点,逻辑上一个立方体应该只需要8个顶点。答案是我们需要用三角形来构建我们的立方体,因此6个立方体面中的每一个都需要由两个三角形构成,总共 6×2=12个三角形(见图4.4)。由于每个三角形由3个顶点指定,因此总共有36个顶点。由于每个顶点具有3个值(x,y,z ),因此数组中总共有36×3=108个值。确实,每个顶点都参与了多个三角形的组成,但我们仍然分别指定每个顶点,因为现在我们会将每个三角形的顶点分别发送到管线。
在这里插入图片描述

图4.4 由三角形组成的立方体

立方体在它自己的坐标系中定义,中心为(0,0,0),它的角在X Y 和 Z这3条轴上分别位于−1.0~+1.0。setupVertices()的其余部分建立了VAO和两个VBO(尽管只使用了一个)并将立方体顶点加载到第0个VBO缓冲区中。

请注意,init()函数负责执行只需要执行一次的任务:读取着色器代码并构建渲染程序,并将立方体顶点加载到VBO中[通过调用setupVertices()]。请注意,它还给定了立方体和相机在世界中的位置。稍后我们将为立方体设置动画,并了解如何移动相机,到那个时候我们可能需要去除这个固定的位置。

现在让我们看一下display()函数。回想一下,display()可以被重复调用,并且调用它的速率被称为帧率。也就是说,通过不断地快速绘制和重绘场景或帧,就可以实现动画。通常需要在渲染帧之前清除深度缓冲区(存储顶点深度的缓冲区,避免画家算法弊端),以便正确地进行隐藏面消除(不清除深度缓冲区有时 会导致每个曲面都被移除,从而导致完全黑屏)。默认情况下, OpenGL中的深度值范围为0.0~1.0。调用 glClear(GL_DEPTH_BUFFER_BIT)就可以清除深度缓冲区,这会使用默认值(通常为1.0)来填充深度缓冲区。

接下来,display()通过调用glUseProgram()来启用着色器,在 GPU上安装GLSL代码。回想一下,这并不会运行着色器程序,但它会让后续的OpenGL调用能够确定着色器的顶点属性和统一变量位置。 display()函数接下来获取统一变量位置,构建透视、视图和模型矩阵[3],将视图和模型矩阵结合成单一的MV矩阵,并将透视和MV矩阵赋值给它们相应的统一变量。在这里,值得注意的是对translate()函数的 GLM调用的形式:

vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, - cameraY, -cameraZ));

看起来有点神秘的调用通过以下方式构建了一个变换矩阵:从单位矩阵开始(使用glm::mat4(1.0f)构造函数)和以向量的形式指定变换值(使用glm::vec3(x,y,z)构造函数)。许多GLM变换操作使用这种方法。

接下来,display()函数启用了包含立方体顶点数据的缓冲区,并将其附加到第0个顶点属性,以准备将顶点数据发送到着色器。

display()函数做的最后一件事是通过调用glDrawArrays()来绘制模型,指定模型由三角形组成并且总共有36个顶点。对 glDrawArrays()的调用通常在其他调整这个模型的渲染设置的命令之前。[4]在这个例子中,有两个这样的命令,这两个命令都与深度测试相关。回忆一下第2章,OpenGL使用深度测试来进行隐藏面消除。在这里,我们启用深度测试并指定希望OpenGL使用的特定深度测试。此处显示的设置对应第2章中的说明;在本书的后续内容中,我们将看到这些命令的其他用途。

最后,说一说着色器。首先,请注意它们都包含相同的统一变量声明块。虽然并不总是一定要这样做,但在特定渲染程序中的所有着色器中包含相同的统一变量声明块通常是一种好习惯。
在这里插入图片描述

在这里插入图片描述

还要注意,顶点着色器中传入的顶点属性的position变量上是否存在layout修饰符。由于它的位置被指定为“0”,因此display()函数可以简单地通过在glVertexAttribPointer()函数调用的第一个参数 和glEnableVertexAttribArray()函数调用中使用0来引用此变量。请注意,position顶点属性被声明为vec3类型,因此需要将其转换为 vec4类型,以便与将要用它乘以的4×4矩阵兼容。这个转换是用vec4(position,1.0)完成的,它用名为“position”的变量构建一个vec4,在新添加的第四个点中放置一个值1.0。

顶点着色器中的乘法将矩阵变换应用于顶点,将其转换为相机空间(请注意从右到左的结合顺序)。这些值被放入内置的OpenGL输出变量gl_Position中,然后继续通过管线并由光栅着色器进行插值。

然后插值后的像素位置(称为片段)被发送到片段着色器(Fragment Shader)。回想一下,片段着色器的主要目的是设置输出像素的颜色。以类似于顶点着色器的方式,片段着色器逐个处理像素,并为每个像素单独调用。在我们的例子中,它固定地输出对应于红色的值。由于前面指出的原因,统一变量已包含在片段着色器中, 即使它们在此示例中并未被使用。
图4.5展示了从C++/OpenGL应用程序开始并通过管线的数据流概况。
在这里插入图片描述

图4.5 程序4.1的数据流

让我们对着色器进行一些轻微的修改。特别是,我们将根据每个顶点的位置为每个顶点指定一种颜色,并将该颜色放在输出的顶点属性varyingColor中。同样,修改片段着色器以接收传入的颜色(由光栅着色器插值)并使用它来设置输出像素的颜色。请注意,代码中也将位置乘以1/2,然后加1/2,以将取值范围从[−1…+1]转换为[0… 1]。还要注意的是,通常约定在程序员定义的插值顶点属性变量名称中包含单词“varying”。每个着色器中的更改都被高亮了,结果如下所示。
修改后的顶点着色器vertShader.glsl:

#version 430

layout (location=0) in vec3 position;

uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
//根据每个顶点的位置为每个顶点指定一种颜色,并将该颜色放在输出的顶点属性varyingColor
out vec4 varyingColor;//varyingColor不是内嵌的顶点属性 对顶点着色器来说是输出值(输出到片段着色器) 所以需要自己声明为out类型

void main(void)
{
    
    	gl_Position = proj_matrix * mv_matrix * vec4(position,1.0);
	varyingColor = vec4(position,1.0)*0.5 + vec4(0.5, 0.5, 0.5, 0.5);//将取值范围从[−1…+1]转换为[0… 1]
} 

修改后的片段着色器fragShader.glsl:

#version 430

in vec4 varyingColor;// 对片段着色器来说是输入值(输入到片段着色器) 所以需要自己声明为in类型
out vec4 color;

uniform mat4 mv_matrix;
uniform mat4 proj_matrix;

void main(void)
{
    
    	color = varyingColor;
}

main.cpp不变 参考上一个main.cpp
请注意,因为颜色是从顶点着色器在顶点属性(varyingColor)中发出的,所以它们也由光栅着色器进行插值!(经过光栅着色器的属性会被自动插值)它的效果可以在图 4.6(见彩插)中看到,从一个角到另一个角的颜色在整个立方体中明显是被插值了。
在这里插入图片描述

图4.6 有插值颜色的立方体

另请注意,顶点着色器中的“out”变量varyingColor也是片段着色器中的“in”变量。两个着色器知道顶点着色器中的哪个变量提供片段着色器中的哪个变量,因为它们在两个着色器中具有相同的名称 “varyingColor”。

由于我们的main()函数包含一个渲染循环,我们可以像在程序2.6 中那样为我们的立方体设置动画,方法是使用基于时间变化的平移和旋转来构建模型矩阵。例如,程序4.1中display()函数中的代码可以修改如下(突出显示更改):
在这里插入图片描述

在模型矩阵中使用当前时间(以及各种三角函数)会使立方体看起来在空间中翻滚。请注意,添加此动画说明了每次通过display()清除深度缓存区以确保正确进行隐藏面消除的重要性。如图4.6所示,它还需要清除颜色缓冲区;否则,立方体会在移动时留下痕迹。(看看视频)
在这里插入图片描述

translate()和rotate()函数是GLM库的一部分。另外,请注意最后一行中的矩阵乘法——操作中tMat和rMat的顺序很重要。它计算两个变换的结合,平移放在左边,旋转放在右边。当顶点随后乘以此矩阵时,计算从右到左进行,这意味着首先完成旋转,然后才是平移。 变换的应用顺序很重要,改变顺序会导致不同的行为。图4.7显示了为立方体设置了动画后显示的一些帧。

在这里插入图片描述

图4.7 为3D立方体设置动画(“翻滚”)

更改后的main.cpp如下 glsl无需修改

#include <GL\glew.h>
#include <GLFW\glfw3.h>
#include <string>
#include <iostream>
#include <fstream>
#include <cmath>
#include <glm\glm.hpp>
#include <glm\gtc\type_ptr.hpp> // glm::value_ptr
#include <glm\gtc\matrix_transform.hpp> // glm::translate, glm::rotate, glm::scale, glm::perspective
#include "Utils.h"
using namespace std;

#define numVAOs 1
#define numVBOs 1

float cameraX, cameraY, cameraZ;
GLuint renderingProgram;
GLuint vao[numVAOs];
GLuint vbo[numVBOs];

// variable allocation for display
GLuint mvLoc, projLoc;
int width, height;
float aspect;
glm::mat4 pMat, vMat, tMat, rMat, mMat, mvMat;

void setupVertices(void)
{
    
    
	float vertexPositions[108] =
	{
    
     -1.0f,  1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f,
		1.0f, -1.0f, -1.0f, 1.0f,  1.0f, -1.0f, -1.0f,  1.0f, -1.0f,
		1.0f, -1.0f, -1.0f, 1.0f, -1.0f,  1.0f, 1.0f,  1.0f, -1.0f,
		1.0f, -1.0f,  1.0f, 1.0f,  1.0f,  1.0f, 1.0f,  1.0f, -1.0f,
		1.0f, -1.0f,  1.0f, -1.0f, -1.0f,  1.0f, 1.0f,  1.0f,  1.0f,
		-1.0f, -1.0f,  1.0f, -1.0f,  1.0f,  1.0f, 1.0f,  1.0f,  1.0f,
		-1.0f, -1.0f,  1.0f, -1.0f, -1.0f, -1.0f, -1.0f,  1.0f,  1.0f,
		-1.0f, -1.0f, -1.0f, -1.0f,  1.0f, -1.0f, -1.0f,  1.0f,  1.0f,
		-1.0f, -1.0f,  1.0f,  1.0f, -1.0f,  1.0f,  1.0f, -1.0f, -1.0f,
		1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f,  1.0f,
		-1.0f,  1.0f, -1.0f, 1.0f,  1.0f, -1.0f, 1.0f,  1.0f,  1.0f,
		1.0f,  1.0f,  1.0f, -1.0f,  1.0f,  1.0f, -1.0f,  1.0f, -1.0f
	};

	glGenVertexArrays(1, vao);
	glBindVertexArray(vao[0]);
	glGenBuffers(numVBOs, vbo);

	glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertexPositions), vertexPositions, GL_STATIC_DRAW);
}

void init(GLFWwindow* window) {
    
    
	renderingProgram = Utils::createShaderProgram("vertShader.glsl", "fragShader.glsl");

	glfwGetFramebufferSize(window, &width, &height);
	aspect = (float)width / (float)height;
	pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);

	cameraX = 0.0f; cameraY = 0.0f; cameraZ = 8.0f;
	setupVertices();
}

void display(GLFWwindow* window, double currentTime) {
    
    
	glClear(GL_DEPTH_BUFFER_BIT);//清除深度缓存 进行隐藏面消除
	glClearColor(0.0, 0.0, 0.0, 1.0);
	glClear(GL_COLOR_BUFFER_BIT);//清除颜色缓存否则,立方体会在移动时留下痕迹.屏蔽这个运行代码很好玩

	glUseProgram(renderingProgram);

	mvLoc = glGetUniformLocation(renderingProgram, "mv_matrix");
	projLoc = glGetUniformLocation(renderingProgram, "proj_matrix");

	vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, -cameraY, -cameraZ));

	//使用当前时间来计算x y z的不同变换
	tMat = glm::translate(glm::mat4(1.0f), glm::vec3(sin(0.35f*currentTime)*2.0f, cos(0.52f*currentTime)*2.0f, sin(0.7f*currentTime)*2.0f));
	//用1.75调整旋转速度
	rMat = glm::rotate(glm::mat4(1.0f), 1.75f*(float)currentTime, glm::vec3(0.0f, 1.0f, 0.0f));
	rMat = glm::rotate(rMat, 1.75f*(float)currentTime, glm::vec3(1.0f, 0.0f, 0.0f));
	rMat = glm::rotate(rMat, 1.75f*(float)currentTime, glm::vec3(0.0f, 0.0f, 1.0f));

	mMat = tMat * rMat;//先旋转再平移

	mvMat = vMat * mMat;

	glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat));
	glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat));

	glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
	glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);
	glEnableVertexAttribArray(0);

	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LEQUAL);

	glDrawArrays(GL_TRIANGLES, 0, 36);
}

void window_size_callback(GLFWwindow* win, int newWidth, int newHeight) {
    
    
	aspect = (float)newWidth / (float)newHeight;
	glViewport(0, 0, newWidth, newHeight);
	pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);
}

int main(void) {
    
    
	if (!glfwInit()) {
    
     exit(EXIT_FAILURE); }
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	GLFWwindow* window = glfwCreateWindow(600, 600, "Chapter 4 - program 1c", NULL, NULL);
	glfwMakeContextCurrent(window);
	if (glewInit() != GLEW_OK) {
    
     exit(EXIT_FAILURE); }
	glfwSwapInterval(1);

	glfwSetWindowSizeCallback(window, window_size_callback);

	init(window);

	while (!glfwWindowShouldClose(window)) {
    
    
		display(window, glfwGetTime());
		glfwSwapBuffers(window);
		glfwPollEvents();
	}

	glfwDestroyWindow(window);
	glfwTerminate();
	exit(EXIT_SUCCESS);
}

4.6 渲染一个对象的多个副本

现在将我们学到的知识扩展到渲染多个对象。在我们解决在单个场景中渲染多种不同的模型的常见情况之前,让我们先考虑更简单的情形——同一模型多次出现。例如,假设我们希望扩展前面的示例, 以便呈现“一大群”(24个)翻滚的立方体。我们可以将display()函 数中构建MV矩阵并绘制立方体的代码(如下所示突出显示部分),移动到一个执行24次的循环中来完成此操作。我们利用循环变量来计算 立方体的旋转和平移参数,以便每次绘制立方体时,都会构建不同的模型矩阵。(我们还将摄像机放置在正Z轴的下方,这样我们就可以看到所有的立方体。)图4.8显示了一帧由此产生的动画场景。

在这里插入图片描述
在这里插入图片描述

图4.8 多个翻滚的立方体

更改后的main.cpp如下 glsl无需修改加粗样式

#include <GL\glew.h>
#include <GLFW\glfw3.h>
#include <string>
#include <iostream>
#include <fstream>
#include <cmath>
#include <glm\glm.hpp>
#include <glm\gtc\type_ptr.hpp> // glm::value_ptr
#include <glm\gtc\matrix_transform.hpp> // glm::translate, glm::rotate, glm::scale, glm::perspective
#include "Utils.h"
using namespace std;

#define numVAOs 1
#define numVBOs 1

float cameraX, cameraY, cameraZ;
GLuint renderingProgram;
GLuint vao[numVAOs];
GLuint vbo[numVBOs];

// variable allocation for display
GLuint mvLoc, projLoc;
int width, height, displayLoopi;
float aspect;
float tf;
glm::mat4 pMat, vMat, tMat, rMat, mMat, mvMat;

void setupVertices(void)
{
    
    
	float vertexPositions[108] =
	{
    
     -1.0f,  1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f,
		1.0f, -1.0f, -1.0f, 1.0f,  1.0f, -1.0f, -1.0f,  1.0f, -1.0f,
		1.0f, -1.0f, -1.0f, 1.0f, -1.0f,  1.0f, 1.0f,  1.0f, -1.0f,
		1.0f, -1.0f,  1.0f, 1.0f,  1.0f,  1.0f, 1.0f,  1.0f, -1.0f,
		1.0f, -1.0f,  1.0f, -1.0f, -1.0f,  1.0f, 1.0f,  1.0f,  1.0f,
		-1.0f, -1.0f,  1.0f, -1.0f,  1.0f,  1.0f, 1.0f,  1.0f,  1.0f,
		-1.0f, -1.0f,  1.0f, -1.0f, -1.0f, -1.0f, -1.0f,  1.0f,  1.0f,
		-1.0f, -1.0f, -1.0f, -1.0f,  1.0f, -1.0f, -1.0f,  1.0f,  1.0f,
		-1.0f, -1.0f,  1.0f,  1.0f, -1.0f,  1.0f,  1.0f, -1.0f, -1.0f,
		1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f,  1.0f,
		-1.0f,  1.0f, -1.0f, 1.0f,  1.0f, -1.0f, 1.0f,  1.0f,  1.0f,
		1.0f,  1.0f,  1.0f, -1.0f,  1.0f,  1.0f, -1.0f,  1.0f, -1.0f
	};

	glGenVertexArrays(1, vao);
	glBindVertexArray(vao[0]);
	glGenBuffers(numVBOs, vbo);

	glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertexPositions), vertexPositions, GL_STATIC_DRAW);
}

void init(GLFWwindow* window) {
    
    
	renderingProgram = Utils::createShaderProgram("vertShader.glsl", "fragShader.glsl");

	glfwGetFramebufferSize(window, &width, &height);
	aspect = (float)width / (float)height;
	pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);

	cameraX = 0.0f; cameraY = 0.0f; cameraZ = 32.0f;
	setupVertices();
}

void display(GLFWwindow* window, double currentTime) {
    
    
	glClear(GL_DEPTH_BUFFER_BIT);
	glClearColor(0.0, 0.0, 0.0, 1.0);
	glClear(GL_COLOR_BUFFER_BIT);

	glUseProgram(renderingProgram);

	mvLoc = glGetUniformLocation(renderingProgram, "mv_matrix");
	projLoc = glGetUniformLocation(renderingProgram, "proj_matrix");

	vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, -cameraY, -cameraZ));

	glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat));

	for (displayLoopi = 0; displayLoopi<24; displayLoopi++) {
    
    //绘制24个方块 
		tf = currentTime + displayLoopi;//tf=='time factor'时间因子 与时间和方块索引有关
		tMat = glm::translate(glm::mat4(1.0f), glm::vec3(sin(.35f*tf)*8.0f, cos(.52f*tf)*8.0f, sin(.70f*tf)*8.0f));//方块平移位置改为与时间相关 造成动画效果
		//旋转速度不变
		rMat = glm::rotate(glm::mat4(1.0f), 1.75f*tf, glm::vec3(0.0f, 1.0f, 0.0f));
		rMat = glm::rotate(rMat, 1.75f*tf, glm::vec3(1.0f, 0.0f, 0.0f));
		rMat = glm::rotate(rMat, 1.75f*tf, glm::vec3(0.0f, 0.0f, 1.0f));
		mMat = tMat * rMat;
		mvMat = vMat * mMat;

		glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat));

		glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
		glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);
		glEnableVertexAttribArray(0);

		glEnable(GL_DEPTH_TEST);
		glDepthFunc(GL_LEQUAL);

		glDrawArrays(GL_TRIANGLES, 0, 36);
	}
}

void window_size_callback(GLFWwindow* win, int newWidth, int newHeight) {
    
    
	aspect = (float)newWidth / (float)newHeight;
	glViewport(0, 0, newWidth, newHeight);
	pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);
}

int main(void) {
    
    
	if (!glfwInit()) {
    
     exit(EXIT_FAILURE); }
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	GLFWwindow* window = glfwCreateWindow(600, 600, "Chapter 4 - program 1d", NULL, NULL);
	glfwMakeContextCurrent(window);
	if (glewInit() != GLEW_OK) {
    
     exit(EXIT_FAILURE); }
	glfwSwapInterval(1);

	glfwSetWindowSizeCallback(window, window_size_callback);

	init(window);

	while (!glfwWindowShouldClose(window)) {
    
    
		display(window, glfwGetTime());
		glfwSwapBuffers(window);
		glfwPollEvents();
	}

	glfwDestroyWindow(window);
	glfwTerminate();
	exit(EXIT_SUCCESS);
}

实例化(参考unity实例化 其实就是创建副本)

实例化(Instancing)提供了一种机制,可以只用一个 C++/OpenGL调用就告诉显卡渲染一个对象的多个副本。这可以带来显著的性能好处,特别是当有数千甚至数百万的对象被绘制时——例如渲染在场地中的许多花朵的时候。

我们首先将我们的C++/OpenGL应用程序中的glDrawArrays()调用改为glDrawArraysInstanced()。这样,我们就可以要求OpenGL绘制尽可能多的副本。我们可以指定绘制如下24个立方体:

glDrawArraysInstanced(GL_TRIANGLES, 0, 36,  24);

使用实例化时,顶点着色器可以访问内置变量gl_InstanceID,这是一个整数,指向当前正在处理对象的第几个实例。

为了使用实例化来重复我们以前的翻滚立方体示例,我们需要将构建不同模型矩阵的计算[先前在display()中的循环内实现]移动到顶点着色器中。由于GLSL不提供平移或旋转函数,并且我们无法从着色器内部调用GLM,因此我们需要使用程序3.1中的工具函数。我们还需要将C++/OpenGL应用程序中的“时间因子”通过统一变量传递给顶点着色器。我们还需要将模型和视图矩阵传递到单独的统一变量中,因为对每个立方体的模型矩阵都需要进行旋转计算。我们对代码的修改,包括C++/OpenGL应用程序中的修改以及新的顶点着色器中的修改,如程序4.2所示。

vertShader.glsl

#version 430

layout (location=0) in vec3 position;

uniform mat4 v_matrix;
uniform mat4 proj_matrix;
uniform float tf;

out vec4 varyingColor;

mat4 buildRotateX(float rad);
mat4 buildRotateY(float rad);
mat4 buildRotateZ(float rad);
mat4 buildTranslate(float x, float y, float z);

void main(void)
{
    
    	float x = gl_InstanceID + tf;
//	float a = sin(203.0 * x/8000.0) * 403.0;	//修改这里 when 100000 instances
//	float b = cos(301.0 * x/4001.0) * 401.0;
//	float c = sin(400.0 * x/6003.0) * 405.0;
	
	float a = sin(.35 * x) * 8.0;				// 修改这里 when 24 instances
	float b = cos(.52 * x) * 8.0;
	float c = sin(.70 * x) * 8.0;
	
	mat4 localRotX = buildRotateX(1.75*x);
	mat4 localRotY = buildRotateY(1.75*x);
	mat4 localRotZ = buildRotateZ(1.75*x);
	mat4 localTrans = buildTranslate(a,b,c);

	mat4 newM_matrix = localTrans * localRotX * localRotY * localRotZ;
	mat4 mv_matrix = v_matrix * newM_matrix;
	gl_Position = proj_matrix * mv_matrix * vec4(position,1.0);
	varyingColor = vec4(position,1.0)*0.5 + vec4(0.5, 0.5, 0.5, 0.5);
}

mat4 buildTranslate(float x, float y, float z)
{
    
    	mat4 trans = mat4(	1.0, 0.0, 0.0, 0.0,
		0.0, 1.0, 0.0, 0.0,
		0.0, 0.0, 1.0, 0.0,
		x, y, z, 1.0 );
	return trans;
}

//  rotation around the X axis
mat4 buildRotateX(float rad)
{
    
    	mat4 xrot = mat4(	1.0, 0.0, 0.0, 0.0,
		0.0, cos(rad), -sin(rad), 0.0,
		0.0, sin(rad), cos(rad), 0.0,
		0.0, 0.0, 0.0, 1.0 );
	return xrot;
}

//  rotation around the Y axis
mat4 buildRotateY(float rad)
{
    
    	mat4 yrot = mat4(	cos(rad), 0.0, sin(rad), 0.0,
		0.0, 1.0, 0.0, 0.0,
		-sin(rad), 0.0, cos(rad), 0.0,
		0.0, 0.0, 0.0, 1.0 );
	return yrot;
}

//  rotation around the Z axis
mat4 buildRotateZ(float rad)
{
    
    	mat4 zrot = mat4(	cos(rad), sin(rad), 0.0, 0.0,
		-sin(rad), cos(rad), 0.0, 0.0,
		0.0, 0.0, 1.0, 0.0,
		0.0, 0.0, 0.0, 1.0 );
	return zrot;
}

fragShader.glsl

#version 430

in vec4 varyingColor;
out vec4 color;

uniform mat4 mv_matrix;
uniform mat4 proj_matrix;

void main(void)
{
    
    	color = varyingColor;
}

程序4.2 实例化——24个动画立方体

#include <GL\glew.h>
#include <GLFW\glfw3.h>
#include <string>
#include <iostream>
#include <fstream>
#include <cmath>
#include <glm\glm.hpp>
#include <glm\gtc\type_ptr.hpp> // glm::value_ptr
#include <glm\gtc\matrix_transform.hpp> // glm::translate, glm::rotate, glm::scale, glm::perspective
#include "Utils.h"
using namespace std;

#define numVAOs 1
#define numVBOs 1

float cameraX, cameraY, cameraZ;
GLuint renderingProgram;
GLuint vao[numVAOs];
GLuint vbo[numVBOs];

// variable allocation for display
GLuint mLoc, vLoc, projLoc, tfLoc;
int width, height;
float aspect, timeFactor;
glm::mat4 pMat, vMat, mMat, mvMat;

void setupVertices(void)
{
    
    
	float vertexPositions[108] =
	{
    
     -1.0f,  1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f,
		1.0f, -1.0f, -1.0f, 1.0f,  1.0f, -1.0f, -1.0f,  1.0f, -1.0f,
		1.0f, -1.0f, -1.0f, 1.0f, -1.0f,  1.0f, 1.0f,  1.0f, -1.0f,
		1.0f, -1.0f,  1.0f, 1.0f,  1.0f,  1.0f, 1.0f,  1.0f, -1.0f,
		1.0f, -1.0f,  1.0f, -1.0f, -1.0f,  1.0f, 1.0f,  1.0f,  1.0f,
		-1.0f, -1.0f,  1.0f, -1.0f,  1.0f,  1.0f, 1.0f,  1.0f,  1.0f,
		-1.0f, -1.0f,  1.0f, -1.0f, -1.0f, -1.0f, -1.0f,  1.0f,  1.0f,
		-1.0f, -1.0f, -1.0f, -1.0f,  1.0f, -1.0f, -1.0f,  1.0f,  1.0f,
		-1.0f, -1.0f,  1.0f,  1.0f, -1.0f,  1.0f,  1.0f, -1.0f, -1.0f,
		1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f,  1.0f,
		-1.0f,  1.0f, -1.0f, 1.0f,  1.0f, -1.0f, 1.0f,  1.0f,  1.0f,
		1.0f,  1.0f,  1.0f, -1.0f,  1.0f,  1.0f, -1.0f,  1.0f, -1.0f
	};

	glGenVertexArrays(1, vao);
	glBindVertexArray(vao[0]);
	glGenBuffers(numVBOs, vbo);

	glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertexPositions), vertexPositions, GL_STATIC_DRAW);
}

void init(GLFWwindow* window) {
    
    
	renderingProgram = Utils::createShaderProgram("vertShader.glsl", "fragShader.glsl");

	glfwGetFramebufferSize(window, &width, &height);
	aspect = (float)width / (float)height;
	pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);

	cameraX = 0.0f; cameraY = 0.0f; cameraZ = 32.0f; //修改这里 Z=32.0f when 24 instances, 420.0f when 100000 instances
	setupVertices();
}

void display(GLFWwindow* window, double currentTime) {
    
    
	glClear(GL_DEPTH_BUFFER_BIT);
	glClearColor(0.0, 0.0, 0.0, 1.0);
	glClear(GL_COLOR_BUFFER_BIT);

	glUseProgram(renderingProgram);

	vLoc = glGetUniformLocation(renderingProgram, "v_matrix");
	projLoc = glGetUniformLocation(renderingProgram, "proj_matrix");

	vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, -cameraY, -cameraZ));

	glUniformMatrix4fv(vLoc, 1, GL_FALSE, glm::value_ptr(vMat));
	glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat));

	timeFactor = ((float)currentTime);
	tfLoc = glGetUniformLocation(renderingProgram, "tf");
	glUniform1f(tfLoc, (float)timeFactor);

	glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
	glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);
	glEnableVertexAttribArray(0);

	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LEQUAL);

	glDrawArraysInstanced(GL_TRIANGLES, 0, 36, 24);	// 修改这里 0, 36, 24  (or 100000)
}

void window_size_callback(GLFWwindow* win, int newWidth, int newHeight) {
    
    
	aspect = (float)newWidth / (float)newHeight;
	glViewport(0, 0, newWidth, newHeight);
	pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);
}

int main(void) {
    
    
	if (!glfwInit()) {
    
     exit(EXIT_FAILURE); }
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	GLFWwindow* window = glfwCreateWindow(600, 600, "Chapter 4 - program 2", NULL, NULL);
	glfwMakeContextCurrent(window);
	if (glewInit() != GLEW_OK) {
    
     exit(EXIT_FAILURE); }
	glfwSwapInterval(1);

	glfwSetWindowSizeCallback(window, window_size_callback);

	init(window);

	while (!glfwWindowShouldClose(window)) {
    
    
		display(window, glfwGetTime());
		glfwSwapBuffers(window);
		glfwPollEvents();
	}

	glfwDestroyWindow(window);
	glfwTerminate();
	exit(EXIT_SUCCESS);
}

程序4.2的输出结果与前一个示例相同,可以在前面的图4.8中看到。

实例化让我们可以极大地扩展对象的副本数量;在这个例子中, 即使对于很普通的GPU,实现100 000个立方体的动画仍然是可行的。对代码的更改主要是一些常量的修改,是为了将大量立方体进一步分散开,如下所示。
顶点着色器如下:
在这里插入图片描述

C++/OpenGL 应用程序如下:
在这里插入图片描述

输出结果如图4.9所示。
程序没有多大变化,只是把main.cpp和vertShader.glsl稍作修改 请参考这两个文件的注释部分

在这里插入图片描述

图4.9 实例化:100 000个动画立方体

4.7 在同一个场景中渲染多个不同模型

要在单个场景中渲染多个模型,一种简单的方法是为每个模型使用单独的缓冲区。每个模型都需要自己的模型矩阵,这样我们就需要为我们渲染的每个模型生成一个新的模型-视图矩阵。还需要为每个模型单独调用glDrawArrays()。因此,我们需要修改init()和display() 函数。

另一个考虑因素是我们是否需要为我们想要绘制的每个对象使用不同的着色器或不同的渲染程序。事实证明,在许多情况下,我们可以为我们绘制的各种对象使用相同的着色器(以及相同的渲染程 序)。只有当它们由不同的图元(例如线而不是三角形)组成,或者涉及复杂的照明或其他效果的时候,我们才会需要为各种对象使用不 同的渲染程序。目前并没有这么复杂,因此我们可以重用相同的顶点和片段着色器,而只需修改我们的C++/OpenGL应用程序,以便在调用display()时将各个模型发送给管线。

让我们继续添加一个简单的金字塔,这样我们的场景就包括一个 立方体和一个金字塔。程序4.3中显示了对代码的相关修改。我们突出显示了一些关键细节,例如当我们指定使用这个还是那个缓冲区的地方,以及我们指定模型中包含的顶点数的地方。注意,金字塔由6个三角形组成——侧面4个,底面2个,总共6×3=18个顶点。

包含立方体和金字塔的场景显示结果如图4.10所示。
vertShader.glsl

#version 430

layout (location=0) in vec3 position;

uniform mat4 mv_matrix;
uniform mat4 proj_matrix;

out vec4 varyingColor;

void main(void)
{
    
    
	gl_Position = proj_matrix * mv_matrix * vec4(position,1.0);
	
	varyingColor = vec4(position,1.0)*0.5 + vec4(0.5, 0.5, 0.5, 0.5);
} 

fragShader.glsl

#version 430

in vec4 varyingColor;
out vec4 color;

uniform mat4 mv_matrix;
uniform mat4 proj_matrix;

void main(void)
{
    
    	color = varyingColor;
}

程序4.3 立方体和金字塔

#include <GL\glew.h>
#include <GLFW\glfw3.h>
#include <string>
#include <iostream>
#include <fstream>
#include <cmath>
#include <glm\glm.hpp>
#include <glm\gtc\type_ptr.hpp> // glm::value_ptr
#include <glm\gtc\matrix_transform.hpp> // glm::translate, glm::rotate, glm::scale, glm::perspective
#include "Utils.h"
using namespace std;

#define numVAOs 1
#define numVBOs 2

float cameraX, cameraY, cameraZ;
float cubeLocX, cubeLocY, cubeLocZ;
float pyrLocX, pyrLocY, pyrLocZ;
GLuint renderingProgram;
GLuint vao[numVAOs];
GLuint vbo[numVBOs];

// variable allocation for display
GLuint mvLoc, projLoc;
int width, height;
float aspect;
glm::mat4 pMat, vMat, mMat, mvMat;

void setupVertices(void) {
    
    
	//立方体顶点坐标
	float vertexPositions[108] =
	{
    
     -1.0f,  1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f,
		1.0f, -1.0f, -1.0f, 1.0f,  1.0f, -1.0f, -1.0f,  1.0f, -1.0f,
		1.0f, -1.0f, -1.0f, 1.0f, -1.0f,  1.0f, 1.0f,  1.0f, -1.0f,
		1.0f, -1.0f,  1.0f, 1.0f,  1.0f,  1.0f, 1.0f,  1.0f, -1.0f,
		1.0f, -1.0f,  1.0f, -1.0f, -1.0f,  1.0f, 1.0f,  1.0f,  1.0f,
		-1.0f, -1.0f,  1.0f, -1.0f,  1.0f,  1.0f, 1.0f,  1.0f,  1.0f,
		-1.0f, -1.0f,  1.0f, -1.0f, -1.0f, -1.0f, -1.0f,  1.0f,  1.0f,
		-1.0f, -1.0f, -1.0f, -1.0f,  1.0f, -1.0f, -1.0f,  1.0f,  1.0f,
		-1.0f, -1.0f,  1.0f,  1.0f, -1.0f,  1.0f,  1.0f, -1.0f, -1.0f,
		1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f,  1.0f,
		-1.0f,  1.0f, -1.0f, 1.0f,  1.0f, -1.0f, 1.0f,  1.0f,  1.0f,
		1.0f,  1.0f,  1.0f, -1.0f,  1.0f,  1.0f, -1.0f,  1.0f, -1.0f
	};
	//金字塔顶点坐标
	float pyramidPositions[54] =
	{
    
     -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 0.0f,     //前面
		1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 0.0f, 1.0f, 0.0f,    //右侧
		1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 0.0f, 1.0f, 0.0f,   //后面
		-1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 0.0f,    //左侧
		-1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f,  //底面左前
		1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f   //底面右后
	};
	glGenVertexArrays(1, vao);
	glBindVertexArray(vao[0]);
	glGenBuffers(numVBOs, vbo);

	glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);//第一个VBO缓冲区vbo0用于存储正方体顶点
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertexPositions), vertexPositions, GL_STATIC_DRAW);
	glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);//第二个VBO缓冲区vbo1用于存储金字塔顶点
	glBufferData(GL_ARRAY_BUFFER, sizeof(pyramidPositions), pyramidPositions, GL_STATIC_DRAW);
}

void init(GLFWwindow* window) {
    
    
	renderingProgram = Utils::createShaderProgram("vertShader.glsl", "fragShader.glsl");

	glfwGetFramebufferSize(window, &width, &height);
	aspect = (float)width / (float)height;
	pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);

	cameraX = 0.0f; cameraY = 0.0f; cameraZ = 8.0f;//相机(人眼)位置
	cubeLocX = 0.0f; cubeLocY = -2.0f; cubeLocZ = 0.0f;//正方体位置
	pyrLocX = 2.0f; pyrLocY = 2.0f; pyrLocZ = 0.0f;//金字塔位置
	setupVertices();
}

void display(GLFWwindow* window, double currentTime) {
    
    
	glClear(GL_DEPTH_BUFFER_BIT);

	glUseProgram(renderingProgram);

	mvLoc = glGetUniformLocation(renderingProgram, "mv_matrix");
	projLoc = glGetUniformLocation(renderingProgram, "proj_matrix");
	
	//计算视图矩阵
	vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, -cameraY, -cameraZ));//二者的视角矩阵一样
	
	//计算正方体的模型矩阵
	mMat = glm::translate(glm::mat4(1.0f), glm::vec3(cubeLocX, cubeLocY, cubeLocZ));
	mvMat = vMat * mMat;
	
	//传递mv矩阵和p矩阵
	glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat));
	glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat));
	
	//缓冲区vbo[0]的顶点发送至顶点着色器
	glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
	glEnableVertexAttribArray(0);

	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LEQUAL);
	
	//绘制缓冲区vbo[0]的顶点数据(正方体)
	glDrawArrays(GL_TRIANGLES, 0, 36);

	//计算金字塔的模型矩阵
	mMat = glm::translate(glm::mat4(1.0f), glm::vec3(pyrLocX, pyrLocY, pyrLocZ));
	mvMat = vMat * mMat;
	
	//传递mv矩阵和p矩阵
	glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat));
	glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat));
	
	//缓冲区vbo[1]的顶点发送至顶点着色器
	glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
	glEnableVertexAttribArray(0);

	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LEQUAL);
	
	//绘制缓冲区vbo[0]的顶点数据(正方体)
	glDrawArrays(GL_TRIANGLES, 0, 18);
}

//使视点可以适应窗口变化 始终位于窗口中心
void window_size_callback(GLFWwindow* win, int newWidth, int newHeight) {
    
    
	aspect = (float)newWidth / (float)newHeight;
	glViewport(0, 0, newWidth, newHeight);
	pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);
}

int main(void) {
    
    
	if (!glfwInit()) {
    
     exit(EXIT_FAILURE); }
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	GLFWwindow* window = glfwCreateWindow(600, 600, "Chapter 4 - program 3", NULL, NULL);
	glfwMakeContextCurrent(window);
	if (glewInit() != GLEW_OK) {
    
     exit(EXIT_FAILURE); }
	glfwSwapInterval(1);

	glfwSetWindowSizeCallback(window, window_size_callback);

	init(window);

	while (!glfwWindowShouldClose(window)) {
    
    
		display(window, glfwGetTime());
		glfwSwapBuffers(window);
		glfwPollEvents();
	}

	glfwDestroyWindow(window);
	glfwTerminate();
	exit(EXIT_SUCCESS);
}

在这里插入图片描述

图4.10 3D立方体和金字塔

关于程序4.3的其他一些值得注意的小细节如下所示。

需要声明变量pyrLocX、pyrLocY和pyrLocZ,然后在init()中将它们初始化为所需的金字塔的位置,就像对立方体位置所做的那样。 在display()的开始构建视图矩阵vMat,然后在立方体和金字塔的模型-视图矩阵中都用到。 顶点和片段着色器代码被省略了——它们和4.5节中的一样。

4.8 矩阵堆栈

到目前为止,我们渲染的模型都是由一组顶点构成的。然而,实际上我们通常希望通过组装较小的简单模型来构建复杂的模型。例如,可以通过分别绘制头部、身体、腿部和手臂来创建“机器人”的模型,这当中每个部件都是一个单独的模型。以这种方式构建的对象通常称为分层模型。构建分层模型的棘手部分是跟踪所有模型-视图矩阵并确保它们完美协调——否则机器人可能会散成几块!

分层模型不仅可用于构建复杂对象——它们还可以用来生成复杂场景。例如,考虑一下我们的行星地球围绕太阳旋转的方式,以及月球围绕地球旋转的方式。这样的一个场景如图4.11所示。计算月球在太空中的实际路径可能很复杂。然而,如果我们能够组合代表两条 简单圆形路径的变换——月球围绕地球旋转的路径和地球围绕太阳旋转的路径——我们就能避免直接计算月球的轨迹。
在这里插入图片描述

图4.11 行星系统动画(太阳和地球的纹理来自[HT16],月球纹理来自[NA16])

事实证明,我们可以使用矩阵堆栈轻松地完成此操作。顾名思义,矩阵堆栈是一堆变换矩阵。正如我们将看到的,矩阵堆栈使得创建和管理复杂的分层对象和场景变得容易,它使得变换可以构建在其他变换之上(或者从其他变换中被移除)。

OpenGL有一个内置的矩阵堆栈,但作为旧的固定功能(非可编程)管线的一部分,它早已被弃用[OL16]。但是,C++标准模板库(STL)有一个名为“stack”的类,通过使用它构建mat4的堆栈,它可以相对简单直接地当作矩阵堆栈使用。正如我们将看到的,复杂场景中通常需要的许多模型、视图和模型-视图矩阵可以由单个 stack<glm::mat4>实例替换。

我们将首先检查实例化和使用C++堆栈的基本命令,然后使用一个堆栈来构建复杂的动画场景。我们将通过以下方法使用C++堆栈类。

在这里插入图片描述

如前面列表中所示,“*=”运算符在mat4中被重载,因此它可以用于连接矩阵。因此,我们通常将它用于向矩阵堆栈顶部的矩阵添加 平移、旋转等,正如我们展示出来的这些形式。

现在,我们不再通过创建mat4的实例来构建变换,而是使用 push()命令在堆栈顶部创建新的矩阵。然后再根据需要将期望的变换应用于堆栈顶部的新创建的矩阵。

推入堆栈的第一个矩阵通常是视图矩阵。它上面的矩阵是复杂程度越来越高的模型-视图矩阵;也就是说,它们应用了越来越多的模型变换。这些变换既可以直接应用,也可以先结合其他矩阵。

在我们的行星系统示例中,位于视图矩阵正上方的矩阵将是太阳的MV矩阵。在它之上的矩阵将是地球的MV矩阵,由太阳的MV矩阵的副本和应用于其之上的地球模型矩阵变换组成。也就是说,地球的MV矩阵是通过将行星的变换结合到太阳的变换中而建立的。同样,月球的 MV矩阵位于地球的MV矩阵之上,并通过将月球的模型矩阵变换应用于紧邻其下方的地球的MV矩阵来构建。

在渲染月球之后,可以通过从堆栈中“弹出”第一个月球的矩阵(将堆栈的顶部恢复到行星的模型-视图矩阵),然后重复第二个月球的过程,来渲染第二个“月球”。

基本方法如下。
(1)我们声明我们的堆栈,给它起名为“mvStack”。
(2)当相对于父对象创建新对象时,调用 “mvStack.push(mvStack.top())”。
(3)应用新对象所需的变换,也就是将所需的变换乘以它。
(4)完成对象或子对象的绘制后,调用“mvStack.pop()”从矩 阵堆栈顶部移除其模型-视图矩阵。

在后面的章节中,我们将学习如何创建球体并使它们看起来像行星和卫星。就目前而言,为了简单起见,我们将使用我们的金字塔和几个立方体构建一个“行星系统”。

表4.1概述了使用矩阵堆栈的display()函数通常是什么结构。
在这里插入图片描述

表4 .1 使用矩阵堆栈的display()函数的结构

请注意,金字塔(“太阳”)绕自己的轴旋转是在它自己的局部坐标空间中,不影响“子对象”(这里指行星和月亮)。因此,“太阳”的旋转(如图4.12所示)被推到堆栈上,但是在绘制“太阳”之后,它必须从堆栈中移除(弹出)。
在这里插入图片描述

图4.12 金字塔(“太阳”)的旋转

大立方体(行星)围绕太阳的旋转(如图4.13(左)所示)将影响月球的运动,因此它被推到堆栈上并在绘制月球时保持在那里。相比之下,行星在其轴上的旋转(如图4.13(右)所示)是局部的,不会影响月亮,因此在绘制月球之前它需要被从堆栈中弹出。
在这里插入图片描述
在这里插入图片描述

图4.13 大立方体(行星)围绕太阳的旋转(上)和行星在其轴上的旋转(下)

类似地,我们会将变换推入堆栈以进行月球的旋转(围绕行星及其轴),如图4.14所示。
在这里插入图片描述
在这里插入图片描述

图4.14 月球的旋转(围绕行星及其轴)

以下是“行星”的步骤顺序。

  • push()将是行星MV矩阵中也会影响子对象的部分。
  • translate(…)将太阳周围的行星运动结合到行星的MV矩阵中。
    在这个例子中,我们使用三角运算来计算行星运动的平移。
  • push()将是行星的完整MV矩阵,也包括它的轴旋转。 rotate(…)结合行星的轴旋转(稍后会弹出,不会影响子对象)。
    glm::value_ptr(mvStack.top())获取MV矩阵,然后将其发送到MV 统一变量。
  • 绘制星球。
  • pop()将行星MV矩阵从堆栈中移除,暴露出它下面行星MV矩阵的不
    包括行星轴旋转的早期副本(因此只有行星的平移会影响月亮)。

现在我们可以编写完整的display()函数,如程序4.4所示。
vertShader.glsl

#version 430

layout (location=0) in vec3 position;

uniform mat4 mv_matrix;
uniform mat4 proj_matrix;

out vec4 varyingColor;

void main(void)
{
    
    	gl_Position = proj_matrix * mv_matrix * vec4(position,1.0);
	varyingColor = vec4(position,1.0)*0.5 + vec4(0.5, 0.5, 0.5, 0.5);
} 

fragShader.glsl

#version 430

in vec4 varyingColor;
out vec4 color;

uniform mat4 mv_matrix;
uniform mat4 proj_matrix;

void main(void)
{
    
    	color = varyingColor;
}

程序4.4 使用矩阵堆栈的简单太阳系

#include <GL\glew.h>
#include <GLFW\glfw3.h>
#include <string>
#include <iostream>
#include <fstream>
#include <cmath>
#include <stack>
#include <glm\glm.hpp>
#include <glm\gtc\type_ptr.hpp> // glm::value_ptr
#include <glm\gtc\matrix_transform.hpp> // glm::translate, glm::rotate, glm::scale, glm::perspective
#include "Utils.h"
using namespace std;

#define numVAOs 1
#define numVBOs 2

float cameraX, cameraY, cameraZ;
GLuint renderingProgram;
GLuint vao[numVAOs];
GLuint vbo[numVBOs];

// variable allocation for display
GLuint mvLoc, projLoc;
int width, height;
float aspect;
glm::mat4 pMat, vMat, mMat, mvMat;

stack<glm::mat4> mvStack;

void setupVertices(void) {
    
    
	float vertexPositions[108] =
	{
    
     -1.0f,  1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f,
		1.0f, -1.0f, -1.0f, 1.0f,  1.0f, -1.0f, -1.0f,  1.0f, -1.0f,
		1.0f, -1.0f, -1.0f, 1.0f, -1.0f,  1.0f, 1.0f,  1.0f, -1.0f,
		1.0f, -1.0f,  1.0f, 1.0f,  1.0f,  1.0f, 1.0f,  1.0f, -1.0f,
		1.0f, -1.0f,  1.0f, -1.0f, -1.0f,  1.0f, 1.0f,  1.0f,  1.0f,
		-1.0f, -1.0f,  1.0f, -1.0f,  1.0f,  1.0f, 1.0f,  1.0f,  1.0f,
		-1.0f, -1.0f,  1.0f, -1.0f, -1.0f, -1.0f, -1.0f,  1.0f,  1.0f,
		-1.0f, -1.0f, -1.0f, -1.0f,  1.0f, -1.0f, -1.0f,  1.0f,  1.0f,
		-1.0f, -1.0f,  1.0f,  1.0f, -1.0f,  1.0f,  1.0f, -1.0f, -1.0f,
		1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f,  1.0f,
		-1.0f,  1.0f, -1.0f, 1.0f,  1.0f, -1.0f, 1.0f,  1.0f,  1.0f,
		1.0f,  1.0f,  1.0f, -1.0f,  1.0f,  1.0f, -1.0f,  1.0f, -1.0f
	};
	float pyramidPositions[54] =
	{
    
     -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 0.0f,    //front
		1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 0.0f, 1.0f, 0.0f,    //right
		1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 0.0f, 1.0f, 0.0f,  //back
		-1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 0.0f,  //left
		-1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, //LF
		1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f  //RR
	};
	glGenVertexArrays(1, vao);
	glBindVertexArray(vao[0]);
	glGenBuffers(numVBOs, vbo);

	glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertexPositions), vertexPositions, GL_STATIC_DRAW);
	glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
	glBufferData(GL_ARRAY_BUFFER, sizeof(pyramidPositions), pyramidPositions, GL_STATIC_DRAW);
}

void init(GLFWwindow* window) {
    
    
	renderingProgram = Utils::createShaderProgram("vertShader.glsl", "fragShader.glsl");

	glfwGetFramebufferSize(window, &width, &height);
	aspect = (float)width / (float)height;
	pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);

	cameraX = 0.0f; cameraY = 0.0f; cameraZ = 12.0f;
	setupVertices();
}

void display(GLFWwindow* window, double currentTime) {
    
    
	glClear(GL_DEPTH_BUFFER_BIT);
	glClearColor(0.0, 0.0, 0.0, 1.0);
	glClear(GL_COLOR_BUFFER_BIT);

	glUseProgram(renderingProgram);

	mvLoc = glGetUniformLocation(renderingProgram, "mv_matrix");
	projLoc = glGetUniformLocation(renderingProgram, "proj_matrix");

	// 将视图矩阵推入堆栈
	vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, -cameraY, -cameraZ));
	mvStack.push(vMat);

	glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat));

	// ---------------------- 金字塔  == 太阳  ------------------------//
	mvStack.push(mvStack.top());
	mvStack.top() *= glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f, 0.0f));// 太阳位置
	mvStack.push(mvStack.top());
	mvStack.top() *= rotate(glm::mat4(1.0f), (float)currentTime, glm::vec3(1.0, 0.0, 0.0));// 太阳旋转
	glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvStack.top()));
	glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
	glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);
	glEnableVertexAttribArray(0);
	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LEQUAL);
	glDrawArrays(GL_TRIANGLES, 0, 18);// 绘制太阳
	mvStack.pop();// 从堆栈中移除太阳的轴旋转

	//----------------------- 立方体  == 行星  ------------------------//
	mvStack.push(mvStack.top());
	mvStack.top() *= glm::translate(glm::mat4(1.0f), glm::vec3(sin((float)currentTime)*4.0, 0.0f, cos((float)currentTime)*4.0));
	mvStack.push(mvStack.top());
	mvStack.top() *= rotate(glm::mat4(1.0f), (float)currentTime, glm::vec3(0.0, 1.0, 0.0));// 行星旋转
	glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvStack.top()));
	glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
	glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);
	glEnableVertexAttribArray(0);
	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LEQUAL);
	glDrawArrays(GL_TRIANGLES, 0, 36);// 绘制行星
	mvStack.pop();// 从堆栈中移除行星的轴旋转

	//----------------------- 小立方体  == 月球  ----------------------//
	mvStack.push(mvStack.top());
	mvStack.top() *= glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, sin((float)currentTime)*2.0, cos((float)currentTime)*2.0));
	mvStack.top() *= rotate(glm::mat4(1.0f), (float)currentTime, glm::vec3(0.0, 0.0, 1.0));//月球旋转
	mvStack.top() *= scale(glm::mat4(1.0f), glm::vec3(0.25f, 0.25f, 0.25f)); // 让月球小一些
	glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvStack.top()));
	glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
	glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);
	glEnableVertexAttribArray(0);
	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LEQUAL);
	glDrawArrays(GL_TRIANGLES, 0, 36);// 绘制月球
	mvStack.pop(); mvStack.pop(); mvStack.pop();
	mvStack.pop();  // 从堆栈中移除月球缩放、旋转、位置矩阵,行星位置矩阵,太阳位置矩阵和视图矩阵
}

void window_size_callback(GLFWwindow* win, int newWidth, int newHeight) {
    
    
	aspect = (float)newWidth / (float)newHeight;
	glViewport(0, 0, newWidth, newHeight);
	pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);
}

int main(void) {
    
    
	if (!glfwInit()) {
    
     exit(EXIT_FAILURE); }
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	GLFWwindow* window = glfwCreateWindow(600, 600, "Chapter 4 - program 4", NULL, NULL);
	glfwMakeContextCurrent(window);
	if (glewInit() != GLEW_OK) {
    
     exit(EXIT_FAILURE); }
	glfwSwapInterval(1);

	glfwSetWindowSizeCallback(window, window_size_callback);

	init(window);

	while (!glfwWindowShouldClose(window)) {
    
    
		display(window, glfwGetTime());
		glfwSwapBuffers(window);
		glfwPollEvents();
	}

	glfwDestroyWindow(window);
	glfwTerminate();
	exit(EXIT_SUCCESS);
}

矩阵堆栈操作已突出显示。有几个值得注意的细节:

我们在模型矩阵中引入了缩放操作。我们希望月球比行星更小, 所以我们在为月球构建MV矩阵时调用了scale()。 在这个例子中,我们使用三角运算sin()和cos()来计算行星绕太阳的旋转(作为平移的方式),以及月球绕行星的旋转。

两个缓冲区#0和#1分别包含立方体和金字塔的顶点。 注意在glUniformMatrix()命令中调用的 glm::value_ptr(mvMatrix.top())函数。这个调用获取了堆栈顶部矩阵中的值,然后将这些值发送到统一变量(在本例中为太阳、行星以及月球的MV矩阵)。
此处省略顶点和片段着色器代码——它们与前一个示例相同。我们还移动了金字塔(“太阳”)和摄像机的初始位置,以使场景在屏幕上居中。

4.9 应对“Z冲突”伪影(Z buffer问题)

回想一下,在渲染多个对象时,OpenGL使用Z缓冲区算法(Z- buffer algorithm)(如图 2.14所示)来进行隐藏面消除。通常情况下,通过选择最接近相机的相应片段的颜色作为像素的颜色,这种方法解决了哪些物体的曲面可见并呈现到屏幕,而哪些曲面位于其他物 体后面因此不应该被渲染。

然而,有时候场景中的两个物体表面重叠并位于重合的平面中,这使得Z缓冲区算法难以确定应该渲染两个表面中的哪一个(因为两者 都不“最接近”摄像机)。发生这种情况时,浮点舍入误差可能会导致渲染表面的某些部分使用其中一个对象的颜色,而其他部分则使用 另一个对象的颜色。这种不自然的伪影被称为Z冲突(Z-fighting)或 深度冲突(Depth-fighting),因为这种效果是渲染的片段在Z缓冲区 中相互对应的像素条目上“冲突斗争”的结果。图4.15(见彩插)显 示了两个具有重叠重合(顶)面的盒子之间的Z冲突示例。
在这里插入图片描述

图4.15 Z冲突示例

创建地形或阴影时经常会出现这种情况。在这种情况下,有时Z冲突是可以预知的,并且校正它的常用方法是稍微移动一个物体,使得表面不再是共面的。我们将在第8章中看到这样的例子。

Z冲突还可能是由于深度缓冲器中的值的精度有限。对于由Z缓冲器算法处理的每个像素,其深度信息的精度受深度缓冲器中可存储的位数限制。用于构建透视矩阵的近剪裁平面和远剪裁平面之间的范围越大,具有相似(但不相等)的实际深度的两个对象的点在深度缓冲区中的数值表示越可能相同。因此,程序员可以选择适当的近、远剪裁平面值来最小化两个平面之间的距离,同时仍然确保场景必需的所有对象都位于视锥内。

同样重要的是要理解,由于透视变换的影响,改变近剪裁平面值可能比对远剪裁平面进行等效变化对于Z冲突伪影具有更大的影响。因此,建议避免选择太靠近眼睛的近剪裁平面。

本书前面的例子只是简单地使用了0.1和1000的值(在我们对perspective()的调用中)用于近剪裁平面和远剪裁平面。这些可能需要针对您的场景进行调整。

4.10 图元的其他选项

OpenGL支持许多图元类型——到目前为止我们已经看到了两个: GL_TRIANGLES和GL_POINTS。事实上,还有好几个其他的选择。OpenGL支持的所有可用图元类型都属于三角形、线、点或者补丁的类别。以下是一个完整的清单。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

4.11 性能优先的编程方法

随着3D场景的复杂性增加,我们将越来越关注性能。我们已经看到一些例子,我们为了速度做出一些编程决策——例如当我们使用实例化时,以及当我们将昂贵的计算转移到着色器时。

实际上,我们展示的代码已经包含了一些我们尚未讨论的其他优化。我们现在来探索这些和其他重要技术。

4.11.1 尽量减少动态内存空间分配

考虑到性能,我们的C++代码的最关键部分显然是display()函 数。这是在任何动画或实时渲染过程中重复调用的函数,因此在此函数中(或在它调用的任何函数中)我们必须努力实现最高的效率。

将display()函数的开销保持在最低限度的一个重要方法是避免任何需要内存分配的步骤。因此,明显要避免的事情的例子包括:

  • 实例化对象;
  • 声明变量。

如果读者回顾我们迄今为止开发的程序,可以观察到,我们实际上在调用display()函数之前就已经声明display()函数中使用到的每个变量,并分配了它的空间。声明或实例化几乎从不出现在 display()函数中。例如,程序4.1在它开头包含以下代码块:
在这里插入图片描述

请注意,我们故意在代码块的顶部放了一个注释,说明这些变量是预先分配的,以便稍后在display()函数中使用(尽管我们到现在才明确地指出这一点)。

在我们的矩阵堆栈示例中发生了一个未预先分配的变量的情况。使用C++堆栈类,每次“推”操作都会导致动态内存分配。有趣的是, 在Java中,JOML库提供了一个与OpenGL一起使用的MatrixStack类,它 允许为矩阵堆栈预先分配空间!我们在本书的Java版中使用它。

还有其他更微妙的例子。例如,将数据从一种类型转换为另一种 类型的函数调用在某些情况下可能会实例化并返回新转换的数据。因 此,理解从display()调用的任何库函数的行为非常重要。数学库GLM 并没有专门针对速度优化设计。这导致一些操作可能引起动态内存分 配。如果可能的话,我们会尽量使用直接在已经分配了空间的变量上 操作的GLM函数。我们鼓励读者在性能至关重要时探索替代方法。

4.11.2 预先计算透视矩阵

可以减少display()函数开销的另一个优化是将透视矩阵的计算移 动到init()函数中。我们在4.5节中提到了这种可能性(在脚注中)。 虽然这很容易做到,但可能会有一点轻微的复杂情况。虽然通常并不 需要重新计算透视矩阵,但是如果运行应用程序的用户调整窗口大小(例如通过拖动窗口角大小调整手柄),则重新计算就是必要的。

幸运的是,GLFW可以配置在调整窗口大小时自动回调指定的函 数。在调用init()之前,我们将以下内容添加到main():
在这里插入图片描述

第一个参数是GLFW窗口,第二个参数是GLFW在调整窗口大小时调用的函数的名称。然后,我们将计算透视矩阵的代码移动到init() 中,同时将其复制到名为window_reshape_callback()的新函数中。

在程序4.1的例子中,如果我们重新组织代码,从display()中删 除透视矩阵的计算,那么main()、init()、display()和新函数 window_reshape_callback()修改后的版本将如下所示。
在这里插入图片描述
在这里插入图片描述

本书配套资源中的程序,与透视矩阵计算有关的实现都是以这种方式组织的,从程序4.1的颜色插值版本开始。

4.11.3 背面剔除

提高渲染效率的另一种方法是利用OpenGL的背面剔除能力。当3D 模型完全“闭合”时,意味着内部永远不可见(例如对于立方体和金 字塔),那么外表面的那些与观察者背离呈一定角度的部分将始终被 同一模型的其他部分遮挡。也就是说,那些背离观察者的三角形不可 能被看到(无论如何它们都会在隐藏面消除的过程中被覆盖),因此 没有理由光栅化或渲染它们。
我们可以使用命令glEnable(GL_CULL_FACE)要求OpenGL识别并 “剔除”(不渲染)背向的三角形。我们还可以使用glDisable(GL_CULL_FACE)禁用背面剔除。默认情况下,背面剔除是关闭的,因此如果您希望OpenGL剔除背向三角形,必须手动启用它。

启用背面剔除时,默认情况下,只有三角形朝前时才会被渲染。 此外,默认情况下,如果三角形的3个顶点从OpenGL摄像机中查看是以 逆时针顺序排列的(基于它们在缓冲区中定义的顺序),则三角形被 视为面向前方。顶点沿顺时针方向排列的三角形(从OpenGL摄像机中 看)是朝后的,不会被渲染。这种逆时针方向定义的“前向”有时被 称为缠绕顺序,可以使用函数调用glFrontFace(GL_CCW)显式设置逆时 针(默认)为正向,或glFrontFace (GL_CW)设置顺时针为正向。

类似 地,也可以显式设置是否渲染正向或背向的三角形。实际上,为了这 个目的,我们指定哪些不被渲染——即哪些被“剔除”。我们可以通 过调用glCullFace(GL_BACK)指定面向背面的三角形被剔除(尽管这是 不必要的,因为它是默认的)。或者,我们可以通过分别用GL_FRONTGL_FRONT_AND_BACK替换参数GL_BACK来指定剔除前向三角形,甚至 剔除所有三角形。

正如我们将在第6章中看到的那样,3D模型通常被设计成外表面由 相同缠绕顺序的三角形构成——最常见的是逆时针——因此如果启用 剔除,则默认情况下模型的外部面向相机的表面部分会被渲染。因为 默认情况下OpenGL假定的缠绕顺序是逆时针方向,如果模型设计缠绕 顺序为顺时针方向,那么如果启用了背面剔除,需要由程序员调用 gl_FrontFace (GL_CW)来解决此问题。

注意,在GL_TRIANGLE_STRIP的情况下,每个三角形的缠绕顺序不停地互换。OpenGL通过在构建每个连续三角形时“翻转”顶点序列来 补偿这一点,如下所示:0-1-2,然后2-1-3、2-3-4、4-3-5、4-5-6 等。

背面剔除通过确保OpenGL不花时间光栅化和渲染从不被看到的表 面来提高性能。我们在本章中看到的大多数示例都非常小,以至于没 有动机进行背面剔除(图4.9中展示了一个例外,其中包含了100 000 个多边形动画实例,这可能会对某些系统造成性能挑战)。在实践 中,大多数3D模型通常是“闭合的”,因此习惯上会常规地启用背面 剔除。例如,我们可以通过修改display()函数向程序4.3添加背面剔除,如下所示。
在这里插入图片描述
使用背面剔除时,正确设置缠绕顺序非常重要。不正确的设置, 例如当应该设置GL_CCW时设置成了GL_CW,可能会导致渲染出对象的内部而不是其外部,这就会产生类似于不正确的透视矩阵的失真。

效率不是进行背面剔除的唯一原因。在后面的章节中,我们将看 到其他用途,例如我们想要查看3D模型内部或使用透明度时的情况。

猜你喜欢

转载自blog.csdn.net/weixin_44848751/article/details/130837241