闲云野鹤:吃鸡(一)之场景制作:使用GPU instancing方式制作刷草插件

article/2025/5/15 4:50:51

GPU instancing方式制作刷草插件(unity版本8.2.2)

先上最终效果图(欢迎加我qq交流:358641634):
十种草 混刷生成比较自然的场景(带阴影、风力、草可见距离可调)
插件编辑界面(草密度、范围、体积大小等等可调)
一键快刷(带空白过滤更自然)
视椎体剪裁(提高效率)
Profiler效率测试(满屏1920X920:63787株草、167帧、21Batches、21setPass calls )
为何要制作这个刷草插件?因为吃鸡类的次时代场景要求真实且效率要高,但感觉unity自带的刷草功能不太满足需要,例如,场景较大,达到十几平方公里,草的有效视距也较大,质量要求较高,但unity自带的刷草功能感觉效果不真实,也无阴影等效果,也不满足大视距要求,在开镜下的最大视距也只有250米。至于unity引擎自带的树木实测只要不是大片树木效率勉强还行。
也许使用一些unity付费插件的话可能可以很好解决问题,但我的想法还是自己动手做一个插件,主要目的还是为了学习和研究而不是做一款商用游戏。
于是决定:1、树木:使用unity引擎自带的树木,因为实测只要不是大片森林,效率还过得去。其实对战的场景如果树木遮挡太多对可玩性会有影响。2、地形:仍然使用unity自带的地形。因为自unity8.3版本以后地形已经升级了,据官方称效率可以提高50%,想必应该满足要求了,不过我现在用的是8.2的版本,以后升级就ok,不必再费心思。3、花草:自己做插件。花草是对效率影响比较大的,也是感觉unity引擎的不足之处,先是想到用unity的制作树木方式来制作花草,但是毕竟花草和树木不一样,数量大了肯定卡得不行。于是决定自己先做一个插件,使用GPU instancing方式,这种方式刷草很符合大量相似小物体的渲染情景,不需要实例化草物体和多次从CPU提交GPU数据,相当于将大片草合批后整体提交给GPU,然后一次性渲染,减少了CPU和GPU之间的信息交换,且GPU的运算速度较CPU要高得多,能轻松搞定同屏100万面渲染而帧率保持在100帧以上,关于GPU instancing技术参考官网 https://docs.unity3d.com/Manual/GPUInstancing.html。

        目标:用GPU instancing方式实现刷草功能,能实现阴影、视椎体裁剪、自定义shader、自定义mesh、风力效果、自定义草可见距离、大量草一次性渲染。

        思路:在编辑状态下(非运行状态)对Scene视图的Terrain刷草,先将地形划成很多小块,刷草时自动将每块草的位置(旋转等)数据保存在磁盘上,然后在运行状态下读出这些位置数据,同时将草的材质、网格数据一并提交给GPU进行运算,并根据视椎体剔除看不见的草数据以提高渲染效率,最后再渲染出来。

        希望:由于本人是第一次做插件,诚盼高手和大佬指点,特别是关于思路和实现细节,先谢谢了!

先贴出一些具体问题的解决方式,供大家一起探讨:

