什么是Matcap?Matcap实际上是Material Capture的缩写,即材质捕捉。实际上,这是一种离线渲染方案。类似光照烘焙,将光照或者其它更复杂环境下的渲染数据存储到一张2D贴图上, 再从这张2D贴图进行采样进行实时渲染。
Matcap是一种在观察空间下使用单位法线采样单位球的离线渲染算法。
- 为什么是观察空间?因为观察空间下,相机变化就可以看到不同的渲染结果。
- 为什么使用法线去采样?法线是描述表面朝向的向量,与渲染结果强相关,法线跟物体的曲率强相关等,因此这种算法经常用于雕刻上。
Matcap的特点总结如下:
- 使用观察空间下的法线向量采样2D贴图,作为光照和反射结果。
- 在缺乏光照烘焙的环境下,可以一定程度上替代或者模拟光图。
- 但是,Matcap代表的2D贴图不局限于光照信息,也可以理解为某种环境下的最终渲染结果。
- 由于是离线方案,因此计算非常廉价,很适合低端机器或者特定场合下使用。
个人理解:一般情况下,一个模型的法线向量描述模型表面的朝向关系,在世界空间下,是不变的,但是我们把它转换到观察空间下之后,这个模型的表面信息会随着摄像机变化,我们再在世界空间下观察模型,会发现它的法线信息固定在摄像机观察面上,我们使用这个信息来作为uv,去采样一张贴图,会让这个贴图贴着物体表面,不仅会跟着物体表面发生变形,而且还一直冲着摄像机,但是需要注意,合格的贴图理想情况下必须是一张圆形贴图,并且四周没有缝隙

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的对象移动到摄像机边缘时,侧面的贴图会出现投射穿帮。
NDotV
前面两节,我们的边缘光的核心原理就是NDotV(世界法线和摄像机向量),但是使用NDotV除了可以实现边缘光,还可以像上面一样,让NDotV的结果来影响uv,从而采样一张Ramp图投射到模型上。

在ASE中,NDotV算出来的是一维结果,我们使用Append转化为二维,y值使用任意值都不影响uv采样,这是因为我们的渐变贴图是这样的:
可以见到从左到右的渐变,y值取任意值颜色都相等,影响不了uv最后的结果,uv结果主要由x值决定,x值由NDotV的结果决定。需要注意的是,这中Ramp贴图需要在导入设置中将Wrap Mode改为Clamp
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 } } }
|