您需要 登录 才可以下载或查看,没有账号?注册
x
本帖最后由 毒游搬运小行家 于 2021-4-2 10:45 编辑
《原神》角色渲染Shader分析还原!
文/游戏蛮牛
原神上线也半年了,卡通渲染水平属实一流。见了很多截帧分析的文章,也有很多人讨论原神的角色渲染。 但是目前还没看过整个角色Shader还原得比较好,讲解得比较详细的文章。因此比较擅长拾人牙慧的本不可燃垃圾,就把自己的Shader分享出来,详细地分析一下原神的角色渲染吧。
由于时间精力有限,并没有去逆向原神的Shader,仅从视觉效果角度去还原,因此也并未100%还原,今后有空会补完。使用渲染管线为URP。
注:即使去掉BaseMap以外所有的贴图,适当调参后,本文的Shader也可以做到不错的卡通渲染效果。请不要执着于模型和贴图资源。
去掉BaseMap以外所有额外贴图的渲染效果贴图
①RGBA通道的身体BaseMap ③RGBA通道的身体LightMap ④身体ShadowRamp ⑤面部BaseMap ⑥头发BaseMap ⑦RGBA通道的头发LightMap ⑧头发ShadowRamp ⑨面部阴影Mask ⑩金属光泽Map
本次还原并未用到②RGB通道的身体BaseMap ⑨面部阴影Mask ⑩金属光泽Map 。部分变量命名为本人随意命名。仅分析重点的片元着色器。
基础的卡通光照
LightMap的G通道:阴影权重
使用了【罪恶装备】的LightMap方案,随光照变化的一级阴影(ShallowShadowColor),不随光照变化的固定二级阴影(DarkShadowColor)。因此不能简单使用HalfLambert后Step去区分明暗部分。网络上可见崩坏3的渲染分析,经过适当修改(也可以不修改)即可用于原神的角色LightMap。 采样BaseMap和LightMap,确定最初的阴影颜色ShadowColor和DarkShadowColor 。 half4 baseColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv.xy);
half4 LightMapColor = SAMPLE_TEXTURE2D(_LightMap, sampler_LightMap, input.uv.xy);
half3 ShadowColor = baseColor.rgb * _ShadowMultColor.rgb;
half3 DarkShadowColor = baseColor.rgb * _DarkShadowMultColor.rgb;以下大致为崩坏3使用的计算一级阴影颜色ShallowShadowColor的算法(已被我修改过)。 //崩坏3原始算法
SWeight = LightMapColor.g * input.color.r;
float SFactor = 1.0f - step(0.5f, SWeight);
float2 halfFactor = SWeight * float2(1.2f, 1.25f) + float2(-0.1f, -0.125f);
SWeight = SFactor * halfFactor.x + (1.0f - SFactor) * halfFactor.y;
SWeight = floor((SWeight + input.lambert) * 0.5 + 1.0 - _ShadowArea);
SFactor = step(SWeight, 0);
ShadowColor.rgb = SFactor * baseColor.rgb + (1 - SFactor) * ShadowColor.rgb;经分析得知,可能考虑到采样贴图时的精度误差,或是为了迷惑逆向Shader的人,设计了这样复杂的算法。本人觉得必要不大,因此修改为下方的代码,效果差不多。 //如果SFactor = 0,ShallowShadowColor为一级阴影色,否则为BaseColor。
float SWeight = (LightMapColor.g * input.color.r + input.lambert) * 0.5 + 1.125;
float SFactor = floor(SWeight - _ShadowArea);
half3 ShallowShadowColor = SFactor * baseColor.rgb + (1 - SFactor) * ShadowColor.rgb;由于希望可选择是否固定二级阴影颜色DarkShadowColor,因此二级阴影颜色如下计算。 //如果SFactor = 0,DarkShadowColor为二级阴影色,否则为一级阴影色。
SFactor = floor(SWeight - _DarkShadowArea);
DarkShadowColor = SFactor * (_FixDarkShadow * ShadowColor + (1 - _FixDarkShadow) * ShallowShadowColor) + (1 - SFactor) * DarkShadowColor;这样阴影颜色的计算基本完成了,但是阴影边缘过于锐利,可以使用smoothstep进行平滑。 // 平滑阴影边缘
half rampS = smoothstep(0, _ShadowSmooth, input.lambert - _ShadowArea);
half rampDS = smoothstep(0, _DarkShadowSmooth, input.lambert - _DarkShadowArea);
ShallowShadowColor.rgb = lerp(ShadowColor, baseColor.rgb, rampS);
DarkShadowColor.rgb = lerp(DarkShadowColor.rgb, ShadowColor, rampDS);所有准备完成,该计算最终的片元使用哪一级阴影的颜色了。 //如果SFactor = 0,FinalColor为二级阴影,否则为一级阴影。
SFactor = floor(LightMapColor.g * input.color.r + 0.9f);
half4 FinalColor;
FinalColor.rgb = SFactor * ShallowShadowColor + (1 - SFactor) * DarkShadowColor;至此崩坏3的LightMap阴影计算大功告成!咦……不好意思,忘了是要还原原神的角色Shader了,应该使用ShadowRamp的贴图才对。但……多一种选择不是更好吗? RampShadow
原神角色的ShadowRamp贴图
LightMap的Alpha通道:RampAreaMask经分析得知,LightMap的Alpha通道存储了5种信息,Alpha值大致对应Ramp贴图内的材质/颜色如下。
0 hard/emission/specular/silk
77 soft/common
128 metal
179 tights
255 skin
而Ramp图中存储了10行颜色,前5行为暖色调阴影,后5行为冷色调阴影,分别对应游戏内白天和晚上的RampShadow。 需要使用HalfLambert进行横向采样,将10行颜色数据全部采样后存储为一个数组。X轴应当避免采样至贴图最最右边,否则会出现黑线,Y轴应当在每一行的尽量中间,避免精度问题。
左:_RampShadowRange - 0.003后采样的效果 右:直接采样的效果//关键的X轴rampValue,控制采样的范围,至于为什么这样写,你一定能看懂。
float rampValue = input.lambert * (1.0 / _RampShadowRange - 0.003);
//Y轴为固定数值
half3 ShadowRamp1 = SAMPLE_TEXTURE2D(_RampMap, sampler_RampMap, float2(rampValue, 0.95)).rgb;
half3 ShadowRamp2 = SAMPLE_TEXTURE2D(_RampMap, sampler_RampMap, float2(rampValue, 0.85)).rgb;
half3 ShadowRamp3 = SAMPLE_TEXTURE2D(_RampMap, sampler_RampMap, float2(rampValue, 0.75)).rgb;
half3 ShadowRamp4 = SAMPLE_TEXTURE2D(_RampMap, sampler_RampMap, float2(rampValue, 0.65)).rgb;
half3 ShadowRamp5 = SAMPLE_TEXTURE2D(_RampMap, sampler_RampMap, float2(rampValue, 0.55)).rgb;
half3 CoolShadowRamp1 = SAMPLE_TEXTURE2D(_RampMap, sampler_RampMap, float2(rampValue, 0.45)).rgb;
half3 CoolShadowRamp2 = SAMPLE_TEXTURE2D(_RampMap, sampler_RampMap, float2(rampValue, 0.35)).rgb;
half3 CoolShadowRamp3 = SAMPLE_TEXTURE2D(_RampMap, sampler_RampMap, float2(rampValue, 0.25)).rgb;
half3 CoolShadowRamp4 = SAMPLE_TEXTURE2D(_RampMap, sampler_RampMap, float2(rampValue, 0.15)).rgb;
half3 CoolShadowRamp5 = SAMPLE_TEXTURE2D(_RampMap, sampler_RampMap, float2(rampValue, 0.05)).rgb;
half3 AllRamps[10] = {
ShadowRamp1, ShadowRamp2, ShadowRamp3, ShadowRamp4, ShadowRamp5, CoolShadowRamp1, CoolShadowRamp2, CoolShadowRamp3, CoolShadowRamp4, CoolShadowRamp5
};根据材质面板内填写的LightMap的Alpha值与Ramp行数的对应关系,使用Step和abs判断Alpha值误差是否在一定范围内(我选10)后,将5行Ramp色彩进行组合得到最终的Ramp色彩数据,并再次使用_RampShadowRange和HalfLambert得到你想要的RampShadow。 half3 skinRamp = step(abs(LightMapColor.a * 255 - _RampArea12.x), 10) * AllRamps[_RampArea12.y]; // CoolShadowRamp2
half3 tightsRamp = step(abs(LightMapColor.a * 255 - _RampArea12.z), 10) * AllRamps[_RampArea12.w]; // CoolShadowRamp5
half3 softCommonRamp = step(abs(LightMapColor.a * 255 - _RampArea34.x), 10) * AllRamps[_RampArea34.y]; // CoolShadowRamp1
half3 hardSilkRamp = step(abs(LightMapColor.a * 255 - _RampArea34.z), 10) * AllRamps[_RampArea34.w]; // CoolShadowRamp3
half3 metalRamp = step(abs(LightMapColor.a * 255 - _RampArea5.x), 10) * AllRamps[_RampArea5.y]; // CoolShadowRamp4
// 组合5个Ramp,得到最终的Ramp阴影,并根据rampValue与BaseColor结合。
half3 finalRamp = skinRamp + tightsRamp + metalRamp + softCommonRamp + hardSilkRamp;
rampValue = step(_RampShadowRange, input.lambert);
half3 RampShadowColor = rampValue * baseColor.rgb + (1 - rampValue) * finalRamp * baseColor.rgb;
ShadowColor = RampShadowColor;
DarkShadowColor = RampShadowColor;
游戏内的Ramp阴影会随着时间切换为白天和夜晚,分别为1~5行和6~10行。具体可以使用C#传输时间值,或者根据光照方向计算时间或角度,使用Step或者Lerp,将上方存储的AllRamps[0~4]分别切换为AllRamps[5~9]即可完成RampShadow色彩的切换。由于时间精力问题,我并未去实现,感兴趣的可以自己试一试。
面部阴影
在还原原神的面部阴影渲染之前,先介绍下我想到的一个小技巧,这个技巧偶然间发现也被【Tales of Arise】使用,有兴趣、懂日语的话可以看看他们的技术分享。 由于面部阴影受光照角度影响极易产生难看的阴影,因此可以虑将光照固定成水平方向,再微调面部法线即可得到比较舒适的面部阴影。_FixLightY=0 即可将光照方向固定至水平。 float3 fixedlightDirWS = normalize(float3(lightDirWS.x, _FixLightY, lightDirWS.z));
lightDirWS = _IgnoreLightY ? fixedlightDirWS: lightDirWS;原神面部阴影的还原细节不多赘述,可以参考 @黑魔姬 的文章。 [backcolor=rgba(246, 246, 246, 0.88)]黑魔姬:神作面部阴影渲染还原zhuanlan.zhihu.com/p/279334552
在此我做了一些改进。由于直接使用会产生头发阴影和面部阴影交错的问题,需要对光照方向进行偏移。但直接在采样得到的FaceLightMap数据上±Offset等操作,会导致光照进入边缘时产生阴影跳变。因此采用旋转偏移光照的方式。只需要构建一个XZ平面上的旋转矩阵即可。 而光照在正前时,由于FaceLightMap的曲线变化问题,会导致阴影变化过快,因此需要修改FaceLightMap的曲线,使其在中间部分趋于平缓,使用pow函数即可做到,pow(0.15~0.3)之间效果最佳。 // FaceLightMap
#if ENABLE_FACE_SHADOW_MAP
// 计算光照旋转偏移
float sinx = sin(_FaceShadowOffset);
float cosx = cos(_FaceShadowOffset);
float2x2 rotationOffset = float2x2(cosx, -sinx, sinx, cosx);
float3 Front = unity_ObjectToWorld._12_22_32;
float3 Right = unity_ObjectToWorld._13_23_33;
float2 lightDir = mul(rotationOffset, mainLight.direction.xz);
//计算xz平面下的光照角度
float FrontL = dot(normalize(Front.xz), normalize(lightDir));
float RightL = dot(normalize(Right.xz), normalize(lightDir));
RightL = - (acos(RightL) / PI - 0.5) * 2;
//左右各采样一次FaceLightMap的阴影数据存于lightData
float2 lightData = float2(SAMPLE_TEXTURE2D(_FaceShadowMap, sampler_FaceShadowMap, float2(input.uv.x, input.uv.y)).r,
SAMPLE_TEXTURE2D(_FaceShadowMap, sampler_FaceShadowMap, float2(-input.uv.x, input.uv.y)).r);
//修改lightData的变化曲线,使中间大部分变化速度趋于平缓。
lightData = pow(abs(lightData), _FaceShadowMapPow);
//根据光照角度判断是否处于背光,使用正向还是反向的lightData。
float lightAttenuation = step(0, FrontL) * min(step(RightL, lightData.x), step(-RightL, lightData.y));
half3 FaceColor = lerp(ShadowColor.rgb, baseColor.rgb, lightAttenuation);
FinalColor.rgb = FaceColor;
#endif其他
LightMap的R通道:高光强度
LightMap的R通道用于控制高光强度,非0部分为Blinn-Phong高光。最大值255即纯白部分为Blinn-Phong加上金属高光,需要使用到下方的金属光泽贴图,使用方式疑似MatCap。
金属光泽贴图
本次并未还原金属光泽贴图的效果,今后会补上。
LightMap的B通道:高光Mask和一些细节?
原神的边缘光可以看出使用的应该是深度边缘光,详情参考 @喵刀Hime 的文章。 [backcolor=rgba(246, 246, 246, 0.88)]喵刀Hime:【JTRP】屏幕空间深度边缘光 Screen Space Depth Rimlightzhuanlan.zhihu.com/p/139290492
我嫌麻烦所以没加进去,只使用了普通的公式类似菲涅尔反射的边缘光,并用正向反向Lambert进行了Mask。 // Rim Light
float lambertF = dot(mainLight.direction, input.normalWS);
float lambertD = max(0, -lambertF);
lambertF = max(0, lambertF);
float rim = 1 - saturate(dot(viewDirWS, input.normalWS));
float rimDot = pow(rim, _RimPow);
rimDot = _EnableLambert * lambertF * rimDot + (1 - _EnableLambert) * rimDot;
float rimIntensity = smoothstep(0, _RimSmooth, rimDot);
half4 Rim = _EnableRim * pow(rimIntensity, 5) * _RimColor * baseColor;
Rim.a = _EnableRim * rimIntensity * _BloomFactor;
rimDot = pow(rim, _DarkSideRimPow);
rimDot = _EnableLambert * lambertD * rimDot + (1 - _EnableLambert) * rimDot;
rimIntensity = smoothstep(0, _DarkSideRimSmooth, rimDot);
half4 RimDS = _EnableRimDS * pow(rimIntensity, 5) * _DarkSideRimColor * baseColor;
RimDS.a = _EnableRimDS * rimIntensity * _BloomFactor;自发光&Bloom 原神使用了RGBA通道BaseMap的Alpha通道作为自发光Mask,经后处理达到Bloom的效果。此外我们也可以在高光和边缘光区域自行增加自发光,修改Alpha的值达到这些区域Bloom的效果。
最后还剩一张贴图,看名字就知道是做什么的了,面部阴影Mask,是否使用影响不是很关键。
描边 描边就是比较常规的BackFace外扩描边,直接复制粘贴Colin大佬的即可。此外原神使用了LightMap的Alpha通道(也有可能是顶点色)制作了彩色的描边,仅限皮肤区域,可以参考上方RampShadow中贴图信息的处理方式自行添加。 顶点色的Alpha通道控制描边粗细,RGB通道暂未分析出用来做了什么很特殊的效果。 https://github.com/ColinLeung-NiloCat/UnityURPToonLitShaderExample 在还原原神角色渲染的基础上,也可以自行增加一些有意思的东西。如我增加了自动着色、全彩色描边,本次的还原中并未加入。以下为完整Shader的Github链接。 https://github.com/ashyukiha/GenshinCharacterShaderZhihuVer
最终还原的效果
END
|