Implement elite style scanner
This commit is contained in:
@@ -1 +1 @@
|
||||
- [ ] Implement a 3D map interface kind of like the one in the classic 80s game Elite
|
||||
- [x] Implement a 3D map interface kind of like the one in the classic 80s game Elite
|
||||
|
||||
+14
-1
@@ -87,7 +87,7 @@
|
||||
|
||||
#coord-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
bottom: 25%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
@@ -97,6 +97,17 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#scanner-separator {
|
||||
position: fixed;
|
||||
bottom: 25%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: rgba(0, 180, 0, 0.6);
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#coord-bar .coord-inner {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
@@ -157,6 +168,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="scanner-separator"></div>
|
||||
|
||||
<div id="coord-bar">
|
||||
<div class="coord-inner">
|
||||
<span><span class="coord-label">X</span><span id="coord-x">10.0</span></span>
|
||||
|
||||
+13
-2
@@ -2,6 +2,7 @@ mod camera;
|
||||
mod input;
|
||||
mod mesh;
|
||||
mod renderer;
|
||||
mod scanner;
|
||||
mod scene;
|
||||
|
||||
use std::sync::Arc;
|
||||
@@ -10,6 +11,7 @@ use camera::Camera;
|
||||
use glam::Vec3;
|
||||
use input::InputState;
|
||||
use renderer::Renderer;
|
||||
use scanner::Scanner;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_bindgen::JsCast;
|
||||
use winit::{
|
||||
@@ -29,6 +31,7 @@ struct App {
|
||||
scene_meshes_and_objects: Option<(Vec<mesh::Mesh>, Vec<scene::SceneObject>)>,
|
||||
start_time: Option<web_time::Instant>,
|
||||
last_frame: Option<web_time::Instant>,
|
||||
scanner: Scanner,
|
||||
help_visible: bool,
|
||||
focused: bool,
|
||||
}
|
||||
@@ -49,6 +52,7 @@ impl App {
|
||||
},
|
||||
input: InputState::new(),
|
||||
scene_meshes_and_objects: Some((meshes, objects)),
|
||||
scanner: Scanner::new(60.0),
|
||||
start_time: None,
|
||||
last_frame: None,
|
||||
help_visible: true,
|
||||
@@ -286,7 +290,8 @@ impl ApplicationHandler for App {
|
||||
WindowEvent::Resized(size) => {
|
||||
if let Some(r) = &mut self.renderer {
|
||||
r.resize(size.width, size.height);
|
||||
self.camera.aspect = size.width as f32 / size.height.max(1) as f32;
|
||||
let game_h = size.height as f32 - r.scanner_strip_height();
|
||||
self.camera.aspect = size.width as f32 / game_h.max(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,7 +330,13 @@ impl ApplicationHandler for App {
|
||||
let renderer = self.renderer.as_ref().unwrap();
|
||||
let (_, objects) = self.scene_meshes_and_objects.as_ref().unwrap();
|
||||
|
||||
match renderer.render(&self.camera, objects, time) {
|
||||
let scanner_blips = self.scanner.compute_blips(
|
||||
self.camera.position,
|
||||
self.camera.yaw,
|
||||
objects,
|
||||
);
|
||||
|
||||
match renderer.render(&self.camera, objects, time, &scanner_blips) {
|
||||
Ok(_) => {}
|
||||
Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
|
||||
let size = self.window.as_ref().unwrap().inner_size();
|
||||
|
||||
+226
-47
@@ -45,6 +45,92 @@ pub struct Mesh {
|
||||
pub vertices: Vec<Vertex>,
|
||||
}
|
||||
|
||||
/// Push a box defined by min/max corners with a given color and correct normals.
|
||||
fn push_box(verts: &mut Vec<Vertex>, min: Vec3, max: Vec3, color: [f32; 3]) {
|
||||
let corners = [
|
||||
Vec3::new(min.x, min.y, min.z), // 0: left-bottom-back
|
||||
Vec3::new(max.x, min.y, min.z), // 1: right-bottom-back
|
||||
Vec3::new(max.x, max.y, min.z), // 2: right-top-back
|
||||
Vec3::new(min.x, max.y, min.z), // 3: left-top-back
|
||||
Vec3::new(min.x, min.y, max.z), // 4: left-bottom-front
|
||||
Vec3::new(max.x, min.y, max.z), // 5: right-bottom-front
|
||||
Vec3::new(max.x, max.y, max.z), // 6: right-top-front
|
||||
Vec3::new(min.x, max.y, max.z), // 7: left-top-front
|
||||
];
|
||||
|
||||
// 6 faces, 2 triangles each, CCW winding from outside
|
||||
let faces: &[([usize; 3], Vec3)] = &[
|
||||
// front (+Z)
|
||||
([4, 5, 6], Vec3::Z),
|
||||
([4, 6, 7], Vec3::Z),
|
||||
// back (-Z)
|
||||
([1, 0, 3], Vec3::NEG_Z),
|
||||
([1, 3, 2], Vec3::NEG_Z),
|
||||
// right (+X)
|
||||
([5, 1, 2], Vec3::X),
|
||||
([5, 2, 6], Vec3::X),
|
||||
// left (-X)
|
||||
([0, 4, 7], Vec3::NEG_X),
|
||||
([0, 7, 3], Vec3::NEG_X),
|
||||
// top (+Y)
|
||||
([3, 7, 6], Vec3::Y),
|
||||
([3, 6, 2], Vec3::Y),
|
||||
// bottom (-Y)
|
||||
([0, 1, 5], Vec3::NEG_Y),
|
||||
([0, 5, 4], Vec3::NEG_Y),
|
||||
];
|
||||
|
||||
for (idx, normal) in faces {
|
||||
for &i in idx {
|
||||
verts.push(Vertex {
|
||||
position: corners[i].into(),
|
||||
normal: (*normal).into(),
|
||||
color,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a mesh from line segments as thin flat quads in the XY plane.
|
||||
fn line_segments_mesh(
|
||||
segments: &[([f32; 2], [f32; 2])],
|
||||
thickness: f32,
|
||||
z: f32,
|
||||
color: [f32; 3],
|
||||
) -> Mesh {
|
||||
let mut vertices = Vec::new();
|
||||
let normal = [0.0, 0.0, 1.0];
|
||||
let ht = thickness / 2.0;
|
||||
|
||||
for &([x0, y0], [x1, y1]) in segments {
|
||||
let dx = x1 - x0;
|
||||
let dy = y1 - y0;
|
||||
let len = (dx * dx + dy * dy).sqrt();
|
||||
if len < 1e-6 {
|
||||
continue;
|
||||
}
|
||||
// Perpendicular (90 deg CCW from direction)
|
||||
let px = -dy / len * ht;
|
||||
let py = dx / len * ht;
|
||||
|
||||
// v0=start+perp, v1=start-perp, v2=end-perp, v3=end+perp
|
||||
let v0 = [x0 + px, y0 + py, z];
|
||||
let v1 = [x0 - px, y0 - py, z];
|
||||
let v2 = [x1 - px, y1 - py, z];
|
||||
let v3 = [x1 + px, y1 + py, z];
|
||||
|
||||
vertices.push(Vertex { position: v0, normal, color });
|
||||
vertices.push(Vertex { position: v1, normal, color });
|
||||
vertices.push(Vertex { position: v2, normal, color });
|
||||
|
||||
vertices.push(Vertex { position: v0, normal, color });
|
||||
vertices.push(Vertex { position: v2, normal, color });
|
||||
vertices.push(Vertex { position: v3, normal, color });
|
||||
}
|
||||
|
||||
Mesh { vertices }
|
||||
}
|
||||
|
||||
impl Mesh {
|
||||
/// Create an octahedron with the given color.
|
||||
pub fn octahedron(color: [f32; 3]) -> Self {
|
||||
@@ -125,53 +211,6 @@ impl Mesh {
|
||||
let y_neg = desat(y_pos);
|
||||
let z_neg = desat(z_pos);
|
||||
|
||||
// Helper: push a box defined by min/max corners with a given color
|
||||
let push_box =
|
||||
|verts: &mut Vec<Vertex>, min: Vec3, max: Vec3, color: [f32; 3]| {
|
||||
let corners = [
|
||||
Vec3::new(min.x, min.y, min.z), // 0: left-bottom-back
|
||||
Vec3::new(max.x, min.y, min.z), // 1: right-bottom-back
|
||||
Vec3::new(max.x, max.y, min.z), // 2: right-top-back
|
||||
Vec3::new(min.x, max.y, min.z), // 3: left-top-back
|
||||
Vec3::new(min.x, min.y, max.z), // 4: left-bottom-front
|
||||
Vec3::new(max.x, min.y, max.z), // 5: right-bottom-front
|
||||
Vec3::new(max.x, max.y, max.z), // 6: right-top-front
|
||||
Vec3::new(min.x, max.y, max.z), // 7: left-top-front
|
||||
];
|
||||
|
||||
// 6 faces, 2 triangles each, CCW winding from outside
|
||||
let faces: &[([usize; 3], Vec3)] = &[
|
||||
// front (+Z)
|
||||
([4, 5, 6], Vec3::Z),
|
||||
([4, 6, 7], Vec3::Z),
|
||||
// back (-Z)
|
||||
([1, 0, 3], Vec3::NEG_Z),
|
||||
([1, 3, 2], Vec3::NEG_Z),
|
||||
// right (+X)
|
||||
([5, 1, 2], Vec3::X),
|
||||
([5, 2, 6], Vec3::X),
|
||||
// left (-X)
|
||||
([0, 4, 7], Vec3::NEG_X),
|
||||
([0, 7, 3], Vec3::NEG_X),
|
||||
// top (+Y)
|
||||
([3, 7, 6], Vec3::Y),
|
||||
([3, 6, 2], Vec3::Y),
|
||||
// bottom (-Y)
|
||||
([0, 1, 5], Vec3::NEG_Y),
|
||||
([0, 5, 4], Vec3::NEG_Y),
|
||||
];
|
||||
|
||||
for (idx, normal) in faces {
|
||||
for &i in idx {
|
||||
verts.push(Vertex {
|
||||
position: corners[i].into(),
|
||||
normal: (*normal).into(),
|
||||
color,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Axis lines — split at origin into positive and negative halves
|
||||
// X axis
|
||||
push_box(&mut vertices, Vec3::new(0.0, -axis_hw, -axis_hw), Vec3::new(extent, axis_hw, axis_hw), x_pos);
|
||||
@@ -371,4 +410,144 @@ impl Mesh {
|
||||
|
||||
Self { vertices }
|
||||
}
|
||||
|
||||
/// Filled circle in XY plane (triangle fan from center), normals pointing +Z.
|
||||
pub fn disc(segments: u32, radius: f32, color: [f32; 3]) -> Self {
|
||||
let mut vertices = Vec::new();
|
||||
let normal = [0.0, 0.0, 1.0];
|
||||
let center = [0.0, 0.0, 0.0];
|
||||
|
||||
for i in 0..segments {
|
||||
let a0 = (i as f32 / segments as f32) * std::f32::consts::TAU;
|
||||
let a1 = ((i + 1) as f32 / segments as f32) * std::f32::consts::TAU;
|
||||
vertices.push(Vertex { position: center, normal, color });
|
||||
vertices.push(Vertex {
|
||||
position: [a0.cos() * radius, a0.sin() * radius, 0.0],
|
||||
normal,
|
||||
color,
|
||||
});
|
||||
vertices.push(Vertex {
|
||||
position: [a1.cos() * radius, a1.sin() * radius, 0.0],
|
||||
normal,
|
||||
color,
|
||||
});
|
||||
}
|
||||
|
||||
Self { vertices }
|
||||
}
|
||||
|
||||
/// Thin annulus in XY plane at given z height.
|
||||
pub fn ring(segments: u32, inner_radius: f32, outer_radius: f32, z: f32, color: [f32; 3]) -> Self {
|
||||
let mut vertices = Vec::new();
|
||||
let normal = [0.0, 0.0, 1.0];
|
||||
|
||||
for i in 0..segments {
|
||||
let a0 = (i as f32 / segments as f32) * std::f32::consts::TAU;
|
||||
let a1 = ((i + 1) as f32 / segments as f32) * std::f32::consts::TAU;
|
||||
|
||||
let in0 = [inner_radius * a0.cos(), inner_radius * a0.sin(), z];
|
||||
let in1 = [inner_radius * a1.cos(), inner_radius * a1.sin(), z];
|
||||
let out0 = [outer_radius * a0.cos(), outer_radius * a0.sin(), z];
|
||||
let out1 = [outer_radius * a1.cos(), outer_radius * a1.sin(), z];
|
||||
|
||||
vertices.push(Vertex { position: in0, normal, color });
|
||||
vertices.push(Vertex { position: out0, normal, color });
|
||||
vertices.push(Vertex { position: out1, normal, color });
|
||||
|
||||
vertices.push(Vertex { position: in0, normal, color });
|
||||
vertices.push(Vertex { position: out1, normal, color });
|
||||
vertices.push(Vertex { position: in1, normal, color });
|
||||
}
|
||||
|
||||
Self { vertices }
|
||||
}
|
||||
|
||||
/// Concentric rings + two perpendicular cross lines for scanner grid overlay.
|
||||
pub fn scanner_grid(num_rings: u32, radius: f32, line_width: f32, color: [f32; 3]) -> Self {
|
||||
let mut vertices = Vec::new();
|
||||
let normal = [0.0, 0.0, 1.0];
|
||||
let z = 0.001;
|
||||
let hw = line_width / 2.0;
|
||||
let segments = 64u32;
|
||||
|
||||
// Concentric rings
|
||||
for ring in 1..=num_rings {
|
||||
let r = radius * (ring as f32 / (num_rings + 1) as f32);
|
||||
let inner = r - hw;
|
||||
let outer = r + hw;
|
||||
|
||||
for i in 0..segments {
|
||||
let a0 = (i as f32 / segments as f32) * std::f32::consts::TAU;
|
||||
let a1 = ((i + 1) as f32 / segments as f32) * std::f32::consts::TAU;
|
||||
|
||||
let p0_in = [inner * a0.cos(), inner * a0.sin(), z];
|
||||
let p1_in = [inner * a1.cos(), inner * a1.sin(), z];
|
||||
let p0_out = [outer * a0.cos(), outer * a0.sin(), z];
|
||||
let p1_out = [outer * a1.cos(), outer * a1.sin(), z];
|
||||
|
||||
vertices.push(Vertex { position: p0_in, normal, color });
|
||||
vertices.push(Vertex { position: p0_out, normal, color });
|
||||
vertices.push(Vertex { position: p1_out, normal, color });
|
||||
|
||||
vertices.push(Vertex { position: p0_in, normal, color });
|
||||
vertices.push(Vertex { position: p1_out, normal, color });
|
||||
vertices.push(Vertex { position: p1_in, normal, color });
|
||||
}
|
||||
}
|
||||
|
||||
// Cross lines (flat quads through center)
|
||||
// X-axis line
|
||||
vertices.push(Vertex { position: [-radius, -hw, z], normal, color });
|
||||
vertices.push(Vertex { position: [radius, -hw, z], normal, color });
|
||||
vertices.push(Vertex { position: [radius, hw, z], normal, color });
|
||||
vertices.push(Vertex { position: [-radius, -hw, z], normal, color });
|
||||
vertices.push(Vertex { position: [radius, hw, z], normal, color });
|
||||
vertices.push(Vertex { position: [-radius, hw, z], normal, color });
|
||||
|
||||
// Y-axis line
|
||||
vertices.push(Vertex { position: [-hw, -radius, z], normal, color });
|
||||
vertices.push(Vertex { position: [hw, -radius, z], normal, color });
|
||||
vertices.push(Vertex { position: [hw, radius, z], normal, color });
|
||||
vertices.push(Vertex { position: [-hw, -radius, z], normal, color });
|
||||
vertices.push(Vertex { position: [hw, radius, z], normal, color });
|
||||
vertices.push(Vertex { position: [-hw, radius, z], normal, color });
|
||||
|
||||
Self { vertices }
|
||||
}
|
||||
|
||||
/// Vertical box from z=0 to z=height, centered on XY at origin.
|
||||
pub fn thin_box(half_width: f32, height: f32, color: [f32; 3]) -> Self {
|
||||
let mut vertices = Vec::new();
|
||||
push_box(
|
||||
&mut vertices,
|
||||
Vec3::new(-half_width, -half_width, 0.0),
|
||||
Vec3::new(half_width, half_width, height),
|
||||
color,
|
||||
);
|
||||
Self { vertices }
|
||||
}
|
||||
|
||||
/// Compound axis label glyph: sign (+/-) on left, letter (X/Y) on right.
|
||||
pub fn axis_label(positive: bool, is_x: bool, size: f32, thickness: f32, color: [f32; 3]) -> Self {
|
||||
let mut segments: Vec<([f32; 2], [f32; 2])> = Vec::new();
|
||||
let s = size;
|
||||
|
||||
// Sign (left side, centered at x = -1.5*s)
|
||||
segments.push(([-2.2 * s, 0.0], [-0.8 * s, 0.0])); // horizontal bar
|
||||
if positive {
|
||||
segments.push(([-1.5 * s, -0.7 * s], [-1.5 * s, 0.7 * s])); // vertical bar
|
||||
}
|
||||
|
||||
// Letter (right side, centered at x = 0.5*s)
|
||||
if is_x {
|
||||
segments.push(([-0.5 * s, -s], [1.5 * s, s]));
|
||||
segments.push(([-0.5 * s, s], [1.5 * s, -s]));
|
||||
} else {
|
||||
segments.push(([0.5 * s, 0.0], [-0.5 * s, s]));
|
||||
segments.push(([0.5 * s, 0.0], [1.5 * s, s]));
|
||||
segments.push(([0.5 * s, 0.0], [0.5 * s, -s]));
|
||||
}
|
||||
|
||||
line_segments_mesh(&segments, thickness, 0.0, color)
|
||||
}
|
||||
}
|
||||
|
||||
+265
-2
@@ -3,8 +3,11 @@ use std::sync::Arc;
|
||||
use wgpu::util::DeviceExt;
|
||||
use wgpu::ExperimentalFeatures;
|
||||
|
||||
use glam::{Mat4, Vec3};
|
||||
|
||||
use crate::camera::{Camera, CameraUniform};
|
||||
use crate::mesh::{Mesh, Vertex};
|
||||
use crate::scanner::ScannerBlip;
|
||||
use crate::scene::{ModelUniform, SceneObject};
|
||||
|
||||
/// GPU-side mesh: vertex buffer + vertex count
|
||||
@@ -29,6 +32,9 @@ pub struct Renderer {
|
||||
pub model_bind_group: wgpu::BindGroup,
|
||||
pub model_uniform_alignment: u64,
|
||||
pub gpu_meshes: Vec<GpuMesh>,
|
||||
pub scanner_camera_buffer: wgpu::Buffer,
|
||||
pub scanner_camera_bind_group: wgpu::BindGroup,
|
||||
pub scanner_mesh_start: usize,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
@@ -136,6 +142,32 @@ impl Renderer {
|
||||
}],
|
||||
});
|
||||
|
||||
// --- Scanner camera (orthographic, tilted top-down view of the scanner disc) ---
|
||||
let scanner_eye = Vec3::new(0.0, -0.8, 1.5);
|
||||
let scanner_view = Mat4::look_at_rh(scanner_eye, Vec3::ZERO, Vec3::Z);
|
||||
let scanner_proj = Mat4::orthographic_rh(-1.4, 1.4, -1.0, 1.8, 0.1, 5.0);
|
||||
let scanner_cam_uniform = CameraUniform {
|
||||
view_proj: (scanner_proj * scanner_view).to_cols_array_2d(),
|
||||
eye_pos: scanner_eye.into(),
|
||||
_padding: 0.0,
|
||||
};
|
||||
|
||||
let scanner_camera_buffer =
|
||||
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("scanner_camera_buf"),
|
||||
contents: bytemuck::bytes_of(&scanner_cam_uniform),
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
});
|
||||
|
||||
let scanner_camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("scanner_camera_bg"),
|
||||
layout: &camera_bind_group_layout,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: scanner_camera_buffer.as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
|
||||
// --- Model uniform (dynamic offsets for per-object transforms) ---
|
||||
let model_uniform_alignment =
|
||||
device.limits().min_uniform_buffer_offset_alignment as u64;
|
||||
@@ -224,7 +256,7 @@ impl Renderer {
|
||||
});
|
||||
|
||||
// --- Upload meshes ---
|
||||
let gpu_meshes = meshes
|
||||
let mut gpu_meshes: Vec<GpuMesh> = meshes
|
||||
.iter()
|
||||
.map(|m| {
|
||||
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
@@ -239,6 +271,32 @@ impl Renderer {
|
||||
})
|
||||
.collect();
|
||||
|
||||
// --- Scanner meshes ---
|
||||
let scanner_mesh_start = gpu_meshes.len();
|
||||
let scanner_meshes = [
|
||||
Mesh::disc(64, 1.0, [0.02, 0.05, 0.02]), // 0: scanner disc
|
||||
Mesh::scanner_grid(3, 1.0, 0.008, [0.0, 0.25, 0.0]), // 1: grid
|
||||
Mesh::ring(64, 0.97, 1.0, 0.002, [0.0, 0.5, 0.0]), // 2: border ring
|
||||
Mesh::octahedron([0.0, 1.0, 0.0]), // 3: blip dot
|
||||
Mesh::thin_box(0.015, 1.0, [0.0, 0.5, 0.0]), // 4: blip stalk
|
||||
Mesh::disc(32, 1.0, [0.0, 0.0, 0.0]), // 5: black panel bg
|
||||
Mesh::axis_label(true, true, 1.0, 0.25, [0.7, 0.2, 0.2]), // 6: +X label
|
||||
Mesh::axis_label(false, true, 1.0, 0.25, [0.7, 0.2, 0.2]), // 7: -X label
|
||||
Mesh::axis_label(true, false, 1.0, 0.25, [0.2, 0.7, 0.2]), // 8: +Y label
|
||||
Mesh::axis_label(false, false, 1.0, 0.25, [0.2, 0.7, 0.2]), // 9: -Y label
|
||||
];
|
||||
for m in &scanner_meshes {
|
||||
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("scanner_mesh_vb"),
|
||||
contents: bytemuck::cast_slice(&m.vertices),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
gpu_meshes.push(GpuMesh {
|
||||
vertex_buffer,
|
||||
vertex_count: m.vertices.len() as u32,
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
surface,
|
||||
device,
|
||||
@@ -250,11 +308,19 @@ impl Renderer {
|
||||
camera_bind_group,
|
||||
model_buffer,
|
||||
model_bind_group,
|
||||
model_uniform_alignment: model_uniform_alignment,
|
||||
model_uniform_alignment,
|
||||
gpu_meshes,
|
||||
scanner_camera_buffer,
|
||||
scanner_camera_bind_group,
|
||||
scanner_mesh_start,
|
||||
}
|
||||
}
|
||||
|
||||
/// Height in pixels of the scanner strip at the bottom of the screen.
|
||||
pub fn scanner_strip_height(&self) -> f32 {
|
||||
(self.config.height as f32 * 0.25).max(180.0)
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: u32, height: u32) {
|
||||
let max_dim = self.device.limits().max_texture_dimension_2d;
|
||||
if width > 0 && height > 0 {
|
||||
@@ -270,6 +336,7 @@ impl Renderer {
|
||||
camera: &Camera,
|
||||
objects: &[SceneObject],
|
||||
time: f32,
|
||||
scanner_blips: &[ScannerBlip],
|
||||
) -> Result<(), wgpu::SurfaceError> {
|
||||
// Update camera uniform
|
||||
let cam_uniform = CameraUniform::from_camera(camera);
|
||||
@@ -329,7 +396,11 @@ impl Renderer {
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let strip_h = self.scanner_strip_height();
|
||||
let game_h = self.config.height as f32 - strip_h;
|
||||
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
pass.set_viewport(0.0, 0.0, self.config.width as f32, game_h, 0.0, 1.0);
|
||||
pass.set_bind_group(0, &self.camera_bind_group, &[]);
|
||||
|
||||
for (i, obj) in objects.iter().enumerate() {
|
||||
@@ -342,6 +413,198 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Scanner pass ---
|
||||
{
|
||||
let scene_count = objects.len();
|
||||
// Slots: 0=panel, 1-3=disc/grid/border, 4-7=labels(+X,-X,+Y,-Y), 8+2*bi=dot, 8+2*bi+1=stalk
|
||||
let max_blips = ((MAX_OBJECTS - scene_count - 8) / 2).min(scanner_blips.len());
|
||||
|
||||
// Panel background: large black disc behind everything
|
||||
{
|
||||
let slot = scene_count;
|
||||
let panel_transform = Mat4::from_translation(Vec3::new(0.0, 0.0, -0.5))
|
||||
* Mat4::from_scale(Vec3::splat(5.0));
|
||||
let uniform = ModelUniform {
|
||||
transform: panel_transform.to_cols_array_2d(),
|
||||
};
|
||||
self.queue.write_buffer(
|
||||
&self.model_buffer,
|
||||
slot as u64 * slot_size,
|
||||
bytemuck::bytes_of(&uniform),
|
||||
);
|
||||
}
|
||||
|
||||
// Write scanner fixed geometry transforms (identity)
|
||||
for i in 0..3 {
|
||||
let slot = scene_count + 1 + i;
|
||||
let uniform = ModelUniform {
|
||||
transform: Mat4::IDENTITY.to_cols_array_2d(),
|
||||
};
|
||||
let offset = slot as u64 * slot_size;
|
||||
self.queue
|
||||
.write_buffer(&self.model_buffer, offset, bytemuck::bytes_of(&uniform));
|
||||
}
|
||||
|
||||
// Axis label transforms — rotate with camera yaw to show world axis directions
|
||||
// After the 90° CCW rotation in scanner.rs, world directions map to scanner disc as:
|
||||
// +X -> ( sin(yaw), cos(yaw))
|
||||
// -X -> (-sin(yaw), -cos(yaw))
|
||||
// +Y -> (-cos(yaw), sin(yaw))
|
||||
// -Y -> ( cos(yaw), -sin(yaw))
|
||||
let yaw = camera.yaw;
|
||||
let label_radius = 1.12;
|
||||
let label_scale = 0.09;
|
||||
let label_positions = [
|
||||
Vec3::new( yaw.sin() * label_radius, yaw.cos() * label_radius, 0.003), // +X
|
||||
Vec3::new(-yaw.sin() * label_radius, -yaw.cos() * label_radius, 0.003), // -X
|
||||
Vec3::new(-yaw.cos() * label_radius, yaw.sin() * label_radius, 0.003), // +Y
|
||||
Vec3::new( yaw.cos() * label_radius, -yaw.sin() * label_radius, 0.003), // -Y
|
||||
];
|
||||
for (i, pos) in label_positions.iter().enumerate() {
|
||||
let transform = Mat4::from_translation(*pos)
|
||||
* Mat4::from_scale(Vec3::splat(label_scale));
|
||||
let uniform = ModelUniform {
|
||||
transform: transform.to_cols_array_2d(),
|
||||
};
|
||||
self.queue.write_buffer(
|
||||
&self.model_buffer,
|
||||
(scene_count + 4 + i) as u64 * slot_size,
|
||||
bytemuck::bytes_of(&uniform),
|
||||
);
|
||||
}
|
||||
|
||||
// Write blip transforms
|
||||
for (bi, blip) in scanner_blips[..max_blips].iter().enumerate() {
|
||||
// Dot: small octahedron at blip position + altitude
|
||||
let dot_slot = scene_count + 8 + bi * 2;
|
||||
let dot_transform = Mat4::from_translation(Vec3::new(
|
||||
blip.disc_pos.x,
|
||||
blip.disc_pos.y,
|
||||
blip.altitude,
|
||||
)) * Mat4::from_scale(Vec3::splat(0.06));
|
||||
let uniform = ModelUniform {
|
||||
transform: dot_transform.to_cols_array_2d(),
|
||||
};
|
||||
self.queue.write_buffer(
|
||||
&self.model_buffer,
|
||||
dot_slot as u64 * slot_size,
|
||||
bytemuck::bytes_of(&uniform),
|
||||
);
|
||||
|
||||
// Stalk: thin box from disc surface to altitude
|
||||
let stalk_slot = scene_count + 8 + bi * 2 + 1;
|
||||
let (stalk_z, stalk_h) = if blip.altitude >= 0.0 {
|
||||
(0.0_f32, blip.altitude.max(0.001))
|
||||
} else {
|
||||
(blip.altitude, (-blip.altitude).max(0.001))
|
||||
};
|
||||
let stalk_transform = Mat4::from_translation(Vec3::new(
|
||||
blip.disc_pos.x,
|
||||
blip.disc_pos.y,
|
||||
stalk_z,
|
||||
)) * Mat4::from_scale(Vec3::new(1.0, 1.0, stalk_h));
|
||||
let uniform = ModelUniform {
|
||||
transform: stalk_transform.to_cols_array_2d(),
|
||||
};
|
||||
self.queue.write_buffer(
|
||||
&self.model_buffer,
|
||||
stalk_slot as u64 * slot_size,
|
||||
bytemuck::bytes_of(&uniform),
|
||||
);
|
||||
}
|
||||
|
||||
// Second render pass: scanner strip
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("scanner_pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
})],
|
||||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||
view: &self.depth_view,
|
||||
depth_ops: Some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(1.0),
|
||||
store: wgpu::StoreOp::Store,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let strip_h = self.scanner_strip_height();
|
||||
let game_h = self.config.height as f32 - strip_h;
|
||||
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
pass.set_bind_group(0, &self.scanner_camera_bind_group, &[]);
|
||||
|
||||
// Draw black panel background across the full strip
|
||||
pass.set_viewport(0.0, game_h, self.config.width as f32, strip_h, 0.0, 1.0);
|
||||
{
|
||||
let dynamic_offset = (scene_count as u64 * slot_size) as u32;
|
||||
pass.set_bind_group(1, &self.model_bind_group, &[dynamic_offset]);
|
||||
let panel_mesh = &self.gpu_meshes[self.scanner_mesh_start + 5];
|
||||
pass.set_vertex_buffer(0, panel_mesh.vertex_buffer.slice(..));
|
||||
pass.draw(0..panel_mesh.vertex_count, 0..1);
|
||||
}
|
||||
|
||||
// Switch to centered scanner viewport
|
||||
let scanner_size = (strip_h - 16.0).min(self.config.width as f32 * 0.4);
|
||||
let vp_x = (self.config.width as f32 - scanner_size) / 2.0;
|
||||
let vp_y = game_h + (strip_h - scanner_size) / 2.0;
|
||||
pass.set_viewport(vp_x, vp_y, scanner_size, scanner_size, 0.0, 1.0);
|
||||
|
||||
// Fixed geometry: disc, grid, border ring
|
||||
for i in 0..3 {
|
||||
let slot = scene_count + 1 + i;
|
||||
let dynamic_offset = (slot as u64 * slot_size) as u32;
|
||||
pass.set_bind_group(1, &self.model_bind_group, &[dynamic_offset]);
|
||||
let mesh = &self.gpu_meshes[self.scanner_mesh_start + i];
|
||||
pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
|
||||
pass.draw(0..mesh.vertex_count, 0..1);
|
||||
}
|
||||
|
||||
// Axis labels (+X, -X, +Y, -Y positioned at disc edge)
|
||||
for i in 0..4 {
|
||||
let slot = scene_count + 4 + i;
|
||||
let dynamic_offset = (slot as u64 * slot_size) as u32;
|
||||
pass.set_bind_group(1, &self.model_bind_group, &[dynamic_offset]);
|
||||
let mesh = &self.gpu_meshes[self.scanner_mesh_start + 6 + i];
|
||||
pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
|
||||
pass.draw(0..mesh.vertex_count, 0..1);
|
||||
}
|
||||
|
||||
// Blip dots and stalks
|
||||
let dot_mesh = &self.gpu_meshes[self.scanner_mesh_start + 3];
|
||||
let stalk_mesh = &self.gpu_meshes[self.scanner_mesh_start + 4];
|
||||
|
||||
for bi in 0..max_blips {
|
||||
// Dot
|
||||
let dot_slot = scene_count + 8 + bi * 2;
|
||||
pass.set_bind_group(
|
||||
1,
|
||||
&self.model_bind_group,
|
||||
&[(dot_slot as u64 * slot_size) as u32],
|
||||
);
|
||||
pass.set_vertex_buffer(0, dot_mesh.vertex_buffer.slice(..));
|
||||
pass.draw(0..dot_mesh.vertex_count, 0..1);
|
||||
|
||||
// Stalk
|
||||
let stalk_slot = scene_count + 8 + bi * 2 + 1;
|
||||
pass.set_bind_group(
|
||||
1,
|
||||
&self.model_bind_group,
|
||||
&[(stalk_slot as u64 * slot_size) as u32],
|
||||
);
|
||||
pass.set_vertex_buffer(0, stalk_mesh.vertex_buffer.slice(..));
|
||||
pass.draw(0..stalk_mesh.vertex_count, 0..1);
|
||||
}
|
||||
}
|
||||
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
output.present();
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
use glam::{Vec2, Vec3};
|
||||
|
||||
use crate::scene::SceneObject;
|
||||
|
||||
pub struct Scanner {
|
||||
pub range: f32,
|
||||
}
|
||||
|
||||
pub struct ScannerBlip {
|
||||
pub disc_pos: Vec2,
|
||||
pub altitude: f32,
|
||||
pub color: [f32; 3],
|
||||
}
|
||||
|
||||
impl Scanner {
|
||||
pub fn new(range: f32) -> Self {
|
||||
Self { range }
|
||||
}
|
||||
|
||||
pub fn compute_blips(
|
||||
&self,
|
||||
camera_pos: Vec3,
|
||||
camera_yaw: f32,
|
||||
objects: &[SceneObject],
|
||||
) -> Vec<ScannerBlip> {
|
||||
let mut blips = Vec::new();
|
||||
let cos_yaw = (-camera_yaw).cos();
|
||||
let sin_yaw = (-camera_yaw).sin();
|
||||
let disc_scale = 1.0 / self.range;
|
||||
|
||||
for obj in objects {
|
||||
let rel = obj.position - camera_pos;
|
||||
let horiz_dist = (rel.x * rel.x + rel.y * rel.y).sqrt();
|
||||
|
||||
if horiz_dist > self.range {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Rotate by negative yaw so scanner is view-relative,
|
||||
// then apply 90° CCW so forward maps to scanner +Y (top of disc)
|
||||
let rotated_x = rel.x * cos_yaw - rel.y * sin_yaw;
|
||||
let rotated_y = rel.x * sin_yaw + rel.y * cos_yaw;
|
||||
let rx = -rotated_y;
|
||||
let ry = rotated_x;
|
||||
|
||||
let disc_x = rx * disc_scale;
|
||||
let disc_y = ry * disc_scale;
|
||||
|
||||
// Skip if outside the disc
|
||||
if disc_x * disc_x + disc_y * disc_y > 1.0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let altitude = (rel.z * disc_scale).clamp(-0.6, 0.6);
|
||||
|
||||
blips.push(ScannerBlip {
|
||||
disc_pos: Vec2::new(disc_x, disc_y),
|
||||
altitude,
|
||||
color: obj.color,
|
||||
});
|
||||
}
|
||||
|
||||
blips
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ pub struct SceneObject {
|
||||
pub rotation_speed: f32,
|
||||
pub scale: f32,
|
||||
pub base_rotation: f32,
|
||||
pub color: [f32; 3],
|
||||
}
|
||||
|
||||
impl SceneObject {
|
||||
@@ -72,6 +73,7 @@ pub fn build_scene() -> (Vec<Mesh>, Vec<SceneObject>) {
|
||||
rotation_speed: 0.0,
|
||||
scale: 1.0,
|
||||
base_rotation: 0.0,
|
||||
color: [0.5, 0.5, 0.5],
|
||||
});
|
||||
|
||||
// Rotating spiked ball at origin
|
||||
@@ -84,6 +86,7 @@ pub fn build_scene() -> (Vec<Mesh>, Vec<SceneObject>) {
|
||||
rotation_speed: 0.5,
|
||||
scale: 0.8,
|
||||
base_rotation: 0.0,
|
||||
color: [0.3, 0.5, 1.0],
|
||||
});
|
||||
|
||||
let num_objects = 30;
|
||||
@@ -115,6 +118,7 @@ pub fn build_scene() -> (Vec<Mesh>, Vec<SceneObject>) {
|
||||
rotation_speed,
|
||||
scale,
|
||||
base_rotation,
|
||||
color: COLORS[mesh_index],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user