Unity Shader - What is MatCap - 什么是 MatCap - ShaderLab 中实现


声明

下面部分资源是通过抓帧得到的资源,仅用于学习,也不公开

如果有 qin quan,可以提醒一下,马上删除


环境

Unity : 5.X 以上
Pipeline : BRP

(修改到 URP 也就非常简单)


什么是 MatCap?

MatCaps

Matcap stands for “material capture” and is an image that is used as an image texture to fake a whole material including lighting and reflections in 3D applications. The most common use is while sculpting objects but it can be used in games for objects that should always be lit or to achieve special effects such as x-ray, ghost or radar.

The image consist of a simple sphere that is mapped to the object’s normals in relation to the camera. As the camera moves around the object, the reflections and highlights move around your object much like the object where moving and not the camera. In other words, if your object were a sphere, no matter how you looked at it, it would look like the matcap sphere (shading, colors , reflections etc. would always be displayed in the same place). But when a matcap is applied to non-spherical shapes the material responds as if it were made of a complex material.

Matcaps is nice while sculpting. It do work with rendering, to an extent, as the material do not interact with the scenes lightning and reflection of other objects in the scene is best used for proof of concepts. It can be, and is, used to achieve special effects in games. It’s a good time saver when you do not have the time to set up complex lighting or materials.


Matcap 代表“材料捕捉”,是一种图像,用作图像纹理来伪造整个材料,包括 3D 应用程序中的照明和反射。最常见的用途是在雕刻物体时,但它可用于游戏中应始终点亮的物体或实现特殊效果,例如 X 射线、鬼影或雷达。

该图像由一个简单的球体组成,该球体映射到相对于相机的对象法线。当相机围绕对象移动时,反射和高光围绕您的对象移动,就像移动的对象而不是相机一样。换句话说,如果你的对象是一个球体,无论你怎么看它,它看起来都像 matcap 球体(阴影、颜色、反射等总是显示在同一个地方)。但是,当将 matcap 应用于非球形形状时,材料的反应就好像它是由复杂材料制成的。

Matcaps 在雕刻时很好用。在一定程度上,它确实适用于渲染,因为材质不会与场景中的灯光交互,并且场景中其他对象的反射最适合用于概念、效果验证。它可以并且用于在游戏中实现特殊效果。当您没有时间设置复杂的照明或材质时,这是一个很好的节省时间的方法。


总结一句话,就是:MatCap 是早期为了方便查看在 Sculpt(雕刻模型时的) 一些光影(灯光、反射)效果而制作的简单的 shader


因为效果好,见效快,提升建模效果预览,然后一直流传开来,在各个建模阶段的模型材质预览

直到后面,甚至部分游戏中的实时渲染的情况,也制作用上了,如:角色选择界面,为了给 角色补一些反射效果时,可以使用到


看个效果

在这里插入图片描述

别看这个效果以为是 PBR,其实 shader 超级简单

在 鹅C,早期(2017发布的SDSXS)项目 中也看到有使用到 MatCap
在这里插入图片描述


Unity ShaderLab 实现 MatCap

下面代码是在 这篇: MatCap — Render & Art pipeline optimization for mobile devices 上实现的

// jave.lin : 参考:https://medium.com/playkids-tech-blog/matcap-render-art-pipeline-optimization-for-mobile-devices-4e1a520b9f1a
// 简单到不能再简单了
// 原理是:将 法线转换到 ViewSpace,然后将 ViewSpace 下的法线 [-1~1] 转为 [0~1]
Shader "MatCap1"
{
    
    
	Properties
	{
    
    
		_MainTex ("Diffuse (RGB)", 2D) = "white" {
    
    }
		_MatCap  ("MatCap (RGB)", 2D) = "gray" {
    
    }
	}

	Subshader
	{
    
    
		Tags {
    
     "Queue"="Geometry" "RenderType"="Opaque" }

		Pass
		{
    
    
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc"
			
			uniform sampler2D _MainTex;
			uniform sampler2D _MatCap;
			uniform float4 _MainTex_ST;
			uniform float4 _MatCap_ST;
			
			struct appdata 
			{
    
    
			    float4 vertex    : POSITION;
			    float4 texcoord0 : TEXCOORD0;
			    float3 normal    : NORMAL;
			};

			struct v2f
			{
    
    
			    float4 pos	  : SV_POSITION;
			    float2 uv 	  : TEXCOORD0;
			    float2 uvCap	  : TEXCOORD1;
			};
	
			v2f vert (appdata v)
			{
    
    
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.uv  = TRANSFORM_TEX(v.texcoord0, _MainTex);

				float3 worldNormal = normalize( unity_WorldToObject[0].xyz * v.normal.x + 
						                        unity_WorldToObject[1].xyz * v.normal.y + 
						                        unity_WorldToObject[2].xyz * v.normal.z);

				worldNormal = mul((float3x3)UNITY_MATRIX_V, worldNormal);
				o.uvCap.xy = worldNormal.xy * 0.5 + 0.5;

				return o;
			}
	
			float4 frag (v2f i) : COLOR
			{
    
    
				float4 albedo = tex2D(_MainTex, i.uv);
				float4 MatCap = tex2D(_MatCap, i.uvCap);
        		return (albedo + (MatCap * 2) - 1); // jave.lin : 这里就不应该 albedo + matcap,会过曝
			}
			ENDCG
		}
	}
}

