贴图分析

LightMap.r :⾼光类型Layer,根据值域选择不同的⾼光类型(eg:BlinPhong 裁边视⾓光)
LightMap.g :阴影AO ShadowAOMask
LightMap.b :BlinPhong⾼光强度Mask SpecularIntensityMask
LightMap.a :Ramp类型Layer,根据值域选择不同的Ramp
VertexColor.g :Ramp偏移值,值越⼤的区域 越容易”感光”(在⼀个特定的⾓度,偏移光照明暗)
VertexColor.a :描边粗细,这里的信息丢失了
角色shader还原
漫反射
ramp原理
漫反射分层,对ramp的使用。Ramp原理,在卡通渲染里面Ramp是很常用的,原理其实也非常简单,先算一个半兰伯特,然后把Ramp的某一行(uv.x) 映射到半兰伯特上。而这张Ramp可以做一个从最软到最硬的一个渐变,如果效果需要我们甚至可以再用顶点色或者贴图上去控制(uv.y)阴影在不同区域的软硬变化。

我们不单只可以映射到半兰伯特上,还可以映射到phong,GGX, kajiya-kay, Fresnel, 等等等的光照模型。只要你算出一个灰度渐变,你想映射某一组颜色上去都是可以的。

一些不一样的ramp

通过控制2个颜色之间插值像素的多少得到明暗边缘的硬过渡或者是软过渡![image.png]
看一下原神的ramp,大小为256*20像素。一共有10条Ramp,分别对应着白天和夜晚,根据Lightmap.a通道中的不同值域确定使用的层,以不同的值域表现不同的材质。

漫反射还原

根据LightMap.a的灰度区分不同的材质
灰度1.0 : 皮肤质感/头发质感(头发的部分是没有皮肤的)
灰度0.7 : 丝绸/丝袜
灰度0.5 : 金属/金属投影
灰度0.3 : 软的物体
灰度0.0 : 硬的物体

半兰伯特截断0.5-1.0的值将0大于0.5的值全部变为1.0然后乘以固有阴影
1
| halfLambert = smoothstep(0.0,0.5,halfLambert) * shadowAOMask;
|


明暗交界处的ramp
采样白天ramp,用半兰伯特作为U,Lightmap.a(rampLayerMask)作为V采样
1
| float3 lightRamp = tex2D(_RampMap, float2(halfLambert, rampLayerMask * 0.45 + 0.55));
|

给固有阴影区域添加ramp变化
1 2
| float3 baseColShadowed = lerp(baseCol * lightRamp, baseCol, shadowAOMask); baseColShadowed = lerp(baseCol, baseColShadowed, _ShadowRampLerp);
|

非常重要的baseColBright与baseColDark的遮罩,用来混合亮部与暗部
1
| float IsBrightSide = shadowAOMask * step(_LightThreshold, halfLambert);//明暗区域遮罩
|

Diffuse完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| float4 LightMap = tex2D(_LightMap, i.uv);
float specularLayerMask = LightMap.r; float shadowAOMask = LightMap.g; float specularShapeMask = LightMap.b; float rampLayerMask = LightMap.a; float rampOffsetMask = i.vertexCol.g; float halfLambert = NdotL * 0.5 + 0.5; shadowAOMask = 1.0 - smoothstep(saturate(shadowAOMask), 0.2, 0.55); halfLambert = smoothstep(0.0,0.5,halfLambert) * shadowAOMask;
float3 lightRamp = tex2D(_RampMap, float2(halfLambert, rampLayerMask * 0.45 + 0.55));
float3 baseCol = tex2D(_BaseMap, i.uv); float3 baseColShadowed = lerp(baseCol * lightRamp, baseCol, shadowAOMask); baseColShadowed = lerp(baseCol, baseColShadowed, _ShadowRampLerp); float IsBrightSide = shadowAOMask * step(_LightThreshold, halfLambert); float3 diffuse = lerp(lerp(baseColShadowed, baseCol * lightRamp, _RampLerp) * _DarkIntensity, baseColShadowed * _BrightIntensity, IsBrightSide * _RampIntensity * 1.0) * _CharacterIntensity;
|
面部SDF阴影

原神面部阴影的过渡十分平滑,使用了一张阈值图进行控制,如何采样这张阈值图呢?先来分析一下,观察游戏我们可以看到根据光源旋转角度的不同,面部阴影可以从左到右过渡,也可以从右到左过渡,这样我们就需要判断何时将采样的UV的U轴进行反向,将左右翻转进行采样。




上图中的曰(ceita)角需要加上右边的90度,画的时候没画上。
首先我们需要构造一个4x4的矩阵这个矩阵为
(-1,0,0,0
0,1,0,0
0,0,1,0
0,0,0,1)
第一行为X轴,正负控制着阴影变化方向是否正确,第二行为Y轴,确定朝向向上的单位向量,第三行为Z轴,代表这我们的角色朝向。我们需要将这个矩阵与灯光向量进行矩阵乘法,其数学含义是将灯光向量沿着YoZ平面进行对称。角色朝向是固定的,根据上图可知曰(ceita)角使我们需要求得角度,这样就可以将亮部与暗部按照角度进行混合。这里我们需要取归一化后的Ldir.xz进行atan2(反正切),经过angle = atan2(Ldir.x,Ldir.z)计算可以得到弧度,我们需要把他转为角度(乘以1/2 PI * 360)方便后面的计算。
若Ldir与Forward的夹角>=0 && <180需要将器remap到0-1之间,反之若>180则需要映射到1-0之间,将这个值定义为value,之后我们需要判断什么时候用0-1,什么时候用1-0,即是否需要对阈值图进行左右翻转,很明显我们的条件是角度>180。
然后我们再用step依据value对阈值图进行步进得到遮罩混合亮部与暗部就完成了。

SDF效果优化
考虑到SDF的数据内容,其实它相比尺寸,他对精度的要求更高一些。
所以我们可以切换成更高的色深来储存贴图数据,但是可以使用更小的贴图尺寸来储存。
以下操作来自上海的同事给的参考:
- 在Photoshop中打开SDF图
- 图像/模式/32位通道
- 滤镜/模糊/高斯模糊-半径2像素,确定
- 保存贴图,格式为exr(注意,只有exr可以保存各通道16位以上)
- 导入到Unity中之后,贴图选择不压缩,但是可以在保持效果下尽可能降低尺寸。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| float4 Left = mul(unity_ObjectToWorld, float4(-1, 0, 0, 0)); float4 Up = mul(unity_ObjectToWorld, float4(0, 1, 0, 0)); float4 Forward = mul(unity_ObjectToWorld, float4(0, 0, 1, 0));
float4x4 XYZ = float4x4(Left, Up, Forward, float4(0, 0, 0, 1)); float4 Light = mul(XYZ, float4(-ldirWS.xyz, 0)); Light .xz = normalize(Light.xz); float angle = atan2(Light.x, Light.z); float angle001 = angle * InvHalfPi + 0.5; float angle360 = angle001 * 360.0; float value = 0.0; if(angle360 >=0 && angle360 <180) { value = remap(angle360, 0, 180, 0, 1); } else { value = remap(angle360, 180, 360, 1, 0); } float needFlip = angle360 > 180; float4 faceSDFMap = tex2D(_FaceSDFMap, lerp(i.uv, float2(1-i.uv.x, i.uv.y), needFlip)); float faceSDFMask = 1.0 - tex2D(_FaceSDFMaskMap, i.uv).a; float faceLight = step(value, faceSDFMap.g) * faceSDFMask; float3 faceDiffuse = lerp(baseCol * _ShadowColor, baseCol, faceLight);
|
高光
通用高光
依据Lightmap.b选择不同的高光

可以看到除了头部,身体部分大致按灰度分了3个高光层,根据在PS中吸取颜色可以得知分层按灰度氛围100-150、150-250、250以上(基本是金属)。前2个高光采用的是裁边高光。
100-150的高光部分:

150-250:保持常亮,勾勒金属的一些边缘

250以上:采用BlinnPhong高光,做一些特殊化的处理

最后混合的高光效果

对于裁边高光的做法,一般是
stepSpecular = step(1 - _StepSpecularWidth, saturate(NdotV)) *_StepSpecularIntensity;
高光会随着视角进行变化,同时可以配合贴图控制高光的形状。

