github仓库:livingsu/OpenGL-simulate-turning
之前用的一直是旧OpenGL的固定管线(glut库),但是当接触了新OpenGL以后,感觉着色器、VAO、VBO实在是太强大了,不仅更加灵活,而且利用缓存可以极大提升渲染性能。
主要参考**Learn-OpenGL-CN官网**,项目中加载模型用到的Assimp库,加载图片用的stb_image库,另外还用到了learn-OpenGL作者写的一些类:camera.h,shader.h,model.h都是及其方便的类。
实现的主要功能:
- 显示背景图片,圆柱形原料、车刀模型的绘制
- 通过鼠标移动车刀,完成切削
- 切削时有粒子系统模拟飞溅效果
- 可以选择圆柱体材质(金属或者木质),使用的是pbr光照模型
- 切削后圆柱体光照材质发生变化
- 可以创建三次贝塞尔曲线作为切削的约束线
效果预览:
1.背景图片
画两个三角形,设置纹理图片即可,注意将z值设置和w一样,那么其深度就是最大的z/w=1.0,就会显示在所有物体的后面。
// background.vs
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec2 aTexCoords;
out vec2 TexCoords;
void main(){
TexCoords = aTexCoords;
gl_Position = vec4(aPos.xy,1.0,1.0); //设置深度为最大
}
要注意画背景之前打开glDepthFunc(GL_LEQUAL)并在之后恢复默认值。
// 画背景图片
// -------------
glDepthFunc(GL_LEQUAL); //设置背景在所有物体的最后面(最大深度)绘制
bgShader.use();
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, bgTexture);
glBindVertexArray(bgVAO);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
glDepthFunc(GL_LESS);
2.圆柱体原料
圆柱体拥有很多信息
//圆柱体信息,注意应该用double而不是float,避免精度不够导致除法出错
const double cylinderRadius = 0.4;
const double cylinderLength = 1.6;
const int angleStep = 2; //切分角度增量
const double lengthStep = 0.001; //切分长度增量
const double radiusStep = 0.001; //半径增量
vector<int> radiusArray; //半径数组,均为radiusStep的整数倍
vector<int> radiusMinArray; //半径最小值数组,规定半径的最小值,用于贝塞尔曲线的限制
vector<glm::vec3> allPoints; //所有点数组,包括顶点、法向量和纹理坐标
vector<unsigned int> indices; //索引数据
unsigned int cylinderVAO;
unsigned int cylinderVBO;
unsigned int vertexNum; //顶点数量
绘制的主要思想:
只需要绘制圆柱体侧面,将侧面展开得到一个矩形,将矩形沿底边分成slices(=360/angleStep)份,沿z轴分成stacks(=cylinderLength/lengthStep)份,得到的小矩形用两个三角形绘制即可。由于相同的点在不同的小矩形中重复利用,故用EBO进行索引缓存即可。
void initCylinder() {
int slices = 360 / angleStep; //围绕z轴的细分
int stacks = cylinderLength / lengthStep; //沿z轴的细分
//初始化半径数组和半径限制数组
unsigned int initRadius = cylinderRadius / radiusStep;
for (int i = 0; i <= stacks; ++i) {
radiusArray.push_back(initRadius);
}
for (int i = 0; i <= stacks; ++i) {
radiusMinArray.push_back(0); //开始时,半径最小均可以取0
}
float R, alpha, x, y, z, texX, texY;
for (int i = 0; i <= slices; i++) {
for (int j = 0; j <= stacks; j++) {
R = radiusArray[j] * radiusStep;
alpha = i * angleStep;
x = R * (float)glm::cos(glm::radians(alpha));
y = R * (float)glm::sin(glm::radians(alpha));
z = lengthStep * j;
texX = (float)i / (float)slices; //纹理坐标
texY = (float)j / (float)stacks;
//顶点
glm::vec3 V(x, y, z);
//侧面
allPoints.push_back(V);
allPoints.push_back(glm::vec3(V.x, V.y, 0.0f)); //法向量
allPoints.push_back(glm::vec3(texX, texY, 0.0f));//2d纹理坐标+是否被切削(0为否,1为是)
//添加索引坐标
if (i < slices && j < stacks) {
unsigned int leftDown, leftUp, rightDown, rightUp;
leftDown = i * (stacks + 1) + j;
leftUp = i * (stacks + 1) + (j + 1);
rightDown = (i + 1) * (stacks + 1) + j;
rightUp = (i + 1) * (stacks + 1) + (j + 1);
indices.push_back(leftDown);
indices.push_back(leftUp);
indices.push_back(rightUp);
indices.push_back(leftDown);
indices.push_back(rightUp);
indices.push_back(rightDown);
}
}
}
vertexNum = indices.size();
unsigned int VAO, VBO, EBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(float)*allPoints.size() * 3, &allPoints[0], GL_STREAM_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(unsigned int)*vertexNum, &indices[0], GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
glVertexAttribPointer(3, 1, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)(8 * sizeof(float)));
glEnableVertexAttribArray(3);
cylinderVAO = VAO;
cylinderVBO = VBO;
}
绘制(没加上着色器):
glBindVertexArray(cylinderVAO);
glDrawElements(GL_TRIANGLES, vertexNum, GL_UNSIGNED_INT, 0);
3.车刀的建模和载入
我用的是3dmax建模,车刀的建模比较简单,加上金属材质,然后导出为3ds文件格式或者是obj格式均可,然后用learn-OpenGL作者写的model类读取模型即可。
4.鼠标控制车刀移动,模拟切削
这里通过将鼠标的屏幕坐标(原点在左上角)转化为标准化设备坐标(原点在屏幕中央,范围是-1~1),将车刀移至指定位置上即可。
//直接将车刀移至指定的标准化设备坐标上
model = glm::translate(model, glm::vec3(clipX, clipY, 0.0f));
//旋转到正确方位
model = glm::rotate(model, glm::radians(90.0f), glm::vec3(0.0f, 1.0f, 0.0f));
model = glm::rotate(model, glm::radians(90.0f), glm::vec3(1.0f, 0.0f, 0.0f));
model = glm::scale(model, glm::vec3(0.02f));
modelShader.setMat4("model", model);
myModel.Draw(modelShader);
模拟切削
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
//将屏幕坐标转化为裁剪坐标(-1~1范围)
double newClipX = xpos * 2.0 / (double)(WIN_WIDTH - 1) - 1.0;
double newClipY = -ypos * 2.0 / (double)(WIN_HEIGHT - 1) + 1.0;
//模拟切削
if (mode == 1) {
//切削模式
if (newClipY > clipY0) {
//模型不得高于圆心位置
newClipY = clipY0;
}
if (newClipX < clipX0 || clipX < clipX0) {
int zStart = (clipX0 - (clipX > newClipX ? clipX : newClipX)) / lengthStep; //圆柱体z轴方向下标
int zEnd = (clipX0 - (clipX > newClipX ? newClipX : clipX)) / lengthStep;
int zGap = zEnd - zStart;
//开始部分对应的半径是radiusStep的整数倍
int R_start = (clipY0 - (clipX > newClipX ? clipY : newClipY)) / radiusStep;
int R_end = (clipY0 - (clipX > newClipX ? newClipY : clipY)) / radiusStep;
int R_gap = R_end - R_start;
int stacks = cylinderLength / lengthStep;
if (zStart >= 0 && zStart <= stacks && zEnd >= 0 && zEnd <= stacks && zGap >= 0 && R_start >= 0) {
isCut = false;
for (int i = zStart; i <= zEnd; ++i) {
int newRadius;
if (zGap == 0)
newRadius = R_start;
else
newRadius = R_start + R_gap * (float)(i - zStart) / (float)zGap;
if (newRadius < 0) newRadius = 0;
//更新半径数组
if (radiusArray[i] > newRadius&&radiusArray[i] > radiusMinArray[i]) {
//最小半径不能比现有半径小
isCut = true;
radiusArray[i] = newRadius < radiusMinArray[i] ? radiusMinArray[i] : newRadius; //新半径必须比现有半径小,且>=规定的最小半径
}
}
//更新VBO缓冲区
glBindBuffer(GL_ARRAY_BUFFER, cylinderVBO);
if (isCut) {
if (newClipX < clipX) {
//车刀左移,粒子速度方向应向右
isLeft = false;
}
else {
isLeft = true;
}
int slices = 360 / angleStep;
for (int i = 0; i <= slices; ++i) {
for (int j = zStart; j <= zEnd; ++j) {
int pointOffset = (i*(stacks + 1) + j) * 3;
glm::vec3 newPoint = allPoints[pointOffset];
float alpha = i * angleStep;
float newR = radiusArray[j] * radiusStep;
newPoint.x = newR * (float)glm::cos(glm::radians(alpha));
newPoint.y = newR * (float)glm::sin(glm::radians(alpha));
float isCut = 1.0f;
glBufferSubData(GL_ARRAY_BUFFER, 3 * sizeof(float) * pointOffset, 2 * sizeof(float), &newPoint);
glBufferSubData(GL_ARRAY_BUFFER, 3 * sizeof(float) * pointOffset + 2 * sizeof(glm::vec3) +2* sizeof(float), sizeof(float), &isCut);
allPoints[pointOffset] = newPoint;
}
}
}
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
}
}
//车刀跟随鼠标移动
clipX = newClipX;
clipY = newClipY;
}
结束语
通过对新OpenGL的学习,我对于图形学的原理有了更深入的理解。之前学的都是十多年前的固定管线,所以对原理一知半解。而着色器、VAO、VBO、EBO不仅提升了渲染性能,而且灵活易变。