Init
This commit is contained in:
232
03-UnrealEngine/卡通渲染相关资料/渲染功能/阴影控制/SDF面部阴影/二维SDF面部阴影方案.md
Normal file
232
03-UnrealEngine/卡通渲染相关资料/渲染功能/阴影控制/SDF面部阴影/二维SDF面部阴影方案.md
Normal file
@@ -0,0 +1,232 @@
|
||||
---
|
||||
title: 二维SDF面部阴影方案
|
||||
date: 2023-12-08 23:43:36
|
||||
excerpt:
|
||||
tags:
|
||||
rating: ⭐
|
||||
---
|
||||
原文为:https://zhuanlan.zhihu.com/p/670837192
|
||||
# 效果
|
||||
https://www.bilibili.com/video/BV1Ac411D7gj/?t=2&spm_id_from=333.1007.seo_video.first&vd_source
|
||||
|
||||
## 一、前言
|
||||
|
||||
目前实时渲染领域卡通渲染脸部光照主要有两种实现方法:
|
||||
1. 修改法线
|
||||
2. 使用SDF贴图
|
||||
|
||||
关于脸部卡通渲染方案的介绍可以看参考下面这篇文章:
|
||||
https://zhuanlan.zhihu.com/p/411188212
|
||||
|
||||
下面说一下我对这两种方案的理解。
|
||||
|
||||
### 1.1修改法线的方案
|
||||

|
||||
|
||||
首先,如果只是修改模型顶点的法线的话,模型顶点数就那么多,比如上面的模型已经做得很细致了,但顶点数也就5k,传递球形法线做比较柔和的光照效果还比较合适,想表现复杂的结构是比较困难的。
|
||||
当然也有修改模型拓扑等方法,但我觉得这些方法都不是特别优雅。
|
||||
我个人觉得使用法线贴图会更有潜力一些,自己私底下也有在研究把SDF烘培成法线贴图的方法,目前的结果我自己并不是很满意,如果后续有一些有意思的结果,我会再出一篇文章跟大家分享。
|
||||
|
||||

|
||||
|
||||
通过SDF生成的法线贴图
|
||||

|
||||
使用SDF法线贴图shading的效果
|
||||
> 为了方便烘培成世界空间的法线,烘培到切线空间会更合适
|
||||
|
||||
### 1.2 使用SDF的方案
|
||||
SDF的方案是目前市面上使用得最多的了,新出的二游基本都会用这种方式。
|
||||
在实现上,SDF的方案并不困难,但是由于我自己使用unreal引擎比较多,自己想通过改管线的方法来实现的话很难改得很优雅,所以我以后还会继续研究法线贴图方案。
|
||||
在表现上,目前SDF的方案最明显的问题是没有Z轴上的变化,这也是这篇文章主要想解决的问题。本文的实现思路其实很简单,目前的SDF都只画了一组水平轴的光照,那么我们把其他所有轴都画出来就行了。
|
||||

|
||||
|
||||
使用全角度的SDF图集代替水平轴的SDF
|
||||
两三年前看到原神的SDF方案时就想到了这个方法,相信平时比较关注卡通渲染的各位都想到过这个方法,但是几年过去了,没有看到任何一篇文章实现了这个方案,看来大伙也都觉得把所有方向的光照都画出来工作量太大了不敢尝试,所以只能我自己尝试实现给大伙看了。
|
||||
|
||||
## 二、实现流程
|
||||
### 2.1 流程概况
|
||||
三句话简单概括一下:
|
||||
1.画好各个角度的光照,如下我一共画了65张,稍微偷懒点少画写应该也没问题。
|
||||
|
||||

|
||||
|
||||
在blender中绘制光照图
|
||||
2.把画完的光照图转成SDF,然后拼成一张图集
|
||||
|
||||

|
||||
|
||||
生成的SDF和SDF图集
|
||||
3.通过光源角度<E8A792>和<EFBFBD>计算合适的uv,采样4次进行插值。
|
||||
|
||||

|
||||
|
||||
采样四次SDF图集计算光照结果
|
||||
|
||||
### 2.2 光照图绘制
|
||||
绘制光照没有目前没想到什么很好的生成方式,纯靠手绘,我没啥美术能力,画这65张图,花了我一整个星期的空闲时间,如果有美术大佬愿意画的这东西的话可能效果会更好一些。(这一堆的模型,有点制作表情blend shape的感觉了)
|
||||
光照图我画了9行9列(第一行和最后一行只有一张,所有一共9x7+2=65张)
|
||||
光照图的每一行代表平行光从下面的半球从上到下的9种不同的经度投射到脸上,每一列代表9不同的纬度,第一行和最后一行平行光从正上和正下投射过来,所以只有一张。
|
||||
|
||||

|
||||
绘制的光照图相当于光照从左半球上各点照射过来的结果
|
||||
|
||||

|
||||
|
||||
有一点需要注意的,可以看到我绘制光照图的时候涂出了uv的边界,这样生成的SDF在uv边界效果不会出问题(不想让SDF超出uv边界的话等生成了SDF之后再把超出边界的部分去掉就行了):
|
||||
|
||||

