Qt OpenGL:学习现代3D图形编程之三,移动三角形

    这里以《画一个三角形》为基础,只介绍与《画一个三角形》不同的地方

一、移动顶点

    移动三角最简单的方法是直接改变顶点的位置数据,我们知道顶点数据存储在缓冲区对象,所以我们的任务就是改变缓冲区对象中的顶点数据。改变顶点的位置数据分为两步,第一步是生成位置相对于x和y的偏移量,第二步是将这些偏移量应用于每个顶点的位置。如函数ComputePositionOffsets所示。该函数循环计算偏移量,这些偏移围绕一个圆环运动,并且每隔5秒偏移量返回到圆环的起点。

void ComputePositionOffsets(float &fXOffset, float &fYOffset)
{
    //每隔五秒返回起点
    const float fLoopDuration = 5.0f;
    const float fScale = 3.14159f * 2.0f / fLoopDuration;
    //time.elapsed()返回程序启动到当前时间的毫秒数,除以1000转换为秒数
    float fElapsedTime = time.elapsed() / 1000.0f;
    //fmodf为取模函数,效果与fElapsedTime%fLoopDuration一样,返回[0, fLoopDuration)之间的值
    float fCurrTimeThroughLoop = fmodf(fElapsedTime, fLoopDuration);
    //sinf和cosf函数计算出一个半径为2的圆,乘以0.5将半径减小为1。
    fXOffset = cosf(fCurrTimeThroughLoop * fScale) * 0.5f;
    fYOffset = sinf(fCurrTimeThroughLoop * fScale) * 0.5f;
}

    计算出偏移量后,需要将这些偏移量添加到顶点数据,这是通过AdjustVertexData函数实现的。该函数将顶点数据复制到一个新的std::vector容器中,然后将偏移量应用到每个顶点的x坐标和y坐标。

void AdjustVertexData(float fXOffset, float fYOffset)
{
    //新建float类型的容器,宏ARRAY_COUNT用于计算顶点数组的大小
    std::vector<float> fNewData(ARRAY_COUNT(vertexPositions));
    //将每个顶点数据复制到容器
    memcpy(&fNewData[0], vertexPositions, sizeof(vertexPositions));
    //将偏移量应用到每个顶点的x坐标和y坐标
    for(int iVertex = 0; iVertex < ARRAY_COUNT(vertexPositions); iVertex += 4)
    {
        fNewData[iVertex] += fXOffset;
        fNewData[iVertex + 1] += fYOffset;
    }
    //将缓冲区对象绑定到绑定目标GL_ARRAY_GUFFER,OpenGL中的所有对象在进行操作前都需要绑定到上下文,缓冲区对象也不例外。
    glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
    //注意这里使用的是glBufferSubData,而不是glBufferData。glBufferData会给该缓冲区象分配足够的GPU内存来存储顶点数据,
    //然后该函数将我们定义的数据拷贝到缓冲区对象;而glBufferSubData不分配GPU内存,它仅仅是将我们定义的数据拷贝到缓冲区对象。
    //对已经分配GPU内存的缓冲区对象调用glBufferData将重新分配一块新的内存,并丢弃之前存储的数据。对未分配GPU内存的缓冲区对象
    //调用glBufferSubData是一种错误行为。可以将glBufferData看做是malloc和memcpy的组合,而glBufferSubData只是memcpy。
    //第一个参数是绑定目标,第二个参数是相对于缓冲区对象首地址偏移的字节数,第三个参数是要拷贝的字节数,第四个参数是要拷贝的数组。
    glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertexPositions), &fNewData[0]);
    //解除绑定
    glBindBuffer(GL_ARRAY_BUFFER, 0);
}

   最后显示到屏幕,注意与《画一个三角形》中不同的地方。

