前言
久违地发一下技术文章,社畜之后一些工作内容无法公开分享,不过一些业内已公开的方案还是可以聊聊的。
所谓“二次元脸部光照阈值图”便是类似如下的图,在业内常被称为“SDF图”:
然而其实它在应用时与SDF毫无关联,仅是生成时用到了SDF罢了,因此笔者更想称其为“阈值图”。
总而言之,这篇文章将描述如何生成这么一张图,在Unity中编写工具时遇到了什么坑。
描述各个光照角度
为了制作阈值图,我们需要让美术去绘制出例如下图中一般,离散的各个角度的光照结果图。
素材出自https://zhuanlan.zhihu.com/p/356185096
其实我们要做的,就是让这些离散的图合成一张,使其变得连续。
至于这些图怎么出,如果不是直接PS画,也可以在SP里提供特别的shader让美术绘制。
使用SDF让它们变得连续
所谓SDF,便是“有向距离场(Signed Distance Field)”,能够描述某一点到最近的边缘的距离,同时使用正与负来区分该点是在边缘内还是外。
网上生成SDF的代码不少,我直接借用了这位老哥的,使用Compute Shader生成SDF,速度还算蛮快:
https://zhuanlan.zhihu.com/p/390255113
1 | // Each #kernel tells which function to compile; you can have many kernels |
原理而言就是遍历一个像素周围一圈范围的像素,找出其中最近的“边缘”,计算距离值。
而这个范围便是SDF的最大距离,试验下来,2048的图片需要约350~400的范围值。
而1024则减半,我通常给个256。
原图
SDF图
而下一步才是生成阈值图的关键,我们需要两两SDF图之间生成一张平滑的小范围“阈值图”。
SDF图1
SDF图2
阈值图
其实说来也很简单,笔者用自身多年的绘画功底画一张示意图:
红色区域表示SDF图1,蓝色区域则表示SDF图2
假设我们需要求P点的值,而我们知道从红色的边缘到蓝色的边缘,P点的值将从1递减到0。
那么我们其实只需要P到两个边缘的距离的比值即可,而两个距离我们可在SDF图中取得。
此时要注意,之前的SDF图中,正负距离被映射到了[0, 1]范围内,需要还原一下。
代码也比较简单:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19Texture2D originPart;
Texture2D nextPart;
Texture2D originSDF;
Texture2D nextSDF;
[numthreads(8,8,1)]
void CSSmooth (uint3 id : SV_DispatchThreadID)
{
int2 coord = id.xy;
float distanceOriginPart = originSDF[coord].a * 2 - 1;
float distanceNextPart = nextSDF[coord].a * 2 - 1;
float totalDistance = abs(distanceNextPart) + abs(distanceOriginPart);
float lerpValue = abs(distanceNextPart) / max(totalDistance, 0.001);
outputTexture[coord] = lerp(originPart[coord], nextPart[coord], lerpValue);
outputTexture[coord] = pow(outputTexture[coord], 0.45);//gamma correct
}
将各个小阈值图进行合并
之后便是将几个阈值图进行相加,并除以图片的总量即可。
此时笔者第一次在Compute Shader中使用Texture Array,遇到了一点小坑。
创建TextureArray的代码极为简单,但绝不能画蛇添足加个Apply(),之前TextureArray硬是内容全空,查了数个小时才发现罪魁祸首竟是看上去人畜无害的textureArray.Apply()
……1
2
3
4
5
6
7
8var textureArray = new Texture2DArray(textureWidth, textureWidth, textureCount, TextureFormat.ARGB32, false);
for (int i = 0; i < textureCount - 1; i++)
{
Graphics.CopyTexture(smoothParts[i], 0, textureArray, i);
}
computeShader.SetTexture(kernel, "_Textures", textureArray);
而设置给Compute Shader也直接用SetTexture即可。
Compute Shader中:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16int _TextureCount;
Texture2DArray<float4> _Textures;
[numthreads(8,8,1)]
void CSCombine (uint3 id : SV_DispatchThreadID)
{
int2 coord = id.xy;
outputTexture[coord] = 0;
float sum = 0;
for(uint i = 0; i < _TextureCount; i++)
{
sum += _Textures[int3(id.x, id.y, i)];
}
outputTexture[coord] = sum / _TextureCount;
}
最初笔者在for循环中写的是outputTexture[coord] += _Textures[int3(id.x, id.y, i)];
然后再在循环外边/= _TextureCount;
发现怎么都不对,才意识到texture的RGBA32格式将会自动将值Clamp到[0, 1],于是才改成如上写法。
对于Texture的写入操作,还是先用其他float或half值替代,最后赋值为好。说到底本来应该也就不太推荐频繁地读写Texture,该说是吃一堑长一智吧。
于是导出后便可获得如下的平滑阈值图:
生成一张2k图,调整sdf范围为350后在笔者的1060老爷机上需要15秒左右,而1k的图范围设置为256只需要5秒左右,还算是能接受的速度。
注:Compute Shader中读取Unity设置的SRGB图,似乎并不是以pow 2.2来进行伽马校正,感觉是个小坑,还是尽量将涉及这种计算的图片都设置为Linear空间为好
感谢您阅读完本文,若您认为本文对您有所帮助,可以将其分享给其他人;若您发现文章对您的利益产生侵害,请联系作者进行删除。