1、屏蔽鼠标在Scene视图的框选作用。也就是说鼠标左键按下并拖动时是种草而不是框选物体,所以要屏蔽鼠标对物体的选择功能。
主要代码:
    public void OnEnable(){SceneView.onSceneGUIDelegate -= OnSceneGUI;SceneView.onSceneGUIDelegate += OnSceneGUI;}void OnSceneGUI(SceneView sceneView){int controlID = GUIUtility.GetControlID(FocusType.Passive);//使scene视图不能选择物体HandleUtility.AddDefaultControl(controlID);}

 

2、在刷草的时候首先将地形自动成多块以便提高数据处理以及存储的速度,也就是说仅更新所刷那一块的数据并保存,没有刷到的就不处理,也不更新储存的数据,提高了效率。 

主要代码:

for (int i = 0; i < n; i++)//每帧都绘制{UpdateBuffers(i);if (virtuTerrain.perPiece[i].pos.Count == 0 || viewAble == false)continue;//仅渲染有数据的草类instanceMaterial[i].SetFloat("_WindSpeed", windSpeed);Graphics.DrawMeshInstancedIndirect(currentMesh, subMeshIndex, instanceMaterial[i],new Bounds(Vector3.zero, new Vector3(2000.0f, 2000.0f, 2000.0f)), argsBuffers[i]);}SceneView.RepaintAll();}void UpdateBuffers(int number)//某块有草的数量变化时只更新那一块的数据{if (virtuTerrain.perPiece[number].pos.Count == 0) return;if (cachedInstanceCount[number] == virtuTerrain.perPiece[number].pos.Count)//判断数据有无更新{instanceMaterial[number].SetBuffer("positionBuffer", positionBuffer[number]);return;//数据无更新则返回}subMeshIndex = Mathf.Clamp(subMeshIndex, 0, currentMesh.subMeshCount - 1);if (positionBuffer[number] != null)positionBuffer[number].Release();positionBuffer[number] = new ComputeBuffer(virtuTerrain.perPiece[number].pos.Count, 16);instanceMaterial[number].SetBuffer("positionBuffer", positionBuffer[number]);positionBuffer[number].SetData(virtuTerrain.perPiece[number].pos);args[0] = (uint)currentMesh.GetIndexCount(subMeshIndex);args[1] = (uint)virtuTerrain.perPiece[number].pos.Count;args[2] = (uint)currentMesh.GetIndexStart(subMeshIndex);args[3] = (uint)currentMesh.GetBaseVertex(subMeshIndex);argsBuffers[number].SetData(args);cachedInstanceCount[number] = virtuTerrain.perPiece[number].pos.Count;}

 

3、在编辑状态下(非运行状态)对Scene视图的Terrain刷草,需要用到编辑器模式,但在编辑模式下对scene的操作,其刷新性能不佳很卡,于是想到了一个方法,但是这个方法需要scene场景中有运动物体才会不断更新,因此就有了编辑器放置一个物体在scene中并旋转,解决了scene场景更新慢的问题。(代码较简单,略)

4、渲染多种草:播放状态时unity每个时间周期内最多可同时渲染10种草。  

主要代码:      

for (int i = 0; i < KindOfGrass; i++){if (xyzwsList[i].Length < 1) continue;bs = grassDataCount[i] / 64;bs = bs == 0 ? 1 : bs;positionAppendBuffer[i].SetCounterValue(0);positionComputeShader.SetFloat("viewDistance", viewDistance * viewDistance);if (lastAspect != camera.aspect){w = Mathf.Tan(camera.fieldOfView * 0.5f * Mathf.Deg2Rad) * farClipPlane * camera.aspect;lastAspect = camera.aspect;}Vector3 vR =farClipPlane * transform.forward + w * transform.right;Vector3 vL =farClipPlane * transform.forward - w * transform.right;Vector3 RightPlane_N = Vector3.Cross(vR,transform.up);//视椎体右平面法线Vector3 LeftPlane_N = Vector3.Cross(transform.up,vL);//视椎体左平面法线positionComputeShader.SetVector("Centerposition", transform.position-transform.forward*3);//视椎体顶点(退后3米)positionComputeShader.SetVector("RightPlane_N", RightPlane_N);positionComputeShader.SetVector("LeftPlane_N", LeftPlane_N);positionComputeShader.SetBuffer(positionComputeKernelId, "positionListBuffer", positionListBuffer[i]);positionComputeShader.SetBuffer(positionComputeKernelId, "positionBuffer", positionAppendBuffer[i]);instanceMaterial[i].SetBuffer("positionBuffer", positionAppendBuffer[i]);positionComputeShader.Dispatch(positionComputeKernelId, bs, 1, 1);ComputeBuffer.CopyCount(positionAppendBuffer[i], argsBuffers[i], 4);Graphics.DrawMeshInstancedIndirect(instanceMesh[i], 0, instanceMaterial[i], instanceMesh[i].bounds, argsBuffers[i], 0, null, castShadows, receiveShadows);}

 

5、视椎体裁剪提高效率:简化视椎体裁剪,只计算水平方向不计算竖直方向,毕竟竖直方向在视线外的情况非常少如仰望天空和俯瞰下方。方法是求出视椎体左右面的法线,然后计算草像素的向量与法线的投影是否为正,取二者的交集即可判断是否应该渲染该像素。

主要代码:

//cs关键代码Vector3 vR =farClipPlane * transform.forward + w * transform.right;Vector3 vL =farClipPlane * transform.forward - w * transform.right;Vector3 RightPlane_N = Vector3.Cross(vR,transform.up);//视椎体右平面法线Vector3 LeftPlane_N = Vector3.Cross(transform.up,vL);//视椎体左平面法线positionComputeShader.SetVector("Centerposition", transform.position-transform.forward*3);//视椎体顶点(退后3米)positionComputeShader.SetVector("RightPlane_N", RightPlane_N);positionComputeShader.SetVector("LeftPlane_N", LeftPlane_N);
//computer关键代码
void CSPositionKernel (uint3 id : SV_DispatchThreadID)
{float3 V3=float3(positionListBuffer[id.x].x-Centerposition.x,positionListBuffer[id.x].y-Centerposition.y,positionListBuffer[id.x].z-Centerposition.z);float fR=dot(RightPlane_N,V3);float fL=dot(LeftPlane_N,V3);float Sqr_Distence=(positionListBuffer[id.x].x -Centerposition.x)*(positionListBuffer[id.x].x -Centerposition.x)+(positionListBuffer[id.x].y -Centerposition.y)*(positionListBuffer[id.x].y -Centerposition.y)+(positionListBuffer[id.x].z -Centerposition.z)*(positionListBuffer[id.x].z -Centerposition.z);if(Sqr_Distence < viewDistance && fR > 0 && fL > 0 )//限定在左右视椎体面内且小于可视距离{float4 v4=float4(positionListBuffer[id.x].x,positionListBuffer[id.x].y,positionListBuffer[id.x].z,positionListBuffer[id.x].w);positionBuffer.Append(v4);}
}

 

6、延伸视椎体边界,防止视椎体边缘处草和阴影突然出现和消失。采用简单处理:在计算视椎体的时候将视椎体顶点向后退几米。
关键代码:positionComputeShader.SetVector( "Centerposition", transform.position-transform.forward*3); //视椎体顶点(退后3米)

7、一键快速刷草:  

主要代码: 

 public void SpeedCreadeGrass()//一键快速刷草{//  if (currentMaterial == null) return;for (int i = 0; i < 10000; i++){float x = UnityEngine.Random.Range(0, TerrainWidth);//随机刷float z = UnityEngine.Random.Range(0, TerrainLength);Ray ray = new Ray(new Vector3(x, 3000, z), Vector3.down);RaycastHit hit;if (Physics.Raycast(ray, out hit, 5000))//刷草的地方必须在地形范围内{if (hit.transform.name.CompareTo(terrainName) == 0){float _x = hit.point.x;float _z = hit.point.z;if (GrassNoise.GetPixel((int)x, (int)z).r > 0.05f) continue;//按噪声图像素颜色值过滤,留白更自然float _y = terrain.SampleHeight(new Vector3(_x, 0, _z));//取此点对应的地形高度,草随高度相应改变virtuTerrain.addPos(new Vector3(_x, _y, _z), UnityEngine.Random.Range(0.3f, scale), true);//加入旋转,避免雷同}}}}

 

8、地形划块:   

主要代码:

 public float _widthPix, _heightPix;//地形宽高public int _row;//地形行public int _column;//地形列public PerPiece[] perPiece;//每片public virtualTerrain(float widthPix, float heightPix, int row, int column)//初始化虚拟地形长宽(保持与地形相同)、行列等分{if (widthPix * heightPix * row * column == 0){Debug.Log("地形的宽高、行列均不能为0!");return;}_widthPix = widthPix;_heightPix = heightPix;_row = row; _column = column;perPiece = new PerPiece[_row * _column];for (int h = 0; h < _column; h++){for (int w = 0; w < _row; w++){perPiece[h * _row + w] = new PerPiece();perPiece[h * _row + w].center.x = (w + 0.5f) * _widthPix / _row;//每片的中心坐标perPiece[h * _row + w].center.y = (h + 0.5f) * _heightPix / _column;perPiece[h * _row + w].coord.x = w * _widthPix / _row;//每片的网格坐标(左下)perPiece[h * _row + w].coord.y = h * _heightPix / _column;perPiece[h * _row + w].ID = h * _row + w;//每片的ID(从左下0起始)perPiece[h * _row + w].pos = new List<xyzwPos>();}}}

 

除了以上问题其实在制作插件过程中还遇到很多细节问题,如:mesh的制作、shader的调试、一些bug的排除、、、、都一一解决了,不过还有很多可以继续改进的地方,比如在播放状态下必须关闭编辑器,再次编辑又必须重新打开编辑器等,希望大佬能指点一下。

 由于脚本较多,下面将核心脚本贴出来,其它就不一一贴出来了,等用一段时间稳定后再放到下载区供大家测试。

几个主要的核心脚本如下:

1、InstancedIndirectCompute.cs(功能见脚本中的标注)

using UnityEngine;
using UnityEngine.Rendering;
using System.Collections.Generic;
using UnityEditor;
/// <summary>
/// 在 Start()函数中一次性读取硬盘所有类别的草位置数据,并传递给computerBuffer,由于数据含有位置信息,
/// 所以可以在gpu的computerBuffer中根据这些信息过滤掉视线外的草, 仅传递可视部分数据给Materail显示。
///每帧中cpu只需提供当前需要显示的中心坐标即可,其他全由gpu完成,效率较高
/// 注意:1、判断视线内的块使用了简化的计算方法,不做商用效率优先:只判断每个需要渲染的像素是否在视椎体左右面之间,忽略上下判断。
///       2、在传递给Materail时使用的是computerBuffer的Append方式。
///       3、实验在同时显示5种草、开启实时阴影、约一百万面(视线内估计二十万),在关掉垂直同步时全屏显示可以达到200帧。
/// </summary>
/// 
public class InstancedIndirectCompute : MonoBehaviour
{Mesh[] instanceMesh;Material[] instanceMaterial;public ShadowCastingMode castShadows = ShadowCastingMode.Off;public bool receiveShadows = false;public ComputeShader positionComputeShader;private int positionComputeKernelId;private ComputeBuffer[] positionAppendBuffer;private ComputeBuffer[] positionListBuffer;private ComputeBuffer[] argsBuffers;private int[] grassDataCount;private List<xyzwPos[]> xyzwsList = new List<xyzwPos[]>();private uint[] args = new uint[5] { 0, 0, 0, 0, 0 };private uint[][] argsList = new uint[10][];private int bs = 64;GameObject TempObj;PlayerData OffsetData;int viewDistance = 1500;//视距int windSpeed = 2;Camera camera;int InViewCount=0;bool shadow = false;int KindOfGrass;float farClipPlane;// float viewAngleCos;float lastAspect=1;float w;//远视口宽度*0.5GrassRW grassRW;void Start(){grassRW = new GrassRW();viewDistance=(int)grassRW.ReadPlayDate().viewDistance;shadow = grassRW.ReadPlayDate().shadowIsEnable;if (shadow){castShadows = ShadowCastingMode.On;receiveShadows = true;}else{castShadows = ShadowCastingMode.Off;receiveShadows = false;}xyzwsList = grassRW.readGrassDate();TempObj = new GameObject("TempObj");TempObj.transform.position = Vector3.one * -20000;OffsetData = (PlayerData)PlayerDataOperator.LoadPlayerData("myvars.data");camera = Camera.main;farClipPlane = camera.farClipPlane;if (OffsetData != null){//viewDistance = OffsetData.ViewDistance;//camera.farClipPlane = viewDistance;windSpeed = OffsetData.WindSpeed;}KindOfGrass = xyzwsList.Count;instanceMesh = new Mesh[KindOfGrass];for (int i = 0; i < KindOfGrass; i++)instanceMesh[i] = (Instantiate(Resources.Load("grassPrb" + i.ToString()), TempObj.transform) as GameObject).GetComponent<MeshFilter>().mesh;instanceMaterial = new Material[KindOfGrass];for (int i = 0; i < KindOfGrass; i++){instanceMaterial[i] = (Instantiate(Resources.Load("grassMat" + i), TempObj.transform) as GameObject).GetComponent<Renderer>().material;instanceMaterial[i].SetFloat("_WindSpeed", windSpeed);}positionComputeKernelId = positionComputeShader.FindKernel("CSPositionKernel");positionAppendBuffer = new ComputeBuffer[KindOfGrass];positionListBuffer = new ComputeBuffer[KindOfGrass];argsBuffers = new ComputeBuffer[KindOfGrass];grassDataCount = new int[KindOfGrass];TempObj.hideFlags = HideFlags.HideInHierarchy;CreateBuffers();}void CreateBuffers(){for (int i = 0; i < KindOfGrass; i++){if (xyzwsList[i].Length < 1) continue;grassDataCount[i] = xyzwsList[i].Length;if (positionAppendBuffer[i] != null) positionAppendBuffer[i].Release();positionAppendBuffer[i] = new ComputeBuffer(grassDataCount[i], 16, ComputeBufferType.Append);positionAppendBuffer[i].SetCounterValue(0);if (positionListBuffer[i] != null) positionListBuffer[i].Release();positionListBuffer[i] = new ComputeBuffer(grassDataCount[i], 16);instanceMesh[i].bounds = new Bounds(Vector3.zero, Vector3.one * 10000f);uint numIndices = (instanceMesh[i] != null) ? (uint)instanceMesh[i].GetIndexCount(0) : 0;argsList[i] = args;argsList[i][0] = numIndices;argsList[i][1] = (uint)grassDataCount[i];argsBuffers[i] = new ComputeBuffer(5, sizeof(uint), ComputeBufferType.IndirectArguments);argsBuffers[i].SetData(argsList[i]);positionListBuffer[i].SetData(xyzwsList[i]);}}void Update(){for (int i = 0; i < KindOfGrass; i++){if (xyzwsList[i].Length < 1) continue;bs = grassDataCount[i] / 64;bs = bs == 0 ? 1 : bs;positionAppendBuffer[i].SetCounterValue(0);positionComputeShader.SetFloat("viewDistance", viewDistance * viewDistance);if (lastAspect != camera.aspect){w = Mathf.Tan(camera.fieldOfView * 0.5f * Mathf.Deg2Rad) * farClipPlane * camera.aspect;lastAspect = camera.aspect;}Vector3 vR =farClipPlane * transform.forward + w * transform.right;Vector3 vL =farClipPlane * transform.forward - w * transform.right;Vector3 RightPlane_N = Vector3.Cross(vR,transform.up);//视椎体右平面法线Vector3 LeftPlane_N = Vector3.Cross(transform.up,vL);//视椎体左平面法线positionComputeShader.SetVector("Centerposition", transform.position-transform.forward*15);//视椎体顶点(退后15米,避免阴影突然消失)positionComputeShader.SetVector("RightPlane_N", RightPlane_N);positionComputeShader.SetVector("LeftPlane_N", LeftPlane_N);positionComputeShader.SetBuffer(positionComputeKernelId, "positionListBuffer", positionListBuffer[i]);positionComputeShader.SetBuffer(positionComputeKernelId, "positionBuffer", positionAppendBuffer[i]);instanceMaterial[i].SetBuffer("positionBuffer", positionAppendBuffer[i]);positionComputeShader.Dispatch(positionComputeKernelId, bs, 1, 1);ComputeBuffer.CopyCount(positionAppendBuffer[i], argsBuffers[i], 4);Graphics.DrawMeshInstancedIndirect(instanceMesh[i], 0, instanceMaterial[i], instanceMesh[i].bounds, argsBuffers[i], 0, null, castShadows, receiveShadows);}}void OnDisable(){for (int i = 0; i < KindOfGrass; i++){if (xyzwsList[i].Length < 1) continue;if (positionListBuffer[i] != null){positionListBuffer[i].Release();positionListBuffer[i] = null;}if (positionAppendBuffer[i] != null){positionAppendBuffer[i].Release();positionAppendBuffer[i] = null;}if (argsBuffers[i] != null){argsBuffers[i].Release();argsBuffers[i] = null;}}//Debug.Log("OK");grassRW.SavePlayerData(viewDistance, shadow);}#if UNITY_EDITORvoid OnGUI(){GUILayout.Label( "阴影");shadow = GUILayout.Toggle(shadow, "");GUILayout.Label("可见距离");viewDistance=(int)GUILayout.HorizontalSlider(viewDistance,15,2000,GUILayout.MaxWidth(200));if (shadow){castShadows = ShadowCastingMode.On;receiveShadows = true;}else{castShadows = ShadowCastingMode.Off;receiveShadows = false;}for (int i = 0; i < KindOfGrass; i++)GUI.Label(new Rect(5,100+ 12 * i, 200, 30), "草"+i.ToString ()+"数量:  "+ grassDataCount[i].ToString("N0"));}
#endif
}

2、EditorWindow控制面板代码(控制所有草)

using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using System;/// <summary>
/// 放一物体到场景并旋转
/// </summary>
#if UNITY_EDITORpublic class Inspector_Edit : EditorWindow
{GameObject go;Texture[] textures_kind; //待扩充为草、石头、树3种Texture[] textures_grass; //草的主贴图public int materIndex = 10;public int materIndex1 = 10;public GameObject ParentTmp;GameObject[] GrassOBJS;Terrain terrain;private bool[] toggleDraw;private bool[] toggleView;bool[] foldout;Mesh[] meshes;Material[] material;Mesh[] lastMeshObj = null;Material[] lastMatObj = null;Texture2D texture2D;GameObject gameObject;Camera camera;bool help = false;public Vector2 scrollPosition;GameObject grassCus;//光标RaycastHit HitPos;float grassCusRadius = 0;//光标半径float grassCusdensity = 0;//光标透明度int choose = 0;//勾选的草Material ma;PlayerData OffsetData;string warning;string warn1;string warn2;string warn3;GUIStyle gUIStyle;string str;// Transform CamTrs;// Transform LastCamTrs;[MenuItem("GrassEdit/GrassEdit")]public static void OpenWindow(){if (Application.isPlaying)//播放状态禁止编辑草return;else{Inspector_Edit InsEdit = EditorWindow.GetWindow<Inspector_Edit>();InsEdit.maxSize = new Vector2(320, 530);InsEdit.minSize = new Vector2(320, 530);InsEdit.Show();}}private void OnEnable(){gUIStyle = new GUIStyle();gUIStyle.normal.textColor = Color.red;warn1 = "   请先建立地形terrain!   无地形不能刷草。";warn2 = "  请选择规定的Material(类似名为grassMaterail的),\n若要自定义Material请点击下面的说明";warn3 = "";terrain = Terrain.activeTerrain;  //获得真实地形// CamTrs = Camera.main.transform;//LastCamTrs = SceneView.lastActiveSceneView.camera.transform;if (terrain == null) return;/// Instantiate(Resources.Load("TerrainExample"));foldout = new bool[materIndex];material = new Material[materIndex];ParentTmp = new GameObject("ParentTmp");meshes = new Mesh[materIndex];lastMeshObj = new Mesh[materIndex];lastMatObj = new Material[materIndex];GrassOBJS = new GameObject[materIndex];toggleDraw = new bool[materIndex];toggleView = new bool[materIndex];gameObject = GameObject.CreatePrimitive(PrimitiveType.Quad);gameObject.transform.parent = ParentTmp.transform;grassCus = Instantiate(Resources.Load("GrassCus"), -Vector3.one * 1000, Quaternion.identity, ParentTmp.transform) as GameObject;grassCus.layer = 2;OffsetData = new PlayerData();OffsetData.density = new int[materIndex];OffsetData.radius = new int[materIndex];OffsetData.scale = new int[materIndex];for (int i = 0; i < materIndex; i++){OffsetData.density[i] = 2; OffsetData.radius[i] = 2; OffsetData.scale[i] = 4;}object tmp = PlayerDataOperator.LoadPlayerData("myvars.data");if (tmp != null)OffsetData = (PlayerData)tmp;texture2D = new Texture2D(100, 100);for (int i = 0; i < materIndex; i++)toggleView[i] = true;for (int i = 0; i < materIndex; i++){GrassOBJS[i] = Instantiate(Resources.Load("SceneCtrlObj"), -Vector3.one * 1000, Quaternion.identity, ParentTmp.transform) as GameObject;//每个GrassOBJS管理一种草GrassOBJS[i].GetComponent<EditInWindow>().OnEnableA();toggleDraw[i] = false;}for (int i = 0; i < materIndex; i++){meshes[i] = (Instantiate(Resources.Load("grassPrb" + i.ToString()), ParentTmp.transform) as GameObject).GetComponent<MeshFilter>().sharedMesh;GrassOBJS[i].GetComponent<EditInWindow>().currentMesh = meshes[i];lastMeshObj[i] = meshes[i];material[i] = (Instantiate(Resources.Load("grassMat" + i), ParentTmp.transform) as GameObject).GetComponent<Renderer>().sharedMaterial;GrassOBJS[i].GetComponent<EditInWindow>().currentMaterial = material[i];lastMatObj[i] = material[i];}toggleDraw[0] = true;//开始至少能绘制一种草textures_kind = new Texture[3];textures_kind[0] = (Texture)Resources.Load("kind0") as Texture;textures_kind[1] = (Texture)Resources.Load("kind1") as Texture;textures_kind[2] = (Texture)Resources.Load("kind2") as Texture;textures_grass = new Texture[materIndex];for (int i = 0; i < materIndex; i++)textures_grass[i] = material[i].mainTexture;go = GameObject.CreatePrimitive(PrimitiveType.Capsule);go.transform.parent = ParentTmp.transform;ParentTmp.transform.position = Vector3.one * -20000;readGrassDate();//读取上次保存的草数据,在开始编辑草之前显示出来ParentTmp.hideFlags = HideFlags.HideInHierarchy;//在Hierarchy面板中隐藏ParentTmpma = grassCus.GetComponent<Renderer>().sharedMaterial;camera = Camera.main;if (camera == null){camera = (Camera)FindObjectOfType(typeof(Camera));camera.tag = "MainCamera";camera.fieldOfView = 45;}else camera.fieldOfView = 45;Transform gameTrs = camera.transform.Find("InstancedIndirectComputeTest(Clone)");if (gameTrs == null)Instantiate(Resources.Load("InstancedIndirectComputeTest"), camera.transform);str = "1、刷草---鼠标左键,删除草---SHIFT+鼠标左键.\n" +"2、mesh可以任选,material只能选择特定的(类似名为grassMaterail的),但也可自定义material,只需要将material的shader选择为Instanced/InstancedIndirectCompute,然后把主贴图和噪声贴图选择好即可。\n" +"3、目前暂时只能刷草,待日后增加刷石头和树的功能.\n" +"4、草阴影在编辑时可能没有,在运行时会有(Game屏幕左上角可控制),关闭阴影可提高帧率约25%";ParentTmp.transform.position = new Vector3(-20000, -20000, -20000);//CamTrs.position = LastCamTrs.position - LastCamTrs.forward;//使GameCamera跟随SceneCamera,避免误判无显示//CamTrs.rotation = LastCamTrs.rotation;}private void OnInspectorUpdate(){if (terrain == null)return;if (go != null)go.transform.Rotate(Vector3.up, 1);EditorApplication.playmodeStateChanged = delegate (){Close();};for (int i = 0; i < materIndex; i++){if (GrassOBJS[i] != null)GrassOBJS[i].GetComponent<EditInWindow>().bol = toggleDraw[i];//控制是否绘制该种草?}for (int i = 0; i < materIndex; i++){GrassOBJS[i].GetComponent<EditInWindow>().scale = OffsetData.scale[i];//控制草体积GrassOBJS[i].GetComponent<EditInWindow>().density = OffsetData.density[i];//控制草密度GrassOBJS[i].GetComponent<EditInWindow>().radius = OffsetData.radius[i];//控制草范围大小GrassOBJS[i].GetComponent<EditInWindow>().windSpeed = OffsetData.WindSpeed;//控制草范围大小}camera.farClipPlane = OffsetData.ViewDistance;}private void OnDestroy(){if (terrain == null) return;PlayerDataOperator.SavePlayerData("myvars.data", OffsetData);for (int i = 0; i < materIndex; i++){List<xyzwPos> listPos = new List<xyzwPos>();listPos.Clear();GrassOBJS[i].GetComponent<EditInWindow>().virtuTerrain.GetTotalAddPos(listPos);BinaryFormatter bf = new BinaryFormatter();if (File.Exists(Application.streamingAssetsPath + "/" + System.Enum.GetName(typeof(Grass_name.grassName), i) + ".data")){File.Delete(Application.streamingAssetsPath + "/" + System.Enum.GetName(typeof(Grass_name.grassName), i) + ".data");}FileStream file = File.Create(Application.streamingAssetsPath + "/" + System.Enum.GetName(typeof(Grass_name.grassName), i) + ".data");bf.Serialize(file, listPos);file.Close();}for (int i = 0; i < materIndex; i++)GrassOBJS[i].GetComponent<EditInWindow>().OnDisableA();//DestroyImmediate(go);DestroyImmediate(ParentTmp);}void readGrassDate()//读取上次保存的草数据,在开始编辑草之前显示出来{for (int j = 0; j < Grass_name.grassNameCount; j++){EditInWindow editInWindow = GrassOBJS[j].GetComponent<EditInWindow>();if (!File.Exists(Application.streamingAssetsPath + "/" + System.Enum.GetName(typeof(Grass_name.grassName), j) + ".data"))continue;BinaryFormatter bf = new BinaryFormatter();FileStream file = File.Open(Application.streamingAssetsPath + "/" + System.Enum.GetName(typeof(Grass_name.grassName), j) + ".data", FileMode.Open);List<xyzwPos> listPos = (List<xyzwPos>)bf.Deserialize(file);Vector3 vector3 = new Vector3();for (int i = 0; i < listPos.Count; i++){vector3.x = listPos[i].x;vector3.y = listPos[i].y;vector3.z = listPos[i].z;editInWindow.virtuTerrain.addPos(vector3, listPos[i].w, false);}file.Close();}}void OnGUI(){//CamTrs.position = LastCamTrs.position - LastCamTrs.forward;//使GameCamera跟随SceneCamera,避免误判无显示//CamTrs.rotation = LastCamTrs.rotation;GUILayout.TextArea(warning, gUIStyle);if (terrain == null){warning = warn1;return;}else warning = warn3;HitPos = GrassOBJS[0].GetComponent<EditInWindow>().hit;grassCus.transform.position = HitPos.point + Vector3.up * 0.2f;grassCus.transform.LookAt(HitPos.point - HitPos.normal.normalized);EditorGUILayout.Space();EditorGUILayout.Space();help = EditorGUILayout.Foldout(help, "说明");//可折叠标签int _help = help == true ? 1 : 0;if (EditorGUILayout.BeginFadeGroup(_help))GUILayout.TextArea(str);EditorGUILayout.EndFadeGroup();scrollPosition = GUILayout.BeginScrollView(scrollPosition, false, false, GUILayout.Width(320), GUILayout.Height(400));for (int i = 0; i < materIndex; i++){EditorGUILayout.Space();EditorGUILayout.BeginHorizontal(GUILayout.Width(2), GUILayout.MinHeight(2));//开始水平布局EditorGUILayout.Space();foldout[i] = EditorGUILayout.Foldout(foldout[i], "草" + i.ToString());//可折叠标签toggleDraw[i] = GUILayout.Toggle(toggleDraw[i], textures_grass[i], GUILayout.Width(50), GUILayout.MaxWidth(50), GUILayout.Height(50), GUILayout.MaxHeight(50));//单选开关(绘制)EditorGUILayout.Space();EditorGUILayout.Space();toggleView[i] = GUILayout.Toggle(toggleView[i], "可见");//单选开关(可见性)GrassOBJS[i].GetComponent<EditInWindow>().viewAble = toggleView[i];EditorGUILayout.Space();// GUILayout.Label("一键种草");if (GUILayout.Button("一键种", GUILayout.Width(40), GUILayout.MaxWidth(100), GUILayout.Height(18), GUILayout.MaxHeight(18)))GrassOBJS[i].GetComponent<EditInWindow>().SpeedCreadeGrass();//GUILayout.Label("一键删除");if (GUILayout.Button("一键删", GUILayout.Width(40), GUILayout.MaxWidth(100), GUILayout.Height(18), GUILayout.MaxHeight(18)))GrassOBJS[i].GetComponent<EditInWindow>().virtuTerrain.deleteAllPos();EditorGUILayout.EndHorizontal();//结束水平布局if (foldout[i])//折叠{int m = foldout[i] == true ? 1 : 0;if (EditorGUILayout.BeginFadeGroup(m)){meshes[i] = (Mesh)EditorGUILayout.ObjectField("   选择Mesh", meshes[i], typeof(Mesh), false);if (meshes[i] == null)meshes[i] = lastMeshObj[i];if (!meshes[i].Equals(lastMeshObj[i])){GrassOBJS[i].GetComponent<EditInWindow>().currentMesh = meshes[i];gameObject.GetComponent<MeshFilter>().mesh = meshes[i];PrefabUtility.CreatePrefab("Assets/Resources/grassPrb" + i.ToString() + ".prefab", gameObject);lastMeshObj[i] = meshes[i];}material[i] = (Material)EditorGUILayout.ObjectField("   选择Material", material[i], typeof(Material), false);if (material[i] == null)material[i] = lastMatObj[i];if (material[i].shader.name.CompareTo("InstancedIndirectCompute") == 1){material[i] = lastMatObj[i];warning = warn2;}else warning = warn3;if (!material[i].Equals(lastMatObj[i])){GrassOBJS[i].GetComponent<EditInWindow>().currentMaterial = material[i];texture2D = (Texture2D)material[i].mainTexture;textures_grass[i] = texture2D;gameObject.GetComponent<Renderer>().material = material[i];PrefabUtility.CreatePrefab("Assets/Resources/grassMat" + i.ToString() + ".prefab", gameObject);lastMatObj[i] = material[i];}EditorGUILayout.BeginHorizontal(GUILayout.Width(300), GUILayout.MinHeight(10));//开始水平布局GUILayout.Label("    刷草密度(/删草)", GUILayout.MinWidth(136));OffsetData.density[i] = EditorGUILayout.IntSlider(OffsetData.density[i], 1, 10);//刷草密度EditorGUILayout.EndHorizontal();//结束水平布局EditorGUILayout.BeginHorizontal(GUILayout.Width(300), GUILayout.MinHeight(10));//开始水平布局GUILayout.Label("    刷草范围(/删草)", GUILayout.MinWidth(136));OffsetData.radius[i] = EditorGUILayout.IntSlider(OffsetData.radius[i], 1, 20);//刷草半径EditorGUILayout.EndHorizontal();//结束水平布局EditorGUILayout.BeginHorizontal(GUILayout.Width(300), GUILayout.MinHeight(10));//开始水平布局GUILayout.Label("    草体积", GUILayout.MinWidth(136));OffsetData.scale[i] = EditorGUILayout.IntSlider(OffsetData.scale[i], 1, 5);//草缩放EditorGUILayout.EndHorizontal();//结束水平布局choose}EditorGUILayout.EndFadeGroup();}if (toggleDraw[i])//控制草光标颜色和半径{grassCusRadius += OffsetData.radius[i];grassCusdensity += OffsetData.density[i];choose++;}}if (choose != 0){float G = grassCusRadius / choose;grassCus.transform.localScale = new Vector3(G, G, 0) * 3;G = grassCusdensity / choose;ma.SetFloat("_AlphaScale", G * 0.08f);}grassCusRadius = 0;grassCusdensity = 0;choose = 0;GUILayout.EndScrollView();EditorGUILayout.BeginHorizontal(GUILayout.Width(5), GUILayout.MinHeight(5));//开始水平布局GUILayout.Label("                   ");if (GUILayout.Button("全部种", GUILayout.Width(50), GUILayout.Height(25)))for (int i = 0; i < materIndex; i++)GrassOBJS[i].GetComponent<EditInWindow>().SpeedCreadeGrass();if (GUILayout.Button("全部删", GUILayout.Width(50), GUILayout.Height(25)))for (int i = 0; i < materIndex; i++)GrassOBJS[i].GetComponent<EditInWindow>().virtuTerrain.deleteAllPos();EditorGUILayout.EndHorizontal();//结束水平布局EditorGUILayout.Space();EditorGUILayout.Space();EditorGUILayout.Space();EditorGUILayout.BeginHorizontal(GUILayout.Width(300), GUILayout.MinHeight(10));//开始水平布局EditorGUILayout.Space();GUILayout.Label("风力大小", GUILayout.MinWidth(80), GUILayout.Width(60));OffsetData.WindSpeed = EditorGUILayout.IntSlider(OffsetData.WindSpeed, 1, 5);//风大小EditorGUILayout.EndHorizontal();//结束水平布局EditorGUILayout.BeginHorizontal(GUILayout.Width(300), GUILayout.MinHeight(10));//开始水平布局EditorGUILayout.Space();GUILayout.Label("视野(米)", GUILayout.MinWidth(80), GUILayout.Width(60));OffsetData.ViewDistance = EditorGUILayout.IntSlider(OffsetData.ViewDistance, 50, 2000);//视距EditorGUILayout.EndHorizontal();//结束水平布局Repaint();}
}
#endif

3、EditInWindow.cs(将地形划分为多块,某块有草的数量变化时只更新那一块的数据,提高效率。)

using UnityEngine;
using UnityEditor;
/// <summary>
/// 将地形划分为多块,某块有草的数量变化时只更新那一块的数据,提高效率。
/// </summary>
#if UNITY_EDITOR
[ExecuteInEditMode]
public class EditInWindow : MonoBehaviour
{private int terraimRow = 12;        //虚拟地形竖直分块private int terraimColumn = 12;      //虚拟地形水平分块public int subMeshIndex = 0;private int[] cachedInstanceCount;private ComputeBuffer[] positionBuffer;private ComputeBuffer[] argsBuffers;private uint[] args = new uint[5] { 0, 0, 0, 0, 0 };public xyzwPos[] v4 = new xyzwPos[1000];private Terrain terrain;private bool bo = false;public bool bol = false;//控制是否绘制草(Inspector传入)public virtualTerrain virtuTerrain;Material[] instanceMaterial;GameObject go;int n;//总块数string terrainName;float TerrainWidth;float TerrainLength;Texture2D GrassNoise;GameObject grassData;public bool viewAble = true;public Material currentMaterial = null;Material lastMaterial = null;public Mesh currentMesh = null;public int scale;//草大小(来自inspector_Edit.cs)public int density = 5;//刷草密度(来自inspector_Edit.cs)public int radius = 3;//刷草的半径(来自inspector_Edit.cs)public int windSpeed = 3;//风大小(来自inspector_Edit.cs)private GameObject GrassCus;public RaycastHit hit;public void OnEnableA(){terrain = Terrain.activeTerrain;                                         //真实地形if (terrain != null)terrainName = terrain.name;TerrainWidth = terrain.terrainData.size.x;TerrainLength = terrain.terrainData.size.z;virtuTerrain = new virtualTerrain(TerrainWidth, TerrainLength, terraimRow, terraimColumn);//虚拟地形n = terraimRow * terraimColumn;//总块数GrassNoise = Resources.Load("grassNoise256") as Texture2D;cachedInstanceCount = new int[n];SceneView.onSceneGUIDelegate -= OnSceneGUI;SceneView.onSceneGUIDelegate += OnSceneGUI;positionBuffer = new ComputeBuffer[n];argsBuffers = new ComputeBuffer[n];for (int i = 0; i < n; i++)argsBuffers[i] = new ComputeBuffer(1, args.Length * sizeof(uint), ComputeBufferType.IndirectArguments);currentMesh = new Mesh();instanceMaterial = new Material[n];UpdateBuffers(0);}private void Update(){if (!Equals(lastMaterial, currentMaterial)){for (int i = 0; i < n; i++)instanceMaterial[i] = Instantiate(currentMaterial);lastMaterial = currentMaterial;}for (int i = 0; i < n; i++)//每帧都绘制{UpdateBuffers(i);if (virtuTerrain.perPiece[i].pos.Count == 0 || viewAble == false)continue;instanceMaterial[i].SetFloat("_WindSpeed", windSpeed);Graphics.DrawMeshInstancedIndirect(currentMesh, subMeshIndex, instanceMaterial[i],new Bounds(Vector3.zero, new Vector3(2000.0f, 2000.0f, 2000.0f)), argsBuffers[i]);}SceneView.RepaintAll();}void UpdateBuffers(int number)//某块有草的数量变化时只更新那一块的数据{if (virtuTerrain.perPiece[number].pos.Count == 0) return;if (cachedInstanceCount[number] == virtuTerrain.perPiece[number].pos.Count){instanceMaterial[number].SetBuffer("positionBuffer", positionBuffer[number]);return;}subMeshIndex = Mathf.Clamp(subMeshIndex, 0, currentMesh.subMeshCount - 1);if (positionBuffer[number] != null)positionBuffer[number].Release();positionBuffer[number] = new ComputeBuffer(virtuTerrain.perPiece[number].pos.Count, 16);instanceMaterial[number].SetBuffer("positionBuffer", positionBuffer[number]);positionBuffer[number].SetData(virtuTerrain.perPiece[number].pos);args[0] = (uint)currentMesh.GetIndexCount(subMeshIndex);args[1] = (uint)virtuTerrain.perPiece[number].pos.Count;args[2] = (uint)currentMesh.GetIndexStart(subMeshIndex);args[3] = (uint)currentMesh.GetBaseVertex(subMeshIndex);argsBuffers[number].SetData(args);cachedInstanceCount[number] = virtuTerrain.perPiece[number].pos.Count;}void OnSceneGUI(SceneView sceneView)//绘制和删除草{int controlID = GUIUtility.GetControlID(FocusType.Passive);//使scene视图不能选择物体HandleUtility.AddDefaultControl(controlID);if (Event.current.button != 1 && Event.current.button != 2 && Event.current.rawType == EventType.MouseDown)bo = true;if (Event.current.button != 1 && Event.current.button != 2 && Event.current.rawType == EventType.MouseUp)bo = false;Ray ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition);bool _bool1 = false;bool _bool = false;if (_bool = Physics.Raycast(ray, out hit)){if (hit.transform.name.CompareTo(terrainName) == 0)_bool1 = true;}if (Event.current.shift && bo && bol && _bool && _bool1)//清除草{virtuTerrain.minusPos(hit.point, radius * radius, density * 0.4f);return;}if (bo && bol && _bool && _bool1)    //增加草{for (int i = 0; i < density; i++){Vector2 v2 = UnityEngine.Random.insideUnitCircle * radius;v4[i].x = v2.x + hit.point.x;v4[i].z = v2.y + hit.point.z;v4[i].y = terrain.SampleHeight(new Vector3(v4[i].x, 0, v4[i].z));//取此点对应的地形高度virtuTerrain.addPos(new Vector3(v4[i].x, v4[i].y, v4[i].z), UnityEngine.Random.Range(0.3f, scale), true);}}sceneView.Repaint();}public void SpeedCreadeGrass()//一键快速刷草{//  if (currentMaterial == null) return;for (int i = 0; i < 2000; i++){float x = UnityEngine.Random.Range(0, TerrainWidth);float z = UnityEngine.Random.Range(0, TerrainLength);Ray ray = new Ray(new Vector3(x, 3000, z), Vector3.down);RaycastHit hit;if (Physics.Raycast(ray, out hit, 5000)){if (hit.transform.name.CompareTo(terrainName) == 0){float _x = hit.point.x;float _z = hit.point.z;if (GrassNoise.GetPixel((int)x, (int)z).r > 0.05f) continue;//按噪声图像素颜色值过滤float _y = terrain.SampleHeight(new Vector3(_x, 0, _z));//取此点对应的地形高度virtuTerrain.addPos(new Vector3(_x, _y, _z), UnityEngine.Random.Range(0.3f, scale), true);//加入旋转}}}}public void OnDisableA(){SceneView.onSceneGUIDelegate -= OnSceneGUI;for (int i = 0; i < n; i++){if (positionBuffer[i] != null)positionBuffer[i].Release();positionBuffer[i] = null;if (argsBuffers[i] != null)argsBuffers[i].Release();argsBuffers[i] = null;}}
}
#endif

4、InstancedIndirectCompute.shader(草专用shader,不能用unity自带shader)

Shader "Instanced/InstancedIndirectCompute" 
{Properties{_MainTex("Albedo (RGB)", 2D) = "white" {}_WindTex ("WindMap", 2D) = "white" {}_Glossiness("Smoothness", Range(0,1)) = 0.0_Metallic("Metallic", Range(0,1)) = 0.0[HDR]_Color ("Color", Color) = (0,1,0,1)_Height("Height",Float)=1_ScaleVH("Scale",Float)=1_WindSpeed("WindVelocity",Float)=0_WindSize("WinSize",Float)=10_Cutoff ("Alpha cutoff", Range(0,1)) = 0.5}SubShader{Tags{"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"} LOD 200Cull OffCGPROGRAM#pragma surface surf Standard addshadow alphatest:_Cutoff  vertex:vert// #pragma surface surf Standard addshadow  vertex:vert#pragma multi_compile_instancing #pragma instancing_options procedural:setup#include "UnityPBSLighting.cginc"sampler2D _MainTex;sampler2D _WindTex;float _Height;float _WindSpeed;float _WindSize;float _ScaleVH;struct Input {float2 uv_MainTex;};half _Glossiness;half _Metallic;float4 _Color;#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLEDStructuredBuffer<float4> positionBuffer;StructuredBuffer<float4> colorBuffer;#endifvoid rotate2D(inout float2 v, float r){float s, c;sincos(r, s, c);v = float2(v.x * c - v.y * s, v.x * s + v.y * c);}void setup(){
#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED/// Positions are calculated in the compute shader./// here we just use them.float4 position = positionBuffer[unity_InstanceID];float scale = position.w*_ScaleVH;unity_ObjectToWorld._11_21_31_41 = float4(scale, 0, 0, 0);unity_ObjectToWorld._12_22_32_42 = float4(0, scale, 0, 0);unity_ObjectToWorld._13_23_33_43 = float4(0, 0, scale, 0);unity_ObjectToWorld._14_24_34_44 = float4(position.xyz, 1);unity_WorldToObject = unity_ObjectToWorld;unity_WorldToObject._14_24_34 *= -1;unity_WorldToObject._11_22_33 = 1.0f / unity_WorldToObject._11_22_33;
#endif}void RotateFixed(inout appdata_full v,float rote){float2 rv2=float2(v.vertex.x,v.vertex.z);rotate2D(rv2,rote);v.vertex.x=rv2.x;v.vertex.z=rv2.y;}float GetWindWave(float2 position,float height){
//每个顶点摆动大小受风力图采样、高度、位置同时影响而变得随机float4 p=tex2Dlod(_WindTex,float4(position/_WindSize+float2(_Time.x*_WindSpeed+height*.01,0),0.0,0.0)); return (height*(p.r-.5));
}void vert (inout appdata_full v, out Input o) {UNITY_INITIALIZE_OUTPUT(Input,o);#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLEDfloat4 data = positionBuffer[unity_InstanceID];//旋转 RotateFixed(v,(data.x+data.y+data.z+data.w*10000));//获取风float w=GetWindWave(data.xz,v.vertex.y);//顶点水平移动v.vertex.x+=w;v.vertex.y+=w*.4;            #endif}void surf(Input IN, inout SurfaceOutputStandard o) {float4 col = 1.0f;#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLEDcol = colorBuffer[unity_InstanceID];
#elsecol = float4(0, 0, 1, 1);
#endiffixed4 c = tex2D(_MainTex, IN.uv_MainTex);o.Albedo = c.rgb*_Color;o.Metallic = _Metallic;o.Smoothness = _Glossiness;o.Alpha = c.a;}ENDCG}FallBack "Diffuse"
}

 


http://chatgpt.dhexx.cn/article/VdpLepAP.shtml

相关文章

《实践论》笔记及当下反思(一)

目录 一句话概括核心观点 笔记 1、人的认识&#xff0c;主要地依赖于物质的生产活动 2、只有人们的社会实践&#xff0c;才是人们对于外界认识的真理性的标准 3、强调理论对于实践的依赖关系&#xff0c;理论的基础是实践&#xff0c;又转过来为实践服务 4、你要知道梨子…

有吃的!

问题描述 妇添小有一个很厉害的技能&#xff1a;发现吃的&#xff01;如果有好吃的东西&#xff0c;不论多远&#xff0c;只要一闻就能知道在哪里。这天他刚刚在程设rejudge完&#xff0c;忽然鼻子一抽——有吃的&#xff01;他决定马上赶去吃这么好吃的东西。 语文男为了考验妇…

今天中午吃什么转盘html,吃到撑的几种简单午饭,让你再也不发愁吃什么了!...

大家好这里是树新游四方,今天给大家介绍几款午饭美食! 家里做的饭菜,总是那么乏味,想吃点新鲜的,又不会做,于是来来回回就那么几种菜式,早就吃烦了,今天给大家推出几种简单的菜饭,让你再也不发愁吃什么了。 第一道:蛋包饭 第一次了解这个美食,还是看电视中学到的,自…

如何“吃”掉一本书

不知道小时候你有没有干过这样一件事&#xff0c;用一把剪刀把报纸或杂志上看到的“一片”美好的词汇剪下来&#xff0c;然后贴到一个笔记本里面&#xff0c;这样一年下来&#xff0c;就有厚厚的一本笔记。 后来的你是不是逐渐丧失了这种“技能”了呢&#xff0c;但李敖却一直使…

帧数达不到144用144hz_怎么能一直吃鸡一直爽?144fps+144Hz告诉你结果“帧”香!...

原标题&#xff1a;怎么能一直吃鸡一直爽&#xff1f;144fps144Hz告诉你结果“帧”香&#xff01; 在过去的几年中&#xff0c;“大逃杀”类型的游戏可以说是风靡全球&#xff0c;引来了无数玩家的热捧。像《绝地求生&#xff1a;大逃杀》、《APEX英雄》、《堡垒之夜》&#xf…

《二吃一》游戏加蓝牙代码

转载自&#xff1a;http://www.aisidachina.com/forum/thread-105-1-2.html 二吃一又名四步顶或是四棋&#xff0c;起源于中国民间。规则是两块吃一块&#xff0c;就是在一条直线上的自己的两个棋子可以吃掉对方的一个棋子。在这说一下这个游戏的主要思想和大家分享一下&#…

今天出去吃了一下午小吃~

转载于:https://www.cnblogs.com/keaideweiwei/archive/2012/12/07/2807834.html

吃一吃

1.给/dev/sdb2分区创建文件系统&#xff0c;类型为ext3mkfs -t ext3 /dev/sdb22.列出磁盘分区信息fdisk -l3.找到根目录下用户为root&#xff0c;权限为644的文件&#xff0c;修改权限为其他用户没有权限find / -user root -a -prem 644 -exec chomd 640 {} \;4./etc/passwd文件…

CSS行高的测量

一般默认的字体大小为16px行高是相邻行“基线”之间的距离&#xff0c;是包含行间距的 使用line-height设置行高时&#xff0c;除去字体大小剩下的距离会在行的上边和下边平分&#xff0c;即&#xff1a;行高字体大小上边距下边距&#xff08;同时上边距等于下边距&#xff09;…

css 行高解析

line-height:1.5; //行高不设置单位时&#xff0c;为当前元素字体大小*1.5不是文字撑开了div的高度&#xff0c;而是line-height .test1{font-size:20px; line-height:0; border:1px solid #cccccc; background:#eeeeee;} .test2{font-size:0; line-height:20px; border:1px s…

css设置1.5倍行高,css设定行高、绝对定位

设定行高2种方式 使用width、height(假定现宽38,高22 &#xff1b;目标宽70&#xff0c;高30) .welcome{ width: 70px; height: 30px; line-height: 30px; text-align: center; } 使用padding .welcome{ padding: 4px 16px; line-height: 22px; } 补充&#xff1a; 使用width、…

CSS 行高 line-height属性

在CSS中&#xff0c;通过 line-height属性来定义行高&#xff0c;行高是指相邻两行文本基线之间的垂直距离。 那什么是基线呢&#xff1f;对任何一个行内非替换元素&#xff0c;其内容区都会存在四条假想的线&#xff0c;分别是底线&#xff08;bottom&#xff09;、基线&…

P53-前端基础CSS-行高设置

P53-前端基础CSS-行高设置 1.概述 行高&#xff08;line height&#xff09; 行高指的是文字占有的实际高度可以通过line-height来设置行高 行高可以直接指定一个大小&#xff08;px em&#xff09;也可以直接为行高设置一个整数如果是一个整数的话&#xff0c;行高将会是字体…

CSS行高背景

一.行高 1.定义&#xff1a; 行高上距离内容高度下距离 其中 上距离下距离 2.应用场景 让单行文本在盒子中垂直居中对齐。在浏览器中行高是跟随字体大小变化的。 所以我们让文字的行高等于盒子的高度。 div{ height:50px; line-height:50px; } 3.行高与盒子高度的三种关系 如…

css控制文本的行高

line-height可以控制文本的行高 示例 <p> 这是一个标准行高的段落。 在大多数浏览器默认行高约20 px。 这是一个标准行高的段落。 这是一个标准行高的段落。 </p> <p class"p1"> 这是一个更小行高的段落。 这是一个更小行高的段落。 这是一个更小…

CSS行高line-height属性理解及应用

行高的概念看上去很简单——文字行的高度&#xff0c;其实&#xff0c;行高所涉及到的基础知识&#xff0c;对于今后理解其它属性也很重要。 大片密密麻麻的文字往往会让人觉得乏味&#xff0c;因此适当地调整行高&#xff08;line-height&#xff09;可以减低阅读的困难与枯…

css行高和盒子高区别

行高 什么是行高 在css中所有的行都有自己的行高。 .box1 {border: 1px solid black;width: 200px; }<div class"box1">我是文字</div>被撑起来的高度就是行高 line-height属性 line-height: 60px;80px就是行高 注意点&#xff1a; 1.行高和盒子的…

css 行高

1. 什么是行高&#xff0c;以及行高的概念 我们可以试想一下&#xff0c;为什么会要有行高。我现在不需要行高不是完全可以的嘛。 我们可以仔细看看这个&#xff0c;这不是很正常的嘛。 那我们来看看这个&#xff0c;那当我们第一次看到这个的时候你觉得是横着度&#xff0c;…

css行高(line-height)及文本垂直居中原理

css行高&#xff08;line-height&#xff09;及文本垂直居中原理 一、行高的定义 标准定义&#xff1a;两行文字基线之间的距离。 那么什么是基线&#xff1f; 基线是在英文字母中用到的一个概念&#xff0c;我们刚学英语的时使用的那个英语本子每行有四条线。在 CSS 中&am…

css设置1.5倍行高,CSS怎么控制行高?

CSS怎么控制行高&#xff1f; css中&#xff0c;调整每行文字字体间距(行距)是使用line-height属性。 ● line-height 属性设置行间的距离(行高)。 注&#xff1a;不允许使用负值。 要实现上下换行文字行间距行高样式其实我们只用对文字所在对象设置line-height样式即可&#x…