Desc: 草地编辑器之缝合怪
效果
草的绘制 上一篇中,已经得到了需要绘制的草的信息,包括顶点、法线、顶点色等,其中顶点由一个 Vector3 存储,可以通过CPU计算出,草对应的顶点位置,传递给GPU,也可以通过计算着色器的方式,让GPU去计算草的顶点位置信息
脚本通过 ComputeBuffer 将数据传递给计算着色器
ComputeBuffer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private ComputeBuffer m_sourceVertexBuffer;private ComputeBuffer m_drawBuffer;private ComputeBuffer m_argsBuffer;... m_sourceVertexBuffer = new ComputeBuffer(vertices.Length, Source_Vert_Stride, ComputeBufferType.Structured, ComputeBufferMode.Immutable); m_sourceVertexBuffer.SetData(vertices); m_drawBuffer = new ComputeBuffer(numSourceVertices * maxBladeTriangles, Draw_Stride, ComputeBufferType.Append); m_drawBuffer.SetCounterValue(0 ); m_argsBuffer = new ComputeBuffer(1 , Indirect_Args_Stride, ComputeBufferType.IndirectArguments); m_idGrassKernel = m_instantiatedComputeShader.FindKernel("Main" ); m_instantiatedComputeShader.SetBuffer(m_idGrassKernel, "_SourceVertices" ,m_sourceVertexBuffer); m_instantiatedComputeShader.SetBuffer(m_idGrassKernel, "_DrawTriangles" , m_drawBuffer); m_instantiatedComputeShader.SetBuffer(m_idGrassKernel, "_IndirectArgsBuffer" , m_argsBuffer); m_instantiatedMaterial.SetBuffer("_DrawTriangles" , m_drawBuffer);
更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private void LateUpdate () { m_drawBuffer.SetCounterValue (0 ); m_argsBuffer.SetData (argsBufferReset); SetGrassDataUpdate (); m_instantiatedComputeShader.Dispatch (m_idGrassKernel, m_dispatchSize, 1 , 1 ); Graphics.DrawProceduralIndirect (m_instantiatedMaterial, bounds, MeshTopology.Triangles, m_argsBuffer, 0 , null, null, castShadow, true , gameObject.layer); }
ComputeBuffer 缓冲对象(Buffer Object)传递:缓冲对象是一种可以在GPU端存储数据的对象,它可以用来传递一些较大的数据结构,如矩阵、数组等。在Shader中,可以通过glBindBuffer和glVertexAttribPointer函数绑定和传递缓冲对象
在Unity中,缓冲对象(Buffer Object)通常指的是ComputeBuffer。ComputeBuffer是一种可以在GPU端存储数据的对象,它可以用来传递一些较大的数据结构,如矩阵、数组等,以及用于计算着色器的数据。ComputeBuffer可以在Unity中使用ComputeShader进行创建和绑定,可以通过SetBufferData方法和GetData方法来传递数据和获取数据
除了ComputeBuffer,Unity中还有其他类型的缓冲对象,如GraphicsBuffer、VertexBuffer、IndexBuffer等,这些缓冲对象通常用于渲染过程中,用于传递顶点数据、索引数据等。在Unity中,可以通过Mesh类的SetVertexBufferParams和SetIndexBufferParams方法来创建和绑定这些缓冲对象
计算着色器中数据的创建
SourceVertices
1 2 3 4 5 6 7 struct SourceVertex{ float3 positionOS; float3 normalOS; float2 uv; float3 color; };
上一步中存储的顶点信息,由C#脚本传递给 Compute Shader,cs中根据顶点数据进而创建相关草的顶点信息
DrawTriangle
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct DrawVertex{ float3 positionWS; float2 uv; float3 color; }; struct DrawTriangle{ float3 normalOS; DrawVertex vertices[3 ]; };
计算着色器中返回给Shader的数据结构,代表着一个三角面的数据,包括法线、世界空间坐标、顶点色信息.三角面的三个顶点法线是一致的
Data Create
来源 每个位置去设置相应的叶片、草数量信息
写入缓存中
1 2 3 4 5 6 7 8 9 10 11 12 13 AppendStructuredBuffer<DrawTriangle> _DrawTriangles; ... for (int k = 0 ; k < numTrianglesPerBlade; ++k){ DrawTriangle tri = (DrawTriangle)0 ; tri.normalOS = faceNormal; tri.vertices[0 ] = drawVertices[k]; tri.vertices[1 ] = drawVertices[k + 1 ]; tri.vertices[2 ] = drawVertices[k + 2 ]; _DrawTriangles.Append(tri); }
InterlockedAdd(_IndirectArgsBuffer[0].numVerticesPerInstance, numTrianglesPerBlade * numBladesPerVertex * 3);
Shader中的计算
数据结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 struct DrawVertex{ float3 positionWS; float2 uv; float3 diffuseColor; }; struct DrawTriangle{ float3 normalOS; DrawVertex vertices[3 ]; }; StructuredBuffer<DrawTriangle> _DrawTriangles;
顶点着色器中数据的读取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 v2f vert (uint vertexID : SV_VertexID ) { v2f output = (v2f)0 ; DrawTriangle tri = _DrawTriangles[vertexID / 3 ]; DrawVertex input = tri.vertices[vertexID % 3 ]; output.positionCS = UnityWorldToClipPos(input.positionWS); output.positionWS = input.positionWS; float3 faceNormal = tri.normalOS; output.normalWS = UnityObjectToWorldNormal(faceNormal); output.uv = input.uv; output.diffuseColor = input.diffuseColor; return output; }
像素着色器部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 void UpdateTex (){ camToDrawWith.enabled = true ; camToDrawWith.targetTexture = tempTex; Shader.SetGlobalTexture("_TerrainDiffuse" , tempTex); } public void DrawDiffuseMap (){ DrawToMap("_TerrainDiffuse" ); } void DrawToMap (string target ){ camToDrawWith.enabled = true ; camToDrawWith.targetTexture = tempTex; camToDrawWith.depthTextureMode = DepthTextureMode.Depth; Shader.SetGlobalFloat("_OrthographicCamSize" , camToDrawWith.orthographicSize); Shader.SetGlobalVector("_OrthographicCamPos" , camToDrawWith.transform.position); camToDrawWith.Render(); Shader.SetGlobalTexture(target, tempTex); camToDrawWith.enabled = false ; } fixed4 frag (v2f i ) : SV_Target { ... _TopTint = _TopTint * ambient; terrainForBlending = lerp(terrainForBlending,terrainForBlending+ ( _TopTint* float4(i.diffuseColor, 1 )) , verticalFade); final = terrainForBlending; return _MainColor; }
采样地表颜色进行混合 采样地表的颜色,通过创建一个摄像机,通过采样指定层,得到一张RT贴图,Shader中对于RT贴图进行采样,与草的颜色进行混合
靠近相机时按比例缩小草 Twitter
你的浏览器不支持播放该视频
为了让面片两面显示,需要将剔除关闭
OverDraw问题 大批量草地通常会引发大量的 overdraw,因为草地由许多小片叶子组成,每个叶子都需要渲染,这会导致大量的重叠和 overdraw 问题
解决方案 Early-Z和Z-Prepass
1: 前面的像素可能被clip掉,但是深度依旧写入,会导致后面深度测试不通过
3: 透明度混合开启后的物体一般不会写入深度,Early-Z因此无法生效
由近及远排序会导致CPU计算量增大,而且会破坏和批,使用Z-Prepass的方法解决
在第一个pass后,就可以得到一个深度缓存,对应和摄像机的最小距离 存在的问题:一个物体有多个pass会破坏动态和批
对于数量多的不透明物体需要节省overdraw时,会考虑用到这种技术