BlinnPhong高光就是对NdotH做pow处理并且需要乘以一个200-300的值得到硬边缘
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| float metalMap = saturate(tex2D(_MetallicaMap, mul((float3x3)UNITY_MATRIX_V, ndirWS).xy * 0.5f + 0.5f ).r); metalMap = step(_MetalMapV, metalMap) * _MetalMapIntensity; float3 specular = 0.0; float3 stepSpecular1 = 0.0; float3 stepSpecular2 = 0.0; specularLayerMask = pow(specularLayerMask, 1 / 2.2); float specularLayer = specularLayerMask * 255;
if (specularLayer > 100 && specularLayer <= 150) { stepSpecular1 = step(1 - _StepSpecularWidth1, saturate(NdotV)) *_StepSpecularIntensity1; stepSpecular1 *= baseCol; }
if (specularLayer > 150 && specularLayer < 250) { float stepSpecularMask = step(200, pow(specularShapeMask, 1 / 2.2) * 255); stepSpecular1 = step(1 - _StepSpecularWidth2, saturate(NdotV)) *_StepSpecularIntensity2; stepSpecular2 = step(1 - _StepSpecularWidth3 * 5, saturate(NdotV)) * stepSpecularMask * _StepSpecularIntensity3; stepSpecular1 = lerp(stepSpecular1, 0, stepSpecularMask); stepSpecular2 *= baseCol * 0.6; stepSpecular1 *= baseCol * 0.6; } if (specularLayer >= 250) { specular = saturate(pow(saturate(NdotH), 1 * _SpecularExp) * specularShapeMask *_SpecularIntensity * 300); specular = max(0, specular); float specularAdjustMask = pow(specularShapeMask, 7); float3 specularAdjustCol = specularAdjustMask * baseCol; specular += metalMap; specular *= baseCol * 0.6; specular += specularAdjustCol; }
|
头发高光
原神的头发渲染,高光在亮部出现,也会在暗部出现部分,有一部分在暗部则会消失


1 2 3 4
| float specularRange = step(1- _HairSpecularRange, saturate(NdotH)); float viewRange = step(1- _HairSpecularViewRange, saturate(NdotV)); float3 hairSpecular = specularShapeMask * _HairSpecularIntensity * specularRange * viewRange; hairSpecular = max(0.0, hairSpecular);
|
我们需要高光在暗部消失,所以我们需要借用漫反射里面制作的亮部与暗部的Mask
hairSpecular *= IsBrightSide;这样暗部的一部分高光就会消失。
边缘光
菲尼尔边缘光的变种,添加了Lambert项进行控制,以区分左侧与右侧


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| float lambertF = NdotL; float lambertD = max(0, -lambertF); lambertF = max(0, lambertF); float rim = 1 - saturate(NdotV);
float rimDot = saturate(pow(rim, _RimPow)); rimDot = _EnableLambert * lambertF * rimDot + (1 - _EnableLambert) * rimDot; float rimIntensity = smoothstep(0, _RimSmooth, rimDot); float4 Rim = _EnableRim * saturate(pow(rimIntensity, 5)) * _RimColor.rgba * float4(baseCol.rgb, 1.0);
rimDot = saturate(pow(rim, _DarkSideRimPow)); rimDot = _EnableLambert * lambertD * rimDot + (1 - _EnableLambert) * rimDot; rimIntensity = smoothstep(0, _DarkSideRimSmooth, rimDot); float4 RimDS = _EnableRimDS * saturate(pow(rimIntensity, 5)) * _DarkSideRimColor * float4(baseCol.rgb, 1.0); float4 rimLight = Rim + RimDS;
|
屏幕空间边缘光
https://www.bilibili.com/read/cv11841147
自发光
源自BaseMap的A通道有一张Mask图,简单乘以一个颜色(开了HDR)加在finalColor上就好

就是这两部分,一个宝石,一个神瞳


1
| float3 emission = tex2D(_BaseMap, i.uv).a * _EmissiomColor;
|


描边
相机空间下的法线外扩,并用顶点色的a通道控制描边,通用Pass
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| Pass { Cull Front CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc"
struct appdata { float4 vertex : POSITION; float2 uv0 : TEXCOORD0; float3 normal : NORMAL; float4 color : COLOR; };
struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float4 vertexCol : TEXCOORD1; };
sampler2D _BaseMap; float _OutLineWidth; float4 _OutLineColor;
v2f vert (appdata v) { v2f o; float3 posVS = UnityObjectToViewPos(v.vertex).xyz; float3 ndirWS = UnityObjectToWorldNormal(v.normal); float3 ndirVS = mul((float3x3)UNITY_MATRIX_V, ndirWS); posVS += ndirVS * _OutLineWidth * 0.001 * v.color.a; o.pos = mul(UNITY_MATRIX_P, float4(posVS, 1.0)); o.uv = v.uv0; o.vertexCol = v.color; return o; }
float4 frag (v2f i) : SV_Target { float3 baseCol = tex2D(_BaseMap,i.uv).rgb; float maxComponent = max(max(baseCol.r, baseCol.g), baseCol.b) - 0.004; float saturatedCol = step(maxComponent.rrr, baseCol) * baseCol; saturatedCol = lerp(baseCol, saturatedCol, 0.6); float3 outlineCol = 0.8 * saturatedCol * baseCol * _OutLineColor; return float4(outlineCol, 1.0); } ENDCG }
|
最后效果


时间久远,留个纪念,欢迎交流_(:з」∠)_