void display()
{
    float fXOffset = 0.0f, fYOffset = 0.0f;
    //计算偏移量
    ComputePositionOffsets(fXOffset, fYOffset);
    //改变顶点数据
    AdjustVertexData(fXOffset, fYOffset);
    //这里先简单介绍一下缓冲区,缓冲区存在于图形卡的显存中,OpenGL在绘制图元时,先是在一个缓冲区中完成渲染,然后再把渲染结果交换到屏幕上。
    //指定OpenGL清理屏幕是将要使用的颜色,这里为黑色。
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    //开始清理屏幕,GL_COLOR_BUFFER_BIT表示清理将影响颜色缓冲区,清理时使用上面指定的颜色。
    glClear(GL_COLOR_BUFFER_BIT);
    //告知OpenGL渲染的时候需要调用应用程序对象。
    glUseProgram(theProgram);
    //下面三行设置三角形的坐标,它们告知OpenGL三角形在缓冲区中的位置
    //获取已经初始化的缓冲区对象。
    glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
    //启用缓冲区对象中的数据,参数指定指定要修改的顶点属性的索引值
    glEnableVertexAttribArray(0);
    //尽管这个函数包含“Pointer”,但是它不处理指针,它用来对缓冲区对象中的顶点属性进行设定。
    //参数1指定指定要修改的顶点属性的索引值;参数2指定每个顶点向量的维数;参数3指定每个顶点向量的数据类型;
    //参数4指定是否归一化;参数5指定各顶点向量间是否有空隙,0表示紧密排列;参数6指定顶点数组起始位置与缓冲区
    //对象起始位置的偏移量,0表示无偏移。后三个参数通常取默认值。
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);
    //渲染函数,从顶点数组索引0开始,读取3个顶点,然后将他们连接成一个三角形。
    glDrawArrays(GL_TRIANGLES, 0, 3);
    //下面两行是清理工作,释放为了实现渲染所做的一些设置。
    glDisableVertexAttribArray(0);
    glUseProgram(0);
}
二、一种更好的方法

       通过直接改变顶点位置数据来移动三角形虽然对于三个顶点行得通,但是当场景中有成千上万的顶点时,通过这种方法意味着需要从原始顶点中拷贝成千上万次,并将偏移量添加到每个顶点。很显然有效率更高的替代方法,游戏不可能在这样做的同时还能很好的保证帧率。顶点着色器在每个顶点执行一次,很容易想到的是可以给顶点着色器提供偏移量并让它计算最终的顶点位置。

    第二行定义了一个二维向量offset,定义它时用的是uiform限定符,而不是in或者out,这有特殊的意义。着色器在每次执行的时候,会通过in限定符定义的变量来获取新的值,这对顶点位置数据非常有效,但是并不适用于偏移量,因为我们需要每个顶点使用同一偏移量,于是一个定义为uniform的偏移量应运而生。uniform变量和in变量不会同步变化,in变量在每次执行着色器时变化,uniform变量只有在执行渲染的时候才变化,此时用户需要明确给该变量设置一个新值。顶点着色器的输入来自顶点数组定义和缓冲区对象,相比之下,uniform变量通过应用程序对象直接设置。

const std::string strVertexShader(
    "#version 330\n"
    "layout(location = 0) in vec4 position;\n"
    "uniform vec2 offset;\n"
    "void main()\n"
    "{\n"
    "   vec4 totalOffset = vec4(offset.x, offset.y, 0.0, 0.0);\n"
    "   gl_Position = position+totalOffset;\n"
    "}\n"
);
   为了设置uniform变量,我们需要做两件事情,第一是获取变量索引位置,与属性类似的是每个特定的uniform变量都有一个索引位置与之对应;与属性不同的是,我们不能自己在着色器中设置索引位置,只能去访问它。glGetUniformLocation通过第二个参数取得uniform变量的索引位置,当uniform变量没有索引位置时,函数返回-1。第二是在渲染的时候使用该索引位置。索引位置在InitializeProgram函数中获取。

