Skip to content

Instantly share code, notes, and snippets.

@andyzhshg
Created June 17, 2024 11:25
Show Gist options
  • Save andyzhshg/82793fa301b37d802bba7b673c3c6a1d to your computer and use it in GitHub Desktop.
Save andyzhshg/82793fa301b37d802bba7b673c3c6a1d to your computer and use it in GitHub Desktop.
Build a Perfect Round Cornered Cube in Unity with Code
using UnityEngine;
// unity 采用左手坐标系
// +x -> 右
// +y -> 上
// +z -> 屏幕内
// 计算顺序原则
// x -> y -> z 升序
// 顶点编号
// 0 左下外
// 1 左下内
// 2 左上外
// 3 左上内
// 4 右下外
// 5 右下内
// 6 右上外
// 7 右上内
// 面编号
// 0 -X 左
// 1 -Y 下
// 2 -Z 外
// 3 +X 右
// 4 +Y 上
// 5 +Z 内
public class RoundedCube : MonoBehaviour
{
[Range(0f, 0.5f)]
[Tooltip("倒角半径,值为正方体边长的百分比")]
public float radius = 0.1f;
[Range(0.01f, 10f)]
public float size = 1f;
[Range(2, 32)]
public int segments = 8;
private float lastRadius;
private int lastSegments;
private float lastSize;
void Start()
{
MeshFilter meshFilter = gameObject.AddComponent<MeshFilter>();
MeshRenderer meshRenderer = gameObject.AddComponent<MeshRenderer>();
meshRenderer.material = new Material(Shader.Find("Standard"));
meshFilter.mesh = CreateSpheerPart();
}
bool IsChanged()
{
return lastRadius != radius || lastSegments != segments || lastSize != size;
}
Mesh CreateSpheerPart()
{
lastRadius = radius;
lastSegments = segments;
lastSize = size;
RoundMeshMaker maker = new(size, radius, segments);
return maker.GetMesh();
}
void Update()
{
if (IsChanged())
{
MeshFilter meshFilter = gameObject.GetComponent<MeshFilter>();
meshFilter.mesh = CreateSpheerPart();
}
}
}
using System.Collections.Generic;
using UnityEngine;
public class RoundMeshMaker
{
private readonly float size;
private readonly float radius;
private readonly int cornerDivide;
readonly List<(Vector2, Vector2)> faceUVs;
// constructor
public RoundMeshMaker(float size, float radius, int cornerDivide = 8, List<(Vector2, Vector2)> faceUVs = null)
{
this.size = size;
this.radius = radius;
this.cornerDivide = cornerDivide;
this.faceUVs = faceUVs ?? defaultFaceUVs;
}
// 6 个面的法向量
static readonly List<Vector3> faces = new()
{
new Vector3(-1f, 0f, 0f), // 0 -X 左
new Vector3(0f, -1f, 0f), // 1 -Y 下
new Vector3(0f, 0f, -1f), // 2 -Z 外
new Vector3(1f, 0f, 0f), // 3 +X 右
new Vector3(0f, 1f, 0f), // 4 +Y 上
new Vector3(0f, 0f, 1f), // 5 +Z 内
};
// 8 个顶点的球面三角形的顶点顺序
static readonly List<(int, int, int)> cornerWise = new()
{
(0, 2, 1), // 0 左下外
(0, 1, 5), // 1 左下内
(0, 4, 2), // 2 左上外
(0, 5, 4), // 3 左上内
(3, 1, 2), // 4 右下外
(3, 5, 1), // 5 右下内
(3, 2, 4), // 6 右上外
(3, 4, 5), // 7 右上内
};
// 1----2
// | \ |
// 0----3
static readonly List<(int, int, int, int)> faceCorners = new() {
(1, 3, 2, 0), // 0 -X 左
(1, 0, 4, 5), // 1 -Y 下
(0, 2, 6, 4), // 2 -Z 外
(4, 6, 7, 5), // 3 +X 右
(2, 3, 7, 6), // 4 +Y 上
(5, 7, 3, 1), // 5 +Z 内
};
static readonly List<Vector3> corners = new() {
new Vector3(-1f, -1f, -1f), // 0 左下外
new Vector3(-1f, -1f, 1f), // 1 左下内
new Vector3(-1f, 1f, -1f), // 2 左上外
new Vector3(-1f, 1f, 1f), // 3 左上内
new Vector3(1f, -1f, -1f), // 4 右下外
new Vector3(1f, -1f, 1f), // 5 右下内
new Vector3(1f, 1f, -1f), // 6 右上外
new Vector3(1f, 1f, 1f), // 7 右上内
};
// 默认的UV坐标,分布如下
// | -X | +X | -Y |
// | +Y | -Z | +Z |
static List<(Vector2, Vector2)> defaultFaceUVs = new()
{
(new Vector2(0f/3f, 1f/2f), new Vector2(1f/3f, 2f/2f)), // 0 -X 左
(new Vector2(2f/3f, 1f/2f), new Vector2(3f/3f, 2f/2f)), // 1 -Y 下
(new Vector2(1f/3f, 0f/2f), new Vector2(2f/3f, 1f/2f)), // 2 -Z 外
(new Vector2(1f/3f, 1f/2f), new Vector2(2f/3f, 2f/2f)), // 3 +X 右
(new Vector2(0f/3f, 0f/2f), new Vector2(1f/3f, 1f/2f)), // 4 +Y 上
(new Vector2(2f/3f, 0f/2f), new Vector2(3f/3f, 1f/2f)), // 5 +Z 内
};
// cornerV 为圆角的中心点,mainV是球面三角形的一个顶点,followV1和followV2是球面三角形的另外两个顶点,mainV、followV1、followV2按照顺时针方向排列
// 这个函数将计算球面三角形的三分之一的网格
(List<Vector3>, List<int>, List<Vector2>) DivideCornerPart(Vector3 cornerV, Vector3 mainV, Vector3 followV1, Vector3 followV2, int divide, Vector2 uv00, Vector2 uv01, Vector2 uv11, Vector2 uv10)
{
List<Vector3> vertices = new();
List<int> indice = new();
List<Vector2> uvs = new();
// 将一个球面三角形从顶点a向边bc均匀分割为divide行三角形,每行的顶点数分别为1,2,3,...,divide+1
void divideTranglePart(Vector3 a, Vector3 b, Vector3 c, Vector2 suv00, Vector2 suv10, Vector2 suv11)
{
int offset = vertices.Count;
// 顶点计算
vertices.Add(a);
uvs.Add(suv00);
for (int n = 1; n <= divide; n++)
{
float t = (float)n / divide;
var vB = Vector3.Slerp(a, b, t);
vertices.Add(vB);
var uvB = Vector2.Lerp(suv00, suv10, t);
uvs.Add(uvB);
var vC = Vector3.Slerp(a, c, t);
var uvC = Vector2.Lerp(suv00, suv11, t);
for (int m = 1; m < n; m++)
{
float s = (float)m / n;
var v = Vector3.Slerp(vB, vC, s);
vertices.Add(v);
var uv = Vector2.Lerp(uvB, uvC, s);
uvs.Add(uv);
}
vertices.Add(vC);
uvs.Add(uvC);
}
// 计算三角形索引
for (int n = 1; n <= divide; n++)
{
// 上边一行的起始索引 0 + 1 + 2 + ... +(n-1) = n(n-1)/2
int topStart = n * (n - 1) / 2;
// 本行的起始索引 0 + 1 + 2 + ... +(n-1) + n
int thisStart = topStart + n;
for (int i = 0; i <= n; i++)
{
if (i < n)
{
indice.Add(topStart + i + offset);
indice.Add(thisStart + i + offset);
indice.Add(thisStart + i + 1 + offset);
}
if (i > 0)
{
indice.Add(topStart + i + offset);
indice.Add(topStart + i - 1 + offset);
indice.Add(thisStart + i + offset);
}
}
}
}
// 将球面四边形分割为两个三角形,沿着mainV到cornerV的连接线分割,然后在将两个三角形细分
// part 1
divideTranglePart(mainV, Vector3.Slerp(mainV, followV1, 0.5f), cornerV, uv00, uv01, uv11);
// part 2
divideTranglePart(mainV, cornerV, Vector3.Slerp(mainV, followV2, 0.5f), uv00, uv11, uv10);
return (vertices, indice, uvs);
}
public Mesh GetMesh()
{
List<Vector3> vertices = new();
List<int> indices = new();
List<Vector3> normals = new();
List<Vector2> uvs = new();
for (int i = 0; i < 6; i++)
{
// 纹理的坐标范围
// 1----2
// | \ | -> y
// 0----3
// |
// v
// x
// 对应到一个面的纹理坐标范围是(x, y, w, h)
(float x, float y, float w, float h) = (
faceUVs[i].Item1.x, faceUVs[i].Item1.y,
faceUVs[i].Item2.x - faceUVs[i].Item1.x, faceUVs[i].Item2.y - faceUVs[i].Item1.y
);
// 圆角部分的纹理坐标的宽高
(float rx, float ry) = (w * radius, h * radius);
// 只有圆角半径大于0时才计算圆角
if (radius > 0 && radius <= 0.5f)
{
// ---------------------------------------------------------------------------------------------------------
// 计算4个圆角
// ---------------------------------------------------------------------------------------------------------
void CalcACorner(int c, Vector3 face, Vector2 uv00, Vector2 uv01, Vector2 uv11, Vector2 uv10)
{
// 圆角顶点
Vector3 cornerV = faces[cornerWise[c].Item1] + faces[cornerWise[c].Item2] + faces[cornerWise[c].Item3];
// 圆角在这个面的顶点
int[] facesInCorner = new int[] { cornerWise[c].Item1, cornerWise[c].Item2, cornerWise[c].Item3 };
int mainN = 0, followN1 = 0, followN2 = 0;
for (int n = 0; n < 3; n++)
{
if (face == faces[facesInCorner[n]])
{
mainN = facesInCorner[n];
followN2 = facesInCorner[(n + 2) % 3];
followN1 = facesInCorner[(n + 1) % 3];
break;
}
}
(List<Vector3> cornerVertices, List<int> cornerIndices, List<Vector2> cornerUVs) = DivideCornerPart(
cornerV.normalized, faces[mainN], faces[followN1], faces[followN2], cornerDivide,
uv00, uv01, uv11, uv10
);
int indexOffset = vertices.Count;
normals.AddRange(cornerVertices);
Vector3 cornerOffset = cornerV * (1f - 2 * radius);
vertices.AddRange(cornerVertices.ConvertAll(v => 2 * radius * v + cornerOffset));
indices.AddRange(cornerIndices.ConvertAll(idx => idx + indexOffset));
uvs.AddRange(cornerUVs);
}
// 圆角纹理坐标的起止位置如下图所示
// |↖| |↗|
// | | | |
// |↙| |↘|
CalcACorner(
faceCorners[i].Item1, faces[i],
new Vector2(x + rx, y + ry), new Vector2(x + rx, y), new Vector2(x, y), new Vector2(x, y + ry)
);
CalcACorner(
faceCorners[i].Item2, faces[i],
new Vector2(x + rx, y + h - ry), new Vector2(x, y + h - ry), new Vector2(x, y + h), new Vector2(x + rx, y + h)
);
CalcACorner(
faceCorners[i].Item3, faces[i],
new Vector2(x + w - rx, y + h - ry), new Vector2(x + w - rx, y + h), new Vector2(x + w, y + h), new Vector2(x + w, y + h - ry)
);
CalcACorner(
faceCorners[i].Item4, faces[i],
new Vector2(x + w - rx, y + ry), new Vector2(x + w, y + ry), new Vector2(x + w, y), new Vector2(x + w - rx, y)
);
// ---------------------------------------------------------------------------------------------------------
// 计算4条棱
// ---------------------------------------------------------------------------------------------------------
void CalcAnEdge(int a, int b, Vector3 face, Vector2 uv00, Vector2 uv11, bool horizontal)
{
List<Vector3> edgeVertices = new();
List<int> edgeIndices = new();
List<Vector3> edgeNormals = new();
List<Vector2> edgeUvs = new();
var (v1, v2) = (corners[a], corners[b]);
var from = face;
var to = (v1 + v2) / 2 - from;
to = Vector3.Slerp(from, to, 0.5f);
Vector3 offsetV1 = (faces[cornerWise[a].Item1] + faces[cornerWise[a].Item2] + faces[cornerWise[a].Item3]) * (1f - 2 * radius);
Vector3 offsetV2 = (faces[cornerWise[b].Item1] + faces[cornerWise[b].Item2] + faces[cornerWise[b].Item3]) * (1f - 2 * radius);
int indexOffset = 0;
for (int n = 0; n <= cornerDivide; n++)
{
float t = (float)n / cornerDivide;
var v = Vector3.Slerp(from, to, t);
edgeVertices.Add(2 * radius * v + offsetV1);
edgeVertices.Add(2 * radius * v + offsetV2);
edgeNormals.Add(v);
edgeNormals.Add(v);
if (horizontal)
{ // 横向按照uv的y方向插值
edgeUvs.Add(new Vector2(uv00.x, uv00.y + t * (uv11.y - uv00.y)));
edgeUvs.Add(new Vector2(uv11.x, uv00.y + t * (uv11.y - uv00.y)));
}
else
{
// 纵向按照uv的x方向插值
edgeUvs.Add(new Vector2(uv00.x + t * (uv11.x - uv00.x), uv00.y));
edgeUvs.Add(new Vector2(uv00.x + t * (uv11.x - uv00.x), uv11.y));
}
if (n > 0)
{
edgeIndices.AddRange(new int[] {
indexOffset + 0, indexOffset + 3, indexOffset + 1,
indexOffset + 0, indexOffset + 2, indexOffset + 3,
});
indexOffset += 2;
}
}
int indexOffset2 = vertices.Count;
vertices.AddRange(edgeVertices);
normals.AddRange(edgeNormals);
indices.AddRange(edgeIndices.ConvertAll(idx => idx + indexOffset2));
uvs.AddRange(edgeUvs);
}
// 边缘纹理坐标的起止位置如下图所示
// | |↗| |
// |↖| |↘|
// | |↙| |
CalcAnEdge(faceCorners[i].Item1, faceCorners[i].Item2, faces[i], new Vector2(x + rx, y + ry), new Vector2(x, y + h - ry), false);
CalcAnEdge(faceCorners[i].Item2, faceCorners[i].Item3, faces[i], new Vector2(x + rx, y + h - ry), new Vector2(x + w - rx, y + h), true);
CalcAnEdge(faceCorners[i].Item3, faceCorners[i].Item4, faces[i], new Vector2(x + w - rx, y + h - ry), new Vector2(x + w, y + ry), false);
CalcAnEdge(faceCorners[i].Item4, faceCorners[i].Item1, faces[i], new Vector2(x + w - rx, y + ry), new Vector2(x + rx, y), true);
}
// ---------------------------------------------------------------------------------------------------------
// 计算面
// ---------------------------------------------------------------------------------------------------------
if (radius >= 0 && radius < 0.5)
{
int indexOffset = vertices.Count;
(int a, int b, int c, int d) = faceCorners[i];
Vector3 faceNormal = faces[i];
Vector3 basFace = faces[i];
// 除了所在面这一维度,另两个维度需要考虑圆角
Vector3 scaleFactor = new Vector3(basFace.x == 0 ? (1 - 2 * radius) : 1f, basFace.y == 0 ? (1 - 2 * radius) : 1f, basFace.z == 0 ? (1 - 2 * radius) : 1f);
vertices.Add(MultiplyVectors(corners[a], scaleFactor));
vertices.Add(MultiplyVectors(corners[b], scaleFactor));
vertices.Add(MultiplyVectors(corners[c], scaleFactor));
vertices.Add(MultiplyVectors(corners[d], scaleFactor));
// 四个顶点的法向量都是面的法向量
normals.AddRange(new Vector3[] { faceNormal, faceNormal, faceNormal, faceNormal });
// 1----2
// | \ |
// 0----3
// 0, 1, 3
// 1, 2, 3
indices.AddRange(new int[] {
indexOffset + 0, indexOffset + 1, indexOffset + 3,
indexOffset + 1, indexOffset + 2, indexOffset + 3,
});
uvs.Add(new Vector2(x + rx, y + ry));
uvs.Add(new Vector2(x + rx, y + h - ry));
uvs.Add(new Vector2(x + w - rx, y + h - ry));
uvs.Add(new Vector2(x + w - rx, y + ry));
}
}
return new Mesh()
{
vertices = vertices.ConvertAll(v => 0.5f * size * v).ToArray(),
normals = normals.ToArray(),
triangles = indices.ToArray(),
uv = uvs.ToArray(),
};
}
private Vector3 MultiplyVectors(Vector3 v1, Vector3 v2)
{
return new Vector3(v1.x * v2.x, v1.y * v2.y, v1.z * v2.z);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment