【Unity URP】以Render Feature實現卡通渲染中的劉海投影
0。 前言
眾所周知,在卡通渲染領域有著許許多多的Trick,而角色的臉部作為阿宅們關注的重點,Trick自然也少不了。
在各種插畫、動畫中,角色頭髮常常在角色臉上投下與頭髮形狀相似的陰影,但這個效果如果使用ShadowMap來實現的話,對ShadowMap的精度要求大到有些不現實。
一些例圖:
筆者近日在實現卡渲時發現,如果沒有這個劉海投影,總感覺還差點味。
此時的臉部陰影是使用軟陰影做的,可見效果並不是很好
而使用本文所講述的方法可以產生較好的效果:
(臉部法線筆者並沒有進行球形對映或者其他什麼修改,所以普通的自陰影部分或許有些不美觀,各位只關注劉海投影即可)
0。1 效果實現原理
其實效果的原理相當簡單,簡略來說就一句話:
首先生成一個頭發的純色buffer,然後在渲染角色臉部的時候對這個純色buffer做取樣取得陰影區域即可。
本文目錄為:
使用Render Feature生成純色buffer
渲染臉部時對這個純色buffer進行取樣
一些改良
美中不足
結語
Github範例工程連結
本文內容不多,因為只涉及這一個細節效果,不會談及整個卡渲模型,如果讀者對整體的卡通渲染模型,推薦閱讀:
0。2 注
筆者所用Unity版本為2019。4。6f1,URP 7。3。1
筆者經驗甚少,才淺學疏,難以避免文中出現錯誤,還請大家不吝斧正,只求輕噴。
1。 使用Render Feature生成純色buffer
以Create/Rendering/URP/Render Feature新建一個Render Feature。
開啟之後能看見是這樣的:
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
// 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
// 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:
[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所存放的路徑)下,
透過Add Render Feature新增我們剛剛新建的這個Render Feature
現在是這樣的,我們可以先新增Layer
並設定Hair Layer和Face Layer
(實際上眼睛也需要設定為Face,這些沒意思的圖就不多發了)
我們先把Material給填上吧,
新建一個Shader——由於URP沒有預設的Shader模板,因此在此我給各位提供一個網上找來的模板,直接複製貼上覆蓋原有的Shader即可
// Example Shader for Universal RP
// Written by @Cyanilux
// https://cyangamedev。wordpress。com/urp-shader-code/
Shader
“Custom/UnlitShaderExample”
{
Properties
{
_BaseMap
(
“Example Texture”
,
2
D
)
=
“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,一個是給臉部寫入深度用,一個是繪製頭髮用的,兩個都很簡單
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類中新增以下內容
//用於儲存之後申請來的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就會找到符合這些條件的物體,並用於之後的渲染。
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之類,這裡就不展開講了。
此時使用Frame Debugger即可看到我們所渲染出的這個buffer(因為這個凱露模型的衣服和頭髮是同一網格,便使用同一材質,所以我們能在buffer中看見衣服也被渲染了)。
那麼,讓我們進入下一個階段吧。
2。 渲染臉部時對這個純色buffer進行取樣
筆者雖然自己在URP實現了Cel Shading,奈何本人學疏才淺,Shader寫得有些不堪入目、雜亂無章,不適合拿出來給各位展示(2021。01。15更新Github工程,可見文末),因此筆者就提供一些資料供各位參考,還請見諒。
一是前文所分享的2173大佬的文
還有一個不得不提的當然是Colin大佬的URP Toon Lit Shader:
如果你並不會書寫URP中的Shader,筆者建議學習此文:
————————————————-
那麼讓我們迴歸正題,
既然我們已經擁有繪製了頭髮的buffer,要怎麼取樣它呢?
本文的方法是使用View Space的Light Direction
比如當環境的
光照方向
是這樣時,便會有如紅箭頭方向的取樣,
得到的就會是與下圖類似的結果。
我在Shader中聲明瞭Keyword “_IsFace”,來標記是否為臉,產生Shader的變體(Variant)
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
//……
}
此時調整_HairShadowDistace的值便可獲得類似如此的結果
基本的效果已經有了,但是還是存在一點問題。
3。 一些改良
3。1 以NDC。w調整取樣距離
如果只是這樣取樣的話,會發現如果攝像機離人物的臉比較近,則陰影取樣距離會顯得很小,如圖
而離得遠了,取樣距離又會顯得很大
顯然,這並不是我們想看到的結果,那要如何調整呢?
讀者是否覺得這種情況似曾相識?
沒錯,瞭解卡通渲染的朋友可能會覺得這跟解決Back facing描邊粗細的問題十分相似,
使用NDC來調整back facing描邊,是一大著名的方案。
只是back facing描邊存在的問題是“
近大遠小
”,而我們這個是“近小遠大”。
不妨試試看?
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
//……
}
此時我們在較近距離也可以看到明顯的陰影區域了
中等距離也能看見較為明顯的陰影
較遠時陰影就會消隱至看不見了。
那麼這一問題也基本解決了。
當然,這並不是最完美的方法,或許美術會覺得近處陰影範圍太大,中等距離陰影範圍太小等……可以在這個基礎上用曲線函式再調整一下采樣距離,精益求精,爭取獲得最好的效果。
3。2 解決特定角度錯誤取樣的問題
當我們攝像機的角度與臉的正前方基本垂直時,我們會發現角色臉部出現了非常詭異的現象,如圖
究其原因,是由於臉部錯誤取樣到了它“後面”的頭髮,而顯然這種情況是我們不願看到的。
如圖,臉部錯誤取樣了兩個紅框圈出的頭髮。
那咋辦呢……這也沒法深度檢測,又沒深度圖。
確實是沒有深度圖,不過我們可以手動來進行“深度檢測”。
我們只要在buffer中寫入頭髮的深度,不就可以在渲染臉部時進行兩者的深度比較了嗎?
不過……深度值咋算的來著?
對
深度圖
中的深度值如何計算不清楚的讀者可以參考這篇文章:
這邊直接貼出結論(在OpenGL中):
depth = POSclip。z / POSclip。w;
depth = depth * 0。5 + 0。5;
因此我們將用於RenderFeature的Shader的第二個Pass改為
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
/
i
。
positionCS
。
w
)
*
0。5
+
0。5
;
return
float4
(
1
,
depth
,
0
,
1
);
}
ENDHLSL
}
我們在buffer的g通道中寫入了頭髮的depth,在取樣時進行比較即可
當然,我們也要用同樣的方法計算臉部的depth值。
#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
/
i
。
positionCS
。
w
)
*
0。5
+
0。5
;
float
hairDepth
=
SAMPLE_TEXTURE2D
(
_HairSoildColor
,
sampler_HairSoildColor
,
samplingPoint
)。
g
;
//0。0001為bias,用於
精度校正
float
depthCorrect
=
depth
<
hairDepth
+
0。0001
?
0
:
1
;
float
hairShadow
=
1
-
SAMPLE_TEXTURE2D
(
_HairSoildColor
,
sampler_HairSoildColor
,
samplingPoint
)。
r
;
hairShadow
=
lerp
(
0
,
1
,
depthCorrect
);
ramp
*=
hairShadow
;
#else
————————-
2020。09。14 更新
此時可以回過頭來看shader
//這行的取樣已經不再必要
//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寫入深度的那一次渲染也是可以捨棄的
。這樣一來就更加節省資源了。
或許有的朋友要問了,“那為什麼不一開始就往最合理的方向寫文章呢?你這樣讓我改來改去不是很煩”。
筆者其實不僅僅是想分享一個技術,同時也想跟大家分享一下自己創作的思路、一個想法不斷被完善的過程。
每一個想法的實現都不是一帆風順的,都會經歷數次刪刪改改,個人認為學習這一過程其實比直接學習所謂最終效果更有價值。
————————-
那麼,我們終於解決了之前的鼻樑錯誤陰影問題。
……但我們真的取得完全勝利了嗎?
4。 美中不足
上一張圖,如果讀者仔細看的話,會發現其實額頭部分的一些陰影也被消除了,如下圖紅圈圈出的部分。
這又是什麼原因呢?
正是因為我們消除了臉部“後面”的頭髮的投影,但是實際上這種投影是完全可能發生的。
也就是說,之前解決“
鼻樑陰影
”的方法還是過於一刀切了,將一些可能實際存在的狀況也否決,也就導致一些情況下會出錯了。
而這種情況又要怎麼解決?
或許我們要給臉部加一個Mask,比如額頭上不採用深度檢測,而鼻樑上採用。
筆者比較懶,就暫且使用positionWS了,其實原理上應該使用positionOS或者上個貼圖。
//……
float
mask
=
smoothstep
(
1。6
,
1。51
,
i
。
positionWS
。
y
);
float
depthCorrect
=
depth
*
mask
<
hairDepth
+
0。001
?
0
:
1
;
//……
mask
中途截斷的Shadow顯然不怎麼好看……
可惜筆者並沒有想到更好的方法來解決這個問題,或許畫一個Mask是
最優解
?
但僅是這種方法已經足以應付絕大部分情況了
加mask之前的效果
加了mask之後
咱們的Mask並沒有木大,今後也要繼續做加把勁騎士。
那麼最後,放上一個燈光和人物都在轉的效果影片吧
5。 結語
在此必須要感謝某waifu群的群友們,他們肯於抽出時間回答我這種
圖形學
萌新的問題,並給出不少有意義的建議,如果沒有你們就沒有這篇文章,實在感謝。
雖然這個方法仍然存在一些瑕疵,但是個人認為已經能夠使用了,如果各位有建議的話,歡迎踴躍評論交流~
6。 Github範例工程
參考資料
https://
zhuanlan。zhihu。com/p/10
9101851
https://
github。com/ColinLeung-N
iloCat/UnityURPToonLitShaderExample
https://
cyangamedev。wordpress。com
/2020/06/05/urp-shader-code/
https://
zhuanlan。zhihu。com/p/14
4209981
https://www。
jianshu。com/p/6eb1f4501
126
上一篇:什麼品牌的衛浴產品比較好呢?