一般情况下,我们编写unity shader的基本函数有三个:vertex、fragment、surface。surface shader是unity提供给我们方便构建光照渲染使用的,vertex和fragment则是nvidia cg原生的。不过由于后期显卡性能的提升和特殊渲染需求,tessellation shader被dx11和opengl4.0创造出来,中文名曲面细分着色器。
tessellation shader在unity手游开发中用的很少,因为tessellation意味着在源网格基础上新增顶点数据,使曲面达到更平滑效果,这就增加了渲染消耗,所以在手机为平台的三维程序中,很少用到这个。但是真正次世代渲染3A大作中,顶级特效情况下,tessellation是必须用上的。
首先从美术角度来看,模型师为了效果和效率双重考虑,会尽可能减少网格顶点三角面和纹理大小的前提下,使用法向量贴图提高光照细节。这是美术方面很常规的做法,但是有一个致命的缺点,就是顶点三角面细节不够,虽然使用法线贴图处理了光照细节,但是观察细节不够,“眼尖”一点的情况下,从任何角度都能观察到那些“毛糙”的三角面,如下图:
上面是unity官方贴图,一眼都能看出来虽然使用了法向量贴图进行光照细节计算,但是三角面太少导致看起来“棱棱角角”。
于是dx11/opengl4.0在vertex到fragment中间阶段新增tessellation函数将顶点进行插值补间。
经过tessellation补充顶点后的网格三角面图如下:
当然经过差值补间的顶点可以使用置换贴图(高度贴图)进行顶点偏移,或者使用常用算法,比如贝塞尔曲线计算顶点偏移坐标,处理出“圆滑”的效果。
nvidia提供的tessellation技术实现效果就很明显,如下:
tessellation技术可以在显卡层面拓扑更平滑的网格,不仅轻松了美术建模师,而且减小了游戏载体容量,同时开发可以控制tessellation细节层次,更轻松的把控性能和效果之间的平衡。
在图形渲染流程中,tessellation位于vertex和fragment之间,分为三个阶段:1.hull shader stage 2.tessellator stage 3.domain shader stage,主流程图如下:
我们开发主要可以编程的阶段在hull shader和domain shader。
1.hull shader 主要是确定怎么多几何图元进行细分,或者说确定细分规则和参数等,比较通俗来说就是细分得很“细致”还是很“粗糙”,就跟lod一样。
A programmable shader stage that produces a geometry patch (and patch constants) that correspond to each input patch (quad, triangle, or line).
2.tessellator shader 自动处理阶段,对细分的顶点采样uv并拓展三角面
A fixed function pipeline stage that creates a sampling pattern of the domain that represents the geometry patch and generates a set of smaller objects (triangles, points, or lines) that connect these samples.
3.domain shader 通过hull shader提供的顶点和tessellator shader提供的uv进行操作,输出细分顶点的坐标(包含MVP变换)到fragment shader
A domain shader calculates the vertex position of a subdivided point in the output patch. A domain shader is run once per tessellator stage output point and has read-only access to the tessellator stage output UV coordinates, the hull shader output patch, and the hull shader output patch constants
unity官方建议我们方便使用的tessellation的情况全都在surface下,链接如下:
unity官方tessellation技术
unity官方建议我们在光照着色中使用tessellation,而且提供我们Fixed/Distance/Edge/Phong四个曲面细分样例和函数。
使用特定的tessellation方法细分出更多顶点后,然后使用置换贴图(高度贴图)进行顶点“圆滑”计算。
当然了,也可以使用dx11的vertex-tessellation-fragment流程shader,这是我在一位大神博主那找到的,如下:
Shader "Custom/TessellationShader" { Properties { _MainTex("Texture", 2D) = "white" {} _TessFactor("Tessel Factor",Range(1,10)) = 1 } SubShader { Tags { "RenderType" = "Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex tessvert #pragma fragment frag #pragma hull hs #pragma domain ds #pragma target 4.6 #include "UnityCG.cginc" #include "Lighting.cginc" struct appdata { float4 vertex : POSITION; float4 tangent : TANGENT; float3 normal : NORMAL; float2 texcoord : TEXCOORD0; }; struct v2f { float2 texcoord:TEXCOORD0; float4 vertex : SV_POSITION; float4 tangent : TANGENT; float3 normal : NORMAL; }; struct InternalTessInterp_appdata { float4 vertex : INTERNALTESSPOS; float4 tangent : TANGENT; float3 normal : NORMAL; float2 texcoord : TEXCOORD0; }; sampler2D _MainTex; float4 _MainTex_ST; int _TessFactor; /*曲面细分参数*/ InternalTessInterp_appdata tessvert(appdata v) { InternalTessInterp_appdata o; o.vertex = v.vertex; o.tangent = v.tangent; o.normal = v.normal; o.texcoord = v.texcoord; return o; } v2f vert(appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex); return o; } UnityTessellationFactors hsconst(InputPatch<InternalTessInterp_appdata,3> v) { UnityTessellationFactors o; float4 tf; tf = float4(_TessFactor, _TessFactor, _TessFactor, _TessFactor); o.edge[0] = tf.x; o.edge[1] = tf.y; o.edge[2] = tf.z; o.inside = tf.w; return o; } [UNITY_domain("tri")] [UNITY_partitioning("fractional_odd")] [UNITY_outputtopology("triangle_cw")] [UNITY_patchconstantfunc("hsconst")] [UNITY_outputcontrolpoints(3)] InternalTessInterp_appdata hs(InputPatch<InternalTessInterp_appdata,3> v, uint id : SV_OutputControlPointID) { return v[id]; } [UNITY_domain("tri")] v2f ds(UnityTessellationFactors tessFactors, const OutputPatch<InternalTessInterp_appdata,3> vi, float3 bary : SV_DomainLocation) { appdata v; v.vertex = vi[0].vertex*bary.x + vi[1].vertex*bary.y + vi[2].vertex*bary.z; v.tangent = vi[0].tangent*bary.x + vi[1].tangent*bary.y + vi[2].tangent*bary.z; v.normal = vi[0].normal*bary.x + vi[1].normal*bary.y + vi[2].normal*bary.z; v.texcoord = vi[0].texcoord*bary.x + vi[1].texcoord*bary.y + vi[2].texcoord*bary.z; v2f o = vert(v); return o; } fixed4 frag(v2f i) : SV_Target { fixed4 col = tex2D(_MainTex,i.texcoord); return col; } ENDCG } } }
能得到如下效果,如图:
hull和domain tessellation中间shader函数的意义,因为都是unitycg提供的,所以语法和处理属于固定形式,博主博文链接:tessellation处理
总结一下,tessellation是制作3A和未来次世代渲染的关键技术之一,在我眼中和光线追踪一样很有意义,当机器性能整体达到一定高度,这个技术和光追一样就能很通用普及开来。