-
Notifications
You must be signed in to change notification settings - Fork 0
Description
目的
将模型提取出来放到网页中并查看模型的动画
解包的方法就不赘述了,由于碧蓝档案(下称ba)是基于Unity开发的,所以相关的解包工具网上是可以找到的。
模型提取
由于我们是需要在线查看模型和动画,以AssetStudioGUI为例,需要找到对应人物的Animator以及相关的AnimationClip,这里有一点需要注意的是,导出的模型可能会缺少光环。这里以Hina为例,两个与模型有关的文件分别是Hina_Original和Hina_Original_Mesh,前者是包含光环的。
提取出来的模型文件是由贴图Texture以及白模组成的,其中白模是Fbx格式中。
网页渲染
这里我使用的是three.js v0.131.3。
模型提取出来后要做的就是在three.js中加载,这块官方有提供FBXLoader以及相关的使用案例。
const loader = new FBXLoader();
loader.load( '/Animator/Hina_Original/Hina_Original.fbx', function ( object ) {
mixer = new THREE.AnimationMixer( object );
const action = mixer.clipAction( object.animations[ 0 ] );
action.play();
object.traverse( function ( child ) {
if ( child.isMesh ) {
child.castShadow = true;
child.receiveShadow = true;
}
} );
scene.add( object );
} );看上去这样可以实现模型的在线查看了,但是实际运行的时候会出现两个问题
1. 贴图丢失
可以看到控制台上有警告THREE.Material: 'map' parameter is undefined.
而three.js中材质贴图是通过map关联的,那么问题很显然了,在加载fbx模型的时候贴图没有关联上
FBXLoader: can't check image until loaded
这个问题的根本原因就是在关联贴图的时候会先检查是否存在,但是如果贴图是远程加载的就会导致返回undefined, 进而出现上面的警告。
比较尴尬的一点是,这个问题在131之前不存在,而在132这个版本被修复了。
2. 骨骼动画错误
关于这一点,因为之前从来没接触过,所以并不知道导致这个问题的原因是什么。只能大胆猜测AssetStudioGUI导出的数据格式有问题。
我的解决方法是将解包出的Fbx模型先导入到Unity中,再通过插件gltf-exporter作为Gltf导出即可。
嘴部贴图
可以看到嘴部周围有一圈很规则的白色区域。
这是因为hina眼睛和嘴的贴图(eyeMouth)就是这样的

因为嘴巴有不同的口型,因此是放在另一张独立的贴图中,根据不同的需求渲染不同位置的口型。

根据eyeMouth的贴图不难判断左下角就是嘴部使用贴图,只要将这块位置扣掉,再叠加上嘴型的贴图就可以了。但是实际上不论是Unity还是Threejs都没有直接提供纹理叠加的功能,ue5好像是支持材质中的贴图叠加的。
由于看不到代码,只能猜测ba是通过着色器来实现贴图叠加的。而关于Threejs的实现可以参考How to apply two texture in single face of cube
This should help.
You can also use CanvasTexture and mix textures there - if it’s one-time only, it shouldn’t hurt performance too much.
简单的说就是可以先在canvas中进行贴图的叠加,再把它通过CanvasTexture转换成纹理绑定到材质上。由于纹理是通过UV映射到模型的对应位置上的,所以可以直接把eyeMouth覆盖到嘴型贴图上。
但是实际操作的时候会发现嘴型确实改变了,但是嘴部的颜色不是预想中皮肤的颜色而是黑色。这是由于Threejs默认会将透明的地方渲染成黑色。
material.transparent = true通知Threejs将其视为透明材质。
但是实际渲染的结果却很奇怪。

