Files
MetaCrate/crates/metacrate-rendering-wgpu/src/lib.rs
Chili Palmer cb9233c59c
Some checks failed
CI / rust-skia (Rust only) (push) Has been cancelled
CI / required (push) Has been cancelled
Profile and preinitialize viewport rendering
2026-08-23 12:16:52 +02:00

288 lines
7.4 KiB
Rust

//! Reusable, windowless `OpenSim` scene rendering on stable `wgpu`.
#![allow(clippy::missing_errors_doc)]
type CameraBasis = ([f32; 3], [f32; 3], [f32; 3]);
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Camera {
pub position: [f32; 3],
pub forward: [f32; 3],
pub up: [f32; 3],
pub vertical_fov_degrees: f32,
}
impl Camera {
pub fn look_at(
position: [f32; 3],
target: [f32; 3],
up: [f32; 3],
vertical_fov_degrees: f32,
) -> Result<Self, RenderError> {
let camera = Self {
position,
forward: normalize(sub(target, position))?,
up: normalize(up)?,
vertical_fov_degrees,
};
camera.basis()?;
Ok(camera)
}
fn basis(self) -> Result<CameraBasis, RenderError> {
if !(10.0..=140.0).contains(&self.vertical_fov_degrees)
|| self
.position
.iter()
.chain(self.forward.iter())
.chain(self.up.iter())
.any(|value| !value.is_finite())
{
return Err(RenderError::InvalidScene);
}
let forward = normalize(self.forward)?;
let right = normalize(cross(forward, self.up))?;
Ok((right, cross(right, forward), forward))
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Vertex {
pub position: [f32; 3],
pub normal: [f32; 3],
pub tex_coord: [f32; 2],
pub color_srgb: [u8; 4],
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Mesh {
pub vertices: Vec<Vertex>,
pub indices: Vec<u32>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Texture {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct UvTransform {
pub scale: [f32; 2],
pub offset: [f32; 2],
pub rotation_radians: f32,
}
impl Default for UvTransform {
fn default() -> Self {
Self {
scale: [1.0; 2],
offset: [0.0; 2],
rotation_radians: 0.0,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct TextureSlot {
pub texture: Option<usize>,
pub transform: UvTransform,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum AlphaMode {
#[default]
Opaque,
Mask {
cutoff: f32,
},
Blend,
}
#[derive(Clone, Debug, PartialEq)]
pub struct BlinnPhongMaterial {
pub diffuse_color_srgb: [u8; 4],
pub diffuse: TextureSlot,
pub normal: TextureSlot,
pub specular: TextureSlot,
pub specular_color_srgb: [u8; 3],
pub shininess: f32,
pub environment_intensity: f32,
pub fullbright: bool,
pub double_sided: bool,
pub alpha_mode: AlphaMode,
/// Optional diffuse-texture cutout applied before face-alpha blending.
pub alpha_cutoff: Option<f32>,
}
impl Default for BlinnPhongMaterial {
fn default() -> Self {
Self {
diffuse_color_srgb: [255; 4],
diffuse: TextureSlot::default(),
normal: TextureSlot::default(),
specular: TextureSlot::default(),
specular_color_srgb: [0; 3],
shininess: 32.0,
environment_intensity: 0.0,
fullbright: false,
double_sided: false,
alpha_mode: AlphaMode::Opaque,
alpha_cutoff: None,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct PbrMaterial {
pub base_color_srgb: [u8; 4],
pub base_color: TextureSlot,
pub normal: TextureSlot,
/// glTF convention: roughness in G and metallic in B.
pub metallic_roughness: TextureSlot,
pub emissive: TextureSlot,
pub metallic_factor: f32,
pub roughness_factor: f32,
pub emissive_factor_srgb: [u8; 3],
pub double_sided: bool,
pub alpha_mode: AlphaMode,
}
impl Default for PbrMaterial {
fn default() -> Self {
Self {
base_color_srgb: [255; 4],
base_color: TextureSlot::default(),
normal: TextureSlot::default(),
metallic_roughness: TextureSlot::default(),
emissive: TextureSlot::default(),
metallic_factor: 1.0,
roughness_factor: 1.0,
emissive_factor_srgb: [0; 3],
double_sided: false,
alpha_mode: AlphaMode::Opaque,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Material {
BlinnPhong(BlinnPhongMaterial),
Pbr(PbrMaterial),
}
impl Default for Material {
fn default() -> Self {
Self::BlinnPhong(BlinnPhongMaterial::default())
}
}
impl Material {
#[must_use]
pub fn alpha_mode(&self) -> AlphaMode {
match self {
Self::BlinnPhong(material) => material.alpha_mode,
Self::Pbr(material) => material.alpha_mode,
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Renderable {
pub mesh: Mesh,
pub material: Material,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RenderLimits {
pub width: u32,
pub height: u32,
pub max_triangles: usize,
pub max_texture_bytes: usize,
pub max_texture_pixels: usize,
pub far_distance: u32,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RenderError {
AdapterUnavailable,
InvalidScene,
ResourceLimit,
TimedOut,
Readback,
Device(String),
}
impl std::fmt::Display for RenderError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::AdapterUnavailable => "wgpu adapter is unavailable",
Self::InvalidScene => "render scene is invalid",
Self::ResourceLimit => "render resource limit exceeded",
Self::TimedOut => "wgpu render timed out",
Self::Readback => "wgpu readback failed",
Self::Device(_) => "wgpu device failed",
})?;
if let Self::Device(detail) = self {
write!(formatter, ": {detail}")?;
}
Ok(())
}
}
impl std::error::Error for RenderError {}
fn sub(left: [f32; 3], right: [f32; 3]) -> [f32; 3] {
std::array::from_fn(|axis| left[axis] - right[axis])
}
fn normalize(value: [f32; 3]) -> Result<[f32; 3], RenderError> {
let length = dot(value, value).sqrt();
if length < 0.0001 || !length.is_finite() {
return Err(RenderError::InvalidScene);
}
Ok(value.map(|component| component / length))
}
fn dot(left: [f32; 3], right: [f32; 3]) -> f32 {
left[0] * right[0] + left[1] * right[1] + left[2] * right[2]
}
fn cross(left: [f32; 3], right: [f32; 3]) -> [f32; 3] {
[
left[1] * right[2] - left[2] * right[1],
left[2] * right[0] - left[0] * right[2],
left[0] * right[1] - left[1] * right[0],
]
}
#[cfg(feature = "wgpu")]
mod gpu;
#[cfg(feature = "wgpu")]
pub use gpu::{RenderTimings, RenderedFrame, Renderer};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn camera_and_both_material_models_are_explicit() {
let camera =
Camera::look_at([1.0, 2.0, 3.0], [5.0, 2.0, 3.0], [0.0, 0.0, 1.0], 60.0).unwrap();
assert!(
camera
.forward
.iter()
.zip([1.0, 0.0, 0.0])
.all(|(actual, expected)| (actual - expected).abs() < f32::EPSILON)
);
assert!(Camera::look_at([0.0; 3], [0.0; 3], [0.0, 0.0, 1.0], 60.0).is_err());
assert!(matches!(Material::default(), Material::BlinnPhong(_)));
assert!(matches!(
Material::Pbr(PbrMaterial::default()),
Material::Pbr(_)
));
}
}