前言
本篇介绍OpenGL实现明日方舟界面球体动画
OpenGL基础请看这个网站:LearnOpenGL
目前我只学习了入门部分,制作的效果还比较粗糙,之后有机会会继续完善。
那么我们开始吧。
需要完成的是一个旋转的镂空球体,镂空可以使用线框模式绘制,旋转用变换矩阵或直接使用glm库实现,比较麻烦一点的是球体顶点和索引怎么获得。
旋转与镂空实现
镂空:绘制时加一句就行
// 使用线框模式绘制
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
旋转:
先导入glm头文件
#include <glm.hpp>
#include <gtc/matrix_transform.hpp>
#include <gtc/type_ptr.hpp>
绘制时传递变换矩阵给着色器
// 初始化单位矩阵
glm::mat4 transform = glm::mat4(1.0f);
// 进行旋转变换,旋转随时间持续进行,轴向量为(-1.0f, 1.0f, 0.0f)
transform = glm::rotate(transform, (float)glfwGetTime()*0.2f, glm::vec3(-1.0f, 1.0f, 0.0f));
// 获取着色器uniform变量位置
unsigned int transformLoc = glGetUniformLocation(shader.ID, "transform");
// 传递矩阵到着色器
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(transform));
球体顶点和索引
方法1
首先尝试了用经纬度计算球面顶点,球面坐标公式:
x
=
r
⋅
s
i
n
θ
⋅
c
o
s
ϕ
y
=
r
⋅
c
o
s
θ
z
=
r
⋅
s
i
n
θ
⋅
s
i
n
ϕ
\begin{equation} \begin{split} &x=r⋅sinθ⋅cosϕ\\ &y=r⋅cosθ\\ &z=r⋅sinθ⋅sinϕ \end{split} \end{equation}
x=r⋅sinθ⋅cosϕy=r⋅cosθz=r⋅sinθ⋅sinϕ
生成顶点和索引:
// 参数为:球体半径,纬度数量,经度数量,顶点数组,索引数组
void GenerateSphere(float radius, int latSegments, int lonSegments,
std::vector<float>& vertices, std::vector<unsigned int>& indices) {
const float PI = 3.14159265359f;
vertices.clear();
indices.clear();
// 生成顶点
for (int lat = 0; lat <= latSegments; lat++) {
float theta = lat * PI / latSegments; // 计算当前纬度角θ,纬度范围 [0, π]
float sinTheta = sin(theta);
float cosTheta = cos(theta);
for (int lon = 0; lon <= lonSegments; lon++) {
float phi = lon * 2 * PI / lonSegments; // 计算当前经度角φ,经度范围 [0, 2π]
float x = radius * sinTheta * cos(phi);
float y = radius * cosTheta;
float z = radius * sinTheta * sin(phi);
vertices.push_back(x);
vertices.push_back(y);
vertices.push_back(z);
}
}
// 生成索引(线框模式绘制时每2个索引画一条线段)
for (int lat = 0; lat < latSegments; lat++) {
for (int lon = 0; lon < lonSegments; lon++) {
int current = lat * (lonSegments + 1) + lon;
int next = current + lonSegments + 1;
// 纬线(水平)
indices.push_back(current);
indices.push_back(current + 1);
// 经线(垂直)
indices.push_back(current);
indices.push_back(next);
}
}
}
进行调用:
GenerateSphere(0.6f,8,16,sphereVertices,sphereIndices); //8纬度,16经度
进行绘制,图元形式选GL_LINES
glDrawElements(GL_LINES, sphereIndices.size(), GL_UNSIGNED_INT, 0);
渲染结果:
16纬度,32经度时:
可以看到确实成功渲染出了球体,但效果与目标还有挺大差距,这种方法渲染的球体主要由四边形构成而不是三角形。
那么有没有方法能做出一个由相同三角形构成的球体呢,有的,兄弟,有的,我们可以先生成一个正20面体,然后在这个多面体上递归细分变成球体。(其它正多面体也可以作为基础但效果没这么好)
方法2
暴力生成20面体:
void GenerateIcosahedron(float radius, std::vector<float>& vertices, std::vector<unsigned int>& indices) {
const float PI = 3.14159265359f;
const float GOLDEN_RATIO = (1.0f + sqrt(5.0f)) / 2.0f; // 黄金比例 φ
// 二十面体的12个顶点(归一化到球面)
std::vector<glm::vec3> baseVertices = {
glm::normalize(glm::vec3(-1, GOLDEN_RATIO, 0)),
glm::normalize(glm::vec3(1, GOLDEN_RATIO, 0)),
glm::normalize(glm::vec3(-1, -GOLDEN_RATIO, 0)),
glm::normalize(glm::vec3(1, -GOLDEN_RATIO, 0)),
glm::normalize(glm::vec3(0, -1, GOLDEN_RATIO)),
glm::normalize(glm::vec3(0, 1, GOLDEN_RATIO)),
glm::normalize(glm::vec3(0, -1, -GOLDEN_RATIO)),
glm::normalize(glm::vec3(0, 1, -GOLDEN_RATIO)),
glm::normalize(glm::vec3(GOLDEN_RATIO, 0, -1)),
glm::normalize(glm::vec3(GOLDEN_RATIO, 0, 1)),
glm::normalize(glm::vec3(-GOLDEN_RATIO, 0, -1)),
glm::normalize(glm::vec3(-GOLDEN_RATIO, 0, 1))
};
// 二十面体的20个三角形面
const std::vector<unsigned int> baseIndices = {
0, 11, 5, 0, 5, 1, 0, 1, 7, 0, 7, 10, 0, 10, 11,
1, 5, 9, 5, 11, 4, 11, 10, 2, 10, 7, 6, 7, 1, 8,
3, 9, 4, 3, 4, 2, 3, 2, 6, 3, 6, 8, 3, 8, 9,
4, 9, 5, 2, 4, 11, 6, 2, 10, 8, 6, 7, 9, 8, 1
};
// 输出顶点和索引(乘以半径)
for (const auto& vertex : baseVertices) {
vertices.push_back(vertex.x * radius);
vertices.push_back(vertex.y * radius);
vertices.push_back(vertex.z * radius);
}
indices = baseIndices;
}
进行绘制,图元形式选GL_TRIANGLES
glDrawElements(GL_TRIANGLES, sphereIndices.size(), GL_UNSIGNED_INT, 0);
12个顶点数,20个三角形数,所有三角形完全相同(等边三角形)的正20面体
对二十面体递归细分:
// 参数为:递归次数,球体半径,顶点数组,索引数组
void SubdivideIcosahedron(int subdivisions,float radius, std::vector<float>& vertices, std::vector<unsigned int>& indices) {
// 生成基础二十面体
GenerateIcosahedron(radius, vertices, indices);
// 递归细分
for (int i = 0; i < subdivisions; i++) {
std::vector<unsigned int> newIndices;
std::vector<glm::vec3> newVertices;
// 将每个三角形细分为4个小三角形
for (size_t j = 0; j < indices.size(); j += 3) {
// 获取原始三角形的三个顶点
glm::vec3 v1 = glm::vec3(vertices[indices[j] * 3], vertices[indices[j] * 3 + 1], vertices[indices[j] * 3 + 2]);
glm::vec3 v2 = glm::vec3(vertices[indices[j + 1] * 3], vertices[indices[j + 1] * 3 + 1], vertices[indices[j + 1] * 3 + 2]);
glm::vec3 v3 = glm::vec3(vertices[indices[j + 2] * 3], vertices[indices[j + 2] * 3 + 1], vertices[indices[j + 2] * 3 + 2]);
// 计算新顶点(边的中点)
glm::vec3 m1 = glm::normalize(v1 + v2) * 0.5f;
glm::vec3 m2 = glm::normalize(v2 + v3) * 0.5f;
glm::vec3 m3 = glm::normalize(v3 + v1) * 0.5f;
// 添加到新顶点列表
unsigned int baseIndex = static_cast<unsigned int>(newVertices.size());
newVertices.insert(newVertices.end(), { v1, m1, m3, v2, m2, m1, v3, m3, m2, m1, m2, m3 });
// 添加新三角形索引
newIndices.insert(newIndices.end(), {
baseIndex, baseIndex + 1, baseIndex + 2,
baseIndex + 3, baseIndex + 4, baseIndex + 5,
baseIndex + 6, baseIndex + 7, baseIndex + 8,
baseIndex + 9, baseIndex + 10, baseIndex + 11
});
}
// 更新顶点和索引
vertices.clear();
for (const auto& v : newVertices) {
vertices.push_back(v.x);
vertices.push_back(v.y);
vertices.push_back(v.z);
}
indices = newIndices;
}
}
进行调用:
SubdivideIcosahedron(1, 0.6f, sphereVertices, sphereIndices);//1次递归
渲染结果:
对,对吗…?
再来看看递归2次的:
肯定不对吧,这谁家刺球
问题应该出在有的顶点没在球面上,改进一下:
// 计算边的中点,并投影到球面
glm::vec3 m1 = glm::normalize(v1 + v2) * radius; // 归一化后乘以半径
glm::vec3 m2 = glm::normalize(v2 + v3) * radius;
glm::vec3 m3 = glm::normalize(v3 + v1) * radius;
渲染结果:
嗯嗯,不错,再看看递归一次的:
ohhhhhhhh,成功了!效果还可以呀