找回密码
 立即注册
查看: 390|回复: 2

Unity | 解决描边断开问题

[复制链接]
发表于 2022-5-2 15:10 | 显示全部楼层 |阅读模式
当使用法线外扩描边法时, 会产生断开现象
v.vertex.xyz += v.normal.xyz * _LineWidth * 0.1;


断开

<hr/>解法1:
跟美术策划商量, 不要用硬边(误
<hr/>
解法2:
在dcc软件里, 将软边模型的法线信息, 烘焙到硬边模型(引擎里需要用的模型)的顶点色里, 在shader进行外扩描外扩时, 朝顶点色映射后的方向扩.

需要注意的点:

  • 建议将软边模型的切线空间的法线信息烘焙到顶点色(是个面试考点), 防止蒙皮动画带来变形. 要是偷懒用物体空间法线也不是不行...
  • 烘焙后拿到的顶点色, 需要从[0, 1]映射到[-1, 1]再用(别忘了, 别忘了, 别忘了!)
  • 烘焙可能出现难以解决的错误(本人对dcc的熟悉度, 暂时还不能解决这个烘焙问题...)
  • 需要有uv map



  • 以上注意点不代表所有注意点, 只因本人目前只遇到这些坑
引擎里使用的shader见后文.

我用的是blender来烘焙顶点色的, 但存在肉眼可见的误差, 所以这里提供dcc脚本的方法, 若在引擎的效果错误, 则检查一下dcc里顶点色layer是否正确.
模型空间(效果正确):
import bpy
#切线空间顶点色烘焙
mesh = bpy.context.object.data #mesh
bpy.ops.mesh.vertex_color_add()
#bpy.ops.mesh.uv_texture_add()
mesh.calc_tangents()
mesh.calc_normals()

dic = {}
nList = []
for i in mesh.loops:
    dic[i.index] = mesh.vertex_normals[i.vertex_index].vector
    t = i.tangent
    b = i.bitangent
    n = i.normal
    tn = [0, 0, 0]
    tn[0] = t[0] * dic[i.index][0] + t[1] * dic[i.index][1] + t[2] * dic[i.index][2]
    tn[1] = b[0] * dic[i.index][0] + b[1] * dic[i.index][1] + b[2] * dic[i.index][2]
    tn[2] = n[0] * dic[i.index][0] + n[1] * dic[i.index][1] + n[2] * dic[i.index][2]
    nList.append(tn)
   
n = 0
vl = mesh.vertex_colors[0]#vertex color layer
for i in vl.data:
    i.color[0] = nList[n][0] * 0.5 + 0.5#x invers
    i.color[1] = nList[n][1] * 0.5 + 0.5
    i.color[2] = nList[n][2] * 0.5 + 0.5
    i.color[3] = 1
    n += 1
    pass


切线空间(有些许误差):
import bpy
import mathutils

mesh = bpy.context.object.data #mesh
bpy.ops.mesh.vertex_color_add()
#bpy.ops.mesh.uv_texture_add()
mesh.calc_tangents()
mesh.calc_normals()


nList = []
for i in mesh.loops:
    n_smooth = mathutils.Vector((0, 0, 0))
    for j in mesh.loops:
        if i.vertex_index == j.vertex_index:
            n_smooth += j.normal        
    n_smooth.normalize()
    t = i.tangent
    n = i.normal
    b = i.bitangent_sign * n.cross(t)   
    tn = mathutils.Vector((0, 0, 0))
    tn[0] = t[0] * n_smooth[0] + t[1] * n_smooth[1] + t[2] * n_smooth[2]
    tn[1] = b[0] * n_smooth[0] + b[1] * n_smooth[1] + b[2] * n_smooth[2]
    tn[2] = n[0] * n_smooth[0] + n[1] * n_smooth[1] + n[2] * n_smooth[2]
    nList.append(tn)
   
n = 0
vl = mesh.vertex_colors[0]#vertex color layer
for i in vl.data:
    i.color[0] = nList[n][0] * 0.5 + 0.5
    i.color[1] = nList[n][1] * 0.5 + 0.5
    i.color[2] = nList[n][2] * 0.5 + 0.5
    i.color[3] = 1
    n += 1
    pass

虽然上面这张图肉眼看来没问题, 但细细观察(下图), 还是有断开地方, 初步判定是, 因为模型空间向切线空间转换过程出现误差(目前还没找到解决方法, 之后会了再补上...)



<hr/>解法3:
用代码, 计算出模型平滑后的法线, 用平滑法线替换模型法线, 或填充到顶点色, 都可以. 即, 在引擎里, 做 解法2中dcc代码部分
这种方法大概率不会出错 (为什么说是大概率, 因为本人做了一个垃圾模型, 出现了走样...但此外大部分模型是不会走样的), 如果出错, 可以优先检查一下uv map

平滑法线参考:

本文代码的思路:

  • 遍历模型的每一个vertices
  • 遍历模型的每一个normals
  • 将每一个normal与模型的所有normal对比, 若两个normal对应的vertice位置相同, 则认为两个normal是同一个顶点为平滑的法线, 需要被平滑
  • 将一个顶点所有未平滑的法线相加, 归一化, 得到平滑后的法线
  • 将平滑后的法线信息, 写到模型顶点色
  • 模型材质shader中, 将模型顶点, 沿着顶点色映射成向量的方向, 外扩
若储存切线空间法线, 则需要模型展uv
private Vector3[] nList;
private Vector3[] vList;
private Color[] cList;
private Color[] ori_cList;

//......

nList = meshFilter.sharedMesh.normals;
vList = meshFilter.sharedMesh.vertices;
ori_cList = new Color[vList.Length];
cList = new Color[vList.Length];

for (int i = 0; i < ori_cList.Length; i++)
{
    ori_cList = new Color(0.5f, 0.5f, 0.5f);
}


//......



private void SmoothData()
{
    for (int i = 0; i < nList.Length; i++)//遍历每一根法线
    {
        Vector3 nor = Vector3.zero;
        for (int j = 0; j < nList.Length; j++)//将当前取出的法线, 与模型所有法线对比
        {
            if(vList == vList[j])//若两根法线对应点坐标相同
            {
                nor += nList[j];//法线累积
            }
        }
        //[-1, 1] -> [0, 1]
        //obj -> tangent
        Vector3 nCol = Obj2Tangent(nor.normalized, i) * 0.5f + Vector3.one * 0.5f;//所有累积的法线归一化, 转换到切线空间
        cList = new Color(nCol.x, nCol.y, nCol.z);//填充到顶点色
    }
    meshFilter.sharedMesh.SetColors(cList);//修改模型顶点色
}

在c#遍历模型sharedMesh的所有vertices, 与normals时, 发现两种数量是一样的, 不用担心顶点比法线少, 对比顶点时产生越界问题.
例如下图, 红框的一个顶点, 实际代表3个vertices(该点与3边连接, 每个边拆出1个), 3个normals


效果:


有些时候不希望内部出现顶点挤出现象, 可以用模板测试去掉





本人菜鸡...上述代码的效率还有待优化, 高模用这个脚本会卡上很久...高模慎用! dcc修改顶点色的方法比引擎修改的方法效率高点.
但各文章解决外扩断开的核心基本一致, 用一个平滑后的法线替换原法线
完整代码:
c#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[ExecuteInEditMode]
public class Outline : MonoBehaviour
{
    private MeshFilter meshFilter;
    private Vector3[] nList;
    private Vector3[] vList;
    private Color[] cList;
    private Color[] ori_cList;

    private void OnEnable()
    {        
        InitData();
        SmoothData();
    }
    private void OnDisable()
    {
                if (meshFilter == null) return;
        ResetData();
    }

    private void InitData()
    {
        meshFilter = GetComponent<MeshFilter>();
                if (meshFilter == null)
                {
                        enabled = false;
                        return;
                }               
        nList = meshFilter.sharedMesh.normals;
        vList = meshFilter.sharedMesh.vertices;
        ori_cList = new Color[vList.Length];
        cList = new Color[vList.Length];

        for (int i = 0; i < ori_cList.Length; i++)
        {
            ori_cList = new Color(0.5f, 0.5f, 0.5f);
        }
    }

    private void SmoothData()
    {
        for (int i = 0; i < nList.Length; i++)
        {
            Vector3 nor = Vector3.zero;
            for (int j = 0; j < nList.Length; j++)
            {
                if(vList == vList[j])
                {
                    nor += nList[j];
                }
            }
            //[-1, 1] -> [0, 1]
            //obj -> tangent
            Vector3 nCol = Obj2Tangent(nor.normalized, i) * 0.5f + Vector3.one * 0.5f;
            cList = new Color(nCol.x, nCol.y, nCol.z);
        }
        meshFilter.sharedMesh.SetColors(cList);//set data
    }

    private Vector3 Obj2Tangent(Vector3 ori, int id)
    {
        Vector4 t4 = meshFilter.sharedMesh.tangents[id];
        //tbn
        Vector3 t = new Vector3(t4.x, t4.y, t4.z);
        Vector3 n = meshFilter.sharedMesh.normals[id];
        Vector3 b = Vector3.Cross(n, t) * t4.w;

        Vector3 tNor = Vector3.zero;
        tNor.x = t.x * ori.x + t.y * ori.y + t.z * ori.z;
        tNor.y = b.x * ori.x + b.y * ori.y + b.z * ori.z;
        tNor.z = n.x * ori.x + n.y * ori.y + n.z * ori.z;

        return tNor;
    }

    private void ResetData()
    {
        meshFilter.sharedMesh.SetColors(ori_cList);
    }
}
shader
Shader "Outline/Outline_Surf"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0

        _LineCol ("Line Color", Color) = (1, 1, 1, 1)
        _LineWidth ("Line Width", Float) = 1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200
        Stencil
        {
            Ref 1
            Comp GEqual //该ref值(1)比缓冲中的值大于等于时通过
            Pass Replace
        }
        CGPROGRAM
        #pragma surface surf Standard fullforwardshadows addshadow

        struct Input
        {
            float2 uv_MainTex;
        };

        sampler2D _MainTex;
        float4 _Color;
        float _Glossiness, _Metallic;

        UNITY_INSTANCING_BUFFER_START(Props)
        UNITY_INSTANCING_BUFFER_END(Props)

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG

        //outline pass
        Tags { "RenderType"="Opaque" }
        Cull Front        
        Stencil
        {
            Ref 0
            Comp Equal
            Pass Keep
        }
        CGPROGRAM
        #pragma surface surf Standard fullforwardshadows vertex:vert
               
        struct Input
        {
            float2 uv_MainTex;
        };
        
        float4 _LineCol;
        float _LineWidth;
        
        UNITY_INSTANCING_BUFFER_START(Props)
        UNITY_INSTANCING_BUFFER_END(Props)

        void vert(inout appdata_full v)
        {
            float3 t = v.tangent.xyz;
            float3 n = v.normal;
            float3 b = cross(n, t) * v.tangent.w;
            float3x3 TBN_Line = float3x3(t, b, n);//tangent to obj

            float3 dir = mul((v.color.xyz * 2 - 1), TBN_Line);

            v.vertex.xyz += dir * _LineWidth * 0.1;
        }
        
        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            o.Albedo = 0;
            o.Metallic = 0;
            o.Smoothness = 0;
            o.Alpha = 1;
            o.Emission = _LineCol;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

更加完美的解决方案:

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2022-5-2 15:17 | 显示全部楼层
还可以降采样在屏幕空间做
发表于 2022-5-2 15:20 | 显示全部楼层
blender里计算切线空间的软边法线的确会有问题,blender里的tangent信息和unity里的是不太一样的。[捂脸]
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-22 11:30 , Processed in 0.066879 second(s), 23 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表