对于PBR材质来说,想要通过PBR属性还原真实的渲染效果,需要有一定的材质编辑能力。材质编辑工具通过提供实时编辑材质并且实时预览效果的能力,降低PBR材质编辑的门槛。
在得物3D空间改用filament引擎进行渲染之后,PBR材质的渲染得到了很大的提升,但是从材质编辑到最后材质验收环节所花费的时间还有很大的提升空间。由于材质验收环节在移动端上进行,整套流程涉及材质编辑 - 材质编译 - 移动端渲染 - 验收材质,仅仅是产出一个材质就需要花费很多时间,因此想要通过实现一个PBR材质编辑器,降低PBR材质的编辑门槛、减少材质产出时间。
回顾下当前filament的材质产出流程:
图1
首先,材质编辑和材质渲染不在同一平台。这意味着材质文件在多个平台编译的时间开销。其次,移动端欠缺迅速校正光照的能力,由于移动端视口大小的限制,只能观察到场景的有限范围,没办法对光源位置进行操作和进行光强调节。结合这两个特点,考虑将材质渲染迁移至PC端,由于PC端场景尺寸足够大可以很方便的调节光照,同时因为材质编辑和材质渲染皆在PC端上完成从而可以节省多平台的材质文件编译时间。同时filament引擎保证了在多端上渲染效果也是一致的,因此通过在PC端渲染的结果等同于移动端。
最后是材质验收,考虑到filament的多平台能力,可以基于一份材质编辑工具代码编译出windows、mac可运行的程序,编辑好的材质可以由开发者导出材质包给设计师,设计师在自己的电脑上通过移植后的材质编辑工具运行出来的渲染效果完成验收,减少因多人协作带来需时间对齐所造成的花销。同时,设计师也可以使用材质编辑工具的材质编辑能力,自行产出材质,提高材质产出生产力。
至此,Filament材质编辑工具要达到的目标如下:
材质编辑工具由GUI和渲染引擎组成,它的主要原理就是材质编辑之后如何实现渲染效果的实时预览。根据filament官方给出的代码,filament渲染引擎可以通过共享contex的方式与ImGui进行结合,由ImGui完成按钮等交互组件的绘制,而filament进行模型、材质的渲染。ImGui组件响应用户操作后,再将改动同步到filament,从而filament根据用户操作实时反馈渲染效果。
材质编辑工具的主要功能如下:
filament渲染引擎是一款跨平台的PBR渲染引擎,而实现一个基于PC端的PBR材质编辑工具,除了渲染引擎,还需要GUI库。Filament Creator采用的是ImGui库,ImGui库采用c++实现,具有可移植、速度快等优点。这为Filament Creator能同时在mac和windows上运行提供了条件。同时ImGui库能提供例如按钮、选择框、进度条等组件,做为GUI库足够满足材质编辑工具对于交互性的要求。
图2
Filament Creator采用Filament作为渲染引擎,ImGui作为GUI库提供交互,共同工作在SDL2的上下文环境中。
材质编辑可分为三大块,分别为材质属性、光线渲染模型、贴图的编辑。
PBR材质虽然数目众多,但是材质编辑器面向PBR属性提供编辑能力,通过组合的方式来还原PBR材质。同时,考虑光线在介质中的反射、透射、折射等行为,filament提供了多个物理正确的光线渲染模型去模拟这些光线传输行为,而材质编辑工具通过穷举的方式提供用户去选择不同光线传输的渲染模式。最后,是贴图的编辑,用户可以在材质编辑工具里基于mesh去选择对应的纹理贴图、粗糙度贴图等。
图3
材质编辑工具的左侧显示这个模型的mesh构成,当用户点击选中特定的mesh时,工具中间渲染的模型会通过线框的方式标出选中的mesh。
材质编辑工具的右侧提供属性编辑和全局配置。在属性编辑这一栏,用户可以选择特定的光线渲染模型,编辑材质属性,以及通过按钮选择对应的纹理贴图。全局配置一栏主要是光照调节和HDR天空盒配置,其中光照调节包括环境光强度和方向光的方向和强度。
材质编辑工具的底部,由材质库、材质、纹理、模型四个区域构成。材质库提供默认的PBR材质,例如塑料、玻璃、金属等材质,用户可以通过拖拽直接设置对应mesh的材质默认属性。材质区里用户可以创建自定义材质,之后也是通过拖拽的方式设置特定mesh引用该材质,自定义材质属性的更改会影响到所有引用它的mesh。纹理区主要提供一些常用纹理的存放,用户可以通过拖拽设置对应的纹理贴图。模型区域主要用来显示已经加载过的模型,用户点击可以切换模型并且可以把所有mesh恢复到上一次编辑该模型时的材质属性数据。
一个模型可以由多个mesh构成,而每个mesh可以对应不同材质。例如图3的手表模型,它的镜面对应玻璃材质,它的腕带对应皮革。先来了解下Assimp库是如何完成多mesh模型解析以及材质映射的:
图4
Assimp库会在解析obj模型的过程中生成aiScene,它存储着模型所有节点的顶点数据、法线数据、材质信息等数据。每个节点对应的数据结构为aiNode,每个aiNode会具有多个aiMesh。每个aiMesh都会对应到一个材质aiMaterial。之后Filament通过深度遍历所有aiNode,为每个aiNode生成Mesh数据结构,引用的多个aiMesh生成Part数据结构,同时Part通过materialId属性记录引用对应材质。最后filament为每个Mesh创建Renderable,加入场景进行渲染。
在上述解析流程中,材质映射发生在内存化数据生成Part的阶段,每个内存化后的Part和aiMesh是一一对应的。其中aiMesh通过下标mMaterialIndex索引到模型的材质,Part则维护了自定义属性materialId,它跟mMaterialIndex是一致的。当Part通过下标映射到对应材质后,根据该材质的材质名做映射,如果filametn没有提供同名的材质实例,则Part会采用默认的材质进行渲染。因此,要完成多mesh模型的材质映射, 可以采用在渲染Mesh时为每个Part提供与其对应材质名一致的材质实例来指定需要使用的材质材质实例。
材质编辑工具在Mesh的粒度匹配材质。当filament渲染特定mesh时,材质编辑工具会根据该Mesh所持有的首个Part对应的材质名称,采用材质名映射材质实例的方式来完成材质映射。同时,当用户在工具中基于mesh编辑材质后,工具会根据用户当前已编辑好的材质属性、光线渲染模型和贴图文件生成json文件,下面是json化材质的一部分内容:
{
"129" : {
"baseColorIndex" : 1,
"baseColorMap" : "/Users/test/Downloads/Watch_TISSOT_Classic/image/Watch_TISSOT_Classic_Color.png",
"exported" : false,
"metallicIndex" : 1,
"metallicMap" : "/Users/test/Downloads/Watch_TISSOT_Classic/image/Watch_TISSOT_Classic_M.png",
"normalIndex" : 1,
"normalMap" : "/Users/test/Downloads/Watch_TISSOT_Classic/image/Watch_TISSOT_Classic_Normal.png",
"shadingModel" : "lit"
}
}
"129"是这个材质的材质名称,而这个材质内容里还有对应的光线渲染模式、纹理贴图、金属度贴图、法线贴图等。材质编辑工具会在解析模型后载入这个json化材质,再根据这个材质名称提供对应的材质实例到对应的Mesh、Part中。
对于材质编辑工具来说,需要提供针对模型的编辑能力。例如一个obj模型在通过assimp解析后,会解析出一个aiScene、多个aiNode和aiMesh。每个aiMesh都有相应的mMaterialIndex,这会对应到特定的材质aiMaterial。基于前述的材质名映射材质实例,如果想在任意一个平台解析模型时自动针对任意一个mesh映射到编辑 好的自定义材质,那自定义材质的材质名和模型里该aiMesh对应的材质名得保持一致。具体实现如下:
图5
材质编辑工具在完成指定材质名覆盖到对应aiMesh对应的aiMaterial名称后,进行模型导出。
材质编辑工具提供材质属性保存的能力,并且材质和模型绑定,重新进入工具后会提供对应的入口图标,用户可以通过点选对应的模型图标恢复上一次材质编辑的属性。
首先来确定持久化数据的构成:
持久化数据意味着可以通过重新载入这部分数据完成场景重建,这个场景重建在材质编辑工具里就相当于材质属性数值的恢复,同时由于模型本身数据有可能在材质编辑过程中受到更改,所以要在持久化目录中做新的模型备份。
材质编辑工具持久化数据的更新时机是保存操作,保存操作会持久化各个mesh对应材质的光线渲染模型以及PBR属性和依赖的贴图路径,对于一些冲突的材质属性,比如颜色采用rgb值还是纹理贴图,引入新增的flag数值用于标识。
数据持久化就是通过jsoncpp将数据写入json文件,每个材质属性都采用各自的材质名作为键,同时把引用的模型拷贝到持久化目录中,在json文件里写入备份模型的路径。以下是持久化的代码实现:
if (!dst_dir.exists()) {
dst_dir.mkdir();
}
Json::Value* root = new Json::Value();
Json::Value& tmp = *root;
std::vector dependency_resources;
for (auto entry: memoryPersist) {
std::string material_name = entry.first;
if (material_name.empty()) {
continue;
}
tmp[entry.first] = Json::Value(exportMaterialJson(entry.second, &dependency_resources));
}
// 复制模型文件到./Asset/Models/${模型}/${模型}.obj
utils::Path dst_obj = dst_dir.concat(model_path.getName());
if (!dst_obj.exists()) {
std::filesystem::copy_file(model_path.c_str(), dst_obj.c_str());
}
tmp["model"] = model_path.getName();
// 写入清单文件
std::ofstream ofs;
utils::Path json_path = dst_dir.concat("material.json");
ofs.open(json_path, std::ios::out);
if (ofs.is_open()) {
Json::StyledWriter sw;
ofs<
用户重新打开工具后可以在模型区点选对应的模型图标进行持久化数据的加载。首先加载模型,filament会遍历模型里每个mesh,并尝试根据它们对应的材质的名称去匹配材质实例,如果匹配失败则生成默认的材质实例。之后加载持久化数据的json文件,读取持久化后的材质属性,遍历持久化后的每个材质,并根据名字和模型mesh对应的材质名一致的方式覆盖该mesh默认的材质实例。
材质编辑工具提供在导出材质时针对模型做filamesh格式转化的模型优化。
filamesh通过将模型的切线空间法线数据做16位降阶、删除相同vertex来减少模型体积,与目前线上3D空间采用assimp一样,都是通过法线降阶来完成模型加载,只不过filamesh把这个步骤提前,转化为filamehs的模型压缩比在1:3左右。而在移动端,模型体积的优化会带来cpu解析模型效率的提升,针对一个原始大小为4m的obj,转化成filamesh减少的解析时间可达200ms,同时也将减少模型动态下发所花费的时间并提高下发成功率。
图6
图7
在图6中是经过格式转化后的filamesh,图7为obj。它们的渲染效果基本保持一致(由于做filamesh转化后会写入位移矩阵信息,上图两个模型的位置不一致),同时细节部分filamesh也保存得很好。
在移动端3d渲染的场景中,面临的两个瓶颈分别是纹理占用内存大小以及解析纹理耗时。多mesh模型的一个法线贴图占3m、4m左右,加上其他纹理贴图和模型,所占用的体积大小就接近20m,这对于下发材质和模型以及动态下发的成功率都带来了不少压力。而对于需要高精度渲染贴图的场景,贴图的尺寸往往都在2k * 2k 以上,如此精度的单张贴图经过解码后占用的内存可达60m以上,如果是一个模型对应多张贴图占用的内存大小更是几倍的上升,这在可用内存所剩不足的终端将会造成gc无法响应用户或者直接因为内存无法分配而崩溃。因此,材质编辑工具提供发布场景为移动端时进行纹理压缩的能力。针对法线等贴图数据Android采用etc,IOS采用PVR进行纹理压缩。
其次是解析纹理的耗时。多mesh模型的一个特点就是一个模型对应多个贴图,解析所有贴图的耗时如果不进行优化终将会上升到用户无法忍受的程度。由此,材质编辑工具在完成纹理压缩后,提供压缩纹理 转换为ktx格式支持gpu直接消费,ktx纹理不再需要cpu进行解码,单张高清度的纹理解析耗时也从几百ms缩短至几十ms甚至几ms的数量级。
在实现材质导出功能之前,先来明确导出功能要达成的目标:
上述两点,主要考虑的就是材质内容如何在移动端解耦。filament渲染引擎采用材质文件mat来描述一个材质,用户可以在材质文件里自定义PBR属性,随后filament通过spirv等库编译出基于不同平台(metal、opengl等)可执行的着色器代码文件filamat。我们来看看材质文件mat的构成:
material {
name : LitOpaque,
shadingModel : lit,
parameters : [
{
type : float3,
name : baseColor
},
{ type : int, name : baseColorIndex },
{ type : sampler2d, name : baseColorMap },
{ type : int, name : normalIndex },
{ type : sampler2d, name : normalMap}
],
specularAntiAliasing : true,
requires: [
uv0
]
}
fragment {
void material(inout MaterialInputs material) {
if (materialParams.normalIndex > -1) {
material.normal = texture(materialParams_normalMap, getUV0()).xyz * 2.0 - 1.0;
}
prepareMaterial(material);
if (materialParams.baseColorIndex > -1) {
material.baseColor = texture(materialParams_baseColorMap, getUV0());
} else {
material.baseColor.rgb = materialParams.baseColor;
}
if (materialParams.roughnessIndex > -1 ) {
material.roughness = texture(materialParams_roughnessMap, getUV0()).r;
} else {
material.roughness = materialParams.roughness;
}
}
}
mat文件中主要由material和fragment两大部分构成。material部分用来描述材质对应的光线渲染模型以及材质属性。可以看到上述材质采用的是lit(普通光照)的光线渲染模型,同时还声明了baseColor属性、baseColorMap纹理等,而fragment部分则通过程序语言来规范如何使用贴图等。
仔细观察mat文件内容,可以发现有些逻辑是固定写死的,例如采用何种光线渲染模型,而有些逻辑可以在生成材质后由程序决定哪部分逻辑去执行,例如材质渲染的baseColor颜色属性,当baseColorIndex > -1时,材质采用颜色贴图,否则采用运行时给材质设置的baseColor数值。总结下,filament渲染材质有以下这些特点:
由于mat文件描述的材质属性有动态的也有写死的,跟动态下发这一方式相结合,决定采用切割的方式将材质、光线渲染模型的声明保留在mat文件中,而材质属性的具体数值以及通过flag动态执行fragment部分中的程序逻辑迁移到材质清单json文件。例如材质是采用颜色贴图还是rgb颜色值进行渲染这部分程序逻辑,可以通过json文件中的baseColorIndex具体数值去指定,从而这些本来会由移动端去开发写死的渲染逻辑就可以通过下发材质json文件的方式动态指定,达到材质内容跟移动端解耦。而固定写死的材质内容例如光线渲染模型则直接在mat文件声明并且在pc端编译成filamt着色器代码产物。由此我们确定了材质导出这一功能的具体导出内容:filamat着色器代码产物 + 材质清单的json文件 + 模型以及一些纹理贴图。
材质导出的流程如下:
图8
由于PBR材质最终是在移动端上线,filamat产物可以由官方的matc指定发布平台为mobile。至于材质清单json文件和对应的纹理贴图,可以通过材质编辑工具在内存里保存材质属性来生成json。考虑到模型数据改动的可能,会检查是否需要更改模型数据指定材质名称,同时针对移动端做纹理压缩,最后将所有的导出内容都放在同一目录来进行打 包方便解决路径问题。
材质导入功能是为材质验收环节设计的。设计师可以通过导入材质包还原模型的渲染效果,在材质导出功能中,材质编辑工具会导出:filamat着色器文件 + 材质清单的json文件 + 模型以及一些纹理贴图。材质导入功能会先把材质包解压,随后解析材质清单的json文件。将持久化后的材质属性数据内存化,并解析模型,同时载入纹理,完成渲染场景还原。
高质量的3D模型更能激发用户消费的意识,因此提高3D模型的呈现效果就是我们的重点攻坚方向,在此之前我们都是使用一张贴纸来呈现球鞋效果,所以无法表现出诸如皮革、毛绒、金属、塑料等材质的效果,Filament creator材质编辑工具,可以帮助我们使用不同的渲染引擎材质来呈现球鞋效果,让用户可以通过模型获取更真实的效果。
文/WANGJUNJIE
关注得物技术,做最潮技术人!