Project SEKAI 逆向(7):3D 模型
1. 文件结构
- 目前发现的 3D 模型基本都在
[ab cache]/live_pv/model/
下
初步观察:
- (1) Body 即目标模型;当然,作为skinned mesh,而且带有blend shapes,处理细节会很多;后面继续讲
- (2) 处的
MonoBehavior
就其名字猜测是碰撞盒 - (3) 处的几个
Texture2D
则作为texture map
2. 模型收集
利用sssekai
取得数据的流程在(5)中已有描述,这里不再多说
首先整理下根据mesh发现的数据需求
- (1) Static Mesh
pjsk发现的所有mesh在相应assetbundle中会有1个或更多GameObject
的ref;对于这些ref,static mesh会出现在m_MeshRenderer
之中
其他细节暂且不说;因为做 Skinned Mesh 导入时都是我们要处理的东西
- (2) Skinned Mesh
不同于static mesh,这些ref会出现在m_SkinnedMeshRenderer
之中
同时,我们也会需要骨骼结构的信息;bone weight以外,也需要bone path(后面会用来反向hash)和transform
(3) Blend Shapes
这些可以出现在static/skinned mesh之中;如果存在,我们也会需要blend shape名字的hash,理由和bone path一致
加之,Unity存在aseetbundle中动画path也都是crc,blendshape不是例外
总结:
(1) 所以对于static mesh,搜集对应
GameObject
即可(2) 对于skinned mesh,同时也需要构造bone hierarchy(就是个单根有向无环图啦),并且整理vertex权重;
则需要收集的,反而只是bone的transform而已;transform有子/父节点信息,也有拥有transform的
GameObject
的ref(3) 的数据,在(1)(2)中都会有
3. 模型导入
当然,这里就不考虑将模型转化为中间格式了(i.e. FBX,GLTF)
利用Blender Python,可以直接给这些素材写个importer
实现细节上,有几个值得注意的地方:
Unity读到的mesh是triangle list
Blender使用右手系,Unity/Direct3D使用左手系
坐标系 | 前 | 上 | 左 |
---|---|---|---|
Unity | Z | Y | X |
Blender | -Y | Z | -X |
意味着对向量需要如下转化
$\vec{V_{blender}}(X,Y,Z) = \vec(-V_{unity}.X,-V_{unity}.Z,V_{unity}.Y)$
对四元数XYZ部分
$\vec{Q_{blender}}(W,X,Y,Z) = \overline{\vec(V_{unity}.W,-V_{unity}.X,-V_{unity}.Z,V_{unity}.Y)}$
Unity存储vector类型数据可能以2,3,4或其他个数浮点数读取,而vector不会额外封包,需要从flat float array中读取
意味着需要这样的处理
vtxFloats = int(len(data.m_Vertices) / data.m_VertexCount) vert = bm.verts.new(swizzle_vector3( data.m_Vertices[vtx * vtxFloats], # x,y,z data.m_Vertices[vtx * vtxFloats + 1], data.m_Vertices[vtx * vtxFloats + 2] ))
嗯。这里的
vtxFloats
就有可能是$4$. 虽然$w$项并用不到对于BlendShape, blender并不支持用他们修改法线或uv;这些信息只能丢掉
Blender的BlendShape名字不能超过64字,否则名称会被截取
对于bone,他们会以
Transform
的方式呈现;但在模型(和动画文件)中,他们只会以Scene
中这些transform的完整路径的hash存储然后,Blender的Vertex Group(bone weight group)同样也不能有64+长名字
对于vertex color,blender的
vertex_colors
layer在4.0已被弃用;不过可以放在Color Atrributes
**注:**Blender中对写脚本帮助很大的一个小功能
4. Shaders!
Texture2D
和其他meta信息导入后,接下来就是做shader了
当然,真正搞NPR的话值得也需要再开一个blog系列描述;暂时搓一个基础复现吧
- 手头有的纹理资源如下:
tex_[...]_C
Base Color Map,没什么好说的
tex_[...]_S
Shadowed Color Map(乱猜
NPR渲染中常用的阈值Map;为节省性能(和细节质量),引擎也许并不会绘制真正的Shadow Map
在很多 NPR Shader中,你会见到这样的逻辑:
if (dot(N, L) > threshold) { diffuse = Sample(ColorMap, uv); } else { diffuse = Sample(ShadowColorMap, uv); }
即:对NdotL作阈值处理,光线亮(NdotL更大)采用原map,光线暗/无法照明(NdotL更小或为负)采用阴影map
tex_[...]_H
Hightlight Map
注意到
Format
出于某种原因竟然是未压缩的RGB565
;同时,$R$通道恒为$0$,$B$通道恒为$132$,只有G通道有带意义的信息这些区域标记对应材质发光部分;虽然没有专门提供Emissive,不过如此直接利用base color也不非一种选择
Vertex Color
- 虽然不是texture map,但是放这里讲会合适不少
这里只有RG通道有信息,猜测:
$R$通道决定是否接受描边
$G$通道决定高光强度
5. Shader 实现
- 阴影 / Diffuse
注: BSDF应为Diffuse BSDF,截图暂未更新
这里实现的即为上文所述的阈值阴影,不多说了
- Specular
直接利用Specular BSDF的输出和前文所提到的weight,mix到输出即可
- Emissive
用_H
材质的$G$通道叠加,node如图
至此Shader部分介绍完毕,效果如图
6.描边
ShadeKai
使用将mesh沿法线偏移的tech渲染边界;在 Blender 中,有对应的 Solidify Modifier
- 不过,重现如图效果的话… (PV: 愛して愛して愛して))
- 可见$1$区域带明显描边而$2$区域没有,观察vertex color:
这和之前对描边做的描述是一致的; $R$值决定是否描边
貌似Solidify不能根据顶点定制操作;完全复现需要其他路子
以上;貌似这篇有点太长了,抱歉
下一次回归PJSK应该还是会讨论NPR Shader的相关实现
同时图形学知识会更多,也许就不发在"逆向“专题了吧
那么..
SEE YOU SPACE COWBOY…
References
https://github.com/mos9527/sssekai_blender_io 👈 插件在这
https://github.com/KH40-khoast40/Shadekai
https://github.com/KhronosGroup/glTF-Blender-IO
https://github.com/theturboturnip/yk_gmd_io