diff --git a/Cargo.lock b/Cargo.lock index d5eec15a88..a273c41bbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4989,6 +4989,7 @@ version = "0.6.0" dependencies = [ "authc", "bincode", + "color_quant", "criterion", "crossbeam", "dot_vox", diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron index cc2da69520..7fa7b2177b 100644 --- a/assets/voxygen/i18n/en.ron +++ b/assets/voxygen/i18n/en.ron @@ -353,6 +353,7 @@ magically infused items?"#, "gameinput.freelook": "Free Look", "gameinput.autowalk": "Auto Walk", "gameinput.dance": "Dance", + "gameinput.voxsnap": "Capture Surroundings to .vox", /// End GameInput section diff --git a/common/Cargo.toml b/common/Cargo.toml index 3a50a4e9f4..c21d7bfc38 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -12,7 +12,7 @@ specs-idvs = { git = "https://gitlab.com/veloren/specs-idvs.git" } specs = { version = "0.15.1", features = ["serde", "nightly", "storage-event-control"] } vek = { version = "0.10.0", features = ["serde"] } -dot_vox = "4.0.0" +dot_vox = "4.1.0" fxhash = "0.2.1" image = "0.22.3" mio = "0.6.19" @@ -35,6 +35,7 @@ notify = "5.0.0-pre.2" indexmap = "1.3.0" sum_type = "0.2.0" authc = { git = "https://gitlab.com/veloren/auth.git", rev = "65571ade0d954a0e0bd995fdb314854ff146ab97" } +color_quant = "1.0.1" [dev-dependencies] criterion = "0.3" diff --git a/common/src/util/mod.rs b/common/src/util/mod.rs index ff0d99e80b..11603e134a 100644 --- a/common/src/util/mod.rs +++ b/common/src/util/mod.rs @@ -1,5 +1,6 @@ mod color; mod dir; +mod vox_capture; pub const GIT_VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/githash")); @@ -10,3 +11,4 @@ lazy_static::lazy_static! { pub use color::*; pub use dir::*; +pub use vox_capture::*; diff --git a/common/src/util/vox_capture.rs b/common/src/util/vox_capture.rs new file mode 100644 index 0000000000..b456c3e078 --- /dev/null +++ b/common/src/util/vox_capture.rs @@ -0,0 +1,103 @@ +use crate::{ + terrain::Block, + vol::{ReadVol, Vox}, +}; +use color_quant::NeuQuant; +use std::path::Path; +use vek::*; + +// Given a `ReadVol`, a center position, and a filename +// Saves a 256x256x256 cube of volume data in .vox format +// Uses `color_quant` to keep the color count to the limits imposed by magica +pub fn vox_capture( + vol: &impl ReadVol, + center: Vec3, + save_path: &Path, +) -> Result { + // First read block into color and pos vecs + let (positions, colors) = (-128..128) + .flat_map(move |x| { + (-128..128).flat_map(move |y| (-128..128).map(move |z| Vec3::new(x, y, z) + center)) + }) + .map(|pos| (pos, vol.get(pos).ok().copied().unwrap_or(Block::empty()))) + .filter_map(|(pos, block)| { + block.get_color().map(|color| { + ( + (pos - center + Vec3::from(128)).map(|e| e as u8), + Rgba::from(color), + ) + }) + }) + .fold( + (Vec::new(), Vec::new()), + |(mut positions, mut colors), (pos, color)| { + positions.push(pos); + colors.extend_from_slice(&color); + (positions, colors) + }, + ); + + // Quantize colors + // dot_vox docs seem to imply there are only 255 (and not 256) indices in + // palette + let quant = NeuQuant::new(10, 255, &colors); + // Extract palette + // Note: palette includes alpha, we could abuse this as alternative to indices + // to store extra info + let palette = quant + .color_map_rgba() + .chunks_exact(4) + .map(|c| { + // Magica stores them backwards? + ((c[3] as u32) << 24) + | ((c[2] as u32) << 16) + | ((c[1] as u32) << 8) + | ((c[0] as u32) << 0) + }) + .collect(); + // Build voxel list with palette indices + let voxels = colors + .chunks_exact(4) + .map(|p| quant.index_of(p) as u8) + .zip(positions) + .map(|(index, pos)| dot_vox::Voxel { + x: pos.x, + y: pos.y, + z: pos.z, + i: index, + }) + .collect(); + + let model = dot_vox::Model { + size: dot_vox::Size { + x: 256, + y: 256, + z: 256, + }, + voxels, + }; + + let dot_vox_data = dot_vox::DotVoxData { + version: 150, // TODO: is this correct at all?? + models: vec![model], + palette, + materials: Vec::new(), + }; + + let save_path = save_path.with_extension("vox"); + // Check if folder exists and create it if it does not + if !save_path.parent().map_or(false, |p| p.exists()) { + std::fs::create_dir_all(&save_path.parent().unwrap()) + .map_err(|err| format!("Couldn't create folder for vox capture: {:?}", err))?; + } + // Attempt to create a file (hopefully all this effort wasn't for nothing...) + let mut writer = std::fs::File::create(save_path.with_extension("vox")) + .map(|file| std::io::BufWriter::new(file)) + .map_err(|err| format!("Failed to create file to save vox: {:?}", err))?; + + // Save + dot_vox_data + .write_vox(&mut writer) + .map(|_| format!("Succesfully saved vox to: {}", save_path.to_string_lossy())) + .map_err(|err| format!("Failed to write vox: {:?}", err)) +} diff --git a/voxygen/Cargo.toml b/voxygen/Cargo.toml index cc44fe8c11..9461f04945 100644 --- a/voxygen/Cargo.toml +++ b/voxygen/Cargo.toml @@ -46,7 +46,7 @@ server = { package = "veloren-server", path = "../server", optional = true } glsl-include = "0.3.1" failure = "0.1.6" log = "0.4.8" -dot_vox = "4.0.0" +dot_vox = "4.1.0" image = "0.22.3" serde = "1.0.102" serde_derive = "1.0.102" diff --git a/voxygen/src/key_state.rs b/voxygen/src/key_state.rs index 91fbaf1583..4fc50a168d 100644 --- a/voxygen/src/key_state.rs +++ b/voxygen/src/key_state.rs @@ -12,6 +12,7 @@ pub struct KeyState { pub toggle_dance: bool, pub auto_walk: bool, pub swap_loadout: bool, + pub vox_snap: bool, pub respawn: bool, pub analog_matrix: Vec2, } @@ -30,6 +31,7 @@ impl KeyState { toggle_dance: false, auto_walk: false, swap_loadout: false, + vox_snap: false, respawn: false, analog_matrix: Vec2::zero(), } diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index e6d981b4be..907fff0d06 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -381,6 +381,48 @@ impl PlayState for SessionState { self.client.borrow_mut().swap_loadout(); } } + Event::InputUpdate(GameInput::VoxSnap, state) + if state != self.key_state.vox_snap => + { + self.key_state.vox_snap = state; + if state { + let client = self.client.borrow_mut(); + + let mut path = global_state.settings.screenshots_path.clone(); + path.push(format!( + "voxsnap_{}", + std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .map(|d| d.as_millis()) + .unwrap_or(0) + )); + + let player_pos = client + .state() + .read_storage::() + .get(client.entity()) + .copied() + .unwrap() + .0; + + let result = common::util::vox_capture( + &*client.state().terrain(), + player_pos.map(|e| e as i32), + &path, + ); + + self.hud.new_message(match result { + Ok(message) => Chat { + chat_type: ChatType::Meta, + message, + }, + Err(message) => Chat { + chat_type: ChatType::Meta, + message, + }, + }); + } + } Event::InputUpdate(GameInput::ToggleLantern, true) => { self.client.borrow_mut().toggle_lantern(); }, diff --git a/voxygen/src/settings.rs b/voxygen/src/settings.rs index 140ca54732..8a99b91a41 100644 --- a/voxygen/src/settings.rs +++ b/voxygen/src/settings.rs @@ -152,6 +152,7 @@ impl ControlSettings { GameInput::Slot9 => KeyMouse::Key(VirtualKeyCode::Key9), GameInput::Slot10 => KeyMouse::Key(VirtualKeyCode::Q), GameInput::SwapLoadout => KeyMouse::Key(VirtualKeyCode::LAlt), + GameInput::VoxSnap => KeyMouse::Key(VirtualKeyCode::F8), } } } @@ -213,6 +214,7 @@ impl Default for ControlSettings { GameInput::Slot9, GameInput::Slot10, GameInput::SwapLoadout, + GameInput::VoxSnap, ]; for game_input in game_inputs { new_settings.insert_binding(game_input, ControlSettings::default_binding(game_input)); diff --git a/voxygen/src/window.rs b/voxygen/src/window.rs index 70db080825..39ec8fc567 100644 --- a/voxygen/src/window.rs +++ b/voxygen/src/window.rs @@ -64,6 +64,7 @@ pub enum GameInput { SwapLoadout, FreeLook, AutoWalk, + VoxSnap, } impl GameInput { @@ -117,6 +118,7 @@ impl GameInput { GameInput::Slot9 => "gameinput.slot9", GameInput::Slot10 => "gameinput.slot10", GameInput::SwapLoadout => "gameinput.swaploadout", + GameInput::VoxSnap => "gameinput.voxsnap", } } }