基于皮肤预积分SSS的面部卡通渲染尝试
之前我在资源商店上发了一个免费资源,内容是一个中国角色模型,其中还包含了头发,皮肤,眼球渲染的shader。现在,我想尝试做做卡通风格,而对于本例中面部卡通渲染,我打算在SSS的基础上尝试弄弄。
资源地址:https://assetstore.unity.com/packages/3d/characters/humanoids/sci-fi/chinese-cyborg-warrior-free-287098
在渲染面部次表面散射效果的时候,一个通用的做法是采样一张LUT图,这也是常用的预积分次表面散射(pre-integrated subsurface scattering)。
float3 SkinBRDF(lightDatas lightDat, surfaceDatas surfDat, half3 L, half3 lightCol, float shadow, float curvature)
{
float a2 = Common.Pow4(surfDat.roughness);
half3 H = normalize(lightDat.V + L);
half NdotH = saturate(dot(lightDat.N, H));
half NdotV = saturate(abs(dot(lightDat.N, lightDat.V)) + 1e-5);
half NdotL = saturate(dot(lightDat.N, L));
half VdotH = saturate(dot(lightDat.V, H));//LoH
half LdotH = saturate(dot(H, L));
float3 radiance = NdotL * lightCol *shadow;
half wrapNoL = 0.2 + NdotL * 0.8;
float3 sss = SAMPLE_TEXTURE2D(_SSSLUT, sampler_SSSLUT, float2(wrapNoL, curvature));
float3 diffuseTerm = surfDat.albedo * OneMinusReflectivityMetallicCustom(surfDat.metallic) * sss;
#if defined(_DIFFUSE_OFF)
diffuseTerm = half3(0, 0, 0);
#endif
float3 specularTerm = DirectBRDF_Specular(surfDat.roughness, NdotH, LdotH) * lerp(kDieletricSpec.rgb, surfDat.albedo, surfDat.metallic);// * surfDat.metallic;
#if defined(_SPECULAR_OFF)
specularTerm = half3(0, 0, 0);
#endif
return diffuseTerm * lightCol * shadow + specularTerm * radiance;
}
上面的代码是预积分次表面散射的BRDF函数部分。我们把标准PBR渲染中的diffuseTerm项里的NdotL换成我们的采样LUT得到的颜色。其中的curvature曲率来自
float curvature = SAMPLE_TEXTURE2D(_CurvatureMap, sampler_CurvatureMap, i.uv);
curvature = pow(curvature, _CurvaturePow);
采样一张曲率图得到。当然,如果你手中的模型没有曲率图,也可以使用fwidth函数来计算。
本例中,高光的渲染使用unity中lit.shader中使用的DirectBRDF_Specular函数。
half DirectBRDF_Specular(float roughness, float NdotH, float LdotH)
{
half roughness2 = Common.Pow2(roughness);
float d = NdotH * NdotH * (roughness2 - half(1.0)) + 1.00001f;
half LoH2 = LdotH * LdotH;
half specularTerm = roughness2 / ((d * d) * max(0.1, LoH2) * (roughness * (half)4.0 + half(2.0)));
return specularTerm;
}
那么为了完成卡通渲染的效果,我的尝试做法是使用对diffuse的结果做一个posterize
half4 Posterize(half4 col)
{
col = pow(col, 0.4545);
float3 c = RgbToHsv(col);
c.z = round(c.z * _Step) / _Step;
col = float4(HsvToRgb(c), col.a);
col = pow(col, 2.3);
return col;
}
而我们的人物的头部并不全是皮肤,还有很多部分不是使用次表面散射计算的。在原例中,我们使用一张Mask图来区分,模型一些部分使用带SSS的,另一部分使用StandardBRDF。现在我们把StandardBRDF的部分换成卡通渲染的
float3 toonLightCal(lightDatas lightDat, surfaceDatas surfDat, half3 L, half3 lightCol, float shadow)
{
float a2 = Common.Pow4(surfDat.roughness);
//float a2 = Common.Pow2(surfDat.roughness);
half3 H = normalize(lightDat.V + L);
half NdotH = saturate(dot(lightDat.N, H));
half NdotV = saturate(abs(dot(lightDat.N, lightDat.V)) + 1e-5);
half NdotL = saturate(dot(lightDat.N, L));
half VdotH = saturate(dot(lightDat.V, H));//LoH
half LdotH = saturate(dot(H, L));
half wrapNL = NdotL * 0.5 + NdotL;
float shadowNL = smoothstep(_ShadowStep - _ShadowStepSmooth, _ShadowStep + _ShadowStepSmooth, wrapNL);
float3 radiance = lightCol * shadowNL * shadow;
float3 diffuseTerm = surfDat.albedo * OneMinusReflectivityMetallicCustom(surfDat.metallic);
#if defined(_DIFFUSE_OFF)
diffuseTerm = half3(0, 0, 0);
#endif
float specularNH = smoothstep((1 - _SpecularStep * 0.05) - _SpecularStepSmooth * 0.05, (1 - _SpecularStep * 0.05) + _SpecularStepSmooth * 0.05, NdotH);
float3 specularTerm = _SpecularColor * shadow * shadowNL * specularNH;
#if defined(_SPECULAR_OFF)
specularTerm = half3(0, 0, 0);
#endif
return (diffuseTerm + specularTerm) * radiance;
}
截个完整点的代码
half DirectBRDF_Specular(float roughness, float NdotH, float LdotH)
{
half roughness2 = Common.Pow2(roughness);
float d = NdotH * NdotH * (roughness2 - half(1.0)) + 1.00001f;
half LoH2 = LdotH * LdotH;
half specularTerm = roughness2 / ((d * d) * max(0.1, LoH2) * (roughness * (half)4.0 + half(2.0)));
return specularTerm;
}
half OneMinusReflectivityMetallicCustom(half metallic)
{
// We'll need oneMinusReflectivity, so
// 1-reflectivity = 1-lerp(dielectricSpec, 1, metallic) = lerp(1-dielectricSpec, 0, metallic)
// store (1-dielectricSpec) in kDielectricSpec.a, then
// 1-reflectivity = lerp(alpha, 0, metallic) = alpha + metallic*(0 - alpha) =
// = alpha - metallic * alpha
half oneMinusDielectricSpec = kDielectricSpec.a;
return oneMinusDielectricSpec - metallic * oneMinusDielectricSpec;
}
float3 toonLightCal(lightDatas lightDat, surfaceDatas surfDat, half3 L, half3 lightCol, float shadow)
{
float a2 = Common.Pow4(surfDat.roughness);
//float a2 = Common.Pow2(surfDat.roughness);
half3 H = normalize(lightDat.V + L);
half NdotH = saturate(dot(lightDat.N, H));
half NdotV = saturate(abs(dot(lightDat.N, lightDat.V)) + 1e-5);
half NdotL = saturate(dot(lightDat.N, L));
half VdotH = saturate(dot(lightDat.V, H));//LoH
half LdotH = saturate(dot(H, L));
half wrapNL = NdotL * 0.5 + NdotL;
float shadowNL = smoothstep(_ShadowStep - _ShadowStepSmooth, _ShadowStep + _ShadowStepSmooth, wrapNL);
float3 radiance = lightCol * shadowNL * shadow;
float3 diffuseTerm = surfDat.albedo * OneMinusReflectivityMetallicCustom(surfDat.metallic);
#if defined(_DIFFUSE_OFF)
diffuseTerm = half3(0, 0, 0);
#endif
float specularNH = smoothstep((1 - _SpecularStep * 0.05) - _SpecularStepSmooth * 0.05, (1 - _SpecularStep * 0.05) + _SpecularStepSmooth * 0.05, NdotH);
float3 specularTerm = _SpecularColor * shadow * shadowNL * specularNH;
#if defined(_SPECULAR_OFF)
specularTerm = half3(0, 0, 0);
#endif
return (diffuseTerm + specularTerm) * radiance;
}
half4 Posterize(half4 col)
{
col = pow(col, 0.4545);
float3 c = RgbToHsv(col);
c.z = round(c.z * _Step) / _Step;
col = float4(HsvToRgb(c), col.a);
col = pow(col, 2.3);
return col;
}
float3 SkinBRDF(lightDatas lightDat, surfaceDatas surfDat, half3 L, half3 lightCol, float shadow, float curvature)
{
float a2 = Common.Pow4(surfDat.roughness);
//float a2 = Common.Pow2(surfDat.roughness);
half3 H = normalize(lightDat.V + L);
half NdotH = saturate(dot(lightDat.N, H));
half NdotV = saturate(abs(dot(lightDat.N, lightDat.V)) + 1e-5);//区分正反面
half NdotL = saturate(dot(lightDat.N, L));
half VdotH = saturate(dot(lightDat.V, H));//LoH
half LdotH = saturate(dot(H, L));
float3 radiance = NdotL * lightCol *shadow;
half wrapNoL = 0.2 + NdotL * 0.8;
float3 sss = SAMPLE_TEXTURE2D(_SSSLUT, sampler_SSSLUT, float2(wrapNoL, curvature));
float3 diffuseTerm = surfDat.albedo * OneMinusReflectivityMetallicCustom(surfDat.metallic) * sss;
#if defined(_DIFFUSE_OFF)
diffuseTerm = half3(0, 0, 0);
#endif
float3 specularTerm = DirectBRDF_Specular(surfDat.roughness, NdotH, LdotH) * lerp(kDieletricSpec.rgb, surfDat.albedo, surfDat.metallic);// * surfDat.metallic;
#if defined(_SPECULAR_OFF)
specularTerm = half3(0, 0, 0);
#endif
return Posterize(half4(diffuseTerm, 1)).rgb * lightCol * shadow + specularTerm * radiance;
}
half3 SkinShading(lightDatas lightDat,surfaceDatas surfDat,float3 positionWS,float4 shadowCoord, float curvature)
{
half3 directLighting = (half3)0;
#if defined(_MAIN_LIGHT_SHADOWS_SCREEN) && !defined(_SURFACE_TYPE_TRANSPARENT)
float4 positionCS = TransformWorldToHClip(positionWS);
shadowCoord = ComputeScreenPos(positionCS);
#else
shadowCoord = TransformWorldToShadowCoord(positionWS);
#endif
half4 shadowMask = (half4)1.0;
//add light
half3 directLighting_AddLight = (half3)0;
#ifdef _ADDITIONAL_LIGHTS
uint pixelLightCount = GetAdditionalLightsCount();
if (surfDat.mask > 0.98)
{
//main light
half3 directLighting_MainLight = (half3)0;
{
Light light = GetMainLight(shadowCoord, positionWS, shadowMask);
half3 L = light.direction;
half3 lightColor = light.color;
//SSAO
#if defined(_SCREEN_SPACE_OCCLUSION)
AmbientOcclusionFactor aoFactor = GetScreenSpaceAmbientOcclusion(lightDat.screenUV);
lightColor *= aoFactor.directAmbientOcclusion;
#endif
half shadow = light.shadowAttenuation;
directLighting_MainLight = SkinBRDF(lightDat, surfDat, L, lightColor, shadow, curvature);
}
UNITY_LOOP
for (uint lightIndex = 0; lightIndex < pixelLightCount; lightIndex++)
{
Light light = GetAdditionalLight(lightIndex, positionWS, shadowMask);
half3 L = light.direction;
half3 lightColor = light.color;
half shadow = light.shadowAttenuation * light.distanceAttenuation;
directLighting_AddLight += SkinBRDF(lightDat, surfDat, L, lightColor, shadow, curvature);
}
return directLighting_MainLight + directLighting_AddLight;
}
else
{
//main light
half3 directLighting_MainLight = (half3)0;
{
Light light = GetMainLight(shadowCoord, positionWS, shadowMask);
half3 L = light.direction;
half3 lightColor = light.color;
//SSAO
#if defined(_SCREEN_SPACE_OCCLUSION)
AmbientOcclusionFactor aoFactor = GetScreenSpaceAmbientOcclusion(lightDat.screenUV);
lightColor *= aoFactor.directAmbientOcclusion;
#endif
half shadow = light.shadowAttenuation;
directLighting_MainLight = toonLightCal(lightDat, surfDat, L, lightColor, shadow);
}
UNITY_LOOP
for (uint lightIndex = 0; lightIndex < pixelLightCount; lightIndex++)
{
Light light = GetAdditionalLight(lightIndex, positionWS, shadowMask);
half3 L = light.direction;
half3 lightColor = light.color;
half shadow = light.shadowAttenuation * light.distanceAttenuation;
directLighting_AddLight += toonLightCal(lightDat, surfDat, L, lightColor, shadow);
}
return directLighting_MainLight + directLighting_AddLight;
}
#endif
return 0;
}
最终效果展示: