├── .gitignore ├── .gitmodules ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── contrib ├── aka-example.png └── midori-example.png ├── examples ├── aka.rs └── midori.rs └── src ├── camera.rs ├── core.rs ├── ffi.rs ├── lib.rs ├── macros.rs ├── puppet.rs └── scene.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | /*.so 4 | /*.inx 5 | /*.inp 6 | /.vs 7 | /.vscode 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "examples/models"] 2 | path = examples/models 3 | url = https://github.com/Inochi2D/example-models 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "inochi2d-rs" 3 | version = "0.1.0" 4 | edition = "2021" 5 | build = "build.rs" 6 | authors = [ 7 | "Aki Van Ness " 8 | ] 9 | repository = "https://github.com/Inochi2D/inochi2d-rs" 10 | readme = "README.md" 11 | license = "BSD-2-Clause" 12 | keywords = [ 13 | "Inochi2D", 14 | "gamedev", 15 | "vtuber", 16 | "streaming" 17 | ] 18 | categories = [ 19 | "external-ffi-bindings" 20 | ] 21 | description = "Rust bindings for Inochi2D" 22 | 23 | 24 | [features] 25 | default = ["opengl", "logging", "monotonic"] 26 | logging = ["tracing"] 27 | monotonic = [] 28 | opengl = [] 29 | nightly = [] 30 | 31 | [dependencies] 32 | libc = "0.2" 33 | tracing = { version = "0.1.35", optional = true } 34 | 35 | [build-dependencies] 36 | cc = { version = "1.0", features = ["parallel"] } 37 | 38 | [dev-dependencies] 39 | glutin = "0.28.0" 40 | gl = "0.14.0" 41 | tracing-subscriber = "0.3.14" 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2020, Inochi2D Project 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inochi2D Rust Bindings 2 | 3 | This repository contains the (preliminary) Rust bindings for Inochi2D using the [Inochi2D C SDK](https://github.com/Inochi2D/inochi2d-c). 4 | 5 | 6 | **NOTE:** The bindings pull in a large chunk of the D runtime, including the Garbage Collector. 7 | 8 | ## Building 9 | 10 | To build these bindings, you need [ldc](https://github.com/ldc-developers/ldc), [dub](https://dub.pm/), and [git](https://git-scm.com/) in addition to 11 | the normal rust toolchain. 12 | 13 | The [`build.rs`](./build.rs) will attempt to clone [Inochi2D](https://github.com/Inochi2D/inochi2d/) and [Inochi2D-c](https://github.com/Inochi2D/inochi2d-c) into the target directory and build them from source on first build. 14 | 15 | You should only need to build the crate as you would normally. 16 | 17 | ``` 18 | $ cargo build 19 | ``` 20 | 21 | ## Examples 22 | 23 | To build the examples, make sure you have the submodules checked out to ensure the example Inochi2D puppets are where the examples expect them to be. 24 | 25 | **NOTE:** You need [git lfs](https://git-lfs.github.com/) in order to properly clone the example models 26 | 27 | You can run the main OpenGL example with: 28 | ``` 29 | $ cargo run --example midori 30 | ``` 31 | ![midori example](./contrib/midori-example.png) 32 | ## License 33 | 34 | These bindings are licensed under the [BSD-2-Clause](https://spdx.org/licenses/BSD-2-Clause.html) license, the full text of which can be found in the [LICENSE](./LICENSE) file. 35 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::{env, path::PathBuf, process::Command}; 2 | 3 | const INOCHI2D_REPO: &'static str = "https://github.com/Inochi2D/inochi2d.git"; 4 | const INOCHI2D_C_REPO: &'static str = "https://github.com/Inochi2D/inochi2d-c.git"; 5 | 6 | #[cfg(feature = "opengl")] 7 | const WITH_OPENGL: bool = true; 8 | #[cfg(not(feature = "opengl"))] 9 | const WITH_OPENGL: bool = false; 10 | 11 | fn clone_repo(repo: &'static str, name: &'static str) -> PathBuf { 12 | let outdir = PathBuf::from(env::var("OUT_DIR").unwrap()).join(name); 13 | 14 | if !outdir.exists() { 15 | let cmd_status = Command::new("git") 16 | .arg("clone") 17 | .args(&["--depth", "1"]) 18 | .arg(repo) 19 | .arg(&outdir) 20 | .status() 21 | .unwrap(); 22 | 23 | assert!(cmd_status.success(), "Failed to clone repo"); 24 | } 25 | outdir 26 | } 27 | 28 | fn dub_init() { 29 | let outdir = PathBuf::from(env::var("OUT_DIR").unwrap()); 30 | 31 | let dub_status = Command::new("dub") 32 | .arg("add-path") 33 | .arg(&outdir) 34 | .arg("--cache=local") 35 | .arg("--verror") 36 | .status() 37 | .unwrap(); 38 | 39 | assert!(dub_status.success(), "Failed to setup dub"); 40 | } 41 | 42 | fn dub_build(dir: &PathBuf, cfg: &'static str) { 43 | let dub_status = Command::new("dub") 44 | .current_dir(dir) 45 | .arg("build") 46 | .arg("--compiler=ldc2") 47 | .arg("--non-interactive") 48 | .arg("--config") 49 | .arg(cfg) 50 | .arg("--cache=local") 51 | .arg("--verror") 52 | .status() 53 | .unwrap(); 54 | 55 | assert!(dub_status.success(), "Failed to dub build"); 56 | } 57 | 58 | fn main() { 59 | let inochi2d = clone_repo(INOCHI2D_REPO, "inochi2d"); 60 | let inochi2d_c = clone_repo(INOCHI2D_C_REPO, "inochi2d-c"); 61 | 62 | let libdir = inochi2d_c.join("out").clone(); 63 | 64 | if !libdir.join("libinochi2d-c.so").exists() { 65 | dub_init(); 66 | if WITH_OPENGL { 67 | dub_build(&inochi2d, "full"); 68 | dub_build(&inochi2d_c, "yesgl"); 69 | } else { 70 | println!("cargo:warning=Building without OpenGL support"); 71 | dub_build(&inochi2d, "renderless"); 72 | dub_build(&inochi2d_c, "nogl"); 73 | } 74 | } 75 | 76 | println!("cargo:rustc-link-search=native={}", libdir.display()); 77 | } 78 | -------------------------------------------------------------------------------- /contrib/aka-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Inochi2D/inochi2d-rs/b9450bcdf0440482847093b4349d01b07d0f3d5a/contrib/aka-example.png -------------------------------------------------------------------------------- /contrib/midori-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Inochi2D/inochi2d-rs/b9450bcdf0440482847093b4349d01b07d0f3d5a/contrib/midori-example.png -------------------------------------------------------------------------------- /examples/aka.rs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022, Inochi2D Project 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Aki "lethalbit" Van Ness 6 | */ 7 | 8 | extern crate gl; 9 | extern crate inochi2d_rs; 10 | #[cfg(feature = "logging")] 11 | extern crate tracing_subscriber; 12 | 13 | use std::path::PathBuf; 14 | 15 | use glutin::dpi::LogicalSize; 16 | use glutin::event::{ 17 | ElementState, Event, KeyboardInput, MouseScrollDelta, StartCause, VirtualKeyCode, WindowEvent, 18 | }; 19 | use glutin::event_loop::{ControlFlow, EventLoop}; 20 | use glutin::window::WindowBuilder; 21 | use glutin::{Api, ContextBuilder, GlProfile, GlRequest}; 22 | use inochi2d_rs::{ 23 | camera::Inochi2DCamera, core::Inochi2D, puppet::Inochi2DPuppet, scene::Inochi2DScene, 24 | MONOTONIC_CLOCK, 25 | }; 26 | 27 | #[cfg(feature = "logging")] 28 | use tracing_subscriber::{filter::LevelFilter, fmt, prelude::*}; 29 | 30 | fn main() { 31 | /* You can ignore this, it's used for debug logging and is optional */ 32 | #[cfg(feature = "logging")] 33 | tracing_subscriber::registry() 34 | .with(fmt::layer()) 35 | .with(LevelFilter::DEBUG) 36 | .init(); 37 | 38 | let el = EventLoop::new(); 39 | 40 | /* Create a new window for us to render in */ 41 | let wb = WindowBuilder::new() 42 | .with_inner_size(LogicalSize::new(800.0, 600.0)) 43 | .with_title("inochi2d-rs"); 44 | let windowed_context = ContextBuilder::new() 45 | .with_gl(GlRequest::Specific(Api::OpenGl, (4, 1))) 46 | .with_gl_profile(GlProfile::Core) 47 | .build_windowed(wb, &el) 48 | .expect("Unable to create window"); 49 | 50 | /* Ensure it's the current window */ 51 | let windowed_context = unsafe { windowed_context.make_current().unwrap() }; 52 | let gl_context = windowed_context.context(); 53 | 54 | /* Make sure GL and glutin can talk to each other */ 55 | gl::load_with(|s| gl_context.get_proc_address(s) as *const _); 56 | 57 | /* Create a new Inochi2D context */ 58 | let mut ctx = Inochi2D::new(MONOTONIC_CLOCK, 800, 800); 59 | /* Create a new Inochi2D puppet from a file */ 60 | let mut puppet = Inochi2DPuppet::new(PathBuf::from("./examples/models/Aka.inx")).unwrap(); 61 | 62 | /* Setup the camera and zoom */ 63 | let mut zoom: f64 = 0.15; 64 | let mut cam = Inochi2DCamera::new(Some(zoom as f32), Some(0.0), Some(0.0)); 65 | 66 | /* Setup the Inochi2D scene to draw */ 67 | let mut scene = Inochi2DScene::new(); 68 | 69 | /* Main Window loop */ 70 | el.run(move |event, _, control_flow| { 71 | *control_flow = ControlFlow::Poll; 72 | 73 | match event { 74 | Event::LoopDestroyed => (), 75 | Event::WindowEvent { event, .. } => match event { 76 | WindowEvent::Resized(physical_size) => { 77 | /* Handle window resizing */ 78 | windowed_context.resize(physical_size); 79 | 80 | ctx.set_viewport(physical_size.width as i32, physical_size.height as i32); 81 | unsafe { 82 | gl::Viewport( 83 | 0, 84 | 0, 85 | physical_size.width as i32, 86 | physical_size.height as i32, 87 | ); 88 | } 89 | } 90 | WindowEvent::CloseRequested 91 | | WindowEvent::KeyboardInput { 92 | input: 93 | KeyboardInput { 94 | virtual_keycode: Some(VirtualKeyCode::Escape), 95 | state: ElementState::Pressed, 96 | .. 97 | }, 98 | .. 99 | } => *control_flow = ControlFlow::Exit, 100 | WindowEvent::MouseWheel { 101 | delta: MouseScrollDelta::LineDelta(_, vert), 102 | .. 103 | } => { 104 | /* Allow us to zoom with the scroll wheel */ 105 | zoom += f64::from(vert) * 0.01; 106 | zoom = zoom.clamp(0.01, 10.0); 107 | cam.set_zoom(zoom as f32); 108 | } 109 | _ => (), 110 | }, 111 | Event::NewEvents(StartCause::Poll) | Event::RedrawRequested(_) => { 112 | /* Clear the Framebuffer so we don't get ghosting and garbage */ 113 | unsafe { 114 | gl::ClearColor(0.0, 0.0, 0.0, 1.0); 115 | gl::Clear(gl::COLOR_BUFFER_BIT); 116 | } 117 | 118 | /* Update and then draw the puppet */ 119 | puppet.update(); 120 | puppet.draw(); 121 | /* Draw the scene */ 122 | scene.draw(0.0, 0.0, ctx.view_width as f32, ctx.view_height as f32); 123 | 124 | windowed_context.swap_buffers().unwrap(); 125 | windowed_context.window().request_redraw(); 126 | } 127 | _ => (), 128 | } 129 | }); 130 | } 131 | -------------------------------------------------------------------------------- /examples/midori.rs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022, Inochi2D Project 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Aki "lethalbit" Van Ness 6 | */ 7 | 8 | extern crate gl; 9 | extern crate inochi2d_rs; 10 | #[cfg(feature = "logging")] 11 | extern crate tracing_subscriber; 12 | 13 | use glutin::dpi::LogicalSize; 14 | use glutin::event::{ 15 | ElementState, Event, KeyboardInput, MouseScrollDelta, StartCause, VirtualKeyCode, WindowEvent, 16 | }; 17 | use glutin::event_loop::{ControlFlow, EventLoop}; 18 | use glutin::window::WindowBuilder; 19 | use glutin::{Api, ContextBuilder, GlProfile, GlRequest}; 20 | 21 | use std::path::PathBuf; 22 | 23 | use inochi2d_rs::{camera::Inochi2DCamera, scene::Inochi2DScene, Inochi2DBuilder, MONOTONIC_CLOCK}; 24 | 25 | #[cfg(feature = "logging")] 26 | use tracing_subscriber::{filter::LevelFilter, fmt, prelude::*}; 27 | 28 | fn main() { 29 | /* You can ignore this, it's used for debug logging and is optional */ 30 | #[cfg(feature = "logging")] 31 | tracing_subscriber::registry() 32 | .with(fmt::layer()) 33 | .with(LevelFilter::DEBUG) 34 | .init(); 35 | 36 | let el = EventLoop::new(); 37 | 38 | /* Create a new window for us to render in */ 39 | let wb = WindowBuilder::new() 40 | .with_inner_size(LogicalSize::new(800.0, 600.0)) 41 | .with_title("inochi2d-rs"); 42 | let windowed_context = ContextBuilder::new() 43 | .with_gl(GlRequest::Specific(Api::OpenGl, (4, 1))) 44 | .with_gl_profile(GlProfile::Core) 45 | .build_windowed(wb, &el) 46 | .expect("Unable to create window"); 47 | 48 | /* Ensure it's the current window */ 49 | let windowed_context = unsafe { windowed_context.make_current().unwrap() }; 50 | let gl_context = windowed_context.context(); 51 | 52 | /* Make sure GL and glutin can talk to each other */ 53 | gl::load_with(|s| gl_context.get_proc_address(s) as *const _); 54 | 55 | /* Create a new Inochi2D context via the builder */ 56 | let mut ctx = Inochi2DBuilder::new() 57 | .viewport(800, 800) 58 | .timing(MONOTONIC_CLOCK) 59 | .puppet(PathBuf::from("./examples/models/Midori.inx")) 60 | .build() 61 | .unwrap(); 62 | 63 | /* Setup the camera and zoom */ 64 | let mut zoom: f64 = 0.15; 65 | let mut cam = Inochi2DCamera::new(Some(zoom as f32), Some(0.0), Some(0.0)); 66 | 67 | /* Setup the Inochi2D scene to draw */ 68 | let mut scene = Inochi2DScene::new(); 69 | 70 | /* Main Window loop */ 71 | el.run(move |event, _, control_flow| { 72 | *control_flow = ControlFlow::Poll; 73 | 74 | match event { 75 | Event::LoopDestroyed => (), 76 | Event::WindowEvent { event, .. } => match event { 77 | WindowEvent::Resized(physical_size) => { 78 | /* Handle window resizing */ 79 | windowed_context.resize(physical_size); 80 | 81 | ctx.set_viewport(physical_size.width as i32, physical_size.height as i32); 82 | unsafe { 83 | gl::Viewport( 84 | 0, 85 | 0, 86 | physical_size.width as i32, 87 | physical_size.height as i32, 88 | ); 89 | } 90 | } 91 | WindowEvent::CloseRequested 92 | | WindowEvent::KeyboardInput { 93 | input: 94 | KeyboardInput { 95 | virtual_keycode: Some(VirtualKeyCode::Escape), 96 | state: ElementState::Pressed, 97 | .. 98 | }, 99 | .. 100 | } => *control_flow = ControlFlow::Exit, 101 | WindowEvent::MouseWheel { 102 | delta: MouseScrollDelta::LineDelta(_, vert), 103 | .. 104 | } => { 105 | /* Allow us to zoom with the scroll wheel */ 106 | zoom += f64::from(vert) * 0.01; 107 | zoom = zoom.clamp(0.01, 10.0); 108 | cam.set_zoom(zoom as f32); 109 | } 110 | _ => (), 111 | }, 112 | Event::NewEvents(StartCause::Poll) | Event::RedrawRequested(_) => { 113 | /* Clear the Framebuffer so we don't get ghosting and garbage */ 114 | unsafe { 115 | gl::ClearColor(0.0, 0.0, 0.0, 1.0); 116 | gl::Clear(gl::COLOR_BUFFER_BIT); 117 | } 118 | 119 | /* Update the context */ 120 | ctx.update(); 121 | /* Update and then draw all the loaded puppets */ 122 | ctx.update_puppets(); 123 | ctx.draw_puppets(); 124 | /* Draw the scene */ 125 | scene.draw(0.0, 0.0, ctx.view_width as f32, ctx.view_height as f32); 126 | /* Swap the buffers and continue */ 127 | windowed_context.swap_buffers().unwrap(); 128 | windowed_context.window().request_redraw(); 129 | } 130 | _ => (), 131 | } 132 | }); 133 | } 134 | -------------------------------------------------------------------------------- /src/camera.rs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022, Inochi2D Project 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Aki "lethalbit" Van Ness 6 | */ 7 | 8 | #[cfg(feature = "logging")] 9 | use tracing::debug; 10 | 11 | use crate::ffi::{ 12 | inCameraDestroy, inCameraGetCenterOffset, inCameraGetCurrent, inCameraGetMatrix, 13 | inCameraGetPosition, inCameraGetRealSize, inCameraGetZoom, inCameraSetPosition, 14 | inCameraSetZoom, types::InCameraPtr, 15 | }; 16 | 17 | pub struct Inochi2DCamera { 18 | handle: InCameraPtr, 19 | zoom: f32, 20 | x: f32, 21 | y: f32, 22 | } 23 | 24 | impl Inochi2DCamera { 25 | /// Set Inochi2D Camera Zoom 26 | /// 27 | /// # Example 28 | /// 29 | /// ~~~no_run 30 | /// let mut camera = Inochi2DCamera::new(/* ... */); 31 | /// 32 | /// camera.set_zoom(0.15); 33 | /// ~~~ 34 | /// 35 | pub fn set_zoom(&mut self, zoom: f32) { 36 | #[cfg(feature = "logging")] 37 | debug!("Setting camera zoom to: {}", zoom); 38 | unsafe { 39 | inCameraSetZoom(self.handle, zoom); 40 | } 41 | 42 | self.zoom = zoom; 43 | } 44 | 45 | /// Gets the Inochi2D Camera Zoom 46 | /// 47 | /// # Example 48 | /// 49 | /// ~~~no_run 50 | /// let mut camera = Inochi2DCamera::new(/* ... */); 51 | /// 52 | /// let zoom: f32 = camera.get_zoom(); 53 | /// ~~~ 54 | /// 55 | /// # Returns 56 | /// 57 | /// A single `f32` which describes the cameras zoom. 58 | /// 59 | pub fn get_zoom(&mut self) -> f32 { 60 | let mut _z: f32 = 0.0; 61 | unsafe { 62 | inCameraGetZoom(self.handle, &mut _z); 63 | } 64 | 65 | self.zoom = _z; 66 | _z 67 | } 68 | 69 | /// Set Inochi2D Camera Position 70 | /// 71 | /// # Example 72 | /// 73 | /// ~~~no_run 74 | /// let mut camera = Inochi2DCamera::new(/* ... */); 75 | /// 76 | /// camera.set_pos(0.50, 0.25); 77 | /// ~~~ 78 | /// 79 | pub fn set_pos(&mut self, x: f32, y: f32) { 80 | #[cfg(feature = "logging")] 81 | debug!("Setting camera position to: ({}, {})", x, y); 82 | unsafe { 83 | inCameraSetPosition(self.handle, x, y); 84 | } 85 | 86 | self.x = x; 87 | self.y = y; 88 | } 89 | 90 | /// Get the Inochi2D Camera Position 91 | /// 92 | /// # Example 93 | /// 94 | /// ~~~no_run 95 | /// let mut camera = Inochi2DCamera::new(/* ... */); 96 | /// 97 | /// let pos = camera.get_pos(); 98 | /// 99 | /// println!("Camera is at {} {}", pos.0, pos.1); 100 | /// 101 | /// ~~~ 102 | /// 103 | /// # Returns 104 | /// 105 | /// A tuple with two `f32` elements describing the cameras X and Y 106 | /// 107 | pub fn get_pos(&mut self) -> (f32, f32) { 108 | let mut _x: f32 = 0.0; 109 | let mut _y: f32 = 0.0; 110 | 111 | unsafe { 112 | inCameraGetPosition(self.handle, &mut _x, &mut _y); 113 | } 114 | 115 | self.x = _x; 116 | self.y = _y; 117 | 118 | (_x, _y) 119 | } 120 | 121 | /// Get the Inochi2D Camera position offset 122 | /// 123 | /// # Example 124 | /// 125 | /// ~~~no_run 126 | /// let mut camera = Inochi2DCamera::new(/* ... */); 127 | /// 128 | /// let pos = camera.get_offset(); 129 | /// 130 | /// println!("Camera's offset is {} {}", pos.0, pos.1); 131 | /// 132 | /// ~~~ 133 | /// 134 | /// # Returns 135 | /// 136 | /// A tuple with two `f32` elements describing the cameras X and Y offset 137 | /// 138 | pub fn get_offset(&mut self) -> (f32, f32) { 139 | let mut _x: f32 = 0.0; 140 | let mut _y: f32 = 0.0; 141 | 142 | unsafe { 143 | inCameraGetCenterOffset(self.handle, &mut _x, &mut _y); 144 | } 145 | 146 | (_x, _y) 147 | } 148 | 149 | /// Get the Inochi2D Camera's real size 150 | /// 151 | /// # Example 152 | /// 153 | /// ~~~no_run 154 | /// let mut camera = Inochi2DCamera::new(/* ... */); 155 | /// 156 | /// let sz = camera.get_real_size(); 157 | /// 158 | /// println!("Camera's real size is {} {}", pos.0, pos.1); 159 | /// 160 | /// ~~~ 161 | /// 162 | /// # Returns 163 | /// 164 | /// A tuple with two `f32` elements describing the cameras real size in X and Y 165 | /// 166 | pub fn get_real_size(&mut self) -> (f32, f32) { 167 | let mut _x: f32 = 0.0; 168 | let mut _y: f32 = 0.0; 169 | 170 | unsafe { 171 | inCameraGetRealSize(self.handle, &mut _x, &mut _y); 172 | } 173 | 174 | (_x, _y) 175 | } 176 | 177 | /// Get the Inochi2D Camera's matrix 178 | /// 179 | /// # Example 180 | /// 181 | /// ~~~no_run 182 | /// let mut camera = Inochi2DCamera::new(/* ... */); 183 | /// 184 | /// let matrix = camera.get_matrix(); 185 | /// ~~~ 186 | /// 187 | /// # Returns 188 | /// 189 | /// A an array with 16 `f32` elements describing the cameras matrix 190 | /// 191 | pub fn get_matrix(&mut self) -> [f32; 16] { 192 | let mut matrix: [f32; 16] = [0.0; 16]; 193 | 194 | unsafe { 195 | inCameraGetMatrix(self.handle, &mut matrix); 196 | } 197 | 198 | matrix 199 | } 200 | 201 | /// Get the current Inochi2D camera and optionally set it's zoom and position 202 | /// 203 | /// # Example 204 | /// 205 | /// ~~~no_run 206 | /// let mut camera = Inochi2DCamera::new( 207 | /// Some(0.15), None, None 208 | /// ); 209 | /// 210 | /// ~~~ 211 | /// 212 | /// 213 | /// # Returns 214 | /// 215 | /// A new `Inochi2DCamera` 216 | /// 217 | /// 218 | pub fn new(zoom: Option, x: Option, y: Option) -> Self { 219 | unsafe { 220 | let hndl = inCameraGetCurrent(); 221 | let cam_zoom = zoom.unwrap_or_else(|| { 222 | let mut _z: f32 = 0.0; 223 | inCameraGetZoom(hndl, &mut _z); 224 | _z 225 | }); 226 | // TODO: find a better way to do this 227 | let cam_x = x.unwrap_or_else(|| { 228 | let mut _x: f32 = 0.0; 229 | let mut _y: f32 = 0.0; 230 | inCameraGetPosition(hndl, &mut _x, &mut _y); 231 | _x 232 | }); 233 | let cam_y = y.unwrap_or_else(|| { 234 | let mut _x: f32 = 0.0; 235 | let mut _y: f32 = 0.0; 236 | inCameraGetPosition(hndl, &mut _x, &mut _y); 237 | _y 238 | }); 239 | #[cfg(feature = "logging")] 240 | debug!( 241 | "Creating new camera (x: {} y: {} zoom: {})", 242 | cam_x, cam_y, cam_zoom 243 | ); 244 | 245 | inCameraSetZoom(hndl, cam_zoom); 246 | inCameraSetPosition(hndl, cam_x, cam_y); 247 | 248 | Inochi2DCamera { 249 | handle: hndl, 250 | zoom: cam_zoom, 251 | x: cam_x, 252 | y: cam_y, 253 | } 254 | } 255 | } 256 | } 257 | 258 | impl Drop for Inochi2DCamera { 259 | fn drop(&mut self) { 260 | #[cfg(feature = "logging")] 261 | debug!("Destroying camera"); 262 | unsafe { 263 | inCameraDestroy(self.handle); 264 | } 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/core.rs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022, Inochi2D Project 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Aki "lethalbit" Van Ness 6 | */ 7 | 8 | use crate::puppet::Inochi2DPuppet; 9 | use crate::Result; 10 | 11 | use std::path::PathBuf; 12 | 13 | #[cfg(feature = "logging")] 14 | use tracing::debug; 15 | 16 | use crate::ffi::{inCleanup, inUpdate, inInit, inViewportGet, inViewportSet, types::InTimingFunc}; 17 | 18 | pub struct Inochi2D { 19 | pub puppets: Vec, 20 | 21 | pub view_width: i32, 22 | pub view_height: i32, 23 | } 24 | 25 | impl Inochi2D { 26 | /// Add a new puppet to the Inochi2D context. 27 | /// 28 | /// # Example 29 | /// 30 | /// ~~~no_run 31 | /// let mut ctx = Inochi2D::new(/* ... */); 32 | /// 33 | /// ctx.add_puppet("./puppets/Ada.inx"); 34 | /// 35 | /// ~~~ 36 | /// 37 | pub fn add_puppet(&mut self, puppet: PathBuf) -> Result<()> { 38 | self.puppets.push(Inochi2DPuppet::new(puppet)?); 39 | 40 | Ok(()) 41 | } 42 | 43 | /// Update all puppets in the current context. 44 | /// 45 | /// # Example 46 | /// 47 | /// ~~~no_run 48 | /// let mut ctx = Inochi2D::new(/* ... */); 49 | /// 50 | /// ctx.update_puppets(); 51 | /// 52 | /// ~~~ 53 | /// 54 | pub fn update_puppets(&mut self) { 55 | #[cfg(feature = "logging")] 56 | debug!("Updating all puppets"); 57 | for p in self.puppets.iter_mut() { 58 | p.update() 59 | } 60 | } 61 | 62 | /// Draw all puppets in the current context. 63 | /// 64 | /// # Example 65 | /// 66 | /// ~~~no_run 67 | /// let mut ctx = Inochi2D::new(/* ... */); 68 | /// 69 | /// ctx.draw_puppets(); 70 | /// 71 | /// ~~~ 72 | /// 73 | #[cfg(feature = "opengl")] 74 | pub fn draw_puppets(&mut self) { 75 | #[cfg(feature = "logging")] 76 | debug!("Drawing all puppets"); 77 | for p in self.puppets.iter_mut() { 78 | p.draw() 79 | } 80 | } 81 | 82 | /// Set the viewport geometry for the current context. 83 | /// 84 | /// # Example 85 | /// 86 | /// ~~~no_run 87 | /// let mut ctx = Inochi2D::new(/* ... */); 88 | /// 89 | /// ctx.set_viewport(800, 600); 90 | /// 91 | /// ~~~ 92 | /// 93 | pub fn set_viewport(&mut self, w: i32, h: i32) { 94 | #[cfg(feature = "logging")] 95 | debug!("Setting viewport to {}x{}", w, h); 96 | unsafe { 97 | inViewportSet(w, h); 98 | } 99 | self.view_width = w; 100 | self.view_height = h; 101 | } 102 | 103 | /// Get the viewport geometry for the current context. 104 | /// 105 | /// # Example 106 | /// 107 | /// ~~~no_run 108 | /// let mut ctx = Inochi2D::new(/* ... */); 109 | /// 110 | /// let viewport = ctx.get_viewport(); 111 | /// 112 | /// println!("Viewport is {}x{}", viewport.0, viewport.1); 113 | /// 114 | /// ~~~ 115 | /// 116 | /// # Returns 117 | /// 118 | /// A tuple `(i32, i32)` with item 0 being the width and item 1 being the height. 119 | /// 120 | pub fn get_viewport(&mut self) -> (i32, i32) { 121 | let mut viewport_width: i32 = 0; 122 | let mut viewport_height: i32 = 0; 123 | 124 | unsafe { 125 | inViewportGet(&mut viewport_width, &mut viewport_height); 126 | } 127 | 128 | self.view_width = viewport_width; 129 | self.view_height = viewport_height; 130 | 131 | (viewport_width, viewport_height) 132 | } 133 | 134 | 135 | 136 | /// Update current Inochi2D context 137 | /// 138 | /// # Example 139 | /// 140 | /// ~~~no_run 141 | /// let mut ctx = Inochi2D::new(/* ... */); 142 | /// ctx.update(); 143 | /// ~~~ 144 | pub fn update(&mut self) { 145 | unsafe { 146 | inUpdate(); 147 | } 148 | } 149 | 150 | /// Initialize a new Inochi2D context. 151 | /// 152 | /// # Example 153 | /// 154 | /// ~~~no_run 155 | /// let mut ctx = Inochi2D::new(/* ... */); 156 | /// 157 | /// ~~~ 158 | /// 159 | /// # Returns 160 | /// 161 | /// A new `Inochi2D` context. 162 | /// 163 | pub fn new(timing: InTimingFunc, w: i32, h: i32) -> Self { 164 | #[cfg(feature = "logging")] 165 | debug!("Constructing Inochi2D"); 166 | 167 | unsafe { 168 | inInit(timing); 169 | inViewportSet(w, h); 170 | 171 | Inochi2D { 172 | puppets: Vec::new(), 173 | 174 | view_width: w, 175 | view_height: h, 176 | } 177 | } 178 | } 179 | } 180 | 181 | impl Drop for Inochi2D { 182 | fn drop(&mut self) { 183 | #[cfg(feature = "logging")] 184 | debug!("Disposing of Inochi2D"); 185 | self.puppets.clear(); 186 | 187 | unsafe { 188 | inCleanup(); 189 | } 190 | } 191 | } 192 | 193 | #[cfg(test)] 194 | mod tests { 195 | use crate::core::Inochi2D; 196 | 197 | extern "C" fn timing_func() -> f64 { 198 | 0.0 199 | } 200 | 201 | #[test] 202 | fn test_initialization() { 203 | let _ctx = Inochi2D::new(timing_func, 800, 600); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/ffi.rs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022, Inochi2D Project 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Aki "lethalbit" Van Ness 6 | */ 7 | 8 | pub mod types { 9 | use std::str::Utf8Error; 10 | 11 | #[repr(C)] 12 | #[derive(Debug, Copy, Clone)] 13 | pub struct InError { 14 | len: usize, 15 | msg: *const u8, 16 | } 17 | pub type InErrorPtr = *mut InError; 18 | 19 | impl TryFrom for String { 20 | type Error = Utf8Error; 21 | 22 | fn try_from(value: InError) -> Result { 23 | let slice = unsafe { std::slice::from_raw_parts(value.msg, value.len) }; 24 | 25 | std::str::from_utf8(slice).map(|x| x.to_string()) 26 | } 27 | } 28 | 29 | create_opaque_type!(InPuppet); 30 | pub type InPuppetPtr = *mut InPuppet; 31 | 32 | create_opaque_type!(InCamera); 33 | pub type InCameraPtr = *mut InCamera; 34 | 35 | create_opaque_type!(InRenderable); 36 | pub type InRenderablePtr = *mut InRenderable; 37 | 38 | pub type InTimingFunc = extern "C" fn() -> f64; 39 | } 40 | 41 | #[link(name = "inochi2d-c", kind = "dylib")] 42 | extern "C" { 43 | /* Core Functionality */ 44 | pub fn inInit(timing: types::InTimingFunc); 45 | pub fn inUpdate(); 46 | pub fn inCleanup(); 47 | pub fn inViewportSet(width: i32, height: i32); 48 | pub fn inViewportGet(width: *mut i32, height: *mut i32); 49 | 50 | #[cfg(feature = "opengl")] 51 | pub fn inSceneBegin(); 52 | #[cfg(feature = "opengl")] 53 | pub fn inSceneEnd(); 54 | #[cfg(feature = "opengl")] 55 | pub fn inSceneDraw(x: f32, y: f32, width: f32, height: f32); 56 | 57 | /* Error Stuff */ 58 | pub fn inErrorGet() -> types::InErrorPtr; 59 | 60 | /* Cameras */ 61 | pub fn inCameraGetCurrent() -> types::InCameraPtr; 62 | pub fn inCameraDestroy(camera: types::InCameraPtr); 63 | pub fn inCameraGetPosition(camera: types::InCameraPtr, x: *mut f32, y: *mut f32); 64 | pub fn inCameraSetPosition(camera: types::InCameraPtr, x: f32, y: f32); 65 | pub fn inCameraGetZoom(camera: types::InCameraPtr, zoom: *mut f32); 66 | pub fn inCameraSetZoom(camera: types::InCameraPtr, zoom: f32); 67 | pub fn inCameraGetCenterOffset(camera: types::InCameraPtr, x: *mut f32, y: *mut f32); 68 | pub fn inCameraGetRealSize(camera: types::InCameraPtr, x: *mut f32, y: *mut f32); 69 | pub fn inCameraGetMatrix(camera: types::InCameraPtr, mat4: *mut [f32; 16]); 70 | 71 | /* Puppets */ 72 | pub fn inPuppetLoad(path: *const u8) -> types::InPuppetPtr; 73 | pub fn inPuppetLoadEx(path: *const u8, len: usize) -> types::InPuppetPtr; 74 | pub fn inPuppetLoadFromMemory(data: *const u8, len: usize) -> types::InPuppetPtr; 75 | pub fn inPuppetDestroy(puppet: types::InPuppetPtr); 76 | pub fn inPuppetGetName(puppet: types::InPuppetPtr, name: *const u8, len: *const usize); 77 | pub fn inPuppetUpdate(puppet: types::InPuppetPtr); 78 | #[cfg(feature = "opengl")] 79 | pub fn inPuppetDraw(puppet: types::InPuppetPtr); 80 | } 81 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(feature = "nightly", feature(extern_types))] 2 | #![allow(unsafe_op_in_unsafe_fn, unused_unsafe)] 3 | /* 4 | Copyright © 2022, Inochi2D Project 5 | Distributed under the 2-Clause BSD License, see LICENSE file. 6 | 7 | Authors: Aki "lethalbit" Van Ness 8 | */ 9 | 10 | /*! 11 | An attempted idiomatic wrapper for [Inochi2D](https://github.com/Inochi2D/inochi2d). 12 | 13 | */ 14 | 15 | #[macro_use] 16 | mod macros; 17 | 18 | mod ffi; 19 | 20 | pub mod camera; 21 | pub mod core; 22 | pub mod puppet; 23 | #[cfg(feature = "opengl")] 24 | pub mod scene; 25 | 26 | use crate::core::Inochi2D; 27 | use std::path::PathBuf; 28 | 29 | pub type Result = std::result::Result; 30 | 31 | pub struct Inochi2DBuilder { 32 | viewport_size: (i32, i32), 33 | time_func: Option f64>, 34 | puppets: Vec, 35 | } 36 | 37 | #[cfg(feature = "monotonic")] 38 | use std::sync::Mutex; 39 | #[cfg(feature = "monotonic")] 40 | use std::time::Instant; 41 | 42 | /* A hacky Monotonic clock for Inochi2D */ 43 | #[cfg(feature = "monotonic")] 44 | pub extern "C" fn MONOTONIC_CLOCK() -> f64 { 45 | static mut START: Option> = None; 46 | if let Some(mutex) = unsafe { &START } { 47 | let start = mutex.lock().unwrap(); 48 | Instant::now().duration_since(*start).as_secs_f64() 49 | } else { 50 | unsafe { START.replace(Mutex::new(Instant::now())) }; 51 | 0.0_f64 52 | } 53 | } 54 | 55 | impl<'a> Inochi2DBuilder { 56 | /// Creates a new Inochi2D context builder. 57 | /// 58 | /// 59 | /// # Example 60 | /// ~~~no_run 61 | /// extern crate inochi2d_rs; 62 | /// 63 | /// use inochi2d_rs::Inochi2DBuilder; 64 | /// 65 | /// fn main() { 66 | /// let ctx = Inochi2DBuilder::new() 67 | /// .build() 68 | /// .expect("Unable to create Inochi2D context"); 69 | /// } 70 | /// ~~~ 71 | /// 72 | /// # Returns 73 | /// 74 | /// A new instance of `Inochi2DBuilder`. 75 | /// 76 | pub fn new() -> Self { 77 | Inochi2DBuilder { 78 | viewport_size: (800, 600), 79 | time_func: None, 80 | puppets: Vec::new(), 81 | } 82 | } 83 | 84 | /// Set Inochi2D viewport size. 85 | /// 86 | /// # Example 87 | /// ~~~no_run 88 | /// let ctx = Inochi2DBuilder::new() 89 | /// .viewport(800, 600) 90 | /// .build() 91 | /// .expect("Unable to create Inochi2D context"); 92 | /// ~~~ 93 | /// 94 | /// # Returns 95 | /// 96 | /// The current `Inochi2DBuilder` instance. 97 | /// 98 | pub fn viewport(mut self, w: i32, h: i32) -> Inochi2DBuilder { 99 | self.viewport_size = (w, h); 100 | self 101 | } 102 | 103 | /// Add a puppet to be loaded. 104 | /// 105 | /// # Example 106 | /// ~~~no_run 107 | /// let ctx = Inochi2DBuilder::new() 108 | /// .puppet("./puppets/Ada.idx") 109 | /// .build() 110 | /// .expect("Unable to create Inochi2D context"); 111 | /// ~~~ 112 | /// 113 | /// # Returns 114 | /// 115 | /// The current `Inochi2DBuilder` instance. 116 | /// 117 | pub fn puppet(mut self, puppet: PathBuf) -> Inochi2DBuilder { 118 | self.puppets.push(puppet); 119 | self 120 | } 121 | 122 | /// Add a puppet to be loaded. 123 | /// 124 | /// # Example 125 | /// ~~~no_run 126 | /// let ctx = Inochi2DBuilder::new() 127 | /// .timing(|| { 128 | /// 0.0 129 | /// }) 130 | /// .build() 131 | /// .expect("Unable to create Inochi2D context"); 132 | /// ~~~ 133 | /// 134 | /// # Returns 135 | /// 136 | /// The current `Inochi2DBuilder` instance. 137 | /// 138 | pub fn timing(mut self, func: extern "C" fn() -> f64) -> Inochi2DBuilder { 139 | self.time_func = Some(func); 140 | self 141 | } 142 | 143 | /// Create an Inochi2D context from the current builder. 144 | /// 145 | /// # Example 146 | /// ~~~no_run 147 | /// let ctx = Inochi2DBuilder::new() 148 | /// .build() 149 | /// .expect("Unable to create Inochi2D context"); 150 | /// ~~~ 151 | /// 152 | /// # Returns 153 | /// 154 | /// - If initialization was successful a `Inochi2D` context. 155 | /// - If an error occurred a string indicating the error will be returned. 156 | /// 157 | pub fn build(self) -> Result { 158 | if self.time_func.is_none() { 159 | Err("timing mut be called before build!".into()) 160 | } else { 161 | let mut ctx = Inochi2D::new( 162 | self.time_func.unwrap(), 163 | self.viewport_size.0, 164 | self.viewport_size.1, 165 | ); 166 | 167 | for p in self.puppets { 168 | ctx.add_puppet(p)?; 169 | } 170 | 171 | Ok(ctx) 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | macro_rules! create_opaque_type { 2 | ($type_name:ident) => { 3 | #[cfg(feature = "nightly")] 4 | extern "C" { 5 | pub type $type_name; 6 | } 7 | 8 | #[cfg(not(feature = "nightly"))] 9 | #[repr(C)] 10 | pub struct $type_name { 11 | _private: [u8; 0], 12 | } 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/puppet.rs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022, Inochi2D Project 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Aki "lethalbit" Van Ness 6 | */ 7 | 8 | use std::path::PathBuf; 9 | 10 | #[cfg(feature = "logging")] 11 | use tracing::debug; 12 | 13 | use crate::{ 14 | ffi::{inErrorGet, types::InPuppet}, 15 | Result, 16 | }; 17 | 18 | #[cfg(feature = "opengl")] 19 | use crate::ffi::inPuppetDraw; 20 | use crate::ffi::{ 21 | inPuppetDestroy, inPuppetGetName, inPuppetLoad, inPuppetLoadEx, inPuppetLoadFromMemory, 22 | inPuppetUpdate, types::InPuppetPtr, 23 | }; 24 | 25 | pub struct Inochi2DPuppet { 26 | handle: InPuppetPtr, 27 | pub name: String, 28 | } 29 | 30 | impl Inochi2DPuppet { 31 | pub fn from_raw_handle(handle: *mut InPuppet, name: String) -> Result { 32 | if handle.is_null() { 33 | let error_information = unsafe { inErrorGet() }; 34 | 35 | if error_information.is_null() { 36 | Err("Unknown error (are C bindings valid?)".into()) 37 | } else { 38 | let error_res = unsafe { String::try_from(*error_information) }; 39 | 40 | Err(error_res.unwrap_or_else(|_| "Unknown error (UTF-8 decoding failed!)".into())) 41 | } 42 | } else { 43 | Ok(Inochi2DPuppet { handle, name }) 44 | } 45 | } 46 | 47 | pub unsafe fn from_raw_parts( 48 | buffer: *const u8, 49 | size: usize, 50 | name: Option, 51 | ) -> Result { 52 | #[cfg(feature = "logging")] 53 | debug!("Constructing puppet from {} bytes", size); 54 | let hndl = unsafe { inPuppetLoadFromMemory(buffer, size) }; 55 | 56 | Self::from_raw_handle(hndl, name.unwrap_or(String::from(""))) 57 | } 58 | 59 | pub fn new(puppet: PathBuf) -> Result { 60 | let puppet_path = String::from(puppet.to_str().expect("Unable to get puppet path")); 61 | #[cfg(feature = "logging")] 62 | debug!("Constructing puppet from file {}", puppet_path); 63 | let hndl = unsafe { inPuppetLoadEx(puppet_path.as_ptr(), puppet_path.len()) }; 64 | 65 | Self::from_raw_handle(hndl, puppet_path) 66 | } 67 | 68 | pub fn update(&mut self) { 69 | #[cfg(feature = "logging")] 70 | debug!("Updating puppet {}", self.name); 71 | 72 | unsafe { 73 | inPuppetUpdate(self.handle); 74 | } 75 | } 76 | 77 | #[cfg(feature = "opengl")] 78 | pub fn draw(&mut self) { 79 | #[cfg(feature = "logging")] 80 | debug!("Drawing puppet {}", self.name); 81 | 82 | unsafe { 83 | inPuppetDraw(self.handle); 84 | } 85 | } 86 | } 87 | 88 | impl Drop for Inochi2DPuppet { 89 | fn drop(&mut self) { 90 | #[cfg(feature = "logging")] 91 | debug!("Disposing of puppet {}", self.name); 92 | 93 | unsafe { 94 | inPuppetDestroy(self.handle); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/scene.rs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022, Inochi2D Project 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Aki "lethalbit" Van Ness 6 | */ 7 | 8 | use crate::ffi::{inSceneBegin, inSceneDraw, inSceneEnd}; 9 | 10 | pub struct Inochi2DScene {} 11 | 12 | impl Inochi2DScene { 13 | pub fn new() -> Self { 14 | unsafe { 15 | inSceneBegin(); 16 | } 17 | 18 | Inochi2DScene {} 19 | } 20 | 21 | pub fn draw(&mut self, x: f32, y: f32, width: f32, height: f32) { 22 | unsafe { 23 | inSceneEnd(); 24 | inSceneDraw(x, y, width, height); 25 | inSceneBegin(); 26 | } 27 | } 28 | } 29 | 30 | impl Drop for Inochi2DScene { 31 | fn drop(&mut self) { 32 | unsafe { 33 | inSceneEnd(); 34 | } 35 | } 36 | } 37 | --------------------------------------------------------------------------------