Unity实用案例之——喷漆效果

喷漆功能

**应用场景:**如墙上的标语贴花,汽车上的喷漆等。

选择方案:

  1. 当然实现方法各式各异,最最最简单,也是最“不堪入目”的方法是直接给一个面片,然后获取喷漆位置,加上一个要喷漆表面法线方向的偏移,作为最终面片放置位置,当然,不要忘了设置面片的方向。这种方法虽然说简单,但是效果并不理想,会出经常现与其他物体穿插的情况,如果游戏中曲面太多,那么这个方案基本没法看。
  2. 对于个别特殊的需求来讲,比如说人物身上的纹身,完全可以用一个shader里实现,此方法仅限于一个贴花对应一个物体,如果是一对多的情况,请看后边这两种。
  3. 有一种简易的方法是用Projector,这种方法实现较为简单,不多说。
  4. 接下来说一种动态生成网格方案,也较为常用,接下来就详细说说这种方案。

实现思路:
喷漆的网格是根据场景中所喷位置的物体的网格动态生成的,喷漆的时候,获取规定范围内的物体,再用一个立方体(也可以用球体)去截取这些物体的Mesh,从而构造新的网格,将喷漆渲染在这个Mesh就OK了。

代码实现:
首先,我们需要一个获取规定范围内MeshRenderer的函数:

public GameObject[] GetAffectedObjects(Bounds bounds, LayerMask affectedLayers)
{
	MeshRenderer[] renderers = FindObjectsOfType<MeshRenderer>();
	List<GameObject> objects = new List<GameObject>();
	foreach (Renderer r in renderers)
	{
		if (!r.enabled) continue;
		if ((1 << r.gameObject.layer & affectedLayers.value) == 0) continue;
		if (r.GetComponent<Decal>() != null) continue;

		if (bounds.Intersects(r.bounds))
		{
			objects.Add(r.gameObject);
		}
	}
	return objects.ToArray();
}

然后拿到这些GameObject去做裁剪,裁剪函数:

public void BuildDecal(GameObject affectedObject, bool isLast)
{
	Mesh affectedMesh = affectedObject.GetComponent<MeshFilter>().sharedMesh;
	if (affectedMesh == null) return;

	//这里预存了已获取物体的vertices和triangles,减少了不必要的GC
	Vector3[] vertices = GetVertexList(affectedObject);
	int[] triangles = GetTriangleList(affectedObject);

	//目标顶点转换到当前物体的模型空间
	Matrix4x4 matrix = this.transform.worldToLocalMatrix*affectedObject.transform.localToWorldMatrix;
	//将主要计算移入异步
	Loom.RunAsync(() =>
	{
		for (int i = 0; i < triangles.Length; i += 3)
		{
			int i1 = triangles[i];
			int i2 = triangles[i + 1];
			int i3 = triangles[i + 2];

			Vector3 v1 = matrix.MultiplyPoint(vertices[i1]);
			Vector3 v2 = matrix.MultiplyPoint(vertices[i2]);
			Vector3 v3 = matrix.MultiplyPoint(vertices[i3]);

			Vector3 side1 = v2 - v1;
			Vector3 side2 = v3 - v1;
			Vector3 normal = Vector3.Cross(side1, side2).normalized;

			if (Vector3.Angle(-Vector3.forward, normal) >= maxAngle) continue;

			DecalPolygon poly = new DecalPolygon(v1, v2, v3);

			//用立方体裁剪
			poly = DecalPolygon.ClipPolygon(poly, right);
			if (poly == null) continue;
			poly = DecalPolygon.ClipPolygon(poly, left);
			if (poly == null) continue;
			poly = DecalPolygon.ClipPolygon(poly, top);
			if (poly == null) continue;
			poly = DecalPolygon.ClipPolygon(poly, bottom);
			if (poly == null) continue;
			poly = DecalPolygon.ClipPolygon(poly, front);
			if (poly == null) continue;
			poly = DecalPolygon.ClipPolygon(poly, back);
			if (poly == null) continue;

			AddPolygon(poly, normal);
		}

		if (isLast)
		{
			RenderDecal();
		}
	});
}

