Created
June 17, 2024 11:25
-
-
Save andyzhshg/82793fa301b37d802bba7b673c3c6a1d to your computer and use it in GitHub Desktop.
Build a Perfect Round Cornered Cube in Unity with Code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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