Unity3d surface shader 在DX11上的曲面细分

发表于2017-06-16
评论0 3.7k浏览
Unity中Surface shader 支持 DX11 GPU曲面细分的原因在于:
  1. 曲面细分函数用 tessellate:FunctionName 表示。这个函数能计算三角形边缘和一些曲面细分的内部因素。
  2. 当曲面细分被使用时,“顶点函数”(vertex:FunctionName)在曲面细分之后被调用,在shader包括的物体之内的每个顶点都是如此。因此你可以再次进行贴图置换。
  3. urface shader 能随意地计算 phong 曲面细分 来光滑模型的表面,甚至不需要贴图置换。

现在unity3d对曲面细分支持的局限性:
  1. 只有三角形面片可以而四边形的不行,等值线曲面细分也不支持(isoline tessellation)。
  2. 当使用曲面细分时,shader自动编译成 shader model 5.0 版本,因此曲面细分只能用在DX11上(ShaderModel 5.0 → DirectX 11)。

贴图置换(Displacement mapping),可被用做现有凹凸贴图技术的临时替代技术,bumpmap只是假象,模型并没有真正凹凸,贴图置换是移动模型顶点,使模型真正产生了凹凸。

无细分,只有贴图置换的shader(No GPU tessellation, displacement in the vertex modifier)

我们先在不使用曲面细分的情况下贴图置换。基于贴图置换的数值,沿着法线移动顶点。

让我们看看shader:
Shader "Custom/testShader" {
 Properties {
            _MainTex ("Base (RGB)", 2D) = "white" {}
            _DispTex ("Disp Texture", 2D) = "gray" {}
            _NormalMap ("Normalmap", 2D) = "bump" {}
            _Displacement ("Displacement", Range(0, 1.0)) = 0.3
            _Color ("Color", color) = (1,1,1,0)
            _SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
        }
        SubShader {
            Tags { "RenderType"="Opaque" }
            LOD 300
            CGPROGRAM
            #pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp nolightmap
            #pragma target 5.0
            struct appdata {
                float4 vertex : POSITION;
                float4 tangent : TANGENT;
                float3 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
            };
            sampler2D _DispTex;
            float _Displacement;
            void disp (inout appdata v)
            {
                float d = tex2Dlod(_DispTex, float4(v.texcoord.xy,0,0)).r * _Displacement;
                v.vertex.xyz += v.normal * d;
            }
            struct Input {
                float2 uv_MainTex;
            };
            sampler2D _MainTex;
            sampler2D _NormalMap;
            fixed4 _Color;
            void surf (Input IN, inout SurfaceOutput o) {
                half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                o.Albedo = c.rgb;
                o.Specular = 0.2;
                o.Gloss = 1.0;
                o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
            }
            ENDCG
        }
        FallBack "Diffuse"
    }

tex2Dlod 以指定的细节级别和可选的位移来解析贴图

在这个顶点函数中,把每个顶点都向着法线方向偏移一些(取决于_Displacement的值)

上面的shader非常标准:
  1. 在定点函数disp内,每个顶点都根据displacement的值的大小沿着该点的法线移动
  2. 使用了自定义的顶点输出的结构体appdata,而不是默认值appdata_full。这个现在还用不到,但是结构体尽可能的小,会提高曲面细分的效率。
  3. 顶点数据没有uv坐标,在#pragma处加上nolightmap指令,就不包含光照贴图了

固定数量的曲面细分

细化算法

让我们看看这些点是怎么生成的

按照初始单元网格分类,包括三角形网格细分和四边形网格细分,前面已经提到了,unity只支持三角形网格细分,而不支持四边形。

曲面细分就是用一定的规则对多边形网格进行逐层细化。细分模式的基本规则是从粗糙的大网格,通过添加新的顶点来形成新的边和面(或通过削角的办法形成新的边和面),这样递归地平滑细分,直到最终获得光滑曲面,DX11采用了PN-Triangles算法能将低分辨率模型转化为弯曲表面,该表面之后可以被重新绘制成“高精度曲面细分”的三角形网格。用来消除游戏中本该是圆滑却是多边形的假象。

