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;
    }

}

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

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

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
内置的渲染器,HD,URP和轻量级SRP支持 NEW!HDRP矢量位移样本 新!URP和HDRP的镶嵌选项 新!URP的半透明和透射选项 新!新的“开始屏幕”窗口 NEW!轻松的图形共享和画布截图按钮 新功能!SRP包自动导入程序 NEW!与Unity 2019的兼容性 新!支持后期处理堆栈着色器 新功能!与Unity插件中的Substance 兼容 !支持自定义渲染纹理 新增!同时支持高清,URP和轻量级SRP 。多遍模板 !Xbox One / PS4 / Switch支持 新增!地形支持 NEW! 着色器模板 •通用PBR /未照明SRP •通用2D点亮/未照明SRP •HD点亮/未照明/头发/织物/贴花SRP •轻量PBR /未照明SRP •自定义RT初始化/更新 •后处理效果,包括后处理堆栈 • Alpha混合颗粒 •雪碧 •熄灭 •不亮光照贴图 •UI 新工具 •后处理堆栈工具 新样本 •HDRP向量位移 •独立于比例的图块 • Raphael Ernaelsten的体积像素化 •SRP HD全贴图 •马赛克效果 •未使用光照贴图 新模板 •通用PBR /未照明 •HD点亮/ 未照明/毛发/织物 •后处理堆栈 •未照明光图 新节点 •反投影矩阵 •反视图投影矩阵 •HD发射 •Voronoi •渐变 •渐变样本 新增的着色器功能 •反勒普 •随机范围 •SRP附加光 •流量 •旋转 •高处法线 •噪声正弦波 •锯齿波 •方波 •三角波 •棋盘格 •椭圆 •多边形 •矩形 •圆角矩形 最新改进 •添加了专门用于新通用渲染管线的 模板•添加了与Unity HDRP着色器检查器兼容的新HD Lit模板 •无限循环检测现在更快,减少了连接大型图形上的节点时的命中率。 •改进了节点预览渲染刷新行为 •创建了新的标记系统以改善节点搜索 •只需单击“屏幕截图”按钮即可获取整个画布的屏幕截图 •通过“共享”按钮轻松共享图形的选定部分 •添加了新的后处理堆栈工具它会使用给定着色器的PPS渲染器和设置生成cs脚本。 •Amplify Shader Editor通过高达v7.2.x的模板支持HD,Lightweight和Universal RP。 •Legacy HD和Lightweight SRP v3.xx / v4.xx / v5.xx模板也通过Legacy软件包提供。 •通过自定义RT模板支持在Unity 2017及更高版本上使用自定义渲染纹理。 •现在,也可以通过Unity插件中的Substance,在Unity 2018及更高版本的ASE画布上使用此Substance。 •现在在Unity 2018.2及更高版本上可以访问8个UV通道。 •可以通过键盘方向键平移和缩放ASE画布摄像机。 •支持HD PBR SRP模板中的材料类型。 •支持将Specular工作流程集成到Lightweight PBR SRP模板中。 •现在可以直接在模板上添加自定义选项。 了解更多: 在此处放大Wiki 讨论:Unity论坛线程 着色器示例:完整列表 编辑器在将来的更新中将继续得到改进,当前正在开发许多功能。 显着功能 •完整的源代码 •支持Xbox One / PS4 / Switch • 自定义节点API • 着色器模板 • 着色器功能 •多窗口支持 •直观,熟悉的节点界面 •广泛的节点库 •实例化支持 •用户贡献的节点和着色器 •不断增加的样本收集

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值