写点什么

得物技术 Filament Creator 材质编辑工具的实现

作者:得物技术
  • 2022 年 5 月 26 日
  • 本文字数:7242 字

    阅读完需:约 24 分钟

对于 PBR 材质来说,想要通过 PBR 属性还原真实的渲染效果,需要有一定的材质编辑能力。材质编辑工具通过提供实时编辑材质并且实时预览效果的能力,降低 PBR 材质编辑的门槛


背景

在得物 3D 空间改用 filament 引擎进行渲染之后,PBR 材质的渲染得到了很大的提升,但是从材质编辑到最后材质验收环节所花费的时间还有很大的提升空间。由于材质验收环节在移动端上进行,整套流程涉及材质编辑 - 材质编译 - 移动端渲染 - 验收材质,仅仅是产出一个材质就需要花费很多时间,因此想要通过实现一个 PBR 材质编辑器,降低 PBR 材质的编辑门槛、减少材质产出时间。


目标

回顾下当前 filament 的材质产出流程:



图 1

  1. 材质编辑:filament 的材质通过材质文件去描述,因此主要是在 PC 端去完成材质属性的设置

  2. 材质编译:由于发布端涉及安卓和苹果两个平台,材质文件需要编译成 metal 和 opengl 两个版本

  3. 移动端渲染:移动端需要在端上的渲染逻辑中指定模型不同 mesh 对应的材质,同时模型渲染需要考虑打光方向以及强度,这意味着每做一次光照效果调节都将花费一次代码编译时间。最后,由于发布端涉及安卓和苹果,本环节的时间花费会 Double

  4. 材质验收:材质验收是材质产出的最后一步,在上线前需要同设计师反复确认以及修改。由于设计师只能在测试机上进行验收,设计师需要同开发者对齐彼此的时间从而导致验收时间受影响或者延长

流程优化

首先,材质编辑和材质渲染不在同一平台。这意味着材质文件在多个平台编译的时间开销。其次,移动端欠缺迅速校正光照的能力,由于移动端视口大小的限制,只能观察到场景的有限范围,没办法对光源位置进行操作和进行光强调节。结合这两个特点,考虑将材质渲染迁移至 PC 端,由于 PC 端场景尺寸足够大可以很方便的调节光照,同时因为材质编辑和材质渲染皆在 PC 端上完成从而可以节省多平台的材质文件编译时间。同时 filament 引擎保证了在多端上渲染效果也是一致的,因此通过在 PC 端渲染的结果等同于移动端。

最后是材质验收,考虑到 filament 的多平台能力,可以基于一份材质编辑工具代码编译出 windows、mac 可运行的程序,编辑好的材质可以由开发者导出材质包给设计师,设计师在自己的电脑上通过移植后的材质编辑工具运行出来的渲染效果完成验收,减少因多人协作带来需时间对齐所造成的花销。同时,设计师也可以使用材质编辑工具的材质编辑能力,自行产出材质,提高材质产出生产力。

至此,Filament 材质编辑工具要达到的目标如下:

  • 材质编辑和材质渲染统一迁移至 PC 平台

  • PC 平台和移动端基于同个材质的渲染效果达到一致

  • 材质编辑工具的多平台发布能力,开发者和设计师都可以基于自己所在平台去操作工具,完成材质编辑、材质验收等流程

  • 材质编辑工具可针对编辑好的材质导出移动端可直接运行的材质,并且移动端的 1 渲染逻辑是与模型、材质文件本身解耦


原理

材质编辑工具由 GUI 和渲染引擎组成,它的主要原理就是材质编辑之后如何实现渲染效果的实时预览。根据 filament 官方给出的代码,filament 渲染引擎可以通过共享 contex 的方式与 ImGui 进行结合,由 ImGui 完成按钮等交互组件的绘制,而 filament 进行模型、材质的渲染。ImGui 组件响应用户操作后,再将改动同步到 filament,从而 filament 根据用户操作实时反馈渲染效果。


主要功能

材质编辑工具的主要功能如下:

  • 材质编辑

  • 模型材质指定

  • 工具操作数据持久化

  • 模型优化和纹理压缩

  • 材质导出

  • 材质导入


实现

GUI

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 构成,而每个 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<utils::Path> 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/${模型}/${模型}.objutils::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<<sw.write(tmp);}ofs.close();
复制代码

用户重新打开工具后可以在模型区点选对应的模型图标进行持久化数据的加载。首先加载模型,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 的数量级


材质导出

在实现材质导出功能之前,先来明确导出功能要达成的目标:

  1. 材质导出后,移动端可以不需要做任何代码更改就能完成材质的渲染(即材质的具体内容对于移动端是透明的)

  2. 导出的材质,不需要通过代码指定哪个 mesh 采用哪种材质进行渲染(这个可以采用前面表述的材质名映射材质实例的方式)

上述两点,主要考虑的就是材质内容如何在移动端解耦。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 文件有固定写死的属性也有可以执行 if - else 的程序逻辑

  • 渲染材质必须要有 mat 文件编译后的 filamat 着色器代码产物

  • mat 编译不能放在移动端去做,因为特别耗时

  • 编译 mat 会丧失一部分材质描述的动态性,怎么避免通过增加移动端渲染逻辑来弥补这部分动态性

由于 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

关注得物技术,做最潮技术人!


发布于: 刚刚阅读数: 5
用户头像

得物技术

关注

得物APP技术部 2019.11.13 加入

关注微信公众号「得物技术」

评论

发布
暂无评论
得物技术Filament Creator材质编辑工具的实现_模型_得物技术_InfoQ写作社区