DecalPolygon构建了新的三角形(这里注意顶点的空间变换),然后分别用立方体的每一个面去做裁剪,转换成数学算法,其实是判面与面的关系,具体实现:

/// <summary>
/// 两面相交裁剪
/// </summary>
public static DecalPolygon ClipPolygon(DecalPolygon polygon, Plane plane)
{
	//相交为True
	bool[] positive = new bool[9];
	int positiveCount = 0;

	for (int i = 0; i < polygon.vertices.Count; i++)
	{
		positive[i] = !plane.GetSide(polygon.vertices[i]); //不在裁剪面正面,说明有相交
		if (positive[i]) positiveCount++;
	}

	if (positiveCount == 0)
		return null; //全都在裁剪面正面面,不相交
	if (positiveCount == polygon.vertices.Count) return polygon; //全都在裁剪面反面,完全相交

	DecalPolygon tempPolygon = new DecalPolygon();

	for (int i = 0; i < polygon.vertices.Count; i++)
	{
		int next = i + 1;
		next %= polygon.vertices.Count;

		if (positive[i])
		{
			tempPolygon.vertices.Add(polygon.vertices[i]);
		}

		if (positive[i] != positive[next])
		{
			Vector3 v1 = polygon.vertices[next];
			Vector3 v2 = polygon.vertices[i];

			Vector3 v = LineCast(plane, v1, v2);
			tempPolygon.vertices.Add(v);
		}
	}

	return tempPolygon;
}

OK,到这里已经为新的Mesh准备好了所有的数据,接下来将计算好的数据移步到主线程做渲染:

public void RenderDecal()
{
	//主线程渲染
    Loom.QueueOnMainThread(() =>
	{
		if (sprite == null || Renderer == null||filter==null)
        {
            return;
        }
        //生成uv信息
        GenerateTexCoords(0, sprite);
        //距离偏移
		Push(pushDistance);

		Mesh mesh = CreateMesh();
		if (mesh != null) {
			mesh.name = "DecalMesh";
			filter.mesh = mesh;
			Renderer.material = material;
			Renderer.enabled = true;
		}
	});
}

这样,一个喷漆功能就做好了,有几点需要注意是的是:
1.GC的控制
示例:Vector3[] vertices = mesh.vertices;
注意这里不是简单的内存引用,而是会申请新的内存,所以这样的临时变量会造成GC,当物体的顶点上十几K,甚至几十K的时候,这样的GC是吃不消的!为了尽量避免这样的情况,可以做一次预存处理,对没有检测过物体的顶点和三角形数据进行保存,下次用的时候直接取,从而取代mesh.vertices;

2.计算量的问题
还是出于性能的考虑,当与之裁剪的Mesh顶点数太多,在主线程for循环几十K次,不出意外PC端也会卡顿,所以异步是一个较好的选择。复杂的裁剪计算交给其他线程,计算好主线程直接拿数据做渲染;

