Unity Shader 贴花效果(一)

本文实现的是一个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;
    }

}

最后一部分就比较简单,就是赋值,没什么可说的。

本来想一次性写完,结果还是偷懒了…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值