Unity Shader-Matcap(材质捕获)

版权声明:欢迎转载,共同进步。请注明出处:http://blog.csdn.net/puppet_master https://blog.csdn.net/puppet_master/article/details/83582477

前言

最近通关了《Alan Wake》(心灵杀手),整体感觉很不错,游戏虽然是2010年发行的,但是画面至今看来也还是不错的,尤其是游戏内的体积光效果,贯穿了整个游戏,因为光本身就是这个游戏中最强力的武器。

主人公是一位小说家,但是当你发现你所写的小说都变成了现实的时候,这肯定是一件细思极恐的事情。故事情节一波三折,颇有些美剧的感觉。

每次看到电锯,不由得想起被《生化危机》和《恶灵附身》支配的恐惧,好在这款游戏的电锯不太抗揍。其实《恶灵附身》跟这款游戏有不少相似的地方,都是在“意念”的世界中,所以游戏中经常出现走着走着天上掉下来辆火车啦,走着走着桥飞啦之类的情况。

游戏还是很给力的,我就不多剧透了,下面是本文的正题。之前的blog里面,大多是一些特殊效果以及屏幕后处理相关的内容,但是关于模型本身光照效果的比较少。今天刚好来玩一个简单,但是却很有意思的东东,叫做Matcap。

简介

Matcap,全称为Material Capture,翻译过来就是材质捕获。简单来说就是预先生成的一种存储了光照和反射等信息的贴图,运行时使用法线方向进行采样。Matcap的好处就是可以用很低的消耗来实现很多特殊风格的效果,但是Matcap也有一些缺陷,在于Matcap仅对于固定相机视角的情况较好,这也是Matcap的原理决定的。

Matcap主要是在Zbrush,Mudbox这些软件里面使用的,ZBrush里面雕模的时候有时候虽然没有贴图,但是效果看起来也挺好的,实际上就是Matcap的作用,而且这些美术大佬们不断在扩展这些matcap,积累了很多好玩的matcap效果,而使用这些效果却非常容易,只需要换一下贴图。

Matcap的原理

先来看一下Matcap的原理。先可以考虑一下CubeMap的原理,CubeMap是一个六面体,正常的反射采样时使用的反射对应方向上投影最近的面上进行采样,更简单一点的(应该算是一个Trick了),甚至可以直接使用法线方向进行采样,对于天空盒类型的反射也可以得到不错的效果,即不同法线所指向的方向有不同的效果。而Matcap将这一点发挥得更加淋漓尽致,因为Matcap只用一张图就可以。所以我们需要考虑怎样将这个法线转换到一个合适的区间来采样Matcap。

由于物体的法线是一个三维的朝向,但是我们最终采样到二维贴图也只有两个维度。我们需要去掉一个维度,在相机空间下的物体法线向量,我们可以不考虑Z轴的指向,即不考虑Z轴本身对屏幕空间的XY平面的贡献,因为法线是一个单位向量,如果法线在XY方向上的投影权重很大,那么说明在Z方向的权重就很小,对应到二维的Matcap上采样的位置越靠近边缘;而如果法线在XY方向的投影权重很小,那么Z方向的权重就很大,对应到二维的Matcap上的采样位置就越靠近中心。

接下来我们需要考虑的是在哪一个空间计算Matcap,如果是物体空间或者是世界空间,由于相机位置朝向不确定,不好确定法线的Z方向与屏幕空间的关系,我们不好确定舍弃哪一个维度。而相机空间下的物体在屏幕上的方位都已经大致确定,相机空间下相对运算要比屏幕空间更少一些。所以最合适的实际上就是相机空间下进行计算。

我们可以通过Unity的内置矩阵将法线从物体空间转化到视空间,然后对于是空间的法线方向,由于方向是(-1,1)区间,我们需要再将其*0.5 + 0.5变化到(0,1)区间,就可以实现使用这个xy方向采样Matcap贴图了。

Matcap的实现

上面了解了Matcap的原理,下面直接上代码:

/********************************************************************
 FileName: Matcap.shader
 Description: Matcap效果
 history: 4:11:2018 by puppet_master
 https://blog.csdn.net/puppet_master
*********************************************************************/
Shader "Unlit/Matcap"
{
	Properties
	{
		_MatCap ("Matcap", 2D) = "white" {}
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100

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

			struct v2f
			{
				float2 matcapuv : TEXCOORD0;
				float4 vertex : SV_POSITION;
			};

			sampler2D _MatCap;
			
			v2f vert (appdata_base v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				//乘以逆转置矩阵将normal变换到视空间
				float3 viewnormal = mul(UNITY_MATRIX_IT_MV, v.normal);
				//需要normalize一下,否则保证normal处在(-1,1)区间,否则有scale的object效果不对
				viewnormal = normalize(viewnormal);
				o.matcapuv = viewnormal.xy * 0.5 + 0.5;			
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				fixed4 mat = tex2D(_MatCap, i.matcapuv);
				return mat;
			}
			ENDCG
		}
	}
}

