1use super::Line;
12use crate::gfx::render_types::LineVertex;
13use crate::math::tan;
14use crate::math::vec3::{cross, dot, length, lerp as lerp3, sub};
15use alloc::vec::Vec;
16
17const VERTS_PER_SEGMENT: usize = 6;
19
20#[derive(Copy, Clone, Debug)]
25pub struct LineCamera {
26 pub view: [[f32; 4]; 4],
28 pub cam_pos: [f32; 3],
30 pub fov_y_radians: f32,
32 pub viewport: [f32; 2],
34 pub near: f32,
36}
37
38impl LineCamera {
39 fn forward(&self) -> [f32; 3] {
41 [-self.view[0][2], -self.view[1][2], -self.view[2][2]]
42 }
43
44 fn right(&self) -> [f32; 3] {
47 [self.view[0][0], self.view[1][0], self.view[2][0]]
48 }
49
50 fn tan_half_fov(&self) -> f32 {
53 tan(self.fov_y_radians * 0.5)
54 }
55
56 fn world_per_pixel(&self, depth: f32, tan_half: f32) -> f32 {
58 2.0 * depth * tan_half / self.viewport[1]
59 }
60
61 fn usable(&self, tan_half: f32) -> bool {
62 self.viewport[0] > 0.0 && self.viewport[1] > 0.0 && tan_half > 0.0 && tan_half.is_finite()
63 }
64}
65
66fn normalize(v: [f32; 3]) -> Option<[f32; 3]> {
67 let len = length(v);
68 (len > 1e-6).then(|| [v[0] / len, v[1] / len, v[2] / len])
69}
70
71fn lerp4(a: [f32; 4], b: [f32; 4], t: f32) -> [f32; 4] {
72 [
73 a[0] + (b[0] - a[0]) * t,
74 a[1] + (b[1] - a[1]) * t,
75 a[2] + (b[2] - a[2]) * t,
76 a[3] + (b[3] - a[3]) * t,
77 ]
78}
79
80struct Clipped {
83 start: [f32; 3],
84 end: [f32; 3],
85 start_color: [f32; 4],
86 end_color: [f32; 4],
87}
88
89fn clip_to_near(line: &Line, cam: &LineCamera) -> Option<Clipped> {
92 let fwd = cam.forward();
93 let d0 = dot(sub(line.start, cam.cam_pos), fwd);
94 let d1 = dot(sub(line.end, cam.cam_pos), fwd);
95 let near = cam.near.max(1e-4);
96 match (d0 >= near, d1 >= near) {
97 (true, true) => Some(Clipped {
98 start: line.start,
99 end: line.end,
100 start_color: line.start_color,
101 end_color: line.end_color,
102 }),
103 (false, false) => None,
104 (true, false) => {
105 let t = (d0 - near) / (d0 - d1);
106 Some(Clipped {
107 start: line.start,
108 end: lerp3(line.start, line.end, t),
109 start_color: line.start_color,
110 end_color: lerp4(line.start_color, line.end_color, t),
111 })
112 }
113 (false, true) => {
114 let t = (near - d0) / (d1 - d0);
115 Some(Clipped {
116 start: lerp3(line.start, line.end, t),
117 end: line.end,
118 start_color: lerp4(line.start_color, line.end_color, t),
119 end_color: line.end_color,
120 })
121 }
122 }
123}
124
125pub fn build_vertices(lines: &[Line], cam: &LineCamera) -> Vec<LineVertex> {
130 let mut out = Vec::new();
131 build_vertices_into(lines.iter().copied(), cam, &mut out);
132 out
133}
134
135pub fn build_vertices_into(
138 lines: impl Iterator<Item = Line>,
139 cam: &LineCamera,
140 out: &mut Vec<LineVertex>,
141) {
142 out.clear();
143 let tan_half = cam.tan_half_fov();
144 if !cam.usable(tan_half) {
145 return;
146 }
147 out.reserve(lines.size_hint().0 * VERTS_PER_SEGMENT);
148 let fwd = cam.forward();
149 for line in lines {
150 let line = &line;
151 if line.width_px <= 0.0 || (line.start_color[3] <= 0.0 && line.end_color[3] <= 0.0) {
152 continue;
153 }
154 let Some(seg) = clip_to_near(line, cam) else {
155 continue;
156 };
157 let Some(dir) = normalize(sub(seg.end, seg.start)) else {
158 continue;
159 };
160 let half_px = line.width_px * 0.5;
161 let corner = |p: [f32; 3], color: [f32; 4], side: f32| {
166 let to_eye = sub(p, cam.cam_pos);
167 let normal = normalize(cross(dir, to_eye)).unwrap_or_else(|| cam.right());
168 let half =
169 half_px * cam.world_per_pixel(dot(to_eye, fwd).max(cam.near.max(1e-4)), tan_half);
170 LineVertex {
171 pos: [
172 p[0] + normal[0] * half * side,
173 p[1] + normal[1] * half * side,
174 p[2] + normal[2] * half * side,
175 ],
176 edge: side,
177 color,
178 }
179 };
180 let a = corner(seg.start, seg.start_color, -1.0);
181 let b = corner(seg.start, seg.start_color, 1.0);
182 let c = corner(seg.end, seg.end_color, -1.0);
183 let d = corner(seg.end, seg.end_color, 1.0);
184 out.extend_from_slice(&[a, b, c, c, b, d]);
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use crate::gfx::camera::view_matrix;
192
193 const VP: [f32; 2] = [1280.0, 720.0];
194
195 fn camera() -> LineCamera {
196 LineCamera {
197 view: view_matrix([0.0; 3], 0.0, 0.0),
199 cam_pos: [0.0; 3],
200 fov_y_radians: core::f32::consts::FRAC_PI_2,
201 viewport: VP,
202 near: 0.1,
203 }
204 }
205
206 fn line(start: [f32; 3], end: [f32; 3]) -> Line {
207 Line {
208 start,
209 end,
210 start_color: [1.0, 0.0, 0.0, 1.0],
211 end_color: [1.0, 0.0, 0.0, 0.0],
212 width_px: 2.0,
213 }
214 }
215
216 fn project(cam: &LineCamera, p: [f32; 3]) -> [f32; 2] {
219 let v = &cam.view;
220 let x = v[0][0] * p[0] + v[1][0] * p[1] + v[2][0] * p[2] + v[3][0];
221 let y = v[0][1] * p[0] + v[1][1] * p[1] + v[2][1] * p[2] + v[3][1];
222 let z = v[0][2] * p[0] + v[1][2] * p[1] + v[2][2] * p[2] + v[3][2];
223 let depth = -z;
224 let tan_half = (cam.fov_y_radians * 0.5).tan();
225 let aspect = cam.viewport[0] / cam.viewport[1];
226 [
227 (x / (depth * tan_half * aspect) + 1.0) * 0.5 * cam.viewport[0],
228 (1.0 - y / (depth * tan_half)) * 0.5 * cam.viewport[1],
229 ]
230 }
231
232 #[test]
233 fn each_segment_expands_to_two_triangles() {
234 let cam = camera();
235 let verts = build_vertices(&[line([0.0, 0.0, -5.0], [0.0, 0.0, -50.0])], &cam);
236 assert_eq!(verts.len(), VERTS_PER_SEGMENT);
237 assert!(verts.iter().any(|v| v.edge == -1.0));
239 assert!(verts.iter().any(|v| v.edge == 1.0));
240 }
241
242 #[test]
243 fn ribbon_holds_its_pixel_width_at_any_depth() {
244 let cam = camera();
245 let l = line([-5.0, 0.0, -5.0], [40.0, 0.0, -20.0]);
248 let verts = build_vertices(&[l], &cam);
249 let near_w = {
250 let a = project(&cam, verts[0].pos);
251 let b = project(&cam, verts[1].pos);
252 ((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)).sqrt()
253 };
254 let far_w = {
255 let a = project(&cam, verts[2].pos);
256 let b = project(&cam, verts[5].pos);
257 ((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)).sqrt()
258 };
259 assert!((near_w - l.width_px).abs() < 0.05, "near end {near_w}");
260 assert!((far_w - l.width_px).abs() < 0.05, "far end {far_w}");
261 }
262
263 #[test]
264 fn colors_carry_the_fade_to_the_far_end() {
265 let cam = camera();
266 let verts = build_vertices(&[line([0.0, 0.0, -5.0], [0.0, 0.0, -500.0])], &cam);
267 assert_eq!(verts[0].color[3], 1.0, "solid at the near end");
268 assert_eq!(verts[5].color[3], 0.0, "faded out at the far end");
269 }
270
271 #[test]
272 fn segments_behind_the_camera_are_clipped_away() {
273 let cam = camera();
274 assert!(build_vertices(&[line([0.0, 0.0, 5.0], [0.0, 0.0, 50.0])], &cam).is_empty());
276 let verts = build_vertices(&[line([0.0, 0.0, 10.0], [0.0, 0.0, -10.0])], &cam);
279 assert_eq!(verts.len(), VERTS_PER_SEGMENT);
280 for v in &verts {
281 assert!(
282 v.pos[2] <= -cam.near,
283 "corner {:?} is behind the near",
284 v.pos
285 );
286 }
287 }
288
289 #[test]
290 fn clipping_interpolates_the_endpoint_colour() {
291 let cam = camera();
292 let l = Line {
295 start: [0.0, 0.0, 10.0],
296 end: [0.0, 0.0, -10.0],
297 start_color: [1.0, 0.0, 0.0, 1.0],
298 end_color: [1.0, 0.0, 0.0, 0.0],
299 width_px: 2.0,
300 };
301 let verts = build_vertices(&[l], &cam);
302 assert!(
303 (verts[0].color[3] - 0.5).abs() < 0.02,
304 "{:?}",
305 verts[0].color
306 );
307 }
308
309 #[test]
310 fn nothing_to_draw_yields_no_vertices() {
311 let cam = camera();
312 assert!(build_vertices(&[], &cam).is_empty());
313 let zero_len = line([1.0, 2.0, -3.0], [1.0, 2.0, -3.0]);
316 assert!(build_vertices(&[zero_len], &cam).is_empty());
317 let mut no_width = line([0.0, 0.0, -5.0], [0.0, 0.0, -50.0]);
318 no_width.width_px = 0.0;
319 assert!(build_vertices(&[no_width], &cam).is_empty());
320 let mut invisible = line([0.0, 0.0, -5.0], [0.0, 0.0, -50.0]);
321 invisible.start_color[3] = 0.0;
322 invisible.end_color[3] = 0.0;
323 assert!(build_vertices(&[invisible], &cam).is_empty());
324 }
325
326 #[test]
327 fn a_degenerate_viewport_draws_nothing() {
328 let mut cam = camera();
329 cam.viewport = [0.0, 720.0];
330 assert!(build_vertices(&[line([0.0, 0.0, -5.0], [0.0, 0.0, -50.0])], &cam).is_empty());
331 }
332
333 #[test]
334 fn a_line_aimed_at_the_eye_falls_back_to_the_camera_right() {
335 let cam = camera();
336 let verts = build_vertices(&[line([0.0, 0.0, -5.0], [0.0, 0.0, -50.0])], &cam);
339 assert_eq!(verts.len(), VERTS_PER_SEGMENT);
340 assert!(verts[0].pos[0] < 0.0 && verts[1].pos[0] > 0.0);
343 for v in &verts {
344 assert!(v.pos.iter().all(|c| c.is_finite()));
345 }
346 }
347}