本教程将介绍如何在OpenGL 4.0中编写顶点和像素着色器。这还将有在OpenGL 4.0中使用顶点和索引缓冲区的介绍。这些是您需要理解和利用以渲染3D图形的最基本概念。
顶点缓冲区
要理解的第一个概念是顶点缓冲区。为了说明这个概念,让我们以球的3D模型为例:
3D球形模型实际上由数百个三角形组成:
球体模型中的每个三角形都有三个点;我们称每个点为一个顶点。因此,要渲染球体模型,我们需要将形成球体的所有顶点放入一个特殊的数据数组中,该数据数组称为顶点缓冲区。一旦球体模型的所有点都在顶点缓冲区中,我们就可以将顶点缓冲区发送到GPU,以便它可以渲染模型。
索引缓冲区
索引缓冲区与顶点缓冲区有关。它们的目的是记录顶点缓冲区中每个顶点的位置。然后,GPU使用索引缓冲区快速找到顶点缓冲区中的特定顶点。索引缓冲区的概念类似于在书中使用索引的概念。它可以更快地找到您要查找的主题。同样,使用索引缓冲区还可以增加将顶点数据缓存在视频内存中更快位置的可能性。因此,出于性能方面的考虑,强烈建议使用它们。
顶点着色器
顶点着色器是一些小型程序,主要用于将顶点从顶点缓冲区转换为3D空间。还可以执行其他计算,例如为每个顶点计算法线。GPU将为需要处理的每个顶点调用顶点着色器程序。例如,一个5,000个三角形组成的模型将每帧运行您的顶点着色器程序15,000次,以绘制单个模型。因此,如果将图形程序锁定为60 fps,它将每秒调用顶点着色器900,000次,仅绘制5,000个三角形。如您所知,编写高效的顶点着色器很重要。
像素着色器
像素着色器是编写的小程序,用于对我们绘制的多边形进行着色。它们由GPU针对将绘制到屏幕上的每个可见像素运行。您计划对多边形面进行的着色,纹理化,照明和大多数其他效果均由像素着色器程序处理。由于必须调用像素着色器,因此必须有效地写入像素着色器。
GLSL
GLSL是我们在OpenGL 4.0中使用的用于对这些小的顶点和像素着色器程序进行编码的语言。语法与带有一些预定义类型的C语言几乎完全相同。因为这是第一本GLSL教程,所以我们将使用OpenGL 4.0做一个非常简单的GLSL程序来开始使用。
更新的框架
教程的框架已更新。在GraphicsClass下,我们添加了三个新类,分别称为CameraClass,ModelClass和ColorShaderClass。CameraClass将处理我们之前讨论的视图矩阵。它会处理相机在世界上的位置,并在需要绘制并弄清楚我们从何处看场景时将其传递给着色器。ModelClass将处理我们3D模型的几何形状,在本教程中,出于简化原因,3D模型将只是一个三角形。最后,ColorShaderClass将负责调用我们的GLSL着色器将模型渲染到屏幕上。
我们将首先查看GLSL着色器程序来开始教程代码。
color.vs
这些将是我们的第一个着色器程序。着色器是用于实际渲染模型的小程序。这些着色器以GLSL编写,并存储在名为color.vs和color.ps的源文件中。我现在将带有.cpp和.h文件的文件放置在引擎中。此着色器的目的只是为了绘制彩色三角形,因为我在第一个GLSL教程中将事情简化了。以下是顶点着色器的代码:
// Filename: color.vs
#version 400
/
// INPUT VARIABLES //
/
in vec3 inputPosition;
in vec3 inputColor;
//
// OUTPUT VARIABLES //
//
out vec3 color;
///
// UNIFORM VARIABLES //
///
uniform mat4 worldMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;
// Vertex Shader
void main(void)
{
// Calculate the position of the vertex against the world, view, and projection matrices.
gl_Position = worldMatrix * vec4(inputPosition, 1.0f);
gl_Position = viewMatrix * gl_Position;
gl_Position = projectionMatrix * gl_Position;
// Store the input color for the pixel shader to use.
color = inputColor;
}
这些将是我们的第一个着色器程序。着色器是用于实际渲染模型的小程序。这些着色器以GLSL编写,并存储在名为color.vs和color.ps的源文件中。我现在将带有.cpp和.h文件的文件放置在引擎中。此着色器的目的只是为了绘制彩色三角形,因为我在第一个GLSL教程中将事情简化了。以下是顶点着色器的代码:
/
//文件名:color.vs
// / /
顶点着色器首先定义我们正在使用的GLSL版本。我们正在使用的版本是4.0。版本越高,可以在着色器代码中解锁和使用的功能越多。
#version 400
顶点着色器的下一部分是输入顶点格式。在本教程中,每个顶点将由x,y和z位置以及r,g和b颜色组成。这些是浮点值,并且两个输入值均具有三个浮点数。您会注意到我们使用了一种称为vec3的特殊类型,而不是浮点数数组。GLSL具有有用的类型,例如vec3,vec4,mat3,mat4,以使编程着色器更容易阅读。
/
//
vec3中的
/// inputPosition;
在vec3 inputColor;
顶点着色器的下一部分是将发送到像素着色器中的输出变量。定义的唯一输出变量是颜色,因为我们将把颜色发送到像素着色器中。请注意,转换后的输入顶点位置也将发送到像素着色器中,但会在称为gl_Position的特殊预定义变量中发送。
//
//输出变量//
//
输出vec3颜色;
顶点着色器的下一部分是统一变量。这些是我们一次设置的变量,不会为每个顶点更改。在本教程中,我们将仅设置三个重要的矩阵,即世界,视图和投影。
///
//统一变量//
/ //
统一的mat4 worldMatrix;
统一的mat4 viewMatrix;
均匀的mat4 projectionMatrix;
顶点着色器的最后一部分是代码主体。该代码通过将输入顶点乘以世界,视图和投影矩阵开始。这会将顶点放置在正确的位置,以便根据我们的视图在3D空间中进行渲染,然后放到2D屏幕上。我们将结果存储在特殊的gl_Position向量中,该向量将自动传递到像素着色器中。还要注意,我确实将输入位置的W值设置为1.0,因此我们可以使用4x4矩阵进行适当的计算。之后,我们将该顶点的输出颜色设置为输入颜色的副本。这将允许像素着色器访问颜色。
/ /
//顶点着色器
/ / /
void main(void)
{
//根据世界,视图和投影矩阵计算顶点的位置。
gl_Position = worldMatrix * vec4(inputPosition,1.0f);
gl_Position = viewMatrix * gl_Position;
gl_Position = projectionMatrix * gl_Position;
//存储供像素着色器使用的输入颜色。
color = inputColor;
}
color.ps
// Filename: color.ps
#version 400
/
// INPUT VARIABLES //
/
in vec3 color;
//
// OUTPUT VARIABLES //
//
out vec4 outputColor;
// Pixel Shader
void main(void)
{
outputColor = vec4(color, 1.0f);
}
像素着色器在将要渲染到屏幕的多边形上绘制每个像素。这个像素着色器程序非常简单,因为我们只是告诉它使用与颜色输入值相同的颜色对像素进行着色。像素着色器接收颜色作为输入向量,并设置代表最终像素颜色的outputColor输出变量。我们需要将输入颜色从vec3转换为vec4,以便它具有与我们的像素格式匹配的alpha分量。还要记住,像素着色器是从顶点着色器输出获取输入的,因此正确命名变量很重要。
colorshaderclass.h
ColorShaderClass是我们将用于编译和执行颜色GLSL着色器的类,以便我们可以渲染GPU上的3D模型。
// Filename: colorshaderclass.h
#ifndef _COLORSHADERCLASS_H_
#define _COLORSHADERCLASS_H_
//
// INCLUDES //
//
#include <fstream>
using namespace std;
///
// MY CLASS INCLUDES //
///
#include "openglclass.h"
// Class name: ColorShaderClass
class ColorShaderClass
{
public:
ColorShaderClass();
ColorShaderClass(const ColorShaderClass&);
~ColorShaderClass();
//此处的功能处理着色器的初始化和关闭。
//SetShaderParameters函数设置着色器统一变量,
//SetShader函数将着色器代码设置为当前渲染系统。
bool Initialize(OpenGLClass*, HWND);
void Shutdown(OpenGLClass*);
void SetShader(OpenGLClass*);
bool SetShaderParameters(OpenGLClass*, float*, float*, float*);
private:
bool InitializeShader(char*, char*, OpenGLClass*, HWND);
char* LoadShaderSourceFile(char*);
void OutputShaderErrorMessage(OpenGLClass*, HWND, unsigned int, char*);
void OutputLinkerErrorMessage(OpenGLClass*, HWND, unsigned int);
void ShutdownShader(OpenGLClass*);
private:
unsigned int m_vertexShader;
unsigned int m_fragmentShader;
unsigned int m_shaderProgram;
};
#endif
colorshaderclass.cpp
// Filename: colorshaderclass.cpp
#include "colorshaderclass.h"
ColorShaderClass::ColorShaderClass()
{
}
ColorShaderClass::ColorShaderClass(const ColorShaderClass& other)
{
}
ColorShaderClass::~ColorShaderClass()
{
}
//Initialize函数将调用着色器的初始化函数。
//我们传入GLSL着色器文件的名称,在本教程中,它们分别命名为color.vs和color.ps。
bool ColorShaderClass::Initialize(OpenGLClass * OpenGL, HWND hwnd)
{
bool result;
// 初始化顶点和像素着色器.
result = InitializeShader("../Engine/color.vs", "../Engine/color.ps", OpenGL, hwnd);
if (!result)
{
return false;
}
return true;
}
//Shutdown函数将调用着色器的关闭
void ColorShaderClass::Shutdown(OpenGLClass * OpenGL)
{
// 关闭顶点和像素着色器以及相关对象
ShutdownShader(OpenGL);
return;
}
//SetShader会将颜色GLSL顶点和像素着色器设置为用于绘制所有3D几何体的当前渲染程序。
void ColorShaderClass::SetShader(OpenGLClass * OpenGL)
{
// 将着色器程序安装为当前渲染状态的一部分。
OpenGL->glUseProgram(m_shaderProgram);
return;
}
//现在,我们将从本教程中更重要的功能之一开始,即InitializeShader。
//该功能实际上是加载着色器文件并使它们可用于OpenGL和GPU的功能。
//您还将看到在GPU中的图形管线上如何查看顶点缓冲区数据的设置。
//输入变量将需要与modelclass.h文件中的VertexType(稍后将介绍)以及color.vs文件中定义的输入变量相匹配。
bool ColorShaderClass::InitializeShader(char* vsFilename, char* fsFilename, OpenGLClass * OpenGL, HWND hwnd)
{
const char* vertexShaderBuffer;
const char* fragmentShaderBuffer;
int status;
//此函数的第一部分是我们加载顶点和像素着色器源文件并进行编译的地方。
// 将顶点着色器源文件加载到文本缓冲区中。
vertexShaderBuffer = LoadShaderSourceFile(vsFilename);
if (!vertexShaderBuffer)
{
return false;
}
// 将片段着色器源文件加载到文本缓冲区中。
fragmentShaderBuffer = LoadShaderSourceFile(fsFilename);
if (!fragmentShaderBuffer)
{
return false;
}
// 创建一个顶点和片段着色器对象
m_vertexShader = OpenGL->glCreateShader(GL_VERTEX_SHADER);
m_fragmentShader = OpenGL->glCreateShader(GL_FRAGMENT_SHADER);
// 将着色器源代码字符串复制到顶点和片段着色器对象中
OpenGL->glShaderSource(m_vertexShader, 1, &vertexShaderBuffer, NULL);
OpenGL->glShaderSource(m_fragmentShader, 1, &fragmentShaderBuffer, NULL);
//释放顶点和片段着色器缓冲区。
delete[] vertexShaderBuffer;
vertexShaderBuffer = 0;
delete[] fragmentShaderBuffer;
fragmentShaderBuffer = 0;
//编译着色器.
OpenGL->glCompileShader(m_vertexShader);
OpenGL->glCompileShader(m_fragmentShader);
//检查顶点着色器是否编译成功
OpenGL->glGetShaderiv(m_vertexShader, GL_COMPILE_STATUS, &status);
if (status != 1)
{
// 如果未编译,则将语法错误消息写出到文本文件中以供检查。
OutputShaderErrorMessage(OpenGL, hwnd, m_vertexShader, vsFilename);
return false;
}
//检查片段着色器是否成功编译
OpenGL->glGetShaderiv(m_fragmentShader, GL_COMPILE_STATUS, &status);
if (status != 1)
{
// 如果未编译,则将语法错误消息写出到文本文件中以供检查。
OutputShaderErrorMessage(OpenGL, hwnd, m_fragmentShader, fsFilename);
return false;
}
//GLSL程序成功编译后,我们可以创建一个着色器程序对象,然后将顶点和像素着色器附加到该对象。
//我们还绑定输入变量,然后最终链接程序。
// 创建一个着色器程序对象.
m_shaderProgram = OpenGL->glCreateProgram();
//将顶点和片段着色器附加到程序对象。
OpenGL->glAttachShader(m_shaderProgram, m_vertexShader);
OpenGL->glAttachShader(m_shaderProgram, m_fragmentShader);
//绑定着色器输入变量。
OpenGL->glBindAttribLocation(m_shaderProgram, 0, "inputPosition");
OpenGL->glBindAttribLocation(m_shaderProgram, 1, "inputColor");
//链接着色器程序。
OpenGL->glLinkProgram(m_shaderProgram);
//检查链接的状态。
OpenGL->glGetProgramiv(m_shaderProgram, GL_LINK_STATUS, &status);
if (status != 1)
{
// 如果未链接,则将语法错误消息写出到文本文件中以供检查。
OutputLinkerErrorMessage(OpenGL, hwnd, m_shaderProgram);
return false;
}
return true;
}
//LoadShaderSourceFile函数将着色器代码加载到可以编译的缓冲区中。
char* ColorShaderClass::LoadShaderSourceFile(char* filename)
{
ifstream fin;
int fileSize;
char input;
char* buffer;
// 打开着色器源文件。
fin.open(filename);
// 如果无法打开文件,则退出。
if (fin.fail())
{
return 0;
}
// 初始化文件的大小。
fileSize = 0;
// 读取文件的第一个元素。.
fin.get(input);
// 计算文本文件中的元素数。
while (!fin.eof())
{
fileSize++;
fin.get(input);
}
//立即关闭文件。
fin.close();
// 初始化缓冲区以将着色器源文件读入其中
buffer = new char[fileSize + 1];
if (!buffer)
{
return 0;
}
// 再次打开着色器源文件
fin.open(filename);
// 将着色器文本文件作为一个块读入缓冲区.
fin.read(buffer, fileSize);
// 关闭文件
fin.close();
// Null终止缓冲区。
buffer[fileSize] = '\0';
return buffer;
}
//万一GLSL着色器无法编译,OutputShaderErrorMessage函数会将错误写到文本文件中。
//文本文件将包含调试和更正着色器代码所需的信息。
void ColorShaderClass::OutputShaderErrorMessage(OpenGLClass * OpenGL, HWND hwnd, unsigned int shaderId, char* shaderFilename)
{
int logSize, i;
char* infoLog;
ofstream fout;
wchar_t newString[128];
unsigned int error, convertedChars;
// 获取包含失败的着色器编译消息的信息日志的字符串的大小
OpenGL->glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &logSize);
// 将大小增加一以处理空终止符.
logSize++;
// 创建一个char缓冲区来保存信息日志。
infoLog = new char[logSize];
if (!infoLog)
{
return;
}
// 现在检索信息日志。
OpenGL->glGetShaderInfoLog(shaderId, logSize, NULL, infoLog);
// 打开一个文件,将错误消息写入其中
fout.open("shader-error.txt");
//写出错误信息。
for (i = 0; i < logSize; i++)
{
fout << infoLog[i];
}
//关闭文件
fout.close();
// /将着色器文件名转换为宽字符串.
error = mbstowcs_s(&convertedChars, newString, 128, shaderFilename, 128);
if (error != 0)
{
return;
}
// 在屏幕上弹出一条消息,通知用户检查文本文件是否存在编译错误
MessageBox(hwnd, L"Error compiling shader. Check shader-error.txt for message.", newString, MB_OK);
return;
}
//如果InitializeShader函数中的链接失败,则OutputLinkerErrorMessage函数会将链接器错误写到文本文件中。
void ColorShaderClass::OutputLinkerErrorMessage(OpenGLClass * OpenGL, HWND hwnd, unsigned int programId)
{
int logSize, i;
char* infoLog;
ofstream fout;
// 获取包含失败的着色器编译消息的信息日志的字符串的大小。
OpenGL->glGetProgramiv(programId, GL_INFO_LOG_LENGTH, &logSize);
//将大小增加一以处理空终止符。
logSize++;
//创建一个char缓冲区来保存信息日志。
infoLog = new char[logSize];
if (!infoLog)
{
return;
}
//现在检索信息日志.
OpenGL->glGetProgramInfoLog(programId, logSize, NULL, infoLog);
//打开一个文件,将错误消息写入其中。
fout.open("linker-error.txt");
// 写出错误信息。
for (i = 0; i < logSize; i++)
{
fout << infoLog[i];
}
// 关闭文件。
fout.close();
// 在屏幕上弹出一条消息,通知用户检查文本文件中的链接器错误。
MessageBox(hwnd, L"Error compiling linker. Check linker-error.txt for message.", L"Linker Error", MB_OK);
return;
}
//ShutdownShader释放着色器和着色器程序。
void ColorShaderClass::ShutdownShader(OpenGLClass * OpenGL)
{
//从程序中分离顶点和片段着色器。
OpenGL->glDetachShader(m_shaderProgram, m_vertexShader);
OpenGL->glDetachShader(m_shaderProgram, m_fragmentShader);
//删除顶点和片段着色器。
OpenGL->glDeleteShader(m_vertexShader);
OpenGL->glDeleteShader(m_fragmentShader);
//删除着色器程序
OpenGL->glDeleteProgram(m_shaderProgram);
return;
}
//存在SetShaderVariables函数可简化在着色器中设置统一变量的过程。
//在GraphicsClass中创建此函数中使用的矩阵,然后调用此函数以在Render函数调用期间将其从那里发送到顶点着色器。
bool ColorShaderClass::SetShaderParameters(OpenGLClass * OpenGL, float* worldMatrix, float* viewMatrix, float* projectionMatrix)
{
unsigned int location;
// 在顶点着色器中设置世界矩阵.用的统一变量
location = OpenGL->glGetUniformLocation(m_shaderProgram, "worldMatrix");
if (location == -1)
{
return false;
}
OpenGL->glUniformMatrix4fv(location, 1, false, worldMatrix);
// 在顶点着色器中设置视图矩阵。
location = OpenGL->glGetUniformLocation(m_shaderProgram, "viewMatrix");
if (location == -1)
{
return false;
}
OpenGL->glUniformMatrix4fv(location, 1, false, viewMatrix);
//在顶点着色器中设置投影矩阵
location = OpenGL->glGetUniformLocation(m_shaderProgram, "projectionMatrix");
if (location == -1)
{
return false;
}
OpenGL->glUniformMatrix4fv(location, 1, false, projectionMatrix);
return true;
}
modelclass.h
如前所述,ModelClass负责封装3D模型的几何图形。在本教程中,我们将手动设置单个绿色三角形的数据。我们还将为三角形创建一个顶点和索引缓冲区,以便可以对其进行渲染。
// Filename: modelclass.h
#ifndef _MODELCLASS_H_
#define _MODELCLASS_H_
///
// MY CLASS INCLUDES //
///
#include "openglclass.h"
// Class name: ModelClass
class ModelClass
{
private:
//这是我们的顶点类型的定义,它将与该ModelClass中的顶点缓冲区一起使用。
//还要注意,此typedef必须与ColorShaderClass以及GLSL顶点着色器中的输入变量布局匹配。
struct VertexType
{
float x, y, z;
float r, g, b;
};
public:
ModelClass();
ModelClass(const ModelClass&);
~ModelClass();
//此处的函数处理模型的顶点和索引缓冲区的初始化和关闭。
//渲染功能将模型几何图形放置在显卡上,并使用GLSL着色器进行绘制。
bool Initialize(OpenGLClass*);
void Shutdown(OpenGLClass*);
void Render(OpenGLClass*);
private:
bool InitializeBuffers(OpenGLClass*);
void ShutdownBuffers(OpenGLClass*);
void RenderBuffers(OpenGLClass*);
//ModelClass中的私有变量是顶点数组对象,顶点缓冲区和索引缓冲区ID。
//另外,还有两个整数可以跟踪顶点和索引缓冲区的大小。
private:
int m_vertexCount, m_indexCount;
unsigned int m_vertexArrayId, m_vertexBufferId, m_indexBufferId;
};
#endif
modelclass.cpp
三角形的三个顶点我分别设置颜色为红绿蓝,最后三角形内填充颜色会形成插值。
// Filename: modelclass.cpp
#include "modelclass.h"
ModelClass::ModelClass()
{
}
ModelClass::ModelClass(const ModelClass& other)
{
}
ModelClass::~ModelClass()
{
}
//Initialize函数将调用顶点和索引缓冲区的初始化函数。
bool ModelClass::Initialize(OpenGLClass * OpenGL)
{
bool result;
// 初始化保存三角形几何图形的顶点和索引缓冲区
result = InitializeBuffers(OpenGL);
if (!result)
{
return false;
}
return true;
}
//关闭功能将为缓冲区和相关数据调用关闭功能。
void ModelClass::Shutdown(OpenGLClass * OpenGL)
{
// 释放顶点和索引缓冲区。
ShutdownBuffers(OpenGL);
return;
}
//从GraphicsClass :: Render函数调用Render。
//此函数调用RenderBuffers将顶点和索引缓冲区放在图形管线上,并使用颜色着色器进行渲染。
void ModelClass::Render(OpenGLClass * OpenGL)
{
//将顶点和索引缓冲区放在图形管线上,以准备进行绘制。
RenderBuffers(OpenGL);
return;
}
//InitializeBuffers函数是我们处理创建顶点和索引缓冲区的地方。通常,您会读入一个模型并从该数据文件创建缓冲区。
//在本教程中,我们将手动设置顶点和索引缓冲区中的点,因为它只是一个三角形。
bool ModelClass::InitializeBuffers(OpenGLClass * OpenGL)
{
VertexType* vertices;
unsigned int* indices;
//首先创建两个临时数组,以保存顶点和索引数据,稍后我们将使用这些数据来填充最终缓冲区。
// 设置顶点数组中的顶点数。
m_vertexCount = 3;
// 设置索引数组中的索引数。
m_indexCount = 3;
//创建顶点数组.
vertices = new VertexType[m_vertexCount];
if (!vertices)
{
return false;
}
//创建索引数组.
indices = new unsigned int[m_indexCount];
if (!indices)
{
return false;
}
//现在,用三角形的三个点以及每个点的索引填充顶点和索引数组。
//请注意,我按绘制它们的顺时针顺序创建了这些点。
//如果沿逆时针方向进行操作,则会认为该三角形朝向相反的方向,并且由于背面剔除而无法绘制该三角形。
//始终记住,将顶点发送到GPU的顺序非常重要。
//颜色也是顶点描述的一部分,因此也可以在此处设置颜色。我将颜色设置为绿色。
// 用数据加载顶点数组。
// 左下方。.
vertices[0].x = -1.0f; // 位置.
vertices[0].y = -1.0f;
vertices[0].z = 0.0f;
vertices[0].r = 1.0f; // 颜色红.
vertices[0].g = 0.0f;
vertices[0].b = 0.0f;
// 中间居中.
vertices[1].x = 0.0f; // 位置.
vertices[1].y = 1.0f;
vertices[1].z = 0.0f;
vertices[1].r = 0.0f; // 颜色绿.
vertices[1].g = 1.0f;
vertices[1].b = 0.0f;
// 右下角.
vertices[2].x = 1.0f; // 位置.
vertices[2].y = -1.0f;
vertices[2].z = 0.0f;
vertices[2].r = 0.0f; //颜色蓝.
vertices[2].g = 0.0f;
vertices[2].b = 1.0f;
//用数据加载索引数组.
indices[0] = 0; // 左下方.
indices[1] = 1; // 中间居中.
indices[2] = 2; // 右下角.
//填写完顶点数组和索引数组后,我们现在可以使用它们来创建顶点缓冲区和索引缓冲区。
//但是首先,我们必须创建一个顶点数组对象,该对象将存储有关缓冲区和属性的所有信息,VAO,
//以便我们可以对顶点数组对象进行一次调用以为我们处理所有渲染。
//绑定顶点数组对象后,我们可以创建顶点和索引缓冲区,并从上面创建的临时数组中将数据加载到它们中。
//我们还绑定了顶点数组属性,以便它知道顶点缓冲区内数据的格式。
//设置glVertexAttribPointer函数中的最后一个参数很重要,这样它才能知道每个顶点缓冲元素开始位置的偏移量。
// 分配OpenGL顶点数组对象.
OpenGL->glGenVertexArrays(1, &m_vertexArrayId);
// 绑定顶点数组对象VAO以存储我们在此处创建的所有缓冲区和顶点属性
OpenGL->glBindVertexArray(m_vertexArrayId);
// 生成顶点缓冲区的ID。
OpenGL->glGenBuffers(1, &m_vertexBufferId);
// 绑定顶点缓冲区并将顶点(位置和颜色)数据加载到顶点缓冲区中。
OpenGL->glBindBuffer(GL_ARRAY_BUFFER, m_vertexBufferId);
OpenGL->glBufferData(GL_ARRAY_BUFFER, m_vertexCount * sizeof(VertexType), vertices, GL_STATIC_DRAW);
// 启用两个顶点数组属性.
OpenGL->glEnableVertexAttribArray(0); // 顶点坐标.
OpenGL->glEnableVertexAttribArray(1); // 顶点颜色.
// 指定顶点缓冲区的位置部分的位置和格式.
OpenGL->glBindBuffer(GL_ARRAY_BUFFER, m_vertexBufferId);
OpenGL->glVertexAttribPointer(0, 3, GL_FLOAT, false, sizeof(VertexType), 0);
// 指定顶点缓冲区颜色部分的位置和格式.
OpenGL->glBindBuffer(GL_ARRAY_BUFFER, m_vertexBufferId);
OpenGL->glVertexAttribPointer(1, 3, GL_FLOAT, false, sizeof(VertexType), (unsigned char*)NULL + (3 * sizeof(float)));
// 生成索引缓冲区的ID.
OpenGL->glGenBuffers(1, &m_indexBufferId);
//绑定索引缓冲区并将索引数据加载到其中
OpenGL->glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_indexBufferId);
OpenGL->glBufferData(GL_ELEMENT_ARRAY_BUFFER, m_indexCount * sizeof(unsigned int), indices, GL_STATIC_DRAW);
//创建顶点缓冲区和索引缓冲区后,您可以删除顶点数组和索引数组,因为将数据复制到缓冲区中不再需要它们。
//现在,缓冲区已加载,我们可以释放数组数据了.
delete[] vertices;
vertices = 0;
delete[] indices;
indices = 0;
return true;
}
//ShutdownBuffers函数释放在InitializeBuffers函数中创建的缓冲区和属性。
void ModelClass::ShutdownBuffers(OpenGLClass * OpenGL)
{
// 禁用两个顶点数组属性.
OpenGL->glDisableVertexAttribArray(0);
OpenGL->glDisableVertexAttribArray(1);
// 释放顶点缓冲区.
OpenGL->glBindBuffer(GL_ARRAY_BUFFER, 0);
OpenGL->glDeleteBuffers(1, &m_vertexBufferId);
// 释放索引缓冲区。
OpenGL->glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
OpenGL->glDeleteBuffers(1, &m_indexBufferId);
// 释放顶点数组对象.
OpenGL->glBindVertexArray(0);
OpenGL->glDeleteVertexArrays(1, &m_vertexArrayId);
return;
}
//从Render函数调用RenderBuffers。
//此功能的目的是通过绑定OpenGL顶点数组对象,将顶点缓冲区和索引缓冲区设置为在GPU中的输入汇编器上处于活动状态。
//GPU具有活动的顶点缓冲区后,便可以使用当前设置的着色器渲染该缓冲区。
//此函数还定义应如何绘制这些缓冲区,例如三角形,直线,扇形等。
//在本教程中,我们将顶点缓冲区和索引缓冲区在输入汇编器上设置为活动状态,
//并告诉GPU应该使用glDrawElements OpenGL函数将缓冲区绘制为三角形。
//glDrawElements函数还指示我们将使用索引缓冲区进行绘制。
void ModelClass::RenderBuffers(OpenGLClass* OpenGL)
{
// 绑定存储有关顶点和索引缓冲区的所有信息的顶点数组对象。
OpenGL->glBindVertexArray(m_vertexArrayId);
// 使用索引缓冲区渲染顶点缓冲区
glDrawElements(GL_TRIANGLES, m_indexCount, GL_UNSIGNED_INT, 0);
return;
}
cameraclass.h
我们已经研究了如何编写GLSL着色器,如何设置顶点和索引缓冲区以及如何调用GLSL着色器以使用ColorShaderClass绘制这些缓冲区。但是,我们缺少的一件事是从中设置视点。为此,我们将需要一个摄影机类,以让OpenGL 4.0从何处以及如何查看场景。相机类将跟踪相机的位置及其当前旋转。它将使用位置和旋转信息生成视图矩阵,该视图矩阵将传递到GLSL着色器中进行渲染。
// Filename: cameraclass.h
#ifndef _CAMERACLASS_H_
#define _CAMERACLASS_H_
//
// INCLUDES //
//
#include <math.h>
// Class name: CameraClass
class CameraClass
{
private:
struct VectorType
{
float x, y, z;
};
public:
CameraClass();
CameraClass(const CameraClass&);
~CameraClass();
//CameraClass标头非常简单,仅使用四个函数。SetPosition和SetRotation函数将用于设置相机对象的位置和旋转。
//Render将用于基于相机的位置和旋转来创建视图矩阵。
//最后,将使用GetViewMatrix从摄影机对象检索视图矩阵,以便着色器可以将其用于渲染。
void SetPosition(float, float, float);
void SetRotation(float, float, float);
void Render();
void GetViewMatrix(float*);
private:
void MatrixRotationYawPitchRoll(float*, float, float, float);
void TransformCoord(VectorType&, float*);
void BuildViewMatrix(VectorType, VectorType, VectorType);
private:
float m_positionX, m_positionY, m_positionZ;
float m_rotationX, m_rotationY, m_rotationZ;
float m_viewMatrix[16];
};
#endif
cameraclass.cpp
// Filename: cameraclass.cpp
#include “cameraclass.h”
//类的构造函数会将摄像机的位置和旋转初始化为场景的原点。
CameraClass::CameraClass()
{
m_positionX = 0.0f;
m_positionY = 0.0f;
m_positionZ = 0.0f;
m_rotationX = 0.0f;
m_rotationY = 0.0f;
m_rotationZ = 0.0f;
}
CameraClass::CameraClass(const CameraClass& other)
{
}
CameraClass::~CameraClass()
{
}
//SetPosition和SetRotation函数用于设置摄像机的位置和旋转。
void CameraClass::SetPosition(float x, float y, float z)
{
m_positionX = x;
m_positionY = y;
m_positionZ = z;
return;
}
void CameraClass::SetRotation(float x, float y, float z)
{
m_rotationX = x;
m_rotationY = y;
m_rotationZ = z;
return;
}
//渲染功能使用摄像机的位置和旋转来构建和更新视图矩阵。我们首先为up,position,旋转等设置变量。
//然后,在场景的起点,我们首先根据摄像机的x,y和z旋转来旋转摄像机。正确旋转后,我们便将相机平移到3D空间中的位置。
//在正确的位置,lookAt和向上的值之后,我们可以使用BuildViewMatrix函数创建表示当前相机旋转和平移的视图矩阵。
void CameraClass::Render()
{
VectorType up, position, lookAt;
float yaw, pitch, roll;
float rotationMatrix[9];
// 设置指向上方的向量.
up.x = 0.0f;
up.y = 1.0f;
up.z = 0.0f;
// 设置相机在世界上的位置。
position.x = m_positionX;
position.y = m_positionY;
position.z = m_positionZ;
// 设置默认情况下相机所处的位置.
lookAt.x = 0.0f;
lookAt.y = 0.0f;
lookAt.z = 1.0f;
//在左手坐标系中,以弧度为单位设置偏转pitch(Y轴),俯仰yaw(X轴)和滚动roll(Z轴)旋转
pitch = m_rotationX * 0.0174532925f;
yaw = m_rotationY * 0.0174532925f;
roll = m_rotationZ * 0.0174532925f;
// 根据偏转,俯仰和滚动值创建旋转矩阵
MatrixRotationYawPitchRoll(rotationMatrix, yaw, pitch, roll);
// 通过旋转矩阵变换lookAt和上向量,以便视图在原点正确旋转
TransformCoord(lookAt, rotationMatrix);
TransformCoord(up, rotationMatrix);
// 将旋转的相机位置平移到查看器的位置
lookAt.x = position.x + lookAt.x;
lookAt.y = position.y + lookAt.y;
lookAt.z = position.z + lookAt.z;
// 最后,从三个更新的向量创建视图矩阵。
BuildViewMatrix(position, lookAt, up);
return;
}
//以下函数根据偏转,俯仰和横滚值创建左手旋转矩阵
void CameraClass::MatrixRotationYawPitchRoll(float* matrix, float yaw, float pitch, float roll)
{
float cYaw, cPitch, cRoll, sYaw, sPitch, sRoll;
// 获得偏航,俯仰和横滚的余弦和正弦.
cYaw = cosf(yaw);
cPitch = cosf(pitch);
cRoll = cosf(roll);
sYaw = sinf(yaw);
sPitch = sinf(pitch);
sRoll = sinf(roll);
// 计算偏航,俯仰,横滚旋转矩阵.
matrix[0] = (cRoll * cYaw) + (sRoll * sPitch * sYaw);
matrix[1] = (sRoll * cPitch);
matrix[2] = (cRoll * -sYaw) + (sRoll * sPitch * cYaw);
matrix[3] = (-sRoll * cYaw) + (cRoll * sPitch * sYaw);
matrix[4] = (cRoll * cPitch);
matrix[5] = (sRoll * sYaw) + (cRoll * sPitch * cYaw);
matrix[6] = (cPitch * sYaw);
matrix[7] = -sPitch;
matrix[8] = (cPitch * cYaw);
return;
}
//以下函数将3浮点矢量乘以3x3矩阵,其实是列向量左乘矩阵。然后将结果返回到输入矢量中
void CameraClass::TransformCoord(VectorType & vector, float* matrix)
{
float x, y, z;
// 通过3x3矩阵转换向量.
x = (vector.x * matrix[0]) + (vector.y * matrix[3]) + (vector.z * matrix[6]);
y = (vector.x * matrix[1]) + (vector.y * matrix[4]) + (vector.z * matrix[7]);
z = (vector.x * matrix[2]) + (vector.y * matrix[5]) + (vector.z * matrix[8]);
// 将结果存储在引用中.
vector.x = x;
vector.y = y;
vector.z = z;
return;
}
//以下函数构建左手视图矩阵
void CameraClass::BuildViewMatrix(VectorType position, VectorType lookAt, VectorType up)
{
VectorType zAxis, xAxis, yAxis;
float length, result1, result2, result3;
// zAxis = normal(lookAt - position)
zAxis.x = lookAt.x - position.x;
zAxis.y = lookAt.y - position.y;
zAxis.z = lookAt.z - position.z;
length = sqrt((zAxis.x * zAxis.x) + (zAxis.y * zAxis.y) + (zAxis.z * zAxis.z));
zAxis.x = zAxis.x / length;
zAxis.y = zAxis.y / length;
zAxis.z = zAxis.z / length;
// xAxis = normal(cross(up, zAxis))
xAxis.x = (up.y * zAxis.z) - (up.z * zAxis.y);
xAxis.y = (up.z * zAxis.x) - (up.x * zAxis.z);
xAxis.z = (up.x * zAxis.y) - (up.y * zAxis.x);
length = sqrt((xAxis.x * xAxis.x) + (xAxis.y * xAxis.y) + (xAxis.z * xAxis.z));
xAxis.x = xAxis.x / length;
xAxis.y = xAxis.y / length;
xAxis.z = xAxis.z / length;
// yAxis = cross(zAxis, xAxis)
yAxis.x = (zAxis.y * xAxis.z) - (zAxis.z * xAxis.y);
yAxis.y = (zAxis.z * xAxis.x) - (zAxis.x * xAxis.z);
yAxis.z = (zAxis.x * xAxis.y) - (zAxis.y * xAxis.x);
// -dot(xAxis, position)
result1 = ((xAxis.x * position.x) + (xAxis.y * position.y) + (xAxis.z * position.z)) * -1.0f;
// -dot(yaxis, eye)
result2 = ((yAxis.x * position.x) + (yAxis.y * position.y) + (yAxis.z * position.z)) * -1.0f;
// -dot(zaxis, eye)
result3 = ((zAxis.x * position.x) + (zAxis.y * position.y) + (zAxis.z * position.z)) * -1.0f;
// 在视图矩阵中设置计算值.
m_viewMatrix[0] = xAxis.x;
m_viewMatrix[1] = yAxis.x;
m_viewMatrix[2] = zAxis.x;
m_viewMatrix[3] = 0.0f;
m_viewMatrix[4] = xAxis.y;
m_viewMatrix[5] = yAxis.y;
m_viewMatrix[6] = zAxis.y;
m_viewMatrix[7] = 0.0f;
m_viewMatrix[8] = xAxis.z;
m_viewMatrix[9] = yAxis.z;
m_viewMatrix[10] = zAxis.z;
m_viewMatrix[11] = 0.0f;
m_viewMatrix[12] = result1;
m_viewMatrix[13] = result2;
m_viewMatrix[14] = result3;
m_viewMatrix[15] = 1.0f;
return;
}
//调用Render函数创建视图矩阵后,我们可以使用此GetViewMatrix函数将更新的视图矩阵提供给调用函数。
//视图矩阵将是GLSL顶点着色器中使用的三个主要矩阵之一。
void CameraClass::GetViewMatrix(float* matrix)
{
matrix[0] = m_viewMatrix[0];
matrix[1] = m_viewMatrix[1];
matrix[2] = m_viewMatrix[2];
matrix[3] = m_viewMatrix[3];
matrix[4] = m_viewMatrix[4];
matrix[5] = m_viewMatrix[5];
matrix[6] = m_viewMatrix[6];
matrix[7] = m_viewMatrix[7];
matrix[8] = m_viewMatrix[8];
matrix[9] = m_viewMatrix[9];
matrix[10] = m_viewMatrix[10];
matrix[11] = m_viewMatrix[11];
matrix[12] = m_viewMatrix[12];
matrix[13] = m_viewMatrix[13];
matrix[14] = m_viewMatrix[14];
matrix[15] = m_viewMatrix[15];
return;
}
graphicsclass.h
GraphicsClass现在添加了三个新类。CameraClass,ModelClass和ColorShaderClass在此处添加了标头以及私有成员变量。请记住,GraphicsClass是用于通过调用项目所需的所有类对象来渲染场景的主要类。
// Filename: graphicsclass.h
#ifndef _GRAPHICSCLASS_H_
#define _GRAPHICSCLASS_H_
///
// MY CLASS INCLUDES //
///
#include "openglclass.h"
#include "cameraclass.h"
#include "modelclass.h"
#include "colorshaderclass.h"
/
// GLOBALS //
/
const bool FULL_SCREEN = false;
const bool VSYNC_ENABLED = true;
const float SCREEN_DEPTH = 1000.0f;
const float SCREEN_NEAR = 0.1f;
// Class name: GraphicsClass
class GraphicsClass
{
public:
GraphicsClass();
GraphicsClass(const GraphicsClass&);
~GraphicsClass();
bool Initialize(OpenGLClass*, HWND);
void Shutdown();
bool Frame();
private:
bool Render();
private:
//OpenGL类的对象
OpenGLClass* m_OpenGL;
CameraClass* m_Camera;
ModelClass* m_Model;
ColorShaderClass* m_ColorShader;
};
#endif
graphicsclass.cpp
对GraphicsClass的第一个更改是将类构造函数中的相机,模型和颜色着色器对象初始化为null。
// Filename: graphicsclass.cpp
#include "graphicsclass.h"
GraphicsClass::GraphicsClass()
{
m_OpenGL = 0;
m_Camera = 0;
m_Model = 0;
m_ColorShader = 0;
}
GraphicsClass::GraphicsClass(const GraphicsClass& other)
{
}
GraphicsClass::~GraphicsClass()
{
}
//Initialize函数可以创建和初始化三个新对象。
bool GraphicsClass::Initialize(OpenGLClass* OpenGL, HWND hwnd)
{
bool result;
// 存储指向OpenGL类对象的指针
m_OpenGL = OpenGL;
//创建相机对象。
m_Camera = new CameraClass;
if (!m_Camera)
{
return false;
}
//设置摄像机的初始位置。
m_Camera->SetPosition(0.0f, 0.0f, -10.0f);
//创建模型对象。
m_Model = new ModelClass;
if (!m_Model)
{
return false;
}
//初始化模型对象。
result = m_Model->Initialize(m_OpenGL);
if (!result)
{
MessageBox(hwnd, L"Could not initialize the model object.", L"Error", MB_OK);
return false;
}
//创建颜色着色器对象。
m_ColorShader = new ColorShaderClass;
if (!m_ColorShader)
{
return false;
}
//初始化颜色着色器对象。
result = m_ColorShader->Initialize(m_OpenGL, hwnd);
if (!result)
{
MessageBox(hwnd, L"Could not initialize the color shader object.", L"Error", MB_OK);
return false;
}
return true;
}
//关机
void GraphicsClass::Shutdown()
{
//释放颜色着色器对象
if (m_ColorShader)
{
m_ColorShader->Shutdown(m_OpenGL);
delete m_ColorShader;
m_ColorShader = 0;
}
//释放模型对象。
if (m_Model)
{
m_Model->Shutdown(m_OpenGL);
delete m_Model;
m_Model = 0;
}
//释放相机对象。
if (m_Camera)
{
delete m_Camera;
m_Camera = 0;
}
//释放指向OpenGL类对象的指针
m_OpenGL = 0;
return;
}
//帧函数已更新,可以在每个框架上调用渲染函数
bool GraphicsClass::Frame()
{
bool result;
// 渲染图形场景.
result = Render();
if (!result)
{
return false;
}
return true;
}
//“渲染”功能仍然从清除场景开始,清除为黑色。
//之后,它会调用相机对象的Render函数以根据在Initialize函数中设置的相机位置创建视图矩阵。
//创建视图矩阵后,我们会从相机类中获取它的副本。我们还从OpenGLClass对象获取世界和投影矩阵的副本。
//接下来,将颜色GLSL着色器设置为当前渲染程序,以便绘制的任何内容都将使用这些顶点和像素着色器。
//然后,我们调用ModelClass :: Render函数绘制绿色三角形模型的几何形状。
//现在将绿色三角形绘制到后台缓冲区。这样,场景就完成了,我们调用EndScene将其显示在屏幕上。
bool GraphicsClass::Render()
{
float worldMatrix[16];
float viewMatrix[16];
float projectionMatrix[16];
// 清除缓冲区,开始场景
m_OpenGL->BeginScene(1.0f, 1.0f, 0.0f, 1.0f);
// 根据相机的位置生成视图矩阵.
m_Camera->Render();
// 从opengl和camera对象获取世界,视图和投影矩阵.
m_OpenGL->GetWorldMatrix(worldMatrix);
m_Camera->GetViewMatrix(viewMatrix);
m_OpenGL->GetProjectionMatrix(projectionMatrix);
// 将颜色着色器设置为当前着色器程序,并设置它将用于渲染的矩阵
m_ColorShader->SetShader(m_OpenGL);
m_ColorShader->SetShaderParameters(m_OpenGL, worldMatrix, viewMatrix, projectionMatrix);
// 使用颜色着色器渲染模型
m_Model->Render(m_OpenGL);
// 将渲染的场景呈现到屏幕上.
m_OpenGL->EndScene();
return true;
}
概要
因此,总而言之,您应该了解有关顶点和索引缓冲区如何工作以及顶点数组对象如何封装它们的基础知识。您还应该了解顶点和像素着色器的基础知识,以及如何使用GLSL编写它们。最后,您应该了解我们如何将这些新概念整合到框架中,以产生可渲染到屏幕的绿色三角形。我还想提一提,我意识到代码很长,只画了一个三角形,它可能全部卡在了一个main()函数中。但是,我这样做是通过适当的框架工作来完成的,因此接下来的教程仅需要很少的代码更改即可制作更复杂的图形。
由于每个顶点的颜色我设置了不同,最终的效果插值厚显示:
练习
1.编译并运行教程。确保它在屏幕上绘制了一个三角形。按下后退出即可退出。
2.将三角形的颜色更改为红色。
3.将三角形更改为正方形。
4.再将摄像机向后移动10个单位。
5.更改像素着色器,以输出一半亮度的颜色。(巨大提示:将像素着色器中的内容乘以0.5f)