我们使用了UNITY_MATRIX_IT_MV,即正常矩阵的逆转置进行计算,原因在于直接用ModelView矩阵变换法线时,对于非uniform变换可能导致法线与平面不垂直的问题。关于法线的变换,可以参考本人之前的blog《Unity Shader-描边效果》

我们使用一张Matcap,在Matcap中,我们最终采样的是一个法线方向变换到(0,1)区间的结果,实际上有效的区域就只是贴图中心周围的圆形范围内有效。而Matcap比较直白的效果就是,在当前视角方向看,Matcap的上下左右方向的颜色就对应模型在当前视角方向对应的法线所指向的上下左右方向。

比如我们用下面一张Matcap贴图:

在不同的模型上的效果如下:

从上图中,我们很容易看出,对于一个球体,球体上显示的,实际上就是Matcap上对应的内容,不论我们从哪个视角看,球体上都是这样的一个表现。Matcap左上角的高光两点,对应到模型上左上角的部分也会有高光亮点。

另外,还有一点需要注意,在我们把Normal变换到视空间后,我们进行了一次Normalize操作。其实我在网上看到了绝大部分的版本的Matcap的uv计算实现是这样的:

o.matcapuv.x = mul(UNITY_MATRIX_IT_MV[0], v.normal);
o.matcapuv.y = mul(UNITY_MATRIX_IT_MV[1], v.normal);

可能是出于性能的考虑,直接使用UNITY_MATRIX_IT_MV的x和y轴作为基,直接求Normal在xy轴的投影,虽然可以减少矩阵的计算,但是这样有一个比较严重的问题。如果对象的Scale是1,那么效果没有问题,但是如果对象的Scale非1,那么得到的法线并非在(-1,1)区间,转化之后的uv值肯定也跑偏了,结果自然就不对了,如下图。

上图中,中间的球体Scale为1,效果正常;左侧Scale为0.5,边缘的采样效果不对;右侧Scale为2,只采样了一部分Matcap。所以,在计算后一定要保证ViewNormal处在(-1,1)区间,否则最终(0,1)区间采样Matcap的结果肯定不对。所以,建议还是使用更加保证效果正确的方式,直接Normalize之后,就可以保证Scale之后效果也正常啦。

Matcap的效果

因为Matcap本身的原理很简单,我们使用的Shader就相当于是一个模板了,任意的效果都是通过替换这张Matcap图来实现的。下面就是来看一下Matcap效果的时候啦,我从ZBrush内置的Matcap以及提供的Matcap库中选取了几个好玩的Matcap图,ZBrush中的材质是ZMT格式的,我们直接可以预览的时候把Matcap求截个图使用(感觉方法low了点,不过这也是最简单的方法啦)。注:对应Matcap图实际上就与场景中的球体表现一致。

先来个小金人效果,最简单的模拟金属的效果:

玉石次表面效果:

另一种玉石的效果,有点大理石的赶脚:

Matcap也可以实现边缘光效果,甚至画一个半球也可以实现一部分的边缘光效果:

简单模拟反射的效果:

一种挺好玩的风格,ZBrush里面叫fish skin(鱼皮效果是什么鬼):

Matcap优化

虽然直接使用Matcap可以用很省的消耗做出各种特殊效果,但是实际上Matcap也有一些限制,这也是Matcap的原理导致的。我们看下面的图片:

在比较圆润的对象上,我们可以看到较好的效果,但是在小狮子的底座上平面,还有右侧的正方体上,在同样的一个平面内,我们看不到任何变化,整个平面都是平的;而且在传统的Matcap上有一个问题,整体的渲染效果与视角关系不算很大,有区别的情况仅在于平面的法线分布不同导致表面效果不同,这导致了Matcap在视角方面有一些限制,仅对于固定的相机视角较好。

我们考虑一下上面我们采样matcap的操作应该就可以了解这种情况的原因啦,当遇到一个平面时,这个平面上的所有的法线方向都是相同的,转化到视空间后,方向也是相同的,再*0.5 + 0.5之后的uv值也是相同的,最终就导致了一个平面上所有的像素点采样matcap得到是值都是相同的。为了缓解这种情况,我们就需要不仅仅考虑视方向的问题,还需要考虑一下位置的问题,换句话说,我们可以把相机的位置也加入计算,物体相对于相机的方向也作为matcap采样的影响因子之一。

最简单的,我们可以在计算方向的时候,不直接使用Normal计算,而是根据当前像素点的相机空间位置,相机空间法线,计算一个反射的方向,再用这个反射的方向进行matcap采样即可:

float3 viewnormal = mul(UNITY_MATRIX_IT_MV, v.normal);
float3 viewPos = UnityObjectToViewPos(v.vertex);
float3 r = reflect(viewPos, viewnormal);
r = normalize(r);
o.matcapuv = r.xy * 0.5 + 0.5;

