什么是Matcap?Matcap实际上是Material Capture的缩写,即材质捕捉。实际上,这是一种离线渲染方案。类似光照烘焙,将光照或者其它更复杂环境下的渲染数据存储到一张2D贴图上, 再从这张2D贴图进行采样进行实时渲染。

Matcap是一种在观察空间下使用单位法线采样单位球的离线渲染算法。

  • 为什么是观察空间?因为观察空间下,相机变化就可以看到不同的渲染结果。
  • 为什么使用法线去采样?法线是描述表面朝向的向量,与渲染结果强相关,法线跟物体的曲率强相关等,因此这种算法经常用于雕刻上。

Matcap的特点总结如下:

  • 使用观察空间下的法线向量采样2D贴图,作为光照和反射结果。
  • 在缺乏光照烘焙的环境下,可以一定程度上替代或者模拟光图。
  • 但是,Matcap代表的2D贴图不局限于光照信息,也可以理解为某种环境下的最终渲染结果。
  • 由于是离线方案,因此计算非常廉价,很适合低端机器或者特定场合下使用。

个人理解:一般情况下,一个模型的法线向量描述模型表面的朝向关系,在世界空间下,是不变的,但是我们把它转换到观察空间下之后,这个模型的表面信息会随着摄像机变化,我们再在世界空间下观察模型,会发现它的法线信息固定在摄像机观察面上,我们使用这个信息来作为uv,去采样一张贴图,会让这个贴图贴着物体表面,不仅会跟着物体表面发生变形,而且还一直冲着摄像机,但是需要注意,合格的贴图理想情况下必须是一张圆形贴图,并且四周没有缝隙

Matcap

Matcap库

https://github.com/nidorx/matcaps

如何取样

把法线转换到观察空间,然后再将法线偏移到[0,1]的范围内,然后取xy分量作为uv,对matcap纹理进行采样。法线是方向向量,每个维度的取值范围是[-1,1],所以需要偏移到[0,1]的范围内,符合uv规范,法线转换到观察空间后z分量始终垂直于摄像机观察方向,所以取xy分量。

ASE实现MatCap

在ASE中,使用World Normal节点获取当前模型的世界空间下的法线,再使用View Matrix节点获取到从世界空间到观察空间的矩阵,相乘后就会得到观察空间下的法线,再使用Swizzle节点取得xy二维,参考上面的进行数据偏移得到的uv再进行采样即可。

MatCap

缺点

应用MatCap的对象移动到摄像机边缘时,侧面的贴图会出现投射穿帮。

NDotV

前面两节,我们的边缘光的核心原理就是NDotV(世界法线和摄像机向量),但是使用NDotV除了可以实现边缘光,还可以像上面一样,让NDotV的结果来影响uv,从而采样一张Ramp图投射到模型上。

NDotV

在ASE中,NDotV算出来的是一维结果,我们使用Append转化为二维,y值使用任意值都不影响uv采样,这是因为我们的渐变贴图是这样的:ramp

可以见到从左到右的渐变,y值取任意值颜色都相等,影响不了uv最后的结果,uv结果主要由x值决定,x值由NDotV的结果决定。需要注意的是,这中Ramp贴图需要在导入设置中将Wrap Mode改为Clamp

ASE采样Normal贴图

ASE采样Normal

薄膜干涉天牛最终代码

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
Shader "KerryTA/ASE/04MatCapShaderCode"
{
Properties
{
_Diffuse ("Diffuse", 2D) = "white" {}
_MatCapTex("MatCapTex", 2D) = "black" {}
_MatCapIntensity("MatCapIntensity", Float) = 5.0
_MatCapAddTex("MatCapAddTex", 2D) = "white" {}
_MatCapAddIntensity("MatCapAddIntensity", Float) = 0.5
_RampTex("RampTex", 2D) = "black" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"

struct appdata
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float3 normal : NORMAL;
};

struct v2f
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
float3 pos_world : TEXCOORD1;
float3 normal_world : TEXCOORD2;
};

sampler2D _Diffuse;
float4 _Diffuse_ST;
sampler2D _MatCapTex;
float _MatCapIntensity;
sampler2D _MatCapAddTex;
float _MatCapAddIntensity;
sampler2D _RampTex;

v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _Diffuse);
float3 pos_world = mul(unity_ObjectToWorld,v.vertex).xyz;
o.pos_world = pos_world;
o.normal_world = normalize(mul(float4(v.normal,0.0), unity_WorldToObject).xyz);
return o;
}

fixed4 frag (v2f i) : SV_Target
{
half3 normal_world = normalize(i.normal_world);
half3 view_world = normalize(_WorldSpaceCameraPos - i.pos_world);
half NdotV = saturate(dot(normal_world, view_world));
half2 NdotV_uv = half2(1-NdotV,0.5);

half2 matcap_uv = mul(UNITY_MATRIX_V, half4(normal_world,0.0)).xy;
matcap_uv = matcap_uv * 0.5 + 0.5;

half4 matcap_color = tex2D(_MatCapTex, matcap_uv) * _MatCapIntensity;
half4 matcapadd_color = tex2D(_MatCapAddTex, matcap_uv) * _MatCapAddIntensity;

half4 diffuse_color = tex2D(_Diffuse, i.uv);
half4 ramp_color = tex2D(_RampTex, NdotV_uv);

half4 final_color = diffuse_color * matcap_color * ramp_color + matcapadd_color;

return final_color;
}
ENDCG
}
}
}