use std::sync::Arc;
use std::time::Instant;
use glam::{DVec3, IVec3};
use roxlap_core::opticast::OpticastSettings;
use roxlap_core::Camera;
use roxlap_render::{
BackendPreference, BillboardMode, DynSpriteTransform, FrameParams, Kv6, LoopMode,
RenderOptions, Rgb, SceneRenderer, SpriteInstanceId, VoxColor, VoxelClip,
};
use roxlap_scene::{GridTransform, Scene};
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::window::{Window, WindowId};
const GRASS: VoxColor = VoxColor(0x80_4d_8a_3a);
const GEM: VoxColor = VoxColor(0x80_c8_50_d0);
const ORB: VoxColor = VoxColor(0x80_d0_a0_50);
const SKY: Rgb = Rgb(0x00_8f_bc_d4);
fn build_scene() -> Scene {
let mut scene = Scene::new();
let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
let grid = scene.grid_mut(id).expect("grid just added");
grid.set_rect(
IVec3::new(-128, -128, 210),
IVec3::new(127, 127, 254),
Some(GRASS),
);
scene
}
fn gem_kv6() -> Kv6 {
const DIM: u32 = 14;
let c = (DIM as f32 - 1.0) * 0.5;
Kv6::from_fn(DIM, DIM, DIM, |x, y, z| {
let (dx, dy, dz) = (x as f32 - c, y as f32 - c, z as f32 - c);
let round = dx * dx + dy * dy + dz * dz <= (c * 0.98).powi(2);
let cut = dx.abs() + dy.abs() + dz.abs() <= c * 1.35;
(round && cut).then_some(GEM)
})
}
fn orb_clip() -> VoxelClip {
const DIM: u32 = 18;
let c = (DIM as f32 - 1.0) * 0.5;
let frames: Vec<Kv6> = [5.0_f32, 6.5, 8.0, 6.5]
.iter()
.map(|&r| {
let r2 = r * r;
Kv6::from_fn(DIM, DIM, DIM, |x, y, z| {
let (dx, dy, dz) = (x as f32 - c, y as f32 - c, z as f32 - c);
(dx * dx + dy * dy + dz * dz <= r2).then_some(ORB)
})
})
.collect();
VoxelClip::from_kv6_frames(&frames, 1.0, LoopMode::Loop, &[], 150, 1)
.expect("frames are non-empty and share dims")
}
#[derive(Default)]
struct App {
renderer: Option<SceneRenderer>,
window: Option<Arc<Window>>,
scene: Option<Scene>,
started: Option<Instant>,
last_frame: Option<Instant>,
spinner: Option<SpriteInstanceId>,
}
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let window = Arc::new(
event_loop
.create_window(Window::default_attributes().with_title("roxlap book_sprites"))
.expect("create window"),
);
let size = window.inner_size();
let backend = if std::env::var_os("ROXLAP_GPU").is_none_or(|v| v != "0") {
BackendPreference::PreferGpu
} else {
BackendPreference::Cpu
};
let opts = RenderOptions {
backend,
clear_sky: SKY,
..RenderOptions::default()
};
let mut renderer = SceneRenderer::new(window.clone(), (size.width, size.height), &opts);
let gem = renderer.add_sprite_model(&gem_kv6());
renderer
.add_sprite_instance_posed(
gem,
DynSpriteTransform {
pos: [-50.0, 0.0, 202.0],
..DynSpriteTransform::default() },
)
.expect("model just registered");
renderer
.add_sprite_instance_posed(
gem,
DynSpriteTransform {
pos: [0.0, 0.0, 198.0],
right: [2.0, 0.0, 0.0], up: [0.0, 2.0, 0.0], forward: [0.0, 0.0, 2.0],
},
)
.expect("model just registered");
let spinner = renderer
.add_sprite_instance_posed(
gem,
DynSpriteTransform {
pos: [50.0, 0.0, 202.0],
..DynSpriteTransform::default()
},
)
.expect("model just registered");
let clip = renderer.add_voxel_clip(&orb_clip().decode().expect("self-authored clip"));
renderer
.add_clip_instance_playing(
clip,
DynSpriteTransform {
pos: [0.0, -60.0, 200.0],
..DynSpriteTransform::default()
},
1.0, 0, )
.expect("clip just registered");
renderer
.add_billboard_instance(clip, [0.0, 60.0, 200.0], BillboardMode::Cylindrical)
.expect("clip just registered");
self.renderer = Some(renderer);
self.window = Some(window);
self.scene = Some(build_scene());
self.started = Some(Instant::now());
self.last_frame = Some(Instant::now());
self.spinner = spinner.into();
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
let (Some(renderer), Some(scene)) = (self.renderer.as_mut(), self.scene.as_mut()) else {
return;
};
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::Resized(size) => renderer.resize(size.width.max(1), size.height.max(1)),
WindowEvent::RedrawRequested => {
let t = self.started.map_or(0.0, |s| s.elapsed().as_secs_f64());
let now = Instant::now();
let dt = self
.last_frame
.map_or(0.0, |l| (now - l).as_secs_f64())
.min(0.1);
self.last_frame = Some(now);
let camera = Camera::orbit(t * 0.2, 0.3, 200.0, [0.0, 0.0, 195.0]);
if let Some(id) = self.spinner {
let (s, c) = (t * 1.5).sin_cos();
let (s, c) = (s as f32, c as f32);
renderer.set_sprite_instance_transform(
id,
DynSpriteTransform {
pos: [50.0, 0.0, 202.0],
right: [c, s, 0.0],
up: [-s, c, 0.0],
forward: [0.0, 0.0, 1.0],
},
);
}
renderer.tick(&camera, dt);
let window = self.window.as_ref().expect("window outlives renderer");
let size = window.inner_size();
let settings =
OpticastSettings::for_oracle_framebuffer(size.width.max(1), size.height.max(1));
let mut frame = FrameParams::new(&settings);
frame.sky_color = SKY;
frame.fog_color = SKY;
renderer.render(scene, &camera, &frame);
renderer.present();
window.request_redraw();
}
_ => {}
}
}
fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
if let Some(renderer) = self.renderer.as_mut() {
renderer.wait_idle();
}
}
}
fn main() {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let event_loop = EventLoop::new().expect("create event loop");
event_loop
.run_app(&mut App::default())
.expect("run event loop");
}