diff --git a/.github/workflows/ci-rs.yml b/.github/workflows/ci-rs.yml index 168ddc0..61a0b03 100644 --- a/.github/workflows/ci-rs.yml +++ b/.github/workflows/ci-rs.yml @@ -24,9 +24,7 @@ concurrency: jobs: ci-rs: - # revert later - # runs-on: ubuntu-latest - runs-on: macos-latest + runs-on: [ macos-latest, ubuntu-latest] strategy: fail-fast: false matrix: @@ -44,22 +42,15 @@ jobs: components: rustfmt, clippy enable-sccache: "true" - # - name: Install binaries - # run: sudo apt-get update && sudo apt-get install -y clang pkg-config libx11-dev libasound2-dev libudev-dev libxkbcommon-x11-0 gcc-multilib + - name: Install binaries + run: sudo apt-get update && sudo apt-get install -y clang pkg-config libx11-dev libasound2-dev libudev-dev libxkbcommon-x11-0 gcc-multilib - name: Build - run: cargo build --release # --verbose + run: cargo build --release --features docker - name: Test - run: cargo test --release # --verbose + run: cargo test --release - name: Lint run: cargo fmt --all -- --check - # && cargo clippy --verbose -- -D warnings - - # - name: Bloat Check - # uses: cs50victor/cargo-bloat-action@master - # with: - # token: ${{ secrets.GITHUB_TOKEN }} - # kv_token: ${{ secrets.KV_TOKEN }} - # included_packages: "lkgpt" + # && cargo clippy --verbose -- -D warnings \ No newline at end of file diff --git a/crates/bevy_remote_asset/Cargo.toml b/crates/bevy_remote_asset/Cargo.toml index 0b06bac..b98cb49 100644 --- a/crates/bevy_remote_asset/Cargo.toml +++ b/crates/bevy_remote_asset/Cargo.toml @@ -5,3 +5,6 @@ edition = "2021" rust-version.workspace = true [dependencies] +bevy.workspace = true +pin-project = "1.1.3" +surf = {version = "2.3", default-features = false, features = ["h1-client-rustls"]} \ No newline at end of file diff --git a/crates/bevy_remote_asset/src/lib.rs b/crates/bevy_remote_asset/src/lib.rs index 7d12d9a..79106de 100644 --- a/crates/bevy_remote_asset/src/lib.rs +++ b/crates/bevy_remote_asset/src/lib.rs @@ -1,14 +1,7 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right -} +// derived from https://github.com/johanhelsing/bevy_web_asset -#[cfg(test)] -mod tests { - use super::*; +mod web_asset_plugin; +mod web_asset_source; - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub use web_asset_plugin::WebAssetPlugin; +pub use web_asset_source::WebAssetReader; diff --git a/crates/bevy_remote_asset/src/web_asset_plugin.rs b/crates/bevy_remote_asset/src/web_asset_plugin.rs new file mode 100644 index 0000000..2fc9334 --- /dev/null +++ b/crates/bevy_remote_asset/src/web_asset_plugin.rs @@ -0,0 +1,33 @@ +use bevy::prelude::*; + +use crate::web_asset_source::*; +use bevy::asset::io::AssetSource; + +/// Add this plugin to bevy to support loading http and https urls. +/// +/// Needs to be added before Bevy's `DefaultPlugins`. +/// +/// # Example +/// +/// ```no_run +/// # use bevy::prelude::*; +/// # use bevy_web_asset::WebAssetPlugin; +/// +/// let mut app = App::new(); +/// +/// app.add_plugins(( +/// WebAssetPlugin::default(), +/// DefaultPlugins +/// )); +/// ``` +#[derive(Default)] +pub struct WebAssetPlugin; + +impl Plugin for WebAssetPlugin { + fn build(&self, app: &mut App) { + app.register_asset_source( + "https", + AssetSource::build().with_reader(|| Box::new(WebAssetReader::Https)), + ); + } +} diff --git a/crates/bevy_remote_asset/src/web_asset_source.rs b/crates/bevy_remote_asset/src/web_asset_source.rs new file mode 100644 index 0000000..8c9fe3c --- /dev/null +++ b/crates/bevy_remote_asset/src/web_asset_source.rs @@ -0,0 +1,143 @@ +use bevy::{asset::io::PathStream, utils::BoxedFuture}; +use std::path::{Path, PathBuf}; + +use bevy::asset::io::{AssetReader, AssetReaderError, Reader}; + +/// Treats paths as urls to load assets from. +pub enum WebAssetReader { + /// Use TLS for setting up connections. + Https, +} + +impl WebAssetReader { + fn make_uri(&self, path: &Path) -> PathBuf { + PathBuf::from(match self { + Self::Https => "https://", + }) + .join(path) + } + + /// See [bevy::asset::io::get_meta_path] + fn make_meta_uri(&self, path: &Path) -> PathBuf { + let mut uri = self.make_uri(path); + let mut extension = + path.extension().expect("asset paths must have extensions").to_os_string(); + extension.push(".meta"); + uri.set_extension(extension); + uri + } +} + +async fn get<'a>(path: PathBuf) -> Result>, AssetReaderError> { + use std::{ + future::Future, + io, + pin::Pin, + task::{Context, Poll}, + }; + + use bevy::asset::io::VecReader; + use surf::StatusCode; + + #[pin_project::pin_project] + struct ContinuousPoll(#[pin] T); + + impl Future for ContinuousPoll { + type Output = T::Output; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + // Always wake - blocks on single threaded executor. + cx.waker().wake_by_ref(); + + self.project().0.poll(cx) + } + } + + let str_path = path.to_str().ok_or_else(|| { + AssetReaderError::Io(io::Error::new( + io::ErrorKind::Other, + format!("non-utf8 path: {}", path.display()), + )) + })?; + let mut response = ContinuousPoll(surf::get(str_path)).await.map_err(|err| { + AssetReaderError::Io(io::Error::new( + io::ErrorKind::Other, + format!( + "unexpected status code {} while loading {}: {}", + err.status(), + path.display(), + err.into_inner(), + ), + )) + })?; + + match response.status() { + StatusCode::Ok => Ok(Box::new(VecReader::new( + ContinuousPoll(response.body_bytes()) + .await + .map_err(|_| AssetReaderError::NotFound(path.to_path_buf()))?, + )) as _), + StatusCode::NotFound => Err(AssetReaderError::NotFound(path)), + code => Err(AssetReaderError::Io(io::Error::new( + io::ErrorKind::Other, + format!("unexpected status code {} while loading {}", code, path.display()), + ))), + } +} + +impl AssetReader for WebAssetReader { + fn read<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, Result>, AssetReaderError>> { + Box::pin(get(self.make_uri(path))) + } + + fn read_meta<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, Result>, AssetReaderError>> { + Box::pin(get(self.make_meta_uri(path))) + } + + fn is_directory<'a>( + &'a self, + _path: &'a Path, + ) -> BoxedFuture<'a, Result> { + Box::pin(async { Ok(false) }) + } + + fn read_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, Result, AssetReaderError>> { + Box::pin(async { Err(AssetReaderError::NotFound(self.make_uri(path))) }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn make_https_uri() { + assert_eq!( + WebAssetReader::Https + .make_uri(Path::new("s3.johanhelsing.studio/dump/favicon.png")) + .to_str() + .unwrap(), + "https://s3.johanhelsing.studio/dump/favicon.png" + ); + } + + #[test] + fn make_https_meta_uri() { + assert_eq!( + WebAssetReader::Https + .make_meta_uri(Path::new("s3.johanhelsing.studio/dump/favicon.png")) + .to_str() + .unwrap(), + "https://s3.johanhelsing.studio/dump/favicon.png.meta" + ); + } +} diff --git a/new_media/Cargo.toml b/new_media/Cargo.toml index b6550f9..2657e21 100644 --- a/new_media/Cargo.toml +++ b/new_media/Cargo.toml @@ -2,7 +2,7 @@ name = "new_media" version = "0.0.1" edition = "2021" -rus.workspace = true +rust.workspace = true default-run = "new_media" [dependencies] @@ -12,6 +12,7 @@ base64.workspace = true bevy.workspace = true bevy_ws_server = { path = "../crates/bevy_ws_server" } bevy_headless = { path = "../crates/bevy_headless" } +bevy_remote_asset = { path = "../crates/bevy_remote_asset" } bevy_gaussian_splatting = { version = "2.0.2", default-features = true } bevy_panorbit_camera.workspace = true crossbeam-channel.workspace = true @@ -24,4 +25,4 @@ tungstenite.workspace = true openssl = { version = "0.10.63", features = ["vendored"], optional = true} [features] -docker = ["dep:openssl","bevy/x11"] +docker = ["dep:openssl","bevy/x11"] \ No newline at end of file diff --git a/new_media/src/asset.rs b/new_media/src/asset.rs new file mode 100644 index 0000000..a388fd5 --- /dev/null +++ b/new_media/src/asset.rs @@ -0,0 +1,57 @@ +use bevy::{ + asset::{AssetServer, Assets}, + core::Name, + core_pipeline::{core_3d::Camera3dBundle, tonemapping::Tonemapping}, + ecs::system::{Commands, Res, ResMut}, + math::Vec3, + render::{camera::Camera, texture::Image}, + transform::components::Transform, + utils::default, +}; +use bevy_headless::ImageExportSource; +use bevy_panorbit_camera::PanOrbitCamera; + +use bevy_gaussian_splatting::{GaussianCloud, GaussianSplattingBundle}; + +pub fn setup_gaussian_cloud( + mut commands: Commands, + asset_server: Res, + mut gaussian_assets: ResMut>, + mut scene_controller: ResMut, + mut images: ResMut>, + export_sources: ResMut>, +) { + // let remote_file = Some("https://huggingface.co/datasets/cs50victor/splats/resolve/main/train/point_cloud/iteration_7000/point_cloud.gcloud"); + // TODO: figure out how to load remote files later + let splat_file = "splats/garden/point_cloud/iteration_7000/point_cloud.gcloud"; + log::info!("loading {}", splat_file); + // let cloud = asset_server.load(splat_file.to_string()); + + let cloud = gaussian_assets.add(GaussianCloud::test_model()); + + let render_target = bevy_headless::setup_render_target( + &mut commands, + &mut images, + &mut scene_controller, + export_sources, + ); + + let gs = GaussianSplattingBundle { cloud, ..default() }; + commands.spawn((gs, Name::new("gaussian_cloud"))); + + commands.spawn(( + Camera3dBundle { + transform: Transform::from_translation(Vec3::new(0.0, 1.5, 5.0)), + tonemapping: Tonemapping::None, + camera: Camera { target: render_target, ..default() }, + ..default() + }, + PanOrbitCamera { + allow_upside_down: true, + orbit_smoothness: 0.0, + pan_smoothness: 0.0, + zoom_smoothness: 0.0, + ..default() + }, + )); +} diff --git a/new_media/src/main.rs b/new_media/src/main.rs index 7198dbf..e73392f 100644 --- a/new_media/src/main.rs +++ b/new_media/src/main.rs @@ -1,68 +1,22 @@ +mod asset; mod controls; mod server; +use asset::setup_gaussian_cloud; use bevy::{ app::{App as Engine, ScheduleRunnerPlugin, Startup, Update}, - asset::{AssetServer, Assets}, - core::Name, - core_pipeline::{clear_color::ClearColor, core_3d::Camera3dBundle, tonemapping::Tonemapping}, - ecs::system::{Commands, Res, ResMut}, - math::Vec3, - render::{camera::Camera, color::Color, texture::Image}, - transform::components::Transform, - utils::default, + core_pipeline::clear_color::ClearColor, + render::color::Color, }; -use bevy_headless::{HeadlessPlugin, ImageExportSource}; -use bevy_panorbit_camera::{PanOrbitCamera, PanOrbitCameraPlugin}; +use bevy_headless::HeadlessPlugin; +use bevy_panorbit_camera::PanOrbitCameraPlugin; -use bevy_gaussian_splatting::{GaussianCloud, GaussianSplattingBundle, GaussianSplattingPlugin}; +use bevy_gaussian_splatting::GaussianSplattingPlugin; +use bevy_remote_asset::WebAssetPlugin; use bevy_ws_server::WsPlugin; use controls::{update_world_from_input, WorldControlChannel}; use server::{receive_message, start_ws}; -fn setup_gaussian_cloud( - mut commands: Commands, - asset_server: Res, - mut gaussian_assets: ResMut>, - mut scene_controller: ResMut, - mut images: ResMut>, - export_sources: ResMut>, -) { - // let remote_file = Some("https://huggingface.co/datasets/cs50victor/splats/resolve/main/train/point_cloud/iteration_7000/point_cloud.gcloud"); - // TODO: figure out how to load remote files later - let splat_file = "splats/garden/point_cloud/iteration_7000/point_cloud.gcloud"; - log::info!("loading {}", splat_file); - // let cloud = asset_server.load(splat_file.to_string()); - - let cloud = gaussian_assets.add(GaussianCloud::test_model()); - - let render_target = bevy_headless::setup_render_target( - &mut commands, - &mut images, - &mut scene_controller, - export_sources, - ); - - let gs = GaussianSplattingBundle { cloud, ..default() }; - commands.spawn((gs, Name::new("gaussian_cloud"))); - - commands.spawn(( - Camera3dBundle { - transform: Transform::from_translation(Vec3::new(0.0, 1.5, 5.0)), - tonemapping: Tonemapping::None, - camera: Camera { target: render_target, ..default() }, - ..default() - }, - PanOrbitCamera { - allow_upside_down: true, - orbit_smoothness: 0.0, - pan_smoothness: 0.0, - zoom_smoothness: 0.0, - ..default() - }, - )); -} - pub struct AppConfig { pub width: u32, pub height: u32, @@ -88,6 +42,7 @@ fn main() { // .insert_resource(ClearColor(Color::rgb_u8(255, 255, 255))) .insert_resource(ClearColor(Color::rgb_u8(0, 0, 0))) .add_plugins(( + WebAssetPlugin, HeadlessPlugin, WsPlugin, ScheduleRunnerPlugin::run_loop(std::time::Duration::from_secs_f64(1.0 / 60.0)),