PBR Lego Shading – 基于PBR的偏风格化的乐高材质
采用PBR的思路,用自定义的光照系统制作《乐高大电影》那种偏欧美浓艳色调的风格化的乐高材质。
首先我找了挺多现有的乐高作品的画面参考图。有游戏的,玩具官宣图的,也有近几年的乐高大电影的。可以看出风格是在一步步进化的。
乐高游戏的画面渲染质感普遍很单一,角色基本都是PBR-Specular这一套,场景也是写实风格的PBR风格。但缺点也很明显,完全写实的PBR会有点脏脏的,特别是灯光暗的地方全是黑色,没有动画电影那种可爱舒服的色调。感觉这些老美开发者也不太考究卡通色调的风格,基本就是把乐高当作《Call of Duty》系列的写实画风在做。
而乐高玩具的官宣图跟乐高官方的大电影系列现在都明显偏向了主流的欧美卡通渲染方向,并不是一味追求写实材质的。而且场景打光很厉害,颜色比较浓艳,整体气氛烘托得很好。
我的目标也是往乐高大电影风格上走的。但是游戏渲染不可能做到电影那么华丽细腻,打光那么讲究。但可以尽量保证在大多数情况下还原百分之六七十乐高大电影的画面色调风格。
首先做个乐高人仔的模型。乐高的细节亮点在边角的曲面过度上,人仔用了4000多面,让过度曲面更加柔和。
我先用Unity的Standard-Specular流程做了一遍
为了提高颜色饱和度,加了一点Emission。然后各种数值调下来就是下图这样。
正面光:
侧面光:
背面光:
分析一下需要改进的问题:
- Standard材质的色调是写实风格的,整体颜色饱和度和对比度都不够高,灰灰的。这一点可以用Emission或者Ambient补偿,但是这样做的话,以后做白天黑夜光影色调变化就会越来越麻烦。另一个办法就是靠复杂的场景灯光渲染,但是缺点是延迟渲染开销太大,不适合移动端。
- 用场景的Reflection产生的一圈Fresnel的逆光是左右完全对称的。而我需要的是侧逆光,要卡通风格化一点,这个也要另外实现。
- 正面光照下的渲染感觉还可以,但是背光和阴影下的效果很差,细节没有对比,色调灰灰的没有立体感。而且背光没有通透的SSS效果,这些也是要自定义光照实现的。
综上所述,Unity自带的PBR光照还是更适合纯写实的风格。想调整成乐高大电影的风格,就两条路:
- 继续用Unity的PBR流程,然后通过场景的打光,和各种屏幕后期来实现。这条路显然不太适合移动端。
- 基于PBR的思路,另做一套自定义的光照。反正手机端也用不了一些高端实时的渲染效果,所以我认为多用点Trick的手段来实现反而效率更高。
开始第二条路,用Custom Lighting来实现
具体罗列一下自定义光照里要实现的内容:
- 基础光照 + Specular高光 + Fresnel环境反光 + SSS + Rim高亮侧逆光 + Shadow + Ambient
基础光照:基于PBR-Specular的流程思路,NdotL作为基础Diffuse,通过高光形态来显示材质的粗糙度。
加上基础色纹理:
加上SSS:次表面散射分成正面光和背面光两次计算,用VdotL乘以光照颜色实现。注意背光SSS效果要增强一点,做出通透材质感。再叠加材质的内在色InteriorColor。在背光和光影交界的地方会溢出内在色,增强材质通透感。
Fresnel环境反光:Fresnel不用来做高亮逆光,而是跟灯光,环境和物体自身颜色混合,做成微弱的环境光。
Rim高亮侧逆光:根据灯光方向和Ramp的方式算出的边缘侧逆光。影子颜色混合了物体自身颜色和环境色,并且cut或削弱掉了其他高光反光。
高光部分叠加躁点贴图,模拟出粗糙颗粒感。躁点贴图是映射屏幕UV的,可以简单的做到颗粒感平均。(用PBR流程的话,是用NormalMap做的,而且需要把角色UV平均展开,性能开销太大。)
Ambient:环境色我暂时空着没用进去。以后加了场景再弄吧。
效果做完了,整理一下贴图和材质参数:
基础色的Alpha通道当作粗糙度。(其中粗糙度的部分要把漆色发亮的部分抠出来)
内在色贴图的Alpha通道当作高光度。(内在色也要把漆色发亮的部分做出来)
Noise和RimSample贴图(以后这俩贴图也可以合并的)
看看各个角度光照的效果(加了景深模糊):
这样基本上就做完了。以后再抽时间加入场景。
最后再对比一下【Unity PBR-Specular模型】和我做的【自定义光照】的效果。
Shader代码:
Shader "WalkingFat/PbrLego/SmoothBrick"
{
Properties
{
//Basic
_BasicColAndRoughnessTex ("Basic Color & Roughness Texture", 2D) = "white" {} // RGB = Basic Color; A = Roughness
_InteriorColAndSpecularTex ("Interior Color & Specular Texture", 2D) = "white" {} // RGB = Interior Color; A = Specular
_Tint ("Tint", Color) = (1,1,1,1)
// specular
_SpecularRate ("Specular Rate", Range(1,5)) = 1
_NoiseTex ("Noise Texture", 2D) = "White" {}
// Directional Subsurface Scattering
_FrontSssDistortion ("Front SSS Distortion", Range(0,1)) = 0.5
_BackSssDistortion ("Back SSS Distortion", Range(0,1)) = 0.5
_FrontSssIntensity ("Front SSS Intensity", Range(0,1)) = 0.2
_BackSssIntensity ("Back SSS Intensity", Range(0,1)) = 0.2
_InteriorColorPower ("Interior Color Power", Range(0,5)) = 2
// Lighting
_UnlitRate ("Unlit Rate", range(0,1)) = 0.5
_AmbientLight ("Ambient Light", Color) = (0.5,0.5,0.5,1)
// fresnel
_FresnelPower("Fresnel Power", Range(0.0, 36)) = 0.1
_FresnelIntensity("Fresnel Intensity", Range(0, 1)) = 0.2
// side rim
_RimColor("Rim Color", Color) = (0.5,0.5,0.5,1)
_RimIntensity("Rim Intensity", Range(0.0, 5)) = 1.0
_RimLightSampler("Rim Mask", 2D) = "white"{}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
Tags {"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#pragma multi_compile_fwdbase nolightmap nodirlightmap nodynlightmap novertexlight
#include "AutoLight.cginc" // shadow helper functions and macros
#include "UnityLightingCommon.cginc" // for _LightColor0
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float4 posWorld : TEXCOORD1;
float3 normalDir : TEXCOORD2;
float3 lightDir : TEXCOORD3;
float3 viewDir : TEXCOORD4;
SHADOW_COORDS(5) // for shadow
UNITY_FOG_COORDS(6)
};
inline float SubsurfaceScattering (float3 viewDir, float3 lightDir, float3 normalDir,
float frontSubsurfaceDistortion, float backSubsurfaceDistortion, float frontSssIntensity)
{
float3 frontLitDir = normalDir * frontSubsurfaceDistortion - lightDir;
float3 backLitDir = normalDir * backSubsurfaceDistortion + lightDir;
float frontSSS = saturate(dot(viewDir, -frontLitDir));
float backSSS = saturate(dot(viewDir, -backLitDir));
float result = saturate(frontSSS * frontSssIntensity + backSSS);
return result;
}
sampler2D _BasicColAndRoughnessTex;
float4 _BasicColAndRoughnessTex_ST;
sampler2D _InteriorColAndSpecularTex;
sampler2D _RimLightSampler;
sampler2D _NoiseTex;
float4 _Tint, _RimColor, _AmbientLight;
float _FrontSssDistortion, _BackSssDistortion, _FrontSssIntensity, _BackSssIntensity, _InteriorColorPower;
float _SpecularRate, _FresnelPower, _RimIntensity, _FresnelIntensity, _UnlitRate, _LitCubeIntensity;
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos (v.vertex);
o.posWorld = mul (unity_ObjectToWorld, v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _BasicColAndRoughnessTex);
o.normalDir = normalize(UnityObjectToWorldNormal (v.normal));
o.viewDir = normalize(_WorldSpaceCameraPos.xyz - o.posWorld.xyz);
o.lightDir = normalize(_WorldSpaceLightPos0.xyz);
TRANSFER_SHADOW(o); // for shadow
UNITY_TRANSFER_FOG(o,o.pos);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// common data
float3 NdotL = dot(i.normalDir, i.lightDir);
float3 NdotV = dot(i.normalDir, i.viewDir);
fixed atten = SHADOW_ATTENUATION(i); // recieve shadow
float lightRate = saturate((_LightColor0.x + _LightColor0.y + _LightColor0.z) * 0.3334);
float lightingValue = saturate(NdotL.r * atten.r) * lightRate;
float4 lightCol = lerp(_LightColor0, fixed4(1,1,1,1), 0.6);
// sample the texture
fixed4 col = tex2D(_BasicColAndRoughnessTex, i.uv) * _Tint;
fixed noise = tex2D(_NoiseTex, float2(i.posWorld.x, i.posWorld.y)).r;
fixed4 interiorSpecular = tex2D(_InteriorColAndSpecularTex, i.uv);
// Directional light SSS
float sssValue = SubsurfaceScattering (i.viewDir, i.lightDir, i.normalDir,
_FrontSssDistortion, _BackSssDistortion, _FrontSssIntensity);
fixed3 sssCol = lerp(interiorSpecular, _LightColor0, saturate(pow(sssValue, _InteriorColorPower))).rgb * sssValue;
sssCol *= _BackSssIntensity;
// Diffuse
fixed4 unlitCol = col * interiorSpecular * _UnlitRate;
fixed4 diffCol = lerp(unlitCol, col, lightingValue) * lightCol;
// Specular
float gloss = lerp(0.95, 0.3, interiorSpecular.a);
float specularPow = exp2 ((1 - gloss) * 10.0 + 1.0);
float3 halfVector = normalize (i.lightDir + i.viewDir);
float3 directSpecular = pow (max (0,dot (halfVector, normalize(i.normalDir))), specularPow) * interiorSpecular.a;
float specular = directSpecular * lerp (lightingValue, 1, 0.4) * _SpecularRate;
float noiseSpecular = lerp (specular, lerp (1 - pow(noise, specular), specular, specular), col.a);
fixed3 specularCol = noiseSpecular * _LightColor0.rgb;
// Side Rim
float falloffU = clamp (1.0 - abs(NdotV), 0.02, 0.98);
float rimlightDot = saturate (0.5 * (dot(i.normalDir, float3(i.lightDir.z, i.lightDir.y, i.lightDir.x)) + 1.5));
falloffU = saturate (rimlightDot * falloffU);
falloffU = tex2D (_RimLightSampler, float2 (falloffU, 0.25f)).r;
float3 rimCol = falloffU * _RimColor * _RimIntensity * lerp(lightingValue, 1, 0.6);
// Fresnel
float fresnel = 1.0 - max(0, NdotV);
float fresnelValue = lerp(fresnel, 0, sssValue);
float3 fresnelCol = saturate(lerp(interiorSpecular, lightCol.rgb, fresnelValue) * pow(fresnelValue, _FresnelPower) * _FresnelIntensity);
// final color
fixed3 final = sssCol + diffCol.rgb + specularCol + fresnelCol + rimCol;
// apply fog
UNITY_APPLY_FOG(i.fogCoord, final);
return fixed4(final, 1);
}
ENDCG
}
// cast shadow
Pass
{
Tags {"LightMode"="ShadowCaster"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_shadowcaster
#include "UnityCG.cginc"
struct v2f {
V2F_SHADOW_CASTER;
};
v2f vert(appdata_base v)
{
v2f o;
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
return o;
}
float4 frag(v2f i) : SV_Target
{
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
}
}