无极安卓网

原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)

来源:无极安卓网

原神动态贴图怎么制作,原神作为一款备受关注的游戏,其流畅的画面以及精美的人物设计深受玩家喜爱。许多玩家也开始尝试制作原神相关的动态贴图和MMD模型,但是制作过程中也会遇到各种问题和难点。在本文中我们将详细介绍原神动态贴图和MMD模型的制作流程,希望能为初入这一领域的玩家提供一定的帮助和指导。

从原神人物渲染到MMD制作的详细流程(小记)

序言

后续在此基础上学习了MMD的制作,大力出奇迹莽了几个视频出来,出乎意料火了几个。(非大会员画质的话会有点糊……)

原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)

于是就有了这篇个人小记,希望感兴趣点进来的你也可以有所收获。

还原的过程没有截帧逆向(太菜了),各种私人trick和不合理参数请见谅,参考各种大佬的分享(感谢!),所用到的模型和贴图资源来自网络,无意侵权(提前免责)。

卡通渲染特性简述

实现时,主要用到这些特性,漫反射,高光,边缘光,视角光,描边

对于光照模型的处理,卡通渲染里希望明快的色调对比,不希望有额外的过渡光照信息,所以会对光照模型(如半兰伯特)加一个Step做二分,以此表现出亮部和暗部。

原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)

二分后,进行smoothstep光滑过渡,可以方便采样ramp图

原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)

高光(如布林冯),边缘光,视角光(NdotV)等,同理也会进行Step做裁边处理。

描边通常的做法是基于模型沿法线方向挤出。本文实现提前对模型法线进行平滑处理,将平滑的法线存储到切线中。描边的粗细可以用顶点色的一个通道来控制,(比如顶点色的Alpha),卡通渲染里人物眼睛鼻子的地方通常不需要描边,那么就可以把这部分的Alpha通道填为零。

边缘光有常用的菲涅尔光,原神中的边缘光是等宽而且可被遮挡的,因此实现使用的是屏幕空间深度边缘光。

贴图与模型(甘雨为例)

在网上获取到原神的角色贴图和模型,分析后个人结论如下

原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)

【身体+头发+脸部】Diffuse:基础漫反射颜色

【身体+头发】LightMap.r::高光类型Layer,根据值域选择不同的高光类型

【身体+头发】LightMap.g:阴影AO ShadowAOMask

【身体+头发】LightMap.b:BlinPhong高光强度

【身体+头发】LightMap.a:Ramp类型Layer,根据值域选择不同的Ramp

【身体+头发】RampTex:不同行数的渐变色,上部分为暖色调渐变,下部分为冷色调渐变

原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)

【身体+头发】MetalMap:模拟金属反射的matcap

【脸部】SDF图平滑过渡+脸部阴影遮罩

VertexColor.g:Ramp偏移值,值越大的区域 越容易"感光"(在一个特定的角度,偏移光照明暗)

VertexColor.a:描边粗细

模型分为几个部分:

原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)

因此主要写的shader也就是三个,身体+头发+脸

角色渲染(以甘雨为例)身体

分为几个部分记录

1.漫反射分层

身体漫反射主要使用分层采样ramp

思路是根据LightMap.a通道,结合光照模型(半兰伯特)的范围,分层采样ramp图赋予漫反射颜色。

身体LightMap.a通道如下图

同时有一些个性化调整

比如需要顶点色偏移基础的半兰伯特,根据观察,脖子处无ramp分界效果,因此将手动把顶点色G通道涂黑了。

又比如,甘雨根据我的观察结果,LightMap.a为0的那一层(上上张图的黑色部分,游戏中的胸和腿部分),好像有两层,于是做了特殊处理。

原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)

对半兰伯特进行裁边与软化,可将其作为采样ramp图的X坐标。

/*==========================Diffuse核心:叠加阴影区域,分层采样ramp ==========================*/
// Y轴采样尽量在每一行的ramp颜色中间
float RampPixelY = 0.03125; // 0.03125=1/16/2  16行的第1行正中间