然后自己撸一个 - 只有 Reflect

// jave.lin : 参考:https://medium.com/playkids-tech-blog/matcap-render-art-pipeline-optimization-for-mobile-devices-4e1a520b9f1a
// 简单到不能再简单了
// 原理是:将 法线转换到 ViewSpace,然后将 ViewSpace 下的法线 [-1~1] 转为 [0~1]
// 然后将  0~1 作为正面半球法线的 MatCapTex 纹理的采样坐标即可
Shader "MatCap2"
{
    
    
	Properties
	{
    
    
		_MainTex ("MainTex (RGB)", 2D) = "white" {
    
    }
		_MatCapTex("MatCap Reflect Tex (RGB)", 2D) = "gray" {
    
    }
		_ReflectIntensity ("Reflection Intensity", Range(0, 1)) = 0.5
	}

	Subshader
	{
    
    
		Tags {
    
     "Queue"="Geometry" "RenderType"="Opaque" }

		Pass
		{
    
    
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc"
			
			struct a2v {
    
    
			  float4 positionOS : POSITION;
			  float2 uv : TEXCOORD0;
			  float3 normal : NORMAL;
			};

			struct v2f {
    
    
			  float4 positionCS : SV_POSITION;
			  float2 uv : TEXCOORD0;
			  float2 uvMC : TEXCOORD1;
			};

			sampler2D _MainTex;
			sampler2D _MatCapTex;
			float4 _MatCap_TexelSize;
			fixed _ReflectIntensity;

			v2f vert(a2v v)
			{
    
    
			  v2f o = (v2f)0;
			  o.positionCS = UnityObjectToClipPos(v.positionOS);
			  o.uv = v.uv;

			  float aspect = _ScreenParams.x / _ScreenParams.y;

			  float3 worldNormal = UnityObjectToWorldNormal(v.normal); // jave.lin : obj to world space
			  worldNormal = mul((float3x3)UNITY_MATRIX_V, worldNormal); // jave.lin : world to view space
			  o.uvMC = worldNormal.xy * 0.5 + 0.5; // jave.lin : [-1~1] to [0~1], 正对镜头的法线是 0.5, 0.5
			  // jave.lin : 另外,这里值得注意的是,因为 uvMC 是在 ViewSpace 下的
			  // 而 ViewSpace 下的变换是正交的
			  // 所以需要将 Camera.project type 修改为:othrographic 才能正常预览此效果
			  // 如果使用 perspective 透视的话,离相机透视中心越远的像素,将会出现边缘采样问题
			  return o;
			}

			float4 frag(v2f i) : SV_TARGET
			{
    
    
			  float4 albedo = tex2D(_MainTex, i.uv);

#ifdef _TEST_ON
			  // jave.lin : 测试采样控制在圆形内
			  float2 UVVec = i.uvMC - 0.5;
			  float2 normalUV = normalize(UVVec);
			  float lenUV = length(UVVec);
			  float2 matcapUV = 0.5 + lenUV * normalUV;
			  float4 matcap = tex2D(_MatCapTex, matcapUV);
#else
			  // jave.lin : 发现根本不用控制在原型内
			  // 因为 uvMC 的值本身就是在 圆形内
			  // 因为 镜头永远只能看到正面,也就是法线 xyz 方向都是正面的
			  // 那么 xyz 中的 xy 也是正面方向上的分量,所以不可能会有超出正面半球的情况
			  // 而且 _MatCapTex 本身是提供与 正面半球的采样纹理
			  float4 matcap = tex2D(_MatCapTex, i.uvMC);
#endif
			  return lerp(albedo, matcap, _ReflectIntensity); // jave.lin : 这里使用 lerp 混合就好了
			}

			ENDCG
		}
	}
}

再撸一个 - Reflect, Normal, BaseCol - 后续可以按需求,自己添加

// jave.lin : 参考:https://medium.com/playkids-tech-blog/matcap-render-art-pipeline-optimization-for-mobile-devices-4e1a520b9f1a
// 简单到不能再简单了
// 原理是:将 法线转换到 ViewSpace,然后将 ViewSpace 下的法线 [-1~1] 转为 [0~1]
// 然后将  0~1 作为正面半球法线的 MatCapTex 纹理的采样坐标即可
Shader "MatCap3"
{
    
    
	Properties
	{
    
    
		_MainTex ("MainTex (RGB)", 2D) = "white" {
    
    }
		_MatCapTex("MatCap Reflect Tex (RGB)", 2D) = "gray" {
    
    }
		_ReflectIntensity ("Reflection Intensity", Range(0, 1)) = 0.5
		[KeywordEnum(REFLECT, NORMAL, BASECOL)] _MC("Mode", float) = 0
	}

	Subshader
	{
    
    
		Tags {
    
     "Queue"="Geometry" "RenderType"="Opaque" }

		Pass
		{
    
    
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma multi_compile _ _MC_REFLECT _MC_NORMAL _MC_BASECOL
			#include "UnityCG.cginc"
			
			struct a2v {
    
    
			  float4 positionOS : POSITION;
			  float2 uv : TEXCOORD0;
			  float3 normal : NORMAL;
			};

			struct v2f {
    
    
			  float4 positionCS : SV_POSITION;
			  float2 uv : TEXCOORD0;
			  float2 uvMC : TEXCOORD1;
			};

			sampler2D _MainTex;
			sampler2D _MatCapTex;
			float4 _MatCap_TexelSize;
			fixed _ReflectIntensity;

			v2f vert_reflect(a2v v)
			{
    
    
				v2f o = (v2f)0;
				o.positionCS = UnityObjectToClipPos(v.positionOS);
				o.uv = v.uv;

				float aspect = _ScreenParams.x / _ScreenParams.y;

				float3 worldNormal = UnityObjectToWorldNormal(v.normal); // jave.lin : obj to world space
				worldNormal = mul((float3x3)UNITY_MATRIX_V, worldNormal); // jave.lin : world to view space
				o.uvMC = worldNormal.xy * 0.5 + 0.5; // jave.lin : [-1~1] to [0~1], 正对镜头的法线是 0.5, 0.5
				// jave.lin : 另外,这里值得注意的是,因为 uvMC 是在 ViewSpace 下的
				// 而 ViewSpace 下的变换是正交的
				// 所以需要将 Camera.project type 修改为:othrographic 才能正常预览此效果
				// 如果使用 perspective 透视的话,离相机透视中心越远的像素,将会出现边缘采样问题
				return o;
			}

			v2f vert_normal(a2v v)
			{
    
    
				v2f o = (v2f)0;
				o.positionCS = UnityObjectToClipPos(v.positionOS);
				float3 worldNormal = UnityObjectToWorldNormal(v.normal); // jave.lin : obj to world space
				worldNormal = mul((float3x3)UNITY_MATRIX_V, worldNormal); // jave.lin : world to view space
				o.uvMC = worldNormal.xy * 0.5 + 0.5; // jave.lin : [-1~1] to [0~1]
				return o;
			}

			v2f vert_basecol(a2v v)
			{
    
    
				v2f o = (v2f)0;
				o.positionCS = UnityObjectToClipPos(v.positionOS);
				o.uv = v.uv;
				return o;
			}

			v2f vert(a2v v)
			{
    
    
#ifdef _MC_REFLECT
			  return vert_reflect(v);
#elif defined(_MC_NORMAL)
			  return vert_normal(v);
#else
			  return vert_basecol(v);
#endif
			}

			float4 frag_reflect(v2f i)
			{
    
    
			  float4 albedo = tex2D(_MainTex, i.uv);

#ifdef _TEST_ON
			  // jave.lin : 测试采样控制在圆形内
			  float2 UVVec = i.uvMC - 0.5;
			  float2 normalUV = normalize(UVVec);
			  float lenUV = length(UVVec);
			  float2 matcapUV = 0.5 + lenUV * normalUV;
			  float4 matcap = tex2D(_MatCapTex, matcapUV);
#else
			  // jave.lin : 发现根本不用控制在原型内
			  // 因为 uvMC 的值本身就是在 圆形内
			  // 因为 镜头永远只能看到正面,也就是法线 xyz 方向都是正面的
			  // 那么 xyz 中的 xy 也是正面方向上的分量,所以不可能会有超出正面半球的情况
			  // 而且 _MatCapTex 本身是提供与 正面半球的采样纹理
			  float4 matcap = tex2D(_MatCapTex, i.uvMC);
#endif
			  return lerp(albedo, matcap, _ReflectIntensity); // jave.lin : 这里使用 lerp 混合就好了
			}

			float4 frag_normal(v2f i)
			{
    
    
			  return float4(i.uvMC, 0, 1);
			}

			float4 frag_basecol(v2f i)
			{
    
    
			  return tex2D(_MainTex, i.uv);
			}

			float4 frag(v2f i) : SV_TARGET
			{
    
    
#ifdef _MC_REFLECT
			  return frag_reflect(i);
#elif defined(_MC_NORMAL)
			  return frag_normal(i);
#else
			  return frag_basecol(i);
#endif
			}

			ENDCG
		}
	}
}