如果整个模型在屏幕上的细分程度一样,使用这个方法很合适。有些脚本基于模型与相机的的距离来改变细分程度。
Shader "Custom/testShader" {
 Properties {
            _Tess ("Tessellation", Range(1,32)) = 4
            _MainTex ("Base (RGB)", 2D) = "white" {}
            _DispTex ("Disp Texture", 2D) = "gray" {}
            _NormalMap ("Normalmap", 2D) = "bump" {}
            _Displacement ("Displacement", Range(0, 1.0)) = 0.3
            _Color ("Color", color) = (1,1,1,0)
            _SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
        }
        SubShader {
            Tags { "RenderType"="Opaque" }
            LOD 300
            CGPROGRAM
            #pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessFixed nolightmap
            #pragma target 5.0
            struct appdata {
                float4 vertex : POSITION;
                float4 tangent : TANGENT;
                float3 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
            };
            float _Tess;
            float4 tessFixed()
            {
                return _Tess;
            }
            sampler2D _DispTex;
            float _Displacement;
            void disp (inout appdata v)
            {
                float d = tex2Dlod(_DispTex, float4(v.texcoord.xy,0,0)).r * _Displacement;
                v.vertex.xyz += v.normal * d;
            }
            struct Input {
                float2 uv_MainTex;
            };
            sampler2D _MainTex;
            sampler2D _NormalMap;
            fixed4 _Color;
            void surf (Input IN, inout SurfaceOutput o) {
                half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                o.Albedo = c.rgb;
                o.Specular = 0.2;
                o.Gloss = 1.0;
                o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
            }
            ENDCG
        }
        FallBack "Diffuse"
    }

曲面细分函数tessFixed返回一个float4的值:xyz是三角形的三个顶点的细分程度,w是比例。在本shader里只是一个float常量来作为模型的细分程度。


以距离为基础的曲面细分(Distance-based tessellation)

我们也能通过模型与相机的距离改变细分程度。举个例子,我们需要定义两个距离值;一个是距离相机最近的距离值,一个是最远的距离值。
Shader "Custom/testShader" {
 Properties {
            _Tess ("Tessellation", Range(1,32)) = 4
            _MainTex ("Base (RGB)", 2D) = "white" {}
            _DispTex ("Disp Texture", 2D) = "gray" {}
            _NormalMap ("Normalmap", 2D) = "bump" {}
            _Displacement ("Displacement", Range(0, 1.0)) = 0.3
            _Color ("Color", color) = (1,1,1,0)
            _SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
        }
        SubShader {
            Tags { "RenderType"="Opaque" }
            LOD 300
            CGPROGRAM
            #pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessDistance nolightmap
            #pragma target 5.0
            #include "Tessellation.cginc"
            struct appdata {
                float4 vertex : POSITION;
                float4 tangent : TANGENT;
                float3 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
            };
            float _Tess;
            float4 tessDistance (appdata v0, appdata v1, appdata v2) {
                float minDist = 10.0;
                float maxDist = 25.0;
                return UnityDistanceBasedTess(v0.vertex, v1.vertex, v2.vertex, minDist, maxDist, _Tess);
            }
            sampler2D _DispTex;
            float _Displacement;
            void disp (inout appdata v)
            {
                float d = tex2Dlod(_DispTex, float4(v.texcoord.xy,0,0)).r * _Displacement;
                v.vertex.xyz += v.normal * d;
            }
            struct Input {
                float2 uv_MainTex;
            };
            sampler2D _MainTex;
            sampler2D _NormalMap;
            fixed4 _Color;
            void surf (Input IN, inout SurfaceOutput o) {
                half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                o.Albedo = c.rgb;
                o.Specular = 0.2;
                o.Gloss = 1.0;
                o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
            }
            ENDCG
        }
        FallBack "Diffuse"
    }

“Tessellation.cginc”中我们找到了UnityDistanceBasedTess函数
float4 UnityDistanceBasedTess (float4 v0, float4 v1, float4 v2, float minDist, float maxDist, float tess)
{
	float3 f;
	f.x = UnityCalcDistanceTessFactor (v0,minDist,maxDist,tess);
	f.y = UnityCalcDistanceTessFactor (v1,minDist,maxDist,tess);
	f.z = UnityCalcDistanceTessFactor (v2,minDist,maxDist,tess);
	return UnityCalcTriEdgeTessFactors (f);
}

我们能看到UnityDistanceBasedTess函数主要还是调用UnityCalcDistanceTessFactor这个函数,于是我们又在"Tessellation.cginc"中找到了它;
float UnityCalcDistanceTessFactor (float4 vertex, float minDist, float maxDist, float tess)
{
	float3 wpos = mul(_Object2World,vertex).xyz;
	float dist = distance (wpos, _WorldSpaceCameraPos);
	float f = clamp(1.0 - (dist - minDist) / (maxDist - minDist), 0.01, 1.0) * tess;
	return f;
}

在UnityCalcDistanceTessFactor函数中把点转换为世界坐标wpos,在求出点与同是世界坐标的相机的距离dist,再求出细分程度tess。

这里的细分函数有三个参数;三角形的三个角在曲面细分之前顶点数据。这通过顶点位置来计算细分程度。我们声明包含帮助文件“Tessellation.cginc”(EditorDataCGIncludes中)并且调用其中的UnityDistanceBasedTess函数来做所有工作。那个函数计算每个顶点与相机的距离,并返回细分函数的float4值。

远(far):

近(close):

这个gif也能体现相机距离与细分程度的变化

基于边缘长度的曲面细分(Edge length based tessellation)

单纯的基于距离的曲面细分只是在当在三角形大小差不多时效果很好。
将要基于屏幕上的三角形边缘长度计算曲面细分,可以使用更大的细分程度
Shader "Custom/testShader" {
Properties {
            _EdgeLength ("Edge length", Range(2,50)) = 15
            _MainTex ("Base (RGB)", 2D) = "white" {}
            _DispTex ("Disp Texture", 2D) = "gray" {}
            _NormalMap ("Normalmap", 2D) = "bump" {}
            _Displacement ("Displacement", Range(0, 1.0)) = 0.3
            _Color ("Color", color) = (1,1,1,0)
            _SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
        }
        SubShader {
            Tags { "RenderType"="Opaque" }
            LOD 300
            CGPROGRAM
            #pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessEdge nolightmap
            #pragma target 5.0
            #include "Tessellation.cginc"
            struct appdata {
                float4 vertex : POSITION;
                float4 tangent : TANGENT;
                float3 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
            };
            float _EdgeLength;
            float4 tessEdge (appdata v0, appdata v1, appdata v2)
            {
                return UnityEdgeLengthBasedTess (v0.vertex, v1.vertex, v2.vertex, _EdgeLength);
            }
            sampler2D _DispTex;
            float _Displacement;
            void disp (inout appdata v)
            {
                float d = tex2Dlod(_DispTex, float4(v.texcoord.xy,0,0)).r * _Displacement;
                v.vertex.xyz += v.normal * d;
            }
            struct Input {
                float2 uv_MainTex;
            };
            sampler2D _MainTex;
            sampler2D _NormalMap;
            fixed4 _Color;
            void surf (Input IN, inout SurfaceOutput o) {
                half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                o.Albedo = c.rgb;
                o.Specular = 0.2;
                o.Gloss = 1.0;
                o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
            }
            ENDCG
        }
        FallBack "Diffuse"
    }

float4 UnityEdgeLengthBasedTess (float4 v0, float4 v1, float4 v2, float edgeLength)
{
	float3 pos0 = mul(_Object2World,v0).xyz;
	float3 pos1 = mul(_Object2World,v1).xyz;
	float3 pos2 = mul(_Object2World,v2).xyz;
	float4 tess;
	tess.x = UnityCalcEdgeTessFactor (pos1, pos2, edgeLength);
	tess.y = UnityCalcEdgeTessFactor (pos2, pos0, edgeLength);
	tess.z = UnityCalcEdgeTessFactor (pos0, pos1, edgeLength);
	tess.w = (tess.x + tess.y + tess.z) / 3.0f;
	return tess;
}

UnityEdgeLengthBasedTess传入的v0,v1,v2是三角形的三个顶点
float UnityCalcEdgeTessFactor (float3 wpos0, float3 wpos1, float edgeLen)
{
	// distance to edge center
	float dist = distance (0.5 * (wpos0+wpos1), _WorldSpaceCameraPos);
	// length of the edge
	float len = distance(wpos0, wpos1);
	// edgeLen is approximate desired size in pixels
	float f = max(len * _ScreenParams.y / (edgeLen * dist), 1.0);
	return f;
}

UnityCalcEdgeTessFactor中先求出两个点的中点与相机的距离(全是世界坐标),再求出两个点的距离,再求出细分程度。

由于性能的原因,可以适当的调用UnityEdgeLengthBasedTessCull函数代替(通过判断相机平截头体,对不在范围内的点不进行细分),调用这个代替函数虽然也浪费一些性能,但不必细分不必要的点
float4 UnityEdgeLengthBasedTessCull (float4 v0, float4 v1, float4 v2, float edgeLength, float maxDisplacement)
{
	float3 pos0 = mul(_Object2World,v0).xyz;
	float3 pos1 = mul(_Object2World,v1).xyz;
	float3 pos2 = mul(_Object2World,v2).xyz;
	float4 tess;
	if (UnityWorldViewFrustumCull(pos0, pos1, pos2, maxDisplacement))
// UnityWorldViewFrustumCull平截头体的剔除如果被剔除(返回0)则不进行细分(Tess = 0)
	{
		tess = 0.0f;
	}
	else
	{
		tess.x = UnityCalcEdgeTessFactor (pos1, pos2, edgeLength);
		tess.y = UnityCalcEdgeTessFactor (pos2, pos0, edgeLength);
		tess.z = UnityCalcEdgeTessFactor (pos0, pos1, edgeLength);
		tess.w = (tess.x + tess.y + tess.z) / 3.0f;
	}
	return tess;
}



