--- 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=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 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的结果重新映射以便控制,重新映射的结果如下图的紫色曲线(链接内有每个参数的值)。 ![](https://pic1.zhimg.com/80/v2-e09ca89f907402120b95de8b6b844fbc_720w.webp) # Matcap 又到了万能的Matcap出场了,《七つの大罪 光と闇の交戦 : グラクロ》的技术分享中讲述了这种方式,作者在分享中讲到使用这种方式的原因:「根据(动画)演出中非现实的阴影设定,比起物理事实更重视感情的传达」 在大部分情况下卡通画面不需要保证光照的正确性,只要画面中的效果是感观舒适,就是可行的。所以在非动态光照的下,使用Matcap表现角色的光影结构切分是非常简单并且效果还不错的方式。 ![](https://pic4.zhimg.com/80/v2-8761284b26369d168ef9ad41e26f287f_720w.webp)