加载中...
返回

【图形学学习笔记】【OpenGL】旋转法实现环面

许久未在博客写技术向的文章了,虽说此篇也算不得非常技术,好歹沾点边,免得这博客成为个人荒废的见证。

按照《计算机图形学编程(使用OpenGL和C++)》这本书对OpenGL的学习终于触及了一些难啃的部分,这一周前几天花了点时间来领会球体的实现,今日又花了不少时间来领会环面,颇觉得环面的实现还是比球体难一些。值得一记。

一些背景

  1. 这本书的坐标系都是右手坐标系,z轴指向屏幕外
          y轴  
           |  
           |  
           O——→ x轴(右)  
          /  
         /  
        z轴(指向屏幕外)
  1. 环面的内径和外径的定义如下图

  1. 总体思路是微积分,把环面视为一个位于xOy平面上的圆绕y轴旋转出来的,每次旋转微小的角度,形成环面。

实现

教程上的方法是基于旋转,悟到之后是不算难。

对于 Torus 类,提供一些常规的方法,供主函数使用:

#include <cmath>
#include <glm/glm.hpp>
#include <vector>

class Torus
{
public:
    /**
     * @brief Construct a new Torus object
     * @param outerRadius Outer radius of the torus
     * @param innerRadius Inner radius of the torus
     * @param precision Precision of the torus
     */
    Torus(float outerRadius, float innerRadius, uint16_t precision);

    size_t GetIndicesCount() const;
    size_t GetVerticesCount() const;

    /** @brief Get the indices of the torus */
    std::vector<uint16_t> GetIndices() const;

    /** @brief Get the normals of the torus */
    std::vector<glm::vec3> GetNormals() const;

    /** @brief Get the texture coordinates of the torus */
    std::vector<glm::vec2> GetTexCoords() const;

    /** @brief Get the vertices of the torus */
    std::vector<glm::vec3> GetVertices() const;

    /** @brief Get the tangents of the torus (direction: y axis) */
    std::vector<glm::vec3> GetSTangents() const;

    /** @brief Get the tangents of the torus (direction: z axis) */
    std::vector<glm::vec3> GetTTangents() const;

private:
    uint16_t mPrecision{0U};
    float mOuterRadius{.0f};
    float mInnerRadius{.0f};

    std::vector<uint16_t> mIndices;
    std::vector<glm::vec3> mNormals;
    std::vector<glm::vec2> mTexCoords;
    std::vector<glm::vec3> mVertices;
    std::vector<glm::vec3> mSTangents;
    std::vector<glm::vec3> mTTangents;

private:
    void Build();
};

旋转法构造环面的思路:

  1. 构建第0个环,位于XOY平面,它是由位于原点的圆平移内径距离得到的。原点上的圆的采样点本身又由 (outerRadius,0,0) 绕z轴旋转得到。因此,对于精度 mPrecision 的环,第 i 个顶点就是 (outerRadius,0,0) 先绕z轴旋转 angle 、再向外平移 innerRadius 得到,其中angle 就是 i * 2π/mPrecision

  1. 切向量随便找两条,按照教程上采用旋转后的y轴和z轴,相乘得到法向量。目前这些向量没啥用。

  2. 对于纹理,这里的思路是把纹理水平伸展贴到环面上,类似于把一张长条形的包装纸贴到甜甜圈上,其中用包装纸短边包裹柱体。因此,对于XOY平面的纹理,只有y坐标需要赋值(根据采样精度来取点),x坐标是 0

综上,第一个环如下构造:

for (uint16_t i = 0; i <= mPrecision; ++i)
{
    float angle = 2.0f * glm::pi<float>() * i / mPrecision;
    glm::mat4 rMat = glm::rotate(glm::mat4(1.0f), angle, glm::vec3(0.0f, 0.0f, 1.0f)); // 沿z轴转
    glm::vec3 point = rMat * glm::vec4(mOuterRadius, 0.0f, 0.0f, 1.0f);
    mVertices.emplace_back(point + glm::vec3(mInnerRadius, 0.0f, 0.0f)); // 旋转后平移,得出XOY平面上的圆的采样点

    // 纹理坐标
    mTexCoords.emplace_back(0.0, static_cast<float>(i) / mPrecision);

    // 切向量
    mTTangents.emplace_back(rMat * glm::vec4(0.0f, -1.0f, 0.0f, 1.0f));
    mSTangents.emplace_back(0.0f, 0.0f, 1.0f);

    // 法向量
    mNormals.emplace_back(glm::cross(mTTangents.back(), mSTangents.back()));
}

对于第2~n个环,实际就是把第一个环沿y轴旋转,采样精度是 mPrecision 的情况下,每此旋转 2π/mPrecision 。切向量和法向量都和顶点一样直接拿第一个环的数据旋转。

纹理,要从x方向采样,因此纹理的x坐标每次增加 1/mPrecision

