参考了这篇 https://blog.csdn.net/oldside1/article/details/106456379
导出模型的时候最好勾上导出切线空间和顶点色
这篇有优化方案 https://schemingdeveloper.com/2014/10/17/better-method-recalculate-normals-unity/
https://answers.unity.com/questions/1435670/can-a-vertex-point-vector-have-more-than-one-norma.html
左边没处理,右边处理过
原理
记得补上,先去碎觉😄
其实就是构造TBN矩阵
这里记录了一些tangent.w的用途
https://blog.csdn.net/ronintao/article/details/52136673
实现
编辑器脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
public class VextexColorTool : Editor
{
public static string NewMeshPath = "Assets/Models/VectexCube.asset";
[MenuItem("Tools/SmoothNormalToColor")]
static void SmoothNormalToColor()
{
var trans = Selection.activeTransform;
//获取Mesh
Mesh mesh = new Mesh();
if (trans.GetComponent<SkinnedMeshRenderer>())
{
mesh = trans.GetComponent<SkinnedMeshRenderer>().sharedMesh;
}
if (trans.GetComponent<MeshFilter>())
{
mesh = trans.GetComponent<MeshFilter>().sharedMesh;
}
Debug.Log(mesh.name);
//声明一个Vector3数组,长度与mesh.normals一样,用于存放
//与mesh.vertices中顶点一一对应的光滑处理后的法线值
Vector3[] smoothedNormals = new Vector3[mesh.normals.Length];
//开始一个循环,循环的次数 = mesh.normals.Length = mesh.vertices.Length = meshNormals.Length
for (int i = 0; i < smoothedNormals.Length; i++)
{
//定义一个零值法线
Vector3 smoothedNormal = new Vector3(0, 0, 0);
//遍历mesh.vertices数组,如果遍历到的值与当前序号顶点值相同,则将其对应的法线与Normal相加
for (int j = 0; j < smoothedNormals.Length; j++)
{
if (mesh.vertices[j] == mesh.vertices[i])
{
smoothedNormal += mesh.normals[j];
}
}
//归一化Normal并将meshNormals数列对应位置赋值为Normal,到此序号为i的顶点的对应法线光滑处理完成
//此时求得的法线为模型空间下的法线
smoothedNormal.Normalize();
smoothedNormals[i] = smoothedNormal;
}
//构建模型空间→切线空间的转换矩阵
ArrayList OtoTMatrixs = new ArrayList();
for (int i = 0; i < mesh.normals.Length; i++)
{
Vector3[] OtoTMatrix = new Vector3[3];
OtoTMatrix[0] = new Vector3(mesh.tangents[i].x, mesh.tangents[i].y, mesh.tangents[i].z);
OtoTMatrix[1] = Vector3.Cross(mesh.normals[i], OtoTMatrix[0]) * mesh.tangents[i].w;
OtoTMatrix[2] = mesh.normals[i];
OtoTMatrixs.Add(OtoTMatrix);
}
//将meshNormals数组中的法线值一一与矩阵相乘,求得切线空间下的法线值
for (int i = 0; i < smoothedNormals.Length; i++)
{
Vector3 tNormal;
tNormal = Vector3.zero;
tNormal.x = Vector3.Dot(((Vector3[])OtoTMatrixs[i])[0], smoothedNormals[i]);
tNormal.y = Vector3.Dot(((Vector3[])OtoTMatrixs[i])[1], smoothedNormals[i]);
tNormal.z = Vector3.Dot(((Vector3[])OtoTMatrixs[i])[2], smoothedNormals[i]);
smoothedNormals[i] = tNormal;
}
//新建一个颜色数组把光滑处理后的法线值存入其中
Color[] meshColors = new Color[mesh.colors.Length];
for (int i = 0; i < meshColors.Length; i++)
{
meshColors[i].r = smoothedNormals[i].x * 0.5f + 0.5f;
meshColors[i].g = smoothedNormals[i].y * 0.5f + 0.5f;
meshColors[i].b = smoothedNormals[i].z * 0.5f + 0.5f;
meshColors[i].a = mesh.colors[i].a;
}
//新建一个mesh,将之前mesh的所有信息copy过去
// 如果模型还有blendshape 可以改成 Instansiate法
Mesh newMesh = new Mesh();
newMesh.vertices = mesh.vertices;
newMesh.triangles = mesh.triangles;
newMesh.normals = mesh.normals;
newMesh.tangents = mesh.tangents;
newMesh.uv = mesh.uv;
newMesh.uv2 = mesh.uv2;
newMesh.uv3 = mesh.uv3;
newMesh.uv4 = mesh.uv4;
newMesh.uv5 = mesh.uv5;
newMesh.uv6 = mesh.uv6;
newMesh.uv7 = mesh.uv7;
newMesh.uv8 = mesh.uv8;
//将新模型的颜色赋值为计算好的颜色
newMesh.colors = meshColors;
//newMesh.colors32 = new Color32(meshColors);
newMesh.bounds = mesh.bounds;
newMesh.indexFormat = mesh.indexFormat;
newMesh.bindposes = mesh.bindposes;
newMesh.boneWeights = mesh.boneWeights;
//将新mesh保存为.asset文件,路径可以是"Assets/Character/Shader/VertexColorTest/TestMesh2.asset"
AssetDatabase.CreateAsset(newMesh, NewMeshPath);
AssetDatabase.SaveAssets();
Debug.Log("Done");
}
}
shader
v2f vert(a2v v)
{
v2f o;
// 从顶点颜色中读取法线信息,并将其值范围从0~1还原为-1~1
float3 vertNormal = v.vertexColor.rgb * 2 - 1;
// 使用法线与切线叉乘计算副切线用于构建切线→模型空间转换矩阵
float3 binormal = cross(v.normal, v.tangent) * v.tangent.w;
// 构建切线 -> 模型空间转换矩阵
float3x3 TtoO = float3x3(v.tangent.xyz,
binormal.xyz,
v.normal.xyz);
TtoO = transpose(TtoO);
// 将法线转换到模型空间下
vertNormal = mul(TtoO, vertNormal);
// 模型坐标 + 法线 * 自定义粗细值 * 顶点颜色A通道 = 轮廓线模型
o.vertex = UnityObjectToClipPos(v.vertex + vertNormal *_OutlineWidth);
return o;
}
优化
上一个计算法线的算法事件复杂度是
O
(
n
2
)
O(n^2)
O(n2)
下面我们来优化一下,其实就是空间换时间嘛😀
// 空间换时间 (存下每个法线的相同的顶点索引) 其实就是真正的共享顶点 立方体的话因该是24 / 3=8个共享顶点。
// 这里用到了hash表 (可以去领扣刷刷hash表相关的题,游戏还是会用到的)
Dictionary<Vector3, List<int>> vertexDic = new Dictionary<Vector3, List<int>>();
for (int i = 0; i < mesh.vertices.Length; i++)
{
if (!vertexDic.ContainsKey(mesh.vertices[i]))
{
List<int> vertexIndexs = new List<int>();
vertexIndexs.Add(i);
vertexDic.Add(mesh.vertices[i], vertexIndexs);
}
else
{
vertexDic[mesh.vertices[i]].Add(i);
}
}
// 平均化每个顶点
foreach (var item in vertexDic)
{
Vector3 smoothedNormal = new Vector3(0, 0, 0);
foreach (var index in item.Value)
{
smoothedNormal += mesh.normals[index];
}
// 记得归一化
smoothedNormal.Normalize();
foreach (var index in item.Value)
{
smoothedNormals[index] = smoothedNormal;
}
}
这样事件复杂度就从O(n^2) 降到了 O(n) 降的还是蛮高的
如果加入blendshape文件过大的问题
可以package manage搜fbx导出工具 导出二进制FBX,这样就小了
平滑法线
public static List<Vector3> SmoothNormals(Mesh mesh)
{
// Group vertices by location
var groups = mesh.vertices
.Select((vertex, index) => new KeyValuePair<Vector3, int>(vertex, index))
.GroupBy(pair => pair.Key);
// Copy normals to a new list
var smoothNormals = new List<Vector3>(mesh.normals);
// Average normals for grouped vertices
foreach (var group in groups)
{
// Skip single vertices
// 不注掉的话 角色平滑后的法线再平滑会出问题 神奇的问题
// 有大佬知道为啥请告诉我??? 感激不尽 mail: wssgg13@qq.com
// if (group.Count() == 1) {
// continue;
// }
// Calculate the average normal
var smoothNormal = Vector3.zero;
foreach (var pair in group)
{
smoothNormal += mesh.normals[pair.Value];
}
smoothNormal.Normalize();
// Assign smooth normal to each vertex
foreach (var pair in group)
{
var normals = mesh.normals;
var tangents = mesh.tangents;
var binormal = (Vector3.Cross(normals[pair.Value], tangents[pair.Value]) * tangents[pair.Value].w)
.normalized;
var tbn = new Matrix4x4(
tangents[pair.Value],
binormal,
normals[pair.Value],
Vector4.zero);
tbn = tbn.transpose;
smoothNormals[pair.Value] = tbn.MultiplyVector(smoothNormal).normalized;
}
}
return smoothNormals;
}