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
//! Decal projection volumes and grounded blob shadows for no_std embedded systems.
//!
//! Inspired by Fyrox's `fyrox-impl::scene::decal::Decal`, adapted for zero-heap-allocation
//! MCU rendering.
//!
//! Decals project textures (bullet holes, footstep marks, blood splatters, scorch marks)
//! onto existing 3D geometry without modifying the source mesh.
//!
//! # Example
//! ```
//! use embedded_3dgfx::decal::{BlobShadow, DecalProjector};
//! use nalgebra::{Point3, Vector3};
//!
//! let projector = DecalProjector::new(
//! Point3::new(0.0, 1.0, 0.0),
//! Vector3::new(0.5, 0.5, 0.5),
//! Vector3::new(0.0, -1.0, 0.0), // projecting downward
//! );
//!
//! let shadow = BlobShadow::new(Point3::new(0.0, 0.05, 0.0), 0.4);
//! ```
use embedded_graphics_core::pixelcolor::Rgb565;
use nalgebra::{Point3, Vector3};
/// A decal projection volume defining an oriented bounding box.
#[derive(Debug, Clone, Copy)]
pub struct DecalProjector {
/// World-space center of projection volume.
pub position: Point3<f32>,
/// Half-extents `(half_width, half_height, half_depth)`.
pub half_extents: Vector3<f32>,
/// Projection direction (e.g. `(0, -1, 0)` for floor decals).
pub direction: Vector3<f32>,
}
impl DecalProjector {
/// Create a new decal projector volume.
pub fn new(position: Point3<f32>, half_extents: Vector3<f32>, direction: Vector3<f32>) -> Self {
let dir = if direction.norm_squared() > 1e-6 {
direction.normalize()
} else {
Vector3::new(0.0, -1.0, 0.0)
};
Self {
position,
half_extents,
direction: dir,
}
}
/// Check if a 3D point is inside the decal bounding volume.
pub fn contains_point(&self, p: Point3<f32>) -> bool {
let diff = p - self.position;
diff.x.abs() <= self.half_extents.x
&& diff.y.abs() <= self.half_extents.y
&& diff.z.abs() <= self.half_extents.z
}
/// Calculate projected UV coordinates `[0.0, 1.0]` for a point inside the volume.
pub fn project_uv(&self, p: Point3<f32>) -> [f32; 2] {
let diff = p - self.position;
let u = ((diff.x / (2.0 * self.half_extents.x)) + 0.5).clamp(0.0, 1.0);
let v = ((diff.z / (2.0 * self.half_extents.z)) + 0.5).clamp(0.0, 1.0);
[u, v]
}
}
/// A circular grounded blob shadow quad for character/object grounding.
#[derive(Debug, Clone, Copy)]
pub struct BlobShadow {
/// Center of shadow on the ground plane.
pub center: Point3<f32>,
/// Radius of shadow.
pub radius: f32,
/// Shadow color (typically dark/black or dark tint).
pub color: Rgb565,
}
impl BlobShadow {
/// Create a new blob shadow.
pub const fn new(center: Point3<f32>, radius: f32) -> Self {
Self {
center,
radius,
color: Rgb565::new(2, 4, 2),
}
}
/// Set shadow color.
pub const fn with_color(mut self, color: Rgb565) -> Self {
self.color = color;
self
}
/// Generate a 4-vertex horizontal quad in world space for this shadow.
pub fn generate_quad_verts(&self) -> [[f32; 3]; 4] {
let r = self.radius;
let p = self.center;
[
[p.x - r, p.y, p.z - r],
[p.x + r, p.y, p.z - r],
[p.x + r, p.y, p.z + r],
[p.x - r, p.y, p.z + r],
]
}
/// Generate a 2-triangle face index array.
pub const fn generate_faces() -> [[usize; 3]; 2] {
[[0, 1, 2], [0, 2, 3]]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decal_projector_containment_and_uv() {
let projector = DecalProjector::new(
Point3::new(0.0, 0.0, 0.0),
Vector3::new(1.0, 1.0, 1.0),
Vector3::new(0.0, -1.0, 0.0),
);
assert!(projector.contains_point(Point3::new(0.5, 0.5, 0.5)));
assert!(!projector.contains_point(Point3::new(1.5, 0.0, 0.0)));
let uv_center = projector.project_uv(Point3::new(0.0, 0.0, 0.0));
assert_eq!(uv_center, [0.5, 0.5]);
let uv_min = projector.project_uv(Point3::new(-1.0, 0.0, -1.0));
assert_eq!(uv_min, [0.0, 0.0]);
let uv_max = projector.project_uv(Point3::new(1.0, 0.0, 1.0));
assert_eq!(uv_max, [1.0, 1.0]);
}
#[test]
fn test_blob_shadow_quad_generation() {
let shadow = BlobShadow::new(Point3::new(0.0, 0.1, 0.0), 1.0);
let verts = shadow.generate_quad_verts();
assert_eq!(verts.len(), 4);
assert_eq!(verts[0], [-1.0, 0.1, -1.0]);
assert_eq!(verts[2], [1.0, 0.1, 1.0]);
}
}