Skip to content

关于《Blue Archive》人物模型嘴部贴图的处理方法 #15

@SepVeneto

Description

@SepVeneto

目的

将模型提取出来放到网页中并查看模型的动画

解包的方法就不赘述了,由于碧蓝档案(下称ba)是基于Unity开发的,所以相关的解包工具网上是可以找到的。

image

模型提取

由于我们是需要在线查看模型和动画,以AssetStudioGUI为例,需要找到对应人物的Animator以及相关的AnimationClip,这里有一点需要注意的是,导出的模型可能会缺少光环。这里以Hina为例,两个与模型有关的文件分别是Hina_OriginalHina_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. 骨骼动画错误

image

关于这一点,因为之前从来没接触过,所以并不知道导致这个问题的原因是什么。只能大胆猜测AssetStudioGUI导出的数据格式有问题。

我的解决方法是将解包出的Fbx模型先导入到Unity中,再通过插件gltf-exporter作为Gltf导出即可。

嘴部贴图

image

可以看到嘴部周围有一圈很规则的白色区域。

这是因为hina眼睛和嘴的贴图(eyeMouth)就是这样的
image

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

根据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将其视为透明材质。

但是实际渲染的结果却很奇怪。
image
可以看到眼睛有很明显的透明度渐变的现象。

这是因为模型的geomertry上设置了顶点颜色,而顶点颜色中指定了眼睛的部分顶点是存在透明度的,所以当transparent设置为true时,看上去是部分透明的。

解决方法很简单

const eyeMouth = obj.getObjectByName('Hina_Original_Body_3')
eyeMouth.material.vertexColors = false

至于出现这个问题的根本原因应该是在Fbx转GLTF的时候出了问题。

gltf-exporter导出的模型
image

assetStudio导出的模型
image

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;
  }
}

顶点着色器中需要额外引入骨骼相关的预处理指令,不然在播放动画的时候会出现头动眼睛不动的情况

对于一般的材质可以这么做,但是导入的模型使用的是物理材质,简单的说就是贴图的实际颜色会受金属度、粗糙度、光照等的影响。

不难看出来相较于没有物理效果的,第二组没那么鲜艳

image
image

image

至于为什么第一组会表现出不同的颜色,这个涉及到色彩空间这个概念。

所以为了保留物理效果,就需要对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')
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions