本文主要记录使用所学OpenGL相关知识绘制OpenGL中的一个经典案例--球体世界,最终实现效果如下:
球体世界.gif准备工作
系统环境
macOS Catalina 10.15.6
开发工具
XCode 11.4.1
依赖库
- OpenGL.framework
- GLUT.framework
- libToools.a
相关头文件导入
main.cpp:
#include <GLTools.h>
#include <GLShaderManager.h>
#include <GLFrustum.h>
#include <GLFrame.h>
#include <GLBatch.h>
#include <GLMatrixStack.h>
#include <GLGeometryTransform.h>
#include <StopWatch.h>
#include <math3d.h>
#include <stdio.h>
#include <math.h>
#ifdef __APPLE__
#include <glut/glut.h>
#else
#define FREEGLUT_STATIC
#include <GL/glut.h>
#endif
相关属性声明
// 小球数量
#define NUM_SPHERES 50
// 小球对象数组
GLFrame spheres[NUM_SPHERES];
// 观察者
GLFrame cameraFrame;
// 视角
GLFrustum frustum;
// 投影矩阵堆栈
GLMatrixStack projectionMatrix;
// 模型视图矩阵堆栈
GLMatrixStack modelViewMatrix;
// 变换管道
GLGeometryTransform transformPipeline;
// 固定管线管理者
GLShaderManager shaderManager;
// 地板批次类
GLBatch floorBatch;
// 大球批次类
GLTriangleBatch torusBatch;
// 小球批次类
GLTriangleBatch sphereBatch;
// 纹理数组
GLuint textures[3];
初始化
在main函数中:
int main(int argc, char* argv[])
{
gltSetWorkingDirectory(argv[0]);
glutInit(&argc, argv);
// 设置双缓冲区/颜色缓冲区/深度缓冲区
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
// 设置宽口大小
glutInitWindowSize(800,600);
// 创建窗口
glutCreateWindow("OpenGL SphereWorld");
// 指定窗口大小变化监听函数
glutReshapeFunc(ChangeSize);
// 指定渲染函数
glutDisplayFunc(RenderScene);
// 键位输入函数
glutSpecialFunc(SpecialKeys);
GLenum err = glewInit();
if (GLEW_OK != err) {
fprintf(stderr, "GLEW Error: %s\n", glewGetErrorString(err));
return 1;
}
// 设置相关参数
SetupRC();
// 运行循环
glutMainLoop();
ShutdownRC();
return 0;
}
具体函数的执行流程如图:
函数执行流程.pngChangeSize
在这个函数中,我们需要的事情有:
- 设置视口大小
- 设置投影方式
- 加载投影矩阵
- 加载模型试图矩阵
- 设置变化管道
void ChangeSize(int width, int height) {
// 设置视口
glViewport(0, 0, width, height);
// 设置投影方式
frustum.SetPerspective(35.5f, float(width)/float(height), 1.f, 300.f);
// 加载投影矩阵
projectionMatrix.LoadMatrix(frustum.GetProjectionMatrix());
// 模型视图矩阵加载单元矩阵
modelViewMatrix.LoadIdentity();
// 设置管道
transformPipeline.SetMatrixStacks(modelViewMatrix, projectionMatrix);
}
SetupRC
在这函数中,我们需要做的有:
- 设置清屏颜色
- 初始化固定着色器管理者
- 开启深度测试和正背面剔除
- 设置顶点
- 加载纹理
void SetupRC() {
// 设置颜色
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
// 初始话着色器管理者
shaderManager.InitializeStockShaders();
// 开启深度测试
glEnable(GL_DEPTH_TEST);
// 开启正背面剔除
glEnable(GL_CULL_FACE);
// 设置地板顶点
floorBatch.Begin(GL_TRIANGLE_FAN, 4, 1);
// 纹理坐标与地板顶点对应
floorBatch.MultiTexCoord2f(0, 0.0f, 0.0f);
floorBatch.Vertex3f(-20.0f, -0.4f, -20.f);
floorBatch.MultiTexCoord2f(0, 0.0f, 10.0f);
floorBatch.Vertex3f(-20.0f, -0.4f, 20.f);
floorBatch.MultiTexCoord2f(0, 10.0f, 10.0f);
floorBatch.Vertex3f(20.0f, -0.4f, 20.f);
floorBatch.MultiTexCoord2f(0, 10.0f, 0.0f);
floorBatch.Vertex3f(20.0f, -0.4f, -20.f);
floorBatch.End();
// 设置大球顶点
gltMakeSphere(torusBatch, 0.4f, 40, 80);
// 设置小球
gltMakeSphere(sphereBatch, 0.1f, 20, 40);
// 循环随机布置小球位置
for (int i = 0; i < NUM_SPHERES; i ++) {
GLfloat x = ((GLfloat)((rand() % 400) -200) * 0.1f);
GLfloat z = ((GLfloat)((rand() % 400) -200) * 0.1f);
spheres[i].SetOrigin(x, 0.0f, z);
}
// 命名纹理对象
glGenTextures(3, textures);
// 加载地板纹理
glBindTexture(GL_TEXTURE_2D, textures[0]);
LoadTGATexture("marble.tga", GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR, GL_REPEAT);
// 加载大球纹理
glBindTexture(GL_TEXTURE_2D, textures[1]);
LoadTGATexture("marslike.tga", GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR, GL_CLAMP_TO_EDGE);
//加载小球纹理
glBindTexture(GL_TEXTURE_2D, textures[2]);
LoadTGATexture("moonlike.tga", GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR, GL_CLAMP_TO_EDGE);
}
LoadTGATexture函数中具体实现了加载纹理的过程:
bool LoadTGATexture(const char *szFileName, GLenum minFilter, GLenum magFilter, GLenum wrapMode) {
// 声明相关参数
GLbyte *pBits;
GLint iWidth, iHeight, iComponents;
GLenum eFormat;
// 读取纹理
pBits = gltReadTGABits(szFileName, &iWidth, &iHeight, &iComponents, &eFormat);
if (pBits == NULL) {
return false;
}
// 设置过滤参数
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, minFilter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, magFilter);
// 设置环绕方式参数
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapMode);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapMode);
// 载入纹理
glTexImage2D(GL_TEXTURE_2D, 0, iComponents, iWidth, iHeight, 0, eFormat, GL_UNSIGNED_BYTE, pBits);
// 释放
free(pBits);
// 生成MIP
if (minFilter == GL_LINEAR_MIPMAP_LINEAR || minFilter == GL_LINEAR_MIPMAP_NEAREST ||minFilter == GL_NEAREST_MIPMAP_LINEAR || minFilter == GL_NEAREST_MIPMAP_NEAREST)
{
glGenerateMipmap(GL_TEXTURE_2D);
}
return true;
}
RenderScene
RenderScene()中具体实现了绘制的过程,具体过程如下:
void RenderScene() {
// 清空缓存
glClear(GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT);
// 设置地板颜色
static GLfloat vFloorColor[] = {1.0f, 1.0f, 0.0f, 0.75f};
// 定时器
static CStopWatch timer;
float yRot = timer.GetElapsedSeconds() * 60.0f;
// 观察者矩阵
M3DMatrix44f mCamera;
cameraFrame.GetCameraMatrix(mCamera);
// 后续坐标都以此变化,所以先压栈
modelViewMatrix.PushMatrix(mCamera);
// 绘制镜像
modelViewMatrix.PushMatrix();
// 矩阵翻转
modelViewMatrix.Scale(1.f, -1.f, 1.f);
// 往下平移一个大球直径的距离(此时矩阵是翻转的说y向下位正)
modelViewMatrix.Translate(0.f, 0.8f, 0.f);
//设置顺时针位正面(因为已经翻转了)
glFrontFace(GL_CW);
// 具体绘制
DrawSomething(yRot);
//恢复正面
glFrontFace(GL_CCW);
// 矩阵出栈恢复,开始绘制镜面及镜面以上
modelViewMatrix.PopMatrix();
// 绘制地板
// 开启颜色混色
glEnable(GL_BLEND);
// 指定颜色和方程
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// 绑定纹理
glBindTexture(GL_TEXTURE_2D, textures[0]);
// 使用纹理调整着色器
shaderManager.UseStockShader(GLT_SHADER_TEXTURE_MODULATE, transformPipeline.GetModelViewProjectionMatrix(), vFloorColor, 0);
//开始绘制
floorBatch.Draw();
// 关闭颜色混合
glDisable(GL_BLEND);
// 绘制镜面以上
DrawSomething(yRot);
// 观察者矩阵出栈
modelViewMatrix.PopMatrix();
// 交换缓冲区
glutSwapBuffers();
// 提交渲染
glutPostRedisplay();
}
DrawSomething具体实现了大球及小球的绘制,代码如下
void DrawSomething(float yRot) {
// 设置点光源位置
M3DVector4f vLight = {0, 10, 5, 1.0f};
// 白色
static GLfloat vWhite[] = {1.0f, 1.0f, 1.0f, 1.0f};
// 完y轴正方向平移,并往里偏移-3.0,便于观察
modelViewMatrix.Translate(0.0f, 0.3f, -3.0f);
// 开始绘制大球
modelViewMatrix.PushMatrix();
// 大球自传
modelViewMatrix.Rotate(yRot, 0.0f, 1.0f, 0.0f);
// 绑定纹理
glBindTexture(GL_TEXTURE_2D, textures[1]);
// 使用纹理光源着色器
shaderManager.UseStockShader(GLT_SHADER_TEXTURE_POINT_LIGHT_DIFF, transformPipeline.GetModelViewMatrix(), transformPipeline.GetProjectionMatrix(), vLight, vWhite, 0);
// 绘制大球
torusBatch.Draw();
//大球绘制完成将矩阵堆栈里的自转矩阵出栈
modelViewMatrix.PopMatrix();
// 小球
for (int i = 0; i < NUM_SPHERES; i++) {
modelViewMatrix.PushMatrix();
// 小球位置
modelViewMatrix.MultMatrix(spheres[i]);
// 绑定纹理
glBindTexture(GL_TEXTURE_2D, textures[2]);
// 使用纹理光源着色器
shaderManager.UseStockShader(GLT_SHADER_TEXTURE_POINT_LIGHT_DIFF, transformPipeline.GetModelViewMatrix(), transformPipeline.GetProjectionMatrix(), vLight, vWhite, 0);
// 绘制小球
sphereBatch.Draw();
// 出栈
modelViewMatrix.PopMatrix();
}
// 小球公转
modelViewMatrix.PushMatrix();
// 绕y轴旋转
modelViewMatrix.Rotate(-2*yRot, 0.f, 1.f, 0.f);
// 往x轴正方向平移1.0
modelViewMatrix.Translate(1.0f, 0.0f, 0.0f);
// 使用纹理光源着色器
glBindTexture(GL_TEXTURE_2D, textures[2]);
shaderManager.UseStockShader(GLT_SHADER_TEXTURE_POINT_LIGHT_DIFF, transformPipeline.GetModelViewMatrix(), transformPipeline.GetProjectionMatrix(), vLight, vWhite, 0);
// 开始绘制
sphereBatch.Draw();
// 整个矩阵出栈
modelViewMatrix.PopMatrix();
}
SpecialKeys
在这个函数中,我们修改观察者的位置或观察方向
void SpecialKeys(int key, int x, int y) {
float linear = 0.1f;
float angular = float(m3dDegToRad(5.0f));
if (key == GLUT_KEY_UP) {
// 往前移动
cameraFrame.MoveForward(linear);
} else if (key == GLUT_KEY_DOWN) {
// 往后移动
cameraFrame.MoveForward(-linear);
} else if (key == GLUT_KEY_LEFT) {
// 像左看
cameraFrame.RotateWorld(angular, 0, 1, 0);
} else if (key == GLUT_KEY_RIGHT) {
// 像右看
cameraFrame.RotateWorld(-angular, 0, 1, 0);
}
}
注意细节
- 地板纹理坐标与顶点坐标需要对应正确
- 每次绘制前一定要清空颜色缓冲区和深度缓冲区,避免上一次的绘制对本次产生影响
- 正背面规则修改后要记得修正回来
- 矩阵堆栈压栈出栈需要成对出现,否则会对下一个绘制图像产生影响
- 矩阵相乘不满足交换律,比如小球公转要先旋转在平移,如果交换顺序就会变成小球在一个固定位置自转
网友评论