原神动态贴图怎么制作,原神作为一款备受关注的游戏,其流畅的画面以及精美的人物设计深受玩家喜爱。许多玩家也开始尝试制作原神相关的动态贴图和MMD模型,但是制作过程中也会遇到各种问题和难点。在本文中我们将详细介绍原神动态贴图和MMD模型的制作流程,希望能为初入这一领域的玩家提供一定的帮助和指导。
从原神人物渲染到MMD制作的详细流程(小记)
序言后续在此基础上学习了MMD的制作,大力出奇迹莽了几个视频出来,出乎意料火了几个。(非大会员画质的话会有点糊……)
于是就有了这篇个人小记,希望感兴趣点进来的你也可以有所收获。
还原的过程没有截帧逆向(太菜了),各种私人trick和不合理参数请见谅,参考各种大佬的分享(感谢!),所用到的模型和贴图资源来自网络,无意侵权(提前免责)。
卡通渲染特性简述实现时,主要用到这些特性,漫反射,高光,边缘光,视角光,描边
对于光照模型的处理,卡通渲染里希望明快的色调对比,不希望有额外的过渡光照信息,所以会对光照模型(如半兰伯特)加一个Step做二分,以此表现出亮部和暗部。
二分后,进行smoothstep光滑过渡,可以方便采样ramp图
高光(如布林冯),边缘光,视角光(NdotV)等,同理也会进行Step做裁边处理。
描边通常的做法是基于模型沿法线方向挤出。本文实现提前对模型法线进行平滑处理,将平滑的法线存储到切线中。描边的粗细可以用顶点色的一个通道来控制,(比如顶点色的Alpha),卡通渲染里人物眼睛鼻子的地方通常不需要描边,那么就可以把这部分的Alpha通道填为零。
边缘光有常用的菲涅尔光,原神中的边缘光是等宽而且可被遮挡的,因此实现使用的是屏幕空间深度边缘光。
贴图与模型(甘雨为例)在网上获取到原神的角色贴图和模型,分析后个人结论如下
【身体+头发+脸部】Diffuse:基础漫反射颜色
【身体+头发】LightMap.r::高光类型Layer,根据值域选择不同的高光类型
【身体+头发】LightMap.g:阴影AO ShadowAOMask
【身体+头发】LightMap.b:BlinPhong高光强度
【身体+头发】LightMap.a:Ramp类型Layer,根据值域选择不同的Ramp
【身体+头发】RampTex:不同行数的渐变色,上部分为暖色调渐变,下部分为冷色调渐变
【身体+头发】MetalMap:模拟金属反射的matcap
【脸部】SDF图平滑过渡+脸部阴影遮罩
VertexColor.g:Ramp偏移值,值越大的区域 越容易"感光"(在一个特定的角度,偏移光照明暗)
VertexColor.a:描边粗细
模型分为几个部分:
因此主要写的shader也就是三个,身体+头发+脸
角色渲染(以甘雨为例)身体分为几个部分记录
1.漫反射分层
身体漫反射主要使用分层采样ramp
思路是根据LightMap.a通道,结合光照模型(半兰伯特)的范围,分层采样ramp图赋予漫反射颜色。
身体LightMap.a通道如下图
同时有一些个性化调整
比如需要顶点色偏移基础的半兰伯特,根据观察,脖子处无ramp分界效果,因此将手动把顶点色G通道涂黑了。
又比如,甘雨根据我的观察结果,LightMap.a为0的那一层(上上张图的黑色部分,游戏中的胸和腿部分),好像有两层,于是做了特殊处理。
对半兰伯特进行裁边与软化,可将其作为采样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裁边。
/*甘雨的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));
给阴影范围上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;
2.高光部分
高光使用LightMap.r通道(如下图)来控制各种高光表现
LightMap.r的灰色区域,根据观察,有裁边视角光
根据该层特性,进行编写
/*==========================高光核心:逐层高光叠加 ==========================*/ 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; }
金属层(也就是lightmap.r的白色层),具有基础的布林冯和使一张MatCap图来做金属的裁边视角光。
// 金属部分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; }
带上贴图颜色,配合明暗交界范围,得到高光区域
Specular *= BaseMap; Specular = lerp(StepSpecular, Specular, SpecularLayerMask ); Specular = lerp(0, Specular, SpecularLayerMask ); Specular *= IsBrightSide;
3.自发光为角色的神之眼部分,step取出mask单独给强度即可
4.边缘光
游戏中人物的边缘光始终是等宽的,并且不会受到视角变化的影响。
因此否定了菲涅尔的NdotV做法,实现中采用了屏幕空间对物体进行深度偏移采样,根据深度差做边缘光计算。
开启相机的深度图模式
在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的不同区域给了不同的描边颜色(下图为游戏中不同描边颜色)。
新开一个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; }
关于平滑法线,直接使用法线外扩,会断断续续,因为顶点上的法线不止1个,而是被分成了好几个方向(如左图的硬边,法线朝多个方向,右边为软边的平滑法线)。
但是也不可能去直接光滑组可以合并法线,于是可以在unity中编写一个工具,求出一个顶点所在的所有三角面的法线的平均值,存到模型的切线中,方便使用。
左为原本的法线,右为存在切线中的光滑法线。
头发头发与身体的步骤大同小异,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层是独立的(如下图),因此对头发的高光做一次裁边视角光。
// 甘雨高光就俩层 // 一层裁边高光 (高光在暗部消失) 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视频所示,还做过其他一些角色,每尝试一个新角色都会对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作品,还是很开心的。
原神动态贴图的制作需要耗费一定的时间和精力,但是最终的效果却是非常惊艳的。期待大家能够通过本篇小记了解到更多制作动态贴图的流程和技巧,希望大家能够在未来的创作中有所收获,走出自己的独特风格。