TA-Shader-文件 《原神》角色渲染Shader分析还原!
发布于
2021-4-2
36289
14
TA资源类型
TA资源类型: 算法思路 
shader资源类型: shader代码 
适用引擎: unity 
资源介绍: -

您需要 登录 才可以下载或查看,没有账号?注册

x
本帖最后由 毒游搬运小行家 于 2021-4-2 10:45 编辑

《原神》角色渲染Shader分析还原!

文/游戏蛮牛


序言:
原神上线也半年了,卡通渲染水平属实一流。见了很多截帧分析的文章,也有很多人讨论原神的角色渲染。
但是目前还没看过整个角色Shader还原得比较好,讲解得比较详细的文章。因此比较擅长拾人牙慧的本不可燃垃圾,就把自己的Shader分享出来,详细地分析一下原神的角色渲染吧。

由于时间精力有限,并没有去逆向原神的Shader,仅从视觉效果角度去还原,因此也并未100%还原,今后有空会补完。使用渲染管线为URP。
注:即使去掉BaseMap以外所有的贴图,适当调参后,本文的Shader也可以做到不错的卡通渲染效果。请不要执着于模型和贴图资源。 640

去掉BaseMap以外所有额外贴图的渲染效果贴图


以刻晴为例,以下为需要用到的贴图(来源于大佬):
640



①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





参与人数 4 元素币 +35 活跃度 +76
天若有情天亦老, 人间正道是沧桑。

使用道具 举报 登录

回复 <
黄小厨  发表于 2021-4-2 10:37:36  
2#
很好的资源,很有诚意
回复 收起回复
使用道具
s458613  发表于 2021-4-2 19:44:43  
3#
非明觉厉
回复 收起回复
使用道具
纳尼我靠尼玛  发表于 2021-4-5 10:16:54  
4#
回复 收起回复
使用道具
digua139  发表于 2021-4-8 09:38:08  
5#
这次来看看东西好不好
回复 收起回复
使用道具
gxz040721  发表于 2021-5-24 14:16:04  
6#
牛逼
回复 收起回复
使用道具
dyzy2000  发表于 2021-5-26 15:58:21  
7#
回复 收起回复
使用道具
〃所以然  发表于 2021-6-18 10:39:10  
8#
66666666666666666666666666666666
回复 收起回复
使用道具
lsy丶  发表于 2021-10-27 15:21:47  
9#
很棒
回复 收起回复
使用道具
SoupRice  发表于 2021-11-12 11:33:29  
10#
流劈,谢谢分享!
回复 收起回复
使用道具
100069  发表于 2022-2-8 09:29:10  
11#
千点万点,不如微元素指点
回复 收起回复
使用道具
jxw167  发表于 2023-2-2 11:05:19  
12#
谢谢分享,不错滴!
回复 收起回复
使用道具
liubing129  发表于 2023-2-14 11:26:06  
13#
看得人一脸萌逼
回复 收起回复
使用道具
乔克大魔王  发表于 2023-8-23 18:59:17  
14#
求shader
回复 收起回复
使用道具
jhuh  发表于 2023-9-24 08:53:29  
15#
666
回复 收起回复
使用道具

快来发表你宝贵的意见吧!

毒游小行家 实名

通过了实名认证的内容创造者

吸鼠霸王

主题
274
精华
57
超神
0
扩散
192
微金
1366
智慧
199
余额
9
在线时间
2994 小时

翡翠剑 【绝】红龙战甲 英雄盾 铁剑 钢盾 钢剑 长剑 元素铜币 元素银币 元素金币 短杖 【绝】珍珠戒指 【绝】黄金项链 碧之轨迹 长枪 魔神战甲 裁决 火元素 赤色药水 紫色药水 钢铁矿镐 彗星钛晶矿 阿尔法晶矿 守望者【EX】 希望人没事 振金项链 满天星 黑珍珠戒指 玫瑰金项链 烈-红龙战甲 无色原始矿 绿母翠晶矿 粒粒皆辛苦

快速回复 返回顶部 返回列表