use bytemuck::{Pod, Zeroable};
use glam::{Mat4, Vec3};
use crate::color::{Color, ColorSpace};
use crate::paint::rgba_f32_in;
use crate::scene::data::{LineDraw, MeshDraw, PointDraw, Scene3DData};
use crate::scene::geometry::{LineData, MeshData, PointData};
use crate::scene::style::{GridPlanes, LinePattern, Material, PointShape, SceneStyle, SizeMode};
const MAX_GRID_LINES: i32 = 256;
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
pub struct PointUniform {
mvp: [[f32; 4]; 4],
screen_size_px: [f32; 2],
point_size_px: f32,
size_mode: u32,
shape: u32,
_pad: [u32; 3],
}
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
pub struct LineUniform {
mvp: [[f32; 4]; 4],
screen_size: [f32; 2],
width_mode: u32,
default_width: f32,
dash_length: f32,
gap_length: f32,
_pad: [f32; 2],
}
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
pub struct MeshUniform {
view_proj: [[f32; 4]; 4],
model: [[f32; 4]; 4],
base_color: [f32; 4], light_dir: [f32; 4], key_color: [f32; 4], sky_color: [f32; 4], ground_color: [f32; 4], eye_pos: [f32; 4], }
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
pub struct PointInstance {
position: [f32; 3],
color: [f32; 4],
}
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
pub struct LineInstance {
start: [f32; 3],
end: [f32; 3],
color: [f32; 4],
width: f32,
}
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
pub struct MeshVertexGpu {
position: [f32; 3],
normal: [f32; 3],
}
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
pub struct CompositeInstance {
rect: [f32; 4],
matrix: [f32; 4],
translation: [f32; 2],
}
impl CompositeInstance {
pub fn new(rect: [f32; 4], matrix: [f32; 4], translation: [f32; 2]) -> Self {
Self {
rect,
matrix,
translation,
}
}
}
pub fn to_linear(srgba: [f32; 4], working: ColorSpace) -> [f32; 4] {
rgba_f32_in(
Color::in_space(ColorSpace::SRGB, srgba[0], srgba[1], srgba[2], srgba[3]),
working,
)
}
pub fn size_mode_code(mode: SizeMode) -> u32 {
match mode {
SizeMode::ScreenSpace => 0,
SizeMode::World => 1,
}
}
pub fn point_uniform(mvp: Mat4, screen: [f32; 2], draw: &PointDraw) -> PointUniform {
PointUniform {
mvp: mvp.to_cols_array_2d(),
screen_size_px: screen,
point_size_px: draw.style.size,
size_mode: size_mode_code(draw.style.size_mode),
shape: match draw.style.shape {
PointShape::Circle => 0,
PointShape::Square => 1,
},
_pad: [0; 3],
}
}
pub fn line_uniform(mvp: Mat4, screen: [f32; 2], draw: &LineDraw) -> LineUniform {
let (dash, gap) = match draw.style.pattern {
LinePattern::Solid => (0.0, 0.0),
LinePattern::Dashed => (8.0, 6.0),
};
LineUniform {
mvp: mvp.to_cols_array_2d(),
screen_size: screen,
width_mode: size_mode_code(draw.style.size_mode),
default_width: draw.style.width,
dash_length: dash,
gap_length: gap,
_pad: [0.0; 2],
}
}
pub fn grid_uniform(view_proj: Mat4, screen: [f32; 2]) -> LineUniform {
LineUniform {
mvp: view_proj.to_cols_array_2d(),
screen_size: screen,
width_mode: 0, default_width: 1.0,
dash_length: 0.0,
gap_length: 0.0,
_pad: [0.0; 2],
}
}
pub fn mesh_uniform(
view_proj: Mat4,
draw: &MeshDraw,
scene: &Scene3DData,
working: ColorSpace,
) -> MeshUniform {
let light = &scene.lights;
let dir = light.key_direction.normalize_or_zero();
let eye = scene.camera.eye;
let (base, specular, shininess, unlit) = match &draw.material {
Material::Matte { base } => (*base, 0.0, 1.0, false),
Material::Glossy {
base,
specular,
shininess,
} => (*base, *specular, shininess.max(1.0), false),
Material::Flat { color } => (*color, 0.0, 1.0, true),
Material::Custom { .. } => (Color::srgb_u8(214, 220, 230), 0.0, 1.0, false),
};
let white = Color::srgb_u8(255, 255, 255);
let key_intensity = if unlit { 0.0 } else { light.key_intensity };
let ambient_scale = if unlit { 1.0 } else { light.ambient };
let sky = rgba_f32_in(if unlit { white } else { light.sky_color }, working);
let ground = rgba_f32_in(if unlit { white } else { light.ground_color }, working);
let key = rgba_f32_in(light.key_color, working);
let base = rgba_f32_in(base, working);
MeshUniform {
view_proj: view_proj.to_cols_array_2d(),
model: draw.transform.to_cols_array_2d(),
base_color: base,
light_dir: [dir.x, dir.y, dir.z, key_intensity],
key_color: [key[0], key[1], key[2], specular],
sky_color: [sky[0], sky[1], sky[2], shininess],
ground_color: [ground[0], ground[1], ground[2], ambient_scale],
eye_pos: [eye.x, eye.y, eye.z, 0.0],
}
}
pub fn mesh_vertices(data: &MeshData) -> Vec<MeshVertexGpu> {
data.vertices
.iter()
.map(|v| MeshVertexGpu {
position: v.position.to_array(),
normal: v.normal.to_array(),
})
.collect()
}
pub fn point_instances(data: &PointData, working: ColorSpace) -> Vec<PointInstance> {
data.points
.iter()
.map(|p| PointInstance {
position: p.position.to_array(),
color: to_linear(p.color, working),
})
.collect()
}
pub fn line_instances(data: &LineData, working: ColorSpace) -> Vec<LineInstance> {
data.segments
.iter()
.map(|s| LineInstance {
start: s.start.to_array(),
end: s.end.to_array(),
color: to_linear(s.color, working),
width: 0.0,
})
.collect()
}
pub fn build_grid_lines(style: &SceneStyle, working: ColorSpace, out: &mut Vec<LineInstance>) {
let g = &style.grid;
let spans = g.axis_spans();
let extent = g.extent.max(0.0);
if extent > 0.0 && g.planes != GridPlanes::NONE {
let grid_color = rgba_f32_in(g.color, working);
let step = (g.spacing / g.subdivisions.max(1) as f32).max(1e-4);
if g.planes.xz {
plane_grid(out, Vec3::X, Vec3::Z, spans[0], spans[2], step, grid_color);
}
if g.planes.xy {
plane_grid(out, Vec3::X, Vec3::Y, spans[0], spans[1], step, grid_color);
}
if g.planes.yz {
plane_grid(out, Vec3::Y, Vec3::Z, spans[1], spans[2], step, grid_color);
}
}
if style.show_axes {
let fallback = extent.max(g.spacing).max(1.0);
for (i, dir, rgb) in [
(0usize, Vec3::X, Color::srgb_u8(206, 86, 86)),
(1, Vec3::Y, Color::srgb_u8(120, 190, 110)),
(2, Vec3::Z, Color::srgb_u8(110, 150, 225)),
] {
let (lo, hi) = match g.bounds.axis(i) {
Some((a, b)) => (a.min(b), a.max(b)),
None => (-fallback, fallback),
};
push_seg(out, dir * lo, dir * hi, rgba_f32_in(rgb, working), 1.6);
}
}
}
fn plane_grid(
out: &mut Vec<LineInstance>,
u: Vec3,
v: Vec3,
u_span: (f32, f32),
v_span: (f32, f32),
step: f32,
color: [f32; 4],
) {
for off in grid_offsets(v_span, step) {
push_seg(
out,
u * u_span.0 + v * off,
u * u_span.1 + v * off,
color,
0.0,
);
}
for off in grid_offsets(u_span, step) {
push_seg(
out,
v * v_span.0 + u * off,
v * v_span.1 + u * off,
color,
0.0,
);
}
}
fn grid_offsets(span: (f32, f32), step: f32) -> Vec<f32> {
let (lo, hi) = (span.0.min(span.1), span.0.max(span.1));
let k0 = (lo / step).ceil() as i32;
let k1 = (hi / step).floor() as i32;
let mut offs = Vec::new();
for k in k0..=k1 {
offs.push(k as f32 * step);
if offs.len() >= 2 * MAX_GRID_LINES as usize {
break;
}
}
offs
}
fn push_seg(out: &mut Vec<LineInstance>, a: Vec3, b: Vec3, color: [f32; 4], width: f32) {
out.push(LineInstance {
start: a.to_array(),
end: b.to_array(),
color,
width,
});
}
#[cfg(test)]
mod tests {
use super::*;
use std::mem::size_of;
#[test]
fn per_axis_bounds_clip_grid_and_axis_lines() {
use crate::scene::style::{AxisBounds, GridPlanes, GridSettings, SceneStyle};
let style = SceneStyle {
grid: GridSettings {
planes: GridPlanes::XZ,
extent: 10.0,
spacing: 1.0,
bounds: AxisBounds {
y: Some((0.0, 100.0)),
..Default::default()
},
..GridSettings::default()
},
show_axes: true,
..SceneStyle::default()
};
let mut lines = Vec::new();
build_grid_lines(&style, ColorSpace::SRGB, &mut lines);
let axis_span = |pick: fn(&LineInstance) -> bool, comp: usize| {
let l = lines
.iter()
.find(|l| l.width > 0.0 && pick(l))
.expect("axis line");
(
l.start[comp].min(l.end[comp]),
l.start[comp].max(l.end[comp]),
)
};
let y = axis_span(|l| l.start[0] == 0.0 && l.start[2] == 0.0, 1);
assert_eq!(y, (0.0, 100.0));
let x = axis_span(|l| l.start[1] == 0.0 && l.start[2] == 0.0, 0);
assert_eq!(x, (-10.0, 10.0));
for l in lines.iter().filter(|l| l.width == 0.0) {
for v in [l.start, l.end] {
assert!(v[0].abs() <= 10.0 + 1e-3 && v[2].abs() <= 10.0 + 1e-3);
}
}
}
#[test]
fn gpu_struct_sizes_are_stable() {
assert_eq!(size_of::<PointUniform>(), 96);
assert_eq!(size_of::<LineUniform>(), 96);
assert_eq!(size_of::<MeshUniform>(), 224);
assert_eq!(size_of::<PointInstance>(), 28);
assert_eq!(size_of::<LineInstance>(), 44);
assert_eq!(size_of::<MeshVertexGpu>(), 24);
assert_eq!(size_of::<CompositeInstance>(), 40);
}
}