dede企业网站模板下载,电脑打不开网页怎么回事,网站建设全屏,网页设计购物网站模板大家好#xff0c;我是阿赵。 之前介绍了使用动态法线贴图混合的方式模拟轨迹的凹凸感#xff0c;这次来讲一下更真实的凹凸感制作。不过在说这个内容之前#xff0c;这一篇先要介绍一下曲面细分着色器(Tessellation Shader)的用法。
一、为什么要做曲面细分 之前通过法… 大家好我是阿赵。 之前介绍了使用动态法线贴图混合的方式模拟轨迹的凹凸感这次来讲一下更真实的凹凸感制作。不过在说这个内容之前这一篇先要介绍一下曲面细分着色器(Tessellation Shader)的用法。
一、为什么要做曲面细分 之前通过法线贴图模拟了凹凸的感觉 法线贴图不会真的产生凹凸它只是改变了这个平面上面的法线方向。所以只有通过光照模型通过法线方向和灯光方向进行点乘才会计算出不同的光照角度让我们产生一定的凹凸感觉。 但如果想做到这样的效果法线贴图是不行的 这种效果球是真的陷进去地面了。很明显这些都是需要偏移顶点让网格产生真实的变形才能做到。 不过这里有一个问题如果地面的网格面数并不是很高那么就算我们有能力去偏移顶点也产生不了这样好的效果。 比如一般的地面网格的面数都很低只有这样的水平 这个时候球所在的地方根本就没有顶点所以也偏移不了。就算再稍微多一点面这样的地面网格面数算比较高了仍然产生不了很好的凹凸效果 所以这里有一个很严重的问题我们难道需要用几十万甚至几百万面去做一个地面的模型才能产生真实的凹凸感吗 这是不可能的实际的情况是 在需要到很精确的顶点控制的一个小局部才需要把面数变高其他的地方面数很是很低的。具体可以看看这个视频 Unity引擎动态曲面细分 而这里用到的局部增加面数的技术就是曲面细分(Tessellation)了。
二、曲面细分的过程 在Unity里面写顶点片段着色器的Shader我们一般只会注意到需要些Vertex顶点程序和fragment片段程序因为在大多数情况下其他的渲染管线流程都不是我们可以控制的而我们能控制顶点程序改变模型的形状控制片段程序来改变模型的颜色。 但在顶点程序和片段程序中间其实还有一个曲面细分(tessellate)的过程这个过程有2个程序是我们可以控制的 1、hullProgram 这个程序会接受每个多边形各个顶点的信息记录下来然后通过指定一个Patch Constant Function去设置细分的数量这个过程是针对多边形的每一条边还有多边形的内部分别设置拆分的数量的。 2、domainProgram 在前面的hullProgram里面其实只是设置了顶点信息和拆分数量并没有真正的生成新的网格。而在这个domainProgram里面拆分后的顶点信息已经产生了所以可以对拆分后的顶线进行操作可以计算他们的位置、法线、uv等。 为了避免难以理解也不说太多只要知道需要做曲面细分的时候需要添加2个程序过程一个过程设置了拆分的数量和其他参数另外一个过程就得到了顶点可以进行实际操作这样就行了。
三、曲面细分在Unity引擎的实现
1、Surface类型着色器 Surface类型的Shader提供了很多Unity封装好的方法也包括提供了对应曲面细分着色器的方法。 使用很简单: 1.#include “Tessellation.cginc” 2.指定曲面细分的方法tessellate:tessFunction 3.指定target 4.6
看到这里有target 4.6的声明了没错Unity官方的说明也是这样的 When you use tessellation, the shader is automatically compiled into the Shader Model 4.6 target, which prevents support for running on older graphics targets. 这里着重说一下曲面细分方法。 由于Surface的曲面细分方法是Unity封装好的所以我们不需要走正常的渲染流程不需要指定hullProgram、Patch Constant Function和domainProgram只需要指定一个tessellate处理方法。这个方法实际是返回一个曲面细分的值来决定某个面具体要细分成多少个网格。 而在Unity提供的方法里面对于怎样细分曲面提供了3种选择
1.Fixed固定数量细分 这种方式细分在tessFunction里面直接返回一个数值然后全部面就按照统一的数值去细分。
unity官方文档里面的例子是这样
Shader Tessellation Sample {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 { RenderTypeOpaque }LOD 300CGPROGRAM#pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessFixed nolightmap#pragma target 4.6struct 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}其中曲面细分方法是直接返回了一个指定的值
float4 tessFixed()
{return _Tess;
}2.根据距离细分 这里的距离指的是和摄像机的距离。根据离摄像机不同的距离设置一个范围来细分
unity官方文档里面的例子是这样 Shader Tessellation Sample {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 { RenderTypeOpaque }LOD 300CGPROGRAM#pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessDistance nolightmap#pragma target 4.6#include Tessellation.cgincstruct 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}其中曲面细分方法是传入了最小距离、最大距离和一个控制值
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);
}UnityDistanceBasedTess就是Unity提供的根据距离计算细分值的方法。
3.根据边的长度细分 这个根据边的长度指的是多边形的边在屏幕里面渲染的大小。 所以从左图可以看出越近屏幕的边渲染的长度越大所以细分得越多而离屏幕越远的边渲染的长度越小细分得也越少。 从右图可以看出同一个模型如果通过缩放把边拉长它的细分程度也会随着模型拉长而变大最后保持着一个比较固定的细分密度。 unity官方文档里面的例子是这样的 Shader Tessellation Sample {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 { RenderTypeOpaque }LOD 300CGPROGRAM#pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessEdge nolightmap#pragma target 4.6#include Tessellation.cgincstruct 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 tessEdge (appdata v0, appdata v1, appdata v2)
{return UnityEdgeLengthBasedTess (v0.vertex, v1.vertex, v2.vertex, _EdgeLength);
}UnityEdgeLengthBasedTess 是Unity提供的根据边长细分的方法
2、顶点片段程序实现曲面细分 如果不使用Surface类型的Shader而用传统的顶点片段程序着色器实现曲面细分就只有一种方式就是正常的添加hullProgram、Patch Constant Function和domainProgram然后逐条边和多边形内部指定细分的数量。我这里提供一个最简单的Shader来说明一下写法
Shader azhao/TessVF
{Properties{_MainTex(Texture, 2D) white {}_Color(Color, Color) (1,1,1,1)_EditFactor(edgeFactor, Float) 15_InsideFactor(insideFactor,FLoat) 15}SubShader{Tags { RenderTypeOpaque }LOD 100Pass{CGPROGRAM#pragma vertex vert//在正常的vertex和fragment之间还需要hull和domain所以在这里加上声明#pragma hull hullProgram#pragma domain domainProgram#pragma fragment frag#include UnityCG.cgincsampler2D _MainTex;float4 _MainTex_ST;fixed4 _Color;uniform float _EditFactor;uniform float _InsideFactor;struct a2v{float4 pos : POSITION;float2 uv : TEXCOORD0;};struct v2t{float4 worldPos : TEXCOORD0;float2 uv : TEXCOORD1;};struct t2f{float4 clipPos:SV_POSITION;float2 uv: TEXCOORD0;float4 worldPos:TEXCOORD1;};struct TessOut{float2 uv : TEXCOORD0;float4 worldPos : TEXCOORD1;};struct TessParam{float EdgeTess[3] : SV_TessFactor;//各边细分数float InsideTess : SV_InsideTessFactor;//内部点细分数};v2t vert(a2v i){v2t o;o.worldPos mul(unity_ObjectToWorld,i.pos);o.uv i.uv;return o;}//在hullProgram之前必须设置这些参数不然会报错[domain(tri)]//图元类型,可选类型有 tri, quad, isoline[partitioning(integer)]//曲面细分的过渡方式是整数还是小数[outputtopology(triangle_cw)]//三角面正方向是顺时针还是逆时针[outputcontrolpoints(3)]//输出的控制点数[patchconstantfunc(ConstantHS)]//对应之前的细分因子配置阶段的方法名[maxtessfactor(64.0)]//最大可能的细分段数//vert顶点程序之后调用计算细分前的三角形顶点信息TessOut hullProgram(InputPatchv2t, 3 i, uint idx : SV_OutputControlPointID){TessOut o;o.worldPos i[idx].worldPos;o.uv i[idx].uv;return o;}//指定每个边的细分段数和内部细分段数TessParam ConstantHS(InputPatchv2t, 3 i, uint id : SV_PrimitiveID){TessParam o;o.EdgeTess[0] _EditFactor;o.EdgeTess[1] _EditFactor;o.EdgeTess[2] _EditFactor;o.InsideTess _InsideFactor;return o;}//在domainProgram前必须设置domain参数不然会报错[domain(tri)]//细分之后把信息传到frag片段程序t2f domainProgram(TessParam tessParam, float3 bary : SV_DomainLocation, const OutputPatchTessOut, 3 i){t2f o; //线性转换float2 uv i[0].uv * bary.x i[1].uv * bary.y i[2].uv * bary.z;o.uv uv;float4 worldPos i[0].worldPos * bary.x i[1].worldPos * bary.y i[2].worldPos * bary.z;o.worldPos worldPos;o.clipPos UnityWorldToClipPos(worldPos);return o;}fixed4 frag (t2f i) : SV_Target{// sample the texturefixed4 col tex2D(_MainTex, i.uv)*_Color;return col;}ENDCG}}
}需要注意的地方是 1.声明处理程序
#pragma hull hullProgram
#pragma domain domainProgram2.在hullProgram之前必须设置这些参数不然会报错
[domain(tri)]//图元类型,可选类型有 tri, quad, isoline
[partitioning(integer)]//曲面细分的过渡方式是整数还是小数
[outputtopology(triangle_cw)]//三角面正方向是顺时针还是逆时针
[outputcontrolpoints(3)]//输出的控制点数
[patchconstantfunc(ConstantHS)]//对应之前的细分因子配置阶段的方法名
[maxtessfactor(64.0)]//最大可能的细分段数3.domainProgram前必须设置domain参数不然会报错
[domain(tri)]四、根据范围做局部曲面细分 已经介绍完怎样使用曲面细分了接下来就是要实现文章一开始说的根据指定的中心点和范围做局部的曲面细分。
1、在顶点片段着色器实现局部细分 由于使用顶点片段着色器做曲面细分是可以直接设置每个多边形的边和内部的细分数量所以要实现局部细分也就非常简单了思路是 1.获得中心点坐标和范围半径 2.在着色器取得当前顶点的世界坐标然后判断是否在中心点的半径范围内 3.用一个smoothStep做一个边缘范围过渡作为细分强度 4.根据计算出的细分强度设置最终的细分值。 写成代码大概就是这样
Shader azhao/GroundTessVF
{Properties{_MainTex(Texture, 2D) white {}_Color(Color, Color) (1,1,1,1)_centerPos(CenterPos, Vector) (0,0,0,0)_minVal(minVal, Float) 0_maxVal(maxVal, Float) 10_factor(factor, Float) 15}SubShader{Tags { RenderTypeOpaque }LOD 100Pass{CGPROGRAM#pragma vertex vert//在正常的vertex和fragment之间还需要hull和domain所以在这里加上声明#pragma hull hullProgram#pragma domain domainProgram#pragma fragment frag#include UnityCG.cgincsampler2D _MainTex;float4 _MainTex_ST;fixed4 _Color;uniform float _minVal;uniform float _maxVal;uniform float3 _centerPos;uniform float _factor;struct a2v{float4 pos : POSITION;float2 uv : TEXCOORD0;};struct v2t{float4 worldPos : TEXCOORD0;float2 uv : TEXCOORD1;};struct t2f{float4 clipPos : SV_POSITION;float2 uv : TEXCOORD0;float4 worldPos : TEXCOORD1;};struct TessOut{float2 uv : TEXCOORD0;float4 worldPos : TEXCOORD1;};struct TessParam{float EdgeTess[3] : SV_TessFactor;//各边细分数float InsideTess : SV_InsideTessFactor;//内部点细分数};v2t vert(a2v i){v2t o;o.worldPos mul(unity_ObjectToWorld,i.pos);o.uv i.uv;return o;}//在hullProgram之前必须设置这些参数不然会报错[domain(tri)]//图元类型,可选类型有 tri, quad, isoline[partitioning(integer)]//曲面细分的过渡方式是整数还是小数[outputtopology(triangle_cw)]//三角面正方向是顺时针还是逆时针[outputcontrolpoints(3)]//输出的控制点数[patchconstantfunc(ConstantHS)]//对应之前的细分因子配置阶段的方法名[maxtessfactor(64.0)]//最大可能的细分段数//vert顶点程序之后调用计算细分前的三角形顶点信息TessOut hullProgram(InputPatchv2t, 3 i, uint idx : SV_OutputControlPointID){TessOut o;o.worldPos i[idx].worldPos;o.uv i[idx].uv;return o;}//指定每个边的细分段数和内部细分段数TessParam ConstantHS(InputPatchv2t, 3 i, uint id : SV_PrimitiveID){TessParam o;float4 worldPos (i[0].worldPos i[1].worldPos i[2].worldPos) / 3;float smoothstepResult smoothstep(_minVal, _maxVal, distance(worldPos.xz, _centerPos.xz));float fac max((1.0 - smoothstepResult)*_factor, 1);//由于我这里是根据指定的中心点和半径范围来动态算细分段数所以才有这个计算不然可以直接指定变量来设置。o.EdgeTess[0] fac;o.EdgeTess[1] fac;o.EdgeTess[2] fac;o.InsideTess fac;return o;}//在domainProgram前必须设置domain参数不然会报错[domain(tri)]//细分之后把信息传到frag片段程序t2f domainProgram(TessParam tessParam, float3 bary : SV_DomainLocation, const OutputPatchTessOut, 3 i){t2f o; //线性转换o.worldPos i[0].worldPos * bary.x i[1].worldPos * bary.y i[2].worldPos * bary.z;o.clipPos UnityWorldToClipPos(o.worldPos);float2 uv i[0].uv * bary.x i[1].uv * bary.y i[2].uv * bary.z;o.uv uv;return o;}fixed4 frag (t2f i) : SV_Target{// sample the texturefixed4 col tex2D(_MainTex, i.uv)*_Color;return col;}ENDCG}}
}使用的时候在C#端中心点改变的时候传入centerPos通过调整_maxVal和_minVal可以控制半径和边缘强度渐变的效果
2、在Surface着色器实现局部细分 在Surface着色器里面实现曲面细分需要写的代码很少我们就使用上面介绍的Fixed类型然后同样的通过传入中心点还有_maxVal和_minVal来确定需要细分的范围实现思路和上面的顶点片段着色器是一样的。 代码会是这样的
Shader azhao/FootStepMeshSurface
{Properties{_MainTex(Texture, 2D) white {}_Color (Color, Color) (1,1,1,1)_centerPos(centerPos, Vector) (0,0,0,0)_minVal(minVal, Float) 0_maxVal(maxVal, Float) 10_factor(factor, Float) 15_footstepRect(footstepRect,Vector) (0,0,0,0)_footstepTex(footstepTex,2D) gray{}_height(height ,Float) 0.3_Glossiness(Glossiness,Float) 0_Metallic(Metallic,Float) 0}SubShader{Tags { RenderTypeOpaque }LOD 200CGPROGRAM#include Tessellation.cginc#pragma surface surf Standard fullforwardshadows vertex:vertexDataFunc tessellate:tessFunction #pragma target 4.6struct Input{float2 uv_texcoord;};half _Glossiness;half _Metallic;fixed4 _Color;uniform sampler2D _mainTex;SamplerState sampler_mainTex;uniform float4 _mainTex_ST;uniform float _minVal;uniform float _maxVal;uniform float3 _centerPos;uniform float _factor;float4 _footstepRect;sampler2D _footstepTex;float _height;UNITY_INSTANCING_BUFFER_START(Props)UNITY_INSTANCING_BUFFER_END(Props)float RemapUV(float min, float max, float val){return (val - min) / (max - min);}//这里处理细分相关逻辑float4 tessFunction(appdata_full v0, appdata_full v1, appdata_full v2){float3 worldPos mul(unity_ObjectToWorld, (v0.vertex v1.vertex v2.vertex) / 3);float smoothstepResult smoothstep(_minVal, _maxVal, distance(worldPos.xz, _centerPos.xz));float fac max((1.0 - smoothstepResult)*_factor, 0.1);return fac;}void vertexDataFunc(inout appdata_full v){float4 worldPos mul(unity_ObjectToWorld, v.vertex);float2 footUV float2(RemapUV(_footstepRect.x, _footstepRect.z, worldPos.x), RemapUV(_footstepRect.y, _footstepRect.w, worldPos.z));float4 footstepCol tex2Dlod(_footstepTex, float4(footUV, 0, 0.0));float addVal (footstepCol.r * 2 - 1)*footstepCol.a*_height;v.vertex.y addVal/100;}void surf (Input IN, inout SurfaceOutputStandard o){fixed4 c _Color;float2 uv_mainTex IN.uv_texcoord * _mainTex_ST.xy _mainTex_ST.zw;float4 mainTex tex2D(_mainTex, uv_mainTex);o.Albedo mainTex.rgb*c.rgb;o.Metallic _Metallic;o.Smoothness _Glossiness;o.Alpha c.a;}ENDCG}FallBack Diffuse
}3、选择哪种方式的Shader实现会比较好 这个问题是没有直接答案的需要根据自己的实际情况来选择。 顶点片段着色器的优点是可控性强自己可以随意的定义各种光照模型、修改细节的效果缺点是写法麻烦。 Surface着色器的优点是写法简单缺点是可控性比较弱一点。 我个人是习惯用顶点片段着色器的因为我比较的喜欢自己控制各个环节的细节。所以在接下来的例子里面我还是会用顶点片段着色器的写法来继续做这个地面交互效果的demo。不过其实如果顶点片段着色器上知道了怎样实现在Surface着色器上面实现的过程就更简单了。