**应用场景:**如墙上的标语贴花,汽车上的喷漆等。
选择方案:
实现思路:
喷漆的网格是根据场景中所喷位置的物体的网格动态生成的,喷漆的时候,获取规定范围内的物体,再用一个立方体(也可以用球体)去截取这些物体的Mesh,从而构造新的网格,将喷漆渲染在这个Mesh就OK了。
代码实现:
首先,我们需要一个获取规定范围内MeshRenderer的函数:
public GameObject[] GetAffectedObjects(Bounds bounds, LayerMask affectedLayers)
{
MeshRenderer[] renderers = FindObjectsOfType();
List objects = new List();
foreach (Renderer r in renderers)
{
if (!r.enabled) continue;
if ((1 << r.gameObject.layer & affectedLayers.value) == 0) continue;
if (r.GetComponent() != 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().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构建了新的三角形(这里注意顶点的空间变换),然后分别用立方体的每一个面去做裁剪,转换成数学算法,其实是判面与面的关系,具体实现:
///
/// 两面相交裁剪
///
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的顶点沿当前顶点的法线方向挤出一点距离,这样就实现了一个喷漆功能。