计算机动画实验(一)OpenGL实现关键帧动画

实验题目来自2021年春季学期山东大学软件学院计算机动画基础课程
本人比较菜,代码有很多bug以及莫名其妙的地方,发在这记录一下写代码的艰辛😭,仅供参考思路哦!
现在代码已经找不到了,请不要找我要文件啦!(当然,欢迎指正)

  • 使用glfw,glad库,C++编写,参考LearnOpenGL

实验题目

通过关键帧插值动画技术展现出椭圆形嘴巴变化成长方形口罩的过程,同时体现出颜色的变化

思路

分别定义一个椭圆和一个长方形的顶点数组,用于形状插值。分别加载嘴巴(接近椭圆形)和口罩(接近长方形)的纹理图片,其位置坐标即分别为椭圆和长方形的位置坐标,纹理坐标用其位置坐标稍加计算修改即可。

实现效果

变变变

步骤

  1. 定义顶点数组分别绘制椭圆和长方形。按照对称的一上一下顶点绘制,这种顺序可以直接用于绘制三角形面片,在定义ebo时稍加改动即可。

    void drawEllipse() {
        float s = 3 * 2 * 2 * a / points; //distance
        float x, y;
        for (int t = 0,t1 = 0; t <= points + 6; t+=6 ,t1++) {  //total (points/3) points to draw the ellipse!!!
            x = -a + s * t1;
            y = elipY(x);
    
            elip[t] = x;
        	elip[t + 1] = y;
        	elip[t + 2] = 0;
    
      	   elip[t + 3] = x;
           elip[t + 4] = -y;
           elip[t + 5] = 0; 
         }
     }
    void drawSquare() {
    float n = points / (2 * 3); //number of points of one long edge
    
    float s = 2 * le / n; //distance
    float x;
    for (int t = 0, t2 = 0; t <= points + 6; t += 6 , t2++) {
        x= -le + t2 * s;
    
        sqre[t] = x;
        sqre[t + 1] = se;
        sqre[t + 2] = 0;
    
        sqre[t + 3] = x;
        sqre[t + 4] = -se;
        sqre[t + 5] = 0;
        }
    }
    
  2. 建立ebo,设置顶点绘制三角形的顺序。

    unsigned int indices[999];
    for (int j = 0; j < points-3; j++) {
        indices[j * 3] = j;
        indices[j * 3 + 1] = j + 1;
        indices[j * 3 + 2] = j + 2;
    }
    
  3. 建立vao、vbo,椭圆和长方形的顶点数组分别绑定到不同的vbo(用不同的position值区分),并将两者绑定到同一个vao上,同时绑定ebo。

    unsigned int VBOs[2], VAO, EBO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(2, VBOs);
    glGenBuffers(1, &EBO);
    
    glBindVertexArray(VAO);
    
    glBindBuffer(GL_ARRAY_BUFFER, VBOs[0]);
    glBufferData(GL_ARRAY_BUFFER, sizeof(elip), elip, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    
  4. 加载嘴和口罩的纹理图片,设置环绕方式,并将其与着色器绑定。

    unsigned int texture1, texture2;
    // texture 1
    glGenTextures(1, &texture1);
    glBindTexture(GL_TEXTURE_2D, texture1);
    // set the texture wrapping parameters
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);	// set texture wrapping to GL_REPEAT (default wrapping method)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    // set texture filtering parameters
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    // load image, create texture and generate mipmaps
    int width, height, nrChannels;
    stbi_set_flip_vertically_on_load(true);     unsigned char* data = 	stbi_load("mouth5.png", &width, &height, &nrChannels, 0);
    if (data)
    {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
    	glGenerateMipmap(GL_TEXTURE_2D);
    }
    else
    {
    	std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data);
    
  5. 在渲染循环中改变插值变量t的值,并传给着色器程序,实现渐变。

    t += delt;
    std::cout << "t++ ";
    Sleep(30);
    if (t > 1.0 || t < 0) {	//draw back and forth.
     	std::cout << "delt change ";
      	delt = -delt;
    }
    // set the value
    ourShader.use();
    ourShader.setFloat("t", t);
        ```
        
    
  6. 在顶点着色器中实现椭圆和长方形位置插值。并将二者原输入的位置坐标稍加修改,作为嘴和口罩的纹理坐标输出到片段着色器。

    float nx=(1-t)*aPos.x+t*aPos1.x;
    float ny=(1-t)*aPos.y+t*aPos1.y;
    
    gl_Position = vec4(nx, ny, 0.0f, 1.0f);
    
    mouthTex=vec2(aPos.x*0.85+0.5, aPos.y*1.9+0.5);
    maskTex=vec2(aPos1.x*0.65+0.503, aPos1.y*0.75+0.5);
    
  7. 在片段着色器中接收纹理,并使用内建的mix函数实现两个纹理的插值。其中可以使用之前对位置进行插值的插值变量t改变两者混合的程度。

    FragColor = mix(texture(texture1, mouthTex), texture(texture2, maskTex), tf);
    

结论分析与体会

  1. 学习动画切记不可纸上谈兵,要将代码落到实处!

  2. glfw库与glut库不同,很多东西需要自己手动实现。比如绘制椭圆和长方形,需要确定顶点的组织方式,并逐个定义三角形面片的绘制顺序,需要格外注意,同时这也涉及到了新的概念vao、vbo和ebo。

  3. 刚刚接触到vao、vbo和ebo的概念比较难以理解,其定义、绑定等函数也比较复杂。多次尝试后发现其实其含义可以简单理解为下图,而对于定义、绑定和使用,这三者基本相似,可以总结记忆。
    vao、vbo的使用

    // ..:: 初始化代码 :: ..
    // 1. 绑定顶点数组对象
    glBindVertexArray(VAO);
    // 2. 把我们的顶点数组复制到一个顶点缓冲中,供OpenGL使用
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    // 3. 复制我们的索引数组到一个索引缓冲中,供OpenGL使用
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
    // 4. 设定顶点属性指针
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), 	(void*)0);
    glEnableVertexAttribArray(0);
    [...]
    // ..:: 绘制代码(渲染循环中) :: ..
    glUseProgram(shaderProgram);
    glBindVertexArray(VAO);
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
    glBindVertexArray(0);
    
  4. 对于在vao中绑定的多个vbo,可以在顶点着色器中用position值区分。

  5. 在顶点着色器中,要处理的关键问题就是插值顶点的位置。在主函数中只要在每次绘制时插值变量增加一定的增量,将其传给顶点着色器,并注意来回绘制时将增量设置为负即可,在顶点着色器中直接使用uniform变量就可以用插值公式计算。

  6. 在片段着色器中,要处理的关键问题就是处理纹理图像,在这里只需通过纹理坐标,用内建的mix函数将其混合即可。因为椭圆和长方形的原位置坐标不会改变,所以二者对应的纹理坐标也不会改变。而因为顶点着色器对输入的每一个顶点位置进行了插值,即顶点位置发生改变,对应的纹理坐标不变,所以可以实现随着顶点位置的改变“扭曲”原纹理,插值逼近新纹理图像。

一点不重要的

  • 非常感谢某位同学一直以来的帮助(希望TA不要看见我的垃圾代码)
  • 第一个实验真的很难下手,一开始没有思路,写了很久
  • 但是写完第一个之后感觉茅塞顿开,后面的实验就没那么复杂,至少没那么无从下手了!大家加油!😉😉😉
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值