本文的目标是制作一个虽时间波动的正弦波面,效果如下图

首先在unity中创建一个平面,这没什么可说的。
unity中默认的plane面数是很少的,不足以形成一个光滑的波面,所以第一步是进行曲面细分。

如图所示,我们需要先输入原始的顶点数据,unity中会将他写成vertex shader的形式,但是实际上的vertex shader在后面。
在hull program和domain program中我们可以设定细分的规则。曲面细分的具体原理在此不赘述,会用即可。需要注意的是在hullfun中的参数决定了细分的程度,这里我们直接为它赋值,更科学的方式可能是根据镜头的距离设定它的值。
- TessellationFactors hullFun (InputPatch<tessVertexData,3> v) {
- TessellationFactors o;
- o.edge[0] = _TessellationUniform;//设定的参数
- o.edge[1] = _TessellationUniform;
- o.edge[2] = _TessellationUniform;
- o.inside = _TessellationUniform;
- return o;
- }
-
- [UNITY_domain("tri")] 表示适用于三角形,还有quad(四边形)
- [UNITY_outputcontrolpoints(3)]//输出的控制点数量
- [UNITY_outputtopology("triangle_cw")]//输出拓扑结构为顺时针三角形,还有triangle_ccw(逆时针三角形)、line(线段)
- [UNITY_partitioning("fractional_odd")]//分数分割模式,还有integer(整数模式)
- [UNITY_patchconstantfunc("hullFun")]//细分函数
- //hull着色器:定义细分规则
- tessVertexData hul (InputPatch<tessVertexData,3> v, uint id : SV_OutputControlPointID) {
- return v[id];
- }
-
- [UNITY_domain("tri")]
- //domain着色器:计算细分后的顶点位置和数据,同时执行顶点着色器
- v2g dom (TessellationFactors tessFactors, const OutputPatch<tessVertexData,3> vi, float3 bary : SV_DomainLocation) {
- vertexData 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.uv = vi[0].uv*bary.x + vi[1].uv*bary.y + vi[2].uv*bary.z;
- return vert (v);
- }
为了清晰地看到网格,我们将网格以线条的方式渲染出来,渲染的原理是用几何着色器计算出每个三角网格的重心,根据三角形的重心坐标到三顶点的最小距离插值颜色。
- [maxvertexcount(3)]
- //几何着色器
- void geo (
- triangle v2g v[3],
- inout TriangleStream<g2f> tStream
- ) {
- float4 barycenter = (v[0].vertex + v[1].vertex + v[2].vertex)/3;
- float3 normal = (v[0].normal + v[1].normal + v[2].normal)/3;
-
- v[0].normal = normal;
- v[1].normal = normal;
- v[2].normal = normal;
-
- g2f g0, g1, g2;
- g0.data = v[0];
- g1.data = v[1];
- g2.data = v[2];
-
- //
- g0.barycentricCoordinates = float3(0, 0, 1);
- g1.barycentricCoordinates = float3(0, 1, 0);
- g2.barycentricCoordinates = float3(1, 0, 0);
-
- tStream.Append(g0);
- tStream.Append(g1);
- tStream.Append(g2);
- tStream.RestartStrip();
- }
-
-
- fixed4 frag (g2f i) : SV_Target
- {
- fixed4 col = tex2D(_MainTex, i.data.uv);
- float3 barys = i.barycentricCoordinates;
- float3 deltas = fwidth(barys);
- float3 smoothing = deltas * _WireframeSmoothing;
- float3 thickness = deltas * _WireframeThickness;
- barys = smoothstep(thickness, thickness + smoothing, barys);
- float minBary = min(barys.x, min(barys.y, barys.z));
-
- return float4(lerp(_WireframeColor, col, minBary),1);//
- return col;
- }
- ENDCG
- }
以上部分参考了b站lyh萌主的文章https://www.bilibili.com/read/cv16290237
如标题所说,我们要用到一个随时间变化的位移贴图,显然它需要实时生成。可以选择在脚本的update函数中每次创建新的纹理,然后一个一个像素填充它。如果你这样做会发现帧率非常低,尤其当CPU性能不强时,因为CPU不擅长做这种并行计算。所以我们把这个任务交给GPU,这就用到了compute shader。
虽然都是shader,但是unity中compute shader是用以 DirectX 11 样式 HLSL 语言编写的,所以与其他shader写法有所不同,也不在常规渲染管线中。
下图是默认的compute shader,它包含了最重要的几部分。
#pragma kernel CSMain这一句可以理解为声明了CS中的某个函数(函数名为CSMain)。
RWTexture2D
一般我们shader通常是只读的,大多使用的是sampler2D,然后通过tex2D函数已经UV坐标访问,但RWTexture2D的访问是直接通过Result[uint2(0,0)]来访问,值为float4型。
由于这个纹理是需要读和写的,所以需要使用render texture,而不能是Texture2D。它的创建方式如下,注意需要开启它的读写,并调用create方法。
- public RenderTexture Displace;
- ...
- void Start()
- {
- Displace = new RenderTexture(1024, 1024,0, RenderTextureFormat.ARGBFloat);
- Displace.enableRandomWrite = true;
- Displace.Create();
- ...
- }
[numthreads(8,8,1)]表示一个线程组的线程数量,即8*8*1,线程组的设定会影响计算效率,不过具体我也不太懂,有大佬懂得希望不吝赐教。在这我们让它保持默认。
最后是核函数,重要的是它可以有几个输入的参数
SV_GroupID:线程组的id
SV_GroupIndex:即在每一个线程组元素里,线程的索引,[numthreads(8,8,1)],则索引范围(0, 0, 0) - (8, 8, 0),
SV_DispatchThreadID:这个就是全局唯一的id,可以理解为一张图片的每个像素坐标
所以上图中id.x id.y分别代表纹理上某个像素的横纵坐标。
写完了一个CS,接下来就是如何使用它。
- public class createTexture : MonoBehaviour
- {
- public ComputeShader cshader;
- private int kernelHandle;
-
- ...
-
- void Start()
- {
- ...
- kernelHandle = cshader.FindKernel("CSMain");
- }
-
- void Update()
- {
- cshader.SetTexture(kernelHandle, "Result", Displace);
- cshader.Dispatch(kernelHandle, 1024 / 8, 1024 / 8, 1);
- ...
- }
- }
我们需要定义一个compute shader,并为它的核函数定义一个索引(int类型)
将一个render texture传入作为RWTexture2D。
最后根据我们传入的texture大小(1024*1024)调用dispatch方法启动运算
这部分参考了博文
下面是实际用到的CS,我将位移量保存在纹理的R通道中,注意其中的常量PI最好使用宏定义
- #pragma kernel CSMain
- #define PI 3.14159274f
-
- RWTexture2D
Result; -
- float time;
- [numthreads(8,8,1)]
- void CSMain (uint3 id : SV_DispatchThreadID)
- {
- Result[id.xy] = float4((cos(id.x/ 1024.0f * 2 * PI*5+time) + cos(id.y/1024.0f * 2 * PI*5+time)) * 0.25f + 0.5f, 0, 0, 1);
- }
最后就是把位移贴图应用到渲染中。
将compute shader计算好的texture传入shader,命名为_DisplaceTex,读出它的R通道,计算位移并加在法线方向上。
注意这里对纹理的采样必须用tex2Dlod(),因为tex2D不能用在顶点着色器里(不知原因)。
- v2g vert(vertexData v)
- {
- ...
- float d = pow( tex2Dlod(_DisplaceTex, float4(v.uv.xy, 0, 0)).r, _Power) * _Displacement;
- v.vertex.xyz += v.normal * d;
- ...
- }