Unity 草的制作
Posted CZandQZ
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity 草的制作相关的知识,希望对你有一定的参考价值。
如果把草当成一个一个的模型的话,我们在一个平面上铺满10000个草并且让他和一些物体进行交互的,如果用传统的做法,我们把每一个草上面挂载一个脚本的话,运行的时候你就会发现,这样帧率其实并不高的,在一个update里面每帧访问一个数组长度为10000的数组他的帧率其实都不会很高的,更何况我们还需要草动起来且还需要有一定的交互能力,如果纯粹用cpu来模拟的话可能会比较吃力,所以这里打算用GPU来模拟大量粒子运动和交互。unity提供了一种compute shader来处理大量计算。GPU在大量运算时候它的效率是远远比CPU高好多倍的,所以一般在做游戏的时候我们其实是大量的使用了CPU而GPU的使用效率其实比较低的往往一个游戏中GPU很闲CPU很忙,首先贴出我们需要做出来的效果。
因为截图的缘故 看着草没动 其实草会摆动的。这个就是我们最终的效果了,首先这个效果模型我们是没有的所以草的Mesh我们得重新定义
最终得网格就如图所示,蓝色代表顶点,绿色边构成得三角形代表网格顶点绘制得顺序。顶点的添加顺序是从下到上,从左到右,所以顶点的下标依次从0到6,所以顶点的添加顺序为
0,2,3 , 0,3,1, 2,4,5 , 2,5,3 ,4,6,5。接下来就是草的摆动的及擦除效果就用computer shader来处理
具体的shader只是来接受computer shader处理完了的数据来显示出来,所以先写shader在写compute shader。
Shader "Custom/Grass"{
Properties
{
_Color("Color Tint",Color) = (1.0,1.0,1.0,1.0)
}
SubShader
{
Pass
{
Tags { "RenderType"="Opaque" }
Cull Off
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
#pragma instancing_options procedural:setup
#pragma target 3.5
#if SHADER_TARGET >= 35 && (defined(SHADER_API_D3D11) || defined(SHADER_API_GLES3) || defined(SHADER_API_GLCORE) || defined(SHADER_API_XBOXONE) || defined(SHADER_API_PSSL) || defined(SHADER_API_SWITCH) || defined(SHADER_API_VULKAN) || (defined(SHADER_API_METAL) && defined(UNITY_COMPILER_HLSLCC)))
#define SUPPORT_STRUCTUREDBUFFER
#endif
#if defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) && defined(SUPPORT_STRUCTUREDBUFFER)
#define ENABLE_INSTANCING
#endif
uniform fixed4 _Color;
float4x4 _LocalToWorld;
float4x4 _WorldToLocal;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
uint vid : SV_VertexID;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 pos : SV_POSITION;
fixed3 color : COLOR0;
uint fid: UINT;
};
StructuredBuffer<float3> _PositionBuffer;
StructuredBuffer<float3> _VerticlesBuffer;
StructuredBuffer<int> _DiscardBuffer;
void setup()
{
unity_ObjectToWorld = _LocalToWorld;
unity_WorldToObject = _WorldToLocal;
}
v2f vert(a2v v,uint instanceID : SV_InstanceID)
{
v2f o;
o.fid = instanceID;
float3 offset = float3(0,0,0);
offset = _PositionBuffer[instanceID];
int id = instanceID * 7 + v.vid;
float3 p = _VerticlesBuffer[id];
o.pos = UnityObjectToClipPos(p + float4(offset.x,offset.y,offset.z,0));
float4 bColor = float4(74.0/255.0,192.0/255.0,74.0/255.0,1);
float4 uColor = float4(210.0/255.0,224.0/255.0,39.0/255.0,1);
o.color = lerp(bColor,uColor,v.texcoord.y);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
if(_DiscardBuffer[i.fid] == 0)
{
discard;
}
return fixed4(i.color,1.0);
}
ENDCG
}
}
}
shader中
StructuredBuffer<float3> _PositionBuffer;
StructuredBuffer<float3> _VerticlesBuffer;
StructuredBuffer<int> _DiscardBuffer;
StructuredBuffer这个定义可以理解为数组,_PositionBuffer主要是用来存储所有草的位置,_VerticlesBuffer主要用来存储所有所有草上的顶点位置(这个位置为本地坐标),_DiscardBuffer主要用来存储所有草的显示状态,为0代表不显示,为1代表显示。上图有一个顶点绘制顺序图 假定那张图上的顶点在unity坐标xy平面,那么现在弯曲的话可以想象除了1,2 3 4顶点外 5 6 7顶点会向着z轴弯曲,第一步 3 4 5 6 7所构成的一个大的三角面先向着z轴弯曲,然后 5 6 7构成的三角面再向着z轴弯曲。shader里面旋转的话得单独写,下面一段代码是旋转得。
float4 GetQuaternion(float3 rotateAxis,float angle)
{
float thetaOver2 = angle * 0.5;
float sinthetaOver2 = sin(thetaOver2);
float w = cos(thetaOver2);
float x = rotateAxis.x * sinthetaOver2;
float y = rotateAxis.y * sinthetaOver2;
float z = rotateAxis.z * sinthetaOver2;
return float4(x,y,z,w);
}
float3 QuaternionXDir(float3 p,float3 axis,float angle)
{
float4 rotation = GetQuaternion(axis,angle);
float num1 = rotation.x * 2;
float num2 = rotation.y * 2;
float num3 = rotation.z * 2;
float num4 = rotation.x * num1;
float num5 = rotation.y * num2;
float num6 = rotation.z * num3;
float num7 = rotation.x * num2;
float num8 = rotation.x * num3;
float num9 = rotation.y * num3;
float num10 = rotation.w * num1;
float num11 = rotation.w * num2;
float num12 = rotation.w * num3;
float x = ((1.0 - (num5 + num6)) * p.x + (num7 - num12) * p.y + ( num8 + num11) * p.z);
float y = ((num7 + num12) * p.x + (1.0 - (num4 + num6)) * p.y + (num9 - num10) * p.z);
float z = ((num8 - num11) * p.x + (num9 + num10) * p.y + (1.0 - (num4 + num5)) * p.z);
return float3(x,y,z);
}
上面代码得功能主要是一个向量绕着一个轴旋转特定得角度。首先创建compute shader得时候会有一个内核,这里将其定义为
[numthreads(256,1,1)]
void Grass (uint id : SV_DispatchThreadID)
方法中的id可以理解为每棵草在一个草数组中的索引,在这个内核里面我们需要计算出每一个草的顶点相对位置这个位置是一个相对位置而不是绝对位置。首先假定最下面的坐标为a1和a2,假定a1和a2的距离为1,定义a3和a4的距离为0.5,定义dir12为a1-a2,dir34为a3-a4这里dir12和dir34向量是平行的 这里假定dir12到dir34向量的距离为0.1,dir12和dir34所在平面垂直z轴,所以我们额可以求出a3和a4顶点坐标。
那么后面的节点就依次类推了所以这里我就直接贴出这个后面顶点的计算方式了。
#pragma kernel Grass
#include "UnityCG.cginc"
RWStructuredBuffer<float3> PositionBuffer;
RWStructuredBuffer<float3> DirBuffer;
RWStructuredBuffer<float3> VerticlesBuffer;
RWStructuredBuffer<int> DiscardBuffer;
CBUFFER_START(Params)
float4 EraserPos;
float TotlaTime;
CBUFFER_END
float4 GetQuaternion(float3 rotateAxis,float angle)
{
float thetaOver2 = angle * 0.5;
float sinthetaOver2 = sin(thetaOver2);
float w = cos(thetaOver2);
float x = rotateAxis.x * sinthetaOver2;
float y = rotateAxis.y * sinthetaOver2;
float z = rotateAxis.z * sinthetaOver2;
return float4(x,y,z,w);
}
float3 QuaternionXDir(float3 p,float3 axis,float angle)
{
float4 rotation = GetQuaternion(axis,angle);
float num1 = rotation.x * 2;
float num2 = rotation.y * 2;
float num3 = rotation.z * 2;
float num4 = rotation.x * num1;
float num5 = rotation.y * num2;
float num6 = rotation.z * num3;
float num7 = rotation.x * num2;
float num8 = rotation.x * num3;
float num9 = rotation.y * num3;
float num10 = rotation.w * num1;
float num11 = rotation.w * num2;
float num12 = rotation.w * num3;
float x = ((1.0 - (num5 + num6)) * p.x + (num7 - num12) * p.y + ( num8 + num11) * p.z);
float y = ((num7 + num12) * p.x + (1.0 - (num4 + num6)) * p.y + (num9 - num10) * p.z);
float z = ((num8 - num11) * p.x + (num9 + num10) * p.y + (1.0 - (num4 + num5)) * p.z);
return float3(x,y,z);
}
[numthreads(256,1,1)]
void Grass (uint id : SV_DispatchThreadID)
{
float length1 = 0.3;
float width = 0.03;
float3 upDir = float3(0,1,0);
float3 p1 = - DirBuffer[id] * width *3;
float3 p2 = DirBuffer[id] * width *3;
float3 p3 = - DirBuffer[id] * width*2 + upDir * length1;
float3 p4 = DirBuffer[id] * width*2 + upDir * length1;
float3 p34 = (p3 + p4) / 2;
float3 o = float3(-7, 0 ,7) - PositionBuffer[id];
float d = o.x*o.x + o.y*o.y + o.z*o.z;
float angle = 5 * (sin(TotlaTime+ d) + sin(2*TotlaTime+d));
float3 upDir1 = QuaternionXDir(upDir, DirBuffer[id], angle * 0.01745329);
float3 normal1 = cross(DirBuffer[id] , upDir1);
float dir1 = QuaternionXDir(upDir1, normal1, -90 * 0.01745329);
float3 p5 = p34 - DirBuffer[id] * width + upDir1 * length1 * 2;
float3 p6 = p34 + DirBuffer[id] * width + upDir1 * length1 * 2;
float3 p56 = (p5 + p6) / 2;
float3 upDir2 = QuaternionXDir(upDir1,dir1, 10 * 0.01745329);
float3 p7 = p56 + upDir2 * length1 * 3;
VerticlesBuffer[0+id * 7] = p1;
VerticlesBuffer[1+id * 7] = p2;
VerticlesBuffer[2+id * 7] = p3;
VerticlesBuffer[3+id * 7] = p4;
VerticlesBuffer[4+id * 7] = p5;
VerticlesBuffer[5+id * 7] = p6;
VerticlesBuffer[6+id * 7] = p7;
float3 offset = float3(EraserPos.x,EraserPos.y,EraserPos.z) - PositionBuffer[id];
float dis = offset.x * offset.x + offset.y * offset.y + offset.z*offset.z;
if(dis < 0.5 * 0.5)
{
DiscardBuffer[id] = 0;
}
}
最后一段代码是判断擦除的,计算方式就一定半径内擦除这种计算方式应该比较简单这里不做详细叙述了。最后就是渲染方法了——Graphics.DrawMeshInstancedIndirect,api具体的参数我就不做赘述了。比较简单这里就直接贴出脚本吧,脚本里面就是cpu和gpu交互的代码,compute计算出来一些数据然后传给shader,最后把它渲染出来,所以这个类就是一个统筹所有资源的一个类。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
public class Grass : MonoBehaviour
{
[SerializeField] private float _width;
[SerializeField] private float _height;
[SerializeField] private Transform _eraser;
[SerializeField] private Material _grassMaterial;
[SerializeField] private ComputeShader _grassCompute;
[SerializeField] private int _grassCount;
private uint[] args = new uint[5] { 0, 0, 0, 0, 0 };
private ComputeBuffer _drawArgsBuffer;
private ComputeBuffer _positionBuffer;
private ComputeBuffer _dirBuffer;
private ComputeBuffer _verticlesBuffer;
private ComputeBuffer _discardBuffer;
private Mesh _originMesh;
private MaterialPropertyBlock _props;
private bool _isSet;
private int[] _discardInts;
private Vector3[] _posList;
private float _totalTime;
private static readonly int KThreadCount = 256;
private void Start()
{
_props = new MaterialPropertyBlock();
_props.SetFloat("_UniqueID", Random.value);
InitiMesh();
InitiBuff();
_isSet = false;
}
private void Update()
{
var kernel = _grassCompute.FindKernel("Grass");
int groupX = _grassCount / KThreadCount;
_totalTime += Time.deltaTime * 2f;
if (_totalTime >= Mathf.PI * 2)
_totalTime -= Mathf.PI * 2;
if (!_isSet)
{
_grassCompute.SetBuffer(kernel, "PositionBuffer", _positionBuffer);
_grassCompute.SetBuffer(kernel, "DirBuffer", _dirBuffer);
_grassCompute.SetBuffer(kernel, "VerticlesBuffer", _verticlesBuffer);
_grassCompute.SetBuffer(kernel, "DiscardBuffer", _discardBuffer);
args[0] = (uint)_originMesh.GetIndexCount(0);
args[1] = (uint)_grassCount;
args[2] = (uint)_originMesh.GetIndexStart(0);
args[3] = (uint)_originMesh.GetBaseVertex(0);
_drawArgsBuffer.SetData(args);
_isSet = true;
_grassMaterial.SetMatrix("_LocalToWorld", transform.localToWorldMatrix);
_grassMaterial.SetMatrix("_WorldToLocal", transform.worldToLocalMatrix);
_grassMaterial.SetBuffer("_PositionBuffer", _positionBuffer);
_grassMaterial.SetBuffer("_VerticlesBuffer", _verticlesBuffer);
}
Vector3 t = _eraser.position;
_grassCompute.SetFloat("TotlaTime", _totalTime);
_grassCompute.SetVector("EraserPos", new Vector4(t.x, t.y, t.z, 0));
_grassCompute.Dispatch(kernel, groupX, 1, 1);
_grassMaterial.SetBuffer("_DiscardBuffer", _discardBuffer);
Graphics.DrawMeshInstancedIndirect(
_originMesh, 0, _grassMaterial,
new Bounds(transform.position, transform.lossyScale * 5),
_drawArgsBuffer
);
}
private void OnDestroy()
{
_drawArgsBuffer.Release();
_positionBuffer?.Release();
_dirBuffer?.Release();
_verticlesBuffer?.Release();
_discardBuffer?.Release();
}
private void InitiBuff()
{
_drawArgsBuffer = new ComputeBuffer(_grassCount, 5 * sizeof(uint), ComputeBufferType.IndirectArguments);
if (_positionBuffer == null)
{
_posList = new Vector3[_grassCount];
float minWidht = -_width / 2f;
float maxWidth = _width / 2f;
float minHight = -_height / 2f;
float maxHeight = _height / 2f;
for (int i = 0; i < _grassCount; i++)
{
float x = Random.Range(minWidht, maxWidth);
float z = Random.Range(minHight, maxHeight);
_posList[i] = new Vector3(x, 0, z);
}
_positionBuffer = new ComputeBuffer(_grassCount, 4 * 3);
_positionBuffer.SetData(_posList);
}
if (_dirBuffer == null)
{
List<Vector3> dirList = new List<Vector3>();
for (int i = 0; i < _grassCount; i++)
{
Vector2 t1 = Random.insideUnitCircle;
dirList.Add(new Vector3(t1.x, 0, t1.y).normalized);
}
_dirBuffer = new ComputeBuffer(_grassCount, 4 * 3);
_dirBuffer.SetData(dirList);
}
if (_verticlesBuffer == null)
{
_verticlesBuffer = new ComputeBuffer(_grassCount * 7, 4 * 3);
}
if (_discardBuffer == null)
{
_discardBuffer = new ComputeBuffer(_grassCount, 4);
_discardInts = new int[_grassCount];
for (int i = 0; i < _grassCount; i++)
{
_discardInts[i] = 1;
}
_discardBuffer.SetData(_discardInts);
}
}
private void InitiMesh()
{
_originMesh = new Mesh();
_originMesh.vertices = new Vector3[7];
_originMesh.SetUVs(0, new List<Vector2>()
{
new Vector2(0,0), new Vector2(1,0),
new Vector2(1/6f,1/3f), new Vector2(5/6f,1/3f),
new Vector2(2/6f,2/3f), new Vector2(4/6f,2/3f),
new Vector2(0.5f,1f)
});
_originMesh.SetIndices(new []
{
0,2,3 , 0,3,1, 2,4,5 , 2,5,3 ,4,6,5
},MeshTopology.Triangles,0);
_originMesh.UploadMeshData(true);
}
}
好了草就已经做完了,后面有什么问题可以联系本人qq:1850761495
以上是关于Unity 草的制作的主要内容,如果未能解决你的问题,请参考以下文章