RecursiveFrog 发表于 2022-5-2 15:10

Unity | 解决描边断开问题

当使用法线外扩描边法时, 会产生断开现象
v.vertex.xyz += v.normal.xyz * _LineWidth * 0.1;


断开

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

需要注意的点:

[*]建议将软边模型的切线空间的法线信息烘焙到顶点色(是个面试考点), 防止蒙皮动画带来变形. 要是偷懒用物体空间法线也不是不行...
[*]烘焙后拿到的顶点色, 需要从映射到[-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 = mesh.vertex_normals.vector
    t = i.tangent
    b = i.bitangent
    n = i.normal
    tn =
    tn = t * dic + t * dic + t * dic
    tn = b * dic + b * dic + b * dic
    tn = n * dic + n * dic + n * dic
    nList.append(tn)
   
n = 0
vl = mesh.vertex_colors#vertex color layer
for i in vl.data:
    i.color = nList * 0.5 + 0.5#x invers
    i.color = nList * 0.5 + 0.5
    i.color = nList * 0.5 + 0.5
    i.color = 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 = t * n_smooth + t * n_smooth + t * n_smooth
    tn = b * n_smooth + b * n_smooth + b * n_smooth
    tn = n * n_smooth + n * n_smooth + n * n_smooth
    nList.append(tn)
   
n = 0
vl = mesh.vertex_colors#vertex color layer
for i in vl.data:
    i.color = nList * 0.5 + 0.5
    i.color = nList * 0.5 + 0.5
    i.color = nList * 0.5 + 0.5
    i.color = 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;
cList = new Color;

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)//若两根法线对应点坐标相同
            {
                nor += nList;//法线累积
            }
      }
      //[-1, 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;


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;
      cList = new Color;

      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)
                {
                  nor += nList;
                }
            }
            //[-1, 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;
      //tbn
      Vector3 t = new Vector3(t4.x, t4.y, t4.z);
      Vector3 n = meshFilter.sharedMesh.normals;
      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"
}

更加完美的解决方案:

zifa2003293 发表于 2022-5-2 15:17

还可以降采样在屏幕空间做

yukamu 发表于 2022-5-2 15:20

blender里计算切线空间的软边法线的确会有问题,blender里的tangent信息和unity里的是不太一样的。[捂脸]
页: [1]
查看完整版本: Unity | 解决描边断开问题