// 第2~n个环,沿y轴旋转第一个环得出
for (uint16_t ring = 1; ring <= mPrecision; ++ring)
{
    float theta = 2.0f * glm::pi<float>() * ring / mPrecision;
    glm::mat4 rMat = glm::rotate(glm::mat4(1.0f), theta, glm::vec3(0.0f, 1.0f, 0.0f));
    // 第 ring 个环上第 i 个顶点
    for (uint16_t i = 0; i <= mPrecision; ++i)
    {
        // 直接旋转第0个环的顶点
        mVertices.emplace_back(rMat * glm::vec4(mVertices[i], 1.0f));

        // 纹理水平展开
        mTexCoords.emplace_back(static_cast<float>(ring) / mPrecision, static_cast<float>(i) / mPrecision);

        // 切向量和法向量都直接旋转
        mSTangents.emplace_back(rMat * glm::vec4(mSTangents[i], 0.0f));
        mTTangents.emplace_back(rMat * glm::vec4(mTTangents[i], 0.0f));
        mNormals.emplace_back(rMat * glm::vec4(mNormals[i], 0.0f));
    }
}

顶点索引,是指第 i 个三角形的三个顶点分别取用哪些点。本书的思路是取相邻环上的四个点,构成两个三角形。

这里的竖线可以理解为某个圆环的 侧视 ,环上的一点 i 和它的邻点 i + 1

隔壁环上与点 i 处于同一水平线的一点,一定与当前的点 i 隔了 precision + 1 个点(因为每个环采样了 precision + 1 个点),因此是 i + precision + 1 和它的邻点 i + precision + 2

综上所述,按照旋转法构建的环面,核心代码如下:

void Torus::Build()
{
    // 第一个环,XOY平面,构建第 i 个顶点
    for (uint16_t i = 0; i <= mPrecision; ++i)
    {
        float angle = 2.0f * glm::pi<float>() * i / mPrecision;
        glm::mat4 rMat = glm::rotate(glm::mat4(1.0f), angle, glm::vec3(0.0f, 0.0f, 1.0f)); // 沿z轴转
        glm::vec3 point = rMat * glm::vec4(mOuterRadius, 0.0f, 0.0f, 1.0f);
        mVertices.emplace_back(point + glm::vec3(mInnerRadius, 0.0f, 0.0f)); // 旋转后平移,得出XOY平面上的圆的采样点

        // 纹理坐标
        mTexCoords.emplace_back(0.0, static_cast<float>(i) / mPrecision);

        // 切向量
        mTTangents.emplace_back(rMat * glm::vec4(0.0f, -1.0f, 0.0f, 1.0f));
        mSTangents.emplace_back(0.0f, 0.0f, 1.0f);

        // 法向量
        mNormals.emplace_back(glm::cross(mTTangents.back(), mSTangents.back()));
    }

    // 第2~n个环,沿y轴旋转第一个环得出
    for (uint16_t ring = 1; ring <= mPrecision; ++ring)
    {
        float theta = 2.0f * glm::pi<float>() * ring / mPrecision;
        glm::mat4 rMat = glm::rotate(glm::mat4(1.0f), theta, glm::vec3(0.0f, 1.0f, 0.0f));
        // 第 ring 个环上第 i 个顶点
        for (uint16_t i = 0; i <= mPrecision; ++i)
        {
            // 直接旋转第0个环的顶点
            mVertices.emplace_back(rMat * glm::vec4(mVertices[i], 1.0f));

            // 纹理水平展开
            mTexCoords.emplace_back(static_cast<float>(ring) / mPrecision, static_cast<float>(i) / mPrecision);

            // 切向量和法向量都直接旋转
            mSTangents.emplace_back(rMat * glm::vec4(mSTangents[i], 0.0f));
            mTTangents.emplace_back(rMat * glm::vec4(mTTangents[i], 0.0f));
            mNormals.emplace_back(rMat * glm::vec4(mNormals[i], 0.0f));
        }
    }

    for (uint16_t i = 0; i < mPrecision; ++i)
    {
        for (uint16_t j = 0; j <= mPrecision; ++j)
        {
            uint16_t first = i * (mPrecision + 1) + j;
            uint16_t second = first + mPrecision + 1;
            mIndices.emplace_back(first);
            mIndices.emplace_back(second);
            mIndices.emplace_back(first + 1);

            mIndices.emplace_back(second);
            mIndices.emplace_back(second + 1);
            mIndices.emplace_back(first + 1);
        }
    }
}

main.cpp 中,初始化时就把环面的顶点数据传入VBO,其中索引数据通过 GL_ELEMENT_ARRAY_BUFFER 标识。

Torus torus { 0.5f, 1.0f, 48U };

