Unity URP渲染管线PBR基础上增加卡通Shader效果

游戏中经常出现使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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值