自制载具系统:CylinderCaster

本博客实现的代码以 Unity Asset Store 中 USHISOFT 的资源代码 CylinderCaster 为参考

整体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
using System.Collections.Generic;
using UnityEngine;

namespace AntarctiCar
{
public class CylinderCaster : MonoBehaviour
{
private Rigidbody _rigidbody;
private MeshCollider _meshCollider;

private float _radius;
private float _width;
private int _resolution;

public float Radius
{
get => _radius;
}

public float Width
{
get => _width;
}

public int Resolution
{
get => _resolution;
}

private void Awake()
{
_rigidbody = gameObject.AddComponent<Rigidbody>();
_rigidbody.isKinematic = true;

_meshCollider = gameObject.AddComponent<MeshCollider>();
_meshCollider.convex = true;
_meshCollider.isTrigger = true;
}

public bool Cast(Vector3 direction, out RaycastHit hitInfo, float distance)
{
return _rigidbody.SweepTest(
direction, // 用于扫掠刚体的方向
out hitInfo, // 如果返回true,则将包含有关碰撞器被击中的更多信息
distance); // 扫掠的最大长度
}

public void Init(float radius, float width, int resolution)
{
_radius = radius;
_width = width;
_resolution = resolution;

_meshCollider.sharedMesh = GenerateMesh(_radius, _width, _resolution);
}

private Mesh GenerateMesh(float radius, float width, int resolution)
{
var verts = new List<Vector3>(); // 创建顶点 List
for (var i = 0; i < resolution; i++) // for 循环遍历每个角度来计算顶点位置
{
var radian = 2f * Mathf.PI * (float)i / (float)resolution; // 使用输入的分辨率 resolution 计算弧度 radian,分辨率越大,弧度越精细
var y = Mathf.Sin(radian) * radius; // 计算该顶点的 y 坐标值
var z = -Mathf.Cos(radian) * radius; // 计算该顶点的 x 坐标值
verts.Add(new Vector3(-width / 2f, y, z)); // 添加第一个计算出的轮胎顶点(一共有两个,分别位于轮胎同一弧度的两侧)
verts.Add(new Vector3(width / 2f, y, z)); // 添加第二个计算出的轮胎顶点(一共有两个,分别位于轮胎同一弧度的两侧)
}

var tris = new List<int>(); // 创建三角形面片索引 List
for (var i = 0; i < resolution; i++)
{
// 第一组三角形
tris.Add(i * 2); // 添加当前细分段左侧顶点
tris.Add(i * 2 + 1); // 添加当前细分段右侧顶点
tris.Add(((i + 1) * 2) % (resolution * 2)); // 添加下一细分段左侧顶点

// 第二组三角形
tris.Add(i * 2 + 1); // 添加当前细分段右侧顶点
tris.Add(((i + 1) * 2 + 1) % (resolution * 2)); // 添加下一细分段右侧顶点
tris.Add(((i + 1) * 2) % (resolution * 2)); // 添加下一细分段左侧顶点
}

var mesh = new Mesh(); // 新建 mesh 对象
mesh.vertices = verts.ToArray(); // 将顶点列表赋值给 mesh 对象的 vertices 顶点数组 (vertice - 顶点)
mesh.triangles = tris.ToArray(); // 将三角形面片索引列表赋值给 mesh 对象的 triangles 三角形数组
return mesh; // 返回该 mesh 对象
}
}
}

变量解释

在开始主要程序的编写之前,我们需要先对 CylinderCaster 所需的变量有一个基本的了解

1
2
3
4
5
6
private Rigidbody _rigidbody;
private MeshCollider _meshCollider;

private float _radius;
private float _width;
private int _resolution;
  1. _rigidbody 引用刚体组件,使物体能够参与物理模拟,如碰撞检测、重力影响等。同时,在 Awake 方法中初始化时,将其设置为运动学模式(isKinematic = true),这意味着它不会受到物理引擎的影响(例如重力或碰撞力),但仍然可以进行碰撞检测。

  2. _meshCollider 引用附加到游戏对象的网格碰撞器组件,即根据轮胎模型的网格生成碰撞体。在 Awake 方法中初始化时,设置为凸多边形(convex = true)以提高性能,并设置为触发器(isTrigger = true),以便可以在不施加物理力的情况下检测碰撞。

  3. _radius_width_resolution 的详细介绍可以在 自制载具系统:WheelCollider2 找到,已经了解的可跳过此处。

_radius_width_resolution 的读取权限暴露给外界,实现只读不写的部分公共化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public float Radius
{
get => _radius;
}

public float Width
{
get => _width;
}

public int Resolution
{
get => _resolution;
}

这些只读属性提供了访问私有成员变量的方式,允许外部脚本获取轮胎的尺寸和分辨率信息。


方法部分

初始化方法 Awake

1
2
3
4
5
6
7
8
9
private void Awake()
{
_rigidbody = gameObject.AddComponent<Rigidbody>();
_rigidbody.isKinematic = true;

_meshCollider = gameObject.AddComponent<MeshCollider>();
_meshCollider.convex = true;
_meshCollider.isTrigger = true;
}

Awake 方法在脚本实例化时调用,用于初始化组件,在执行顺序上具有高于 Start 的优先级。

Rigidbody:

  • 添加一个刚体组件,并将其设置为运动学模式(isKinematic = true),这意味着它不会受到物理引擎的影响,但仍然可以进行碰撞检测。

MeshCollider:

  • 添加一个网格碰撞器,并将其设置为凸多边形(convex = true),以提高碰撞检测性能。

  • 设置为触发器(isTrigger = true),以便可以在不施加物理力的情况下检测碰撞。

碰撞检测方法 Cast

1
2
3
4
5
6
7
public bool Cast(Vector3 direction, out RaycastHit hitInfo, float distance)
{
return _rigidbody.SweepTest(
direction, // 用于扫掠刚体的方向
out hitInfo, // 如果返回true,则将包含有关碰撞器被击中的更多信息
distance); // 扫掠的最大长度
}
  1. Cast 方法从外界获取 Vector3 类型的 direction 方向、获取并返回修改后的 RaycastHit 碰撞信息、获取浮点类型的 distance 距离。

  2. 在方法内部使用刚体的 SweepTest 方法执行射线检测。

  3. SweepTest 方法检查轮子是否会在给定方向和距离内碰到其他物体,并返回碰撞信息(RaycastHit),该碰撞信息也会原路返回到调用 Cast 方法的位置。

  4. 返回值为布尔型,当刚体扫掠与任何碰撞器相交时为 true,否则为 false。

初始化几何形状 Init

1
2
3
4
5
6
7
8
public void Init(float radius, float width, int resolution)
{
_radius = radius;
_width = width;
_resolution = resolution;

_meshCollider.sharedMesh = GenerateMesh(_radius, _width, _resolution);
}
  1. Init 方法从外部获取浮点类型的 radiuswidth,整型的 resolution 变量

  2. 用从外界获取到的这三个变量初始化 CylinderCaster 类的 _radius_width_resolution 三个变量。

  3. 将这三个变量传递给 GenerateMesh() 方法生成新的网格,并将其分配给网格碰撞器 _meshCollider

生成网格方法 GenerateMesh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private Mesh GenerateMesh(float radius, float width, int resolution)
{
var verts = new List<Vector3>(); // 创建顶点 List
for (var i = 0; i < resolution; i++) // for 循环遍历每个角度来计算顶点位置
{
var radian = 2f * Mathf.PI * (float)i / (float)resolution; // 使用输入的分辨率 resolution 计算弧度 radian,分辨率越大,弧度越精细
var y = Mathf.Sin(radian) * radius; // 计算该顶点的 y 坐标值
var z = -Mathf.Cos(radian) * radius; // 计算该顶点的 x 坐标值
verts.Add(new Vector3(-width / 2f, y, z)); // 添加第一个计算出的轮胎顶点(一共有两个,分别位于轮胎同一弧度的两侧)
verts.Add(new Vector3(width / 2f, y, z)); // 添加第二个计算出的轮胎顶点(一共有两个,分别位于轮胎同一弧度的两侧)
}

var tris = new List<int>(); // 创建三角形面片索引 List
for (var i = 0; i < resolution; i++)
{
// 第一组三角形
tris.Add(i * 2); // 添加当前细分段左侧顶点
tris.Add(i * 2 + 1); // 添加当前细分段右侧顶点
tris.Add(((i + 1) * 2) % (resolution * 2)); // 添加下一细分段左侧顶点

// 第二组三角形
tris.Add(i * 2 + 1); // 添加当前细分段右侧顶点
tris.Add(((i + 1) * 2 + 1) % (resolution * 2)); // 添加下一细分段右侧顶点
tris.Add(((i + 1) * 2) % (resolution * 2)); // 添加下一细分段左侧顶点
}

var mesh = new Mesh(); // 新建 mesh 对象
mesh.vertices = verts.ToArray(); // 将顶点列表赋值给 mesh 对象的 vertices 顶点数组 (vertice - 顶点)
mesh.triangles = tris.ToArray(); // 将三角形面片索引列表赋值给 mesh 对象的 triangles 三角形数组
return mesh; // 返回该 mesh 对象
}

顶点生成

  1. 使用循环遍历每个角度(从 0 到 2π),计算圆柱表面的顶点位置。

  2. 每个角度对应两个顶点(左右两侧),形成圆柱侧面。

  3. 注意生成点的顺序:如果从车轮后方顺平行于地面的一条直径从后往前看,生成的点的顺序为:

第 0 细分段:顶点 0(左)、顶点 1(右) 弧度 0
第 1 细分段:顶点 2(左)、顶点 3(右) 弧度 0.5PI
第 2 细分段:顶点 4(左)、顶点 5(右) 弧度 1.0PI
第 3 细分段:顶点 6(左)、顶点 7(右) 弧度 1.5PI

注意:上面的“弧度”等价于从轮胎左侧观察建立坐标系的一般弧度值(从 X 轴正方向开始逆时针增加)。且该示例为 resolution = 4 的情况,只是为了方便理解,实际的 resolution 的最小值为 8,每个细分段对应着一个弧度角在该圆柱体侧面的一条高线。

三角形索引生成

  • 初始化三角形索引列表:

1
var tris = new List<int>();

  tris 是一个整数列表,用来存储构成网格的三角形顶点索引。

  每个三角形由三个顶点索引组成,而整个圆柱表面由多个这样的三角形拼接而成。

  • 循环遍历分辨率以构建三角形:

1
2
3
4
for (var i = 0; i < resolution; i++)
{
// 添加三角形索引
}

  resolution 是圆柱表面沿周长方向的细分数量,决定了圆柱的平滑度。

  这个循环会遍历每个细分段,并为每一段添加两个三角形来形成一个四边形面片。

  • 添加三角形索引

1
2
3
4
5
6
7
8
9
// 第一组三角形
tris.Add(i * 2); // 添加当前细分段左侧顶点
tris.Add(i * 2 + 1); // 添加当前细分段右侧顶点
tris.Add(((i + 1) * 2) % (resolution * 2)); // 添加下一细分段左侧顶点

// 第二组三角形
tris.Add(i * 2 + 1); // 添加当前细分段右侧顶点
tris.Add(((i + 1) * 2 + 1) % (resolution * 2)); // 添加下一细分段右侧顶点
tris.Add(((i + 1) * 2) % (resolution * 2)); // 添加下一细分段左侧顶点

假设我们有 resolution = 4,即圆柱侧面分为 4 个细分段(此时的圆柱实际上为长方体)。以下是详细的三角形索引生成过程:

细分段 0:
第一组三角形:0, 1, 2
第二组三角形:1, 3, 2
细分段 1:
第一组三角形:2, 3, 4
第二组三角形:3, 5, 4
细分段 2:
第一组三角形:4, 5, 6
第二组三角形:5, 7, 6
细分段 3:
第一组三角形:6, 7, 0
第二组三角形:7, 1, 0

将这个详细过程与生成顶点顺序搭配起来看,就可以明白这个函数是如何在圆柱体侧面通过顶点绘制三角形的了:

(为了防止翻页麻烦我把生成顶点的顺序摆在这里)

第 0 细分段:顶点 0(左)、顶点 1(右)
第 1 细分段:顶点 2(左)、顶点 3(右)
第 2 细分段:顶点 4(左)、顶点 5(右)
第 3 细分段:顶点 6(左)、顶点 7(右)

注意:由于此处不需要渲染出实际的网格,所以这里不讲解生成顶点的顺序问题。如果需要渲染出网格和画面,Unity 的渲染引擎就会使用这些索引来从 mesh.vertices 中提取对应的顶点,并根据这些顶点绘制三角形。每个三角形的顶点顺序决定了其法线方向,从而影响光照效果和正面 / 背面剔除。

  • 创建并配置 Mesh 对象

1
2
3
4
var mesh = new Mesh();              // 新建 mesh 对象
mesh.vertices = verts.ToArray(); // 将顶点列表赋值给 mesh 对象的 vertices 顶点数组 (vertice - 顶点)
mesh.triangles = tris.ToArray(); // 将三角形面片索引列表赋值给 mesh 对象的 triangles 三角形数组
return mesh; // 返回该 mesh 对象
  1. verts 是之前生成的顶点列表,包含了所有圆柱表面的顶点位置。

  2. tris 是刚刚构建的三角形索引列表,指定了哪些顶点应该连接成三角形。

  3. 将这两个数组赋值给新的 Mesh 对象。

  4. mesh.triangles = tris.ToArray(); 这一行代码将 tris 列表中的整数数组赋值给 Mesh 对象的 triangles 属性。

  5. Mesh.triangles 定义网格中所有三角形的顶点索引,它规定:每个三角形由三个顶点索引组成,这些索引指向 Mesh.vertices 数组中的具体顶点位置。

  6. 当我们将 tris 转换为数组并赋值给 mesh.triangles 时,Unity 的 Mesh 渲染器会按照每三个连续的整数定义一个三角形。


总结

CylinderCaster 类通过接受圆柱体的半径、宽度、圆细分量来构建一个圆柱体形的网格,并且提供方法让外界访问生成圆柱体形网格的每个顶点,以及该圆柱体是否与任意方向任意距离内的物体发生交互。

该类的代码核心在于 GenerateMesh 生成网格方法。该方法通过循环遍历每个细分段,并为每个细分段添加两个三角形索引,最终生成一个完整的圆柱形网格。 这种方法高效而且灵活,可以根据不同的 resolution 值生成不同平滑度的圆柱表面。 这对于创建逼真的车辆轮胎非常有用,也为 WheelCollider2 类使用该类进行车轮的物理效果仿真提供了基础。