游戏中经常出现使3D模型呈现卡通效果的Shader。这里分享一下我的一个卡通shader,使用unity 2021.3.16f1c1版本,URP渲染管线,ShaderLab以及HLSL语言编程。(第一次发文,写的不好多多海涵:)
卡通shader的效果主要需要处理两个方面,一个是模型的描边,另一个是着色的色阶。这里我们一一对两者的做法进行描述。
描边
我们需要再模型的边缘绘制一个黑框,首先在属性中声明两个变量,一个是边框挤出的宽度,另一个是边框的颜色。
_Extrude("Edge Extrude", Float) = 1.0
_EdgeColor("Edge Color", Color) = (0, 0, 0, 0)
描边的效果其实很好制作,我们只需要增加一个pass通道负责绘制边框。这个通道中,每一个顶点都向外侧(法线方向)挤出一定距离。
然后应用背面剔除Cull Back,这样,正面的fragment就不会显示,不会把我们的模型盖住。
同时关掉深度缓冲。
在顶点着色器中,我们根据法线方向,将顶点的位置加上一个向外的偏移。
vOut vert(vIn i)
{
vOut o;
float4 dir;
dir.xyz = normalize(i.normalOS);
i.positionOS = i.positionOS + dir * _Extrude;
const VertexPositionInputs vertexInput = GetVertexPositionInputs(i.positionOS);
o.positionCS = vertexInput.positionCS;
return o;
}
在fragment shader中,直接将之前的边框颜色_EdgeColor输出就行。
half4 frag(vOut o) : SV_Target
{
return _EdgeColor;
}
整个pass通道是这样的:
Pass
{
Name"Edge"
Cull Front
ZWrite Off
HLSLPROGRAM
pragma vertex vert
pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
float _Extrude;
float4 _EdgeColor;
struct vIn
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
};
struct vOut
{
float4 positionCS : SV_POSITION;
};
vOut vert(vIn i)
{
vOut o;
float4 dir;
dir.xyz = normalize(i.normalOS);
i.positionOS = i.positionOS + dir * _Extrude;
const VertexPositionInputs vertexInput = GetVertexPositionInputs(i.positionOS);
o.positionCS = vertexInput.positionCS;
return o;
}
half4 frag(vOut o) : SV_Target
{
return _EdgeColor;
}
ENDHLSL
}
色阶
描边解决了之后,下一步就是制作色阶。我们这个toon shader是在PBR光照模型基础上制作的。有关unity PBR,这里不在赘述。当然,代码也会给大家。我是参考了油管博主NedMakeGames的视频教程以及文档。感兴趣且能够科学上网的同学可以参考以下链接:https://www.youtube.com/watch?v=5GGISvt4KEA
PBR本来渲染出来的效果是这样的 :
我们使用一个pass通道计算顶点,光照以及最终的像素颜色输出。在我们使用UniversalFragmentPBR函数计算出一个fragment的颜色后,我们可以对这个颜色做色阶处理。
卡通效果的色阶,就是把颜色区分成固定的几个离散化的阶段。这样,我们不允许颜色的RGB值连续地变化。比如,对于某颜色的R值,我们只允许它是0.0, 0.1, 0.2, 0.3, ..., 1.0,不允许它成为中间的某个值,如0.15。
我们事先规定色阶的数量_Step。假如_Step为10,则颜色的RGB值都只能是0.0, 0.1, 0.2, 0.3, ..., 1.0中的一个。加入_Step为5,那么颜色的RGB值都只能是0.0, 0.2, 0.4, 0.8, ..., 1.0中的一个,以此类推。
我们把PBR计算出的颜色存到变量temp中,然后使用以下公式计算res值。
float4 mul = (temp + _Offset)* _Step;
res = ( mul - frac(mul)) / _Step ;
这里,frac()函数的作用是取得一个浮点数的小数部分。
这样计算后,比如我们的_Step为10,则假如一个颜色是(0.11,0.53,0.46,1),则计算出来就得到(0.1,0.5,0.4,1)。我们可以发现,所有的RGB值都减小了,也就是说得到的效果会变暗。所以我们增加了一个_Offset属性,用于使颜色亮一些。
黑白漫画
另外,有的漫画是黑白的,给人一种十分硬朗的感觉,我们这里也可以尝试制作一下这个黑白效果。其实非常简单,只要在计算完PBR之后,将RGB取平均,再赋回去就行。
temp = (res.r + res.g + res.b) / 3;
为了方便打开并关闭黑白效果,我们使用一个Toggle在属性里面
[Toggle(BLACK_AND_WHITE)] BlackAndWhiteToggle("Black and white", Float) = 0
这个宏只在fragment shader中被用到,使用一个shader_feature_local_fragment
#pragma shader_feature_local_fragment BLACK_AND_WHITE
效果展示 :)
下面展示一下效果:
下面贴代码:
Toon.shader
Shader "Custom/Toon"
{
Properties{
[Header(Surface options)]
[MainTexture] _ColorMap("Albedo", 2D) = "white" {}
[MainColor] _ColorTint("Tint", Color) = (1, 1, 1, 1)
[NoScaleOffset][Normal] _NormalMap("Normal", 2D) = "bump" {}
_NormalStrength("Normal strength", Range(0, 1)) = 1
[NoScaleOffset] _MetalnessMask("Metalness mask", 2D) = "white" {}
_Metalness("Metalness strength", Range(0, 1)) = 0
[Toggle(_SPECULAR_SETUP)] _SpecularSetupToggle("Use specular workflow", Float) = 0
[NoScaleOffset] _SpecularMap("Specular map", 2D) = "white" {}
_SpecularTint("Specular tint", Color) = (1, 1, 1, 1)
_Smoothness("Smoothness multiplier", Range(0, 1)) = 0.5
[NoScaleOffset] _EmissionMap("Emission map", 2D) = "white" {}
[HDR] _EmissionTint("Emission tint", Color) = (0, 0, 0, 0)
[HideInInspector] _SurfaceType("Surface type", Float) = 0
[HideInInspector] _BlendType("Blend type", Float) = 0
[HideInInspector] _FaceRenderingMode("Face rendering type", Float) = 0
_Step("Toon Color Steps", Float) = 5
_Offset("Toon Color Offset", Float) = 0.03
_Extrude("Edge Extrude", Float) = 1.0
_EdgeColor("Edge Color", Color) = (0, 0, 0, 0)
[Toggle(BLACK_AND_WHITE)] BlackAndWhiteToggle("Black and white", Float) = 0
}
SubShader{
Tags{"RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" "UniversalMaterialType" = "Lit" "IgnoreProjector" = "True" "ShaderModel" = "4.5"}
Pass {
Name "ForwardLit"
Tags{"LightMode" = "UniversalForward"}
Blend One Zero
ZWrite On
Cull Back
HLSLPROGRAM
#define _NORMALMAP
#pragma shader_feature_local_fragment _SPECULAR_SETUP
#if UNITY_VERSION >= 202120
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE
#else
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
#endif
#pragma multi_compile_fragment _ _SHADOWS_SOFT
#if UNITY_VERSION >= 202120
#pragma multi_compile_fragment _ DEBUG_DISPLAY
#endif
#pragma shader_feature_local_fragment BLACK_AND_WHITE
#pragma vertex Vertex
#pragma fragment Fragment
#include "MyToonForwardLitPass.hlsl"
ENDHLSL
}
Pass{
Name "ShadowCaster"
Tags{"LightMode" = "ShadowCaster"}
ColorMask 0
Cull Back
HLSLPROGRAM
#pragma vertex Vertex
#pragma fragment Fragment
#include "MyLitShadowCasterPass.hlsl"
ENDHLSL
}
Pass
{
Name"Edge"
Cull Front
ZWrite Off
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
float _Extrude;
float4 _EdgeColor;
struct vIn
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
};
struct vOut
{
float4 positionCS : SV_POSITION;
};
vOut vert(vIn i)
{
vOut o;
float4 dir;
dir.xyz = normalize(i.normalOS);
i.positionOS = i.positionOS + dir * _Extrude;
const VertexPositionInputs vertexInput = GetVertexPositionInputs(i.positionOS);
o.positionCS = vertexInput.positionCS;
return o;
}
half4 frag(vOut o) : SV_Target
{
return _EdgeColor;
}
ENDHLSL
}
}
}
MyLitCommon.hlsl
#ifndef MY_LIT_COMMON_INCLUDED
#define MY_LIT_COMMON_INCLUDED
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
TEXTURE2D(_ColorMap); SAMPLER(sampler_ColorMap);
TEXTURE2D(_NormalMap); SAMPLER(sampler_NormalMap);
TEXTURE2D(_MetalnessMask); SAMPLER(sampler_MetalnessMask);
TEXTURE2D(_SpecularMap); SAMPLER(sampler_SpecularMap);
TEXTURE2D(_EmissionMap); SAMPLER(sampler_EmissionMap);
float4 _ColorMap_ST;
float4 _ColorTint;
float _NormalStrength;
float _Metalness;
float3 _SpecularTint;
float3 _EmissionTint;
#endif
MyToonForwardLitPass.hlsl
#ifndef MY_LIT_FORWARD_LIT_PASS_INCLUDED
#define MY_LIT_FORWARD_LIT_PASS_INCLUDED
#include "MyLitCommon.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
struct Attributes {
float3 positionOS : POSITION;
float3 normalOS : NORMAL;
float4 tangentOS : TANGENT;
float2 uv : TEXCOORD0;
};
float _Step;
float _Offset;
float _Extrude;
float4 _EdgeColor;
struct Interpolators {
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
float3 positionWS : TEXCOORD1;
float3 normalWS : TEXCOORD2;
float4 tangentWS : TEXCOORD3;
};
Interpolators Vertex(Attributes input) {
Interpolators output;
// Found in URP/ShaderLib/ShaderVariablesFunctions.hlsl
VertexPositionInputs posnInputs = GetVertexPositionInputs(input.positionOS);
VertexNormalInputs normInputs = GetVertexNormalInputs(input.normalOS, input.tangentOS);
output.positionCS = posnInputs.positionCS;
output.uv = TRANSFORM_TEX(input.uv, _ColorMap);
output.normalWS = normInputs.normalWS;
output.tangentWS = float4(normInputs.tangentWS, input.tangentOS.w);
output.positionWS = posnInputs.positionWS;
return output;
}
float4 Fragment(Interpolators input) : SV_TARGET{
float3 normalWS = input.normalWS;
float3 positionWS = input.positionWS;
float3 viewDirWS = GetWorldSpaceNormalizeViewDir(positionWS); // In ShaderVariablesFunctions.hlsl
float3 viewDirTS = GetViewDirectionTangentSpace(input.tangentWS, normalWS, viewDirWS); // In ParallaxMapping.hlsl
float2 uv = input.uv;
float4 colorSample = SAMPLE_TEXTURE2D(_ColorMap, sampler_ColorMap, uv) * _ColorTint;
float3 normalTS = UnpackNormalScale(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, uv), _NormalStrength);
float3x3 tangentToWorld = CreateTangentToWorld(normalWS, input.tangentWS.xyz, input.tangentWS.w);
normalWS = normalize(TransformTangentToWorld(normalTS, tangentToWorld));
InputData lightingInput = (InputData)0;
lightingInput.positionWS = positionWS;
lightingInput.normalWS = normalWS;
lightingInput.viewDirectionWS = viewDirWS;
lightingInput.shadowCoord = TransformWorldToShadowCoord(positionWS);
#if UNITY_VERSION >= 202120
lightingInput.positionCS = input.positionCS;
lightingInput.tangentToWorld = tangentToWorld;
#endif
SurfaceData surfaceInput = (SurfaceData)0;
surfaceInput.albedo = colorSample.rgb;
surfaceInput.alpha = colorSample.a;
#ifdef _SPECULAR_SETUP
surfaceInput.specular = SAMPLE_TEXTURE2D(_SpecularMap, sampler_SpecularMap, uv).rgb * _SpecularTint;
surfaceInput.metallic = 0;
#else
surfaceInput.specular = 1;
surfaceInput.metallic = SAMPLE_TEXTURE2D(_MetalnessMask, sampler_MetalnessMask, uv).r * _Metalness;
#endif
surfaceInput.emission = SAMPLE_TEXTURE2D(_EmissionMap, sampler_EmissionMap, uv).rgb * _EmissionTint;
surfaceInput.normalTS = normalTS;
float4 temp = UniversalFragmentPBR(lightingInput, surfaceInput);
#ifdef BLACK_AND_WHITE
temp = (temp.r + temp.g + temp.b) / 3;
#endif
float4 mul = (temp + _Offset)* _Step;
return ( mul - frac(mul)) / _Step ;
}
#endif
MyLitShadowCasterPass.hlsl
#ifndef MY_LIT_SHADOW_CASTER_PASS_INCLUDED
#define MY_LIT_SHADOW_CASTER_PASS_INCLUDED
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
struct Attributes {
float3 positionOS : POSITION;
float3 normalOS : NORMAL;
};
struct Interpolators {
float4 positionCS : SV_POSITION;
};
float3 _LightDirection;
float4 GetShadowCasterPositionCS(float3 positionWS, float3 normalWS) {
float3 lightDirectionWS = _LightDirection;
float4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, lightDirectionWS));
#if UNITY_REVERSED_Z
positionCS.z = min(positionCS.z, UNITY_NEAR_CLIP_VALUE);
#else
positionCS.z = max(positionCS.z, UNITY_NEAR_CLIP_VALUE);
#endif
return positionCS;
}
Interpolators Vertex(Attributes input) {
Interpolators output;
VertexPositionInputs posnInputs = GetVertexPositionInputs(input.positionOS);
VertexNormalInputs normInputs = GetVertexNormalInputs(input.normalOS);
output.positionCS = GetShadowCasterPositionCS(posnInputs.positionWS, normInputs.normalWS);
return output;
}
float4 Fragment(Interpolators input) : SV_TARGET{
return 0;
}
#endif