还有一种方式,是我从一个老外的blog里面发现的,也是使用了反射方向进行计算,但是不是直接用反射方向的xy,而是通过一个公式将xy按照一个权重进行调制,得到的效果更好。公式如下:

优化后的Shader如下:

/********************************************************************
 FileName: Matcap.shader
 Description: Matcap效果
 history: 5:11:2018 by puppet_master
 https://blog.csdn.net/puppet_master
*********************************************************************/
Shader "Unlit/MatcapReflect"
{
	Properties
	{
		_MatCap ("Matcap", 2D) = "white" {}
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100
 
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"
 
			struct v2f
			{
				float2 matcapuv : TEXCOORD0;
				float4 vertex : SV_POSITION;
			};
 
			sampler2D _MatCap;
			sampler2D _GlobalMatcap;
			
			v2f vert (appdata_base v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				//乘以逆转置矩阵将normal变换到视空间
				float3 viewnormal = mul(UNITY_MATRIX_IT_MV, v.normal);
				viewnormal = normalize(viewnormal);
				float3 viewPos = UnityObjectToViewPos(v.vertex);
				float3 r = reflect(viewPos, viewnormal);
				float m = 2.0 * sqrt(r.x * r.x + r.y * r.y + (r.z + 1) * (r.z + 1));
				o.matcapuv = r.xy / m + 0.5;
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				fixed4 mat = tex2D(_MatCap, i.matcapuv);
				return mat;
			}
			ENDCG
		}
	}
}

最终效果如下,可见,在正方体的平面上,也出现了类似次表面玉石的效果变化:

在换一张反射效果的贴图,在Cube上也能显示出比较好的效果,来张动图:

动态生成Matcap

Matcap目前主要还是用于实现一些好玩的特殊效果,但是对于动态光照等效果,Matcap着实是做不到的。毕竟Matcap本身就是一种预计算好的特殊光照效果贴图,要想随着场景的光进行动态变化,目前本人想到的就两种方式。第一,仅把Matcap作为一个输入的参数,额外按照光照计算一个权重来调制Matcap效果;另一种就是直接渲染一个球体,作为动态的Matcap,这个球体可以使用很复杂的计算Shader,然后场景里面其他的对象采样Matcap。这个idea也是源自另一篇老外的blog《World Space MatCap Shading》(不过该blog作者使用的是世界空间的Normal进行的计算,结果遇到了一堆问题,个人并不是很看好这种方式)。这种方式可能会节省一些光照的计算,但是本人没有具体测试过,所以就当玩一下啦。

下面,我们实现一下动态生成一张Matcap进行一个最简单的diffuse计算效果,首先我们使用一个简单的Shader如下:

	
Shader "Unlit/SimpleLight"
{

	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100

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

			struct v2f
			{
				float4 vertex : SV_POSITION;
				float3 worldNormal : NORMAL;
			};

			
			v2f vert (appdata_base v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				
				o.worldNormal = UnityObjectToWorldNormal(v.normal);

				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				float3 lightDir = _WorldSpaceLightPos0.xyz;
				float3 normal = normalize(i.worldNormal);
				float ndotl = saturate(dot(lightDir, normal));
				return fixed4(ndotl,ndotl,ndotl,1);
			}
			ENDCG
		}
	}
}

然后我们用CommandBuffer在相机前面绘制一个Sphere到RT上作为动态的Matcap:

var cam = GetComponent<Camera>();
matcap = RenderTexture.GetTemporary(512, 512, 24, RenderTextureFormat.Default, RenderTextureReadWrite.Default, 4);
var commandbuffer = new CommandBuffer();
commandbuffer.ClearRenderTarget(true, true, Color.black);
commandbuffer.SetRenderTarget(matcap);
commandbuffer.DrawRenderer(sphereRenderer, sphereRenderer.sharedMaterial);
cam.AddCommandBuffer(CameraEvent.BeforeForwardOpaque, commandbuffer);
Shader.SetGlobalTexture("_GlobalMatcap", matcap);

我们在场景中放置两个模型,左侧为Matcap效果,右侧为SimpleLight效果,上面的Sphere显示动态的Matcap,可见Matcap效果与SimpleLight效果类似:

通过这样的一个方式,如果计算方式很复杂的光照计算,我们就可以通过Matcap进行预计算一次,然后其他所有对象采样Matcap来达到动态的效果。

总结

本文主要实现了基本的Matcap效果,基于反射的Matcap效果,以及动态生成Matcap效果。在正式使用时Matcap可能并不会直接作为结果输出使用,有可能是用于某些特殊光照效果,或者特殊材质效果,配合正常的贴图,Mask贴图等使用。

周末又通关了一个小游戏《12 is better than 6》,流程很短,但是很硬核,而且很有特点,下一篇的开头又有东西写啦!

猜你喜欢

转载自blog.csdn.net/puppet_master/article/details/83582477