OpenGL入门:实现明日方舟界面球体动画

前言

本篇介绍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=rsinθcosϕy=rcosθz=rsinθ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,成功了!效果还可以呀

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值