use glam::{Mat3, Mat4, Vec2, Vec3, Vec4};
use crate::scene::{Mesh, Object};
use crate::{Blending, Camera, Light, Material, Scene, Side};
pub(crate) struct Draw<'s> {
pub material: &'s Material,
pub cast_shadow: bool,
pub receive_shadow: bool,
pub has_uv: bool,
pub has_color: bool,
}
#[derive(Default)]
pub(crate) struct Streams {
pub pos: Vec<Vec3>,
pub nrm: Vec<Vec3>,
pub uv: Vec<Vec2>,
pub col: Vec<Vec4>,
}
impl Streams {
fn lerp(&mut self, a: u32, b: u32, t: f32) -> u32 {
let (a, b) = (a as usize, b as usize);
self.pos.push(self.pos[a].lerp(self.pos[b], t));
self.nrm.push(self.nrm[a].lerp(self.nrm[b], t));
self.uv.push(self.uv[a].lerp(self.uv[b], t));
self.col.push(self.col[a].lerp(self.col[b], t));
self.pos.len() as u32 - 1
}
}
#[derive(Clone, Copy)]
pub(crate) struct WorldTri {
pub v: [u32; 3],
pub draw: u32,
}
pub(crate) enum LightKind {
Directional {
direction: Vec3,
shadow: Option<usize>,
},
Point {
position: Vec3,
distance: f32,
decay: f32,
},
Spot {
position: Vec3,
direction: Vec3,
distance: f32,
decay: f32,
cos_outer: f32,
cos_inner: f32,
},
}
pub(crate) struct LightInfo {
pub color: Vec3,
pub kind: LightKind,
}
#[derive(Default)]
pub(crate) struct Lights {
pub direct: Vec<LightInfo>,
pub ambient: Vec3,
pub hemisphere: Option<(Vec3, Vec3)>,
pub shadow_casters: Vec<(Vec3, crate::Shadow)>,
}
#[derive(Default)]
pub(crate) struct World<'s> {
pub streams: Streams,
pub tris: Vec<WorldTri>,
pub draws: Vec<Draw<'s>>,
pub sprites: Vec<WorldSprite>,
}
pub(crate) struct WorldSprite {
pub position: Vec3,
pub size: f32,
pub color: Vec3,
pub opacity: f32,
pub additive: bool,
}
pub(crate) fn build_world(scene: &Scene) -> (World<'_>, Lights) {
let worlds = scene.world_matrices();
let visible = scene.visibilities();
let mut world = World::default();
let mut lights = Lights::default();
for (id, node) in scene.nodes() {
if !visible[id.index()] {
continue;
}
let m = worlds[id.index()];
match &node.object {
Object::Group => {}
Object::Mesh(mesh) => push_mesh(&mut world, mesh, &m, &worlds),
Object::Light(light) => push_light(&mut lights, light, &m),
Object::Particles(particles) => {
let scale = m.x_axis.truncate().length();
for p in &particles.items {
if p.opacity <= 0.0 {
continue;
}
world.sprites.push(WorldSprite {
position: m.transform_point3(p.position),
size: p.size * scale,
color: p.color.0 * p.opacity,
opacity: p.opacity,
additive: particles.blending == Blending::Additive,
});
}
}
}
}
(world, lights)
}
pub(crate) fn push_light(lights: &mut Lights, light: &Light, m: &Mat4) {
let rotate = |d: Vec3| m.transform_vector3(d).normalize_or(d);
let position = m.w_axis.truncate();
match light {
Light::Ambient { color, intensity } => lights.ambient += color.0 * *intensity,
Light::Hemisphere {
sky,
ground,
intensity,
} => {
let (s, g) = lights.hemisphere.unwrap_or((Vec3::ZERO, Vec3::ZERO));
lights.hemisphere = Some((s + sky.0 * *intensity, g + ground.0 * *intensity));
}
Light::Directional(l) => {
let direction = rotate(l.direction);
let shadow = l.shadow.as_ref().map(|shadow| {
lights.shadow_casters.push((direction, shadow.clone()));
lights.shadow_casters.len() - 1
});
lights.direct.push(LightInfo {
color: l.color.0 * l.intensity,
kind: LightKind::Directional { direction, shadow },
});
}
Light::Point(l) => lights.direct.push(LightInfo {
color: l.color.0 * l.intensity,
kind: LightKind::Point {
position,
distance: l.distance,
decay: l.decay,
},
}),
Light::Spot(l) => lights.direct.push(LightInfo {
color: l.color.0 * l.intensity,
kind: LightKind::Spot {
position,
direction: rotate(l.direction),
distance: l.distance,
decay: l.decay,
cos_outer: l.angle.cos(),
cos_inner: (l.angle * (1.0 - l.penumbra)).cos(),
},
}),
}
}
fn push_mesh<'s>(world: &mut World<'s>, mesh: &'s Mesh, m: &Mat4, worlds: &[Mat4]) {
let g = &*mesh.geometry;
if g.indices.is_empty() || g.positions.is_empty() {
return;
}
let deformed = deformed(mesh, m, worlds);
let instances = mesh.instances.as_deref().filter(|_| deformed.is_none());
let copies: Vec<(Mat4, Vec4)> = match instances {
Some(instances) => (instances.transforms.iter().enumerate())
.map(|(i, t)| (*m * *t, instances.color(i)))
.collect(),
None => vec![(*m, Vec4::ONE)],
};
let draw = world.draws.len() as u32;
world.draws.push(Draw {
material: &mesh.material,
cast_shadow: mesh.cast_shadow,
receive_shadow: mesh.receive_shadow,
has_uv: !g.uvs.is_empty(),
has_color: !g.colors.is_empty() || instances.is_some_and(|i| !i.colors.is_empty()),
});
let count = g.positions.len();
for (m, tint) in copies {
let base = world.streams.pos.len() as u32;
let s = &mut world.streams;
if let Some((pos, nrm)) = &deformed {
s.pos.extend_from_slice(pos);
s.nrm.extend_from_slice(nrm);
} else {
let normal_matrix = Mat3::from_mat4(m).inverse().transpose();
s.pos
.extend(g.positions.iter().map(|p| m.transform_point3(*p)));
s.nrm.extend((0..count).map(|i| {
(normal_matrix * g.normals.get(i).copied().unwrap_or(Vec3::Y)).normalize_or(Vec3::Y)
}));
}
if g.uvs.len() == count {
s.uv.extend_from_slice(&g.uvs);
} else {
s.uv.extend(std::iter::repeat_n(Vec2::ZERO, count));
}
if g.colors.len() == count {
s.col.extend(g.colors.iter().map(|c| *c * tint));
} else {
s.col.extend(std::iter::repeat_n(tint, count));
}
let mirrored = mesh.skin.is_none() && m.determinant() < 0.0;
let tris = g.indices.as_chunks::<3>().0.iter();
world.tris.extend(
tris.filter(|t| t.iter().all(|&i| (i as usize) < count))
.map(|t| {
let [a, b, c] = t.map(|i| base + i);
let v = if mirrored { [a, c, b] } else { [a, b, c] };
WorldTri { v, draw }
}),
);
}
}
pub(crate) fn deformed(mesh: &Mesh, m: &Mat4, worlds: &[Mat4]) -> Option<(Vec<Vec3>, Vec<Vec3>)> {
let g = &*mesh.geometry;
let count = g.positions.len();
let morphing = mesh.morph_weights.iter().any(|w| *w != 0.0) && !g.morph_targets.is_empty();
let skinning = mesh.skin.is_some() && g.skin.is_some();
if !morphing && !skinning {
return None;
}
let mut pos = g.positions.clone();
let mut nrm: Vec<Vec3> = (0..count)
.map(|i| g.normals.get(i).copied().unwrap_or(Vec3::Y))
.collect();
if morphing {
for (target, &w) in g.morph_targets.iter().zip(&mesh.morph_weights) {
if w == 0.0 {
continue;
}
for (p, d) in pos.iter_mut().zip(&target.positions) {
*p += *d * w;
}
for (n, d) in nrm.iter_mut().zip(&target.normals) {
*n += *d * w;
}
}
}
if let (Some(skin), Some(weights)) = (&mesh.skin, &g.skin) {
let joints: Vec<Mat4> = skin
.joints
.iter()
.enumerate()
.map(|(i, joint)| {
let world = worlds.get(joint.index()).copied().unwrap_or(Mat4::IDENTITY);
let inverse_bind = skin.inverse_bind_matrices.get(i).copied();
world * inverse_bind.unwrap_or(Mat4::IDENTITY)
})
.collect();
for i in 0..count {
let vertex_joints = weights.joints.get(i).copied().unwrap_or_default();
let vertex_weights = weights.weights.get(i).copied();
let mut skin_m = Mat4::ZERO;
for (&joint, &w) in vertex_joints
.iter()
.zip(&vertex_weights.unwrap_or([1.0, 0.0, 0.0, 0.0]))
{
if w != 0.0 {
skin_m += joints
.get(joint as usize)
.copied()
.unwrap_or(Mat4::IDENTITY)
* w;
}
}
pos[i] = skin_m.transform_point3(pos[i]);
nrm[i] = (Mat3::from_mat4(skin_m) * nrm[i]).normalize_or(Vec3::Y);
}
} else {
let normal_matrix = Mat3::from_mat4(*m).inverse().transpose();
for (p, n) in pos.iter_mut().zip(&mut nrm) {
*p = m.transform_point3(*p);
*n = (normal_matrix * *n).normalize_or(Vec3::Y);
}
}
Some((pos, nrm))
}
#[derive(Clone, Copy)]
pub(crate) struct Rect {
pub x0: usize,
pub y0: usize,
pub x1: usize,
pub y1: usize,
}
impl Rect {
fn covering(min: Vec2, max: Vec2, size: Vec2) -> Option<Rect> {
let lo = min.floor().max(Vec2::ZERO);
let hi = max.ceil().min(size);
(lo.x < hi.x && lo.y < hi.y).then_some(Rect {
x0: lo.x as usize,
y0: lo.y as usize,
x1: hi.x as usize,
y1: hi.y as usize,
})
}
pub fn intersect(self, other: Rect) -> Rect {
Rect {
x0: self.x0.max(other.x0),
y0: self.y0.max(other.y0),
x1: self.x1.min(other.x1),
y1: self.y1.min(other.y1),
}
}
}
#[derive(Clone, Copy)]
pub(crate) struct ScreenTri {
pub a: Vec3,
pub b: Vec3,
pub c: Vec3,
pub kx: f32,
pub ky: f32,
pub k0: f32,
pub ox: f32,
pub oy: f32,
pub inv_w: Vec3,
pub v: [u32; 3],
pub draw: u32,
pub clip: Option<Vec3>,
pub bounds: Rect,
pub depth: f32,
}
impl ScreenTri {
#[inline]
pub fn key(&self, x: f32, y: f32) -> f32 {
self.k0 + self.kx * (x - self.ox) + self.ky * (y - self.oy)
}
#[inline]
pub fn bary(&self, x: f32, y: f32) -> Vec3 {
self.a * x + self.b * y + self.c
}
#[inline]
pub fn clipped(&self, x: f32, y: f32) -> bool {
self.clip.is_some_and(|e| self.bary(x, y).dot(e) < 0.0)
}
#[inline]
pub fn perspective_bary(&self, x: f32, y: f32) -> Vec3 {
let p = self.bary(x, y) * self.inv_w;
let sum = p.element_sum();
if sum.abs() < 1e-20 {
return Vec3::splat(1.0 / 3.0);
}
p / sum
}
}
const GUARD: f32 = 32.0;
const PLANES: [Vec4; 6] = [
Vec4::new(0.0, 0.0, 1.0, 0.0),
Vec4::new(-1.0, 0.0, 0.0, GUARD),
Vec4::new(1.0, 0.0, 0.0, GUARD),
Vec4::new(0.0, -1.0, 0.0, GUARD),
Vec4::new(0.0, 1.0, 0.0, GUARD),
Vec4::new(0.0, 0.0, -1.0, 1.0),
];
fn outcode(c: Vec4, guard: f32) -> u8 {
u8::from(c.z < 0.0)
| (u8::from(c.x > c.w * guard) << 1)
| (u8::from(c.x < -c.w * guard) << 2)
| (u8::from(c.y > c.w * guard) << 3)
| (u8::from(c.y < -c.w * guard) << 4)
| (u8::from(c.z > c.w) << 5)
}
pub(crate) struct Viewport {
pub size: Vec2,
pub view_proj: Mat4,
pub ortho: bool,
pub mirrored: bool,
pub clip_plane: Option<Vec4>,
}
pub(crate) fn setup_triangles(
world: &mut World,
viewport: &Viewport,
filter: impl Fn(&Draw) -> bool,
) -> (Vec<ScreenTri>, Vec<ScreenTri>) {
let streams = &mut world.streams;
let mut clip: Vec<Vec4> = streams
.pos
.iter()
.map(|p| viewport.view_proj * p.extend(1.0))
.collect();
let mut opaque = Vec::with_capacity(world.tris.len());
let mut blended = Vec::new();
for tri in &world.tris {
let draw = &world.draws[tri.draw as usize];
if !filter(draw) {
continue;
}
let out = if draw.material.is_blended() {
&mut blended
} else {
&mut opaque
};
let side = draw.material.side;
let c = tri.v.map(|i| clip[i as usize]);
let outside = c.map(|c| outcode(c, 1.0));
if outside[0] & outside[1] & outside[2] != 0 {
continue;
}
let codes = c.map(|c| outcode(c, GUARD));
let crossed = codes[0] | codes[1] | codes[2];
if crossed == 0 {
out.extend(screen_tri(tri.v, c, &streams.pos, tri.draw, side, viewport));
continue;
}
let mut poly: Vec<u32> = tri.v.to_vec();
for (bit, plane) in PLANES.iter().enumerate() {
if crossed & (1 << bit) == 0 {
continue;
}
let mut next = Vec::with_capacity(poly.len() + 2);
for i in 0..poly.len() {
let a = poly[i];
let b = poly[(i + 1) % poly.len()];
let da = plane.dot(clip[a as usize]);
let db = plane.dot(clip[b as usize]);
if da >= 0.0 {
next.push(a);
}
if (da >= 0.0) != (db >= 0.0) {
let t = da / (da - db);
next.push(streams.lerp(a, b, t));
clip.push(clip[a as usize].lerp(clip[b as usize], t));
}
}
poly = next;
if poly.len() < 3 {
break;
}
}
for i in 1..poly.len().saturating_sub(1) {
let v = [poly[0], poly[i], poly[i + 1]];
let c = v.map(|i| clip[i as usize]);
out.extend(screen_tri(v, c, &streams.pos, tri.draw, side, viewport));
}
}
(opaque, blended)
}
fn screen_tri(
v: [u32; 3],
clip: [Vec4; 3],
positions: &[Vec3],
draw: u32,
side: Side,
vp: &Viewport,
) -> Option<ScreenTri> {
let inv_w = Vec3::from_array(clip.map(|c| 1.0 / c.w));
let clip_plane = vp.clip_plane.map(|plane| {
Vec3::from_array(
[0, 1, 2]
.map(|i| (plane.truncate().dot(positions[v[i] as usize]) + plane.w) * inv_w[i]),
)
});
let p = [0, 1, 2].map(|i| {
let ndc = Vec2::new(clip[i].x, clip[i].y) * inv_w[i];
Vec2::new(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5) * vp.size
});
let key = if vp.ortho {
Vec3::ONE - Vec3::new(clip[0].z, clip[1].z, clip[2].z) * inv_w
} else {
inv_w
};
let area2 = (p[1] - p[0]).perp_dot(p[2] - p[0]);
if !area2.is_finite() || area2.abs() < 1e-9 {
return None;
}
let back_facing = (area2 > 0.0) != vp.mirrored;
match side {
Side::Front if back_facing => return None,
Side::Back if !back_facing => return None,
_ => {}
}
let bounds = Rect::covering(p[0].min(p[1]).min(p[2]), p[0].max(p[1]).max(p[2]), vp.size)?;
let inv_area = 1.0 / area2;
let (mut a, mut b, mut c) = (Vec3::ZERO, Vec3::ZERO, Vec3::ZERO);
for i in 0..3 {
let (j, k) = (p[(i + 1) % 3], p[(i + 2) % 3]);
let edge = k - j;
a[i] = -edge.y * inv_area;
b[i] = edge.x * inv_area;
c[i] = j.perp_dot(edge) * inv_area;
}
Some(ScreenTri {
a,
b,
c,
kx: a.y * (key.y - key.x) + a.z * (key.z - key.x),
ky: b.y * (key.y - key.x) + b.z * (key.z - key.x),
k0: key.x,
ox: p[0].x,
oy: p[0].y,
inv_w,
v,
draw,
clip: clip_plane,
bounds,
depth: view_depth(vp, (clip[0] + clip[1] + clip[2]) / 3.0),
})
}
fn view_depth(vp: &Viewport, clip: Vec4) -> f32 {
if vp.ortho { clip.z } else { clip.w }
}
pub(crate) struct ScreenSprite {
pub center: Vec2,
pub z: f32,
pub radius: f32,
pub color: Vec3,
pub opacity: f32,
pub additive: bool,
pub bounds: Rect,
pub depth: f32,
}
pub(crate) fn setup_sprites(
sprites: &[WorldSprite],
camera: &Camera,
vp: &Viewport,
) -> Vec<ScreenSprite> {
let project = |p: Vec3| -> Option<(Vec2, f32, Vec4)> {
let c = vp.view_proj * p.extend(1.0);
if c.w <= 0.0 || c.z < 0.0 || c.z > c.w {
return None;
}
let screen = Vec2::new(c.x / c.w * 0.5 + 0.5, 0.5 - c.y / c.w * 0.5) * vp.size;
let key = if vp.ortho { 1.0 - c.z / c.w } else { 1.0 / c.w };
Some((screen, key, c))
};
let focal = match camera.ortho_height {
Some(h) => vp.size.y / h,
None => vp.size.y / (2.0 * (camera.fov.to_radians() * 0.5).tan()),
};
let project_sprite = |s: &WorldSprite| {
let (center, z, clip) = project(s.position)?;
let radius = if camera.ortho_height.is_some() {
s.size * focal
} else {
s.size * focal / clip.w
};
let drawn = radius.max(0.6);
let energy = (radius / drawn).powi(2);
let opacity = s.opacity * energy;
if opacity <= 1e-5 {
return None;
}
Some(ScreenSprite {
center,
z,
radius: drawn,
color: s.color * energy,
opacity,
additive: s.additive,
bounds: Rect::covering(center - drawn, center + drawn, vp.size)?,
depth: view_depth(vp, clip),
})
};
sprites.iter().filter_map(project_sprite).collect()
}