这里的代码是在 Unity网格编程笔记[五]网格切割
中整合出来的。
这里的mesh可以直接接入到使用mesh的unity组件
一些基础的属性还是要参考 Unity网格编程笔记[零]网格编程基础知识点
Mesh合并
网格的合并,其实底层也没那么复杂。对于三角面,只是顺序需要重新组织一下,其他的属性都是重新添加就可以了。
三角面的添加做法有点特别。
新添加的网格的三角面顺序不变。但是每个元素的顺序是在原来的三角面的基础上添加的。
这里个人认为应该是在顶点的顺序上添加,即用int length = mesh.vertexCount;
替换下面的int length = triangles.Count;
。经过实践发现可以做到切面正常缝合,切面的uv也没有问题。
个人认为这样改的原因是新添加的网格,他们的顶点顺序都是添加到了原网格的数组里,所以索引值都添加了mesh.vertexCount。那么他们对应的三角面的数组中的对应索引值应该也添加mesh.vertexCount。才能保证添加后的顶点索引能对应上顶点数组。
public void Add(Mesh mesh)
{
for (int i = 0; i < mesh.vertexCount; i++)
{
vertices.Add(mesh.vertices[i]);
uvs.Add(mesh.uv[i]);
normals.Add(mesh.normals[i]);
tangents.Add(mesh.tangents[i]);
}
int length = triangles.Count;
//我替换后也能正确运行的做法 int length = mesh.vertexCount;
for (int i = 0; i < mesh.triangles.Length; i++)
{
triangles.Add(mesh.triangles[i] + length);
}
}
顶点合并
这里的主要思想是将过于靠近并且属性几乎一致的顶点,去除掉,以免造成不必要的计算。
首先给定范围range,对于顶点和uv小于这个范围的纳入考虑范围。个人觉得这个范围分开来用数值表示比较好。
对于纳入范围的顶点,最后考虑他们的法线方向,当点积值等于1的时候,就认为法线方向相同。
这时进行顶点的删除
for (int k = 0; k < triangles.Count; k++)
{
if (triangles[k] == j)
triangles[k] = i;
if (triangles[k] > j)
triangles[k]--;
}
vertices.RemoveAt(j);
normals.RemoveAt(j);
tangents.RemoveAt(j);
uvs.RemoveAt(j);
首先在triangles数组中到前后排序的两个属性相同位置相近的顶点中,将后面排序的顶点替换成前面的。然后替换后,对于遍历之后,位于该顶点在vertex数组之后的顶点,其索引值都减一。
减一的原因是在该顶点从vertex数组中被去除后,vertex数组位于该点后面的顶点在顶点序列中的存储的索引都会减少一位。而triangles中存储的就是顶点在vertex中的顶点索引。
public void CombineVertices(float range)
{
range *= range;
for (int i = 0; i < vertices.Count; i++)
{
for (int j = i + 1; j < vertices.Count; j++)
{
bool dis = (vertices[i] - vertices[j]).sqrMagnitude < range;
bool uv = (uvs[i] - uvs[j]).sqrMagnitude < range;
bool dir = Vector3.Dot(normals[i], normals[j]) > 0.999f;
if (dis && uv && dir)
{
for (int k = 0; k < triangles.Count; k++)
{
if (triangles[k] == j)
triangles[k] = i;
if (triangles[k] > j)
triangles[k]--;
}
vertices.RemoveAt(j);
normals.RemoveAt(j);
tangents.RemoveAt(j);
uvs.RemoveAt(j);
}
}
}
}
面反转
public void Reverse()
{
int count = triangles.Count / 3;
for (int i = 0; i < count; i++)
{
int t = triangles[i * 3 + 2];
triangles[i * 3 + 2] = triangles[i * 3 + 1];
triangles[i * 3 + 1] = t;
}
count = vertices.Count;
for (int i = 0; i < count; i++)
{
normals[i] *= -1;
Vector4 tan = tangents[i];
tan.w = -1;
tangents[i] = tan;
}
}
for (int i = 0; i < count; i++)
{
int t = triangles[i * 3 + 2];
triangles[i * 3 + 2] = triangles[i * 3 + 1];
triangles[i * 3 + 1] = t;
}
这里的面反转,是将triangles数组的每三个顶点中的顺序的其中两个对调。这和面向相机的顶点是顺时针而反向相机的是逆时针的原因有关。
triangles中,每三个顶点是按照索引提升的顺序依次连接成面的。一般情况下,这样的自动连接是自动生成mesh的基础,所以连接顺序决定了面的法线朝向。法线永远是朝向图形学中的正面的。参考
正常情况下,在相机视角中,顶点的连接顺序从顺时针变成逆时针,或者从逆时针变成顺时针,那么这时面就翻转了。所以将三角数组triangles中的任意第二三个顶点调换顺序,后,再次按照索引升高的顺序连接三个顶点,一定会连接顺序变换。
Unity网格编程笔记[零]网格编程基础知识点
在连接顺序翻转之后,相关的属性也要相反,例如法线和切线
for (int i = 0; i < count; i++)
{
normals[i] *= -1;
Vector4 tan = tangents[i];
tan.w = -1;
tangents[i] = tan;
}
法线直接乘以负一反向取反
但是切线的做法我回顾了 Unity学习shader笔记[三十一]关于顶点的法线、切线、副切线
才发现的,这里将翻转之前的面认为是正面了。准确一些的做法应该是
tan.w = - tan.w ;
计算切线
public static Vector4 CalculateTangent(Vector3 normal)
{
Vector3 tan = Vector3.Cross(normal, Vector3.up);
if (tan == Vector3.zero)
tan = Vector3.Cross(normal, Vector3.forward);
tan = Vector3.Cross(tan, normal);
return new Vector4(tan.x, tan.y, tan.z, 1.0f);
}
这里计算切线,可以在世界坐标系或者局部坐标系下计算,结果都是一样的。判断叉乘结果是否等于零。原因是对于法线方向刚好正上方的顶点来说的,这时根据点积的原理,与正上方向量点积就是0.
那么此时该法线要与正前方做叉乘。其实根据法线求切线,求到的都是在一个平面空间下,即垂直于法线的平面空间,只要保证法线 切线 副切线两两垂直即可。所以这里直接用叉乘前或者上向量去求了。
最后再将此时求到的切线与法线叉乘,这是因为unity和maya中,切线空间是左手空间。切线 法线 副切线三者的分布与左手三指张开一样。
参考 Unity学习shader笔记[三十一]关于顶点的法线、切线、副切线
而叉乘的结果是右手定则,所以再次求叉乘,此时到切线与法线以及副切线的分布就与左手空间的三个手指一样了。
UV映射
public void MapperCube(Rect range)
{
if (uvs.Count < vertices.Count)
uvs = new List<Vector2>(vertices.Count);
int count = triangles.Count / 3;
for (int i = 0; i < count; i++)
{
int _i0 = triangles[i * 3];
int _i1 = triangles[i * 3 + 1];
int _i2 = triangles[i * 3 + 2];
Vector3 v0 = vertices[_i0] - center + size / 2f;
Vector3 v1 = vertices[_i1] - center + size / 2f;
Vector3 v2 = vertices[_i2] - center + size / 2f;
v0 = new Vector3(v0.x / size.x, v0.y / size.y, v0.z / size.z);
v1 = new Vector3(v1.x / size.x, v1.y / size.y, v1.z / size.z);
v2 = new Vector3(v2.x / size.x, v2.y / size.y, v2.z / size.z);
Vector3 a = v0 - v1;
Vector3 b = v2 - v1;
Vector3 dir = Vector3.Cross(a, b);
float x = Mathf.Abs(Vector3.Dot(dir, Vector3.right));
float y = Mathf.Abs(Vector3.Dot(dir, Vector3.up));
float z = Mathf.Abs(Vector3.Dot(dir, Vector3.forward));
if (x > y && x > z)
{
uvs[_i0] = new Vector2(v0.z, v0.y);
uvs[_i1] = new Vector2(v1.z, v1.y);
uvs[_i2] = new Vector2(v2.z, v2.y);
}
else if (y > x && y > z)
{
uvs[_i0] = new Vector2(v0.x, v0.z);
uvs[_i1] = new Vector2(v1.x, v1.z);
uvs[_i2] = new Vector2(v2.x, v2.z);
}
else if (z > x && z > y)
{
uvs[_i0] = new Vector2(v0.x, v0.y);
uvs[_i1] = new Vector2(v1.x, v1.y);
uvs[_i2] = new Vector2(v2.x, v2.y);
}
uvs[_i0] = new Vector2(range.xMin + (range.xMax - range.xMin) * uvs[_i0].x, range.yMin + (range.yMax - range.yMin) * uvs[_i0].y);
uvs[_i1] = new Vector2(range.xMin + (range.xMax - range.xMin) * uvs[_i1].x, range.yMin + (range.yMax - range.yMin) * uvs[_i1].y);
uvs[_i2] = new Vector2(range.xMin + (range.xMax - range.xMin) * uvs[_i2].x, range.yMin + (range.yMax - range.yMin) * uvs[_i2].y);
}
}
此代码片段是一个名为MapperCube的方法,它用于在一个立方体上进行UV映射。
首先,方法会检查uvs列表的大小是否小于vertices列表的大小,如果是,则会重新分配足够的空间。
然后,方法通过遍历triangles列表中的三角形索引,获取每个顶点的坐标。
通过将顶点坐标减去中心点并加上尺寸的一半,得到一个以中心点为原点的局部坐标系。
接下来,将局部坐标系中的坐标值除以立方体的尺寸值,得到归一化的UV坐标。
通过计算局部坐标系中两个向量的叉积,得到三角形的法向量。
接着,分别计算法向量在三个坐标轴上的投影长度,并比较三个投影长度的大小,确定UV映射的方式。
如果x投影长度最大,则将顶点的UV坐标设置为(v.z, v.y)。
如果y投影长度最大,则将顶点的UV坐标设置为(v.x, v.z)。
如果z投影长度最大,则将顶点的UV坐标设置为(v.x, v.y)。
最后,根据range矩形范围和计算出的UV坐标进行插值计算,将结果赋值给uvs列表中相应的索引位置。
该方法的作用是将三维立方体上的顶点映射到二维的UV坐标空间中,以便在渲染时进行贴图操作。
在三维计算机图形渲染中,UV映射是将三维模型的顶点映射到二维的纹理坐标空间中的过程。UV坐标确定了模型上每个顶点在纹理上的位置,使得纹理图案可以正确地贴在模型表面上。
以立方体为例,它有六个面,每个面都需要贴上纹理。为了实现这一目标,需要为每个顶点分配一个唯一的UV坐标。然后,根据模型的三角面片(triangles)定义,通过插值计算将UV坐标映射到三角面片上的其他点。
在上述代码中,为了获得每个顶点的UV坐标,首先将顶点的坐标值转换为局部坐标系,即以立方体中心为原点。然后,将局部坐标系中的坐标进行归一化处理,以确保UV坐标在(0,0)到(1,1)的范围内。
接下来,通过计算三角形的法向量及其在三个坐标轴上的投影长度,决定使用哪个坐标轴来映射UV。这样可以确保纹理在所有面片上的方向一致,避免出现纹理贴图的扭曲或扭转现象。
最后,使用给定的矩形范围和计算出的UV坐标进行插值计算,将结果赋值给uvs列表中相应的索引位置。这样,每个顶点都获得了正确的UV坐标,可以与纹理图案进行对应,从而实现正确的纹理贴图。
总的来说,UV映射是为了将模型上的顶点映射到纹理坐标空间中,以便在渲染时可以正确地将纹理贴图在模型表面上展示出来。该方法的目的是计算并为每个顶点分配正确的UV坐标。
所有代码
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
namespace SplitMesh
{/// <summary>
/// Mesh网格数据类
/// </summary>
public class MeshInfo
{
public List<Vector3> vertices;
public List<int> triangles;
public List<Vector2> uvs;
public List<Vector3> normals;
public List<Vector4> tangents;
public Vector3 size, center;
public MeshInfo()
{
vertices = new List<Vector3>();
triangles = new List<int>();
uvs = new List<Vector2>();
normals = new List<Vector3>();
tangents = new List<Vector4>();
size = center = Vector3.zero;
}
public MeshInfo(Mesh mesh)
{
vertices = new List<Vector3>(mesh.vertices);
triangles = new List<int>(mesh.triangles);
uvs = new List<Vector2>(mesh.uv);
normals = new List<Vector3>(mesh.normals);
tangents = new List<Vector4>(mesh.tangents);
center = mesh.bounds.center;
size = mesh.bounds.size;
}
public void Add(Mesh mesh)
{
for (int i = 0; i < mesh.vertexCount; i++)
{
vertices.Add(mesh.vertices[i]);
uvs.Add(mesh.uv[i]);
normals.Add(mesh.normals[i]);
tangents.Add(mesh.tangents[i]);
}
int length = triangles.Count;
//我替换后也能正确运行的做法 int length = mesh.vertexCount;
for (int i = 0; i < mesh.triangles.Length; i++)
{
triangles.Add(mesh.triangles[i] + length);
}
}
public void Add(Vector3 vert, Vector2 uv, Vector3 normal, Vector4 tangent)
{
vertices.Add(vert);
uvs.Add(uv);
normals.Add(normal);
tangents.Add(tangent);
}
public Mesh GetMesh()
{
Mesh mesh = new Mesh();
mesh.vertices = vertices.ToArray();
mesh.uv = uvs.ToArray();
mesh.normals = normals.ToArray();
mesh.tangents = tangents.ToArray();
mesh.triangles = triangles.ToArray();
return mesh;
}
//public void MapperSphere(Rect range){}
public void MapperCube(Rect range)
{
if (uvs.Count < vertices.Count)
uvs = new List<Vector2>(vertices.Count);
int count = triangles.Count / 3;
for (int i = 0; i < count; i++)
{
int _i0 = triangles[i * 3];
int _i1 = triangles[i * 3 + 1];
int _i2 = triangles[i * 3 + 2];
Vector3 v0 = vertices[_i0] - center + size / 2f;
Vector3 v1 = vertices[_i1] - center + size / 2f;
Vector3 v2 = vertices[_i2] - center + size / 2f;
v0 = new Vector3(v0.x / size.x, v0.y / size.y, v0.z / size.z);
v1 = new Vector3(v1.x / size.x, v1.y / size.y, v1.z / size.z);
v2 = new Vector3(v2.x / size.x, v2.y / size.y, v2.z / size.z);
Vector3 a = v0 - v1;
Vector3 b = v2 - v1;
Vector3 dir = Vector3.Cross(a, b);
float x = Mathf.Abs(Vector3.Dot(dir, Vector3.right));
float y = Mathf.Abs(Vector3.Dot(dir, Vector3.up));
float z = Mathf.Abs(Vector3.Dot(dir, Vector3.forward));
if (x > y && x > z)
{
uvs[_i0] = new Vector2(v0.z, v0.y);
uvs[_i1] = new Vector2(v1.z, v1.y);
uvs[_i2] = new Vector2(v2.z, v2.y);
}
else if (y > x && y > z)
{
uvs[_i0] = new Vector2(v0.x, v0.z);
uvs[_i1] = new Vector2(v1.x, v1.z);
uvs[_i2] = new Vector2(v2.x, v2.z);
}
else if (z > x && z > y)
{
uvs[_i0] = new Vector2(v0.x, v0.y);
uvs[_i1] = new Vector2(v1.x, v1.y);
uvs[_i2] = new Vector2(v2.x, v2.y);
}
uvs[_i0] = new Vector2(range.xMin + (range.xMax - range.xMin) * uvs[_i0].x, range.yMin + (range.yMax - range.yMin) * uvs[_i0].y);
uvs[_i1] = new Vector2(range.xMin + (range.xMax - range.xMin) * uvs[_i1].x, range.yMin + (range.yMax - range.yMin) * uvs[_i1].y);
uvs[_i2] = new Vector2(range.xMin + (range.xMax - range.xMin) * uvs[_i2].x, range.yMin + (range.yMax - range.yMin) * uvs[_i2].y);
}
}
public void CombineVertices(float range)
{
range *= range;
for (int i = 0; i < vertices.Count; i++)
{
for (int j = i + 1; j < vertices.Count; j++)
{
bool dis = (vertices[i] - vertices[j]).sqrMagnitude < range;
bool uv = (uvs[i] - uvs[j]).sqrMagnitude < range;
bool dir = Vector3.Dot(normals[i], normals[j]) > 0.999f;
if (dis && uv && dir)
{
for (int k = 0; k < triangles.Count; k++)
{
if (triangles[k] == j)
triangles[k] = i;
if (triangles[k] > j)
triangles[k]--;
}
vertices.RemoveAt(j);
normals.RemoveAt(j);
tangents.RemoveAt(j);
uvs.RemoveAt(j);
}
}
}
}
public void Reverse()
{
int count = triangles.Count / 3;
for (int i = 0; i < count; i++)
{
int t = triangles[i * 3 + 2];
triangles[i * 3 + 2] = triangles[i * 3 + 1];
triangles[i * 3 + 1] = t;
}
count = vertices.Count;
for (int i = 0; i < count; i++)
{
normals[i] *= -1;
Vector4 tan = tangents[i];
tan.w = -1;
tangents[i] = tan;
}
}
public static Vector4 CalculateTangent(Vector3 normal)
{
Vector3 tan = Vector3.Cross(normal, Vector3.up);
if (tan == Vector3.zero)
tan = Vector3.Cross(normal, Vector3.forward);
tan = Vector3.Cross(tan, normal);
return new Vector4(tan.x, tan.y, tan.z, 1.0f);
}
}
}