#![forbid(unsafe_code)]
mod cpu;
#[cfg(target_arch = "wasm32")]
mod cpu_blit;
#[cfg(feature = "hud")]
mod cpu_egui;
mod gpu;
#[cfg(not(target_arch = "wasm32"))]
use std::sync::Arc;
use roxlap_core::kfa_draw::{compose_attachment, solve_kfa_limbs};
use roxlap_core::opticast::OpticastSettings;
use roxlap_core::sky::Sky;
use roxlap_core::Camera;
use roxlap_formats::voxel_clip::frame_at;
use roxlap_scene::Scene;
pub use roxlap_formats::character::{Attachment, Character, MeshRef};
pub use roxlap_formats::kfa::KfaSprite;
pub use roxlap_formats::kv6::Kv6;
pub use roxlap_formats::material::{BlendMode, Material};
pub use roxlap_formats::sprite::Sprite;
pub use roxlap_formats::voxel_clip::{
DecodeError, DecodedClip, LoopMode, StreamingClip, VoxelClip, VoxelFrame,
};
pub use roxlap_gpu::{GpuInitError, GpuRendererSettings, PowerPreference};
pub use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
#[cfg(feature = "hud")]
pub use egui;
use crate::cpu::CpuBackend;
use crate::gpu::GpuBackend;
#[cfg(not(target_arch = "wasm32"))]
pub(crate) type DynDisplay = dyn HasDisplayHandle + Send + Sync + 'static;
#[cfg(not(target_arch = "wasm32"))]
pub(crate) type DynWindow = dyn HasWindowHandle + Send + Sync + 'static;
pub struct SpriteInstanceDesc {
pub model: usize,
pub pos: [f32; 3],
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct SpriteModelId {
pub(crate) slot: u32,
pub(crate) gen: u32,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct SpriteInstanceId {
slot: u32,
gen: u32,
}
#[derive(Default)]
struct DynInstanceMap {
slots: Vec<(u32, Option<u32>)>,
order: Vec<u32>,
free: Vec<u32>,
}
impl DynInstanceMap {
fn alloc(&mut self, dyn_index: u32) -> SpriteInstanceId {
debug_assert_eq!(self.order.len() as u32, dyn_index);
let slot = self.free.pop().unwrap_or_else(|| {
self.slots.push((0, None));
(self.slots.len() - 1) as u32
});
let gen = self.slots[slot as usize].0;
self.slots[slot as usize].1 = Some(dyn_index);
self.order.push(slot);
SpriteInstanceId { slot, gen }
}
fn dyn_index(&self, id: SpriteInstanceId) -> Option<u32> {
let (gen, idx) = *self.slots.get(id.slot as usize)?;
(gen == id.gen).then_some(idx).flatten()
}
fn remove(&mut self, id: SpriteInstanceId, removed: u32, moved: Option<u32>) {
self.slots[id.slot as usize].1 = None;
self.slots[id.slot as usize].0 += 1; self.free.push(id.slot);
if let Some(last) = moved {
let moved_slot = self.order[last as usize];
self.slots[moved_slot as usize].1 = Some(removed);
self.order[removed as usize] = moved_slot;
}
self.order.pop();
}
}
#[derive(Default)]
struct DynModelMap {
slots: Vec<(u32, bool)>,
}
impl DynModelMap {
fn reset(&mut self, n: usize) {
self.slots.clear();
self.slots.resize(n, (0, true));
}
fn alloc(&mut self, model_index: u32) -> SpriteModelId {
debug_assert_eq!(self.slots.len() as u32, model_index);
self.slots.push((0, true));
SpriteModelId {
slot: model_index,
gen: 0,
}
}
fn model_index(&self, id: SpriteModelId) -> Option<usize> {
let (gen, live) = *self.slots.get(id.slot as usize)?;
(gen == id.gen && live).then_some(id.slot as usize)
}
fn remove(&mut self, id: SpriteModelId) -> bool {
let Some(slot) = self.slots.get_mut(id.slot as usize) else {
return false;
};
if slot.0 != id.gen || !slot.1 {
return false;
}
slot.1 = false;
true
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct VoxelClipId {
slot: u32,
gen: u32,
}
#[derive(Default)]
struct DynClipMap {
slots: Vec<(u32, bool)>,
epoch: u32,
}
impl DynClipMap {
fn alloc(&mut self, clip_index: u32) -> VoxelClipId {
debug_assert_eq!(self.slots.len() as u32, clip_index);
self.slots.push((self.epoch, true));
VoxelClipId {
slot: clip_index,
gen: self.epoch,
}
}
fn clip_index(&self, id: VoxelClipId) -> Option<usize> {
let (gen, live) = *self.slots.get(id.slot as usize)?;
(gen == id.gen && live).then_some(id.slot as usize)
}
fn remove(&mut self, id: VoxelClipId) -> bool {
let Some(slot) = self.slots.get_mut(id.slot as usize) else {
return false;
};
if slot.0 != id.gen || !slot.1 {
return false;
}
slot.1 = false;
true
}
fn reset(&mut self) {
self.slots.clear();
self.epoch = self.epoch.wrapping_add(1);
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct CharacterId {
slot: u32,
gen: u32,
}
#[derive(Default)]
struct CharMap {
slots: Vec<(u32, bool)>,
epoch: u32,
}
impl CharMap {
fn alloc(&mut self, index: u32) -> CharacterId {
debug_assert_eq!(self.slots.len() as u32, index);
self.slots.push((self.epoch, true));
CharacterId {
slot: index,
gen: self.epoch,
}
}
fn index(&self, id: CharacterId) -> Option<usize> {
let (gen, live) = *self.slots.get(id.slot as usize)?;
(gen == id.gen && live).then_some(id.slot as usize)
}
fn remove(&mut self, id: CharacterId) -> bool {
let Some(slot) = self.slots.get_mut(id.slot as usize) else {
return false;
};
if slot.0 != id.gen || !slot.1 {
return false;
}
slot.1 = false;
true
}
fn reset(&mut self) {
self.slots.clear();
self.epoch = self.epoch.wrapping_add(1);
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct StreamingClipId {
slot: u32,
gen: u32,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct StreamingInstanceId(SpriteInstanceId);
#[derive(Default)]
struct StreamingClipMap {
slots: Vec<(u32, bool)>,
epoch: u32,
}
impl StreamingClipMap {
fn alloc(&mut self, index: u32) -> StreamingClipId {
debug_assert_eq!(self.slots.len() as u32, index);
self.slots.push((self.epoch, true));
StreamingClipId {
slot: index,
gen: self.epoch,
}
}
fn index(&self, id: StreamingClipId) -> Option<usize> {
let (gen, live) = *self.slots.get(id.slot as usize)?;
(gen == id.gen && live).then_some(id.slot as usize)
}
fn remove(&mut self, id: StreamingClipId) -> bool {
let Some(slot) = self.slots.get_mut(id.slot as usize) else {
return false;
};
if slot.0 != id.gen || !slot.1 {
return false;
}
slot.1 = false;
true
}
fn reset(&mut self) {
self.slots.clear();
self.epoch = self.epoch.wrapping_add(1);
}
}
struct StreamingClipState {
cursor: StreamingClip,
model: SpriteModelId,
dims: [u32; 3],
pivot: [f32; 3],
}
struct ClipClock {
durations: Vec<u32>,
loop_mode: LoopMode,
speed_q8: i32,
clock_ms: f64,
}
impl ClipClock {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn tick(&mut self, dt: f64) -> u32 {
self.clock_ms += dt * 1000.0 * f64::from(self.speed_q8) / 256.0;
frame_at(
&self.durations,
self.loop_mode,
self.clock_ms.max(0.0) as u32,
) as u32
}
}
struct ClipMeta {
dims: [u32; 3],
pivot: [f32; 3],
voxel_world_size: f32,
durations: Vec<u32>,
loop_mode: LoopMode,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ClipMetadata {
pub dims: [u32; 3],
pub pivot: [f32; 3],
pub voxel_world_size: f32,
pub loop_mode: LoopMode,
pub frame_count: usize,
pub durations: Vec<u32>,
pub total_ms: u32,
}
#[derive(Clone, Copy)]
enum PlayerTarget {
Flipbook(SpriteInstanceId),
Streaming(StreamingClipId),
}
struct ClipPlayer {
target: PlayerTarget,
clock: ClipClock,
paused: bool,
}
struct AttachInst {
bone: usize,
local_offset: roxlap_formats::xform::BoneXform,
inst: SpriteInstanceId,
clip: Option<ClipClock>,
}
struct CharInstance {
skeleton: KfaSprite,
attaches: Vec<AttachInst>,
models: Vec<SpriteModelId>,
clips: Vec<VoxelClipId>,
}
#[derive(Clone, Copy, Debug)]
pub struct DynSpriteTransform {
pub pos: [f32; 3],
pub right: [f32; 3],
pub up: [f32; 3],
pub forward: [f32; 3],
}
impl Default for DynSpriteTransform {
fn default() -> Self {
Self {
pos: [0.0, 0.0, 0.0],
right: [1.0, 0.0, 0.0],
up: [0.0, 1.0, 0.0],
forward: [0.0, 0.0, 1.0],
}
}
}
impl DynSpriteTransform {
pub(crate) fn apply_to(self, s: &mut Sprite) {
s.p = self.pos;
s.s = self.right;
s.h = self.up;
s.f = self.forward;
}
}
pub struct SpriteSet {
pub models: Vec<Sprite>,
pub instances: Vec<SpriteInstanceDesc>,
pub carve_model: Option<usize>,
}
pub struct FrameParams<'a> {
pub settings: &'a OpticastSettings,
pub sky_color: u32,
pub sky: Option<&'a Sky>,
pub fog_color: u32,
pub fog_max_scan_dist: i32,
pub treat_z_max_as_air: bool,
pub gpu_mip_scan_dist: f32,
pub gpu_max_outer_steps: u32,
pub gpu_fov_y_rad: f32,
pub draw_sprites: bool,
pub side_shades: [i8; 6],
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct PickHit {
pub world: [f32; 3],
pub grid: roxlap_scene::GridId,
pub voxel: glam::IVec3,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct Ray {
pub origin: glam::DVec3,
pub dir: glam::DVec3,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct Line3 {
pub a: [f64; 3],
pub b: [f64; 3],
pub color: u32,
pub width_px: f32,
pub depth_test: bool,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct ImageId(pub(crate) usize);
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum ImageFacing {
World { u: [f32; 3], v: [f32; 3] },
Billboard { up: [f32; 3] },
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct ImageSprite {
pub image: ImageId,
pub origin: [f32; 3],
pub facing: ImageFacing,
pub size: [f32; 2],
pub tint: u32,
pub alpha_cutoff: f32,
pub depth_test: bool,
pub double_sided: bool,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct QuadDraw {
pub corners: [[f32; 3]; 4],
pub image: ImageId,
pub tint: u32,
pub depth_test: bool,
pub alpha_cutoff: f32,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct ImagePickHit {
pub image: ImageId,
pub uv: [f32; 2],
pub texel: (u32, u32),
pub world: [f32; 3],
pub t: f32,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Backend {
Cpu,
Gpu,
}
pub struct RenderOptions {
pub want_gpu: bool,
pub gpu: GpuRendererSettings,
pub clear_sky: u32,
pub cpu_max_grid_vsid: u32,
pub cpu_render_threads: usize,
}
impl Default for RenderOptions {
fn default() -> Self {
Self {
want_gpu: false,
gpu: GpuRendererSettings::default(),
clear_sky: 0x0099_b3d9,
cpu_max_grid_vsid: 32 * roxlap_scene::CHUNK_SIZE_XY,
cpu_render_threads: 4,
}
}
}
const PICK_DEPTH_BIAS: f32 = 0.5;
fn v_sub(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn v_add(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
fn v_scale(a: [f32; 3], s: f32) -> [f32; 3] {
[a[0] * s, a[1] * s, a[2] * s]
}
fn v_dot(a: [f32; 3], b: [f32; 3]) -> f32 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn v_cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn v_norm(a: [f32; 3]) -> [f32; 3] {
let len = v_dot(a, a).sqrt();
if len < 1e-12 {
a
} else {
v_scale(a, 1.0 / len)
}
}
fn ray_quad_uv(
origin: [f32; 3],
dir: [f32; 3],
corners: &[[f32; 3]; 4],
) -> Option<([f32; 2], f32)> {
let [tl, tr, bl, _br] = *corners;
let ue = v_sub(tr, tl); let ve = v_sub(bl, tl); let n = v_cross(ue, ve);
let denom = v_dot(dir, n);
if denom.abs() < 1e-12 {
return None; }
let t = v_dot(v_sub(tl, origin), n) / denom;
if t <= 1e-6 {
return None; }
let p = v_add(origin, v_scale(dir, t));
let rel = v_sub(p, tl);
let guu = v_dot(ue, ue);
let guv = v_dot(ue, ve);
let gvv = v_dot(ve, ve);
let det = guu * gvv - guv * guv;
if det.abs() < 1e-12 {
return None; }
let wu = v_dot(rel, ue);
let wv = v_dot(rel, ve);
let a = (gvv * wu - guv * wv) / det;
let b = (guu * wv - guv * wu) / det;
if !(0.0..=1.0).contains(&a) || !(0.0..=1.0).contains(&b) {
return None; }
Some(([a, b], t))
}
fn resolve_quad(sprite: &ImageSprite, camera: &Camera) -> Option<QuadDraw> {
let cam_pos = [
camera.pos[0] as f32,
camera.pos[1] as f32,
camera.pos[2] as f32,
];
let cam_fwd = v_norm([
camera.forward[0] as f32,
camera.forward[1] as f32,
camera.forward[2] as f32,
]);
let (u_hat, v_hat) = match sprite.facing {
ImageFacing::World { u, v } => (v_norm(u), v_norm(v)),
ImageFacing::Billboard { up } => {
let mut u_hat = v_norm(v_cross(up, cam_fwd));
if v_dot(u_hat, u_hat) < 1e-12 {
u_hat = v_norm([
camera.right[0] as f32,
camera.right[1] as f32,
camera.right[2] as f32,
]);
}
let mut v_hat = v_norm(v_cross(cam_fwd, u_hat));
if v_dot(v_hat, up) > 0.0 {
v_hat = v_scale(v_hat, -1.0);
}
(u_hat, v_hat)
}
};
let du = v_scale(u_hat, sprite.size[0]);
let dv = v_scale(v_hat, sprite.size[1]);
let tl = sprite.origin;
let tr = v_add(tl, du);
let bl = v_add(tl, dv);
let br = v_add(tr, dv);
if !sprite.double_sided {
if let ImageFacing::World { .. } = sprite.facing {
let normal = v_cross(du, dv);
if v_dot(normal, v_sub(cam_pos, tl)) <= 0.0 {
return None;
}
}
}
Some(QuadDraw {
corners: [tl, tr, bl, br],
image: sprite.image,
tint: sprite.tint,
depth_test: sprite.depth_test,
alpha_cutoff: sprite.alpha_cutoff,
})
}
enum BackendImpl {
Cpu(Box<CpuBackend>),
Gpu(Box<GpuBackend>),
}
pub struct SceneRenderer {
inner: BackendImpl,
dyn_map: DynInstanceMap,
model_map: DynModelMap,
clip_map: DynClipMap,
char_map: CharMap,
char_instances: Vec<CharInstance>,
streaming_map: StreamingClipMap,
streaming_clips: Vec<Option<StreamingClipState>>,
clip_meta: Vec<ClipMeta>,
clip_players: Vec<ClipPlayer>,
}
impl SceneRenderer {
#[cfg(not(target_arch = "wasm32"))]
#[must_use]
pub fn new<W>(window: Arc<W>, size: (u32, u32), opts: &RenderOptions) -> Self
where
W: HasWindowHandle + HasDisplayHandle + Send + Sync + 'static,
{
if opts.want_gpu {
match GpuBackend::new(window.clone(), size, opts) {
Ok(g) => {
return Self {
inner: BackendImpl::Gpu(Box::new(g)),
dyn_map: DynInstanceMap::default(),
model_map: DynModelMap::default(),
clip_map: DynClipMap::default(),
char_map: CharMap::default(),
char_instances: Vec::new(),
streaming_map: StreamingClipMap::default(),
streaming_clips: Vec::new(),
clip_meta: Vec::new(),
clip_players: Vec::new(),
};
}
Err(e) => {
eprintln!(
"roxlap-render: GPU init failed ({e}); falling back to the CPU renderer",
);
}
}
}
Self {
inner: BackendImpl::Cpu(Box::new(CpuBackend::new(window, size, opts))),
dyn_map: DynInstanceMap::default(),
model_map: DynModelMap::default(),
clip_map: DynClipMap::default(),
char_map: CharMap::default(),
char_instances: Vec::new(),
streaming_map: StreamingClipMap::default(),
streaming_clips: Vec::new(),
clip_meta: Vec::new(),
clip_players: Vec::new(),
}
}
#[cfg(target_arch = "wasm32")]
pub async fn new_from_canvas_async(
canvas: web_sys::HtmlCanvasElement,
size: (u32, u32),
opts: &RenderOptions,
) -> Self {
if opts.want_gpu {
match GpuBackend::new_async(canvas.clone(), size, opts).await {
Ok(g) => {
return Self {
inner: BackendImpl::Gpu(Box::new(g)),
dyn_map: DynInstanceMap::default(),
model_map: DynModelMap::default(),
clip_map: DynClipMap::default(),
char_map: CharMap::default(),
char_instances: Vec::new(),
streaming_map: StreamingClipMap::default(),
streaming_clips: Vec::new(),
clip_meta: Vec::new(),
clip_players: Vec::new(),
};
}
Err(e) => {
web_sys::console::warn_1(
&format!("roxlap-render: WebGPU init failed ({e}); using the CPU renderer")
.into(),
);
}
}
}
Self {
inner: BackendImpl::Cpu(Box::new(CpuBackend::new_from_canvas(canvas, size, opts))),
dyn_map: DynInstanceMap::default(),
model_map: DynModelMap::default(),
clip_map: DynClipMap::default(),
char_map: CharMap::default(),
char_instances: Vec::new(),
streaming_map: StreamingClipMap::default(),
streaming_clips: Vec::new(),
clip_meta: Vec::new(),
clip_players: Vec::new(),
}
}
#[must_use]
pub fn backend(&self) -> Backend {
match self.inner {
BackendImpl::Cpu(_) => Backend::Cpu,
BackendImpl::Gpu(_) => Backend::Gpu,
}
}
#[must_use]
pub fn adapter_info(&self) -> Option<&str> {
match &self.inner {
BackendImpl::Gpu(g) => Some(g.adapter_info()),
BackendImpl::Cpu(_) => None,
}
}
pub fn set_sky_panorama(&mut self, rgba: &[u8], w: u32, h: u32) {
if let BackendImpl::Gpu(g) = &mut self.inner {
g.set_sky_panorama(rgba, w, h);
}
}
pub fn resize(&mut self, width: u32, height: u32) {
match &mut self.inner {
BackendImpl::Cpu(c) => c.resize(width, height),
BackendImpl::Gpu(g) => g.resize(width, height),
}
}
pub fn render(&mut self, scene: &mut Scene, camera: &Camera, frame: &FrameParams) {
match &mut self.inner {
BackendImpl::Cpu(c) => c.render(scene, camera, frame),
BackendImpl::Gpu(g) => g.render(scene, camera, frame),
}
}
pub fn draw_lines(&mut self, camera: &Camera, lines: &[Line3]) {
match &mut self.inner {
BackendImpl::Cpu(c) => c.draw_lines(camera, lines),
BackendImpl::Gpu(g) => g.draw_lines(camera, lines),
}
}
pub fn upload_image(&mut self, rgba: &[u8], width: u32, height: u32) -> Option<ImageId> {
if width == 0 || height == 0 || rgba.len() != (width as usize) * (height as usize) * 4 {
return None;
}
Some(match &mut self.inner {
BackendImpl::Cpu(c) => c.upload_image(rgba, width, height),
BackendImpl::Gpu(g) => g.upload_image(rgba, width, height),
})
}
pub fn drop_image(&mut self, id: ImageId) {
match &mut self.inner {
BackendImpl::Cpu(c) => c.drop_image(id),
BackendImpl::Gpu(g) => g.drop_image(id),
}
}
pub fn draw_images(&mut self, camera: &Camera, images: &[ImageSprite]) {
if images.is_empty() {
return;
}
let quads: Vec<QuadDraw> = images
.iter()
.filter_map(|s| resolve_quad(s, camera))
.collect();
if quads.is_empty() {
return;
}
match &mut self.inner {
BackendImpl::Cpu(c) => c.draw_images(camera, &quads),
BackendImpl::Gpu(g) => g.draw_images(camera, &quads),
}
}
#[must_use]
pub fn project_point(&self, camera: &Camera, world: [f32; 3]) -> Option<(f32, f32)> {
match &self.inner {
BackendImpl::Cpu(c) => c.project_point(camera, world),
BackendImpl::Gpu(g) => g.project_point(camera, world),
}
}
#[must_use]
pub fn pick_image(
&self,
camera: &Camera,
x: f64,
y: f64,
sprites: &[ImageSprite],
) -> Option<ImagePickHit> {
if sprites.is_empty() {
return None;
}
let dir = self.pixel_ray(camera, x, y)?;
let dir = [dir[0] as f32, dir[1] as f32, dir[2] as f32];
let dir_len = v_dot(dir, dir).sqrt();
if dir_len < 1e-9 {
return None;
}
let origin = [
camera.pos[0] as f32,
camera.pos[1] as f32,
camera.pos[2] as f32,
];
let scene_t = self.pick_depth(x as u32, y as u32);
let mut best: Option<ImagePickHit> = None;
for sprite in sprites {
let Some(q) = resolve_quad(sprite, camera) else {
continue;
};
let Some(([a, b], t)) = ray_quad_uv(origin, dir, &q.corners) else {
continue; };
let d_eucl = t * dir_len;
if best.is_some_and(|cur| d_eucl >= cur.t) {
continue; }
let p = v_add(origin, v_scale(dir, t));
let Some((iw, ih)) = self.image_dims(sprite.image) else {
continue; };
let tx = ((a * iw as f32) as i32).clamp(0, iw as i32 - 1) as u32;
let ty = ((b * ih as f32) as i32).clamp(0, ih as i32 - 1) as u32;
let cutoff_u8 = (sprite.alpha_cutoff.clamp(0.0, 1.0) * 255.0) as u32;
let solid_thresh = cutoff_u8.max(1);
if u32::from(self.image_alpha_at(sprite.image, tx, ty)) < solid_thresh {
continue;
}
if sprite.depth_test {
if let Some(st) = scene_t {
if d_eucl > st + PICK_DEPTH_BIAS {
continue;
}
}
}
best = Some(ImagePickHit {
image: sprite.image,
uv: [a, b],
texel: (tx, ty),
world: p,
t: d_eucl,
});
}
best
}
fn image_dims(&self, id: ImageId) -> Option<(u32, u32)> {
match &self.inner {
BackendImpl::Cpu(c) => c.image_dims(id),
BackendImpl::Gpu(g) => g.image_dims(id),
}
}
fn image_alpha_at(&self, id: ImageId, tx: u32, ty: u32) -> u8 {
match &self.inner {
BackendImpl::Cpu(c) => c.image_alpha_at(id, tx, ty),
BackendImpl::Gpu(g) => g.image_alpha_at(id, tx, ty),
}
}
pub fn set_flip_x(&mut self, flip: bool) {
match &mut self.inner {
BackendImpl::Cpu(c) => c.set_flip_x(flip),
BackendImpl::Gpu(g) => g.set_flip_x(flip),
}
}
pub fn present(&mut self) {
match &mut self.inner {
BackendImpl::Cpu(c) => c.present(),
BackendImpl::Gpu(g) => g.present(),
}
}
#[cfg(feature = "hud")]
pub fn paint_egui(
&mut self,
jobs: &[egui::ClippedPrimitive],
textures: &egui::TexturesDelta,
pixels_per_point: f32,
) {
match &mut self.inner {
BackendImpl::Cpu(c) => c.paint_egui(jobs, textures, pixels_per_point),
BackendImpl::Gpu(g) => g.paint_egui(jobs, textures, pixels_per_point),
}
}
pub fn set_sprites(&mut self, set: &SpriteSet) -> Vec<SpriteModelId> {
match &mut self.inner {
BackendImpl::Cpu(c) => c.set_sprites(set),
BackendImpl::Gpu(g) => g.set_sprites(set),
}
self.dyn_map = DynInstanceMap::default();
self.model_map.reset(set.models.len());
self.clip_map.reset();
self.char_map.reset();
self.char_instances.clear();
self.streaming_map.reset();
self.streaming_clips.clear();
self.clip_meta.clear();
self.clip_players.clear();
(0..set.models.len() as u32)
.map(|slot| SpriteModelId { slot, gen: 0 })
.collect()
}
pub fn refresh_sprite_model(&mut self, model: SpriteModelId, kv6: &Kv6) {
let Some(idx) = self.model_map.model_index(model) else {
return; };
match &mut self.inner {
BackendImpl::Cpu(c) => c.update_sprite_model(idx, kv6),
BackendImpl::Gpu(g) => g.update_sprite_model(idx, kv6),
}
}
pub fn add_sprite_instance(&mut self, model: SpriteModelId, pos: [f32; 3]) -> SpriteInstanceId {
self.add_sprite_instance_posed(
model,
DynSpriteTransform {
pos,
..DynSpriteTransform::default()
},
)
}
pub fn add_sprite_instance_posed(
&mut self,
model: SpriteModelId,
xf: DynSpriteTransform,
) -> SpriteInstanceId {
let Some(idx) = self.model_map.model_index(model) else {
return SpriteInstanceId {
slot: u32::MAX,
gen: u32::MAX,
};
};
let dyn_index = match &mut self.inner {
BackendImpl::Cpu(c) => c.add_dyn_instance_posed(idx, xf),
BackendImpl::Gpu(g) => g.add_dyn_instance_posed(idx, xf),
};
self.dyn_map.alloc(dyn_index as u32)
}
pub fn remove_sprite_instance(&mut self, id: SpriteInstanceId) -> bool {
let Some(dyn_index) = self.dyn_map.dyn_index(id) else {
return false;
};
let moved = match &mut self.inner {
BackendImpl::Cpu(c) => c.remove_dyn_instance(dyn_index as usize),
BackendImpl::Gpu(g) => g.remove_dyn_instance(dyn_index as usize),
};
self.dyn_map.remove(id, dyn_index, moved.map(|m| m as u32));
true
}
#[must_use]
pub fn dynamic_sprite_count(&self) -> usize {
self.dyn_map.order.len()
}
pub fn define_material(&mut self, id: u8, mat: Material) -> bool {
match &mut self.inner {
BackendImpl::Cpu(c) => c.define_material(id, mat),
BackendImpl::Gpu(g) => g.define_material(id, mat),
}
}
#[must_use]
pub fn material(&self, id: u8) -> Material {
match &self.inner {
BackendImpl::Cpu(c) => c.material(id),
BackendImpl::Gpu(g) => g.material(id),
}
}
pub fn set_terrain_materials(&mut self, map: &[(u32, u8)]) {
match &mut self.inner {
BackendImpl::Cpu(c) => c.set_terrain_materials(map),
BackendImpl::Gpu(g) => g.set_terrain_materials(map),
}
}
pub fn add_sprite_model(&mut self, kv6: &Kv6) -> SpriteModelId {
let model_index = match &mut self.inner {
BackendImpl::Cpu(c) => c.add_model(kv6),
BackendImpl::Gpu(g) => g.add_model(kv6),
};
self.model_map.alloc(model_index as u32)
}
pub fn add_sprite_model_with_materials(
&mut self,
kv6: &Kv6,
material_map: &[(u32, u8)],
) -> SpriteModelId {
let model_index = match &mut self.inner {
BackendImpl::Cpu(c) => c.add_model_with_materials(kv6, material_map),
BackendImpl::Gpu(g) => g.add_model_with_materials(kv6, material_map),
};
self.model_map.alloc(model_index as u32)
}
pub fn remove_sprite_model(&mut self, id: SpriteModelId) -> bool {
let Some(idx) = self.model_map.model_index(id) else {
return false;
};
match &mut self.inner {
BackendImpl::Cpu(c) => c.remove_model(idx),
BackendImpl::Gpu(g) => g.remove_model(idx),
}
self.model_map.remove(id)
}
pub fn compact_sprite_models(&mut self) {
match &mut self.inner {
BackendImpl::Cpu(c) => c.compact_models(),
BackendImpl::Gpu(g) => g.compact_models(),
}
}
pub fn set_sprite_instance_transform(&mut self, id: SpriteInstanceId, xf: DynSpriteTransform) {
let Some(dyn_index) = self.dyn_map.dyn_index(id) else {
return;
};
match &mut self.inner {
BackendImpl::Cpu(c) => c.set_dyn_instance_transform(dyn_index as usize, xf),
BackendImpl::Gpu(g) => g.set_dyn_instance_transform(dyn_index as usize, xf),
}
}
pub fn set_sprite_instance_transforms(
&mut self,
updates: &[(SpriteInstanceId, DynSpriteTransform)],
) {
for &(id, xf) in updates {
let Some(dyn_index) = self.dyn_map.dyn_index(id) else {
continue;
};
match &mut self.inner {
BackendImpl::Cpu(c) => c.set_dyn_instance_transform(dyn_index as usize, xf),
BackendImpl::Gpu(g) => g.set_dyn_instance_transform(dyn_index as usize, xf),
}
}
}
pub fn set_sprite_instance_material(&mut self, id: SpriteInstanceId, material: u8) {
let Some(dyn_index) = self.dyn_map.dyn_index(id) else {
return;
};
match &mut self.inner {
BackendImpl::Cpu(c) => c.set_dyn_instance_material(dyn_index as usize, material),
BackendImpl::Gpu(g) => g.set_dyn_instance_material(dyn_index as usize, material),
}
}
pub fn set_sprite_instance_alpha(&mut self, id: SpriteInstanceId, alpha_mul: u8) {
let Some(dyn_index) = self.dyn_map.dyn_index(id) else {
return;
};
match &mut self.inner {
BackendImpl::Cpu(c) => c.set_dyn_instance_alpha(dyn_index as usize, alpha_mul),
BackendImpl::Gpu(g) => g.set_dyn_instance_alpha(dyn_index as usize, alpha_mul),
}
}
pub fn add_voxel_clip(&mut self, clip: &DecodedClip) -> VoxelClipId {
let clip_index = match &mut self.inner {
BackendImpl::Cpu(c) => c.add_voxel_clip(clip),
BackendImpl::Gpu(g) => g.add_voxel_clip(clip),
};
debug_assert_eq!(clip_index, self.clip_meta.len());
self.clip_meta.push(ClipMeta {
dims: clip.dims,
pivot: clip.pivot,
voxel_world_size: clip.voxel_world_size,
durations: clip.durations.clone(),
loop_mode: clip.loop_mode,
});
self.clip_map.alloc(clip_index as u32)
}
pub fn remove_voxel_clip(&mut self, id: VoxelClipId) -> bool {
let Some(clip_index) = self.clip_map.clip_index(id) else {
return false;
};
match &mut self.inner {
BackendImpl::Cpu(c) => c.remove_voxel_clip(clip_index),
BackendImpl::Gpu(g) => g.remove_voxel_clip(clip_index),
}
self.clip_map.remove(id)
}
pub fn add_clip_instance_posed(
&mut self,
clip: VoxelClipId,
xf: DynSpriteTransform,
) -> SpriteInstanceId {
let Some(clip_index) = self.clip_map.clip_index(clip) else {
return SpriteInstanceId {
slot: u32::MAX,
gen: u32::MAX,
};
};
let dyn_index = match &mut self.inner {
BackendImpl::Cpu(c) => c.add_clip_instance(clip_index, xf),
BackendImpl::Gpu(g) => g.add_clip_instance(clip_index, xf),
};
self.dyn_map.alloc(dyn_index as u32)
}
pub fn set_clip_instance_frame(&mut self, id: SpriteInstanceId, frame: u32) {
let Some(dyn_index) = self.dyn_map.dyn_index(id) else {
return;
};
match &mut self.inner {
BackendImpl::Cpu(c) => c.set_clip_frame(dyn_index as usize, frame as usize),
BackendImpl::Gpu(g) => g.set_clip_frame(dyn_index as usize, frame as usize),
}
}
#[must_use]
pub fn clip_frame_count(&self, id: VoxelClipId) -> Option<usize> {
let idx = self.clip_map.clip_index(id)?;
Some(self.clip_meta[idx].durations.len())
}
#[must_use]
pub fn clip_metadata(&self, id: VoxelClipId) -> Option<ClipMetadata> {
let idx = self.clip_map.clip_index(id)?;
let m = &self.clip_meta[idx];
Some(ClipMetadata {
dims: m.dims,
pivot: m.pivot,
voxel_world_size: m.voxel_world_size,
loop_mode: m.loop_mode,
frame_count: m.durations.len(),
durations: m.durations.clone(),
total_ms: m
.durations
.iter()
.fold(0u32, |acc, &d| acc.saturating_add(d)),
})
}
#[must_use]
pub fn get_clip_instance_frame(&self, id: SpriteInstanceId) -> Option<u32> {
let dyn_index = self.dyn_map.dyn_index(id)? as usize;
let frame = match &self.inner {
BackendImpl::Cpu(c) => c.clip_instance_frame(dyn_index),
BackendImpl::Gpu(g) => g.clip_instance_frame(dyn_index),
}?;
u32::try_from(frame).ok()
}
pub fn update_clip_frame(&mut self, id: VoxelClipId, frame: u32, vf: &VoxelFrame) -> bool {
let Some(clip_index) = self.clip_map.clip_index(id) else {
return false;
};
let m = &self.clip_meta[clip_index];
let (dims, pivot, vws) = (m.dims, m.pivot, m.voxel_world_size);
if vf.validate(dims).is_err() {
return false;
}
let frame = frame as usize;
match &mut self.inner {
BackendImpl::Cpu(c) => c.update_clip_frame(clip_index, frame, vf, dims, pivot),
BackendImpl::Gpu(g) => g.update_clip_frame(clip_index, frame, vf, dims, pivot, vws),
}
}
pub fn add_streaming_clip(&mut self, clip: &VoxelClip) -> Result<StreamingClipId, DecodeError> {
let cursor = StreamingClip::new(clip)?;
let dims = cursor.dims();
let pivot = cursor.pivot();
let kv6 = cursor.current_frame().to_kv6(dims, pivot);
let model = self.add_sprite_model(&kv6);
let index = self.streaming_clips.len() as u32;
self.streaming_clips.push(Some(StreamingClipState {
cursor,
model,
dims,
pivot,
}));
Ok(self.streaming_map.alloc(index))
}
pub fn add_streaming_clip_instance(
&mut self,
id: StreamingClipId,
xf: DynSpriteTransform,
) -> StreamingInstanceId {
let model = self
.streaming_map
.index(id)
.and_then(|idx| self.streaming_clips[idx].as_ref())
.map(|s| s.model);
let inst = match model {
Some(model) => self.add_sprite_instance_posed(model, xf),
None => SpriteInstanceId {
slot: u32::MAX,
gen: u32::MAX,
},
};
StreamingInstanceId(inst)
}
pub fn set_streaming_instance_transform(
&mut self,
id: StreamingInstanceId,
xf: DynSpriteTransform,
) {
self.set_sprite_instance_transform(id.0, xf);
}
pub fn remove_streaming_instance(&mut self, id: StreamingInstanceId) -> bool {
self.remove_sprite_instance(id.0)
}
pub fn set_streaming_clip_frame(&mut self, id: StreamingClipId, frame: u32) {
let Some(idx) = self.streaming_map.index(id) else {
return;
};
let Some((model, kv6)) = self.streaming_clips[idx].as_mut().and_then(|s| {
let vf = s.cursor.seek(frame as usize).ok()?;
Some((s.model, vf.to_kv6(s.dims, s.pivot)))
}) else {
return;
};
self.refresh_sprite_model(model, &kv6);
}
pub fn remove_streaming_clip(&mut self, id: StreamingClipId) -> bool {
let Some(idx) = self.streaming_map.index(id) else {
return false;
};
let model = self.streaming_clips[idx].as_ref().map(|s| s.model);
self.streaming_clips[idx] = None;
if let Some(model) = model {
self.remove_sprite_model(model);
}
self.streaming_map.remove(id)
}
pub fn add_clip_instance_playing(
&mut self,
clip: VoxelClipId,
xf: DynSpriteTransform,
speed_q8: i32,
start_phase_ms: u32,
) -> SpriteInstanceId {
let Some(clip_index) = self.clip_map.clip_index(clip) else {
return SpriteInstanceId {
slot: u32::MAX,
gen: u32::MAX,
};
};
let meta = &self.clip_meta[clip_index];
let clock = ClipClock {
durations: meta.durations.clone(),
loop_mode: meta.loop_mode,
speed_q8,
clock_ms: f64::from(start_phase_ms),
};
let inst = self.add_clip_instance_posed(clip, xf);
self.clip_players.push(ClipPlayer {
target: PlayerTarget::Flipbook(inst),
clock,
paused: false,
});
inst
}
pub fn play_streaming_clip(
&mut self,
clip: StreamingClipId,
speed_q8: i32,
start_phase_ms: u32,
) {
let Some(idx) = self.streaming_map.index(clip) else {
return;
};
let Some(state) = self.streaming_clips[idx].as_ref() else {
return;
};
let clock = ClipClock {
durations: state.cursor.durations().to_vec(),
loop_mode: state.cursor.loop_mode(),
speed_q8,
clock_ms: f64::from(start_phase_ms),
};
self.clip_players.push(ClipPlayer {
target: PlayerTarget::Streaming(clip),
clock,
paused: false,
});
}
pub fn advance_voxel_clips(&mut self, dt: f64) {
let dyn_map = &self.dyn_map;
let streaming_map = &self.streaming_map;
let mut updates: Vec<(PlayerTarget, u32)> = Vec::new();
self.clip_players.retain_mut(|p| {
let alive = match p.target {
PlayerTarget::Flipbook(inst) => dyn_map.dyn_index(inst).is_some(),
PlayerTarget::Streaming(clip) => streaming_map.index(clip).is_some(),
};
if !alive {
return false;
}
if !p.paused {
updates.push((p.target, p.clock.tick(dt)));
}
true
});
for (target, frame) in updates {
self.apply_player_frame(target, frame);
}
}
fn apply_player_frame(&mut self, target: PlayerTarget, frame: u32) {
match target {
PlayerTarget::Flipbook(inst) => self.set_clip_instance_frame(inst, frame),
PlayerTarget::Streaming(clip) => self.set_streaming_clip_frame(clip, frame),
}
}
fn flipbook_player_mut(&mut self, inst: SpriteInstanceId) -> Option<&mut ClipPlayer> {
self.clip_players
.iter_mut()
.find(|p| matches!(p.target, PlayerTarget::Flipbook(i) if i == inst))
}
pub fn set_clip_instance_paused(&mut self, id: SpriteInstanceId, paused: bool) {
if let Some(p) = self.flipbook_player_mut(id) {
p.paused = paused;
}
}
#[must_use]
pub fn is_clip_instance_paused(&self, id: SpriteInstanceId) -> Option<bool> {
self.clip_players
.iter()
.find(|p| matches!(p.target, PlayerTarget::Flipbook(i) if i == id))
.map(|p| p.paused)
}
pub fn set_clip_instance_speed(&mut self, id: SpriteInstanceId, speed_q8: i32) {
if let Some(p) = self.flipbook_player_mut(id) {
p.clock.speed_q8 = speed_q8;
}
}
pub fn set_clip_instance_clock_ms(&mut self, id: SpriteInstanceId, clock_ms: f64) {
let Some((target, frame)) = self.flipbook_player_mut(id).map(|p| {
p.clock.clock_ms = clock_ms;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let frame = frame_at(
&p.clock.durations,
p.clock.loop_mode,
clock_ms.max(0.0) as u32,
) as u32;
(p.target, frame)
}) else {
return;
};
self.apply_player_frame(target, frame);
}
#[must_use]
pub fn clip_instance_clock_ms(&self, id: SpriteInstanceId) -> Option<f64> {
self.clip_players
.iter()
.find(|p| matches!(p.target, PlayerTarget::Flipbook(i) if i == id))
.map(|p| p.clock.clock_ms)
}
fn streaming_player_mut(&mut self, clip: StreamingClipId) -> Option<&mut ClipPlayer> {
self.clip_players
.iter_mut()
.find(|p| matches!(p.target, PlayerTarget::Streaming(c) if c == clip))
}
pub fn set_streaming_clip_paused(&mut self, clip: StreamingClipId, paused: bool) {
if let Some(p) = self.streaming_player_mut(clip) {
p.paused = paused;
}
}
#[must_use]
pub fn is_streaming_clip_paused(&self, clip: StreamingClipId) -> Option<bool> {
self.clip_players
.iter()
.find(|p| matches!(p.target, PlayerTarget::Streaming(c) if c == clip))
.map(|p| p.paused)
}
pub fn set_streaming_clip_speed(&mut self, clip: StreamingClipId, speed_q8: i32) {
if let Some(p) = self.streaming_player_mut(clip) {
p.clock.speed_q8 = speed_q8;
}
}
pub fn set_streaming_clip_clock_ms(&mut self, clip: StreamingClipId, clock_ms: f64) {
let Some((target, frame)) = self.streaming_player_mut(clip).map(|p| {
p.clock.clock_ms = clock_ms;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let frame = frame_at(
&p.clock.durations,
p.clock.loop_mode,
clock_ms.max(0.0) as u32,
) as u32;
(p.target, frame)
}) else {
return;
};
self.apply_player_frame(target, frame);
}
#[must_use]
pub fn streaming_clip_clock_ms(&self, clip: StreamingClipId) -> Option<f64> {
self.clip_players
.iter()
.find(|p| matches!(p.target, PlayerTarget::Streaming(c) if c == clip))
.map(|p| p.clock.clock_ms)
}
pub fn add_character(&mut self, ch: &Character, clip: Option<usize>) -> CharacterId {
let model_ids: Vec<SpriteModelId> =
ch.meshes.iter().map(|m| self.add_sprite_model(m)).collect();
let clip_regs: Vec<Option<(VoxelClipId, Vec<u32>, LoopMode)>> = ch
.voxel_clips
.iter()
.map(|vc| {
vc.decode().ok().map(|d| {
let id = self.add_voxel_clip(&d);
(id, d.durations, d.loop_mode)
})
})
.collect();
let mut skeleton = ch.to_kfa_sprite(clip);
solve_kfa_limbs(&mut skeleton);
let mut attaches = Vec::new();
for (bi, bone) in ch.bones.iter().enumerate() {
let limb = &skeleton.limbs[bi];
for att in &bone.attachments {
let (s, h, f, p) =
compose_attachment(limb.s, limb.h, limb.f, limb.p, &att.local_offset);
let xf = DynSpriteTransform {
pos: p,
right: s,
up: h,
forward: f,
};
match att.target {
MeshRef::Static(mi) => {
if let Some(&mid) = model_ids.get(mi) {
let inst = self.add_sprite_instance_posed(mid, xf);
attaches.push(AttachInst {
bone: bi,
local_offset: att.local_offset,
inst,
clip: None,
});
}
}
MeshRef::Clip(ci) => {
if let Some(Some((cid, durations, loop_mode))) = clip_regs.get(ci) {
let inst = self.add_clip_instance_posed(*cid, xf);
attaches.push(AttachInst {
bone: bi,
local_offset: att.local_offset,
inst,
clip: Some(ClipClock {
durations: durations.clone(),
loop_mode: *loop_mode,
speed_q8: att.playback.speed_q8,
clock_ms: f64::from(att.playback.start_phase_ms),
}),
});
}
}
}
}
}
let clips: Vec<VoxelClipId> = clip_regs
.iter()
.filter_map(|r| r.as_ref().map(|(cid, _, _)| *cid))
.collect();
let idx = self.char_instances.len();
self.char_instances.push(CharInstance {
skeleton,
attaches,
models: model_ids,
clips,
});
self.char_map.alloc(idx as u32)
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn advance_character(&mut self, id: CharacterId, dt: f64) {
let Some(idx) = self.char_map.index(id) else {
return;
};
let updates: Vec<(SpriteInstanceId, DynSpriteTransform, Option<u32>)> = {
let CharInstance {
skeleton, attaches, ..
} = &mut self.char_instances[idx];
skeleton.animsprite((dt * 1000.0) as i32);
solve_kfa_limbs(skeleton);
attaches
.iter_mut()
.map(|a| {
let limb = &skeleton.limbs[a.bone];
let (s, h, f, p) =
compose_attachment(limb.s, limb.h, limb.f, limb.p, &a.local_offset);
let xf = DynSpriteTransform {
pos: p,
right: s,
up: h,
forward: f,
};
let frame = a.clip.as_mut().map(|c| c.tick(dt));
(a.inst, xf, frame)
})
.collect()
};
for (inst, xf, frame) in updates {
self.set_sprite_instance_transform(inst, xf);
if let Some(f) = frame {
self.set_clip_instance_frame(inst, f);
}
}
}
pub fn set_character_world_transform(&mut self, id: CharacterId, xf: DynSpriteTransform) {
let Some(idx) = self.char_map.index(id) else {
return;
};
let updates: Vec<(SpriteInstanceId, DynSpriteTransform)> = {
let CharInstance {
skeleton, attaches, ..
} = &mut self.char_instances[idx];
skeleton.p = xf.pos;
skeleton.s = xf.right;
skeleton.h = xf.up;
skeleton.f = xf.forward;
solve_kfa_limbs(skeleton);
attaches
.iter()
.map(|a| {
let limb = &skeleton.limbs[a.bone];
let (s, h, f, p) =
compose_attachment(limb.s, limb.h, limb.f, limb.p, &a.local_offset);
(
a.inst,
DynSpriteTransform {
pos: p,
right: s,
up: h,
forward: f,
},
)
})
.collect()
};
for (inst, t) in updates {
self.set_sprite_instance_transform(inst, t);
}
}
pub fn remove_character(&mut self, id: CharacterId) -> bool {
let Some(idx) = self.char_map.index(id) else {
return false;
};
let insts: Vec<SpriteInstanceId> = self.char_instances[idx]
.attaches
.iter()
.map(|a| a.inst)
.collect();
for inst in insts {
self.remove_sprite_instance(inst);
}
self.char_instances[idx].attaches.clear();
let models = std::mem::take(&mut self.char_instances[idx].models);
let clips = std::mem::take(&mut self.char_instances[idx].clips);
for model in models {
self.remove_sprite_model(model);
}
for clip in clips {
self.remove_voxel_clip(clip);
}
self.char_map.remove(id)
}
pub fn set_kfa_sprites(&mut self, kfas: &mut [KfaSprite]) {
match &mut self.inner {
BackendImpl::Cpu(c) => c.set_kfa_sprites(kfas),
BackendImpl::Gpu(g) => g.set_kfa_sprites(kfas),
}
}
pub fn update_kfa_poses(&mut self, kfas: &mut [KfaSprite]) {
match &mut self.inner {
BackendImpl::Cpu(c) => c.update_kfa_poses(kfas),
BackendImpl::Gpu(g) => g.update_kfa_poses(kfas),
}
}
pub fn carve_active_sprite(&mut self) -> u32 {
match &mut self.inner {
BackendImpl::Cpu(_) => 0,
BackendImpl::Gpu(g) => g.carve_active_sprite(),
}
}
pub fn request_capture(&mut self) {
if let BackendImpl::Cpu(c) = &mut self.inner {
c.request_capture();
}
}
pub fn take_capture(&mut self) -> Option<(Vec<u32>, u32, u32)> {
match &mut self.inner {
BackendImpl::Cpu(c) => c.take_capture(),
BackendImpl::Gpu(_) => None,
}
}
#[must_use]
pub fn pick_depth(&self, x: u32, y: u32) -> Option<f32> {
match &self.inner {
BackendImpl::Cpu(c) => c.pick_depth(x, y),
BackendImpl::Gpu(g) => g.pick_depth(x, y),
}
}
#[must_use]
pub fn pixel_ray(&self, camera: &Camera, x: f64, y: f64) -> Option<[f64; 3]> {
match &self.inner {
BackendImpl::Cpu(c) => c.pixel_ray(camera, x, y),
BackendImpl::Gpu(g) => g.pixel_ray(camera, x, y),
}
}
#[must_use]
pub fn view_ray(&self, camera: &Camera, x: f64, y: f64) -> Option<Ray> {
let d = self.pixel_ray(camera, x, y)?;
let len = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
if len < 1e-12 {
return None;
}
Some(Ray {
origin: glam::DVec3::from_array([camera.pos[0], camera.pos[1], camera.pos[2]]),
dir: glam::DVec3::new(d[0] / len, d[1] / len, d[2] / len),
})
}
#[must_use]
pub fn pick(&self, scene: &Scene, camera: &Camera, x: u32, y: u32) -> Option<PickHit> {
let dir = self.pixel_ray(camera, f64::from(x), f64::from(y))?;
let t = f64::from(self.pick_depth(x, y)?);
let len = (dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]).sqrt();
if len < 1e-9 {
return None;
}
let s = t / len; let world = glam::DVec3::new(
camera.pos[0] + dir[0] * s,
camera.pos[1] + dir[1] * s,
camera.pos[2] + dir[2] * s,
);
let (grid, voxel) = scene.resolve_voxel(world, glam::DVec3::from_array(dir))?;
#[allow(clippy::cast_possible_truncation)]
let world_f32 = [world.x as f32, world.y as f32, world.z as f32];
Some(PickHit {
world: world_f32,
grid,
voxel,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dyn_instance_map_survives_swap_removes() {
let mut map = DynInstanceMap::default();
let mut backend: Vec<u32> = Vec::new();
let mut expect: Vec<(SpriteInstanceId, u32)> = Vec::new();
let add = |map: &mut DynInstanceMap,
backend: &mut Vec<u32>,
expect: &mut Vec<(SpriteInstanceId, u32)>,
payload: u32| {
let dyn_index = backend.len() as u32;
backend.push(payload);
let id = map.alloc(dyn_index);
expect.push((id, payload));
};
for p in 0..6 {
add(&mut map, &mut backend, &mut expect, p);
}
for victim_payload in [2u32, 4, 5] {
let pos = expect
.iter()
.position(|&(_, p)| p == victim_payload)
.unwrap();
let (id, _) = expect.remove(pos);
let dyn_index = map.dyn_index(id).expect("live handle resolves");
let last = backend.len() - 1;
backend.swap_remove(dyn_index as usize);
let moved = (dyn_index as usize != last).then_some(last as u32);
map.remove(id, dyn_index, moved);
assert!(map.dyn_index(id).is_none(), "removed handle is stale");
}
for &(id, payload) in &expect {
let idx = map.dyn_index(id).expect("survivor resolves");
assert_eq!(
backend[idx as usize], payload,
"handle addresses its payload"
);
}
assert_eq!(map.order.len(), backend.len());
assert_eq!(backend.len(), expect.len());
}
#[test]
fn dyn_model_map_lifecycle() {
let mut map = DynModelMap::default();
map.reset(3);
let ids: Vec<SpriteModelId> = (0..3).map(|s| SpriteModelId { slot: s, gen: 0 }).collect();
for (i, &id) in ids.iter().enumerate() {
assert_eq!(map.model_index(id), Some(i));
}
let extra = map.alloc(3);
assert_eq!(extra, SpriteModelId { slot: 3, gen: 0 });
assert_eq!(map.model_index(extra), Some(3));
assert!(map.remove(ids[1]));
assert_eq!(map.model_index(ids[1]), None);
assert_eq!(map.model_index(ids[0]), Some(0));
assert_eq!(map.model_index(ids[2]), Some(2));
assert_eq!(map.model_index(extra), Some(3));
assert!(!map.remove(ids[1]));
let bogus = SpriteModelId { slot: 999, gen: 0 };
assert_eq!(map.model_index(bogus), None);
assert!(!map.remove(bogus));
let wrong_gen = SpriteModelId { slot: 0, gen: 7 };
assert_eq!(map.model_index(wrong_gen), None);
}
#[test]
fn dyn_clip_map_lifecycle() {
let mut map = DynClipMap::default();
let c0 = map.alloc(0);
let c1 = map.alloc(1);
assert_eq!(c0, VoxelClipId { slot: 0, gen: 0 });
assert_eq!(map.clip_index(c0), Some(0));
assert_eq!(map.clip_index(c1), Some(1));
assert!(map.remove(c0));
assert_eq!(map.clip_index(c0), None);
assert_eq!(map.clip_index(c1), Some(1));
assert!(!map.remove(c0));
assert!(!map.remove(VoxelClipId { slot: 99, gen: 0 }));
assert_eq!(map.clip_index(VoxelClipId { slot: 1, gen: 5 }), None);
map.reset();
assert_eq!(map.clip_index(c1), None, "reset invalidates old handles");
let again = map.alloc(0); assert_eq!(again, VoxelClipId { slot: 0, gen: 1 });
assert_eq!(map.clip_index(again), Some(0));
assert_eq!(
map.clip_index(c0),
None,
"a pre-reset handle must not alias a new clip on the same slot"
);
}
#[test]
fn char_map_lifecycle() {
let mut map = CharMap::default();
let a = map.alloc(0);
let b = map.alloc(1);
assert_eq!(a, CharacterId { slot: 0, gen: 0 });
assert_eq!(map.index(a), Some(0));
assert_eq!(map.index(b), Some(1));
assert!(map.remove(a));
assert_eq!(map.index(a), None);
assert_eq!(map.index(b), Some(1));
assert!(!map.remove(a)); assert!(!map.remove(CharacterId { slot: 9, gen: 0 }));
assert_eq!(map.index(CharacterId { slot: 1, gen: 7 }), None);
map.reset();
assert_eq!(map.index(b), None);
assert_eq!(map.alloc(0), CharacterId { slot: 0, gen: 1 });
assert_eq!(map.index(a), None, "pre-reset handle must not alias slot 0");
}
#[test]
fn streaming_clip_map_lifecycle() {
let mut map = StreamingClipMap::default();
let a = map.alloc(0);
let b = map.alloc(1);
assert_eq!(a, StreamingClipId { slot: 0, gen: 0 });
assert_eq!(map.index(a), Some(0));
assert_eq!(map.index(b), Some(1));
assert!(map.remove(a));
assert_eq!(map.index(a), None);
assert_eq!(map.index(b), Some(1));
assert!(!map.remove(a)); assert!(!map.remove(StreamingClipId { slot: 9, gen: 0 }));
assert_eq!(map.index(StreamingClipId { slot: 1, gen: 7 }), None);
map.reset();
assert_eq!(map.index(b), None);
assert_eq!(map.alloc(0), StreamingClipId { slot: 0, gen: 1 });
assert_eq!(map.index(a), None, "pre-reset handle must not alias slot 0");
}
#[test]
fn clip_clock_tick_advances_and_resolves_frames() {
let mut c = ClipClock {
durations: vec![100, 100, 100],
loop_mode: LoopMode::Loop,
speed_q8: 256, clock_ms: 0.0,
};
assert_eq!(c.tick(0.0), 0); assert_eq!(c.tick(0.10), 1); assert_eq!(c.clock_ms as u32, 100);
assert_eq!(c.tick(0.15), 2); assert_eq!(c.tick(0.10), 0); let mut slow = ClipClock {
durations: vec![100, 100],
loop_mode: LoopMode::Once,
speed_q8: 128, clock_ms: 0.0,
};
assert_eq!(slow.tick(0.20), 1); assert!((slow.clock_ms - 100.0).abs() < 1e-6);
let mut phased = ClipClock {
durations: vec![50, 50, 50],
loop_mode: LoopMode::Loop,
speed_q8: -256, clock_ms: 50.0, };
assert_eq!(phased.tick(0.10), 0); assert!(phased.clock_ms < 0.0); }
#[test]
fn dyn_sprite_transform_default_is_identity_and_applies() {
let xf = DynSpriteTransform::default();
assert_eq!(xf.pos, [0.0, 0.0, 0.0]);
assert_eq!(xf.right, [1.0, 0.0, 0.0]);
assert_eq!(xf.up, [0.0, 1.0, 0.0]);
assert_eq!(xf.forward, [0.0, 0.0, 1.0]);
let mut s = Sprite::axis_aligned(
roxlap_formats::kv6::Kv6::solid_cube(2, 0x80_FF_FF_FF),
[9.0, 9.0, 9.0],
);
let posed = DynSpriteTransform {
pos: [1.0, 2.0, 3.0],
right: [0.0, 0.0, 1.0],
up: [0.0, 1.0, 0.0],
forward: [1.0, 0.0, 0.0],
};
posed.apply_to(&mut s);
assert_eq!(s.p, [1.0, 2.0, 3.0]);
assert_eq!(s.s, [0.0, 0.0, 1.0]);
assert_eq!(s.h, [0.0, 1.0, 0.0]);
assert_eq!(s.f, [1.0, 0.0, 0.0]);
}
#[test]
fn options_default_is_cpu_intent() {
let o = RenderOptions::default();
assert!(!o.want_gpu);
assert_eq!(o.clear_sky & 0xFF00_0000, 0, "clear_sky is 0x00RRGGBB");
}
fn cam_looking_y() -> Camera {
Camera {
pos: [0.0, 0.0, 0.0],
right: [1.0, 0.0, 0.0],
down: [0.0, 0.0, 1.0],
forward: [0.0, 1.0, 0.0],
}
}
#[test]
fn world_quad_corner_layout() {
let sprite = ImageSprite {
image: ImageId(0),
origin: [-5.0, 10.0, -5.0],
facing: ImageFacing::World {
u: [1.0, 0.0, 0.0],
v: [0.0, 0.0, 1.0],
},
size: [10.0, 10.0],
tint: 0xFFFF_FFFF,
alpha_cutoff: 0.0,
depth_test: true,
double_sided: true,
};
let q = resolve_quad(&sprite, &cam_looking_y()).expect("front-facing");
assert_eq!(q.corners[0], [-5.0, 10.0, -5.0], "TL = origin");
assert_eq!(q.corners[1], [5.0, 10.0, -5.0], "TR = origin + u·size");
assert_eq!(q.corners[2], [-5.0, 10.0, 5.0], "BL = origin + v·size");
assert_eq!(q.corners[3], [5.0, 10.0, 5.0], "BR = origin + u + v");
}
#[test]
fn world_quad_backface_culls_when_single_sided() {
let sprite = ImageSprite {
image: ImageId(0),
origin: [-5.0, 10.0, -5.0],
facing: ImageFacing::World {
u: [0.0, 0.0, 1.0], v: [1.0, 0.0, 0.0], },
size: [10.0, 10.0],
tint: 0xFFFF_FFFF,
alpha_cutoff: 0.0,
depth_test: true,
double_sided: false,
};
let a = resolve_quad(&sprite, &cam_looking_y()).is_some();
let mut flipped = sprite;
flipped.facing = ImageFacing::World {
u: [1.0, 0.0, 0.0],
v: [0.0, 0.0, 1.0],
};
let b = resolve_quad(&flipped, &cam_looking_y()).is_some();
assert!(a ^ b, "exactly one winding is front-facing");
}
#[test]
fn double_sided_never_culls() {
let mut sprite = ImageSprite {
image: ImageId(0),
origin: [-5.0, 10.0, -5.0],
facing: ImageFacing::World {
u: [0.0, 0.0, 1.0],
v: [1.0, 0.0, 0.0],
},
size: [10.0, 10.0],
tint: 0xFFFF_FFFF,
alpha_cutoff: 0.0,
depth_test: true,
double_sided: true,
};
assert!(resolve_quad(&sprite, &cam_looking_y()).is_some());
sprite.facing = ImageFacing::World {
u: [1.0, 0.0, 0.0],
v: [0.0, 0.0, 1.0],
};
assert!(resolve_quad(&sprite, &cam_looking_y()).is_some());
}
#[test]
fn ray_quad_uv_center_and_corners() {
let corners = [
[-5.0, 10.0, -5.0], [5.0, 10.0, -5.0], [-5.0, 10.0, 5.0], [5.0, 10.0, 5.0], ];
let (uv, t) = ray_quad_uv([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], &corners).expect("center hit");
assert!(
(uv[0] - 0.5).abs() < 1e-5 && (uv[1] - 0.5).abs() < 1e-5,
"centre → (.5,.5)"
);
assert!((t - 10.0).abs() < 1e-4, "t = plane distance");
let (uv_tl, _) = ray_quad_uv([0.0, 0.0, 0.0], [-4.0, 10.0, -4.0], &corners).unwrap();
assert!(uv_tl[0] < 0.2 && uv_tl[1] < 0.2, "toward TL → small uv");
}
#[test]
fn ray_quad_uv_misses_outside_and_behind() {
let corners = [
[-5.0, 10.0, -5.0],
[5.0, 10.0, -5.0],
[-5.0, 10.0, 5.0],
[5.0, 10.0, 5.0],
];
assert!(ray_quad_uv([0.0, 0.0, 0.0], [0.0, -1.0, 0.0], &corners).is_none());
assert!(ray_quad_uv([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], &corners).is_none());
assert!(ray_quad_uv([100.0, 0.0, 0.0], [0.0, 1.0, 0.0], &corners).is_none());
}
#[test]
fn billboard_axes_orthogonal_and_top_toward_up() {
let up = [0.0, 0.0, -1.0];
let sprite = ImageSprite {
image: ImageId(0),
origin: [0.0, 50.0, 0.0],
facing: ImageFacing::Billboard { up },
size: [4.0, 4.0],
tint: 0xFFFF_FFFF,
alpha_cutoff: 0.0,
depth_test: false,
double_sided: false, };
let q = resolve_quad(&sprite, &cam_looking_y()).expect("billboard always faces camera");
let u = v_sub(q.corners[1], q.corners[0]); let v = v_sub(q.corners[2], q.corners[0]); let fwd = [0.0, 1.0, 0.0];
assert!(v_dot(u, fwd).abs() < 1e-5, "u ⟂ view");
assert!(v_dot(v, fwd).abs() < 1e-5, "v ⟂ view");
assert!(v_dot(u, v).abs() < 1e-5, "u ⟂ v");
assert!(
v_dot(v, up) < 0.0,
"rows grow away from `up` (top edge toward up)"
);
}
}