本文实现的是一个Mesh Decal方法的贴花方案,参考了本篇博文链接: unity的贴花方案。链接的文章是转载的,我并没有找到原文地址。本篇文章主要是学习和自己的理解为主。
先看未贴花之前的效果
这里的思路是,给在Cube里的面生成一个新的Mesh来渲染贴花的材质(比如要的是一个血液的效果),那么这个新的Mesh怎么生成呢?其实就是遍历场景中的所有MeshRenderer,获得与Cube的Mesh相交的Mesh(这一步我们可以通过Unity内置的bounds.Intersects实现)进行新的Decal Mesh生成,我们需要给Decal Mesh计算顶点,法线,UV,以及三角形信息,关键的一点在于与Cube相交的三角形我们需要对其进行裁剪操作,其他的三角形在外面的就丢弃,在里面的就保留。最后我们把这个新的Decal Mesh替换掉Cube原来的ShareMesh并解决Z-Fighting现象就大功告成了。这个方法对Shader其实没有要求,用Standard的都行。
经过上面的处理我们就可以得到下面的效果
可以看到第一张图的贴图被拉长了,这是因为我们自己生成的UV信息的原故,解决办法我暂时不知道。
然后是代码
using System.Collections.Generic;
using UnityEngine;
public class MeshDecal : MonoBehaviour {
private MeshFilter meshFilter = null;
private MeshRenderer meshRenderer = null;
private Mesh currMesh = null;
private List<Vector3> vertices = new List<Vector3>();//顶点信息
private List<Vector3> normals = new List<Vector3>();//法线信息
private List<int> indices = new List<int>();//三角形信息
private List<Vector2> texcoords = new List<Vector2>();//UV信息
private void Awake()
{
meshFilter = GetComponent<MeshFilter>();
meshRenderer = GetComponent<MeshRenderer>();
}
[ContextMenu("Gen Decal")]
public void GetTargetObejcts()
{
MeshRenderer[] mrs = FindObjectsOfType<MeshRenderer>();
for (int i=0;i<mrs.Length;i++)
{
//剔除Decal自身
if (mrs[i].transform.CompareTag("Projection"))
continue;
//遍历所有的MeshRenderer判断和自身立方体相交的Mesh进行Decal Mesh生成
if (meshRenderer.bounds.Intersects((mrs[i].bounds)))
{
GenerateDecalMesh((mrs[i]));
}
}
//将存储的数据生成Unity使用的Mesh
GenerateUnityMesh();
}
public void GenerateDecalMesh(MeshRenderer target)
{
Mesh mesh = target.GetComponent<MeshFilter>().sharedMesh;
//GC很高,可以优化
Vector3[] meshVertices = mesh.vertices;
int[] meshTriangles = mesh.triangles;
Matrix4x4 targetToDecalMatrix = transform.worldToLocalMatrix * target.transform.localToWorldMatrix;
for (int i = 0; i < meshTriangles.Length; i = i + 3)
{
int index1 = meshTriangles[i];
int index2 = meshTriangles[i + 1];
int index3 = meshTriangles[i + 2];
Vector3 vertex1 = meshVertices[index1];
Vector3 vertex2 = meshVertices[index2];
Vector3 vertex3 = meshVertices[index3];
//将网格的三角形转化到Decal自身立方体的坐标系中
vertex1 = targetToDecalMatrix.MultiplyPoint(vertex1);
vertex2 = targetToDecalMatrix.MultiplyPoint(vertex2);
vertex3 = targetToDecalMatrix.MultiplyPoint(vertex3);
Vector3 dir1 = vertex1 - vertex2;
Vector3 dir2 = vertex1 - vertex3;
Vector3 normalDir = Vector3.Cross(dir1, dir2).normalized;
var vectorList = new List<Vector3>();
vectorList.Add(vertex1);
vectorList.Add(vertex2);
vectorList.Add(vertex3);
CollisionChecker.CheckCollision(vectorList);
if (vectorList.Count > 0)
AddPolygon(vectorList.ToArray(), normalDir);
}
}
public void AddPolygon(Vector3[] poly, Vector3 normal)
{
int ind1 = AddVertex(poly[0], normal);
for (int i = 1; i < poly.Length - 1; i++)
{
print(i);
int ind2 = AddVertex(poly[i], normal);
int ind3 = AddVertex(poly[i + 1], normal);
indices.Add(ind1);
indices.Add(ind2);
indices.Add(ind3);
}
}
private int AddVertex(Vector3 vertex, Vector3 normal)
{
//优先寻找是否包含该顶点
int index = FindVertex(vertex);
if (index == -1)
{
vertices.Add(vertex);
normals.Add(normal);
//物体空间的坐标作为uv,需要从(-0.5,0.5)转化到(0,1)区间
float u = Mathf.Lerp(0.0f, 1.0f, vertex.x + 0.5f);
float v = Mathf.Lerp(0.0f, 1.0f, vertex.z + 0.5f);
texcoords.Add(new Vector2(u, v));
return vertices.Count - 1;
}
else
{
//已包含时,将该顶点的法线与新插入的顶点进行平均,共享的顶点,需要修改法线
normals[index] = (normals[index] + normal).normalized;
return index;
}
}
private int FindVertex(Vector3 vertex)
{
for (int i = 0; i < vertices.Count; i++)
{
if (Vector3.Distance(vertices[i], vertex) < 0.01f) return i;
}
return -1;
}
public void HandleZFighting(float distance)
{
for (int i = 0; i < vertices.Count; i++)
{
vertices[i] += normals[i] * distance;
}
}
public void GenerateUnityMesh()
{
currMesh = new Mesh();
HandleZFighting(0.001f);
currMesh.Clear(true);
currMesh.vertices = vertices.ToArray();
currMesh.normals = normals.ToArray();
currMesh.triangles = indices.ToArray();
currMesh.uv = texcoords.ToArray();
vertices.Clear();
normals.Clear();
indices.Clear();
texcoords.Clear();
meshFilter.sharedMesh = currMesh;
}
}
using System.Collections.Generic;
using UnityEngine;
public class CollisionChecker
{
private static List<Plane> planList = new List<Plane>();
static CollisionChecker()//Cube长度为1的六个面
{
//front
planList.Add(new Plane(Vector3.forward, 0.5f));
//back
planList.Add(new Plane(Vector3.back, 0.5f));
//up
planList.Add(new Plane(Vector3.up, 0.5f));
//down
planList.Add(new Plane(Vector3.down, 0.5f));
//left
planList.Add(new Plane(Vector3.left, 0.5f));
//right
planList.Add(new Plane(Vector3.right, 0.5f));
}
private static void CheckCollision(Plane plane, List<Vector3> vectorList)
{
var newList = new List<Vector3>();
for (int current = 0; current < vectorList.Count; current++)
{
int next = (current + 1) % vectorList.Count;
Vector3 v1 = vectorList[current];
Vector3 v2 = vectorList[next];
bool currentPointIn = plane.GetSide(v1);
if (currentPointIn == true)
newList.Add(v1);
if (plane.GetSide(v2) != currentPointIn)
{
float distance;
Ray ray = new Ray(v1, v2 - v1);
plane.Raycast(ray, out distance);
Vector3 newPoint = ray.GetPoint(distance);
newList.Add(newPoint);
}
}
vectorList.Clear();
vectorList.AddRange(newList);
}
public static void CheckCollision(List<Vector3> vectorList)
{
for (int i=0;i<planList.Count;i++)
{
CheckCollision(planList[i], vectorList);
}
}
}
接下来我就按照程序执行的顺序来解释一下每个部分的关键代码
[ContextMenu("Gen Decal")]
public void GetTargetObejcts()
{
MeshRenderer[] mrs = FindObjectsOfType<MeshRenderer>();
for (int i=0;i<mrs.Length;i++)
{
//剔除Decal自身,给自身附上一个“Projection”的Tag
if (mrs[i].transform.CompareTag("Projection"))
continue;
//遍历所有的MeshRenderer判断和自身立方体相交的Mesh进行Decal Mesh生成
if (meshRenderer.bounds.Intersects((mrs[i].bounds)))
{
GenerateDecalMesh((mrs[i]));
}
}
//将存储的数据生成Unity使用的Mesh
GenerateUnityMesh();
}
[ContextMenu("Gen Decal")] 一个编辑器扩展指令,C#里叫特性,我也不太了解,反正作用是在Inspector窗口
右键键点击代码可以看到窗口最下面多了一个“Gen Decal”操作,点他这个函数就运行一次
这段代码遍历了除自己以外的所有MeshRenderer找出相交的MeshRenderer进行下一步
public void GenerateDecalMesh(MeshRenderer target)
{
Mesh mesh = target.GetComponent<MeshFilter>().sharedMesh;
//GC很高,可以优化
Vector3[] meshVertices = mesh.vertices;
int[] meshTriangles = mesh.triangles;
Matrix4x4 targetToDecalMatrix = transform.worldToLocalMatrix
* target.transform.localToWorldMatrix;
for (int i = 0; i < meshTriangles.Length; i = i + 3)
{
int index1 = meshTriangles[i];
int index2 = meshTriangles[i + 1];
int index3 = meshTriangles[i + 2];
//因为meshTriangles存储的是meshVertices的索引,可根据索引获取meshVertices中的顶点信息
Vector3 vertex1 = meshVertices[index1];
Vector3 vertex2 = meshVertices[index2];
Vector3 vertex3 = meshVertices[index3];
//将网格的三角形转化到Decal自身立方体的坐标系中
vertex1 = targetToDecalMatrix.MultiplyPoint(vertex1);
vertex2 = targetToDecalMatrix.MultiplyPoint(vertex2);
vertex3 = targetToDecalMatrix.MultiplyPoint(vertex3);
Vector3 dir1 = vertex1 - vertex2;
Vector3 dir2 = vertex1 - vertex3;
Vector3 normalDir = Vector3.Cross(dir1, dir2).normalized;
var vectorList = new List<Vector3>();
vectorList.Add(vertex1);
vectorList.Add(vertex2);
vectorList.Add(vertex3);
CollisionChecker.CheckCollision(vectorList);
if (vectorList.Count > 0)
AddPolygon(vectorList.ToArray(), normalDir);
}
}
计算从传进来的MeshRenderer模型空间转换到目标Mesh模型空间(即Decal Mesh)的矩阵
Matrix4x4 targetToDecalMatrix = transform.worldToLocalMatrix
* target.transform.localToWorldMatrix;
循环里按照一个三角形三个一组遍历顶点信息,并把它们与上面计算的矩阵进行相乘以转换到目标坐标系
再根据叉积获取法线,并将顶点添加到vectorList,传给CollisionChecker.CheckCollision(vectorList)进行
检测和裁剪,然后执行AddPolygon(vectorList.ToArray(), normalDir)
public static void CheckCollision(List<Vector3> vectorList)
{
for (int i=0;i<planList.Count;i++)
{
CheckCollision(planList[i], vectorList);//六个面各自判断
}
}
private static void CheckCollision(Plane plane, List<Vector3> vectorList)
{
var newList = new List<Vector3>();
for (int current = 0; current < vectorList.Count; current++)
{
int next = (current + 1) % vectorList.Count;
Vector3 v1 = vectorList[current];
Vector3 v2 = vectorList[next];
bool currentPointIn = plane.GetSide(v1);
if (currentPointIn == true)
newList.Add(v1);
if (plane.GetSide(v2) != currentPointIn)//如果一个在外面,一个在里面就生成射线
{
float distance;
Ray ray = new Ray(v1, v2 - v1);
plane.Raycast(ray, out distance);
Vector3 newPoint = ray.GetPoint(distance);
newList.Add(newPoint);
}
}
vectorList.Clear();
vectorList.AddRange(newList);
}
for (int current = 0; current < vectorList.Count; current++)
循环里两个两个顶点连线,共得三条线,用射线检测碰撞点并获取该点作为新顶点,最后得到newList的顶点会有
三种情况,4、3、0,4和3的情况都会进入下一个步骤
public void AddPolygon(Vector3[] poly, Vector3 normal)
{
int ind1 = AddVertex(poly[0], normal);
for (int i = 1; i < poly.Length - 1; i++)
{
int ind2 = AddVertex(poly[i], normal);
int ind3 = AddVertex(poly[i + 1], normal);
indices.Add(ind1);
indices.Add(ind2);
indices.Add(ind3);
}
}
private int AddVertex(Vector3 vertex, Vector3 normal)
{
//优先寻找是否包含该顶点
int index = FindVertex(vertex);
if (index == -1)
{
vertices.Add(vertex);
normals.Add(normal);
//物体空间的坐标作为uv,需要从(-0.5,0.5)转化到(0,1)区间
float u = Mathf.Lerp(0.0f, 1.0f, vertex.x + 0.5f);
float v = Mathf.Lerp(0.0f, 1.0f, vertex.z + 0.5f);
texcoords.Add(new Vector2(u, v));
return vertices.Count - 1;
}
else
{
//已包含时,将该顶点的法线与新插入的顶点进行平均,共享的顶点,需要修改法线
normals[index] = (normals[index] + normal).normalized;
return index;
}
}
在这里我们先对顶点进行操作,查询该顶点是否是计算过的,如果不是则要将他的法线和位置添加到表中并为他计算UV
(在这里我用了x和z轴,并将它们转化到0,1范围,这就是为什么会出现开始时贴图拉伸的情况,在x,z相同的情况下同
一高度其UV值是一样的),如果是就要根据返回的的索引对法线进行平均。
然后是对三角形顶点数据生成,因为经过裁剪会有4、3个顶点的情况,所以写了一个循环。当为4循环两次,为3循环一次。
在这里插入代码片
public void HandleZFighting(float distance)
{
//解决Z-Fighting现象现象,每个顶点朝法线向外偏一点点
for (int i = 0; i < vertices.Count; i++)
{
vertices[i] += normals[i] * distance;
}
}
public void GenerateUnityMesh()
{
currMesh = new Mesh();
HandleZFighting(0.001f);
currMesh.Clear(true);
currMesh.vertices = vertices.ToArray();
currMesh.normals = normals.ToArray();
currMesh.triangles = indices.ToArray();
currMesh.uv = texcoords.ToArray();
vertices.Clear();
normals.Clear();
indices.Clear();
texcoords.Clear();
meshFilter.sharedMesh = currMesh;
}
}
最后一部分就比较简单,就是赋值,没什么可说的。
本来想一次性写完,结果还是偷懒了…