【Unity URP】以Render Feature实现卡通渲染中的刘海投影

Posted by FlowingCrescent on 2021-06-05
Estimated Reading Time 19 Minutes
Words 4.6k In Total
Viewed Times

前言

本文初发布于zhihu
https://zhuanlan.zhihu.com/p/232450616

众所周知,在卡通渲染领域有着许许多多的Trick,而角色的脸部作为阿宅们关注的重点,Trick自然也少不了。
在各种插画、动画中,角色头发常常在角色脸上投下与头发形状相似的阴影,但这个效果如果使用ShadowMap来实现的话,对ShadowMap的精度要求大到有些不现实。
一些例图:
image.png
image.png
image.png

笔者近日在实现卡渲时发现,如果没有这个刘海投影,总感觉还差点味。

此时的脸部阴影是使用软阴影做的,可见效果并不是很好

而使用本文所讲述的方法可以产生较好的效果:

(脸部法线笔者并没有进行球形映射或者其他什么修改,所以普通的自阴影部分或许有些不美观,各位只关注刘海投影即可)

效果实现原理

其实效果的原理相当简单,简略来说就一句话:
首先生成一个头发的纯色buffer,然后在渲染角色脸部的时候对这个纯色buffer做采样取得阴影区域即可。

本文目录为:

  1. 使用Render Feature生成纯色buffer
  2. 渲染脸部时对这个纯色buffer进行采样
  3. 一些改良
  4. 美中不足
  5. 结语
  6. Github范例工程链接

本文内容不多,因为只涉及这一个细节效果,不会谈及整个卡渲模型,如果读者对整体的卡通渲染模型,推荐阅读:
https://zhuanlan.zhihu.com/p/109101851

笔者所用Unity版本为2019.4.6f1,URP 7.3.1
笔者经验甚少,才浅学疏,难以避免文中出现错误,还请大家不吝斧正,只求轻喷。


使用Render Feature生成纯色buffer

以Create/Rendering/URP/Render Feature新建一个Render Feature。

image.png
打开之后能看见是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class CelHairShadow_Test : ScriptableRendererFeature
{
class CustomRenderPass : ScriptableRenderPass
{
// This method is called before executing the render pass.
// It can be used to configure render targets and their clear state. Also to create temporary render target textures.
// When empty this render pass will render to the active camera render target.
// You should never call CommandBuffer.SetRenderTarget. Instead call <c>ConfigureTarget</c> and <c>ConfigureClear</c>.
// The render pipeline will ensure target setup and clearing happens in an performance manner.
public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
{
}

// Here you can implement the rendering logic.
// Use <c>ScriptableRenderContext</c> to issue drawing commands or execute command buffers
// https://docs.unity3d.com/ScriptReference/Rendering.ScriptableRenderContext.html
// You don't have to call ScriptableRenderContext.submit, the render pipeline will call it at specific points in the pipeline.
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
}

/// Cleanup any allocated resources that were created during the execution of this render pass.
public override void FrameCleanup(CommandBuffer cmd)
{
}
}

CustomRenderPass m_ScriptablePass;

public override void Create()
{
m_ScriptablePass = new CustomRenderPass();

// Configures where the render pass should be injected.
m_ScriptablePass.renderPassEvent = RenderPassEvent.AfterRenderingOpaques;
}

// Here you can inject one or multiple render passes in the renderer.
// This method is called when setting up the renderer once per-camera.
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
renderer.EnqueuePass(m_ScriptablePass);
}
}



对32行及之后的内容我们基本可以忽略,只需要修改一处:
将第39行的renderPassEvent从AfterRenderingOpaques修改为BeforeRenderingOpaques.
这是为了保证我们在渲染脸部时,这个纯色Buffer已存在。

然后在类的开头写上我们的Setting:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[System.Serializable]
public class Setting
{
//标记头发模型的Layer
public LayerMask hairLayer;
//标记脸部模型的Layer
public LayerMask faceLayer;

//Render Queue的设置
[Range(1000, 5000)]
public int queueMin = 2000;
[Range(1000, 5000)]
public int queueMax = 3000;

//使用的Material
public Material material;
}
public Setting setting = new Setting();

然后到Setting文件夹(默认的Rendering List所存放的路径)下,
image.png通过Add Render Feature添加我们刚刚新建的这个Render Feature
image.png现在是这样的,我们可以先添加Layer
image.png并设置Hair Layer和Face Layer
image.pngimage.png(实际上眼睛也需要设置为Face,这些没意思的图就不多发了
image.pngimage.png

我们先把Material给填上吧,
新建一个Shader——由于URP没有默认的Shader模板,因此在此我给各位提供一个网上找来的模板,直接复制粘贴覆盖原有的Shader即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// Example Shader for Universal RP
// Written by @Cyanilux
// https://cyangamedev.wordpress.com/urp-shader-code/
Shader "Custom/UnlitShaderExample" {
Properties {
_BaseMap ("Example Texture", 2D) = "white" {}
_BaseColor ("Example Colour", Color) = (0, 0.66, 0.73, 1)
}
SubShader {
Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalRenderPipeline" }

HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
float4 _BaseColor;
CBUFFER_END
ENDHLSL

Pass {
Name "Example"
Tags { "LightMode"="UniversalForward" }

HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag

struct a2v {
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
float4 color : COLOR;
};

struct v2f {
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
float4 color : COLOR;
};

TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);

v2f vert(a2v v) {
v2f o;

//VertexPositionInputs positionInputs = GetVertexPositionInputs(input.positionOS.xyz);
//o.positionCS = positionInputs.positionCS;
// Or this :
o.positionCS = TransformObjectToHClip(v.positionOS.xyz);
o.uv = TRANSFORM_TEX(v.uv, _BaseMap);
o.color = v.color;
return o;
}

half4 frag(v2f i) : SV_Target {
half4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv);

return baseMap * _BaseColor * i.color;
}
ENDHLSL
}
}
}

以其新建Material并拖到RenderFeature里

那么回想一下我们的步骤,我们要用Render Feature渲染一个头发纯色图,那么很简单,我们只需要Return (1,1,1,1)即可,反正只要个颜色嘛!

是吗?

当然没有这么简单。如果只是这样画的话,就会绘制角色头后面的头发,无法拿到刘海的形状。
我们之前设置了一个Face Layer,就是为了在绘制头发纯色之前,先写入脸部的深度,之后在绘制头发时进行深度测试即可获取刘海形。

如此便说明我们要在这个Shader中写两个Pass,一个是给脸部写入深度用,一个是绘制头发用的,两个都很简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
Shader "Custom/HairShadowSoild_Test"
{
SubShader
{
Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalRenderPipeline" }

HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

ENDHLSL

Pass
{
Name "FaceDepthOnly"
Tags { "LightMode" = "UniversalForward" }

ColorMask 0
ZTest LEqual
ZWrite On

HLSLPROGRAM

#pragma vertex vert
#pragma fragment frag

struct a2v
{
float4 positionOS: POSITION;
};

struct v2f
{
float4 positionCS: SV_POSITION;
};


v2f vert(a2v v)
{
v2f o;

//VertexPositionInputs positionInputs = GetVertexPositionInputs(input.positionOS.xyz);
//v.positionCS = positionInputs.positionCS;
// Or this :
o.positionCS = TransformObjectToHClip(v.positionOS.xyz);
return o;
}

half4 frag(v2f i): SV_Target
{
return (0, 0, 0, 1);
}
ENDHLSL

}

Pass
{
Name "HairSimpleColor"
Tags { "LightMode" = "UniversalForward" }

Cull Off
ZTest LEqual
ZWrite Off

HLSLPROGRAM

#pragma vertex vert
#pragma fragment frag

struct a2v
{
float4 positionOS: POSITION;
};

struct v2f
{
float4 positionCS: SV_POSITION;
};


v2f vert(a2v v)
{
v2f o;

VertexPositionInputs positionInputs = GetVertexPositionInputs(v.positionOS.xyz);
o.positionCS = positionInputs.positionCS;
// Or this :
//o.positionCS = TransformObjectToHClip(v.positionOS.xyz);
return o;
}

half4 frag(v2f i): SV_Target
{
return float4(1, 1, 1, 1);
}
ENDHLSL

}

}
}

那么让我们把这个Material搁一旁,开始正式动工Render Feature吧
在CustomRenderPass类中添加以下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
      //用于储存之后申请来的RT的ID
public int soildColorID = 0;
public ShaderTagId shaderTag = new ShaderTagId("UniversalForward");
public Setting setting;

FilteringSettings filtering;
FilteringSettings filtering2;

//新的构造方法
public CustomRenderPass(Setting setting)
{
this.setting = setting;

//创建queue以用于两个FilteringSettings的赋值
RenderQueueRange queue = new RenderQueueRange();
queue.lowerBound = Mathf.Min(setting.queueMax, setting.queueMin);
queue.upperBound = Mathf.Max(setting.queueMax, setting.queueMin);

filtering = new FilteringSettings(queue, setting.faceLayer);
filtering2 = new FilteringSettings(queue, setting.hairLayer);
}

前面的每个变量我们之后都会用到,目前还只是声明一下.

然后给CustomRenderPass创建一个构造方法,其中便使用到我们的Setting
此时会有一个报错,因为下面在构造CustomRenderPass时用的还是无参的老方法,加个setting即可。

FilteringSettings笔者个人理解为一个过滤器,更直白来说,它可以帮助我们选择我们想要渲染的物体,
而为了选择物体,我们需要加一些条件,比如这个(些)物体的Render Queue,Layer等,这样Unity就会找到符合这些条件的物体,并用于之后的渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
{
//获取一个ID,这也是我们之后在Shader中用到的Buffer名
int temp = Shader.PropertyToID("_HairSoildColor");
//使用与摄像机Texture同样的设置
RenderTextureDescriptor desc = cameraTextureDescriptor;
cmd.GetTemporaryRT(temp, desc);
soildColorID = temp;
//将这个RT设置为Render Target
ConfigureTarget(temp);
//将RT清空为黑
ConfigureClear(ClearFlag.All, Color.black);

}

public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{

var draw1 = CreateDrawingSettings(shaderTag, ref renderingData, renderingData.cameraData.defaultOpaqueSortFlags);
draw1.overrideMaterial = setting.material;
draw1.overrideMaterialPassIndex = 0;
context.DrawRenderers(renderingData.cullResults, ref draw1, ref filtering);

var draw2 = CreateDrawingSettings(shaderTag, ref renderingData, renderingData.cameraData.defaultOpaqueSortFlags);
draw2.overrideMaterial = setting.material;
draw2.overrideMaterialPassIndex = 1;
context.DrawRenderers(renderingData.cullResults, ref draw2, ref filtering2);

}

RenderTextureDescriptor的设置其实可以更自定义,比如只用屏幕一半分辨率的Texture,或者用RGB565之类,这里就不展开讲了。
image.png
此时使用Frame Debugger即可看到我们所渲染出的这个buffer(因为这个凯露模型的衣服和头发是同一网格,便使用同一材质,所以我们能在buffer中看见衣服也被渲染了)。
那么,让我们进入下一个阶段吧。


渲染脸部时对这个纯色buffer进行采样

笔者虽然自己在URP实现了Cel Shading,奈何本人学疏才浅,Shader写得有些不堪入目、杂乱无章,不适合拿出来给各位展示,因此笔者就提供一些资料供各位参考,还请见谅。

一是前文所分享的2173大佬的文
还有一个不得不提的当然是Colin大佬的URP Toon Lit Shader:
https://github.com/ColinLeung-NiloCat/UnityURPToonLitShaderExample

如果你并不会书写URP中的Shader,笔者建议学习此文:
https://cyangamedev.wordpress.com/2020/06/05/urp-shader-code/


那么让我们回归正题,
既然我们已经拥有绘制了头发的buffer,要怎么采样它呢?

本文的方法是使用View Space的Light Direction
image.png
比如当环境的光照方向是这样时,便会有如红箭头方向的采样,
得到的就会是与下图类似的结果。
image.png

我在Shader中声明了Keyword “_IsFace”,来标记是否为脸,产生Shader的变体(Variant)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
         struct v2f
{
float4 positionCS: SV_POSITION;
float2 uv: TEXCOORD0;
float3 positionWS: TEXCOORD1;
float3 normal: TEXCOORD2;

#if _IsFace
float4 positionSS: TEXCOORD3;
#endif
};

TEXTURE2D(_HairSoildColor);
SAMPLER(sampler_HairSoildColor);

v2f vert(a2v v)
{
v2f o;

//......

#if _IsFace
o.positionSS = ComputeScreenPos(positionInputs.positionCS);
#endif

//......
return o;
}


half4 frag(v2f i): SV_Target
{

//......


//face shadow
#if _IsFace
//计算该像素的Screen Position
float2 scrPos = i.positionSS.xy / i.positionSS.w;
//获取屏幕信息
float4 scaledScreenParams = GetScaledScreenParams();
//计算View Space的光照方向
float3 viewLightDir = normalize(TransformWorldToViewDir(mainLight.direction));

//计算采样点,其中_HairShadowDistace用于控制采样距离
float2 samplingPoint = scrPos + _HairShadowDistace * viewLightDir.xy * float2(1 / scaledScreenParams.x, 1 / scaledScreenParams.y);

//若采样点在阴影区内,则取得的value为1,作为阴影的话还得用1 - value;
float hairShadow = 1 - SAMPLE_TEXTURE2D(_HairSoildColor, sampler_HairSoildColor, samplingPoint).r;

//将作为二分色依据的ramp乘以shadow值
ramp *= hairShadow;

#else
//若不是脸,直接将ramp乘以Shadow map的采样值,Shadow map的计算在此文非重点,姑且略过
ramp *= shadow;

#endif
//......
}

image.png
此时调整_HairShadowDistace的值便可获得类似如此的结果
基本的效果已经有了,但是还是存在一点问题。


一些改良

以NDC.w调整采样距离

如果只是这样采样的话,会发现如果摄像机离人物的脸比较近,则阴影采样距离会显得很小,如图
image.png

而离得远了,采样距离又会显得很大
image.png
显然,这并不是我们想看到的结果,那要如何调整呢?

读者是否觉得这种情况似曾相识?
没错,了解卡通渲染的朋友可能会觉得这跟解决Back facing描边粗细的问题十分相似,
使用NDC来调整back facing描边,是一大著名的方案。
只是back facing描边存在的问题是“近大远小”,而我们这个是“近小远大”。
不妨试试看?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
struct v2f
{
//......

#if _IsFace
float4 positionSS: TEXCOORD3;
float posNDCw: TEXCOORD4;
#endif

//......
};

v2f vert(a2v v)
{
//......

#if _IsFace
o.posNDCw = positionInputs.positionNDC.w;
o.positionSS = ComputeScreenPos(positionInputs.positionCS);
#endif

//......
return o;
}

half4 frag(v2f i): SV_Target
{
//......
#if _IsFace

float2 scrPos = i.positionSS.xy / i.positionSS.w;
float4 scaledScreenParams = GetScaledScreenParams();

//在Light Dir的基础上乘以NDC.w的倒数以修正摄像机距离所带来的变化
float3 viewLightDir = normalize(TransformWorldToViewDir(mainLight.direction)) * (1 / i.posNDCw) ;

float2 samplingPoint = scrPos + _HairShadowDistace * viewLightDir.xy * float2(1 / scaledScreenParams.x, 1 / scaledScreenParams.y);

float hairShadow = 1 - SAMPLE_TEXTURE2D(_HairSoildColor, sampler_HairSoildColor, samplingPoint).r;

ramp *= hairShadow;

#else
//......
}

image.png
此时我们在较近距离也可以看到明显的阴影区域了
image.png中等距离也能看见较为明显的阴影

image.png较远时阴影就会消隐至看不见了。

那么这一问题也基本解决了。
当然,这并不是最完美的方法,或许美术会觉得近处阴影范围太大,中等距离阴影范围太小等……可以在这个基础上用曲线函数再调整一下采样距离,精益求精,争取获得最好的效果。

解决特定角度错误采样的问题

当我们摄像机的角度与脸的正前方基本垂直时,我们会发现角色脸部出现了非常诡异的现象,如图
image.png

究其原因,是由于脸部错误采样到了它“后面”的头发,而显然这种情况是我们不愿看到的。
image.png如图,脸部错误采样了两个红框圈出的头发。

那咋办呢……这也没法深度检测,又没深度图。

确实是没有深度图,不过我们可以手动来进行“深度检测”。
我们只要在buffer中写入头发的深度,不就可以在渲染脸部时进行两者的深度比较了吗?

不过……深度值咋算的来着?
对深度图中的深度值如何计算不清楚的读者可以参考这篇文章:
https://www.jianshu.com/p/6eb1f4501126

这边直接贴出结论:
Pndc = Pclip / w;
d = Pndc.z * 0.5 + 0.5;

因此我们将用于RenderFeature的Shader的第二个Pass改为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
Pass
{
Name "HairSimpleColor"
Tags { "LightMode" = "UniversalForward" }

Cull Off
ZTest LEqual
ZWrite Off

HLSLPROGRAM

#pragma vertex vert
#pragma fragment frag

struct a2v
{
float4 positionOS: POSITION;
};

struct v2f
{
float4 positionCS: SV_POSITION;
};


v2f vert(a2v v)
{
v2f o;

VertexPositionInputs positionInputs = GetVertexPositionInputs(v.positionOS.xyz);
o.positionCS = positionInputs.positionCS;
return o;
}

half4 frag(v2f i): SV_Target
{
float depth = i.positionCS.z * 0.5 + 0.5 / i.positionCS.w;
return float4(1, depth, 0, 1);
}
ENDHLSL

}

我们在buffer的g通道中写入了头发的depth,在采样时进行比较即可

当然,我们也要用同样的方法计算脸部的depth值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#if _IsFace

float2 scrPos = i.positionSS.xy / i.positionSS.w;
float4 scaledScreenParams = GetScaledScreenParams();
float3 viewLightDir = normalize(TransformWorldToViewDir(mainLight.direction)) * (1 / i.posNDCw) ;

float2 samplingPoint = scrPos + _HairShadowDistace * viewLightDir.xy * float2(1 / scaledScreenParams.x, 1 / scaledScreenParams.y);
//新增的“深度测试”
float depth = i.positionCS.z * 0.5 + 0.5 / i.positionCS.w;
float hairDepth = SAMPLE_TEXTURE2D(_HairSoildColor, sampler_HairSoildColor, samplingPoint).g;
//0.001为bias,用于精度校正
float depthCorrect = depth < hairDepth + 0.001 ? 0 : 1;

float hairShadow = 1 - SAMPLE_TEXTURE2D(_HairSoildColor, sampler_HairSoildColor, samplingPoint).r;

hairShadow = lerp(0, 1, depthCorrect);

ramp *= hairShadow;

#else

此时可以回过头来看shader

1
2
3
4
//这行的采样已经不再必要
//float hairShadow = 1 - SAMPLE_TEXTURE2D(_HairSoildColor, sampler_HairSoildColor, samplingPoint).r;

float hairShadow = lerp(0, 1, depthCorrect);

我们在下一行就用lerp(0, 1, depthCorrect)把上一行的采样值覆盖掉了。

究其原因,是因为r通道原本就是只“输出一个值以标记这个像素是头发”,而我们将深度写入g通道时,就隐性地完成了这一步。因此,我们buffer的r通道已经失去意义了。

因此我们在RenderFeature的shader将写入r通道的部分舍去,产出一个只写有深度的buffer即可。

相信这部分的修改各位一定都会,就不加以赘述了。

同样的,还记得我们之前哪一步跟深度有关吗?

没错,之前我们有一次要写入脸部深度,为的就是不让脸部接收到来自“后面”的头发的阴影。

既然我们已经在buffer中写入头发的深度了,那么使用脸部与头发的深度对比,完全能够规避这个问题,于是乎我们Face写入深度的那一次渲染也是可以舍弃的。这样一来就更加节省资源了。

或许有的朋友要问了,“那为什么不一开始就往最合理的方向写文章呢?你这样让我改来改去不是很烦”。

笔者其实不仅仅是想分享一个技术,同时也想跟大家分享一下自己创作的思路、一个想法不断被完善的过程。

每一个想法的实现都不是一帆风顺的,都会经历数次删删改改,个人认为学习这一过程其实比直接学习所谓最终效果更有价值。


image.png
那么,我们终于解决了之前的鼻梁错误阴影问题。
……但我们真的取得完全胜利了吗?


美中不足

上一张图,如果读者仔细看的话,会发现其实额头部分的一些阴影也被消除了,如下图红圈圈出的部分。
image.png

这又是什么原因呢?
正是因为我们消除了脸部“后面”的头发的投影,但是实际上这种投影是完全可能发生的。
也就是说,之前解决“鼻梁阴影”的方法还是过于一刀切了,将一些可能实际存在的状况也否决,也就导致一些情况下会出错了。

而这种情况又要怎么解决?
或许我们要给脸部加一个Mask,比如额头上不采用深度检测,而鼻梁上采用。
笔者比较懒,就暂且使用positionWS了,其实原理上应该使用positionOS或者上个贴图。

1
2
3
4
//......
float mask = smoothstep(1.6, 1.51, i.positionWS.y);
float depthCorrect = depth * mask < hairDepth + 0.001 ? 0 : 1;
//......

image.pngmask
image.png中途截断的Shadow显然不怎么好看……

可惜笔者并没有想到更好的方法来解决这个问题,或许画一个Mask是最优解?

但仅是这种方法已经足以应付绝大部分情况了
image.png加mask之前的效果
image.png加了mask之后

咱们的Mask并没有木大,今后也要继续做加把劲骑士。

那么最后,放上一个灯光和人物都在转的效果视频吧

结语

在此必须要感谢某waifu群的群友们,他们肯于抽出时间回答我这种图形学萌新的问题,并给出不少有意义的建议,如果没有你们就没有这篇文章,实在感谢。

虽然这个方法仍然存在一些瑕疵,但是个人认为已经能够使用了,如果各位有建议的话,欢迎踊跃评论交流~

Github

https://github.com/FlowingCrescent/CelShadingWithFringeShadow_URP

参考资料

https://zhuanlan.zhihu.com/p/109101851
https://github.com/ColinLeung-NiloCat/UnityURPToonLitShaderExample
https://cyangamedev.wordpress.com/2020/06/05/urp-shader-code/
https://zhuanlan.zhihu.com/p/144209981
https://www.jianshu.com/p/6eb1f4501126


感谢您阅读完本文,若您认为本文对您有所帮助,可以将其分享给其他人;若您发现文章对您的利益产生侵害,请联系作者进行删除。