因为非常喜欢Arc System Works的角色渲染,在学习shader一段时间后尝试进行还原。本文还原的效果仅作参考,是学习过程中一些踩坑的总结和经验,如果有写的不对的地方请指出.

贴图分析

还原任何角色渲染效果最重要的一步就是进行贴图分析,因为碧蓝幻想是用UE4引擎开发的游戏,所以我们可以简单的用umodel-/**&……%*&获得一些资源。下面我们来看一下它的贴图。Alt text
一共有5张,分别是头发加身体加脸部的basecolor,一张ssscolor(暗一点的basecolor),一张细节描线图,一张用于叠加的deacl以及ILM的四个通道还有几个贴图的alpha通道需要操作,我们后面说:

BaseColor


就是基本色,basecolor的A通道用于区分皮肤区域和头发区域,如下图
Alt text

ssscolor

Alt text
颜色深一点的basecolor用于表现阴影部分颜色,命名sss还蛮奇怪的

ILM

R通道:金属光泽度 用于高光强度与高光mask
Alt text
G通道:和罪恶装备里一样作为阴影倾向权重
Alt text
B通道:控制高光形状
Alt text
A通道:内描线细节
Alt text
4.detail 更多的细节描线
Alt text

角色shader还原

直接光漫反射

我们先来看一下游戏里的截图
Alt textAlt text
可以看出他的亮部与暗部交界处是一个很硬的边缘,且只有一级阴影。那就很简单的对兰伯特或者半兰伯特光照模型做色阶化处理得到亮部与暗部的mask将basecolor与ssscolor进行一个lerp就完成了。先来看看我们的mask
Alt text
然后有些区域应该是黑色的,但却是白色的,这时候我们就要去修正这个错误,上面我们说道有些贴图的A通道里面有一些信息,那我们就去找找看。emmmm…好像并没有可以修正这个错误的信息。还有那个地方我们没找呢?是顶点色!我们再去仔细寻找,在顶点色的R通道发现了好像 AO的东西
Alt text
我觉得它就是,那我们就乘上AO再看看,就会发现这个错误被修正了。利用顶点色的A通道制作AO的可控性很高,完全可以又美术自主定制那些需要,那里深一点,那里浅一点
Alt text
最后我们进行一个Lerp的混,就完成了我们的直接光漫反射部分。不得不说arc的资源做的真的好,仅仅靠一个漫反射就有这么好的效果
Alt text

1
2
3
4
half half_lambert = (NdotL + 1.0) * 0.5;    //从-1.0-1.0映射到0.0-1.0               
half lambert_term = half_lambert * ao + shadow_control; //shadow_control就是上面所说的阴影倾向权重
half toon_diffuse = saturate((lambert_term - _ShadowThreshold) * _ShadowHardness); //色阶化处理得到亮暗部分mask
half3 dir_diffuse = lerp(sss_col, base_col, toon_diffuse);//进行混合

直接光镜面反射

我们再来看一下游戏里的金属表现与高光表现
Alt textAlt textAlt textAlt text
从贴图分析我们知道arc对高光的处理下了一番功夫,不仅画了高光强度,也画了高光大小的mask。从生活中我们知道高光是会随着视角的变化而发生变化,所以我们要使用的光照模型是Phong或者blinnPhong,当然可以。但测试下来我选择用最简单的菲涅尔反射来操作,因为这么好的贴图一定要好好利用上。

先来看看我们的菲涅尔(NdotV),这里已经事先乘过AO并加上阴影权重了
Alt text
但这个效果不是很好,并且经过仔仔仔仔仔——细观察,感觉高光不是那么纯,像是有一部分漫反射混在里面,并且漫反射的权重还不小。在经过本人不怎么好的眼力与审美观察后。效果较好的表现为反射因子=NdotV(0.1—0.3)+ halflambert(0.7—0.9)之间。下面是0.1NdotV+0.9halflambert的效果
Alt text
同样的我们也需要对高光进行色阶化的处理,并且利用上贴图
Alt text
specsize是金属光泽度,这里反向了一下是因为调参数的时候是往负方向调整,与美术直觉不符合。spec_intensity则是高光区域mask与不同的金属度,乘以300是为了提亮并得到硬边缘。

1
half toon_spec = saturate((spec_term - (1.0 - spec_size * _SpecSize)) * spec_intensity * 300);//色阶化处理得到高光mask

那我们现在有了高光mask就可以开始上色了,定义一个_SpecColor控制颜色,定义一个_SpecIntensity控制强度
Alt text

1
2
3
4
5
half spec_term = (NdotV + 1.0) * 0.5 * ao + shadow_control;
spec_term = spec_term * 0.1 + half_lambert * 0.9; //反射所占权重
half toon_spec = saturate((spec_term - (1.0 - spec_size * _SpecSize)) * spec_intensity * 300);//色阶化处理得到高光mask
half3 spec_col = _SpecColor.xyz * 0.6 + base_col * 0.4; //希望高光颜色带有basecolor的倾向
half3 dir_spec = toon_spec * spec_col * _SpecIntensity;//进行混合

直接光漫反射加直接光镜面发射的效果,已经有点味道了
Alt text

环境光漫反射

并没有什么特别的处理,尝试用了unity自带的球谐光照,对效果也没有特别大的改善,可能需要一个合适的天空盒才会有更好的表现,这里直接将ssscol * 0.1叠加上去了,虽然影响十分微弱

环境光镜面反射

游戏里没做,我也没做+v+,或许可以用vrdirWS采样一张cubemap来做,但目前的效果还挺好,就不做了,诶嘿
Alt text

描线

先来用贴图来做内描线,detail+ILM的A通道,这里的detail要用UV2去采,因为使用了本村线的做法
Alt text
Alt text

什么是本村线呢?
Alt textAlt textAlt textAlt text

GGX贴图采用独特的uv分布方式(本村式线),其原理在于用垂直的黑线来表示内部黑线,从而防止45度线导致的近视角线段锯齿情况的发生。制作很耗时,后面会贴文章,大家有意可以去看

PS示例Alt text

做完了内描线,我们来做外描线。用的是最常规的一套Back-Face的法线外拓方法。还有用顶点色的a通道控制秒描边粗细的常规操作。

顶点色的通道:
B:轮廓线的Z Offset

A:轮廓线的粗细系数,0.5是标准,1是最粗,0的话就没有轮廓线值

B的用法,用背面法膨胀时,对应视点在多大的深度方向(Z方向)上移动(=Offset) 膨胀的系数,这个值设定的很大的话,膨胀的模型就会埋没到邻接的面里,结果就是轮廓线消失了。根据本村氏所说,头发和脸的鼻子下面等,为了防止出现不受欢迎的皱褶一样的轮廓线,而加进的参数
Alt text

面部修正
修正前:
Alt text
修正后:
Alt text

1
2
3
4
5
6
7
//相机空间法线外扩描边
float3 posVS = UnityObjectToViewPos(v.vertex).xyz; //将顶点位置转换到相机空间
float3 ndirWS = UnityObjectToWorldNormal(v.normal); //将法线从模型空间转到世界空间
float3 ndirVS = mul((float3x3)UNITY_MATRIX_V, ndirWS); //将法线从世界空间转到相机空间
ndirVS.z = _OutLineBias * (1.0 - v.color.b); //利用顶点色的B通道陷入深度
posVS += ndirVS * _OutLineWidth * 0.001 * v.color.a; //法线外扩的控制,用顶点色A通道控制描边的宽度
o.pos = mul(UNITY_MATRIX_P, float4(posVS, 1.0)); //将顶点位置从相机空间转到裁剪空间

边缘光

有两种思路,都很简单
第一种:万物皆可菲涅尔,菲涅尔边缘光1-NdotV,用_SmoothstepMin和_SmoothstepMax两个值去控制
第二种:自定义边缘光,定义一个四维向量并转换到相机空间与Ndir点积,处理方式与漫反射相似
Alt text

1
2
3
4
5
6
float3 rimlight_dir = normalize(mul(UNITY_MATRIX_V, _RimLightDir.xyz));//转换到相机空间
half rim_lambert = (dot(Ndir, rimlight_dir) + 1.0) * 0.5;//从-1.0-1.0映射到0.0-1.0
half rimlight_term = half_lambert * ao + shadow_control;//边缘光因子
half toon_rim = saturate((rim_lambert - _ShadowThreshold) * 20);
half3 rim_color = (_RimLightColor + base_col) * 0.5 * sss_mask;//sss_mask区分边缘光区域的强度
half3 final_rim = toon_rim * rim_color * base_mask * toon_diffuse;//base_mask区分皮肤与非皮肤区域,看自己喜欢选择乘不乘

高清渲染图
Alt text
参考文章: https://zhuanlan.zhihu.com/p/376094989