1use bincode::{config, Decode, Encode};
36use khora_core::asset::AssetUUID;
37use khora_core::math::{Aabb, Vec2, Vec3};
38use khora_core::renderer::api::{
39 pipeline::{PrimitiveTopology, VertexAttributeDescriptor, VertexFormat},
40 scene::Mesh,
41};
42
43#[derive(
45 Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, serde::Serialize, serde::Deserialize,
46)]
47pub enum ProceduralMeshKind {
48 Cube,
50 Sphere,
52 Plane,
54}
55
56#[derive(Debug, Clone, PartialEq)]
71pub enum MeshRef {
72 Procedural {
75 kind: ProceduralMeshKind,
77 params: [f32; 4],
79 uuid: AssetUUID,
81 },
82 Asset(AssetUUID),
84}
85
86impl crate::ecs::Component for MeshRef {}
87
88impl MeshRef {
89 pub fn procedural(kind: ProceduralMeshKind, params: [f32; 4]) -> Self {
95 let mut key = Vec::with_capacity(1 + 16);
96 key.push(match kind {
97 ProceduralMeshKind::Cube => 0u8,
98 ProceduralMeshKind::Sphere => 1,
99 ProceduralMeshKind::Plane => 2,
100 });
101 for p in params {
102 key.extend_from_slice(&p.to_le_bytes());
103 }
104 let uuid = AssetUUID::new_v5(&blake3::hash(&key).to_hex());
105 Self::Procedural { kind, params, uuid }
106 }
107
108 pub fn uuid(&self) -> AssetUUID {
112 match self {
113 Self::Procedural { uuid, .. } => *uuid,
114 Self::Asset(uuid) => *uuid,
115 }
116 }
117
118 pub fn unit_cube() -> Self {
120 Self::procedural(ProceduralMeshKind::Cube, [1.0, 0.0, 0.0, 0.0])
121 }
122}
123
124#[derive(Encode, Decode, serde::Serialize, serde::Deserialize)]
129enum SerializableMeshRef {
130 Procedural {
132 kind: ProceduralMeshKind,
134 params: [f32; 4],
136 },
137 Asset(AssetUUID),
139}
140
141impl From<&MeshRef> for SerializableMeshRef {
142 fn from(mesh_ref: &MeshRef) -> Self {
143 match mesh_ref {
144 MeshRef::Procedural { kind, params, .. } => Self::Procedural {
145 kind: *kind,
146 params: *params,
147 },
148 MeshRef::Asset(uuid) => Self::Asset(*uuid),
149 }
150 }
151}
152
153impl From<SerializableMeshRef> for MeshRef {
154 fn from(on_disk: SerializableMeshRef) -> Self {
155 match on_disk {
156 SerializableMeshRef::Procedural { kind, params } => Self::procedural(kind, params),
157 SerializableMeshRef::Asset(uuid) => Self::Asset(uuid),
158 }
159 }
160}
161
162fn serialize_mesh_ref(
164 world: &crate::ecs::World,
165 entity: khora_core::ecs::entity::EntityId,
166) -> Option<Vec<u8>> {
167 let mesh_ref = world.get::<MeshRef>(entity)?;
168 let on_disk = SerializableMeshRef::from(mesh_ref);
169 bincode::encode_to_vec(&on_disk, config::standard()).ok()
170}
171
172fn deserialize_mesh_ref(
174 world: &mut crate::ecs::World,
175 entity: khora_core::ecs::entity::EntityId,
176 data: &[u8],
177) -> Result<(), String> {
178 let (on_disk, _): (SerializableMeshRef, _) =
179 bincode::decode_from_slice(data, config::standard()).map_err(|e| e.to_string())?;
180 world
181 .add_component(entity, MeshRef::from(on_disk))
182 .map_err(|e| format!("{e:?}"))?;
183 Ok(())
184}
185
186fn mesh_ref_to_json(
189 world: &crate::ecs::World,
190 entity: khora_core::ecs::entity::EntityId,
191) -> Option<serde_json::Value> {
192 let mesh_ref = world.get::<MeshRef>(entity)?;
193 serde_json::to_value(SerializableMeshRef::from(mesh_ref)).ok()
194}
195
196fn mesh_ref_from_json(
198 world: &mut crate::ecs::World,
199 entity: khora_core::ecs::entity::EntityId,
200 value: &serde_json::Value,
201) -> Result<(), String> {
202 let on_disk: SerializableMeshRef =
203 serde_json::from_value(value.clone()).map_err(|e| e.to_string())?;
204 let mesh_ref = MeshRef::from(on_disk);
205 if !world.set_component(entity, mesh_ref.clone()) {
206 world
207 .add_component(entity, mesh_ref)
208 .map_err(|e| format!("{e:?}"))?;
209 }
210 Ok(())
211}
212
213inventory::submit! {
214 crate::scene::ComponentRegistration {
215 type_id: std::any::TypeId::of::<MeshRef>(),
216 type_name: "MeshRef",
217 provenance: crate::ecs::ComponentProvenance::Authored,
218 serialize_recipe: serialize_mesh_ref,
219 deserialize_recipe: deserialize_mesh_ref,
220 create_default: |world, entity| {
221 world
222 .add_component(entity, MeshRef::unit_cube())
223 .map_err(|e| format!("{e:?}"))?;
224 Ok(())
225 },
226 to_json: mesh_ref_to_json,
227 from_json: mesh_ref_from_json,
228 remove: |world, entity| {
229 match world.remove_component::<MeshRef>(entity) {
230 Ok(_) => Ok(()),
231 Err(e) => Err(format!("{e:?}")),
232 }
233 },
234 }
235}
236
237pub fn reconstruct_procedural_mesh(kind: ProceduralMeshKind, params: [f32; 4]) -> Mesh {
246 match kind {
247 ProceduralMeshKind::Cube => create_cube(params[0]),
248 ProceduralMeshKind::Plane => create_plane(params[0], params[1]),
249 ProceduralMeshKind::Sphere => create_sphere(params[0], params[1] as u32, params[2] as u32),
250 }
251}
252
253fn default_vertex_layout() -> Vec<VertexAttributeDescriptor> {
254 vec![
255 VertexAttributeDescriptor {
256 shader_location: 0,
257 format: VertexFormat::Float32x3,
258 offset: 0,
259 },
260 VertexAttributeDescriptor {
261 shader_location: 1,
262 format: VertexFormat::Float32x3,
263 offset: 12,
264 },
265 VertexAttributeDescriptor {
266 shader_location: 2,
267 format: VertexFormat::Float32x2,
268 offset: 24,
269 },
270 ]
271}
272
273pub fn create_plane(size: f32, y: f32) -> Mesh {
275 let half = size / 2.0;
276 let positions = vec![
277 Vec3::new(-half, y, -half),
278 Vec3::new(half, y, -half),
279 Vec3::new(half, y, half),
280 Vec3::new(-half, y, half),
281 ];
282 let normals = vec![
283 Vec3::new(0.0, 1.0, 0.0),
284 Vec3::new(0.0, 1.0, 0.0),
285 Vec3::new(0.0, 1.0, 0.0),
286 Vec3::new(0.0, 1.0, 0.0),
287 ];
288 let tex_coords = vec![
289 Vec2::new(0.0, 0.0),
290 Vec2::new(1.0, 0.0),
291 Vec2::new(1.0, 1.0),
292 Vec2::new(0.0, 1.0),
293 ];
294 let indices = vec![0u32, 2, 1, 0, 3, 2];
298 Mesh {
299 positions,
300 normals: Some(normals),
301 tex_coords: Some(tex_coords),
302 tangents: None,
303 colors: None,
304 indices: Some(indices),
305 primitive_type: PrimitiveTopology::TriangleList,
306 bounding_box: Aabb::from_min_max(Vec3::new(-half, y, -half), Vec3::new(half, y, half)),
307 vertex_layout: default_vertex_layout(),
308 }
309}
310
311pub fn create_cube(size: f32) -> Mesh {
313 let half = size / 2.0;
314 let positions = vec![
315 Vec3::new(-half, -half, half),
317 Vec3::new(half, -half, half),
318 Vec3::new(half, half, half),
319 Vec3::new(-half, half, half),
320 Vec3::new(half, -half, -half),
322 Vec3::new(-half, -half, -half),
323 Vec3::new(-half, half, -half),
324 Vec3::new(half, half, -half),
325 Vec3::new(half, -half, half),
327 Vec3::new(half, -half, -half),
328 Vec3::new(half, half, -half),
329 Vec3::new(half, half, half),
330 Vec3::new(-half, -half, -half),
332 Vec3::new(-half, -half, half),
333 Vec3::new(-half, half, half),
334 Vec3::new(-half, half, -half),
335 Vec3::new(-half, half, half),
337 Vec3::new(half, half, half),
338 Vec3::new(half, half, -half),
339 Vec3::new(-half, half, -half),
340 Vec3::new(-half, -half, -half),
342 Vec3::new(half, -half, -half),
343 Vec3::new(half, -half, half),
344 Vec3::new(-half, -half, half),
345 ];
346 let normals = vec![
347 Vec3::new(0.0, 0.0, 1.0),
348 Vec3::new(0.0, 0.0, 1.0),
349 Vec3::new(0.0, 0.0, 1.0),
350 Vec3::new(0.0, 0.0, 1.0),
351 Vec3::new(0.0, 0.0, -1.0),
352 Vec3::new(0.0, 0.0, -1.0),
353 Vec3::new(0.0, 0.0, -1.0),
354 Vec3::new(0.0, 0.0, -1.0),
355 Vec3::new(1.0, 0.0, 0.0),
356 Vec3::new(1.0, 0.0, 0.0),
357 Vec3::new(1.0, 0.0, 0.0),
358 Vec3::new(1.0, 0.0, 0.0),
359 Vec3::new(-1.0, 0.0, 0.0),
360 Vec3::new(-1.0, 0.0, 0.0),
361 Vec3::new(-1.0, 0.0, 0.0),
362 Vec3::new(-1.0, 0.0, 0.0),
363 Vec3::new(0.0, 1.0, 0.0),
364 Vec3::new(0.0, 1.0, 0.0),
365 Vec3::new(0.0, 1.0, 0.0),
366 Vec3::new(0.0, 1.0, 0.0),
367 Vec3::new(0.0, -1.0, 0.0),
368 Vec3::new(0.0, -1.0, 0.0),
369 Vec3::new(0.0, -1.0, 0.0),
370 Vec3::new(0.0, -1.0, 0.0),
371 ];
372 let tex_coords: Vec<Vec2> = (0..6)
373 .flat_map(|_| {
374 [
375 Vec2::new(0.0, 0.0),
376 Vec2::new(1.0, 0.0),
377 Vec2::new(1.0, 1.0),
378 Vec2::new(0.0, 1.0),
379 ]
380 })
381 .collect();
382 let indices = vec![
383 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17,
384 18, 16, 18, 19, 20, 21, 22, 20, 22, 23,
385 ];
386 Mesh {
387 positions,
388 normals: Some(normals),
389 tex_coords: Some(tex_coords),
390 tangents: None,
391 colors: None,
392 indices: Some(indices),
393 primitive_type: PrimitiveTopology::TriangleList,
394 bounding_box: Aabb::from_min_max(
395 Vec3::new(-half, -half, -half),
396 Vec3::new(half, half, half),
397 ),
398 vertex_layout: default_vertex_layout(),
399 }
400}
401
402pub fn create_sphere(radius: f32, segments: u32, rings: u32) -> Mesh {
404 let mut positions = Vec::new();
405 let mut normals = Vec::new();
406 let mut tex_coords = Vec::new();
407
408 for ring in 0..=rings {
409 let phi = std::f32::consts::PI * (ring as f32 / rings as f32);
410 let y = radius * phi.cos();
411 let ring_radius = radius * phi.sin();
412 for segment in 0..=segments {
413 let theta = 2.0 * std::f32::consts::PI * (segment as f32 / segments as f32);
414 let x = ring_radius * theta.cos();
415 let z = ring_radius * theta.sin();
416 positions.push(Vec3::new(x, y, z));
417 normals.push(Vec3::new(x / radius, y / radius, z / radius));
418 tex_coords.push(Vec2::new(
419 segment as f32 / segments as f32,
420 ring as f32 / rings as f32,
421 ));
422 }
423 }
424
425 let mut indices = Vec::new();
429 for ring in 0..rings {
430 for segment in 0..segments {
431 let current = ring * (segments + 1) + segment;
432 let next = current + segments + 1;
433 indices.push(current);
434 indices.push(current + 1);
435 indices.push(next);
436 indices.push(current + 1);
437 indices.push(next + 1);
438 indices.push(next);
439 }
440 }
441
442 Mesh {
443 positions,
444 normals: Some(normals),
445 tex_coords: Some(tex_coords),
446 tangents: None,
447 colors: None,
448 indices: Some(indices),
449 primitive_type: PrimitiveTopology::TriangleList,
450 bounding_box: Aabb::from_min_max(
451 Vec3::new(-radius, -radius, -radius),
452 Vec3::new(radius, radius, radius),
453 ),
454 vertex_layout: default_vertex_layout(),
455 }
456}
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461 use crate::ecs::World;
462 use crate::scene::registry::ComponentRegistration;
463
464 #[test]
466 fn procedural_mesh_ref_recipe_round_trip() {
467 let mut src = World::new();
468 let entity = src.spawn(MeshRef::procedural(
469 ProceduralMeshKind::Sphere,
470 [0.75, 32.0, 16.0, 0.0],
471 ));
472
473 let reg = inventory::iter::<ComponentRegistration>
474 .into_iter()
475 .find(|r| r.type_name == "MeshRef")
476 .expect("MeshRef registration present");
477 let bytes = (reg.serialize_recipe)(&src, entity).expect("serialize");
478
479 let mut dst = World::new();
480 let new_entity = dst.spawn(());
481 (reg.deserialize_recipe)(&mut dst, new_entity, &bytes).expect("deserialize");
482
483 let restored = dst.get::<MeshRef>(new_entity).expect("mesh ref restored");
484 assert_eq!(
486 restored,
487 &MeshRef::procedural(ProceduralMeshKind::Sphere, [0.75, 32.0, 16.0, 0.0])
488 );
489 }
490
491 #[test]
493 fn asset_mesh_ref_recipe_round_trip_preserves_uuid() {
494 let mut src = World::new();
495 let uuid = AssetUUID::new_v5("meshes/teapot.gltf");
496 let entity = src.spawn(MeshRef::Asset(uuid));
497
498 let reg = inventory::iter::<ComponentRegistration>
499 .into_iter()
500 .find(|r| r.type_name == "MeshRef")
501 .expect("MeshRef registration present");
502 let bytes = (reg.serialize_recipe)(&src, entity).expect("serialize");
503
504 let mut dst = World::new();
505 let new_entity = dst.spawn(());
506 (reg.deserialize_recipe)(&mut dst, new_entity, &bytes).expect("deserialize");
507
508 let restored = dst.get::<MeshRef>(new_entity).expect("mesh ref restored");
509 assert_eq!(restored, &MeshRef::Asset(uuid));
510 }
511
512 #[test]
514 fn cube_has_expected_vertex_and_index_counts() {
515 let mesh = reconstruct_procedural_mesh(ProceduralMeshKind::Cube, [2.0, 0.0, 0.0, 0.0]);
516 assert_eq!(mesh.positions.len(), 24);
517 assert_eq!(mesh.indices.as_ref().map_or(0, |i| i.len()), 36);
518 }
519}