可以看到眼睛有很明显的透明度渐变的现象。
这是因为模型的geomertry上设置了顶点颜色,而顶点颜色中指定了眼睛的部分顶点是存在透明度的,所以当transparent设置为true时,看上去是部分透明的。
解决方法很简单
const eyeMouth = obj.getObjectByName('Hina_Original_Body_3')
eyeMouth.material.vertexColors = false至于出现这个问题的根本原因应该是在Fbx转GLTF的时候出了问题。
GLTF格式中合成了顶点颜色,而FBX格式中却是直接使用的纹理贴图,同时绑定的法线贴图和混合贴图也消失了。
因此需要在导入模型的时候关闭顶点颜色的渲染
gltf.scene.traverse( function ( object ) {
if ( object.isMesh ) {
object.castShadow = true
object.material.vertexColors = false
}
})不同口型的切换
游戏中小人实际上在不同的场景下有不同的口型,甚至在EX动画的时候会使用多个口型实现类似逐帧动画的效果。这种时候如果通过CanvasTexture来不断的生成新的纹理很明显不太现实。
这种时候可以考虑使用ShaderMaterial来自定义眼睛与嘴巴的渲染方式。
// vertex
#include <skinning_pars_vertex>
varying vec2 vUv;
void main() {
#include <skinbase_vertex>
#include <begin_vertex>
#include <skinning_vertex>
#include <project_vertex>
vUv = uv;
}// fragment
varying vec2 vUv;
uniform vec2 offset;
uniform sampler2D eyeMouthTex;
uniform sampler2D mouthTex;
void main()
{
vec2 fUv = vUv;
vec4 mouth = texture2D(mouthTex, fUv + offset);
// 这里没有理解,为什么从unity导出后eyeMouth做了y轴镜像翻转
// 但是手动导入后发现uv坐标还是原来的
// 为什么使用原来的uv坐标却使用y轴镜像后的贴图,而且映射的位置还是对的
fUv.y = 1.0 - fUv.y;
vec4 eyeMouth = texture2D(eyeMouthTex, fUv);
float alpha = eyeMouth.a;
if (alpha == 0.0) {
gl_FragColor = mouth;
} else {
gl_FragColor = eyeMouth;
}
}顶点着色器中需要额外引入骨骼相关的预处理指令,不然在播放动画的时候会出现头动眼睛不动的情况
对于一般的材质可以这么做,但是导入的模型使用的是物理材质,简单的说就是贴图的实际颜色会受金属度、粗糙度、光照等的影响。
不难看出来相较于没有物理效果的,第二组没那么鲜艳
至于为什么第一组会表现出不同的颜色,这个涉及到色彩空间这个概念。
所以为了保留物理效果,就需要对MeshStandardMaterial的片元着色器进行扩展。
const fragmentParmas = `
uniform vec2 mouth_offset;
uniform sampler2D mouth_texture;
`
const fragmentStart = `
vec4 mouthColor = diffuseColor * texture2D(mouth_texture, vMapUv + mouth_offset);
`
const fragmentEnd = `
float alpha = diffuseColor.a;
if (alpha == 0.0) {
diffuseColor = mouthColor;
}
`
THREE.ShaderChunk['face_mix_pars_fragment'] = fragmentParmas
THREE.ShaderChunk['face_mix_fragment_start'] = fragmentStart
THREE.ShaderChunk['face_mix_fragment_end'] = fragmentEnd
material.onBeforeCompile = (shader) => {
Object.assign(shader.uniforms, uniforms)
const shaderList = shader.fragmentShader.split('\n')
// 变量声明
shaderList.splice(0, 0, '#include <face_mix_pars_fragment>')
// 在diffuseColor被纹理贴图的颜色污染之前计算
const index = shaderList.findIndex(item => item.includes('#include <map_fragment>'))
shaderList.splice(index, 0, '#include <face_mix_fragment_start>')
// 在计算完法线贴图后
const i = shaderList.findIndex(item => item.includes('#include <alphamap_fragment>'))
shaderList.splice(i + 1, 0, '#include <face_mix_fragment_end>')
shader.fragmentShader = shaderList.join('\n')
}