3.效果问题
由于新生成的喷漆Mesh是由原有物体的mesh裁剪所得的,而这两个Mesh位置是重叠在一起的,两个完全重叠的面,如果其他因变量也相同的情况下,让计算机渲染,计算机也不知道该先渲染哪个,这样就出现z-fighting的问题。所以加一个Push()方法,将新Mesh的顶点沿当前顶点的法线方向挤出一点距离,这样就实现了一个喷漆功能。

  • 1
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
### 回答1: 在Unity游戏开发中,有时候需要将经纬度和Unity坐标进行相互转换。经纬度是用来表示地球上某一点位置的地理坐标,而Unity坐标则是用来表示游戏场景中各个对象的位置。 首先,我们来看如何将经纬度转换为Unity坐标。这个过程涉及到地球模型和坐标系的转换。首先需要了解Unity中使用的坐标系,Unity引擎使用的是左手坐标系,其中X轴是向右的,Y轴是向上的,而Z轴是向前的。 假设我们已经获得了一个地理位置的经纬度,可以通过Unity提供的API将经纬度转换为空间坐标。可以使用Unity的LocationService类来获取设备当前的地理位置信息,其中有一个属性latitude是用来表示纬度的,另一个属性longitude是用来表示经度的。 Unity中的LocationService类还提供了方法将经纬度转换为Unity的空间坐标。具体的转换过程可以使用LocationService的方法:Input.compass.trueHeading可以获取真北指向的方向,然后通过一些数学计算和转换矩阵可以将经纬度转换为Unity坐标。 反过来,如果我们已经有了一个Unity坐标,想要转换为经纬度,也是可以的。可以使用Unity提供的方法将Unity坐标转换为屏幕坐标,然后可以通过一些数学和地理计算,将屏幕坐标转换为经纬度。 总结一下,经纬度和Unity坐标之间的转换是一个比较常见的需求,在Unity游戏开发中可以使用Unity提供的LocationService类和一些数学计算方法来实现。同时,也可以利用一些第三方库来简化这个转换过程。 ### 回答2: Unity是一款流行的游戏引擎,它提供了许多实用功能来帮助开发者创建游戏。其中一个实用功能是经纬度和Unity坐标之间的转换。 经纬度是地球上地理位置的表示方式,由纬度(latitude)和经度(longitude)组成。在许多应用中,我们可能需要将经纬度坐标转换为Unity中的坐标系统,以便在游戏中定位和展示地理位置。 在Unity中,可以使用提供的API函数来进行经纬度和Unity坐标之间的转换。首先,我们可以使用"LocationService"类来获取设备的经纬度信息。通过调用"Input.location.Start()"函数和"Input.location.lastData.latitude"和"Input.location.lastData.longitude"属性,我们可以获取到当前设备的经纬度。 接下来,我们可以将获得的经纬度信息转换为Unity坐标。这可以通过将经纬度映射到Unity的坐标系范围内来实现。一种常见的映射方式是将地球的纬度范围[-90, 90]转换为Unity的Y坐标范围[-10, 10],将地球的经度范围[-180, 180]转换为Unity的X坐标范围[-10, 10]。可以使用简单的比例缩放来完成这个映射过程。 通过将经纬度转换为Unity坐标,我们可以在游戏中使用这些坐标来定位和展示地理位置。例如,我们可以在游戏世界中创建一个物体,并将其放置在经纬度对应的Unity坐标上,以此在游戏中呈现地理位置。这对于开发基于地理位置的游戏和应用程序非常有用。 总之,Unity提供了经纬度和Unity坐标之间转换的实用功能,使得开发者可以方便地在游戏中定位和展示地理位置。通过将经纬度转换为Unity坐标,我们可以使用这些坐标来在游戏中呈现地球上的地理位置。 ### 回答3: 在Unity中,经纬度和Unity坐标之间的转换是一个相当实用的功能,尤其在开发地理位置相关的应用程序时。 首先,我们来看经纬度转换为Unity坐标的过程。经度表示了地球上某个点的东西方向位置,而纬度表示了地球上某个点的南北方向位置。在Unity中,我们可以使用脚本来将经纬度转换为Unity中的坐标。我们可以使用标准的经纬度坐标系,其中纬度的范围是-90到90,经度的范围是-180到180。首先,我们需要确定Unity世界中一个参考点的经纬度,然后计算指定经纬度点与参考点之间的水平和垂直距离,将其转换为Unity坐标的单位。 另一方面,Unity坐标转换为经纬度也是一个常见的需求。在这种情况下,我们需要使用Unity中的Unity API来获取场景中某个物体的位置坐标,然后将其转换为经纬度值。首先,我们需要确定一个参考点的经纬度作为原点,然后计算指定点与参考点之间的水平和垂直距离。最后,通过一些数学运算和公式,我们可以将该距离转换为对应的经度和纬度值。 总结起来,经纬度和Unity坐标之间的转换可以通过一些简单的数学运算和公式实现。这个功能在开发地理位置相关的应用程序时非常有用,例如在虚拟现实游戏中创建基于真实地理位置的游戏世界,或者在实际物理位置点周围放置虚拟对象等等。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值