void InitializeProgram()
{
    //创建着色器对象列表,这些着色器对象将通过链接生成应用程序对象
    std::vector<GLuint> shaderList;
    //下面两行分别编译顶点着色器和片段着色器,着色器的编译和链接与c语言类似,也是先编译后链接。更重要的是,编译还引入了出错检查。
    shaderList.push_back(CreateShader(GL_VERTEX_SHADER, strVertexShader));
    shaderList.push_back(CreateShader(GL_FRAGMENT_SHADER, strFragmentShader));
    theProgram = CreateProgram(shaderList);
    offsetLocation = glGetUniformLocation(theProgram, "offset");
    //删除着色器对象
    std::vector<GLuint>::iterator it;
    for(it=shaderList.begin();it!=shaderList.end();it++)
        glDeleteShader(*it);
}
    一旦我们获取了uniform变量的索引位置,我们就能设置uniform变量的值,与获取变量索引位置不同的是,设置变量的值必须在glUseProgram函数调用之后。我们用glUniform2f函数来设置变量的值,缓冲区对象中的数据并没有改变,着色器做了这些复杂的工作。glUniform2f函数中的“2f”表示函数作用于浮点型的二维向量。
      最后显示到屏幕,注意与上一节不同的地方。

void display()
{
    float fXOffset = 0.0f, fYOffset = 0.0f;
    //计算偏移量
    ComputePositionOffsets<span style="font-size:10px;">(fXOffset, fYOffset);
    //这里先简单介绍一下缓冲区,缓冲区存在于图形卡的显存中,OpenGL在绘制图元时,先是在一个缓冲区中完成渲染,然后再把渲染结果交换到屏幕上。
    //指定OpenGL清理屏幕是将要使用的颜色,这里为黑色。
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    //开始清理屏幕,GL_COLOR_BUFFER_BIT表示清理将影响颜色缓冲区,清理时使用上面指定的颜色。
    glClear(GL_COLOR_BUFFER_BIT);
    //告知OpenGL渲染的时候需要调用应用程序对象。
    glUseProgram(theProgram);
    glUniform2f(offsetLocation, fXOffset, fYOffset);
    //下面三行设置三角形的坐标,它们告知OpenGL三角形在缓冲区中的位置
    //获取已经初始化的缓冲区对象。
    glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
    //启用缓冲区对象中的数据,参数指定指定要修改的顶点属性的索引值
    glEnableVertexAttribArray(0);
    //尽管这个函数包含“Pointer”,但是它不处理指针,它用来对缓冲区对象中的顶点属性进行设定。
    //参数1指定指定要修改的顶点属性的索引值;参数2指定每个顶点向量的维数;参数3指定每个顶点向量的数据类型;
    //参数4指定是否归一化;参数5指定各顶点向量间是否有空隙,0表示紧密排列;参数6指定顶点数组起始位置与缓冲区
    //对象起始位置的偏移量,0表示无偏移。后三个参数通常取默认值。
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);
    //渲染函数,从顶点数组索引0开始,读取3个顶点,然后将他们连接成一个三角形。
    glDrawArrays(GL_TRIANGLES, 0, 3);
    //下面两行是清理工作,释放为了实现渲染所做的一些设置。
    glDisableVertexAttribArray(0);
    glUseProgram(0);
}
三、着色器能做得更多

    不用手动转换每个顶点固然好,但是着色器能做得更多。下面的顶点着色器包含两个uniforms变量,位置移动循环的时间和程序启动到目前为止消耗的时间。这里调用了很多OpenGL着色器语言定义的标准函数,比如说mod,cos和sin。它们只是冰山一角,还有更多其他的标准函数可用。