使用了法线贴图的纹理 的情况

如果使用了 法线贴图的情况,那么只能将 matcap uv 放在 fragment shader 中计算,如下伪代码:

思路是:先要有 TBN,然后将 normal map 中的 normalTS 采样出来,然后 T2W 计算出 normalWS,再将 normalWS.xy * 0.5 + 0.5 之后的结果用于采样 mapcatTex

    struct VS_INPUT
    {
    
    
        float4 vertex : POSITION; 
        float4 uvMC : TEXCOORD0;
        float3 normalOS : NORMAL;
        float4 tangentOS : TANGENT;
    };

    struct VS_OUTPUT
    {
    
    
        float4 pos : SV_Position;
        float4 uvMC : TEXCOORD0;
		...
        half3 normalWS : TEXCOORD3;
		half3 bitangentWS : TEXCOORD4;
		half3 tangentWS : TEXCOORD5;
		...
    };
    ...
	sampler2D __NormalTex;
	sampler2D __MatCapTex;
    ...
    VS_OUTPUT vert(VS_INPUT v)
    {
    
    
    	...
        half3 normalWS = UnityObjectToWorldNormal(v.normalOS);
		half3 tangentWS = mul((float3x3)unity_ObjectToWorld, v.tangentOS.xyz);
        // (float3 bitangentWS = ((normalWS.yzx * tangentWS.zxy) - (normalWS.zxy * tangentWS.yzx)));
		half3 bitangentWS = cross(normalWS, tangentWS) * v.tangentOS.w;
        // jave.lin : 具体 TBN 可以参考:UnityCG.cginc 中的 TANGENT_SPACE_ROTATION 宏定义
        
		...

        VS_OUTPUT output = (VS_OUTPUT)0;
        output.pos = UnityObjectToClipPos(v.vertex);
        output.normalWS = normalWS;
        output.bitangentWS = bitangentWS;
        output.tangentWS = tangentWS;

        return output;
    }

    fixed4 frag(VS_OUTPUT input) : SV_TARGET
    {
    
    
    	...
        half3 normalWS = normalize(input.normalWS);
        half3 bitangentWS = normalize(input.bitangentWS);
        half3 tangentWS = normalize(input.tangentWS);

        half3x3 TBN = half3x3(tangentWS, bitangentWS, normalWS);
		// jave.lin : sample & decode normalTS
        half3 N_from_map_WS = ((tex2D(__NormalTex, input.uv.xy).xyz * 2.0) - 1.0);
        // jave.lin : TS 2 WS
        N_from_map_WS = mul(N_from_map_WS, TBN);
		// jave.lin : WS 2 VS
        half3 N_from_map_VS = mul(N_from_map_WS, (float3x3)UNITY_MATRIX_V);
        // jave.lin : remap : [-1~1] to [0, 1]
        half2 uv_matcap = N_from_map_VS.xy * 0.5 + 0.5;
        // jave.lin : 采样 matcap
        float3 mapcat = tex2D(__MatCapTex, uv_matcap).rgb;
        ...
    }

使用到的纹理 & 材质球

纹理
在这里插入图片描述
材质
在这里插入图片描述


预览 MatCap 需要在 相机投影

一般为了更好的预览 MatCap 的效果,都会在 正交 投影下进行,透视也不是不可以,只不过效果没那么好

如下,透视,下的瑕疵:
在这里插入图片描述

正交就很完美
在这里插入图片描述


Project

backup(不公开) : TestBRP_MatCap


References

猜你喜欢

转载自blog.csdn.net/linjf520/article/details/123669608