#![no_std]
use camera::Camera;
use embedded_graphics_core::pixelcolor::Rgb565;
use embedded_graphics_core::pixelcolor::RgbColor;
use mesh::K3dMesh;
use mesh::RenderMode;
use nalgebra::Matrix4;
use nalgebra::Point2;
use nalgebra::Point3;
use nalgebra::Vector3;
#[allow(unused_imports)]
use nalgebra::ComplexField;
pub mod animation;
pub mod billboard;
pub mod camera;
pub mod display_backend;
pub mod draw;
pub mod lut;
pub mod mesh;
#[cfg(feature = "std")]
pub mod painters;
#[cfg(feature = "perfcounter")]
pub mod perfcounter;
pub mod physics;
pub mod skeleton;
pub mod softbody;
pub mod swapchain;
pub mod texture;
pub use embedded_graphics_framebuf::{
FrameBuf,
backends::{DMACapableFrameBufferBackend, EndianCorrectedBuffer, EndianCorrection},
};
#[cfg(feature = "aa")]
pub use draw::ReadPixel;
#[derive(Debug, Clone)]
pub enum DrawPrimitive {
ColoredPoint(Point2<i32>, Rgb565),
Line([Point2<i32>; 2], Rgb565),
ColoredTriangle([Point2<i32>; 3], Rgb565),
ColoredTriangleWithDepth {
points: [Point2<i32>; 3],
depths: [f32; 3],
color: Rgb565,
},
GouraudTriangle {
points: [Point2<i32>; 3],
colors: [Rgb565; 3],
},
GouraudTriangleWithDepth {
points: [Point2<i32>; 3],
depths: [f32; 3],
colors: [Rgb565; 3],
},
TexturedTriangle {
points: [Point2<i32>; 3],
uvs: [[f32; 2]; 3],
texture_id: u32,
},
TexturedTriangleWithDepth {
points: [Point2<i32>; 3],
depths: [f32; 3],
uvs: [[f32; 2]; 3],
texture_id: u32,
},
}
pub struct K3dengine {
pub camera: Camera,
width: u16,
height: u16,
}
impl K3dengine {
pub fn new(width: u16, height: u16) -> K3dengine {
K3dengine {
camera: Camera::new(width as f32 / height as f32),
width,
height,
}
}
#[inline]
fn should_cull_mesh(&self, mesh: &K3dMesh) -> bool {
let mesh_pos = mesh.get_position();
let to_mesh = mesh_pos - self.camera.position;
let distance = to_mesh.norm();
let radius_sq = mesh.compute_bounding_radius_sq();
let radius = radius_sq.sqrt();
if distance - radius > self.camera.far {
return true;
}
if distance + radius < self.camera.near {
return true;
}
false
}
#[inline(always)]
fn transform_point(&self, point: &[f32; 3], model_matrix: Matrix4<f32>) -> Option<Point3<i32>> {
let point = nalgebra::Vector4::new(point[0], point[1], point[2], 1.0);
let point = model_matrix * point;
if point.w < 0.0 {
return None;
}
if point.z < self.camera.near || point.z > self.camera.far {
return None;
}
let point = Point3::from_homogeneous(point)?;
let x = ((1.0 + point.x) * 0.5 * self.width as f32) as i32;
let y = ((1.0 - point.y) * 0.5 * self.height as f32) as i32;
if x < 0 || x >= self.width as i32 || y < 0 || y >= self.height as i32 {
return None;
}
Some(Point3::new(
x,
y,
(point.z * (self.camera.far - self.camera.near) + self.camera.near) as i32,
))
}
#[inline(always)]
pub fn transform_points<const N: usize>(
&self,
indices: &[usize; N],
vertices: &[[f32; 3]],
model_matrix: Matrix4<f32>,
) -> Option<[Point3<i32>; N]> {
let mut ret = [Point3::new(0, 0, 0); N];
for i in 0..N {
ret[i] = self.transform_point(&vertices[indices[i]], model_matrix)?;
}
Some(ret)
}
pub fn render<'a, MS, F>(&self, meshes: MS, mut callback: F)
where
MS: IntoIterator<Item = &'a K3dMesh<'a>>,
F: FnMut(DrawPrimitive),
{
for mesh in meshes {
if mesh.geometry.vertices.is_empty() {
continue;
}
if self.should_cull_mesh(mesh) {
continue;
}
let mesh_pos = mesh.get_position();
let distance = (mesh_pos - self.camera.position).norm();
let geometry = mesh.select_lod(distance);
let transform_matrix = self.camera.vp_matrix * mesh.model_matrix;
match mesh.render_mode {
RenderMode::Points => {
let screen_space_points = geometry
.vertices
.iter()
.filter_map(|v| self.transform_point(v, transform_matrix));
if geometry.colors.len() == geometry.vertices.len() {
for (point, color) in screen_space_points.zip(geometry.colors) {
callback(DrawPrimitive::ColoredPoint(point.xy(), *color));
}
} else {
for point in screen_space_points {
callback(DrawPrimitive::ColoredPoint(point.xy(), mesh.color));
}
}
}
RenderMode::Lines if !geometry.lines.is_empty() => {
for line in geometry.lines {
if let Some([p1, p2]) =
self.transform_points(line, geometry.vertices, transform_matrix)
{
callback(DrawPrimitive::Line([p1.xy(), p2.xy()], mesh.color));
}
}
}
RenderMode::Lines if !geometry.faces.is_empty() => {
for face in geometry.faces {
if let Some([p1, p2, p3]) =
self.transform_points(face, geometry.vertices, transform_matrix)
{
callback(DrawPrimitive::Line([p1.xy(), p2.xy()], mesh.color));
callback(DrawPrimitive::Line([p2.xy(), p3.xy()], mesh.color));
callback(DrawPrimitive::Line([p3.xy(), p1.xy()], mesh.color));
}
}
}
RenderMode::Lines => {}
RenderMode::SolidLightDir(direction) => {
let color_as_float = Vector3::new(
mesh.color.r() as f32 / 32.0,
mesh.color.g() as f32 / 64.0,
mesh.color.b() as f32 / 32.0,
);
let ambient_color = color_as_float * 0.1;
let adjusted_dir = Vector3::new(direction.x, direction.y, -direction.z);
for (face, normal) in geometry.faces.iter().zip(geometry.normals.iter()) {
let normal = Vector3::new(normal[0], normal[1], normal[2]);
let transformed_normal = mesh.model_matrix.transform_vector(&normal);
if self.camera.get_direction().dot(&transformed_normal) < 0.0 {
continue;
}
if let Some([p1, p2, p3]) =
self.transform_points(face, geometry.vertices, transform_matrix)
{
let intensity = transformed_normal.dot(&adjusted_dir).max(0.0);
let final_color = color_as_float * intensity + ambient_color;
let final_color = Vector3::new(
final_color.x.clamp(0.0, 1.0),
final_color.y.clamp(0.0, 1.0),
final_color.z.clamp(0.0, 1.0),
);
let color = Rgb565::new(
(final_color.x * 31.0) as u8,
(final_color.y * 63.0) as u8,
(final_color.z * 31.0) as u8,
);
callback(DrawPrimitive::ColoredTriangleWithDepth {
points: [p1.xy(), p2.xy(), p3.xy()],
depths: [p1.z as f32, p2.z as f32, p3.z as f32],
color,
});
}
}
}
RenderMode::GouraudLightDir(direction) => {
let color_as_float = Vector3::new(
mesh.color.r() as f32 / 32.0,
mesh.color.g() as f32 / 64.0,
mesh.color.b() as f32 / 32.0,
);
let ambient_color = color_as_float * 0.1;
let adjusted_dir = Vector3::new(direction.x, direction.y, -direction.z);
for (face, face_normal) in geometry.faces.iter().zip(geometry.normals.iter()) {
let fn_vec = Vector3::new(face_normal[0], face_normal[1], face_normal[2]);
let transformed_fn = mesh.model_matrix.transform_vector(&fn_vec);
if self.camera.get_direction().dot(&transformed_fn) < 0.0 {
continue;
}
if let Some([p1, p2, p3]) =
self.transform_points(face, geometry.vertices, transform_matrix)
{
let vertex_colors: [Rgb565; 3] = core::array::from_fn(|k| {
let vn = if !geometry.vertex_normals.is_empty() {
let vn_arr = geometry.vertex_normals[face[k]];
let vn_vec = Vector3::new(vn_arr[0], vn_arr[1], vn_arr[2]);
mesh.model_matrix.transform_vector(&vn_vec)
} else {
transformed_fn
};
let intensity = vn.dot(&adjusted_dir).max(0.0);
let c = color_as_float * intensity + ambient_color;
Rgb565::new(
(c.x.clamp(0.0, 1.0) * 31.0) as u8,
(c.y.clamp(0.0, 1.0) * 63.0) as u8,
(c.z.clamp(0.0, 1.0) * 31.0) as u8,
)
});
callback(DrawPrimitive::GouraudTriangleWithDepth {
points: [p1.xy(), p2.xy(), p3.xy()],
depths: [p1.z as f32, p2.z as f32, p3.z as f32],
colors: vertex_colors,
});
}
}
}
RenderMode::BlinnPhong {
light_dir,
specular_intensity,
shininess,
} => {
let color_as_float = Vector3::new(
mesh.color.r() as f32 / 32.0,
mesh.color.g() as f32 / 64.0,
mesh.color.b() as f32 / 32.0,
);
let ambient_color = color_as_float * 0.1;
let adjusted_light_dir = Vector3::new(light_dir.x, light_dir.y, -light_dir.z);
let light_dir_normalized = adjusted_light_dir.normalize();
for (face, normal) in geometry.faces.iter().zip(geometry.normals.iter()) {
let normal = Vector3::new(normal[0], normal[1], normal[2]);
let transformed_normal = mesh.model_matrix.transform_vector(&normal);
let normalized_normal = transformed_normal.normalize();
if self.camera.get_direction().dot(&normalized_normal) < 0.0 {
continue;
}
if let Some([p1, p2, p3]) =
self.transform_points(face, geometry.vertices, transform_matrix)
{
let v0 = geometry.vertices[face[0]];
let v1 = geometry.vertices[face[1]];
let v2 = geometry.vertices[face[2]];
let face_center = Point3::new(
(v0[0] + v1[0] + v2[0]) / 3.0,
(v0[1] + v1[1] + v2[1]) / 3.0,
(v0[2] + v1[2] + v2[2]) / 3.0,
);
let face_center_world = mesh.model_matrix.transform_point(&face_center);
let view_dir = (self.camera.position - face_center_world).normalize();
let half_vector = (light_dir_normalized + view_dir).normalize();
let diffuse_intensity =
normalized_normal.dot(&light_dir_normalized).max(0.0);
let specular_term =
normalized_normal.dot(&half_vector).max(0.0).powf(shininess);
let diffuse_color = color_as_float * diffuse_intensity;
let specular_color =
Vector3::new(1.0, 1.0, 1.0) * specular_term * specular_intensity;
let final_color = ambient_color + diffuse_color + specular_color;
let final_color = Vector3::new(
final_color.x.clamp(0.0, 1.0),
final_color.y.clamp(0.0, 1.0),
final_color.z.clamp(0.0, 1.0),
);
let color = Rgb565::new(
(final_color.x * 31.0) as u8,
(final_color.y * 63.0) as u8,
(final_color.z * 31.0) as u8,
);
callback(DrawPrimitive::ColoredTriangleWithDepth {
points: [p1.xy(), p2.xy(), p3.xy()],
depths: [p1.z as f32, p2.z as f32, p3.z as f32],
color,
});
}
}
}
RenderMode::Solid => {
if geometry.normals.is_empty() {
for face in geometry.faces.iter() {
if let Some([p1, p2, p3]) =
self.transform_points(face, geometry.vertices, transform_matrix)
{
callback(DrawPrimitive::ColoredTriangleWithDepth {
points: [p1.xy(), p2.xy(), p3.xy()],
depths: [p1.z as f32, p2.z as f32, p3.z as f32],
color: mesh.color,
});
}
}
} else {
for (face, normal) in geometry.faces.iter().zip(geometry.normals) {
let normal = Vector3::new(normal[0], normal[1], normal[2]);
let transformed_normal = mesh.model_matrix.transform_vector(&normal);
if self.camera.get_direction().dot(&transformed_normal) < 0.0 {
continue;
}
if let Some([p1, p2, p3]) =
self.transform_points(face, geometry.vertices, transform_matrix)
{
callback(DrawPrimitive::ColoredTriangleWithDepth {
points: [p1.xy(), p2.xy(), p3.xy()],
depths: [p1.z as f32, p2.z as f32, p3.z as f32],
color: mesh.color,
});
}
}
}
}
}
}
}
}
#[cfg(test)]
mod tests {
extern crate std;
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 model_matrix = nalgebra::Matrix4::identity();
let point = [0.0, 0.0, -0.01];
let result = engine.transform_point(&point, model_matrix);
assert!(result.is_none());
}
#[test]
fn test_transform_point_far_plane_clipping() {
let engine = K3dengine::new(640, 480);
let model_matrix = nalgebra::Matrix4::identity();
let point = [0.0, 0.0, -1000.0];
let result = engine.transform_point(&point, model_matrix);
assert!(result.is_none());
}
#[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]
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 { .. }
));
}
}
}