UE4.26 Custom Cartoon ShadingModel

尝试在UE4.26中实现卡通渲染,因为ue中想要自定义光照非常的麻烦,因此做了许多妥协,也为了某些效果修改了些许源码,在这里做些记录。


一、ShadingModel

要想在ue中实现一些自定义的shading,就需要添加ShadingModel来实现

因为添加shadingModel和材质接口要涉及很多文件,截图下来占用太多篇幅,知乎又有许多相关文章,这里就先不记录了,主要写一写Shader的内容


二、TextureSampler

添加完自定义的ShadingModel之后,就可以开始写shader了,但是又会发现一个问题,在BxDF内,怎么采样贴图呢? 如果想要采样一张rampMap,就需要先计算出x轴,然后再采样,但是ue似乎只能在材质连连看里采样完之后再传入到GBuffer,之后再在BxDF里使用,这样明显不能满足要求,因此需要换一种方法。

ue里有一个名为"PreintegratedSkinBxDF"的Shader,代码如下

可以看到,这里是进行了贴图采样的,而这张贴图,是存放到特定目录下,在编译器运行时直接加载供之后随时调用。所以就可以采用这种方法,添加各种自定义的贴图了。

具体添加方法如下:


三、BxDF

修改完上面的东西就可以开始写shader了,所有的BxDF都在ShadingModels.ush内。

这里有几个参数需要注意,Falloff是光照的衰减,如果要使用点光源就需要对最终的结果乘上这个值。

Shadow里包含很多阴影的参数,可以通过Shadow.SurfaceShadow来得知当前像素是否在阴影内,但是默认情况下在BxDF阶段会直接跳过阴影区域,所以要在LightPass阶段做下判断,在卡渲时对阴影部分也进行处理。只需要在"DeferredLightingCommon.ush"内修改LightAccumulator_AddSplit即可。

//
if(GBuffer.ShadingModelID == SHADINGMODELID_CARTOONDEFAULT || GBuffer.ShadingModelID == SHADINGMODELID_CARTOONSKIN || GBuffer.ShadingModelID == SHADINGMODELID_CARTOONHAIR )
{
	LightAccumulator_AddSplit( LightAccumulator, Lighting.Diffuse, Lighting.Specular, Lighting.Diffuse, LightColor * LightMask , bNeedsSeparateSubsurfaceLightAccumulation );
}
else
{
	LightAccumulator_AddSplit( LightAccumulator, Lighting.Diffuse, Lighting.Specular, Lighting.Diffuse, LightColor * LightMask * Shadow.SurfaceShadow, bNeedsSeparateSubsurfaceLightAccumulation );
}

光照计算:

原神有一张LightMap,在G通道里存了AO信息。A通道是用来区分不同的材质,例如神里的这张,1.0 对应皮肤,0.5 是衣服的部分, 0 是硬的盔甲。对不同部位采样不同的RampMap。

这里我是将HalfLambert与阴影区域和AO计算之后作为横轴采样RampMap。原神的RampMap有些不一样,颜色跨度大的区间在右侧,而一般的RampMap是在0.5的地方,所以这里我用了一根曲线去调整。

//AO
float ToonSelfShadow = GBuffer.Offset.g * 2.0f ;
//用log调整横轴的范围
float ToonNoL = saturate(-log(1 - HalfLambert * Shadow.SurfaceShadow * ToonSelfShadow));
//ramp 上半部分  (这里只采样了上半部分,也就是只有白天的情况)
float RampRange[] = {0.05, 0.25, 0.15, 0.35};
int RampY = floor(GBuffer.Offset.r * 2 + 0.1);
float3 AyakaBodyRamp = Texture2DSampleLevel(View.AyakaBodyRamp, View.AyakaBodyRampSampler, float2(ToonNoL * 0.9997, RampRange[RampY] ), 0).rgb;

Lighting.Diffuse = AreaLight.FalloffColor * Falloff * (Diffuse_Lambert(GBuffer.DiffuseColor) * AyakaBodyRamp + Rimlight);

边缘光:

这里我直接用了菲涅尔。本想用边缘检测来做但是现在还不会在ue加pass,只能在后处理做,之后能加pass了再修改这里。

// rimlight
float Rimlight = ( 1.0f - smoothstep(RimRadius, RimRadius + 0.03f, Context.NoV)) * RimIntensity * (1.0f - (Context.NoL * 0.5f + 0.5f)) * Diffuse_Lambert(GBuffer.DiffuseColor);

高光:

高光分为两种情况,正常的高光与金属区域的高光。正常区域的高光就使用BlinnPhong模型再做一个step。金属区域需要做一个额外的处理,让它根据角度的不同有三个层次的颜色,可以通过对一张MetalMap采样才计算,这张图的中间为1,两边为0。也可以通过计算得到一个符合这张图变化的公式。

//高光   range.g -- 高光形状    range.b -- 高光大小
float3 SpecularColor = Diffuse_Lambert(GBuffer.DiffuseColor) * GBuffer.Range.g;
float SpecularContrib = step(GBuffer.Range.b, Context.NoH);

//金属区域 拟合一个MetalMap
float MetalDir = 1 - saturate(Context.NoV);
float MetalRadius = saturate(1.0f - MetalDir) * saturate(1.0f + MetalDir);
float MetalFactor = saturate( step(0.65f, MetalRadius) + 0.25f ) * 0.5f * saturate( step(0.4f, MetalRadius) + 0.25f ) * 6.5f;
// range.r -- 金属区域范围
float MetalRange = GBuffer.Range.r;
float3 MetalColor = Diffuse_Lambert(GBuffer.DiffuseColor) * MetalRange * MetalFactor * saturate(0.25f + ToonNoL);

Lighting.Specular = AreaLight.FalloffColor * Falloff *( SpecularColor * SpecularContrib + MetalColor);

头发:

原神的头发高光是固定了区域的,因此只需要把上面正常的高光稍作修改,给一个最小值,或者用smoothStep将区域扩大,就可以实现一样的效果。

//高光   range.g -- 高光形状    range.b -- 高光大小
//头发处高光 平滑的范围加大,这样暗处也基本都会有一些高光存在
float3 SpecularColor = Diffuse_Lambert(GBuffer.DiffuseColor) * GBuffer.Range.g;
float SpecularContrib = smoothstep(0.0f, GBuffer.Range.b, Context.NoH);

脸部:

通过采样SDF图来对脸部阴影进行平滑处理。

float FaceShadowRange = dot(normalize(Front.xy), normalize(L.xy));
float SwitchShadow = dot(normalize(Right.xy), normalize(L.xy)) * 0.5 + 0.5 < 0.5;
float FaceShadow = lerp(LightMap, 1 - LightMap, SwitchShadow);

float FaceLight = 1 - smoothstep(FaceShadowRange - 0.1 , FaceShadowRange + 0.1, FaceShadow);

return FaceLight;

描边: