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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
use crate::mesh::{MeshData, Vertex};
use crate::transform::Transform;
use serde::{Serialize, Deserialize};
/// A lightweight opaque handle to a geometry entry in a GPU registry.
///
/// Returned by pipeline-internal registration routines; you do not typically
/// need to construct or inspect this directly.
#[derive(Debug, Copy, Clone)]
pub struct GeometryId(pub usize);
/// Procedural geometry primitives supported by the engine.
///
/// Each variant stores only the parameters needed to describe its shape; the
/// actual vertex data is generated on demand via [`Geometry::build`] or
/// [`Geometry::generate_mesh_data`].
///
/// # Coordinate conventions
/// All dimensions (radii, sizes, heights) are in **world units**. The
/// geometry is centred at the local origin unless otherwise noted.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum Geometry {
/// A uniform cube centred at the origin.
///
/// `size` is the full side length; half-extents are `size / 2` on each axis.
Cube { size: f32 },
/// An axis-aligned rectangular box centred at the origin.
///
/// `width` = X extent, `height` = Y extent, `depth` = Z extent (full, not half).
Box { width: f32, height: f32, depth: f32 },
/// A flat, double-sided horizontal plane centred at the origin lying in
/// the XZ plane.
///
/// `size` is the full side length.
Plane { size: f32 },
/// A four-sided pyramid centred at the origin.
///
/// The base is a square with full side `base_size` at `y = -height / 2`;
/// the apex is at `y = height / 2`.
Pyramid { base_size: f32, height: f32 },
/// A capsule (cylinder capped with hemispheres) centred at the origin,
/// oriented along the Y axis.
///
/// * `radius` — radius of the cylinder body and hemispherical caps.
/// * `height` — length of the **cylindrical** body only (not including caps).
/// * `subdivisions` — number of horizontal segments; higher values produce
/// a smoother silhouette.
Capsule { radius: f32, height: f32, subdivisions: usize },
/// A UV sphere centred at the origin.
///
/// * `radius` — sphere radius.
/// * `subdivisions` — number of horizontal (longitude) segments; latitude
/// segments are derived as `subdivisions / 2`. Minimum effective value
/// is 8 for a reasonable sphere.
Sphere { radius: f32, subdivisions: usize },
}
impl Geometry {
/// Build raw vertex and index arrays for this geometry at the world origin
/// with a neutral white colour.
///
/// Returns `(vertices, indices)`. The result can be passed directly to
/// [`crate::pipeline::Pipeline::create_baked_mesh`] for GPU upload.
pub fn build(&self) -> (Vec<Vertex>, Vec<u32>) {
let mut mesh = MeshData::new();
let identity = Transform::default();
// We use a dummy color [1,1,1,1] because baked meshes
// usually have their colors modified by the Object color later.
self.generate_mesh_data(&mut mesh, &identity, [1.0, 1.0, 1.0, 1.0]);
(mesh.vertices, mesh.indices)
}
/// Append this geometry's triangles into an existing [`MeshData`] builder,
/// applying `transform` and `color` to every vertex.
///
/// This is the low-level primitive used by the scene renderer to batch all
/// objects into a single draw call each frame.
pub fn generate_mesh_data(&self, mesh_data: &mut MeshData, transform: &Transform, color: [f32; 4]) {
match self {
Geometry::Cube { size } => {
let s = *size * 0.5;
Geometry::Box { width: s, height: s, depth: s }.generate_mesh_data(
mesh_data, transform, color
);
}
Geometry::Box { width, height, depth } => {
let w = width * 0.5;
let h = height * 0.5;
let d = depth * 0.5;
let p1 = [-w, -h, d]; // Front-Bottom-Left
let p2 = [ w, -h, d]; // Front-Bottom-Right
let p3 = [ w, h, d]; // Front-Top-Right
let p4 = [-w, h, d]; // Front-Top-Left
let p5 = [-w, -h, -d]; // Back-Bottom-Left
let p6 = [ w, -h, -d]; // Back-Bottom-Right
let p7 = [ w, h, -d]; // Back-Top-Right
let p8 = [-w, h, -d]; // Back-Top-Left
// Note: Winding order matters for culling!
mesh_data.add_transformed_quad([p1, p4, p3, p2], transform, color); // Front
mesh_data.add_transformed_quad([p6, p7, p8, p5], transform, color); // Back
mesh_data.add_transformed_quad([p5, p8, p4, p1], transform, color); // Left
mesh_data.add_transformed_quad([p2, p3, p7, p6], transform, color); // Right
mesh_data.add_transformed_quad([p4, p8, p7, p3], transform, color); // Top
mesh_data.add_transformed_quad([p5, p1, p2, p6], transform, color); // Bottom
}
Geometry::Plane { size } => {
let s = size * 0.5;
// Since using culling makes the back of the geometry not visible,
// we can instead make 2 copies of switched vertices.
let p1 = [-s, 0.0, s];
let p2 = [ s, 0.0, s];
let p3 = [ s, 0.0, -s];
let p4 = [-s, 0.0, -s];
// Push the top face
mesh_data.add_transformed_quad([p1, p2, p3, p4], transform, color);
// Push the bottom face (reversed order)
mesh_data.add_transformed_quad([p4, p3, p2, p1], transform, color);
}
Geometry::Pyramid { base_size, height } => {
let s = base_size * 0.5;
let h = height * 0.5;
let tip = [0.0, h, 0.0];
let b1 = [-s, -h, s]; // Front-Left
let b2 = [s, -h, s]; // Front-Right
let b3 = [s, -h, -s]; // Back-Right
let b4 = [-s, -h, -s]; // Back-Left
// 4 Sides
mesh_data.add_transformed_triangle([tip, b2, b1], transform, color); // Front
mesh_data.add_transformed_triangle([tip, b3, b2], transform, color); // Right
mesh_data.add_transformed_triangle([tip, b4, b3], transform, color); // Back
mesh_data.add_transformed_triangle([tip, b1, b4], transform, color); // Left
// Base
mesh_data.add_transformed_quad([b1, b2, b3, b4], transform, color);
}
Geometry::Capsule { radius, height, subdivisions } => {
let r = *radius;
let h = *height;
let subs = *subdivisions as f32;
let half_h = h * 0.5;
// `lat_subs` is the number of vertical vertices. To maintain a "rounded" shape,
// a minimum of 4 subdivisions is used.
let lat_subs = (*subdivisions / 2).max(4);
// `subdivisions` is the number of horizontal vertices
for i in 0..*subdivisions {
let t1 = (i as f32 * 2.0 * std::f32::consts::PI) / subs;
let t2 = ((i + 1) as f32 * 2.0 * std::f32::consts::PI) / subs;
let x1 = t1.cos();
let z1 = t1.sin();
let x2 = t2.cos();
let z2 = t2.sin();
// The body (Cylinder)
mesh_data.add_transformed_quad(
[
[x1 * r, -half_h, z1 * r],
[x2 * r, -half_h, z2 * r],
[x2 * r, half_h, z2 * r],
[x1 * r, half_h, z1 * r],
],
transform, color
);
// The 2 hemispheres
for j in 0..lat_subs {
let phi1 = (j as f32 * std::f32::consts::FRAC_PI_2) / lat_subs as f32;
let phi2 = ((j + 1) as f32 * std::f32::consts::FRAC_PI_2) / lat_subs as f32;
let r1 = phi1.cos() * r; let y1 = phi1.sin() * r;
let r2 = phi2.cos() * r; let y2 = phi2.sin() * r;
// TOP CAP (Facing Outwards/Up)
mesh_data.add_transformed_quad(
[
[x1 * r1, half_h + y1, z1 * r1],
[x2 * r1, half_h + y1, z2 * r1],
[x2 * r2, half_h + y2, z2 * r2],
[x1 * r2, half_h + y2, z1 * r2],
],
transform, color
);
// BOTTOM CAP (Facing Outwards/Down)
// To ensure the "base" renders, we reverse the sequence of x1 and x2
// so the normal faces DOWN.
mesh_data.add_transformed_quad(
[
[x1 * r1, -half_h - y1, z1 * r1],
[x1 * r2, -half_h - y2, z1 * r2],
[x2 * r2, -half_h - y2, z2 * r2],
[x2 * r1, -half_h - y1, z2 * r1],
],
transform, color
);
}
}
}
Geometry::Sphere { radius, subdivisions } => {
let r = *radius;
let subs = *subdivisions as f32;
let lat_subs = (*subdivisions / 2).max(4);
for i in 0..*subdivisions {
let t1 = (i as f32 * 2.0 * std::f32::consts::PI) / subs;
let t2 = ((i + 1) as f32 * 2.0 * std::f32::consts::PI) / subs;
let (x1, z1) = (t1.cos(), t1.sin());
let (x2, z2) = (t2.cos(), t2.sin());
for j in 0..lat_subs {
// Angle from bottom (-PI/2) to top (PI/2)
let phi1 = (j as f32 * std::f32::consts::PI) / lat_subs as f32 - std::f32::consts::FRAC_PI_2;
let phi2 = ((j + 1) as f32 * std::f32::consts::PI) / lat_subs as f32 - std::f32::consts::FRAC_PI_2;
let r1 = phi1.cos() * r; let y1 = phi1.sin() * r;
let r2 = phi2.cos() * r; let y2 = phi2.sin() * r;
mesh_data.add_transformed_quad(
[
[x1 * r1, y1, z1 * r1],
[x2 * r1, y1, z2 * r1],
[x2 * r2, y2, z2 * r2],
[x1 * r2, y2, z1 * r2],
],
transform, color
);
}
}
}
}
}
}