#![no_std]
#[cfg(feature = "std")]
extern crate std;
#[cfg(feature = "depth-u16")]
pub type ZDepth = u16;
#[cfg(feature = "depth-u16")]
pub const Z_MAX_VALUE: ZDepth = u16::MAX;
#[cfg(feature = "depth-u16")]
pub const DEPTH_EPSILON: ZDepth = 1;
#[cfg(not(feature = "depth-u16"))]
pub type ZDepth = u32;
#[cfg(not(feature = "depth-u16"))]
pub const Z_MAX_VALUE: ZDepth = u32::MAX;
#[cfg(not(feature = "depth-u16"))]
pub const DEPTH_EPSILON: ZDepth = 128;
#[inline(always)]
pub const fn to_zdepth(z: u32) -> ZDepth {
#[cfg(feature = "depth-u16")]
{
(z >> 16) as u16
}
#[cfg(not(feature = "depth-u16"))]
{
z
}
}
#[cfg(feature = "dma2d")]
unsafe extern "Rust" {
#[cfg(feature = "depth-u16")]
fn dma2d_clear_zbuffer_u16(ptr: *mut u16, len: usize, value: u16);
#[cfg(not(feature = "depth-u16"))]
fn dma2d_clear_zbuffer_u32(ptr: *mut u32, len: usize, value: u32);
}
#[inline(always)]
pub fn clear_zbuffer(zbuffer: &mut [ZDepth], value: ZDepth) {
#[cfg(feature = "dma2d")]
{
#[cfg(feature = "depth-u16")]
unsafe {
dma2d_clear_zbuffer_u16(zbuffer.as_mut_ptr(), zbuffer.len(), value);
}
#[cfg(not(feature = "depth-u16"))]
unsafe {
dma2d_clear_zbuffer_u32(zbuffer.as_mut_ptr(), zbuffer.len(), value);
}
}
#[cfg(not(feature = "dma2d"))]
{
zbuffer.fill(value);
}
}
#[allow(unused_imports)]
use crate::mesh::K3dMesh;
#[allow(unused_imports)]
use nalgebra::Vector4;
use nalgebra::{Matrix4, Point3, Vector3};
#[allow(unused_imports)]
use nalgebra::ComplexField;
pub mod animation;
#[cfg(feature = "scene")]
pub mod billboard;
pub mod engine;
pub mod primitive;
pub use engine::{BudgetFallbackOutcome, DegradationOutcome, K3dengine};
pub use primitive::DrawPrimitive;
#[derive(Debug, Clone, Copy)]
pub struct MeshRayCastHit {
pub distance: f32,
pub point: Vector3<f32>,
pub normal: Vector3<f32>,
pub face_index: usize,
pub uv: [f32; 2],
}
#[cfg(feature = "aabb-cull")]
pub mod bounds;
pub mod bridge;
#[cfg(feature = "raycast")]
pub mod bsp;
pub mod camera;
#[cfg(feature = "scene")]
pub mod character;
pub mod command_buffer;
pub mod completion;
pub mod config;
pub mod display_backend;
pub mod draw;
#[cfg(feature = "embassy")]
pub mod embassy;
pub mod error;
#[cfg(feature = "gizmos")]
pub mod gizmos;
pub mod hardware_profile;
#[cfg(feature = "hud")]
pub mod hud;
pub mod input;
#[cfg(feature = "lighting")]
pub mod lights;
pub mod mesh;
#[cfg(feature = "painters")]
pub mod painters;
#[cfg(feature = "scene")]
pub mod particles;
#[cfg(feature = "perfcounter")]
pub mod perfcounter;
#[cfg(feature = "physics")]
pub mod physics;
pub mod raster;
pub mod raycast;
#[cfg(feature = "render-layers")]
pub mod render_layers;
pub mod renderer;
pub mod retro;
pub mod shader;
#[cfg(feature = "scene")]
pub mod scene_format;
#[cfg(feature = "scene")]
pub mod scene_stream;
#[cfg(feature = "raycast")]
pub mod sector_lights;
#[cfg(feature = "scene")]
pub mod skeleton;
#[cfg(feature = "physics")]
pub mod softbody;
pub mod swapchain;
pub mod telemetry;
#[cfg(feature = "textured")]
pub mod texture;
pub mod tilebin;
#[cfg(feature = "scene")]
pub mod transform_anim;
#[cfg(feature = "scene")]
pub mod tween;
pub use embedded_graphics_framebuf::{
FrameBuf,
backends::{DMACapableFrameBufferBackend, EndianCorrectedBuffer, EndianCorrection},
};
pub use draw::PixelRead;
#[cfg(feature = "aa")]
pub use draw::ReadPixel;
#[cfg(feature = "aabb-cull")]
pub use bounds::Aabb;
pub use bridge::{
AsEgPoint, AsNalgebraPoint, draw_to, eg_to_nalgebra, nalgebra_to_eg, render_drawable_to_buffer,
};
pub use camera::{Camera, Ray};
#[cfg(feature = "scene")]
pub use character::CharacterController;
pub use completion::{CompletionSlot, WaitTransfer, WaitTransferFuture};
pub use display_backend::{
AsyncDmaTransfer, DisplayBackend, DisplayError, DisplayRegion, DmaTransfer, SimulatorBackend,
TransferError,
};
#[cfg(feature = "aa")]
pub use draw::draw_zbuffered_2xssaa;
pub use draw::{
DitherConfig, FogConfig, fast_blend_rgb565, fast_blend_rgba8888, fast_blend_rgba8888_to_rgb565,
reverse_color_rgb565, reverse_color_rgba8888,
};
#[cfg(feature = "embassy")]
pub use embassy::{EmbassyWaitTransfer, EmbassyWaitTransferFuture, FrameClock};
pub use swapchain::{StandardSwapChain, SwapChain};
#[cfg(feature = "triple-buffering")]
pub use swapchain::{StandardTripleSwapChain, TripleSwapChain};
#[cfg(feature = "fixed-transform")]
pub use embedded_dsp::fixed_point::{
FP_ONE, Q16, Q16_MAX, Q16_MIN, ScanlineInterp, abs_q16, angle_to_q16, div_f_q16, div_n_q16,
div_q16, from_i16_q16, from_q16, lerp_q16, mul_f_q16, mul_n_q16, mul_q16, q16_to_q31,
q31_to_q16, qadd_q16, qsub_q16, recip_q16, to_i16_q16, to_q16,
};
pub use input::InputState;
#[cfg(feature = "lighting")]
pub use lights::{PointLight, PointLightSet};
#[cfg(feature = "scene")]
pub use particles::{ParticleSpawn, ParticleSystem};
#[cfg(feature = "render-layers")]
pub use render_layers::RenderLayers;
pub use renderer::{DirtyRegion, FrameCtx};
pub use retro::{
LightLevels, PaletteMode, RetroStyle, ScreenTint, SkyConfig, StippleMode, TextureMapping,
};
#[cfg(feature = "raycast")]
pub use sector_lights::{LightEffectKind, SectorLight, light_level_at, light_level_u8_at};
pub use tilebin::{TileBinStats, TileConfig};
#[cfg(feature = "scene")]
pub use transform_anim::{AnimationPlayer, SampledTransform, TransformKeyframe, TransformTrack};
#[cfg(feature = "scene")]
pub use tween::{Easing, Tween, Tween3, apply_easing, lerp, lerp3, scale_rgb565};
pub fn mesh_ray_cast(
ray_origin: Vector3<f32>,
ray_dir: Vector3<f32>,
geometry: &mesh::Geometry<'_>,
model_matrix: &Matrix4<f32>,
max_distance: f32,
) -> Option<MeshRayCastHit> {
#[cfg(feature = "aabb-cull")]
{
return mesh_ray_cast_bounded(
ray_origin,
ray_dir,
geometry,
model_matrix,
max_distance,
None,
);
}
#[cfg(not(feature = "aabb-cull"))]
{
mesh_ray_cast_world(ray_origin, ray_dir, geometry, model_matrix, max_distance)
}
}
#[cfg(not(feature = "aabb-cull"))]
fn mesh_ray_cast_world(
ray_origin: Vector3<f32>,
ray_dir: Vector3<f32>,
geometry: &mesh::Geometry<'_>,
model_matrix: &Matrix4<f32>,
max_distance: f32,
) -> Option<MeshRayCastHit> {
let mut nearest: Option<MeshRayCastHit> = None;
let mut min_dist = max_distance;
for (face_index, face) in geometry.faces.iter().enumerate() {
let raw_v0 = geometry.vertices[face[0]];
let raw_v1 = geometry.vertices[face[1]];
let raw_v2 = geometry.vertices[face[2]];
let v0 = model_matrix
.transform_point(&Point3::new(raw_v0[0], raw_v0[1], raw_v0[2]))
.coords;
let v1 = model_matrix
.transform_point(&Point3::new(raw_v1[0], raw_v1[1], raw_v1[2]))
.coords;
let v2 = model_matrix
.transform_point(&Point3::new(raw_v2[0], raw_v2[1], raw_v2[2]))
.coords;
let edge1 = v1 - v0;
let edge2 = v2 - v0;
let h = ray_dir.cross(&edge2);
let det = edge1.dot(&h);
if det.abs() < 1e-6 {
continue;
}
let inv_det = 1.0 / det;
let s = ray_origin - v0;
let bary_u = inv_det * s.dot(&h);
if !(0.0..=1.0).contains(&bary_u) {
continue;
}
let q = s.cross(&edge1);
let bary_v = inv_det * ray_dir.dot(&q);
if bary_v < 0.0 || bary_u + bary_v > 1.0 {
continue;
}
let t = inv_det * edge2.dot(&q);
if t <= 0.0 || t >= min_dist {
continue;
}
let normal = edge1.cross(&edge2).normalize();
let bary_w = 1.0 - bary_u - bary_v;
let uv = if geometry.uvs.len() > face[0]
&& geometry.uvs.len() > face[1]
&& geometry.uvs.len() > face[2]
{
let uv0 = geometry.uvs[face[0]];
let uv1 = geometry.uvs[face[1]];
let uv2 = geometry.uvs[face[2]];
[
bary_w * uv0[0] + bary_u * uv1[0] + bary_v * uv2[0],
bary_w * uv0[1] + bary_u * uv1[1] + bary_v * uv2[1],
]
} else {
[0.0, 0.0]
};
let point = ray_origin + ray_dir * t;
min_dist = t;
nearest = Some(MeshRayCastHit {
distance: t,
point,
normal,
face_index,
uv,
});
}
nearest
}
#[cfg(feature = "aabb-cull")]
pub fn mesh_ray_cast_bounded(
ray_origin: Vector3<f32>,
ray_dir: Vector3<f32>,
geometry: &mesh::Geometry<'_>,
model_matrix: &Matrix4<f32>,
max_distance: f32,
model_aabb: Option<&Aabb>,
) -> Option<MeshRayCastHit> {
let inv = model_matrix.try_inverse()?;
let origin4 = inv * Vector4::new(ray_origin.x, ray_origin.y, ray_origin.z, 1.0);
let dir4 = inv * Vector4::new(ray_dir.x, ray_dir.y, ray_dir.z, 0.0);
if origin4.w.abs() < 1e-8 {
return None;
}
let local_origin = Vector3::new(origin4.x, origin4.y, origin4.z) / origin4.w;
let local_dir = Vector3::new(dir4.x, dir4.y, dir4.z);
let dir_len = local_dir.norm();
if dir_len < 1e-8 {
return None;
}
let local_dir_n = local_dir / dir_len;
let local_max = max_distance * dir_len;
if let Some(aabb) = model_aabb
&& aabb
.intersect_ray(local_origin, local_dir_n, local_max)
.is_none()
{
return None;
}
let mut nearest: Option<MeshRayCastHit> = None;
let mut min_dist = local_max;
for (face_index, face) in geometry.faces.iter().enumerate() {
let v0 = Vector3::new(
geometry.vertices[face[0]][0],
geometry.vertices[face[0]][1],
geometry.vertices[face[0]][2],
);
let v1 = Vector3::new(
geometry.vertices[face[1]][0],
geometry.vertices[face[1]][1],
geometry.vertices[face[1]][2],
);
let v2 = Vector3::new(
geometry.vertices[face[2]][0],
geometry.vertices[face[2]][1],
geometry.vertices[face[2]][2],
);
let edge1 = v1 - v0;
let edge2 = v2 - v0;
let h = local_dir_n.cross(&edge2);
let det = edge1.dot(&h);
if det.abs() < 1e-6 {
continue;
}
let inv_det = 1.0 / det;
let s = local_origin - v0;
let bary_u = inv_det * s.dot(&h);
if !(0.0..=1.0).contains(&bary_u) {
continue;
}
let q = s.cross(&edge1);
let bary_v = inv_det * local_dir_n.dot(&q);
if bary_v < 0.0 || bary_u + bary_v > 1.0 {
continue;
}
let t_local = inv_det * edge2.dot(&q);
if t_local <= 0.0 || t_local >= min_dist {
continue;
}
let normal_local = edge1.cross(&edge2).normalize();
let rot = model_matrix.fixed_view::<3, 3>(0, 0);
let normal = (rot * normal_local).normalize();
let bary_w = 1.0 - bary_u - bary_v;
let uv = if geometry.uvs.len() > face[0]
&& geometry.uvs.len() > face[1]
&& geometry.uvs.len() > face[2]
{
let uv0 = geometry.uvs[face[0]];
let uv1 = geometry.uvs[face[1]];
let uv2 = geometry.uvs[face[2]];
[
bary_w * uv0[0] + bary_u * uv1[0] + bary_v * uv2[0],
bary_w * uv0[1] + bary_u * uv1[1] + bary_v * uv2[1],
]
} else {
[0.0, 0.0]
};
let local_hit = local_origin + local_dir_n * t_local;
let world_hit = model_matrix
.transform_point(&Point3::from(local_hit))
.coords;
let world_t = (world_hit - ray_origin).norm();
if world_t >= max_distance {
continue;
}
min_dist = t_local;
nearest = Some(MeshRayCastHit {
distance: world_t,
point: world_hit,
normal,
face_index,
uv,
});
}
nearest
}
#[cfg(feature = "aabb-cull")]
pub fn mesh_ray_cast_mesh(
ray_origin: Vector3<f32>,
ray_dir: Vector3<f32>,
mesh: &K3dMesh<'_>,
max_distance: f32,
) -> Option<MeshRayCastHit> {
let distance = (mesh.get_position() - Point3::from(ray_origin)).norm();
let geometry = mesh.select_lod(distance);
let aabb = mesh.model_aabb();
mesh_ray_cast_bounded(
ray_origin,
ray_dir,
geometry,
&mesh.model_matrix,
max_distance,
Some(&aabb),
)
}
#[cfg(feature = "dma2d")]
mod dma2d_stubs {
#[cfg(feature = "depth-u16")]
#[unsafe(no_mangle)]
extern "Rust" fn dma2d_clear_zbuffer_u16(ptr: *mut u16, len: usize, value: u16) {
let slice = unsafe { core::slice::from_raw_parts_mut(ptr, len) };
slice.fill(value);
}
#[cfg(not(feature = "depth-u16"))]
#[unsafe(no_mangle)]
extern "Rust" fn dma2d_clear_zbuffer_u32(ptr: *mut u32, len: usize, value: u32) {
let slice = unsafe { core::slice::from_raw_parts_mut(ptr, len) };
slice.fill(value);
}
}
#[cfg(test)]
mod tests {
extern crate std;
#[cfg(feature = "depth-u16")]
pub type ZDepth = u16;
#[cfg(feature = "depth-u16")]
pub const Z_MAX_VALUE: ZDepth = u16::MAX;
#[cfg(feature = "depth-u16")]
pub const DEPTH_EPSILON: ZDepth = 1;
#[cfg(not(feature = "depth-u16"))]
pub type ZDepth = u32;
#[cfg(not(feature = "depth-u16"))]
pub const Z_MAX_VALUE: ZDepth = u32::MAX;
#[cfg(not(feature = "depth-u16"))]
pub const DEPTH_EPSILON: ZDepth = 128;
#[inline(always)]
pub const fn to_zdepth(z: u32) -> ZDepth {
#[cfg(feature = "depth-u16")]
{
(z >> 16) as u16
}
#[cfg(not(feature = "depth-u16"))]
{
z
}
}
use super::*;
#[test]
fn test_engine_creation() {
let engine = K3dengine::new(640, 480);
assert_eq!(engine.width, 640);
assert_eq!(engine.height, 480);
assert!((engine.camera.get_aspect_ratio() - 640.0 / 480.0).abs() < 0.001);
}
#[test]
fn test_transform_point_basic() {
let engine = K3dengine::new(640, 480);
let transform_matrix = engine.camera.vp_matrix;
let point = [0.0, 0.0, -5.0];
let result = engine.transform_point(&point, transform_matrix);
if let Some(transformed) = result {
assert!(transformed.x >= 0 && transformed.x < 640);
assert!(transformed.y >= 0 && transformed.y < 480);
}
}
#[test]
fn test_transform_point_clamps_out_of_bounds() {
let engine = K3dengine::new(640, 480);
let model_matrix = nalgebra::Matrix4::identity();
let point = [100.0, 100.0, -5.0];
let result = engine.transform_point(&point, model_matrix);
assert!(result.is_none());
}
#[test]
fn test_transform_point_behind_camera() {
let engine = K3dengine::new(640, 480);
let transform_matrix = engine.camera.vp_matrix;
let point = [0.0, 0.0, 1.0];
let _result = engine.transform_point(&point, transform_matrix);
}
#[test]
fn test_transform_point_near_plane_clipping() {
let engine = K3dengine::new(640, 480);
let transform_matrix = engine.camera.vp_matrix;
let point = [0.0, 0.0, -0.1];
let result = engine.transform_point(&point, transform_matrix);
assert!(result.is_none());
}
#[test]
fn test_transform_point_far_plane_clipping() {
let engine = K3dengine::new(640, 480);
let transform_matrix = engine.camera.vp_matrix;
let point = [0.0, 0.0, -1000.0];
let result = engine.transform_point(&point, transform_matrix);
assert!(result.is_none());
}
#[test]
fn test_transform_point_within_near_far_not_culled() {
let engine = K3dengine::new(640, 480);
let transform_matrix = engine.camera.vp_matrix;
let point = [0.0, 0.0, -0.8];
let result = engine.transform_point(&point, transform_matrix);
assert!(
result.is_some(),
"a point at distance 0.8 (within [near=0.4, far=20]) should not be culled"
);
}
#[test]
fn test_transform_points_array() {
let engine = K3dengine::new(640, 480);
let transform_matrix = engine.camera.vp_matrix;
let vertices = [[0.0, 0.0, -5.0], [0.1, 0.0, -5.0], [0.0, 0.1, -5.0]];
let indices = [0, 1, 2];
let result = engine.transform_points(&indices, &vertices, transform_matrix);
if let Some(points) = result {
assert_eq!(points.len(), 3);
}
}
#[test]
fn test_render_empty_faces_mesh() {
let engine = K3dengine::new(640, 480);
let vertices = [[0.0, 0.0, -5.0]]; let geometry = mesh::Geometry {
vertices: &vertices,
faces: &[],
colors: &[],
lines: &[],
normals: &[],
vertex_normals: &[],
uvs: &[],
texture_id: None,
};
let mesh = mesh::K3dMesh::new(geometry);
let mut callback_count = 0;
engine.render(std::iter::once(&mesh), |_| {
callback_count += 1;
});
assert!(callback_count > 0);
}
#[test]
fn test_render_points_mode() {
let engine = K3dengine::new(640, 480);
let vertices = [[0.0, 0.0, -5.0], [0.5, 0.0, -5.0]];
let geometry = mesh::Geometry {
vertices: &vertices,
faces: &[],
colors: &[],
lines: &[],
normals: &[],
vertex_normals: &[],
uvs: &[],
texture_id: None,
};
let mut mesh = mesh::K3dMesh::new(geometry);
mesh.set_render_mode(mesh::RenderMode::Points);
let mut primitives = std::vec::Vec::new();
engine.render(std::iter::once(&mesh), |prim| {
primitives.push(prim);
});
assert!(primitives.len() > 0);
for prim in primitives {
assert!(matches!(prim, DrawPrimitive::ColoredPoint(_, _)));
}
}
#[test]
fn test_render_lines_mode_with_faces() {
let engine = K3dengine::new(640, 480);
let vertices = [[0.0, 0.0, -5.0], [0.5, 0.0, -5.0], [0.0, 0.5, -5.0]];
let faces = [[0, 1, 2]];
let geometry = mesh::Geometry {
vertices: &vertices,
faces: &faces,
colors: &[],
lines: &[],
normals: &[],
vertex_normals: &[],
uvs: &[],
texture_id: None,
};
let mut mesh = mesh::K3dMesh::new(geometry);
mesh.set_render_mode(mesh::RenderMode::Lines);
let mut primitives = std::vec::Vec::new();
engine.render(std::iter::once(&mesh), |prim| {
primitives.push(prim);
});
assert_eq!(primitives.len(), 3);
for prim in primitives {
assert!(matches!(prim, DrawPrimitive::Line(_, _)));
}
}
#[test]
#[cfg(feature = "lighting")]
fn test_render_gouraud_light_dir() {
let mut engine = K3dengine::new(640, 480);
engine.camera.set_position(Point3::new(0.0, 0.0, -10.0));
engine.camera.set_target(Point3::new(0.0, 0.0, 0.0));
let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let faces = [[0, 1, 2]];
let normals = [[0.0, 0.0, -1.0]]; let vertex_normals = [[0.0, 0.0, -1.0], [0.0, 0.0, -1.0], [0.0, 0.0, -1.0]];
let geometry = mesh::Geometry {
vertices: &vertices,
faces: &faces,
colors: &[],
lines: &[],
normals: &normals,
vertex_normals: &vertex_normals,
uvs: &[],
texture_id: None,
};
let mut mesh = mesh::K3dMesh::new(geometry);
mesh.set_render_mode(mesh::RenderMode::GouraudLightDir(Vector3::new(
0.0, 0.0, 1.0,
)));
let mut primitives = std::vec::Vec::new();
engine.render(std::iter::once(&mesh), |prim| {
primitives.push(prim);
});
assert!(!primitives.is_empty());
for prim in &primitives {
assert!(matches!(
prim,
DrawPrimitive::GouraudTriangleWithDepth { .. }
));
}
}
#[test]
fn test_solid_inward_normals_interior_camera() {
let mut engine = K3dengine::new(320, 240);
engine
.camera
.set_position(nalgebra::Point3::new(0.0, 0.0, 0.0));
engine
.camera
.set_target(nalgebra::Point3::new(0.0, 0.0, -1.0));
#[rustfmt::skip]
let vertices: &[[f32; 3]] = &[
[-1.0, -1.0, -2.0],
[ 1.0, -1.0, -2.0],
[ 1.0, 1.0, -2.0],
[-1.0, 1.0, -2.0],
];
let faces: &[[usize; 3]] = &[[0, 1, 2], [0, 2, 3]];
let normals: &[[f32; 3]] = &[[0.0, 0.0, 1.0], [0.0, 0.0, 1.0]];
let geometry = mesh::Geometry {
vertices,
faces,
normals,
colors: &[],
lines: &[],
vertex_normals: &[],
uvs: &[],
texture_id: None,
};
let mut m = mesh::K3dMesh::new(geometry);
m.set_render_mode(mesh::RenderMode::Solid);
let mut count = 0usize;
engine.render(std::iter::once(&m), |_| count += 1);
assert!(
count > 0,
"interior Solid-with-inward-normals emitted 0 primitives — culling is wrong"
);
}
}