引言
本文是大二暑校课程《移动图形概论》的大作业报告。
OpenGLES3DWaifu实现了一个3D人物在3D场景中舞蹈的动画展示,并且加入了传统光照技术以及shadowmapping使得画面更加逼真。
项目地址:OpenGLES3DWaifu
实现效果:
本作业主要分为两个大部分:整合生成模型文件以及编写代码。
一、整合生成模型文件
PowerVR SDK提供了加载模型的api:pvr::assets::loadModel()
,方便我们解析模型中包含的meshes数据(包括顶点坐标、法向切向、骨骼数据等)、蒙皮动画、灯光、相机、相机动画、材质等信息。因此我们需要将收集到的3D人物、背景、动画文件整合生成PowerVR SDK方便读取的模型文件。这一部分我选择使用Blender来整合资源,主要有以下几个要点:
1. 模型文件格式
loadModel()
事实上仅支持POD和GLTF两种格式(截至目前最新版本v5.7),前者是PowerVR推荐格式,而后者则更应用相对广泛一些。但囿于时间,我没能成功调试出程序使其正常读取GLTF文件——通过Blender整合导出的GLTF文件在程序载入时总会在texture加载时出问题,并且也无法读出灯光数据。因此我最终选择了POD格式。
然而市面上常见的3D动画软件多不支持POD格式的导出,POD文件只能由PowerVR提供的工具PVRGeoPOD导出(Blender可以通过安装插件以增加pod文件的导出选项,但其实也是调用的PVRGeoPOD)。所以我们需要先导出到一个中间文件,再转换为POD文件。中间文件我使用了应用较广的fbx格式。
事实上,收集得到的3D人物、背景、动画数据本身也是异于fbx的其它格式(pmx、vmd),导入Blender的过程中就很容易造成数据错误出bug,而且还有两位数的导入选项供选择。再进行一次导出与格式转换出错概率成倍叠加。因此这一部分需要更多的钻研与调试。
2. 纹理数据格式
loadModel()支持模型导入6种格式的纹理文件。然而在调用
void ModelGles::init(pvr::IAssetProvider& assetProvider, pvr::assets::Model& inModel, Flags flags)
来初始化场景时,函数会调用pvr::utils::textureUpload()
,其中的gl::TexStorage2D()
对于纹理文件的编码方式有限定,它几乎不支持BGR(A)顺序的编码方式,比如BGRA8888(GL_BGRA_EXT)。而tga、bmp等多数格式文件都是以BGRA顺序编码的。因此要使用PowerVR SDK提供的api,真正能用的纹理格式只有能以RGBA方式编码的pvr和ktx,并需要用PVRTexTool进转换。我选择了ktx格式。
3. 资源导入与整合
动画数据必须和3D人物的骨架数据相契合才能实现较好的效果。MMD圈有许多优秀资源,并且可以通过在Blender上安装插件Cats Blender Plugin与blender mmd tool来导入,主要有以下流程与细节:
- 如果使用Cats Blender Plugin来导入人物模型并fix Model会造成部分骨骼数据的简化与约舍,再用mmd tool导入动画数据后人物动作会很僵硬。因此可以先用mmd tool导入人物模型,注意导入时需要进行缩放以免尺寸过大不便操作。之后再有选择地利用Cats Blender Plugin的fix Model功能。
- fix Model时主要是用以去除Rigidbodies和Jionts的数据,因为这两者是用于MMD软件的物理引擎计算,在我们的实例中用不到。另外尽量多地保留骨骼数据不进行归并。
- 将所有材质的Mmd Toon Tex节点删除。
- 翻译骨骼标签为Blender的格式,否则动画文件导入无法正常工作。
- 利用mmd tool导入动画文件,缩放比例与导入人物模型时要相同。
- 合并所有textures与meshes,这是最为关键的一步。如果仅仅是将meshes合并,那么在将fbx文件转换为pod文件时,PVRGeoPOD会因为一个mesh使用了多个texture而重新拆解成多个mesh,这个过程会造成严重的bug并且难以发现。因此利用Material Combiner插件将人物模型使用的所有纹理合并为一个纹理文件,并合并所有meshes。
- 利用PVRTexTool将合并后的纹理转为ktx格式,并作垂直翻转。手动修改纹理路径为ktx文件以便我们的程序正常运行。因为PowerVR没有提供修改pod文件中材质纹理路径的工具,因此只能在这一阶段固定好纹理路径。
- 导入背景模型文件,流程与上述类似。
- 修改相机与灯光至合适位置,此后也没有机会再对pod文件中的这些数据进行修改。
- 整理场景集合的数据为如下结构,这样导出并转换得到的pod文件程序才能正确读取:
- 11. 导出为fbx时选择所有物体类型、使用空间变换、应用修改器、导出切向空间、烘培动画。如果在cats插件中fix Model时保留了末端骨头就不再添加叶骨。
4. 转为POD文件
导出选项在默认配置的基础上有以下不同和要点:
- Indexed triangle list。
- Export skinning data打勾,取消Bone batching,后者是在OpenGLES3.0版本后就不必使用了。
- 如果需要可以导出切向和法向向量。
二、编写代码
代码主要是在PowerVR SDK v5.12中的例程Skinning
基础上修改的,并且参照了PowerVR SDK v3.4中的例程CubeShadowMapping以及OpenGL教程:阴影贴图、LearnOpenGL-阴影映射、ARMMALI-Shadow Mapping中的教程与代码。主要做了以下三方面工作:
修改原程序
对原程序的一个主要的修改是针对骨骼数据的处理的。因为原程序适配的演示蒙皮动画文件Robot.pod在数据构成上很特殊,因此并不适用于我们生成的pod文件。
首先,Robot.pod中包含骨骼的mesh只有一个,即robot。而原程序利用ssbo向shader传送BoneMatrix和BoneMatrixIT,在每次渲染节点时都会调用一次gl::MapBufferRange()。
// line 457
void* bones = gl::MapBufferRange(GL_SHADER_STORAGE_BUFFER, 0, static_cast<GLsizeiptr>(_deviceResources->ssboView.getSize()), GL_MAP_WRITE_BIT);
其中的ssboView的大小在原程序275行由最后一个有骨头的mesh所设置:
// line 275
ssboView.setLastElementArraySize(static_cast<uint32_t>(skeleton.bones.size()));
这个大小=ssbo结构大小*影响单个vertex的骨头个数。因为在这个实例中ssbo结构大小为34字节,并且与单个vertex相关联的骨头个数最多为4,因此我直接修改为了:
void *bones = gl::MapBufferRange(GL_SHADER_STORAGE_BUFFER, 0, 112 * numBones, GL_MAP_WRITE_BIT);
其次,Robot.pod中的Robot Mesh,影响单个vertex的骨头个数为4。因此对于相关联骨头个数小于4的pod文件,使用原程序可能会导致传送BoneMatrix时数据错位,同时Vertex Shader也不适用。因此我添加了
uniform int BoneCount; // 每vertex受几个bone影响
并对原代码和shader作了修改。
加入传统光照技术
套用了老师的例程。但是因为我没有法线纹理贴图,因此人物没有纵深感。并且因为没有阴影人物像是悬在空中舞蹈。效果不够好。因此我又加入了ShadowMap。
加入ShadowMap
流程大概为:
- 创建深度纹理与帧缓存
- 将创建的纹理对象绑定到帧缓冲区
- 把光源作为摄像机进行一次渲染(即导入的是光源的PV矩阵)
- 解除绑定的缓冲区对象,重新绑定绘制到系统离屏缓冲。设置刚刚获得深度纹理,以回到相机视角进行一次渲染(导入相机的PV矩阵)。
为简化程序我没有为渲染shadowmap单独创建shader和program,而是又改写了SkinnedVertShader和SkinnedFragShader,用新的uniform isPassLight来区分之前的逻辑,以和人物、背景共用一个渲染程序。
为更好的效果,采用了泊松分布。
三、总结
OpenGLES的程序Debug非常难,一方面可以利用PowerVR提供的工具PVRCarbon,另一方面可以查询kronos和教材的文档。但是实现出来也非常有意思。未来可以进一步改进shader以实现更好的效果。