// ramp图为256*16,计算一个像素的宽度
float RampPixelX = 0.00390625; //1.0/256.0

// 半兰伯特
// 计算,若顶点色的G没有颜色,则需要-1去抵消
float halfLambert = (NL * 0.5 + 0.5 + RampOffsetMask - 1) / _RampOffset;
// 第二层半兰伯特(身上疑似有)
float halfLambert2;
halfLambert2 = halfLambert * _RampOffset / (_RampOffset + 0.22);

// 半兰伯特软硬
halfLambert = smoothstep(0, _ShadowSmooth, halfLambert);
halfLambert2 = smoothstep(0, _ShadowSmooth, halfLambert2);
// 去掉最左最右,避免之后采样到边界上
halfLambert = clamp(halfLambert, RampPixelX, 1 - RampPixelX);
halfLambert2 = clamp(halfLambert2, RampPixelX, 1 - RampPixelX);

游戏中皮肤与布料层,对ramp图进行了采样,而LightMap.a接近0的那一层黑丝,ramp很不明显,因此对该层特殊处理,做一个step裁边。

原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)
/*甘雨的body shadow ramp图
竖着16个像素
一共 5层彩+3层白+5层彩+3层白
但是lightmap只有三层
5层中,怀疑前三行肯定对应lightmap的三层
后两层应该别有用处
注意,不同角色的ramp图行数不一样*/

float RampIndex = 1;

// LayerMask接近0的部分的半兰伯特,根据观察没有渐变,因此step
if (LayerMask >= 0 && LayerMask <= 0.3)
{
    RampIndex = 1;
    halfLambert2 = step(_LightThreshold, halfLambert2);
    halfLambert = step(_LightThreshold, halfLambert);
}

if (LayerMask >= 0.31 && LayerMask <= 0.61)
{
    RampIndex = 2;
}

if (LayerMask >= 0.61 && LayerMask <= 1.0)
{
    RampIndex = 3;
}

得到半兰伯特作为X坐标,以及计算出ramp每行的正中间Y坐标,对阴影和AO区域进行ramp采样,AO区域同样也做了裁边处理。

// 分类采样Ramp
// 取每行的中间
float PixelInRamp = 1 - RampPixelY * (2 * RampIndex - 1);

// 常暗AO区域
ShadowAOMask = 1 - smoothstep(saturate(ShadowAOMask), 0.06, 0.6); //平滑ShadowAOMask

// 常暗AO+半兰伯特 的 阴影ramp
float3 ramp = tex2D(_RampMap, float2(halfLambert* ShadowAOMask, PixelInRamp));
原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)

给阴影范围上BaseMap,根据明暗交界线叠加

// BaseMap叠加常暗AO
float3 BaseMapShadowed = lerp(BaseMap * ramp*_AOColor, BaseMap, ShadowAOMask);

// 控制常暗AO的强度并叠加第二层兰伯特(trick)
float SecondHalfLambert = halfLambert * ShadowAOMask - halfLambert2 * ShadowAOMask;
BaseMapShadowed = lerp(BaseMap, BaseMapShadowed, _ShadowRampLerp) - 0.07*BaseMap * SecondHalfLambert;


// 明暗分界叠加
float IsBrightSide = ShadowAOMask * step(_LightThreshold, halfLambert* ShadowAOMask);

float3 Diffuse = lerp(lerp(BaseMapShadowed, BaseMap * ramp, _RampLerp) * _DarkIntensity,
		      _BrightIntensity * BaseMapShadowed,
		      IsBrightSide * _RampIntensity * 1) * _CharacterIntensity;
原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)

2.高光部分

高光使用LightMap.r通道(如下图)来控制各种高光表现

原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)

LightMap.r的灰色区域,根据观察,有裁边视角光

原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)

根据该层特性,进行编写

/*==========================高光核心:逐层高光叠加 ==========================*/
float3 FinalSpecular = 0;
float3 Specular = 0;
float3 StepSpecular = 0;

