232 lines
11 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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修改法线的方案
![](https://pic3.zhimg.com/80/v2-d936db9c0dca2c5b656e8a83601519d2_720w.webp)
首先如果只是修改模型顶点的法线的话模型顶点数就那么多比如上面的模型已经做得很细致了但顶点数也就5k传递球形法线做比较柔和的光照效果还比较合适想表现复杂的结构是比较困难的。
当然也有修改模型拓扑等方法,但我觉得这些方法都不是特别优雅。
我个人觉得使用法线贴图会更有潜力一些自己私底下也有在研究把SDF烘培成法线贴图的方法目前的结果我自己并不是很满意如果后续有一些有意思的结果我会再出一篇文章跟大家分享。
![](https://pic2.zhimg.com/80/v2-d6d552383739dbf43c57ff074856d8d1_720w.webp)
通过SDF生成的法线贴图
![动图封面](https://pic4.zhimg.com/v2-9c6f061b2ef97919653327e2885fd93f_b.jpg)
使用SDF法线贴图shading的效果
> 为了方便烘培成世界空间的法线,烘培到切线空间会更合适
### 1.2 使用SDF的方案
SDF的方案是目前市面上使用得最多的了新出的二游基本都会用这种方式。
在实现上SDF的方案并不困难但是由于我自己使用unreal引擎比较多自己想通过改管线的方法来实现的话很难改得很优雅所以我以后还会继续研究法线贴图方案。
在表现上目前SDF的方案最明显的问题是没有Z轴上的变化这也是这篇文章主要想解决的问题。本文的实现思路其实很简单目前的SDF都只画了一组水平轴的光照那么我们把其他所有轴都画出来就行了。
![](https://pic4.zhimg.com/80/v2-b54d51aa0fd056e7f7ae1d1ac52930c7_720w.webp)
使用全角度的SDF图集代替水平轴的SDF
两三年前看到原神的SDF方案时就想到了这个方法相信平时比较关注卡通渲染的各位都想到过这个方法但是几年过去了没有看到任何一篇文章实现了这个方案看来大伙也都觉得把所有方向的光照都画出来工作量太大了不敢尝试所以只能我自己尝试实现给大伙看了。
## 二、实现流程
### 2.1 流程概况
三句话简单概括一下:
1.画好各个角度的光照如下我一共画了65张稍微偷懒点少画写应该也没问题。
![](https://pic1.zhimg.com/80/v2-54cfc4f5653757926938a5610988bc14_720w.webp)
在blender中绘制光照图
2.把画完的光照图转成SDF然后拼成一张图集
![](https://pic3.zhimg.com/80/v2-37af28982eff159cb0453c4044783eae_720w.webp)
生成的SDF和SDF图集
3.通过光源角度<E8A792><EFBFBD>计算合适的uv采样4次进行插值。
![](https://pic4.zhimg.com/v2-61cfc45befde6a08947b5c1a7220ddf7_r.jpg)
采样四次SDF图集计算光照结果
### 2.2 光照图绘制
绘制光照没有目前没想到什么很好的生成方式纯靠手绘我没啥美术能力画这65张图花了我一整个星期的空闲时间如果有美术大佬愿意画的这东西的话可能效果会更好一些。这一堆的模型有点制作表情blend shape的感觉了
光照图我画了9行9列第一行和最后一行只有一张所有一共9x7+2=65张
光照图的每一行代表平行光从下面的半球从上到下的9种不同的经度投射到脸上每一列代表9不同的纬度第一行和最后一行平行光从正上和正下投射过来所以只有一张。
![](https://pic2.zhimg.com/80/v2-2644b18113de971f03204050cdd2db69_720w.webp)
绘制的光照图相当于光照从左半球上各点照射过来的结果
![](https://pic1.zhimg.com/80/v2-ff89d7d1c8951d5766b4a4451c419818_720w.webp)
有一点需要注意的可以看到我绘制光照图的时候涂出了uv的边界这样生成的SDF在uv边界效果不会出问题不想让SDF超出uv边界的话等生成了SDF之后再把超出边界的部分去掉就行了
![](https://pic4.zhimg.com/80/v2-99b39fc12e397f46d180b44f1f4d006b_720w.webp)
绘制光照图时建议涂出uv边界
下面这个是反面例子:
![动图封面](https://pic1.zhimg.com/v2-32595804b0860eb38b9c1f0feac33100_b.jpg)
脸部上方uv边界处SDF插值效果不好
下面放点动画里的光影变化作为参考:
![](https://pic3.zhimg.com/80/v2-c92d191c8c8a197f608c47c4dbd00b72_720w.webp)
奇蛋物语
![](https://pic3.zhimg.com/80/v2-92f10c1604c942dfbe0fc52a2d3808a6_720w.webp)
奇蛋物语、偶像大师、Mygo
![](https://pic1.zhimg.com/80/v2-caa00e5a2cb76b2a90e0c28ad7439f38_720w.webp)
轻音少女
### 2.3 SDF图集制作
![](https://pic1.zhimg.com/80/v2-2bb15862e70a9dec8178c7a14c909030_720w.webp)
使用8ssedt算法生成SDF
首先我们需要把黑白的光照图转为SDF我使用的是8ssedt算法生成的关于8ssedt算法的详细可以看下面的这两篇文章
https://zhuanlan.zhihu.com/p/337944099
https://blog.csdn.net/qq_41835314/article/details/128548073
接下来把所有的光照图拼成一张图集:
![](https://pic3.zhimg.com/80/v2-e484cd7511644a1a412c7afa7a0e33ce_720w.webp)
SDF图集
这里解释一下我为什么不生成这种SDF这种SDF在下文都使用“插值后的SDF”特指
![](https://pic2.zhimg.com/80/v2-3236e665764311dfd1744482f4961455_720w.webp)
“插值后的SDF”
使用这种插值后的SDF其实是有一个条件的后面的光照一定要覆盖前面的光照。
比如下面这套SDF图中光照图的范围从左到右一定是逐渐增大的
![](https://pic4.zhimg.com/80/v2-d36721263248ca40e181bde267020ce7_720w.webp)
https://zhuanlan.zhihu.com/p/411188212
这样导致的结果就是当光从前方照过来的时候,如下图左边整个脸必须是完全亮的,不可能出现右边这种脸的后方没被照亮的效果。
![](https://pic1.zhimg.com/80/v2-7d4ee004d2744fee72753611a0e3f888_720w.webp)
“使用插值后的SDF的缺点”
再举一个例子下面3张图之间没有包含关系所有转成SDF再插值之后的效果是右边这样是不是看起来就不对劲。
![](https://pic4.zhimg.com/80/v2-c57042f04721311f5131a31f2c1c8f03_720w.webp)
使用没有包含关系的图像生成“插值后的SDF”
看看效果,确实不对劲:
![动图封面](https://pic1.zhimg.com/v2-c5f040339d6c510f8ed3d6d8d752d308_b.jpg)
“插值后的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和拼图集的操作我写了一个小工具你们直接用我的工具来做就行了不用再浪费时间去重复造轮子了。
![](https://pic4.zhimg.com/80/v2-14be2af3bbfaf948ea57acfb447b8beb_720w.webp)
生成SDF和图集的小工具
工具我和工程放在一起解压SDFTool.zip然后双击main.exe就能打开
![](https://pic3.zhimg.com/80/v2-b6b19fafee9fe566bf660bcde9501e0a_720w.webp)
美术的同学直接使用打包好的工具就行了
工具具体的使用文档可以在我的github上查看源码也在上面工具写得不是很鲁棒如果出了什么问题的话有能力的朋友直接改源码吧。
![](https://pic4.zhimg.com/80/v2-ee795bab4a98c57329746d53446a6e0b_720w.webp)
### 2.5 计算光源角度采样SDF图集
下面很多操作都跟正常的SDF脸部光照一样我就不写得太详细了具体直接打开我的工程看就行了。
1.在蓝图里把角色脸部向前的向量和先左的向量传到角色的材质里
![](https://pic1.zhimg.com/v2-3dc9c43350f9f137a548f7749f8f58f0_r.jpg)
就是把下图下面这两个箭头的方向传到材质里:
![](https://pic4.zhimg.com/80/v2-ac49744b912008682c82bdb7773ae27f_720w.webp)
- 叉乘朝左和朝前的向量,得到朝上的向量
- 将光源方向与脸部朝左向量点乘然后step一下用于后面判断光源是从脸部左边还是右边照过来的
- 将光源方向与脸部朝前、朝上的向量点乘,得到光源的水平夹角的$cos\theta$和垂直夹角的$cos\phi$
![](https://pic4.zhimg.com/v2-a01e1bdf17045438ed416a4fc3ab03ab_r.jpg)
- $cos\theta$和$cos\phi$它们是余弦值不是线性的所以我喜欢使用arccos把它们变成线性的角度
![](https://pic4.zhimg.com/v2-471260088c59a0af2d31b477d1fc0adb_r.jpg)
- 接下来采样的部分看起来比较杂乱
![](https://pic1.zhimg.com/v2-056d729540745190a5e508a6fef58f1c_r.jpg)
我们稍微拆开来看:
- 这里是定义了两个常数应该图集是9x9的所有定义了Row=1Row-1的意思是Row-1=8
![](https://pic1.zhimg.com/80/v2-a9ae4efbaea60467e116196313afde74_720w.webp)
- 当光源在右边时镜像翻转uv
- 然后uv除以行数9就得到了贴图左上角的第一张SDF的uv
![](https://pic4.zhimg.com/v2-015fd1bbeb9f75e78cb8e52ff1c5c6a7_r.jpg)
- 接下来要计算离当前光源角度最近的4张SDF的uv比如当水平角度<E8A792>等于100度垂直角度<E8A792>等于120度时会采样下面画的四个点的SDF然后按照权重来插值跟双线性插值很像。
![](https://pic3.zhimg.com/80/v2-5c692fe323ea9c974ca6bff0a8ae025a_720w.webp)
采样当前光源角度最近的四张SDF
- 这里计算离光源最近的行数和列数
![](https://pic3.zhimg.com/80/v2-c93bf64082d6d6ded87726824aaa809e_720w.webp)
- 接下来计算出uv采样贴图并插值出光照结果最后上色
![](https://pic4.zhimg.com/v2-76e883b08f60d8a92f8b8de0bb04ad0b_r.jpg)
![](https://pic1.zhimg.com/80/v2-3de2833216543354784df04eb139cc18_720w.webp)