IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 游戏开发 -> Unity 草的制作 -> 正文阅读

[游戏开发]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

  游戏开发 最新文章
6、英飞凌-AURIX-TC3XX: PWM实验之使用 GT
泛型自动装箱
CubeMax添加Rtthread操作系统 组件STM32F10
python多线程编程:如何优雅地关闭线程
数据类型隐式转换导致的阻塞
WebAPi实现多文件上传,并附带参数
from origin ‘null‘ has been blocked by
UE4 蓝图调用C++函数(附带项目工程)
Unity学习笔记(一)结构体的简单理解与应用
【Memory As a Programming Concept in C a
上一篇文章      下一篇文章      查看所有文章
加:2021-09-19 08:17:57  更:2021-09-19 08:19:36 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/28 4:26:31-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码