float SpecularLayer255 = LightMap.r*255 ;

// 高光分类 不同的高光层 LightMap.b 用途不一样
// 裁边高光层1
// 甘雨的铃铛 绳子
if (SpecularLayer255 > 120 && SpecularLayer255 < 254)
{
    StepSpecular = step(1 - _StepSpecularWidth, saturate(dot(N, V))) * 1 *_StepSpecularIntensity;
}
原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)

金属层(也就是lightmap.r的白色层),具有基础的布林冯和使一张MatCap图来做金属的裁边视角光。

原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)
// 金属部分matcap图采样
// N转到0-1区间采样
float MetalMap = saturate(tex2D(_MetalMap, mul((float3x3)UNITY_MATRIX_V, N).xy * 0.5f + 0.5f).r);
// 二值化 金属的光
MetalMap = step(_MetalMapV, MetalMap)*_MetalMapIntensity;

// 基础高光
// BlinPhong高光 + 金属高光
if (SpecularLayer255 >= 200)
{
	Specular = pow(saturate(NH), 1 * _SpecularExp) * SpecularIntensityMask *_SpecularIntensity;	
	Specular = max(0, Specular);		
	Specular += MetalMap;
}
原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)

带上贴图颜色,配合明暗交界范围,得到高光区域

Specular *= BaseMap;
Specular = lerp(StepSpecular, Specular, SpecularLayerMask );
Specular = lerp(0, Specular, SpecularLayerMask );
Specular *= IsBrightSide;

3.自发光为角色的神之眼部分,step取出mask单独给强度即可

4.边缘光

游戏中人物的边缘光始终是等宽的,并且不会受到视角变化的影响。

原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)

因此否定了菲涅尔的NdotV做法,实现中采用了屏幕空间对物体进行深度偏移采样,根据深度差做边缘光计算。

开启相机的深度图模式

原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)

在shader中沿着观察空间法线偏移视口坐标,采样深度,根据阈值判断是不是属于边缘光的范围,即可实现等宽边缘光。

/*==========================屏幕空间深度边缘光 ==========================*/
float2 screenParams01 = float2(i.pos.x / _ScreenParams.x, i.pos.y / _ScreenParams.y);
// 沿着法线外扩检测
float2 offectSamplePos = screenParams01 + float2(i.normalVS.xy * _OffsetMul / i.pos.w);
// 检测深度进行对比
float offcetDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, offectSamplePos);
float trueDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, screenParams01);
float linear01EyeOffectDepth = Linear01Depth(offcetDepth);
float linear01EyeTrueDepth = Linear01Depth(trueDepth);

float depthDiffer = linear01EyeOffectDepth - linear01EyeTrueDepth;
float rimMask = step(_Threshold, depthDiffer);

float4 RimCol = float4(rimMask*_RimColor.rgb*_RimColor.a,1);

5.描边

描边采用将平滑法线存于切线中进行外扩的方式处理,根据观察,角色在游戏中的描边,根据位置不同有不同的颜色变化,于是初步猜想是根据lightmap.a的不同区域给了不同的描边颜色(下图为游戏中不同描边颜色)。

原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)

新开一个pass,进行描边操作

// 平滑法线存于切线中进行外扩描边
v2f vert(appdata v)
{
    v2f o;
    v.vertex.xyz += v.tangent.xyz *_OulineScale*0.01*v.vertexColor.a;
	//v.vertex.xyz += v.normal.xyz *_OulineScale*0.01*v.vertexColor.a;
	o.uv = v.uv;

    o.pos = UnityObjectToClipPos(v.vertex);
    return o;
}
// 描边 对着lightmap.a,不同区域描边颜色不同