|
||||
|
||||
绘制光照图时建议涂出uv边界
|
||||
下面这个是反面例子:
|
||||

|
||||
|
||||
脸部上方uv边界处SDF插值效果不好
|
||||
下面放点动画里的光影变化作为参考:
|
||||
|
||||

|
||||
|
||||
奇蛋物语
|
||||
|
||||

|
||||
|
||||
奇蛋物语、偶像大师、Mygo
|
||||
|
||||

|
||||
|
||||
轻音少女
|
||||
### 2.3 SDF图集制作
|
||||
|
||||

|
||||
|
||||
使用8ssedt算法生成SDF
|
||||
|
||||
首先,我们需要把黑白的光照图转为SDF,我使用的是8ssedt算法生成的,关于8ssedt算法的详细可以看下面的这两篇文章:
|
||||
https://zhuanlan.zhihu.com/p/337944099
|
||||
https://blog.csdn.net/qq_41835314/article/details/128548073
|
||||
|
||||
接下来把所有的光照图拼成一张图集:
|
||||
|
||||

|
||||
|
||||
SDF图集
|
||||
这里解释一下,我为什么不生成这种SDF(这种SDF在下文都使用“插值后的SDF”特指)。
|
||||
|
||||

|
||||
|
||||
“插值后的SDF”
|
||||
使用这种插值后的SDF其实是有一个条件的:后面的光照一定要覆盖前面的光照。
|
||||
比如下面这套SDF图中,光照图的范围从左到右,一定是逐渐增大的
|
||||
|
||||

|
||||
|
||||
https://zhuanlan.zhihu.com/p/411188212
|
||||
这样导致的结果就是当光从前方照过来的时候,如下图左边整个脸必须是完全亮的,不可能出现右边这种脸的后方没被照亮的效果。
|
||||
|
||||

|
||||
|
||||
“使用插值后的SDF的缺点”
|
||||
再举一个例子,下面3张图之间没有包含关系,所有转成SDF再插值之后的效果是右边这样,是不是看起来就不对劲。
|
||||
|
||||

|
||||
|
||||
使用没有包含关系的图像生成“插值后的SDF”
|
||||
看看效果,确实不对劲:
|
||||

