关于游戏中的描边
【简介】
描边(Outline)或者叫做勾线,简单来说就是给模型加边,在卡通渲染中,给人一种手绘的效果。
【示例】
1. 风格化的游戏表现。如崩坏3:
2. 用于显示被遮挡的物体。如守望先锋中角色、车被建筑遮挡时,显示高亮的外轮廓:
【方法】
【1. surface angle silhouetting 基于视角法线夹角】:
使用视角和法线之间的点积来计算轮廓边缘,如果这个值接近0,那么这个表面几乎在视线的边缘处,可以被认为是边缘。
float outline = 1 – max(0,dot(viewDirWS,normalWS));
fixed3 outlineCol = pow(outline, _OutlineWidth)*_OutlineColor.rgb;
优点:快,在一个pass中完成。
缺点:只适用于法线和轮廓边缘存在一定关联性的模型,对立方体这种就失效了(随着表面曲率的变化)。
【2. procedural geometry 基于过程几何方法】:
2.1 基本思想是通过两次绘制,第一次渲染角色,第二次渲染描边。
即先剔除背面的正面渲染 正常颜色;
后再剔除正面的背面渲染 描边颜色,在顶点着色器中将顶点沿着法线方向延伸。
ps.两次的渲染顺序颠倒也可以,不过后渲染描边的话,可以通过深度检测过滤掉很多像素,效率会更好。
//vert funciton:
float4 pos = UnityObjectToClipPos(v.vertex);
pos.xy += 0.01 * _OutlineWidth * ndcNormal.xy;
优点:简单,性能友好,可以逐顶点地控制描边(在顶点色刷描边的粗细)。
缺点:硬边相接的时候就失效了,比如立方体的边会裂开;对于有内凹的模型,背面面片会遮挡正面。
2.2 为了解决裂开这个问题,可以让模型沿着平均法线的方向延伸做描边。
我们将相同位置的顶点的法线做平均计算,存到模型的切线数据中。
2.4 除了沿着法线方向扩展,还有一种Z-bias方法,就是将顶点的z值做固定距离的偏移。但是这种方法不可控,效果也比较差。
【3. image processing 基于图像处理】:
3.1 原理:从一维图像说起
曲线是底部图像每个像素的强度F。
虚曲线是上面强度F 的一阶导数,实曲线是强度F 的二阶导数。
基于梯度的边缘检测方法所要找的就是虚曲线的峰值部分。 sobel,robert…
而另一个思想是观察实曲线的左边斜率为正,右边斜率为负,因此中间必然存在一个零点,这一点就是边缘的位置。laplacian
3.2 延伸到二维
图像是二维并且不连续,但是上面的方法依然适用。借助卷积的帮助,用离散的数据近似这些导数。
3.3 边缘检测算子:sobel,laplacian,robert,canny…
sobel滤波是检测一个方向上(一阶)的色彩或者亮度的梯度变化,所以需要在水平和垂直方向进行两次卷积。
分别获得水平和垂直的结果后将其相加G=sqrt(GxGx+GyGy),这样通过判断G是否大于一个阈值,我们就能判断这个像素是否属于边缘。
laplacian算子是一个二阶的微分算子,所以只需要一个卷积核就能检测到边缘,缺点是对噪声比较敏感。
3.4 应用在游戏中
传统图像处理做边缘检测时只有颜色数据,然而游戏可用的数据就很多了。
比如说depth,normal,各种mask图等
robert算子,基于camera depth texture,
/ 方向的depth梯度变化
结合两个方向计算边缘的强度
float edgeDepth = sqrt(pow(depthFiniteDifference0, 2) + pow(depthFiniteDifference1, 2)) * 100
添加阈值
edgeDepth = edgeDepth > _DepthThreshold ? 1 : 0;
_DepthThreshold = 0.4
阈值大的时候,会丢失一些描边细节
_DepthThreshold = 0.1
但阈值小的时候,像cube上方会因为差异比阈值大,出现整个面都填色的情况。
基于depth texture的描边效果 外轮廓比较友好 但是像自身两个面间的描边效果比较差
比如cube的棱 因为深度信息差距很小
3.6 基于normal
camera space normal
通过和depth一样的方法并添加阈值,生成基于normal的描边。
edgeNormal = edgeNormal > _NormalThreshold ? 1 : 0;
_NormalThreshold = 0.1
基于normal的描边 在自身面的变化中效果表现较好,解决了depth边缘检测中自身棱失效的情况
但是如果有物体重合 比如两个cube 放在一起 可能会因为两个cube的面朝向相同 导致描边丢失
3.7 结合depth和normal
float edge = max(edgeDepth, edgeNormal);
_DepthThreshold = 0.4
_NormalThreshold = 0.1
_DepthThreshold = 0.1
_NormalThreshold = 0.1
存在depth阈值小的时候的遗留问题。
3.8 添加视角修正
可以根据视角动态调整阈值
在surface normal和surface view dir的角度较大的地方 就是相机看过去“比较平”的地方 如cube的顶部面 这里阈值就要大一点;然后角度较小的地方,阈值可以小一点
float3 viewNormal = normal0 * 2 – 1;
float NdotV = 1 – dot(viewNormal, -i.viewSpaceDir);
// remap normal dot view (0,1) -> (depth normal threshold,1)
float normalThreshold01 = saturate((NdotV – _DepthNormalThreshold) / (1 – _DepthNormalThreshold));
// Scale the threshold, and add 1 so that it is in the range of 1…_NormalThresholdScale + 1.
float normalThreshold = normalThreshold01 * _DepthNormalThresholdScale + 1;
float depthThreshold = _DepthThreshold * depth0 * normalThreshold;
优点:基于屏幕开销固定,镂空的地方也可以描边。
缺点:robert算子等滤波模板会大量采样,计算量不小。
3.10 基于blur
用描边颜色绘制物体,写入RT1
基于这张RT1做blur,写入RT2
RT2-RT1,得到边缘
将边缘与原色blend
添加阈值模拟硬描边
【4. geometry edge detection 基于轮廓边缘检测】:
迄今为止所描述的大多数技术都有着需要两个pass来渲染轮廓的缺点。
对于过程几何方法来说,第二个pass中的背面渲染通常会检测出比实际边缘更多的像素。随着边缘变厚,会出现更多的问题,而且也无法控制渲染的风格。
图像方法也有着类似生成较粗线条的问题。
另一种方法是检测边缘轮廓并直接渲染它们。这种形式的轮廓边缘渲染允许我们更好的控制如何渲染线条。
轮廓边的两个相邻三角形一个面向观察者一个远离观察者,测试公式为:
其中n0,n1代表三角形的法线,v代表从眼睛指向物体的实现方向(到任一末端)。
【5. a hybrid of these 基于以上混合方法】:
通过标记不同的id进行不同的描边处理,最后混合。
【一般日系角色描边】
过程几何+引入了逐物体的顶点色来控制描边细节
具体来说,顶点色存储的信息包括:
R通道:控制toon shading的阈值
G通道:控制顶点根据视距膨胀的强度
B通道:控制描边的z-bias,越大则描边越不可见
A通道:控制描边的粗细
【参考】
1. My take on shaders: Edge detection image effect
2. 场景物体描边
3. The Sobel and Laplacian Edge Detectors
4. 轮廓&描边—基础
5. 描边Outline初步
6. Ni no Kuni2卡通描边还原实现
7. outline-shader
8. 从零开始的卡通渲染-描边篇
9. Unity Shader-描边效果
10. 使用CommandBuffer实现描边效果
【工程】
AboutOutline