Reference
卡通渲染及其相关技术 - 知乎 (zhihu.com)
Unity NPR之日式卡通渲染(基础篇) - 知乎 (zhihu.com)
【01】从零开始的卡通渲染-描边篇 - 知乎 (zhihu.com)
NPR描边学习笔记 - 知乎 (zhihu.com)
Unity Outline Shader Tutorial - Roystan — Unity 轮廓着色器教程 - Roystan
上面链接讲的非常好我就不瞎逼逼了
描边
方法一:法线外扩
如何实现?
使用两个渲染通道一个通道正常渲染,一个通道渲染描边。
- 描边通道中,在顶点着色器中将顶点沿着法线方向外扩。
1
| o.vertex = UnityObjectToClipPos(v.vertex + v.normal * _EdgeWidth);
|
- 再剔除正面。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| Shader "Unlit/NewUnlitShader" { Properties { _MainColor ("颜色", Color) = (1,1,1,1) _EdgeColor ("边缘颜色", Color) = (1,1,1,1) _EdgeWidth ("边缘宽度", Float) = 0 } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fog #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; float4 _MainColor; v2f vert(appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); return o; } fixed4 frag(v2f i) : SV_Target { fixed4 col = _MainColor; return col; } ENDCG } Pass { Cull front CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float4 normal : NORMAL; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; float4 _EdgeColor; float _EdgeWidth; v2f vert(appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex + v.normal * _EdgeWidth); return o; } fixed4 frag(v2f i) : SV_Target { return _EdgeColor; } ENDCG } }}
|
存在的问题
描边大小随摄像机的远近变化
顶点转化到屏幕空间再外扩。
1 2 3 4 5 6 7 8
| v2f o; float4 pos = UnityObjectToClipPos(v.vertex);
float3 viewNormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal.xyz); float3 ndcNormal = normalize(TransformViewToProjection(viewNormal.xyz)) * pos.w; pos.xy += 0.01 * _EdgeWidth * ndcNormal.xy; o.pos = pos; return o;
|
描边大小随窗口宽度改变
因为我们再屏幕空间中进行法线外扩,视口矩阵中就有窗口的大小,使得边的粗细根据窗口大小变化。
我们在这里除掉宽高比产生的影响。
1 2 3
| float4 nearUpperRight = mul(unity_CameraInvProjection, float4(1, 1, UNITY_NEAR_CLIP_VALUE, _ProjectionParams.y)); float aspect = abs(nearUpperRight.y / nearUpperRight.x); ndcNormal.x *= aspect;
|
最后:
但是无法渲染拐角尖锐的物体
可以看到描边都断开了。
方法一改进
我们可以重新计算法线得到一个平均的法线,再写入切线空间中,为什么写入切线空间,其实在学习TinyRender的时候已经知道了。为了随着模型改变,法线也会跟着改变。
下面代码来自: 【01】从零开始的卡通渲染-描边篇 - 知乎 (zhihu.com)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| public class PlugTangentTools { [MenuItem("Tools/模型平均法线写入切线数据")] public static void WirteAverageNormalToTangentToos() { MeshFilter[] meshFilters = Selection.activeGameObject.GetComponentsInChildren<MeshFilter>(); foreach (var meshFilter in meshFilters) { Mesh mesh = meshFilter.sharedMesh; WirteAverageNormalToTangent(mesh); }
SkinnedMeshRenderer[] skinMeshRenders = Selection.activeGameObject.GetComponentsInChildren<SkinnedMeshRenderer>(); foreach (var skinMeshRender in skinMeshRenders) { Mesh mesh = skinMeshRender.sharedMesh; WirteAverageNormalToTangent(mesh); } }
private static void WirteAverageNormalToTangent(Mesh mesh) { var averageNormalHash = new Dictionary<Vector3, Vector3>(); for (var j = 0; j < mesh.vertexCount; j++) { if (!averageNormalHash.ContainsKey(mesh.vertices[j])) { averageNormalHash.Add(mesh.vertices[j], mesh.normals[j]); } else { averageNormalHash[mesh.vertices[j]] = (averageNormalHash[mesh.vertices[j]] + mesh.normals[j]).normalized; } }
var averageNormals = new Vector3[mesh.vertexCount]; for (var j = 0; j < mesh.vertexCount; j++) { averageNormals[j] = averageNormalHash[mesh.vertices[j]]; }
var tangents = new Vector4[mesh.vertexCount]; for (var j = 0; j < mesh.vertexCount; j++) { tangents[j] = new Vector4(averageNormals[j].x, averageNormals[j].y, averageNormals[j].z, 0); } mesh.tangents = tangents; } }
|
这里我就没有实践了。
还有就是在Shader中读取切线空间的法线向量float3 viewNormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.tangent.xyz);
效果还可以。
方法二:基于图像处理的边缘检测法(Edge Detection by Image Processing)
基于图像处理的边缘检测法一般由屏幕后处理来实现。通常应用于延迟渲染的项目中,因为该方法需要G-Buffer中的深度信息、法线信息和物体ID信息等。通过获取到屏幕空间下的深度信息、法线信息和物体ID信息后,利用边缘检测算子来探测出相邻两个像素点之间的深度值、法线值和物体ID值差异较大的情况,从而找到并绘制出描边。
常见的边缘检测算子有:
3种常见的边缘检测算子
以下图为例,左上角绘制的是一张法线图,上面中间的则是深度图,左下和中下分别是经过Sobel边缘检测算子计算后的结果。当然,因为计算结果不是布尔值,我们还需要一个阈值参数来控制。右上角是将检测出的法线图和深度图叠加并扩张的效果。右下则是合成后的最终效果。
使用基于图像处理的边缘检测的方法绘制描边的各个阶段
这个方法也没有实践。
留下的疑惑
这里怎么得到宽高比的不清楚
1
| float4 nearUpperRight = mul(unity_CameraInvProjection, float4(1, 1, UNITY_NEAR_CLIP_VALUE, _ProjectionParams.y));
|
着色LUT