summaryrefslogtreecommitdiff
path: root/import/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'import/src/main.rs')
-rw-r--r--import/src/main.rs304
1 files changed, 304 insertions, 0 deletions
diff --git a/import/src/main.rs b/import/src/main.rs
new file mode 100644
index 0000000..554a2bd
--- /dev/null
+++ b/import/src/main.rs
@@ -0,0 +1,304 @@
+/*
+ wearechat - generic multiplayer game with voip
+ Copyright (C) 2025 metamuffin
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, version 3 of the License only.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+#![feature(iter_array_chunks)]
+#![allow(clippy::too_many_arguments, clippy::type_complexity)]
+pub mod animation;
+pub mod mesh;
+pub mod physics;
+pub mod prefab;
+pub mod vrm;
+
+use anyhow::{Result, bail};
+use clap::Parser;
+use gltf::{image::Source, scene::Transform};
+use humansize::BINARY;
+use image::{ImageReader, codecs::webp::WebPEncoder};
+use log::{debug, info};
+use prefab::import_prefab;
+use rand::random;
+use std::{
+ borrow::Cow,
+ collections::HashMap,
+ fs::File,
+ io::{BufWriter, Cursor, Read, Write},
+ marker::PhantomData,
+ net::{SocketAddr, TcpStream},
+ path::{Path, PathBuf},
+ sync::{Arc, Mutex},
+ thread::{self, sleep},
+ time::Duration,
+};
+use weareshared::{
+ Affine3A, Vec3A,
+ helper::ReadWrite,
+ packets::{Data, Object, Packet, Resource},
+ resources::{Image, RespackEntry},
+ respack::save_respack,
+ store::ResourceStore,
+ vec3a,
+};
+
+#[derive(Parser)]
+pub struct Args {
+ #[arg(short, long)]
+ address: Option<SocketAddr>,
+
+ /// Output converted prefab as resource package
+ #[arg(short = 'o', long)]
+ pack: Option<PathBuf>,
+
+ /// Path(s) to a glTF file, binary or json format
+ scene: Vec<PathBuf>,
+ /// Send all resources to the server then quit
+ #[arg(short, long)]
+ push: bool,
+
+ /// Remove all other object from the world
+ #[arg(short, long)]
+ clear: bool,
+ /// Add the object to the world
+ #[arg(short, long)]
+ add: bool,
+ /// Transcode all textures to WebP
+ #[arg(short, long)]
+ webp: bool,
+ /// Add skybox
+ #[arg(long)]
+ skybox: Option<PathBuf>,
+ /// Override prefab name
+ #[arg(short, long)]
+ name: Option<String>,
+ #[arg(short, long)]
+ scale: Option<f32>,
+ #[arg(short, long)]
+ dry_run: bool,
+ #[arg(short, long)]
+ line_up: bool,
+
+ #[arg(long)]
+ use_cache: bool,
+
+ #[arg(short = 'S', long)]
+ with_default_sun: bool,
+
+ #[arg(long)]
+ animation: Option<PathBuf>,
+ #[arg(long)]
+ animation_bone_map: Option<PathBuf>,
+ #[arg(long)]
+ animation_rotation_y: Option<f32>,
+ #[arg(long)]
+ animation_scale: Option<f32>,
+ #[arg(long)]
+ animation_apply_ibm: bool,
+
+ /// Spins the object
+ #[arg(long)]
+ debug_spin: bool,
+ /// Adds a light
+ #[arg(long)]
+ debug_light: bool,
+ #[arg(long)]
+ no_particles: bool,
+ #[arg(long)]
+ no_animations: bool,
+}
+
+fn main() -> Result<()> {
+ env_logger::init_from_env("LOG");
+ let args = Args::parse();
+
+ let store = if args.use_cache && !args.pack.is_some() {
+ ResourceStore::new_env()?
+ } else {
+ ResourceStore::new_memory()
+ };
+
+ let mut prefabs = Vec::new();
+ let texture_cache = Arc::new(Mutex::new(HashMap::new()));
+
+ for scenepath in &args.scene {
+ prefabs.push(import_prefab(&store, &texture_cache, scenepath, &args)?);
+ }
+
+ let mut size = 0;
+ store.iter(|_k, len| size += len).unwrap();
+ info!(
+ "prefab has network size of {}",
+ humansize::format_size(size, BINARY)
+ );
+
+ if args.dry_run {
+ return Ok(());
+ }
+ if let Some(outpath) = args.pack {
+ let entry = store.set(&RespackEntry { name: None })?;
+ let mut resources = Vec::new();
+ store.iter(|r, _| resources.push(r))?;
+ save_respack(
+ BufWriter::new(File::create(outpath)?),
+ &store,
+ &resources,
+ Some(entry),
+ )?;
+ } else if let Some(address) = args.address {
+ let mut sock = TcpStream::connect(address)?;
+ Packet::Connect(random()).write(&mut sock)?;
+ for p in &prefabs {
+ Packet::AnnouncePrefab(p.clone()).write(&mut sock)?;
+ }
+ sock.flush()?;
+
+ let mut obs = Vec::new();
+ if args.add {
+ for (i, p) in prefabs.iter().enumerate() {
+ let ob = Object::new();
+ info!("adding object {ob}");
+ Packet::Add(ob, p.clone()).write(&mut sock)?;
+ if args.line_up {
+ Packet::Position(ob, vec3a(i as f32 * 1.2, 0., i as f32 * 0.3), Vec3A::ZERO)
+ .write(&mut sock)?;
+ }
+ obs.push(ob);
+ }
+ sock.flush()?;
+ }
+
+ if args.debug_spin {
+ let ob = obs[0];
+ let mut sock2 = sock.try_clone().unwrap();
+ thread::spawn(move || {
+ let mut x = 0.;
+ loop {
+ Packet::Position(ob, Vec3A::ZERO, vec3a(0., x * 0.1, 0.))
+ .write(&mut sock2)
+ .unwrap();
+ sock2.flush().unwrap();
+ x += 0.1;
+ sleep(Duration::from_millis(50));
+ }
+ });
+ }
+ if args.push {
+ if args.use_cache {
+ return Ok(());
+ }
+ store.iter(|k, _| {
+ Packet::RespondResource(k, Data(store.get_raw(k).unwrap().unwrap()))
+ .write(&mut sock)
+ .unwrap();
+ })?;
+ sock.flush()?;
+ } else {
+ loop {
+ let packet = Packet::read(&mut sock)?;
+ match packet {
+ Packet::RequestResource(hash) => {
+ if let Some(d) = store.get_raw(hash)? {
+ Packet::RespondResource(hash, Data(d)).write(&mut sock)?;
+ sock.flush()?;
+ }
+ }
+ Packet::Add(ob_a, _) => {
+ if args.clear && !obs.contains(&ob_a) {
+ info!("removing object {ob_a}");
+ Packet::Remove(ob_a).write(&mut sock)?;
+ sock.flush()?;
+ }
+ }
+ _ => (),
+ }
+ }
+ }
+ } else {
+ bail!("no output option specified. either provide an address to a server or use --pack")
+ }
+ Ok(())
+}
+
+pub type TextureCache = Arc<Mutex<HashMap<String, Resource<Image<'static>>>>>;
+fn load_texture(
+ name: &str,
+ store: &ResourceStore,
+ path: &Path,
+ buffers: &[gltf::buffer::Data],
+ source: &Source,
+ webp: bool,
+ texture_cache: &TextureCache,
+) -> Result<Resource<Image<'static>>> {
+ let (mut image, uri) = match source {
+ gltf::image::Source::View { view, mime_type } => {
+ debug!("{name} texture is embedded and of type {mime_type:?}");
+ let buf =
+ &buffers[view.buffer().index()].0[view.offset()..view.offset() + view.length()];
+ (Image(Cow::Borrowed(buf)), None)
+ }
+ gltf::image::Source::Uri {
+ uri,
+ mime_type: Some(mime_type),
+ } => {
+ debug!("{name} texture is {uri:?} and of type {mime_type:?}");
+ if let Some(res) = texture_cache.lock().unwrap().get(*uri) {
+ return Ok(res.to_owned());
+ }
+ let path = path.join(uri);
+ let mut buf = Vec::new();
+ File::open(path)?.read_to_end(&mut buf)?;
+ (Image(buf.into()), Some(uri.to_string()))
+ }
+ gltf::image::Source::Uri {
+ uri,
+ mime_type: None,
+ } => {
+ debug!("{name} texture is {uri:?} and has no type");
+ if let Some(res) = texture_cache.lock().unwrap().get(*uri) {
+ return Ok(res.to_owned());
+ }
+ let path = path.join(uri);
+ let mut buf = Vec::new();
+ File::open(path)?.read_to_end(&mut buf)?;
+ (Image(buf.into()), Some(uri.to_string()))
+ }
+ };
+
+ if webp {
+ let mut image_out = Vec::new();
+
+ let len = image.0.len();
+ ImageReader::new(Cursor::new(image.0))
+ .with_guessed_format()?
+ .decode()?
+ .write_with_encoder(WebPEncoder::new_lossless(&mut image_out))?;
+ debug!("webp encode: {len} -> {}", image_out.len());
+ image = Image(Cow::Owned(image_out));
+ }
+ let res = Resource(store.set(&image)?.0, PhantomData);
+ if let Some(uri) = uri {
+ texture_cache.lock().unwrap().insert(uri, res.clone());
+ }
+ Ok(res)
+}
+
+pub fn transform_to_affine(trans: Transform) -> Affine3A {
+ let mat = trans.matrix();
+ Affine3A::from_cols_array_2d(&[
+ [mat[0][0], mat[0][1], mat[0][2]],
+ [mat[1][0], mat[1][1], mat[1][2]],
+ [mat[2][0], mat[2][1], mat[2][2]],
+ [mat[3][0], mat[3][1], mat[3][2]],
+ ])
+}