const std::string strVertexShader(
    "#version 330\n"
    "layout(location = 0) in vec4 position;\n"
    "uniform float loopDuration;\n"
    "uniform float time;\n"
    "void main()\n"
    "{\n"
    "   float timeScale = 3.14159f * 2.0f / loopDuration;\n"
    "   float currTime = mod(time, loopDuration);\n"
    "   vec4 totalOffset = vec4(cos(currTime * timeScale) * 0.5f,sin(currTime * timeScale) * 0.5f,0.0f,0.0f);\n"
    "   gl_Position = position+totalOffset;\n"
    "}\n"
);
    现在通过使用顶点着色器,三角形的位置能绕着圆环移动了,那么片段着色器能否基于时间变化做一些事情呢?当然可以,片段着色器虽然不能改变顶点的位置数据,但是它能控制片段的颜色。下面的片段着色器使用uniforms变量fragLoopDuration来控制颜色循环变化的时间,在循环开始时三角形的颜色为白色,当循环结束时,颜色为绿色,其他时间点颜色为白色和绿色的线性混合。mix函数的第三个参数为混合因子,控制白色和黑色在混合时所占的比重。变量firstColor和secondColor使用了const限定符,与c++中类似,这两个变量的值必须初始化且程序运行期间不能改变。对于没有使用限定符的变量(普通变量),值可以改变。与in、out和uniform变量不同的是,const变量和普通变量不能在着色器间分享或传递。

const std::string strFragmentShader(
    "#version 330\n"
    "out vec4 outputColor;\n"
    "uniform float fragLoopDuration;\n"
    "uniform float time;\n"
    "const vec4 firstColor = vec4(1.0f, 1.0f, 1.0f, 1.0f);\n"
    "const vec4 secondColor = vec4(0.0f, 1.0f, 0.0f, 1.0f);\n"
    "void main()\n"
    "{\n"
    "   float currTime = mod(time, fragLoopDuration);\n"
    "   float currLerp = currTime / fragLoopDuration;\n"
    "   outputColor = mix(firstColor, secondColor, currLerp);\n"
    "}\n"
);
    接下来是在InitializeProgram()函数中获取uniform变量的索引位置。
void InitializeProgram()
{
    //创建着色器对象列表,这些着色器对象将通过链接生成应用程序对象
    std::vector<GLuint> shaderList;
    //下面两行分别编译顶点着色器和片段着色器,着色器的编译和链接与c语言类似,也是先编译后链接。更重要的是,编译还引入了出错检查。
    shaderList.push_back(CreateShader(GL_VERTEX_SHADER, strVertexShader));
    shaderList.push_back(CreateShader(GL_FRAGMENT_SHADER, strFragmentShader));
    theProgram = CreateProgram(shaderList);
    //通过glGetUniformLocation函数获取uniform变量的索引位置,elapsedTimeUniform定义为全局,是因为时间消耗量会实时变化,
    //赋值需要在每帧渲染过程中进行,这里只进行初始化。而loopDurationUnf(位置移动循环时间)和fragLoopDurUnf(颜色变化循环时间)
    //赋值过后不需要改变,所以将其定义为临时变量。
    //需要注意的是,顶点着色器和片段着色器中都包含uniform变量time,在这两种着色器进行链接的时候,相同名字和类型的uniform变量会
    //同一化,也就是time只存在一个索引位置,通过该索引位置可以同时调用两种着色器中的time变量。但是,当两种着色器中的uniform变量
    //名字相同,类型不同时,OpenGl就会提示链接错误。
    elapsedTimeUniform = glGetUniformLocation(theProgram, "time");
    GLuint loopDurationUnf = glGetUniformLocation(theProgram, "loopDuration");
    GLuint fragLoopDurUnf = glGetUniformLocation(theProgram, "fragLoopDuration");
    //给着色器变量赋值的基本流程:使用应用程序对象,赋值,清理应用程序对象。
    glUseProgram(theProgram);
    glUniform1f(loopDurationUnf, 5.0f);
    glUniform1f(fragLoopDurUnf, 10.0f);
    glUseProgram(0);
    //删除着色器对象
    std::vector<GLuint>::iterator it;
    for(it=shaderList.begin();it!=shaderList.end();it++)
        glDeleteShader(*it);
}
    最后调用display函数,显示到屏幕,注意与上一节不同的地方。
