功能下班写完了,冲一篇,记录一下,后续还有深入研究。
假设三维空间中存在两个点,以这两个点为端点生成圆柱体,如下:
空间中两端点f和t构成圆柱体,那么我们首先我们必须得到f和t所在的平面,然后求出平面上所有圆形网格顶点,这个上一篇空间圆已经详细讲解了,这里我们直接写代码计算所有顶点坐标,如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PointCylinderRender : MonoBehaviour
{
[Range(0, 20f)]
public float circleRadius = 10f;
[Range(3, 50)]
public int circleSegement = 20;
public Transform startTrans; //from
public Transform endTrans; //to
[Range(0, 90f)]
public float rotateAngle = 30f;
void Start()
{
}
void Update()
{
DrawCylinderGizmos();
}
/// <summary>
/// 绘制圆柱体网线图
/// </summary>
private void DrawCylinderGizmos()
{
Vector3 start = startTrans.position;
Vector3 end = endTrans.position;
Vector3 p2 = RotateAroundXAxis(start, end, rotateAngle * Mathf.Deg2Rad);
Vector3 p1 = RayLineCrossPanel(start, end, p2);
Vector3 p = start + (p1 - start).normalized * circleRadius;
Vector3[] startposarr = CalculateCirclePoints(start, end, p, circleSegement);
Vector3[] endposarr = CalculateBiasPoints(start, end, startposarr);
for (int i = 0; i < circleSegement; i++)
{
Vector3 ccspos = startposarr[i];
Vector3 ccsposp1 = startposarr[(i + 1) % circleSegement];
Vector3 ccepos = endposarr[i];
Vector3 cceposp1 = endposarr[(i + 1) % circleSegement];
//构建start平面圆形
Debug.DrawLine(ccspos, ccsposp1, Color.black);
//构建start圆形的辐条
Debug.DrawLine(start, ccspos, Color.black);
//构建start->end界面矩形
Debug.DrawLine(ccspos, ccepos, Color.black);
//构建end平面圆形
Debug.DrawLine(ccepos, cceposp1, Color.black);
//构建end圆形的辐条
Debug.DrawLine(end, ccepos, Color.black);
}
}
/// <summary>
/// 已知start为圆心r半径圆环上所有离散坐标sposarr
/// 只需要通过sposarr+=(end-start)即可得到tposarr
/// </summary>
/// <param name="start"></param>
/// <param name="end"></param>
/// <param name="sposarr"></param>
/// <returns></returns>
private Vector3[] CalculateBiasPoints(Vector3 start, Vector3 end, Vector3[] sposarr)
{
Vector3[] eposarr = new Vector3[sposarr.Length];
Vector3 offset = end - start;
for (int i = 0; i < sposarr.Length; i++)
{
Vector3 spos = sposarr[i];
Vector3 epos = spos + offset;
eposarr[i] = epos;
}
return eposarr;
}
/// <summary>
/// 算出圆环上所有离散坐标点
/// </summary>
/// <param name="start"></param>
/// <param name="end"></param>
/// <param name="p"></param>
/// <param name="sege"></param>
/// <returns></returns>
private Vector3[] CalculateCirclePoints(Vector3 start, Vector3 end, Vector3 p, int sege)
{
Vector3[] posarr = new Vector3[sege];
posarr[0] = p;
Vector3 naxis = (end - start).normalized;
float segerad = 2f * Mathf.PI / (float)sege;
for (int i = 1; i < sege; i++)
{
float rad = segerad * i;
Vector3 segepos = RotateAroundAnyAxis(start, p, naxis, rad);
posarr[i] = segepos;
}
return posarr;
}
/// <summary>
/// p(x,y,z)点绕start为起点的任意坐标轴旋转后的坐标
/// </summary>
/// <param name="start"></param>
/// <param name="naxis"></param>
/// <param name="rad"></param>
/// <returns></returns>
private Vector3 RotateAroundAnyAxis(Vector3 start, Vector3 p, Vector3 naxis, float rad)
{
float n1 = naxis.x;
float n2 = naxis.y;
float n3 = naxis.z;
//获取p相对start的本地坐标
p -= start;
float sin = Mathf.Sin(rad);
float cos = Mathf.Cos(rad);
Matrix3x3 mat = new Matrix3x3();
mat.m00 = n1 * n1 * (1 - cos) + cos;
mat.m01 = n1 * n2 * (1 - cos) - n3 * sin;
mat.m02 = n1 * n3 * (1 - cos) + n2 * sin;
mat.m10 = n1 * n2 * (1 - cos) + n3 * sin;
mat.m11 = n2 * n2 * (1 - cos) + cos;
mat.m12 = n2 * n3 * (1 - cos) - n1 * sin;
mat.m20 = n1 * n3 * (1 - cos) - n2 * sin;
mat.m21 = n2 * n3 * (1 - cos) + n1 * sin;
mat.m22 = n3 * n3 * (1 - cos) + cos;
//绕轴旋转后,处理成世界坐标
Vector3 px = mat * p + start;
return px;
}
/// <summary>
/// 通过start end计算start所处平面F方程
/// 通过end p2计算射线与平面F交点p1
/// </summary>
/// <param name="start"></param>
/// <param name="end"></param>
/// <param name="p2"></param>
/// <returns></returns>
private Vector3 RayLineCrossPanel(Vector3 start, Vector3 end, Vector3 p2)
{
//start = from
//end = to
//构建平面F方程参数
Vector3 ft = end - start;
float u = ft.x, v = ft.y, w = ft.z;
float a = start.x, b = start.y, c = start.z;
//构建射线tp2参数
float sx = end.x;
float sy = end.y;
float sz = end.z;
Vector3 ntp2 = (p2 - end).normalized;
float dx = ntp2.x;
float dy = ntp2.y;
float dz = ntp2.z;
//计算p1
float n = ((u * a + v * b + w * c) - (u * sx + v * sy + w * sz)) / (u * dx + v * dy + w * dz);
Vector3 p1 = end + n * ntp2;
return p1;
}
/// <summary>
/// 空间任意起终点
/// 终点绕x轴旋转rad弧度
/// </summary>
/// <param name="start"></param>
/// <param name="end"></param>
/// <param name="rad"></param>
/// <returns></returns>
private Vector3 RotateAroundXAxis(Vector3 start, Vector3 end, float rad)
{
Matrix3x3 mat = new Matrix3x3();
float cos = Mathf.Cos(rad);
float sin = Mathf.Sin(rad);
mat.m00 = 1;
mat.m01 = 0;
mat.m02 = 0;
mat.m10 = 0;
mat.m11 = cos;
mat.m12 = -sin;
mat.m20 = 0;
mat.m21 = sin;
mat.m22 = cos;
Vector3 ret = mat * (start - end) + end;
return ret;
}
}
效果如下:
网线渲染绘制正确。
接下来我们通过meshfilter来构建网格,如下:
可以看出cylinder的triangles拓扑结构,当然这里为了图示,就用四个顶点的”圆形“,开始写代码:
private MeshRenderer meshRender;
private MeshFilter meshFilter;
private float lastCcRadius;
private int lastCcSege;
private Vector3 lastStartPos;
private Vector3 lastEndPos;
private Mesh mesh;
void Start()
{
meshRender = GetComponent<MeshRenderer>();
meshFilter = GetComponent<MeshFilter>();
mesh = new Mesh();
}
void Update()
{
if (CheckParamsChanged())
{
UpdateCylinderMesh();
}
}
/// <summary>
/// 检查绘制参数改变
/// 改变才重绘制
/// </summary>
/// <returns></returns>
private bool CheckParamsChanged()
{
if (lastCcRadius != circleRadius
|| lastCcSege != circleSegement
|| lastStartPos != startTrans.position
|| lastEndPos != endTrans.position)
{
lastCcRadius = circleRadius;
lastCcSege = circleSegement;
lastStartPos = startTrans.position;
lastEndPos = endTrans.position;
return true;
}
return false;
}
/// <summary>
/// 绘制圆柱体网格
/// </summary>
private void UpdateCylinderMesh()
{
Vector3 start = startTrans.position;
Vector3 end = endTrans.position;
Vector3 p2 = RotateAroundXAxis(start, end, rotateAngle * Mathf.Deg2Rad);
Vector3 p1 = RayLineCrossPanel(start, end, p2);
Vector3 p = start + (p1 - start).normalized * circleRadius;
Vector3[] startposarr = CalculateCirclePoints(start, end, p, circleSegement);
Vector3[] endposarr = CalculateBiasPoints(start, end, startposarr);
//构建网格数据
if (mesh != null)
{
mesh.Clear();
}
//构建顶点列表
List<Vector3> vertlist = new List<Vector3>();
vertlist.Add(start);
vertlist.AddRange(startposarr);
vertlist.AddRange(endposarr);
vertlist.Add(end);
//构建拓扑三角列表(逆时针)
//014 043 032 021
List<int> trilist = new List<int>();
for (int i = 0; i < circleSegement; i++)
{
int[] tris = new int[]
{
0,
i+2>circleSegement?(i+2)%circleSegement:i+2,
i+1
};
trilist.AddRange(tris);
}
//165 126
//276 237
//387 348
//458 415
for (int i = 0; i < circleSegement; i++)
{
int[] tris = new int[]
{
i+1,
i+circleSegement+2>circleSegement*2?i+2:i+circleSegement+2,
i+circleSegement+1
};
trilist.AddRange(tris);
tris = new int[]
{
i+1,
i+2>circleSegement?(i+2)%circleSegement:i+2,
i+circleSegement+2>circleSegement*2?i+2:i+circleSegement+2
};
trilist.AddRange(tris);
}
//956 967 978 985
for (int i = 0; i < circleSegement; i++)
{
int[] tris = new int[]
{
circleSegement*2+1,
i+circleSegement+1,
i+circleSegement+2>circleSegement*2?i+2:i+circleSegement+2
};
trilist.AddRange(tris);
}
//构建网格
mesh.vertices = vertlist.ToArray();
mesh.triangles = trilist.ToArray();
meshFilter.sharedMesh = mesh;
}
代码中需要注意的就是拓扑三角索引的构建,效果如下:
可以看得出来网格构建ok,但是光照有问题,因为法向量缺失,我们还得构建法向量,如下:
图示应该很容易理解,接下来写代码:
//构建法向量
List<Vector3> normlist = new List<Vector3>();
Vector3 nf = (start - end).normalized;
normlist.Add(nf);
Vector3[] nfarr = new Vector3[circleSegement];
Vector3[] ntarr = new Vector3[circleSegement];
for (int i = 0; i < circleSegement; i++)
{
nfarr[i] = (startposarr[i] - start).normalized;
ntarr[i] = (endposarr[i] - end).normalized;
}
normlist.AddRange(nfarr);
normlist.AddRange(ntarr);
Vector3 nt = (end - start).normalized;
normlist.Add(nt);
接下来见证一个神奇的时刻:
哈哈哈哈,可以看得出来圆柱体截面渲染正常,两端渲染好像一个半球形,这是当然的!因为两端圆形平面的法向量是通过平行平面F的nfarr和垂直平面F的nf线性插值得到的,这样就变成了半球,实际上unity圆柱体是由两端圆形加中间截面矩形组成,也就是一个圆柱体由三个网格面组成,这样的话才能避免两端平面圆形渲染异常的问题,如下:
看得出来unity自带的cylinder 88verts 80tris,也就是说两端平面圆形和中间截面矩形是“断开”的,所以得重新处理mesh,如下:
接下来继续修改:
/// <summary>
/// 绘制断裂的圆柱体网格
/// </summary>
private void UpdateBreakedCylinderMesh()
{
Vector3 start = startTrans.position;
Vector3 end = endTrans.position;
Vector3 p2 = RotateAroundXAxis(start, end, rotateAngle * Mathf.Deg2Rad);
Vector3 p1 = RayLineCrossPanel(start, end, p2);
Vector3 p = start + (p1 - start).normalized * circleRadius;
Vector3[] startposarr = CalculateCirclePoints(start, end, p, circleSegement);
Vector3[] endposarr = CalculateBiasPoints(start, end, startposarr);
//构建网格数据
if (mesh != null)
{
mesh.Clear();
}
//构建顶点列表
List<Vector3> vertlist = new List<Vector3>();
//起点圆形
vertlist.Add(start);
vertlist.AddRange(startposarr);
//中间截面矩形
vertlist.AddRange(startposarr);
vertlist.AddRange(endposarr);
//终点圆形
vertlist.AddRange(endposarr);
vertlist.Add(end);
//构建拓扑三角列表(逆时针)
//起点圆形
//014 043 032 021
List<int> trilist = new List<int>();
for (int i = 0; i < circleSegement; i++)
{
int[] tris = new int[]
{
0,
i+2>circleSegement?(i+2)%circleSegement:i+2,
i+1
};
trilist.AddRange(tris);
}
//中间截面
//5 10 9 5 6 10
//6 11 10 6 7 11
//7 12 11 7 8 12
//8 9 12 8 5 9
for (int i = 0; i < circleSegement; i++)
{
int[] tris = new int[]
{
i+circleSegement+1,
i+circleSegement*2+2>circleSegement*3?i+circleSegement+2:i+circleSegement*2+2,
i+circleSegement*2+1
};
trilist.AddRange(tris);
tris = new int[]
{
i+circleSegement+1,
i+circleSegement+2>circleSegement*2?(i+circleSegement+2)%circleSegement+circleSegement:i+circleSegement+2,
i+circleSegement*2+2>circleSegement*3?i+circleSegement+2:i+circleSegement*2+2
};
trilist.AddRange(tris);
}
//终点圆形
//17 13 14
//17 14 15
//17 15 16
//17 16 13
for (int i = 0; i < circleSegement; i++)
{
int[] tris = new int[]
{
circleSegement*4+1,
i+circleSegement*3+1,
i+circleSegement*3+2>circleSegement*4?i+circleSegement*2+2:i+circleSegement*3+2
};
trilist.AddRange(tris);
}
//构建法向量
List<Vector3> normlist = new List<Vector3>();
Vector3 nf = (start - end).normalized;
normlist.Add(nf);
Vector3[] nfs = new Vector3[circleSegement];
for (int i = 0; i < circleSegement; i++)
{
nfs[i] = nf;
}
normlist.AddRange(nfs);
Vector3[] nfarr = new Vector3[circleSegement];
Vector3[] ntarr = new Vector3[circleSegement];
for (int i = 0; i < circleSegement; i++)
{
nfarr[i] = (startposarr[i] - start).normalized;
ntarr[i] = (endposarr[i] - end).normalized;
}
normlist.AddRange(nfarr);
normlist.AddRange(ntarr);
Vector3 nt = (end - start).normalized;
Vector3[] nts = new Vector3[circleSegement];
for (int i = 0; i < circleSegement; i++)
{
nts[i] = nt;
}
normlist.AddRange(nts);
normlist.Add(nt);
//构建网格
mesh.vertices = vertlist.ToArray();
mesh.triangles = trilist.ToArray();
mesh.normals = normlist.ToArray();
meshFilter.sharedMesh = mesh;
}
解决了法向量光照的问题:
到这里了,大致已经绘制出一个圆柱体,不过还是有问题需要说明和解决的:
1.uv参数怎么计算?
圆柱体的uv映射如上图所示,起点圆形和终点圆形还有截面矩形,这样才能一张纹理映射一个圆柱体,下一篇会实现。
2.为什么要这么麻烦算空间任意两点生成圆柱体,而不是学建模工具中生成一个圆形,再挤出一个圆柱体?或者在规范化的xyz轴向生成圆柱体,再通过旋转平移到指定的位置?
上面两种方式确实更简单的创建圆柱体,但是不满足最近做的功能的需求,最近做一个动态柔体电缆的功能,需要在三维空间中数学化n截刚体骨骼,然后通过这些刚体骨骼的两个端点生成圆柱体网格蒙皮,再通过质点弹簧相关数学模型,生成柔性电缆,所以目标就是只通过空间两顶点生成个圆柱体。
好,今天就到这里,后面再聊。