目录

  • 前言
  • 要求
  • 场景概览
  • 机器人层级模型
  • 为立方体部件贴纹理
  • 关键帧动画
  • 关键帧动画循环
  • 体素建模
  • 场景布局
  • 添加光影特效
  • 延迟渲染管线
  • 立方体贴图
  • 环境映射
  • Phong光照
  • 阴影映射
  • 体积光
  • debug着色器

前言

这次大作业算是做的比较认真的了,记录一下。全文几乎 完全参考 Learn OpenGL 的教程,感谢大佬 Orz

要求

学生可以通过层级建模( 实验补充1和2)的方式建立多个虚拟物体,由多个虚拟物体组成一个虚拟场景,要求在程序中显示该虚拟场景,场景可以是室内或者室外场景;场景应包含地面。

  1. 场景设计和显示

  2. 添加纹理

  3. 添加光照、材质、阴影效果

  4. 用户交互实现视角切换完成对场景的任意角度浏览

  5. 通过交互控制物体

场景概览

这是一个我十分喜欢的场景,出自游戏守望先锋的 CG 电影《最后的堡垒》。在智械危机大战之后,沉睡的战争机器 “堡垒”,在艾兴瓦尔德旁的原始森林中苏醒……

该场景分为四个部分:

  1. 树木
  2. 机器人
  3. 地面
  4. 环境(比如远景和天空)

其中树木和地面我们使用obj文件+纹理的方式进行渲染,因为这些物体是静态的。而环境我们则使用立方体贴图(cubeMap)来进行绘制。

而机器人我们使用层级建模的方式来描述其每一个组件。层级建模分为 3 层,第一层是身体层,我们将所有肢体都附着到身体上。第二层是主肢体层,它包括了头,大腿,大臂,和机器人机枪炮台。最后第三层是次肢体层,它包括了脚和手,机器人机枪枪管。下面是我们机器人的概览图:

机器人层级模型

机器人的所有肢体均采用立方体组成,一个立方体对应一个 TriMesh 对象。我们定义一个 Robot 类,其中包含一个 map 以根据名字,快速查询对应的组件。

我们定义如下的几个组件名称:body, head, back, gun, left_arm, right_arm, left_hand, right_hand, left_leg, right_leg, left_foot, right_foot

此外,在构造函数中,加入对应的组件。下面以加入head组件为例:

注:这里我大改了 TriMesh 和 MeshPainter 的实现。在 TriMesh 中添加 bindData 方法以单独绑定数据,实现模型和着色器对象分离。

texture_path 会在 TriMesh 的 bindData 中被利用为纹理贴图路径从而进行纹理的加载。而 rotatePoint 则是部件的旋转点,用以描述部件的旋转轴。

为立方体部件贴纹理

我们通过手动指定纹理坐标的方式,为立方体 TriMesh 的每一个面片贴上对应的纹理。我们将一个立方体的纹理描述为 6 张正方形图片的拼接,于是我们用一张图就可以描述立方体的 6 个面。以机枪炮台组件为例:

我们改动 TriMesh 类的 generateCube 函数,手动绑定 36 个顶点的纹理坐标(这里列出部分):

因为一个一个贴实在是太累人了,我给出我实现的一种贴图方案:

// 立方体生成12个三角形的顶点索引
void TriMesh::generateCube(vec3 _color, vec3 _scale)
{// 创建顶点前要先把那些vector清空cleanData();for (int i = 0; i < 8; i++){vertex_positions.push_back(cube_vertices[i] * _scale);if (_color[0] == -1){vertex_colors.push_back(basic_colors[i]);}else{vertex_colors.push_back( _color );}}// 每个三角面片的顶点下标// 每个三角面片的顶点下标faces.push_back(vec3i(0, 3, 1));faces.push_back(vec3i(0, 2, 3));faces.push_back(vec3i(1, 5, 4));faces.push_back(vec3i(1, 4, 0));faces.push_back(vec3i(4, 2, 0));faces.push_back(vec3i(4, 6, 2));faces.push_back(vec3i(5, 6, 4));faces.push_back(vec3i(5, 7, 6));faces.push_back(vec3i(2, 6, 7));faces.push_back(vec3i(2, 7, 3));faces.push_back(vec3i(1, 7, 5));faces.push_back(vec3i(1, 3, 7));// 颜色下标,让一个面的颜色都一样for (int i = 0; i < 6; i++) {color_index.push_back(vec3i(i, i, i));color_index.push_back(vec3i(i, i, i));}texture_index = faces;normal_index = faces;storeFacesPoints();textures.clear();// 顶点纹理坐标,只是自己想的一种贴图方式而已// 031textures.push_back(vec2(0.25, 0.25));textures.push_back(vec2(0.5, 0.5));textures.push_back(vec2(0.5, 0.25));// 023textures.push_back(vec2(0.25, 0.25));textures.push_back(vec2(0.25, 0.5));textures.push_back(vec2(0.5, 0.5));// 154textures.push_back(vec2(0.5, 0.25));textures.push_back(vec2(0.5, 0.0));textures.push_back(vec2(0.25, 0.0));// 140textures.push_back(vec2(0.5, 0.25));textures.push_back(vec2(0.25, 0.0));textures.push_back(vec2(0.25, 0.25));// 420textures.push_back(vec2(0.0, 0.25));textures.push_back(vec2(0.25, 0.5));textures.push_back(vec2(0.25, 0.25));// 462textures.push_back(vec2(0.0, 0.25));textures.push_back(vec2(0.0, 0.5));textures.push_back(vec2(0.25, 0.5));// 564textures.push_back(vec2(0.75, 0.25));textures.push_back(vec2(1.0, 0.5));textures.push_back(vec2(1.0, 0.25));// 576textures.push_back(vec2(0.75, 0.25));textures.push_back(vec2(0.75, 0.5));textures.push_back(vec2(1.0, 0.5));// 267textures.push_back(vec2(0.25, 0.5));textures.push_back(vec2(0.25, 0.75));textures.push_back(vec2(0.5, 0.75));// 273textures.push_back(vec2(0.25, 0.5));textures.push_back(vec2(0.5, 0.75));textures.push_back(vec2(0.5, 0.5));// 175textures.push_back(vec2(0.5, 0.25));textures.push_back(vec2(0.75, 0.5));textures.push_back(vec2(0.75, 0.25));// 137textures.push_back(vec2(0.5, 0.25));textures.push_back(vec2(0.5, 0.5));textures.push_back(vec2(0.75, 0.5));normals.clear();// 正方形的法向量不能靠之前顶点法向量的方法直接计算,因为每个四边形平面是正交的,不是连续曲面for (int i = 0; i < faces.size(); i++){normals.push_back( face_normals[i] );normals.push_back( face_normals[i] );normals.push_back( face_normals[i] );}
}

关键帧动画

我们通过关键帧的形式,让机器人动起来。关键帧代表了一个动作的关键点,以长方体绕点转动为例,我们只需要两个参数就可以描述这个运动,即:

  1. 起始状态旋转角度
  2. 结束状态旋转角度

我们根据时间,在两个关键帧之间进行插值,即可得到当前时间下的旋转角:

比如有一个动作,持续时间是 1s,起始旋转角是 30,结束旋转角是 120,当前时间是 0.5s,那么我们可以得到当前的旋转角是 (120+30)*0.5 = 75°

我们定义机器人的关键帧状态:一个关键帧即为一组关节状态,这些状态描述了该时刻各个关节的旋转,平移,缩放参数。我们手动在 Robot 类内部指定关键帧,通过 map 寻址。以定义名为 stay 的关键帧为例,我们使用二重 map 进行定义:


注:
这里我们定义的缩放参数并没有作用。因为这些缩放是不等轴的!

对于不等轴的缩放,我们在将其法线变换到世界空间下时,不能够使用模型矩阵,而是应该使用模型矩阵的逆矩阵的转置。

严格来说助教师兄师姐的模板代码,在该情况下会得到一个错误的法线方向,但是在大多数时候都是正确的,因为等轴缩放时,模型矩阵是一个正交矩阵,其逆矩阵等于转置。我们直接使用模型矩阵对法线进行变换即可!

我们的angle.h又没有计算逆矩阵的方法。于是为了解决缩放的问题,我们在generateCube的时候,就应该在c++中直接进行缩放!即让cpu将这些顶点进行缩放,从而解决缩放后法向量错误的问题。。。

重回到关键帧动画问题上,对于一个关键帧动画的播放,我们必须指定三个参数:

  1. 起始关键帧
  2. 结束关键帧
  3. 持续时间

我们在 Robot 类中,利用 timer 记录当前关键帧播放的进度,当 timer 减小到 0 之后,我们认为动画播放完成。

然后我们编写 playMotion 函数,将机器人的动作在两个关键帧之间,根据时间进度进行插值。其中 setMotion 是将机器人的状态调整到当前关键帧 currentMotion:


然后我们定义一个函数 changeState,控制机器人的变形动作。我们指定两个关键帧,分别是 drive 和 stay,表示变形状态和人形状态:

我们为键盘的 p 按键绑定该事件,并且执行 robot 的 changeState 方法,即可实现机器人的变形动画。我们让机器人在站立和变形之间切换:

关键帧动画循环

我们仅实现了关键帧动画的播放,我们还需要循环播放关键帧动画,以使得机器人完成走路的动作。参照列表循环的原则,我们建立关键帧列表,不断循环播放关键帧列表里面的动画:

同时修改我们的 playMotion 更新函数


最后我们在键盘回调函数中,通过按键判断来进行播放循环动画。我们指定两个关键帧,分别是 run1 和 run2,对应跑步动画的两个关键帧:

如图,我们实现了简单跑步动画的播放,下面是两个关键帧的详情:

体素建模

使用 MagicaVoxel 软件进行体素建模,并且导出结果到 obj 文件,方便我们读取。我们建立两颗不同的树的模型,并且导出对应的 obj 文件:

场景布局

我们生成一个正方形平面,并且为其贴上草地的纹理,这就是我们的地面了。


而树是重复的,我们无需建立多个 TriMesh 对象,相反地,我们创建两个 TriMesh 即可,我们通过改变其位移 + 多次调用 draw call 的方式实现树木的重复绘制:


添加光影特效

注意到我们的场景十分单调:

我们需要为其添加一些光影特效。这里我简单的实现了如下的渲染效果:

  1. 延迟渲染管线
  2. phong光照
  3. 立方体贴图
  4. 环境映射
  5. 阴影映射
  6. 体积光

在正式开始为场景添加特效之前,我们必须实现一些比较规范的东西。

延迟渲染管线

我们的所有实验都是使用前向渲染,但是大作业我打算实现一个简单的延迟渲染管线。延迟渲染管线能够有效的减少片元着色器的开销,因为我们无需对那些被遮挡的像素运行片段着色器!

我们的延迟渲染管线分为三个阶段:

  1. shadowMap阶段
  2. gbuffer阶段
  3. 后处理阶段

在 shadowMap 阶段我们从光源方向进行一次渲染,获取光源方向的场景的深度图 shadowTexture。

在 gbuffer 阶段,我们只渲染必要的信息,比如颜色,法线,世界坐标和场景深度。我们把这些信息存储到帧缓冲的多个颜色(和深度)附件中,他们分别是由两个帧缓冲和 3 个颜色附件,2 个深度附件组成:


在后处理阶段,我们利用 gbuffer 阶段和 shadowMap 阶段绘制的帧缓冲信息(就是那5张纹理的数据),对最终输出的片元进行计算,比如光照或者是阴影等开销比较大的特效。

下图描述了我的简易延迟渲染管线及其三个阶段之间的缓冲区与顺序关系:


我们使用 5 组着色器进行绘制:

其中 shadow 着色器负责从光源视角渲染深度纹理,而 skybox 和 gbuffer 负责生成 gbuffer 阶段的颜色,法线,世界坐标,深度纹理。其中 skybox 是天空盒专用绘制着色器。composite 着色器负责最终的特效绘制,而 debug 着色器负责输出 5 张纹理的内容,方便我改 bug。

值得注意的是,composite 和 debug 着色器的绘制对象都是一个正方形,它铺满了整个屏幕,我们只是把 gbuffer 的纹理数据取出来并且贴上去而已。

从 shadowMap 阶段开始,我们首先创建光源方向上的阴影贴图:

在 display 中,我们调用一次 draw call 以完成光源方向的绘制:


gbuffer 阶段也是类似,首先我们创建纹理。我们的纹理都是 RGBA32 格式,这样不容易发生截断或者是溢出的异常情况:


然后我们如法炮制进行绘制即可

gbuffer 阶段的着色器也十分简单。我们根据传入的数据,将片元数据输出到对应的纹理即可。下面是 gbuffer 顶点着色器:


片元着色器也是一样的,注意这里我们将反射系数存入 w 分量:

值得注意的是,gl_FragData[] 数组指向的正是我们在init中调用的附件纹理的绘制顺序:

注:这里我们修改了 MeshPainter,一个 TriMesh 在绘制的时候,传递它自己的模型矩阵和纹理,我们将模型矩阵和纹理(也包括一些其他的OpenGL对象,比如vao,vbo等)视为 TriMesh 自己的成员变量:

后处理阶段则稍微简单,我们绘制一个正方形,然后把纹理贴上去即可:

这里就显示出延迟渲染管线的优势:不管场景多么复杂,片元数目都是屏幕分辨率。这意味着片元着色器被更少的执行。

随后我们传递对应的 5 个纹理进去,并且执行 draw call 即可:

我们在 composite 的片段着色器中,直接采样 gbuffer 阶段传递的颜色纹理的值,即可输出我们 gbuffer 阶段绘制的基本画面:

立方体贴图

我们直接输出 gbuffer 阶段绘制的颜色纹理,我们很快发现这个场景十分单调,天空是黑色的。于是我们准备添加天空。这意味着天空的绘制也发生在 gbuffer 阶段。

我们决定使用立方体贴图来贴上天空与环境。立方体贴图本质上是利用一个长宽高为 2 的立方体包住我们的摄像机,然后将黑色的背景改写为立方体贴图的颜色:

如图这是一张立方体贴图,它由 6 张图片组成,我们将把它贴到一个立方体上,以包围我们的相机:

我们编写 liadCubeMap 函数以快速加载我们的立方体贴图。我们以 GL_TEXTURE_CUBE_MAP 的形式加载该纹理:


随后我们创建并且加载立方体贴图:


然后我们在绘制物体的同时,利用 skybox.fsh 和 skybox.vsh 两个着色器,对立方体贴图进行渲染。这里我们将立方体贴图的位置设置到相机的 eye 位置。此外,我们关闭深度测试以在背景处绘制天空贴图:


我们编写 skybox 着色器,下面是顶点着色器:

片段着色器则更加简单,我们直接利用顶点坐标取立方体贴图颜色即可:

注:这里我们对世界坐标缓冲直接输出非常远的距离(比如1000),法线缓冲随意。同时我们往w坐标里面输出一个-1以标记天空。

现在我们应该能够在 gbuffer 的颜色缓冲中,查看到立方体贴图的绘制:


gbuffer 阶段的绘制到此结束。后面的都是后处理阶段的绘制,并且发生在 composite 着色器中进行。所需要的所有信息,都存储在 gbuffer 和 shadowMap 阶段 pass 过来的 5 张纹理中:

环境映射

机器人身上的金属部件会反光。我们需要收集其反射的颜色,收集的方法也很简单,根据视线方向和法线,计算反射光线方向,并且到环境立方图里面取值即可。我们编写函数,根据片元的世界坐标和法向量,取天空盒的颜色:

然后在 main 函数中,我们根据反射率,对原像素进行混色即可:

Phong光照

我们带入 phong 光照模型的公式计算光照的分量。我们以引用的形式返回数据。此外,因为我们没有材质数据,我们默认三个光分量的材质都是 1.0 即可:

阴影映射

相比于使用投影矩阵,我们使用更加通用的阴影映射方法进行阴影绘制。我们利用 shadowMap 阶段绘制的深度纹理,和光源坐标系的变换矩阵 shadowVP 即可实现绘制。我们通过比较采样最近深度和当前深度,以判断点是否在阴影中。编写 shadowMapping 函数以实现阴影的绘制。返回值为 1 则表示在阴影中。

我们将 phong 光照和阴影结合。在有阴影的地方,我们只绘制环境光,其他地方,我们直接绘制所有的 phong 光照:

我们可以看到,在阳光之下的地方和阴影的明显区别:

体积光

体积光用于模拟丁达尔效应,可以大大提升场景的美观程度。

体积光是一个后处理阶段的特效,利用光线追踪方法,从相机视角出发向世界空间投射光线并且沿途记录信息,直到发生碰撞或者达到最大迭代次数。如果当前点不在阴影之中,那么我们累积颜色,否则我们不做处理,光线继续前进:


为了记录世界空间下一点是否和实体发生碰撞,我们需要利用深度缓冲的数据。我们将世界空间的位置,通过视图,投影,视口变换,转换到屏幕坐标系,然后查询深度缓冲中的数据并且进行比对即可,该过程和阴影映射类似。我们编写两个辅助函数,他们分别是屏幕深度转线性深度,和碰撞测试函数:


注:这里因为我们相机的 zFar 高达 100,如果直接读取深度缓冲中的数据,那么会是一片全白,因为他们的数值几乎非常接近 1,而且我们的硬件比较远远达不到精度要求。我们要做一次透视投影的逆变换,将深度重新映射回 0~1 的区间,以方便 FPU 进行比较

然后我们正式开始编写光线行进过程。我们从相机原点出发,沿途积累亮度直到碰撞或者达到最大迭代次数。当当前采样点不在阴影中时,我们积累亮度,表示碰到体积光。否则我们不做处理:


紧接着我们直接将获取的颜色附加到最终的输出:


可以看到最终效果还可以

debug着色器

debug 着色器负责输出 gbuffer 阶段绘制的纹理。因为要可视化深度缓冲,我们还是得转线性深度。此外我通过一个 uniform 变量名叫 mode 来控制 debug 模式:

注:因为光源方向的深度缓冲用的是正交投影,所以深度不用线性化。

和后处理阶段一致,我们直接绘制一张四方形,然后将纹理贴上去即可:

在场景中我们绑定按键事件:按下 y 即可呼出调试界面,按下 u 可以切换调试模式。调试模式使用 viewport,开了一个小窗口在左下角:

深大计算机图形学大作业之虚拟场景建模相关推荐

  1. 【计算机图形学】期末大作业_虚拟场景建模

    文章目录 实验内容 实验步骤 1.场景设计和显示 2.添加纹理 3.添加光照.材质.阴影效果 4.用户交互实现视角切换完成对场景的任意角度浏览 5.通过交互控制物体 *6.补充说明 实验效果 场景效果 ...

  2. 深圳大学计算机图形学实验4.1,深圳大学计算机图形学大作业实验报告.doc

    深圳大学计算机图形学大作业实验报告 深 圳 大 学 实 验 报 告 实验课程名称: 计算机图形学 实验项目名称: 计算机期末综合大作业 学院: 计算机与软件 专业: 计算机科学与技术 报告人: 班级: ...

  3. 计算机图形学考研学校深圳大学,厉害了!深大计算机图形学在亚洲排第三!

    深大计算机图形学排名情况 记者3月25日从深圳大学获悉,根据刚刚发布的CSRankings全球计算机科学榜单2016-2019区间排名显示,深圳大学"计算机图形学"名列亚洲第3,全 ...

  4. 计算机图形学大几学的,计算机图形学大作业-WenhaoYu.PDF

    计算机图形学大作业-WenhaoYu 计算机图形学 大作业 5090379126 虞文豪 题目:通过光照及纹理映射,实现高度真实感青花瓷材质的 茶壶向茶杯倒水动画特效. 一. 概述 之前的作业用使用S ...

  5. 用计算机图形学编程实现茶壶真实感效果,计算机图形学大作业.pdf

    计算机图形学 大作业 5090379126 虞文豪 题目:通过光照及纹理映射,实现高度真实感青花瓷材质的 茶壶向茶杯倒水动画特效. 一. 概述 之前的作业用使用SPH 方法做了一个水流的模拟程序,这次 ...

  6. 计算机图形学期末作业

    作业要求: 期末大作业 虚拟场景建模 一.作业内容 在屏幕上显示一个包含多个虚拟物体的虚拟场景,并且响应一定的用户交互操作. 具体内容包括: 场景设计和显示 学生可以通过层级建模( 实验补充1和2)的 ...

  7. 南邮 | 计算机图形学大作业:Skybox + Shadow volume

    计算机图形学期末大作业:实现 Skybox 天空盒,以及 Shadow volume 阴影体. 写在前面 本人才疏学浅,水平有限,只实现了 Skybox ,Shadow volume 没有完全实现(我 ...

  8. 计算机图形学在线作业,电子科大16秋《计算机图形学》在线作业3答案

    一.单选题(共 10 道试题,共 50 分.) 1.  在下列有关曲线和曲面概念的叙述语句中,错误的论述为____. . 实体模型和曲面造型是系统中常用的主要造型方法,曲面造型是用参数曲面描述来表示一 ...

  9. 计算机图形学在线作业,电子科大16秋《计算机图形学》在线作业1答案

    16秋<计算机图形学>在线作业1 一.单选题(共 10 道试题,共 50 分.) 1.  下列叙述中错误的是____. . 计算机图形处理中,除了应用到各种算法外,还经常会处理大量的图形方 ...

最新文章

  1. java 跳转 oop_Java基础之:OOP——接口
  2. 收藏 | 应对程序员面试,你必须知道的8大数据结构
  3. Web 上一页下一页 用超链接 用按钮
  4. win7删除计算机库文件,win7怎么将“库”从资源管理器导航栏中删除【图文】
  5. 服务程序增加系统托盘
  6. 转载---SQL Server XML基础学习2之--FOR XML AUTO/RAW
  7. 在Ubuntu上以精灵进程daemon process的方式启动一个命令
  8. 简述springmvc过程_spring mvc的工作流程是什么?
  9. Java反序列化漏洞研究
  10. 51CTO独家:2008下半年软考所有科目试题已到
  11. leetcode中文版python,Python 版 LeetCode 刷题笔记 #7 整数反转
  12. Android PackageInstaller:安装应用的应用,从三流Android外包到秒杀阿里P7
  13. 集群容错机制:failover、failfast、failback、failsafe、forking
  14. 【渝粤教育】广东开放大学 开放教育 学生创业案例 形成性考核 (59)
  15. 瑞幸的野望,小鹿茶的突袭
  16. JavaWeb之HTML标签
  17. three.js例子
  18. 关于Docker入门的一些事(4)
  19. 图、邻接矩阵、广度与深度优先、生成树
  20. Linux终端和Line discipline图解

热门文章

  1. 微信商城 开发的准备工作
  2. 【C语言】C语言实现按照考试成绩的等级输出百分制分数段
  3. Python 送你一棵圣诞树
  4. 和刘备相关的人(九 )
  5. 三顾讯时--对讯时新闻发布系统的艰难突破
  6. python自学第七天之字典的增删改查
  7. 渔港股份创业板IPO上会被否:审核被终止,曾计划募资3.3亿元
  8. 第二章 zio 入门
  9. python中求差的函数_python3函数取差是什么?如何写代码?
  10. NLP学习笔记 01 分词、词性标注和关键词提取