float4 frag(v2f i) : SV_Target
{
	float2 uv = i.uv;
	float4 LightMap = tex2D(_LightMap, uv);
	float LayerMask = LightMap.a;
	float4 BaseMap = tex2D(_BaseMap, uv);
	float4 OutColor = 1;
	if (LayerMask >= 0 && LayerMask <= 0.3)
	{
		OutColor.rgb = _ColorHeiBian * BaseMap;
	}

	if (LayerMask >= 0.31 && LayerMask <= 0.61)
	{
		OutColor.rgb = _ColorBuliao* BaseMap;
	}

	if (LayerMask >= 0.61 && LayerMask <= 1.0)
	{
		OutColor.rgb = _ColorPiFu;
	}
	return OutColor;
}
原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)

关于平滑法线,直接使用法线外扩,会断断续续,因为顶点上的法线不止1个,而是被分成了好几个方向(如左图的硬边,法线朝多个方向,右边为软边的平滑法线)。

原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)

但是也不可能去直接光滑组可以合并法线,于是可以在unity中编写一个工具,求出一个顶点所在的所有三角面的法线的平均值,存到模型的切线中,方便使用。

左为原本的法线,右为存在切线中的光滑法线。

原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)头发

头发与身体的步骤大同小异,Diffuse方面半兰伯特采样ramp(头发根据观察,就不存在第二次半兰伯特了)。

由于其lightmap.a只有黑白层(头发和角),ramp图是4层彩+4层白+4层彩+4层白,与身体的不一样,因此只要两层。其他部分的Diffuse和身体处理基本一致。

/*甘雨的hair shadow ramp图
竖着16个像素
4层彩+4层白+4层彩+4层白
但是lightmap.a只有黑白层,也就暂时只要两层
不同角色的不一样*/
float RampIndex = 1;
if (LayerMask >= 0 && LayerMask <= 0.3)
{
   RampIndex = 1;
  
}
if (LayerMask >= 0.61 && LayerMask <= 1.0)
{
   RampIndex = 2;
}

对于高光处理,根据观察游戏中的头发高光层,发现其与Diffuse层是独立的(如下图),因此对头发的高光做一次裁边视角光。

原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)
// 甘雨高光就俩层
// 一层裁边高光 (高光在暗部消失)
if (SpecularLayer > 100 && SpecularLayer < 160)
{
    StepSpecular = step(1 - _StepSpecularWidth, saturate(dot(N, V))) * 1 *_StepSpecularIntensity;
    StepSpecular *= BaseMap;	
    StepSpecular = lerp(0, StepSpecular, IsBrightSide);
    StepSpecular *= SpecularIntensityMask;	
}

同时根据其lightmap.a对角色头部的角也做了分层,因此对角的高光也单独做一层裁边视角光。

// 一层头发的特殊高光(甘雨的角)
float3 HairSpecular = 0;
if (SpecularLayer >= 160 && SpecularLayer<250)
{
    float SpecularRange = step(1 - _HairSpecularRange, saturate(NH));
    float ViewRange = step(1 - _HairSpecularViewRange, saturate(NV));
    HairSpecular = SpecularIntensityMask *_HairSpecularIntensity * SpecularRange * ViewRange;
    HairSpecular = max(0, HairSpecular);
}

边缘光和描边操作与身体的处理一样。

脸部是用SDF生成了平滑的过渡,一张mask限制阴影范围,SDF在游戏中利用阈值来控制。控制的阈值是光线与前方向量的夹角。

算法:将灯光方向转到局部坐标,求出变换后的XZ极坐标,去step 脸部SDF图做明暗过渡。

由于直接使用采样,会产生头发阴影和面部阴影交错的问题,会需要对光照方向进行偏移。但直接在采样得到的FaceLightMap数据上±Offset的操作,会导致光照进入边缘时产生阴影跳变。

因此采用旋转偏移光照的方式。构建一个XZ平面上的旋转矩阵,参数Offset变为旋转光方向。

/*==========================Face ==========================*/
float3 faceLightMap = tex2D(_LightMap, float2(uv.x, uv.y));

float3 Up    = mul(unity_ObjectToWorld, float4(0,1,0,0));                     
float4 Front = mul(unity_ObjectToWorld, float4(0,0,1,0));

float3 Left = cross(Up,Front);
float3 Right = -Left;

// 旋转偏移光照
// 直接在采样得到的FaceLightMap数据上±Offset等操作,会导致光照进入边缘时产生阴影跳变。因此采用旋转偏移光照的方式
// FaceLightMap

// 计算光照旋转偏移
float sinx = sin(_FaceShadowOffset);
float cosx = cos(_FaceShadowOffset);
float2x2 rotationOffset = float2x2(cosx, -sinx, sinx, cosx);
float2 lightDir = mul(rotationOffset, L.xz);

//计算xz平面下的光照角度
float FrontL = dot(normalize(Front.xz), normalize(lightDir));
float RightL = dot(normalize(Right.xz), normalize(lightDir));
RightL = - (acos(RightL) / 3.141592654 - 0.5) * 2;


//左右各采样一次FaceLightMap的阴影数据存于lightData
float2 lightData = float2(tex2D(_LightMap, float2(uv.x, uv.y)).r,tex2D(_LightMap, float2(-uv.x, uv.y)).r);
//return lightData.gggg;

//修改lightData的变化曲线,使中间大部分变化速度趋于平缓。
lightData = pow(abs(lightData), _FaceShadowMapPow);

//根据光照角度判断是否处于背光,使用正向还是反向的lightData。
float lightAttenuation = ShadowMap * step(0, FrontL) * min(step(RightL, lightData.x), step(-RightL, lightData.y));
//return lightAttenuation.rrrr;

half3 FaceColor = lerp(_ShadowColor * BaseMap, BaseMap, lightAttenuation);
FinalColor.rgb = FaceColor;

脸部描边方法也和之前一样,只不过眼睛鼻子等部分不需要描边。所以使用对顶点色进行了绘制

原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)整体呈现

整体效果可以看文章开头的MMD视频,下图是当时初次效果的图

原神动态贴图怎么制作 从原神人物渲染到MMD制作的详细流程(小记)

除了甘雨以外,如最开始的MMD视频所示,还做过其他一些角色,每尝试一个新角色都会对shader进行一些调整

不同角色的lightmap通道里分层,ramp图等等会不太一样,因此有不同数量的裁边视角高光,描边色,ramp采样方法等等,不过大体结构是一致的,想要效果好就多和游戏里对比对比改改。

用Unity制作MMD

列了用到的几个Unity与MMD之间如何导入导出的教程

大致涵盖了,模型,材质,表情,动作这几块的导入

导入表情重点!

MMD的使用教程如下

以下是我自己摸索的,也不是太熟怎么做MMD

下载MikuMikuDance(有用于跨软件导出的bridge版和制作用的汉化版),在制作用的MMD中导入下载好的相机,动作,模型,然后调动作文件防止明显的穿模

(MMD资源下载可以去模之屋 专业模型创作分享社区_模之屋_PlayBox,以及MMD大佬们的配布)

模型和动作的导入,需要在unity中使用MMD插件 MMD4Mecanim 下载地址→Stereoarts Homepage

将mmd的pmx文件,通过该插件导出为FBX,根据上面教程的操作,完成表情动画,动作,物理等设置,然后自己调调参数。

相机可以用unity自带的相机。

如果要用MMD里vmd文件的相机,个人做法(比较莽且笨重。求指教方法):用MMD的bridge版,导出带有角色和相机的abc动画(因为发现纯相机的abc导不出),导入unity时注意设置缩放倍数,用timeline来播放相机abc动画,unity运行时写个脚本把自带导出的角色隐藏。

Unity录屏采用AVPro movie capture这个插件。

后记

写的也比较凌乱,实际过程很多时候都是不断试错和不优美的大力出奇迹(求各路大神指教!)

但最后从学习技术到制作出被喜欢的MMD作品,还是很开心的。

原神动态贴图的制作需要耗费一定的时间和精力,但是最终的效果却是非常惊艳的。期待大家能够通过本篇小记了解到更多制作动态贴图的流程和技巧,希望大家能够在未来的创作中有所收获,走出自己的独特风格。

相关文章

猜你喜欢