|
||||
“插值后的SDF”错误的效果
|
||||
导致这种错误的原因在于生成“插值后的SDF”的算法:
|
||||
- 算法在255次循环中计算出了不同光源角度对应的光照结果,这一步是没问题的
|
||||
- 但是为了能够将255种不同的结果保存在一张贴图里,算法里的做法是将所有结果累加起来,这一步破坏了插值信息
|
||||
|
||||
```cpp
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
for (int x = 0; x < width; x++) {
|
||||
for (int i = 0; i < 255; i++) {
|
||||
if (nextTexIndex >= int(grayImages.size())) {
|
||||
break;
|
||||
}
|
||||
float weight = lerpStep / levelStep;
|
||||
// 这里采样了相邻的两张SDF
|
||||
int curPixel = grayImages[curTexIndex].at<uchar>(y, x);
|
||||
int nextPixel = grayImages[nextTexIndex].at<uchar>(y, x);
|
||||
int lerpPixel = curPixel * weight + nextPixel * (1 - weight);
|
||||
// 在这里计算出了不同光源角度对应光照结果
|
||||
result += lerpPixel > 127 ? 0 : 1;
|
||||
// 结果进行累加
|
||||
lerpStep++;
|
||||
if (lerpStep >= levelStep)
|
||||
{
|
||||
lerpStep = 0;
|
||||
curTexIndex++;
|
||||
nextTexIndex++;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
lerpedSDF.at<Vec3b>(y, x)[0] = int(result);
|
||||
lerpedSDF.at<Vec3b>(y, x)[1] = int(result);
|
||||
lerpedSDF.at<Vec3b>(y, x)[2] = int(result);
|
||||
|
||||
result = 0;
|
||||
curTexIndex = 0;
|
||||
nextTexIndex = 1;
|
||||
lerpStep = 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
如果只做水平轴的SDF,其实“后面的光照范围必须大于前面的光照”的这个条件其实无伤大雅,而且用一张贴图代替了9张SDF,性价比很高
|
||||
但是我们做全角度的SDF光照,本身就需要做图集,直接用SDF就好了,而且不会受上面条件的影响,效果会更好。
|
||||
|
||||
### 2.4 工具
|
||||
|
||||
以上转SDF和拼图集的操作我写了一个小工具,你们直接用我的工具来做就行了,不用再浪费时间去重复造轮子了。
|
||||
|
||||

|
||||
|
||||
生成SDF和图集的小工具
|
||||
工具我和工程放在一起,解压SDFTool.zip然后双击main.exe就能打开
|
||||
|
||||

|
||||
|
||||
美术的同学直接使用打包好的工具就行了
|
||||
工具具体的使用文档可以在我的github上查看,源码也在上面,工具写得不是很鲁棒,如果出了什么问题的话有能力的朋友直接改源码吧。
|
||||
|
||||

|
||||
|
||||
### 2.5 计算光源角度采样SDF图集
|
||||
下面很多操作都跟正常的SDF脸部光照一样,我就不写得太详细了,具体直接打开我的工程看就行了。
|
||||
1.在蓝图里把角色脸部向前的向量和先左的向量传到角色的材质里
|
||||

|
||||
|
||||
就是把下图下面这两个箭头的方向传到材质里:
|
||||

|
||||
|
||||
- 叉乘朝左和朝前的向量,得到朝上的向量
|
||||
- 将光源方向与脸部朝左向量点乘,然后step一下,用于后面判断光源是从脸部左边还是右边照过来的
|
||||
- 将光源方向与脸部朝前、朝上的向量点乘,得到光源的水平夹角的$cos\theta$和垂直夹角的$cos\phi$
|
||||

|
||||
|
||||
- $cos\theta$和$cos\phi$它们是余弦值,不是线性的,所以我喜欢使用arccos把它们变成线性的角度:
|
||||

|
||||
|
||||
- 接下来采样的部分看起来比较杂乱
|
||||

|
||||
|
||||
我们稍微拆开来看:
|
||||
- 这里是定义了两个常数,应该图集是9x9的,所有定义了Row=1,Row-1的意思是Row-1=8
|
||||

|
||||
- 当光源在右边时,镜像翻转uv
|
||||
- 然后uv除以行数9,就得到了贴图左上角的第一张SDF的uv
|
||||

|
||||
|
||||
- 接下来要计算离当前光源角度最近的4张SDF的uv,比如当水平角度<E8A792>等于100度,垂直角度<E8A792>等于120度时会采样下面画的四个点的SDF,然后按照权重来插值,跟双线性插值很像。
|
||||

|
||||
|
||||
采样当前光源角度最近的四张SDF
|
||||
- 这里计算离光源最近的行数和列数
|
||||

|
||||
|
||||
- 接下来计算出uv,采样贴图并插值出光照结果最后上色
|
||||

|
||||
|
||||

|
BIN
03-UnrealEngine/卡通渲染相关资料/渲染功能/阴影控制/SDF面部阴影/二维SDF面部阴影方案.pdf
(Stored with Git LFS)
Normal file
BIN
03-UnrealEngine/卡通渲染相关资料/渲染功能/阴影控制/SDF面部阴影/二维SDF面部阴影方案.pdf
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
03-UnrealEngine/卡通渲染相关资料/渲染功能/阴影控制/SDF面部阴影/二维SDF面部阴影方案_网页备份不全.png
(Stored with Git LFS)
Normal file
BIN
03-UnrealEngine/卡通渲染相关资料/渲染功能/阴影控制/SDF面部阴影/二维SDF面部阴影方案_网页备份不全.png
(Stored with Git LFS)
Normal file
Binary file not shown.
195
03-UnrealEngine/卡通渲染相关资料/渲染功能/阴影控制/ToonShadow.md
Normal file
195
03-UnrealEngine/卡通渲染相关资料/渲染功能/阴影控制/ToonShadow.md
Normal file
@@ -0,0 +1,195 @@
|
||||
---
|
||||
title: Untitled
|
||||
date: 2024-12-08 12:18:54
|
||||
excerpt:
|
||||
tags:
|
||||
rating: ⭐
|
||||
---
|
||||
# 前言
|
||||
阴影渲染笔记:[[Shadow]]
|
||||
|
||||
实现功能:
|
||||
1. [ ] 控制深度偏移
|
||||
2. [ ] CustomDepth制作头发阴影偏移效果哦 https://zhuanlan.zhihu.com/p/689578355
|
||||
3. [ ] ContactShadow接触阴影实现衣服细节的DetailShadow
|
||||
4. [ ] 半程阴影
|
||||
# 半程阴影
|
||||
由晨风&Neverwind提出:
|
||||
- 【[UFSH2024]用虚幻引擎5为《幻塔》定制高品质动画流程风格化渲染管线 | 晨风 Neverwind 完美世界游戏】 【精准空降到 07:27】 https://www.bilibili.com/video/BV1rW2LYvEox/?share_source=copy_web&vd_source=fe8142e8e12816535feaeabd6f6cdc8e&t=447
|
||||
|
||||
**阴影Setup阶段**:
|
||||
```c++
|
||||
if 启用半程阴影:
|
||||
{
|
||||
额外进行一次CreatePerObjectProjectedShadow()
|
||||
{
|
||||
处理阴影光照Matrix //猜测:
|
||||
//在ProjectedShadowInfo->SetupPerObjectProjection()中调整,FProjectedShadowInfo.TranslatedWorldToView。
|
||||
//在LightSceneInfo->Proxy->GetPerObjectProjectedShadowInitializer(Bounds, ShadowInitializer)之后,修改WorldTolight。
|
||||
向阴影信息写入IsHalfViewShadowFlag//猜测:在FProjectedShadowInfo中添加判断Flag并且写入。
|
||||
用新的光源方向画Atlas //猜测:给带有对应Flog的DirectionLigh多创建一个对应的Atlas?
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
截图代码(半程阴影修改LightDirection逻辑):
|
||||
```c++
|
||||
if(PrimitiveSceneinfo->Proxy->IsToonDisableSelfShadow())
|
||||
{
|
||||
...
|
||||
for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
|
||||
{
|
||||
const FViewInfo& View = Views[ViewIndex];
|
||||
const FMatrix& ViewMatrix = View.ShadowViewMatrices.GetViewMatrix()
|
||||
FVector LightDirection = Lightsceneinfo->Proxy->GetDirection();
|
||||
const FVector CameraDirection = ViewMatrix.GetColumn(2).GetSafeNormal()
|
||||
float LightViewBlendFactor = PrimitiveSceneinfo->Proxy->GetToonHalfVienShadowFactor();
|
||||
Fvector HalfViewLightDir = (LightDirection + ( 1 - LightViewBlendFactor) + CameraDirection * LightViewBlendFactor).GetSafeNormal();
|
||||
FMatrix FinalCombineMatrix = FInverseRotationMatrix(HalfViewLightDir.Rotation())
|
||||
ShadowInitializer.WorldToLight = FinalCombineMatrix;
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
相关代码位于`FDirectionalLightSceneProxy::GetPerObjectProjectedShadowInitializer()`,在在FSceneRenderer::CreatePerObjectProjectedShadow()被调用。
|
||||
```c++
|
||||
virtual bool GetPerObjectProjectedShadowInitializer(const FBoxSphereBounds& SubjectBounds,FPerObjectProjectedShadowInitializer& OutInitializer) const override
|
||||
{
|
||||
OutInitializer.PreShadowTranslation = -SubjectBounds.Origin;
|
||||
OutInitializer.WorldToLight = FInverseRotationMatrix(FVector(WorldToLight.M[0][0],WorldToLight.M[1][0],WorldToLight.M[2][0]).GetSafeNormal().Rotation());
|
||||
OutInitializer.Scales = FVector2D(1.0f / SubjectBounds.SphereRadius,1.0f / SubjectBounds.SphereRadius);
|
||||
OutInitializer.SubjectBounds = FBoxSphereBounds(FVector::ZeroVector,SubjectBounds.BoxExtent,SubjectBounds.SphereRadius);
|
||||
OutInitializer.WAxis = FVector4(0,0,0,1);
|
||||
OutInitializer.MinLightW = -UE_OLD_HALF_WORLD_MAX;
|
||||
// Reduce casting distance on a directional light
|
||||
// This is necessary to improve floating point precision in several places, especially when deriving frustum verts from InvReceiverMatrix
|
||||
// This takes the object size into account to ensure that large objects get an extended distance
|
||||
OutInitializer.MaxDistanceToCastInLightW = FMath::Clamp(SubjectBounds.SphereRadius * CVarPerObjectCastDistanceRadiusScale.GetValueOnRenderThread(), CVarPerObjectCastDistanceMin.GetValueOnRenderThread(), (float)WORLD_MAX);
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
PS.很有可能需要创建2个Atlas。Atlas的创建位于***FSceneRenderer::AllocateCachedShadowDepthTargets()***。数据存储在***SortedShadowsForShadowDepthPass.ShadowMapAtlases***中。大致由FSceneRenderer::FinishInitDynamicShadows()调用。
|
||||
|
||||
**阴影Projection阶段**:
|
||||
//此阶段需要屏蔽角色投射到自己的非半程阴影
|
||||
//和角色投射到场景中会跟随视角移动的阴影
|
||||
```c++
|
||||
if(Toon材质,且没有半程阴影Flag的阴影
|
||||
&&非Toon材质但有半程阴影Flag的阴影)
|
||||
{
|
||||
屏蔽此阴影
|
||||
}
|
||||
```
|
||||
|
||||
PS.很有可能在FProjectedShadowInfo::RenderProjection()阶段进行判断以此保证合成正确的**ScreenShadowMask**。
|
||||
|
||||
|
||||
# 实现方法
|
||||
```c++
|
||||
const FMaterialRenderProxy* MaterialRenderProxy = MeshBatch.MaterialRenderProxy;
|
||||
bool bEnableToonMeshDrawOutline = MaterialRenderProxy->GetToonOutlineDataAssetRT()->Settings.bEnableToonMeshDrawOutline;
|
||||
```
|
||||
FProjectedShadowInfo->Scene
|
||||
FPrimitiveSceneProxy
|
||||
## 深度偏移
|
||||
### ~~方法一~~
|
||||
1. FProjectedShadowInfo添加变量。
|
||||
FSceneRenderer::RenderShadowDepthMaps() => RenderShadowDepthMapAtlases() => ProjectedShadowInfo->RenderDepth()
|
||||
|
||||
已放弃,FProjectedShadowInfo无法判断MeshSection。
|
||||
### 方法二(最终实现方法)
|
||||
在材质中使用ShadowPassSwitch再对ViewSpace的Z轴方向(使用DirectionalLightVector比较可以只对方向光进行偏移)进行WPO偏移实现。
|
||||
|
||||
其优点就是可以用贴图来控制偏移过渡。
|
||||
|
||||
## DirectionOffsetToViewShadow
|
||||
### 最终实现方法
|
||||
1. 在**FProjectionShadowInfo**中添加**bDirectionOffsetToViewShadow**标记以及对应的判断函数IsDirectionOffsetToViewShadow()来判断是否是DirectionOffsetToViewShadow。
|
||||
1. 在**FSceneRenderer::CreatePerObjectProjectedShadow()** 中再次调用SetupPerObjectProjection()逻辑创建**DirectionOffsetToViewShadow**时将FProjectedShadowInfo的***bDirectionOffsetToViewShadow设置成true***。
|
||||
2. 在PrimitiveSceneProxy.h 中添加DirectionOffsetToViewShadowAlpha变量作为偏移Alpha,同事添加函数UseDirectionOffsetToViewShadow()来判断是否开启这个功能。
|
||||
1. 在**FSceneRenderer::CreatePerObjectProjectedShadow()** 中取得PrimitiveSceneProxy中的DirectionOffsetToViewShadowAlpha,最后计算向量来设置***ShadowInitializer.WorldToLight***。大致为[[#DirectionOffsetToViewShadow Direction Code]]
|
||||
3. 在ToonDataAsset中添加RecivedViewOffsetShadow作为是否接收DirectionOffsetToViewShadow的依据。
|
||||
1. 将数据渲染到ToonDataAsset Texture中。
|
||||
2. 最终在ShadowProjectionPixelShader.usf获取并且计算。代码如下:
|
||||
|
||||
```c++
|
||||
//BlueRose Modify
|
||||
#if SHADING_PATH_DEFERRED && !FORWARD_SHADING && !SUBPIXEL_SHADOW && !STRATA_ENABLED /*&& !USE_TRANSMISSION*/
|
||||
FGBufferData GBufferData_Toon = GetGBufferData(ScreenUV);
|
||||
const uint ToonDataAssetID = GetToonDataAssetIDFromGBuffer(GBufferData_Toon);
|
||||
float RecivedViewOffsetShadow = GetRecivedViewOffsetShadow(ToonDataAssetID);
|
||||
|
||||
if (IsDirectionOffsetToViewShadow > 0)//ViewShadow ProjectionShadowInfo
|
||||
{
|
||||
/*if (RecivedViewOffsetShadow > 0)//ViewOffsetShadow
|
||||
{
|
||||
PerObjectDistanceFadeFraction *= 1.0;
|
||||
}*/
|
||||
if (RecivedViewOffsetShadow == 0)
|
||||
{
|
||||
PerObjectDistanceFadeFraction *= 0.0;
|
||||
}
|
||||
}else//Normal ProjectionShadowInfo
|
||||
{
|
||||
if (RecivedViewOffsetShadow > 0)
|
||||
{
|
||||
PerObjectDistanceFadeFraction *= 0.0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
//BlueRose Modify End
|
||||
float FadedShadow = lerp(1.0f, Shadow, ShadowFadeFraction * PerObjectDistanceFadeFraction);
|
||||
|
||||
#if FORWARD_SHADING || SHADING_PATH_MOBILE
|
||||
float LightInfluenceMask = GetLightInfluenceMask(TranslateWorldPosition);
|
||||
// Constrain shadowing from this light to pixels inside the light's influence, since other non-overlapping lights are packed into the same channel
|
||||
FadedShadow = lerp(1, FadedShadow, LightInfluenceMask);
|
||||
// Write into all channels, the write mask will constrain to the correct one
|
||||
OutColor = EncodeLightAttenuation(FadedShadow);
|
||||
#else
|
||||
float FadedSSSShadow = lerp(1.0f, SSSTransmission, ShadowFadeFraction * PerObjectDistanceFadeFraction);
|
||||
|
||||
// the channel assignment is documented in ShadowRendering.cpp (look for Light Attenuation channel assignment)
|
||||
OutColor = EncodeLightAttenuation(half4(FadedShadow, FadedSSSShadow, FadedShadow, FadedSSSShadow));
|
||||
#endif
|
||||
```
|
||||
#### DirectionOffsetToViewShadow Direction Code
|
||||
```c++
|
||||
...
|
||||
bool bToonDirectionOffsetToViewShadow = ToonDirectionOffsetToViewShadowCVar->GetValueOnRenderThread();
|
||||
float LightViewBlendFactor = PrimitiveSceneInfo->Proxy->DirectionOffsetToViewShadowAlpha;
|
||||
if (bToonDirectionOffsetToViewShadow && LightSceneInfo->Proxy->GetLightType() == LightType_Directional && LightViewBlendFactor > 0.0f)
|
||||
{
|
||||
//计算半程向量,针对每个View都会生成一个FProjectedShadowInfo,之后在RenderShadowProjection()中判断?
|
||||
for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
|
||||
{
|
||||
const FViewInfo& View = Views[ViewIndex];
|
||||
const FMatrix& ViewMatrix = View.ShadowViewMatrices.GetViewMatrix();
|
||||
FVector LightDirection = LightSceneInfo->Proxy->GetDirection();
|
||||
const FVector CameraDirection = ViewMatrix.GetColumn(2).GetSafeNormal();
|
||||
FVector HalfViewLightDir = (LightDirection * ( 1 - LightViewBlendFactor) + CameraDirection * LightViewBlendFactor).GetSafeNormal();
|
||||
FMatrix FinalCombineMatrix = FInverseRotationMatrix(HalfViewLightDir.Rotation());
|
||||
ShadowInitializer.WorldToLight = FinalCombineMatrix;
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# ~~NoSelfShadow~~此方法是错误的
|
||||
https://zhuanlan.zhihu.com/p/10073818586?utm_psn=1877051516055076866
|
||||
|
||||
UPrimitiveComponent::bSelfShadowOnly
|
||||
=>
|
||||
FPrimitiveSceneProxy::bSelfShadowOnly => **CastsSelfShadowOnly()**
|
||||
=>
|
||||
FProjectedShadowInfo::bSelfShadowOnly
|
||||
|
||||
- FProjectedShadowInfo::SetupMeshDrawCommandsForProjectionStenciling():设置Stencil为1。
|
||||
- 在`FProjectedShadowInfo::GatherDynamicMeshElements()`被调用,FSceneRenderer::GatherShadowDynamicMeshElements()。
|
||||
- FProjectedShadowInfo::SetupProjectionStencilMask:设置Stencil为7( pre-shadow/per-object static shadow)。
|
||||
- 在`FProjectedShadowInfo::RenderProjectionInternal()`被调用。
|
||||
|
||||
FProjectedShadowInfo::RenderProjectionInternal()
|
204
03-UnrealEngine/卡通渲染相关资料/渲染功能/阴影控制/卡通面部阴影控制.md
Normal file
204
03-UnrealEngine/卡通渲染相关资料/渲染功能/阴影控制/卡通面部阴影控制.md
Normal file
@@ -0,0 +1,204 @@
|
||||
---
|
||||
title: 卡通面部阴影控制
|
||||
date: 2023-03-08 10:18:36
|
||||
excerpt:
|
||||
tags:
|
||||
rating: ⭐
|
||||
---
|
||||
# 相关资料
|
||||
- [虚幻5渲染编程(风格化渲染篇) 第七卷: Toon shadow control](https://zhuanlan.zhihu.com/p/519728086)
|
||||
- [二次元角色卡通渲染—面部篇](https://zhuanlan.zhihu.com/p/411188212)
|
||||
- [掰法线的方法 SP Normal Convert Shader](https://note.com/sfna32121/n/n8d46090005d1)
|
||||
- 绘制SDF贴图方法
|
||||
- [卡通脸部阴影贴图生成 渲染原理](https://zhuanlan.zhihu.com/p/389668800)
|
||||
- [【教程】使用csp等高线填充工具制作三渲二面部阴影贴图](https://www.bilibili.com/video/BV16y4y1x7J1/)
|
||||
- SDF Shadow Shader
|
||||
- [神作面部阴影渲染还原](https://zhuanlan.zhihu.com/p/279334552)
|
||||
|
||||
|
||||
# SDF面部阴影
|
||||
## Signed Distance Fields
|
||||
- 生成距离场算法: http://www.codersnotes.com/notes/signed-distance-fields/
|
||||
|
||||
>这实质上就是找目标点及左上方四个点中,SDF最小的值。PASS0就是按照从上到下,从左到右的顺序,遍历整个图像,遍历完成之后,对于所有物体外的点,如果距离它最近的物体是在它的左上方,那么它的SDF值就已确定。类似的,PASS1就是按照从下到上,从右到左的顺序,依次比较右下方的四个点,遍历完成之后,对于所有物体外的点,如果距离它最近的物体是在它的右下方,那么它的SDF也已经确定了。两个PASS结合,那么整个图像的SDF就都计算出来了。(其实这里称SDF并不准确,因为只算了物体外到物体边界的距离,是正值,并没有signed一说,只有做完下一步计算物体内到物体外的距离,两个距离相减,才是SDF)
|
||||
|
||||
>第二个grid的GenerateSDF就很好理解了,就是**计算物体内部到外部的距离**。因为一个点要么在物体内要么在物体外,所以两次的SDF值要么全为零(在边界上),要么一个为0,一个为距离值。用:grid1(pixel).sdf - grid2(pixel).sdf就能得到完整的SDF。
|
||||
|
||||
1. 读取传入的贴图,并且构建2个RT,一个黑(inside Zero)一个白(Outside infinitely),当亮度大于0.5(128)时则反转。
|
||||
2. 分别对这2个RT执行 GenerateSDF()。
|
||||
3. 将2个RT记录的距离值相减,获得最终的SDF。
|
||||
|
||||
链接中的代码:
|
||||
```c++
|
||||
struct Point
|
||||
{
|
||||
int dx, dy;
|
||||
int DistSq() const { return dx*dx + dy*dy; }
|
||||
};
|
||||
|
||||
struct Grid
|
||||
{
|
||||
Point grid[HEIGHT][WIDTH];
|
||||
};
|
||||
```
|
||||
|
||||
```c++
|
||||
void Compare( Grid &g, Point &p, int x, int y, int offsetx, int offsety )
|
||||
{
|
||||
Point other = Get( g, x+offsetx, y+offsety );
|
||||
other.dx += offsetx;
|
||||
other.dy += offsety;
|
||||
|
||||
if (other.DistSq() < p.DistSq())
|
||||
p = other;
|
||||
}
|
||||
|
||||
//采用分离式计算SDF
|
||||
//GenerateSDF:计算并且取得当前UV坐标附近(第一次找到)像素最小值(模型使用 0值填充,即dx,dy=0),放到RT中。
|
||||
void GenerateSDF( Grid &g )
|
||||
{
|
||||
// Pass 0
|
||||
for (int y=0;y<HEIGHT;y++)
|
||||
{
|
||||
for (int x=0;x<WIDTH;x++)
|
||||
{
|
||||
Point p = Get( g, x, y );
|
||||
Compare( g, p, x, y, -1, 0 );
|
||||
Compare( g, p, x, y, 0, -1 );
|
||||
Compare( g, p, x, y, -1, -1 );
|
||||
Compare( g, p, x, y, 1, -1 );
|
||||
Put( g, x, y, p );
|
||||
}
|
||||
|
||||
for (int x=WIDTH-1;x>=0;x--)
|
||||
{
|
||||
Point p = Get( g, x, y );
|
||||
Compare( g, p, x, y, 1, 0 );
|
||||
Put( g, x, y, p );
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 1
|
||||
for (int y=HEIGHT-1;y>=0;y--)
|
||||
{
|
||||
for (int x=WIDTH-1;x>=0;x--)
|
||||
{
|
||||
Point p = Get( g, x, y );
|
||||
Compare( g, p, x, y, 1, 0 );
|
||||
Compare( g, p, x, y, 0, 1 );
|
||||
Compare( g, p, x, y, -1, 1 );
|
||||
Compare( g, p, x, y, 1, 1 );
|
||||
Put( g, x, y, p );
|
||||
}
|
||||
|
||||
for (int x=0;x<WIDTH;x++)
|
||||
{
|
||||
Point p = Get( g, x, y );
|
||||
Compare( g, p, x, y, -1, 0 );
|
||||
Put( g, x, y, p );
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```c++
|
||||
int dist1 = (int)( sqrt( (double)Get( grid1, x, y ).DistSq() ) );
|
||||
int dist2 = (int)( sqrt( (double)Get( grid2, x, y ).DistSq() ) );
|
||||
int dist = dist1 - dist2;
|
||||
```
|
||||
|
||||
## SDF面部阴影
|
||||
本质是使用SDF的过度原理(对X张贴图生成SDF贴图),使用HalfNoL来查询一个(0,180)区间的阴影。
|
||||
|
||||
### 效果
|
||||
- SDF面部阴影效果32秒处:https://www.bilibili.com/video/BV1JG4y1r7EP/?spm_id_from=333.337.search-card.all.click&vd_source=d47c0bb42f9c72fd7d74562185cee290
|
||||
|
||||
![[SDFShadowEffect.mp4]]
|
||||
|
||||
### 绘制方法
|
||||
- 等高线绘制方法:https://www.bilibili.com/video/BV16y4y1x7J1/?spm_id_from=autoNext&vd_source=d47c0bb42f9c72fd7d74562185cee290
|
||||
- 注意:等高线曲线需要关闭绘制抗锯齿。
|
||||
- 使用色阶就可以把各个角度的SDF贴图提取出来。
|
||||
- Maya烘焙绘制方法(应该也可以用UE)
|
||||
1. 给脸部模型一个Lambert材质,并且根据X轴角度要求设置好方向光。
|
||||
2. 使用Arnold的RenderToTexture,将贴图输出。
|
||||
3. 使用PS的色阶功能将颜色二值化,并在基础上进行修改。
|
||||
4. **对8张贴图两两计算SDF值,之后进行重映射** 。最终亮度=亮度/8 * index
|
||||
5. 将结果合并到一起。
|
||||
|
||||
### 代码
|
||||
```c++
|
||||
float3 UP = float3(0,1,0); // cs脚本里动态修改
|
||||
float3 Front = float3(0,0,1);
|
||||
float3 Left = cross(UP, Front);
|
||||
float3 Right = -cross(UP, Front);
|
||||
float FrontL = dot(normalize(Front.xz), normalize(L.xz));
|
||||
float LeftL = dot(normalize(Left.xz), normalize(L.xz));
|
||||
float RightL = dot(normalize(Right.xz), normalize(L.xz));
|
||||
|
||||
float lightAttenuation = (FrontL > 0) * min(
|
||||
(surfaceData._lightMap.r > LeftL),
|
||||
1-(1 - surfaceData._lightMap.r < RightL)
|
||||
);
|
||||
```
|
||||
|
||||
## Multi SDFFaceShadow
|
||||
- X轴(正面):1°~90°(8张) =>
|
||||
- X轴(背面-1°~-90°):考虑**单个阴影贴图**或者**边缘光计算方式的阴影**。
|
||||
- Y轴:30°、 90°、150°+ 背面阴影
|
||||
- Y轴(多张贴图):30、50°、70°、90°、110°、130°+ 背面阴影
|
||||
- -60° 、-40° 、-20° 、0、20°、40°
|
||||
- 考虑硬编码写死,所以可以使用不同步长的角度。
|
||||
- 贴图精度可以考虑8bit=>16bit
|
||||
|
||||
>Y轴结果插值可以考虑使用扩散Shader
|
||||
https://www.shadertoy.com/view/tlyfRw
|
||||
ShaderBits的 UV出血填充贴图方法。
|
||||
|
||||
|
||||
# 距离场+法线混合计算
|
||||
https://www.youtube.com/watch?v=T2iI9hbNqLI
|
||||
|
||||
![[SDF_Normal_Shadow.webp]]![[SDF_Normal_Shadow_Effect.mp4]]
|
||||
虽然分享中并没有说明具体怎么计算,不过有了这个思路,做出类似的效果并不是难事,以下简单还原的过程。
|
||||
首先主要代码如下:(图中还有两块区域,即额头偏亮的区域和鼻部底层暗下去的区域,由于并不清楚这两部分的应用场景,下面的计算就忽略这两部分区域,另外也忽略了精度问题)
|
||||
```c++
|
||||
float3 shaodwRamp = tex2D(_ShaodwRamp, input.uv);
|
||||
float3 lightDirH = normalize(float3(L.x, 0, L.z));
|
||||
float NoL = dot(N, lightDirH);
|
||||
float triArea = saturate((shaodwRamp.g - 0.5) * 2);
|
||||
float noseArea = saturate(shaodwRamp.g * 2);
|
||||
|
||||
//lightAtten
|
||||
float lightAtten = dot(lightDirH, forward);
|
||||
lightAtten = saturate(pow(abs(fmod(lightAtten + _TriAreaOffset, 1)
|
||||
- 0.5) * _TriAreaLimitUp, _TriAreaPow) * _TriAreaLimitDown - _TriAreaLimitDown + 1);
|
||||
|
||||
//uvMask
|
||||
float filpU = saturate(sign(dot(L, left)));
|
||||
float cutU = step(0.5, input.uv.x);
|
||||
float uvMask = lerp(1 - cutU, cutU, filpU);
|
||||
|
||||
//NoL
|
||||
float faceShadow = step(0, NoL);
|
||||
//三角区域
|
||||
float triAreaLight = step(lightAtten, triArea) * uvMask;
|
||||
//鼻部区域
|
||||
float noseAreaLight = step(lightAtten, 1 - noseArea) * (1 - uvMask);
|
||||
float noseAreaShadow = (step(lightAtten, noseArea) - 1) * uvMask + 1;
|
||||
|
||||
//最后的阴影混合
|
||||
float Shadow = min(max(max(faceShadow, triAreaLight), noseAreaLight), noseAreaShadow);
|
||||
```
|
||||
|
||||
`SDF取值的思路和上一小节SDF的思路是类似的,也是利用lightAtten的结果来取值,只不过这张图分别记录了两个区域的值,中间值是0.5,三角区域的阈值在(0.5, 1],鼻尖区域的阈值在[0, 0.5)。将这两部分区域重新映射回[0, 1]的区间上就可以使用之前的step操作了,然后利用计算的uvMask屏蔽掉不需要的区域,最后混合三个部分(三角亮区,鼻部亮区,鼻部暗区)的结果就可以了。`
|
||||
|
||||
另外由于SDF与NdotL的结果要互相配合,所以要对lightAtten的结果重新映射以便控制,重新映射的结果如下图的紫色曲线(链接内有每个参数的值)。
|
||||

|
||||
|
||||
# Matcap
|
||||
又到了万能的Matcap出场了,《七つの大罪 光と闇の交戦 : グラクロ》的技术分享中讲述了这种方式,作者在分享中讲到使用这种方式的原因:「根据(动画)演出中非现实的阴影设定,比起物理事实更重视感情的传达」
|
||||
|
||||
在大部分情况下卡通画面不需要保证光照的正确性,只要画面中的效果是感观舒适,就是可行的。所以在非动态光照的下,使用Matcap表现角色的光影结构切分是非常简单并且效果还不错的方式。
|
||||
|
||||

|
Reference in New Issue
Block a user