Unity-卡通渲染
效果展示:
原模型:
简单分析
卡通渲染又叫非真实渲染(None-Physical Rendering-NPR),一般日漫里的卡通风格有几个特点:
人物有描边
有明显的阴影分界线,没有太平滑的过渡
以下就根据这两点来实现卡渲效果;
描边
法线外扩
实现描边方式多种,比如卷积区分边界;
这里使用更简单的两个Pass,一个只用纯色画背面,利用法线外扩顶点,根据深度的不同这个纯色的背面会被显示出来,同时又不会遮挡正面;
1 | Pass |
细节处理(坑)
细节处理前后对比:
摄像机远近边缘线粗细不同
由于世界坐标系下做外扩,摄像机里物体远近会影响法线外扩的多少;
解决方案,在NDC坐标系下法线外扩;
1
2
3
4
5
6
7
8
9
10
11
12//顶点着色器替换以下代码
float4 pos = UnityObjectToClipPos(v.vertex);
//摄像机空间法线
float3 viewNormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal.xyz);
//将法线变换到NDC空间,投影空间*W分量
float3 ndcNormal = normalize(TransformViewToProjection(viewNormal.xyz)) * pos.w;
//xy两方向外扩
pos.xy += 0.01 * _OutlineWidth * ndcNormal.xy * v.vertColor.a;
o.pos = pos;上下和左右边缘线粗细不同
NDC空间是正方形,而视口宽高比是长方体,导致描边上下和左右的粗细不统一;
解放方案,根据屏幕宽高比缩放法线再外扩;
1
2
3
4
5
6
7//将近裁剪面右上角位置的顶点变换到观察空间
//unity_CameraInvProjection摄像机矩阵逆矩阵,UNITY_NEAR_CLIP_VALUE近截面值,DX:0,OpenGL-1.0;_ProjectionParams.y摄像机近截面
float4 nearUpperRight = mul(unity_CameraInvProjection, float4(1, 1, UNITY_NEAR_CLIP_VALUE, _ProjectionParams.y));
//求得屏幕宽高比
float aspect = abs(nearUpperRight.y / nearUpperRight.x);
ndcNormal.x *= aspect;顶点重合法线不连续
模型顶点重合时会出现多条法线,在不同的面上法线不同导致描边不连续;
解决方案,修改模型顶点数据,同顶点多条法线求平均值;
需要和美工协商修改模型数据,这里写了脚本临时修改模型数据;
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
51public class PlugTangentTools
{
[ ]
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);
}
Debug.Log("重合顶点平均法线写入成功");
}
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;
}
}
ps:利用模型顶点的四个通道RGBA——对描边粗细显影相机距离缩放进行精细控制,需要美工配合;
着色
减少色阶
二分法
将有阴影和没阴影的地方做明显的区分;
1 | half4 frag(v2f i) : SV_TARGET |
Ramp贴图
使用明显分界的色阶图来取样,使阴影有明显的分界线;
逻辑和二分一样,只是多加个几个色阶;
1 | //_ShadowRange范围取样Ramp贴图 |
高光色阶
卡渲高光和阴影一样,和周围色块有明显的分界线;
1 | half3 specular = 0; |
ilmTexture贴图
《GUILTY GEAR Xrd》中使用的方法,又叫Threshold贴图;
贴图的R通道控制漫反射的阴影阈值,G通道控制高光强度,B通道控制高光范围;
需要和美工配合,没贴图就不测了;
总之万物皆可用贴图来传递信息,rgba代表什么意思可以自行做各种trick;
1 | half4 frag (v2f i) : SV_Target |
【翻译】西川善司「实验做出的游戏图形」「GUILTY GEAR Xrd -SIGN-」中实现的「纯卡通动画的实时3D图形」的秘密,前篇(1)
【翻译】西川善司「实验做出的游戏图形」「GUILTY GEAR Xrd -SIGN-」中实现的「纯卡通动画的实时3D图形」的秘密,前篇(2)
【翻译】西川善司的「实验做出的游戏图形」「GUILTY GEAR Xrd -SIGN-」中实现的「纯卡通动画的实时3D图形」的秘密,后篇
边缘泛光
三渲二加点边缘泛光会增加立体感,让画质更真实;效果如下;
__RimMin、_RimMax控制边缘泛光范围;
smoothstep使过渡平缓;再乘以RimColor,alpha控制强度;
1 | half f = 1.0 - saturate(dot(viewDir, worldNormal)); |
mask遮罩图
用一张贴图来修正边缘泛光的效果;
边缘光的计算使用的是法线点乘视线。在物体的法线和视线垂直的时候,边缘光会很强。在球体上不会有问题,但是在一些有平面的物体,当平面和视线接近垂直的时候,会导致整个平面都有边缘光。这会让一些不该有边缘光的地方出现边缘光。
屏幕后效
post-processing官方组件中有bloom效果;
原理:提取图像中较亮区域,存储在纹理中,使用高斯模糊模拟光线扩散效果,将该纹理和原图像混合;过程比较复杂,后面写屏幕后期效果再分析吧;
完整Shader:
1 | Shader "Unlit/CelRenderFull" |