Phong细分

再看Phong曲面细分的shader之前先深入了解一下Phong曲面细分。

Tamy Boubekeur和Marc Alexa 做出的phong曲面细分,

他们用一个视频来简要讲解做法

输入一个带有法线的三角形,在重心插入点来做线性细分

正交投影在与此点的“点法式”(法线相垂直的)平面上

在投射的位置线性的插入
  1. 计算出线性细分
  2. 在三角形三个点上的正切平面上做正交投射
  3. 计算重心插入这三个投影

这是他们做出的结果:


Phong细分修改细分面的位置,使细分面沿着法线突出一点。这是一个让低模变光滑的非常有效的方式。
Unity得surface shaders中能使用tessphong:VariableName编译指令自动计算Phong曲面细分。
Shader "Custom/testShader" {
 Properties {
            _EdgeLength ("Edge length", Range(2,50)) = 5
            _Phong ("Phong Strengh", Range(0,1)) = 0.5
            _MainTex ("Base (RGB)", 2D) = "white" {}
            _Color ("Color", color) = (1,1,1,0)
        }
        SubShader {
            Tags { "RenderType"="Opaque" }
            LOD 300
            CGPROGRAM
            #pragma surface surf Lambert vertex:dispNone tessellate:tessEdge tessphong:_Phong nolightmap
            #include "Tessellation.cginc"
            struct appdata {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
            };
            void dispNone (inout appdata v) { }
            float _Phong;
            float _EdgeLength;
            float4 tessEdge (appdata v0, appdata v1, appdata v2)
            {
                return UnityEdgeLengthBasedTess (v0.vertex, v1.vertex, v2.vertex, _EdgeLength);
            }
            struct Input {
                float2 uv_MainTex;
            };
            fixed4 _Color;
            sampler2D _MainTex;
            void surf (Input IN, inout SurfaceOutput o) {
                half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                o.Albedo = c.rgb;
                o.Alpha = c.a;
            }
            ENDCG
        }
        FallBack "Diffuse"
    }

看看普通shader和Phong细分之后的比较。如果不用之前几个shader的贴图置换,你也能看到模型的表面比以前更圆更光滑了。




Phong细分与贴图置换结合(Phong tessellation&Displacement mapping)

曲面细分再加上贴图置换(Displacement mapping)就是完美
Shader "Custom/sufaceshaderTessellation" {
	Properties {
		_EdgeLength ("Edge length", Range(2,50)) = 5
			_Phong ("Phong Strengh", Range(0,1)) = 0.5
			_MainTex ("Base (RGB)", 2D) = "white" {}
		_Color ("Color", color) = (1,1,1,0)
			_DispTex ("Disp Texture", 2D) = "gray" {}
		_NormalMap ("Normalmap", 2D) = "bump" {}
		_Displacement ("Displacement", Range(0, 1.0)) = 0.3
			_SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 300
			CGPROGRAM
#pragma surface surf Lambert vertex:disp tessellate:tessEdge tessphong:_Phong nolightmap
#include "Tessellation.cginc"
		struct appdata {
			float4 vertex : POSITION;
			float4 tangent : TANGENT;
			float3 normal : NORMAL;
			float2 texcoord : TEXCOORD0;
		};
		sampler2D _DispTex;
		float _Displacement;
		void disp (inout appdata v)
		{
			float d = tex2Dlod(_DispTex, float4(v.texcoord.xy,0,0)).r * _Displacement;
			v.vertex.xyz += v.normal * d;
		}
		float _Phong;
		float _EdgeLength;
		float4 tessEdge (appdata v0, appdata v1, appdata v2)
		{
			return UnityEdgeLengthBasedTess (v0.vertex, v1.vertex, v2.vertex, _EdgeLength);
		}
		struct Input {
			float2 uv_MainTex;
		};
		fixed4 _Color;
		sampler2D _MainTex;
		sampler2D _NormalMap;
		void surf (Input IN, inout SurfaceOutput o) {
			half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
			o.Albedo = c.rgb;
			o.Specular = 0.2;
			o.Gloss = 1.0;
			o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
			o.Alpha = c.a;
		}
		ENDCG
	}
	FallBack "Diffuse"
}


如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引