use image::RgbaImage;
use std::collections::{BTreeMap, HashSet};
use std::ops::{Add, Div, Mul, Neg, Sub};
use std::sync::Arc;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[must_use]
pub struct Vec3 {
pub x: f64,
pub y: f64,
pub z: f64,
}
impl Vec3 {
pub const fn new(x: f64, y: f64, z: f64) -> Self {
Self { x, y, z }
}
#[must_use]
pub fn dot(self, b: Self) -> f64 {
self.x * b.x + self.y * b.y + self.z * b.z
}
pub fn cross(self, b: Self) -> Self {
Self::new(
self.y * b.z - self.z * b.y,
self.z * b.x - self.x * b.z,
self.x * b.y - self.y * b.x,
)
}
#[must_use]
pub fn length(self) -> f64 {
self.x.hypot(self.y).hypot(self.z)
}
pub fn normalized(self) -> Self {
let length = self.length();
if length > 0.0 && length.is_finite() {
self / length
} else {
Self::default()
}
}
#[must_use]
pub fn is_finite(self) -> bool {
self.x.is_finite() && self.y.is_finite() && self.z.is_finite()
}
}
impl Add for Vec3 {
type Output = Self;
fn add(self, b: Self) -> Self {
Self::new(self.x + b.x, self.y + b.y, self.z + b.z)
}
}
impl Sub for Vec3 {
type Output = Self;
fn sub(self, b: Self) -> Self {
Self::new(self.x - b.x, self.y - b.y, self.z - b.z)
}
}
impl Mul<f64> for Vec3 {
type Output = Self;
fn mul(self, s: f64) -> Self {
Self::new(self.x * s, self.y * s, self.z * s)
}
}
impl Div<f64> for Vec3 {
type Output = Self;
fn div(self, s: f64) -> Self {
Self::new(self.x / s, self.y / s, self.z / s)
}
}
impl Neg for Vec3 {
type Output = Self;
fn neg(self) -> Self {
self * -1.0
}
}
impl From<[f32; 3]> for Vec3 {
fn from(v: [f32; 3]) -> Self {
Self::new(f64::from(v[0]), f64::from(v[1]), f64::from(v[2]))
}
}
#[derive(Clone, Debug)]
pub struct Triangle {
pub vertices: [Vec3; 3],
pub color: Option<[u8; 3]>,
}
#[derive(Clone, Debug)]
pub struct Point {
pub position: Vec3,
pub color: Option<[u8; 3]>,
}
#[derive(Clone, Debug)]
pub struct Line {
pub vertices: [Vec3; 2],
pub color: Option<[u8; 3]>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TextureWrap {
#[default]
Repeat,
ClampToEdge,
MirroredRepeat,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TextureFilter {
Nearest,
#[default]
Linear,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TextureInterpolationSpace {
#[default]
Linear,
Srgb,
}
#[derive(Clone, Debug)]
pub struct Texture {
pub image: Arc<RgbaImage>,
pub wrap_s: TextureWrap,
pub wrap_t: TextureWrap,
pub filter: TextureFilter,
pub min_filter: TextureFilter,
pub interpolation_space: TextureInterpolationSpace,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum AlphaMode {
#[default]
Opaque,
Mask {
cutoff: f64,
},
Blend,
}
#[derive(Clone, Debug)]
pub struct TriangleAppearance {
pub base_color: [f64; 4],
pub colors: Option<[[f64; 4]; 3]>,
pub texcoords: Option<[[f64; 2]; 3]>,
pub normals: Option<[Vec3; 3]>,
pub texture: Option<Texture>,
pub alpha_mode: AlphaMode,
pub unlit: bool,
pub double_sided: bool,
}
impl Default for TriangleAppearance {
fn default() -> Self {
Self {
base_color: [1.0; 4],
colors: None,
texcoords: None,
normals: None,
texture: None,
alpha_mode: AlphaMode::Opaque,
unlit: false,
double_sided: true,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct Scene {
pub triangles: Vec<Triangle>,
pub points: Vec<Point>,
pub lines: Vec<Line>,
pub appearances: BTreeMap<usize, TriangleAppearance>,
}
impl Scene {
pub fn validate(&self) -> crate::Result<()> {
if self.triangles.is_empty() && self.points.is_empty() && self.lines.is_empty() {
return Err("model contains no renderable geometry".into());
}
if self.positions().any(|p| !p.is_finite()) {
return Err("model contains non-finite coordinates".into());
}
let mut textures = HashSet::new();
let mut texture_pixels = 0_u64;
for (index, appearance) in &self.appearances {
if *index >= self.triangles.len() {
return Err("triangle appearance refers to a missing triangle".into());
}
let valid_color = |color: &[f64; 4]| {
color
.iter()
.all(|c| c.is_finite() && (0.0..=1.0).contains(c))
};
if !valid_color(&appearance.base_color)
|| appearance
.colors
.is_some_and(|colors| colors.iter().any(|c| !valid_color(c)))
{
return Err(
"triangle appearance colors must be finite values between zero and one".into(),
);
}
if appearance
.texcoords
.is_some_and(|uvs| uvs.iter().flatten().any(|v| !v.is_finite()))
{
return Err("triangle texture coordinates must be finite".into());
}
if appearance.normals.is_some_and(|normals| {
normals
.iter()
.any(|n| !n.is_finite() || !n.length().is_finite() || n.length() == 0.0)
}) {
return Err("triangle appearance normals must be finite nonzero directions".into());
}
if let AlphaMode::Mask { cutoff } = appearance.alpha_mode
&& (!cutoff.is_finite() || cutoff < 0.0)
{
return Err("triangle alpha cutoff must be finite and nonnegative".into());
}
if let Some(texture) = &appearance.texture {
if appearance.texcoords.is_none() {
return Err("triangle color texture requires texture coordinates".into());
}
let width = texture.image.width();
let height = texture.image.height();
let pixels = u64::from(width) * u64::from(height);
if width == 0
|| height == 0
|| width > 8192
|| height > 8192
|| pixels > 16 * 1024 * 1024
{
return Err(
"triangle texture exceeds the dimension or 16 million pixel limit".into(),
);
}
if textures.insert(Arc::as_ptr(&texture.image)) {
texture_pixels += pixels;
if texture_pixels > 64 * 1024 * 1024 {
return Err("scene color textures exceed the 64 million pixel limit".into());
}
}
}
}
Ok(())
}
pub fn positions(&self) -> impl Iterator<Item = Vec3> + '_ {
self.triangles
.iter()
.flat_map(|t| t.vertices)
.chain(self.points.iter().map(|p| p.position))
.chain(self.lines.iter().flat_map(|line| line.vertices))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn invalid_triangle_appearance_is_rejected_before_rasterization() {
let scene = |appearance: TriangleAppearance| Scene {
triangles: vec![Triangle {
vertices: [
Vec3::default(),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
],
color: None,
}],
appearances: [(0, appearance)].into_iter().collect(),
..Scene::default()
};
assert!(scene(TriangleAppearance::default()).validate().is_ok());
assert!(
scene(TriangleAppearance {
alpha_mode: AlphaMode::Mask { cutoff: 1.1 },
..TriangleAppearance::default()
})
.validate()
.is_ok()
);
assert!(
scene(TriangleAppearance {
texcoords: Some([[f64::MAX, 0.0]; 3]),
..TriangleAppearance::default()
})
.validate()
.is_ok()
);
let invalid = [
TriangleAppearance {
base_color: [1.0, 0.0, f64::NAN, 1.0],
..TriangleAppearance::default()
},
TriangleAppearance {
colors: Some([[1.1; 4]; 3]),
..TriangleAppearance::default()
},
TriangleAppearance {
texcoords: Some([[f64::INFINITY, 0.0]; 3]),
..TriangleAppearance::default()
},
TriangleAppearance {
texcoords: Some([[f64::NAN, 0.0]; 3]),
..TriangleAppearance::default()
},
TriangleAppearance {
normals: Some([Vec3::default(); 3]),
..TriangleAppearance::default()
},
TriangleAppearance {
alpha_mode: AlphaMode::Mask { cutoff: -0.1 },
..TriangleAppearance::default()
},
TriangleAppearance {
texture: Some(Texture {
image: Arc::new(RgbaImage::new(1, 1)),
wrap_s: TextureWrap::Repeat,
wrap_t: TextureWrap::Repeat,
filter: TextureFilter::Nearest,
min_filter: TextureFilter::Nearest,
interpolation_space: TextureInterpolationSpace::Linear,
}),
..TriangleAppearance::default()
},
TriangleAppearance {
texture: Some(Texture {
image: Arc::new(RgbaImage::new(0, 1)),
wrap_s: TextureWrap::Repeat,
wrap_t: TextureWrap::Repeat,
filter: TextureFilter::Nearest,
min_filter: TextureFilter::Nearest,
interpolation_space: TextureInterpolationSpace::Linear,
}),
texcoords: Some([[0.0; 2]; 3]),
..TriangleAppearance::default()
},
];
for appearance in invalid {
assert!(scene(appearance).validate().is_err());
}
let mut missing = scene(TriangleAppearance::default());
missing.appearances.insert(1, TriangleAppearance::default());
assert!(missing.validate().is_err());
}
}