void setupVertices(void)
{
    auto vertices = torus.GetVertices();
    auto normals = torus.GetNormals();
    auto texCoords = torus.GetTexCoords();

    auto indices = torus.GetIndices();

    // 先生成并绑定VAO和VBO
    glGenVertexArrays(1, vao);
    glBindVertexArray(vao[0]);
    glGenBuffers(numVBOs, vbo);

    // 填充顶点位置数据
    {
        std::vector<float> vertexData;
        for (auto& vertex : vertices)
        {
            vertexData.push_back(vertex.x);
            vertexData.push_back(vertex.y);
            vertexData.push_back(vertex.z);
        }
        glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
        glBufferData(GL_ARRAY_BUFFER, vertexData.size() * sizeof(float), vertexData.data(), GL_STATIC_DRAW);
    }

    // 填充纹理坐标数据
    {
        std::vector<float> texCoordData;
        for (auto& texCoord : texCoords)
        {
            texCoordData.push_back(texCoord.x);
            texCoordData.push_back(texCoord.y);
        }
        glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
        glBufferData(GL_ARRAY_BUFFER, texCoordData.size() * sizeof(float), texCoordData.data(), GL_STATIC_DRAW);
    }

    // 填充法线数据
    {
        std::vector<float> normalData;
        for (auto& normal : normals)
        {
            normalData.push_back(normal.x);
            normalData.push_back(normal.y);
            normalData.push_back(normal.z);
        }
        glBindBuffer(GL_ARRAY_BUFFER, vbo[2]);
        glBufferData(GL_ARRAY_BUFFER, normalData.size() * sizeof(float), normalData.data(), GL_STATIC_DRAW);
    }

    // 填充索引
    {
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo[3]);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(uint16_t), indices.data(), GL_STATIC_DRAW);
    }
}

【TODO】数学

yeah,环面和球面一样,也是有数学表达式的,感觉会比先构造第一个环、再旋转出第2~n个环优雅一些,不过可能要再考虑一下切向量和法向量怎么求得。todo……

10 comments
Anonymous
Markdown is supported
@mpv945
mpv945commentedalmost 3 years ago

添加图片,如果使用外部图床的http链接 。图片无法点击放大,你那边怎么解决的?

@SGS4ever
SGS4evercommentedalmost 3 years ago

@mpv945
添加图片,如果使用外部图床的http链接 。图片无法点击放大,你那边怎么解决的?

我的博客没有使用图床,所以没办法帮到你~

@Celetherin
Celetherincommentedover 2 years ago

您好,我也是使用的stack主题,我在照着您的方法添加返回顶部按钮时,遇到了按钮虽然出现、也能够点击,但无法实现实际上的返回顶部功能的问题,我没有任何的代码知识,不知道您有没有解决方法?
另外,也是想提醒一下其他需要这篇教程的朋友,最新版的stack主题,添加返回按钮的组件应该在layouts/partials/sidebar/right.html, 在layouts/_default/single.html中添加代码会导致出现两个右边栏。

@jsjcjsjc
jsjcjsjccommentedover 2 years ago

请教一下博主,如何优雅的给stack主题添加广告哈?
我只想在左或者右侧边栏底部,或者每篇文章底部添加一个小小的广告,但是默认似乎的满屏广告哈~~
感谢

@SGS4ever
SGS4evercommentedover 2 years ago
@ClimbingMouse
ClimbingMousecommentedabout 2 years ago

你好,按照你的方法设置页面载入动画,这个动画不会停止咋办啊

@46fafa
46fafacommentedabout 2 years ago

博主你好,请问一下主页布局修改哪里的代码如何作用于整个网页,我发现修改后的布局只存在主页和前两篇文章,其他部分还是没修改的样子

@4kohakunushi
4kohakunushicommentedover 1 year ago

你好,关于左侧栏图标高亮我这里存在一些问题想请教你。我取消了原本主页直接抓取post的内容在中间显示的版块,这个部分改成了其他东西,与此同时新增了一个抓取post信息的与links、search等目录并列的一个目录,现在的问题是这些部分虽然都能正常显示,但是对应的抓取post的那个目录无法选中以后高亮,应该修改增加什么才能让它也可以选中后高亮呢?

@SGS4ever
SGS4evercommentedover 1 year ago

首先我只能基于本文使用的Stack版本来尝试解答,因为没看过当前的Stack主题的代码~
我重新翻了下此前写的关于高亮的内容,理论上只要你的post页面的标题在menu配置中即可高亮。如果post页面是你站点的根路径,那应该可以参考我的文章里写的方法,修改下active的触发逻辑~

@4kohakunushi
你好,关于左侧栏图标高亮我这里存在一些问题想请教你。我取消了原本主页直接抓取post的内容在中间显示的版块,这个部分改成了其他东西,与此同时新增了一个抓取post信息的与links、search等目录并列的一个目录,现在的问题是这些部分虽然都能正常显示,但是对应的抓取post的那个目录无法选中以后高亮,应该修改增加什么才能让它也可以选中后高亮呢?

@sansan-cc
sansan-cccommented3 months ago

感谢博主的建站帖子,有很大的帮助。

有朋自远方来,不亦说乎?