void display()
{
    //这里先简单介绍一下缓冲区,缓冲区存在于图形卡的显存中,OpenGL在绘制图元时,先是在一个缓冲区中完成渲染,然后再把渲染结果交换到屏幕上。
    //指定OpenGL清理屏幕是将要使用的颜色,这里为黑色。
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    //开始清理屏幕,GL_COLOR_BUFFER_BIT表示清理将影响颜色缓冲区,清理时使用上面指定的颜色。
    glClear(GL_COLOR_BUFFER_BIT);
    //告知OpenGL渲染的时候需要调用应用程序对象。
    glUseProgram(theProgram);
    //在新的顶点着色器的帮助下,这部分代码变得简单,只需要将消耗的时间传递给着色器的uniform变量。
    glUniform1f(elapsedTimeUniform, time.elapsed() / 1000.0f);
    //下面三行设置三角形的坐标,它们告知OpenGL三角形在缓冲区中的位置
    //获取已经初始化的缓冲区对象。
    glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
    //启用缓冲区对象中的数据,参数指定指定要修改的顶点属性的索引值
    glEnableVertexAttribArray(0);
    //尽管这个函数包含“Pointer”,但是它不处理指针,它用来对缓冲区对象中的顶点属性进行设定。
    //参数1指定指定要修改的顶点属性的索引值;参数2指定每个顶点向量的维数;参数3指定每个顶点向量的数据类型;
    //参数4指定是否归一化;参数5指定各顶点向量间是否有空隙,0表示紧密排列;参数6指定顶点数组起始位置与缓冲区
    //对象起始位置的偏移量,0表示无偏移。后三个参数通常取默认值。
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);
    //渲染函数,从顶点数组索引0开始,读取3个顶点,然后将他们连接成一个三角形。
    glDrawArrays(GL_TRIANGLES, 0, 3);
    //下面两行是清理工作,释放为了实现渲染所做的一些设置。
    glDisableVertexAttribArray(0);
    glUseProgram(0);
}
    渲染后的效果如下图所示。


参考链接:http://alfonse.bitbucket.org/oldtut/Positioning/Tutorial%2003.html

源码链接:见http://blog.csdn.net/caoshangpa/article/details/51042871的评论

  • 18
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
可以使用以下代码来在 Qt 中使用 OpenGL 绘制三角形: ```cpp #include <QtGui/QOpenGLFunctions> #include <QtGui/QOpenGLShaderProgram> #include <QtGui/QOpenGLVertexArrayObject> #include <QtGui/QOpenGLBuffer> class MyOpenGLWidget : public QOpenGLWidget, protected QOpenGLFunctions { public: MyOpenGLWidget(QWidget *parent = nullptr) : QOpenGLWidget(parent) {} protected: void initializeGL() override { initializeOpenGLFunctions(); // 创建着色器程序 m_program = new QOpenGLShaderProgram(this); m_program->addShaderFromSourceCode(QOpenGLShader::Vertex, R"( #version 330 core layout (location = 0) in vec3 aPos; void main() { gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); } )"); m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, R"( #version 330 core out vec4 FragColor; void main() { FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); } )"); m_program->link(); // 创建顶点数组对象和顶点缓冲对象 m_vao = new QOpenGLVertexArrayObject(this); m_vao->create(); m_vao->bind(); m_vbo = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer); m_vbo->create(); m_vbo->bind(); float vertices[] = { -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f, 0.5f, 0.0f }; m_vbo->allocate(vertices, sizeof(vertices)); // 设置顶点属性指针 m_program->bind(); m_program->enableAttributeArray(0); m_program->setAttributeBuffer(0, GL_FLOAT, 0, 3, 0); // 解绑 m_vao->release(); m_vbo->release(); m_program->release(); } void paintGL() override { glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); m_program->bind(); m_vao->bind(); glDrawArrays(GL_TRIANGLES, 0, 3); m_vao->release(); m_program->release(); } private: QOpenGLShaderProgram *m_program; QOpenGLVertexArrayObject *m_vao; QOpenGLBuffer *m_vbo; }; ``` 这段代码使用 QtOpenGL 模块来创建一个继承自 QOpenGLWidget 的自定义 OpenGL 窗口,然后在 initializeGL() 函数中初始化着色器程序、顶点数组对象和顶点缓冲对象,最后在 paintGL() 函数中绘制三角形

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

草上爬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值