├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── cubism-core-piston2d-renderer ├── Cargo.toml ├── examples │ ├── animate.rs │ ├── haru.rs │ └── ver4.rs └── src │ └── lib.rs ├── cubism-core-sys ├── Cargo.toml ├── build.rs └── src │ ├── lib.rs │ ├── moc.rs │ └── model.rs ├── cubism-core-wgpu-renderer ├── Cargo.toml ├── shader │ ├── default.frag │ ├── default.frag.spv │ ├── default.vert │ └── default.vert.spv └── src │ └── lib.rs ├── cubism-core ├── Cargo.toml └── src │ ├── error.rs │ ├── lib.rs │ ├── log.rs │ ├── mem.rs │ ├── moc.rs │ └── model.rs ├── cubism-examples ├── Cargo.toml └── src │ └── main.rs ├── rustfmt.toml └── src ├── controller.rs ├── controller ├── expression.rs └── eye_blink.rs ├── error.rs ├── expression.rs ├── id.rs ├── json.rs ├── json ├── cdi.rs ├── expression.rs ├── model.rs ├── motion.rs ├── physics.rs ├── pose.rs └── user_data.rs ├── lib.rs ├── model.rs ├── motion.rs ├── physics.rs └── util.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | Cargo.lock 3 | /third-party/ 4 | /.idea/ 5 | 6 | # macOS 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cubism" 3 | version = "0.1.0" 4 | authors = ["Lukas Wirth "] 5 | edition = "2018" 6 | repository = "https://github.com/Veykril/cubism-rs" 7 | homepage = "https://github.com/Veykril/cubism-rs" 8 | license = "MIT OR Apache-2.0" 9 | description = "A high level framework for the cubism sdk" 10 | keywords = ["live2d", "api-bindings", "cubism"] 11 | categories = ["api-bindings"] 12 | 13 | [dependencies] 14 | cubism-core = { version = "0.1.0", path = "cubism-core", default-features = false } 15 | serde_json = "^1.0" 16 | fxhash = "^0.2" 17 | 18 | [dependencies.serde] 19 | version = "^1.0" 20 | features = ["derive"] 21 | 22 | [features] 23 | default = ["static-link"] 24 | static-link = ["cubism-core/static-link"] 25 | 26 | [workspace] 27 | members = ["cubism-core-sys", "cubism-core", "cubism-examples", "cubism-core-wgpu-renderer",] 28 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019 Lukas Wirth 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Lukas Wirth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cubism-rs: Rust bindings for Live2D Cubism 2 | 3 | A rust wrapper around the [Live2D Cubism SDK](https://live2d.github.io/) with extra functionality. 4 | 5 | 6 | ## General usage notes 7 | 8 | The `cubism-core-sys` crate requires the Live2DCubismCore library to build and link properly. 9 | The build script finds the library by reading the environment variable `CUBISM_CORE` for the path. 10 | 11 | An example set up would be the following, where `CUBISM_CORE` would have the path of the third-party dir set to it. 12 | ``` 13 | your-project: 14 | ├─src/ 15 | │ └─... 16 | ├─third-party/ 17 | │ ├─Core/ 18 | │ │ ├─lib/ 19 | │ │ │ ├─linux/ 20 | │ │ │ │ └─x86_64/ 21 | │ │ │ │ └─Live2DCubismCore.a 22 | │ ... 23 | │ └─Samples/ # needed for the examples to run 24 | └─Cargo.toml 25 | 26 | ``` 27 | -------------------------------------------------------------------------------- /cubism-core-piston2d-renderer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cubism-core-piston2d-renderer" 3 | version = "0.1.0" 4 | authors = ["3c1u (Hikaru Terazono) <298748+3c1u@users.noreply.github.com>"] 5 | edition = "2018" 6 | license = "MIT OR Apache-2.0" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | cubism-core = { path = "../cubism-core", version = "0.1.0" } 12 | piston2d-graphics = "0.33.0" 13 | 14 | [dev-dependencies] 15 | cubism = { path = "../", version = "0.1.0" } 16 | piston_window = "0.101.0" 17 | piston = "0.49.0" 18 | glium = "0.25.1" 19 | piston2d-glium_graphics = "0.70.0" 20 | pistoncore-sdl2_window = "0.63.0" 21 | 22 | [[example]] 23 | name = "haru" 24 | 25 | [[example]] 26 | name = "animate" 27 | 28 | [[example]] 29 | name = "ver4" 30 | -------------------------------------------------------------------------------- /cubism-core-piston2d-renderer/examples/animate.rs: -------------------------------------------------------------------------------- 1 | use cubism::{expression::Expression, motion::Motion}; 2 | use cubism_core as core; 3 | use cubism_core_piston2d_renderer::*; 4 | use glium::backend::Facade; 5 | use glium_graphics::{Flip, Glium2d, GliumWindow, OpenGL, Texture, TextureSettings}; 6 | use piston::{input::*, window::WindowSettings}; 7 | use sdl2_window::*; 8 | 9 | fn load_textures(window: &mut F) -> [Texture; 2] { 10 | use std::{iter::FromIterator, path::PathBuf}; 11 | 12 | let res_path = PathBuf::from_iter(&[env!("CUBISM_CORE"), "Samples/Res"]); 13 | 14 | let tex0 = Texture::from_path( 15 | window, 16 | &res_path.join("Haru/Haru.2048/texture_00.png"), 17 | Flip::None, 18 | &TextureSettings::new(), 19 | ) 20 | .expect("Failed to load texture"); 21 | let tex1 = Texture::from_path( 22 | window, 23 | &res_path.join("Haru/Haru.2048/texture_01.png"), 24 | Flip::None, 25 | &TextureSettings::new(), 26 | ) 27 | .expect("Failed to load texture"); 28 | 29 | [tex0, tex1] 30 | } 31 | 32 | fn main() { 33 | let width = 600.0; 34 | let height = 850.0; 35 | let opengl = OpenGL::V3_2; 36 | 37 | let ref mut window: GliumWindow = 38 | WindowSettings::new("Animation Test: Haru", [width, height]) 39 | .exit_on_esc(true) 40 | .graphics_api(opengl) 41 | .samples(4) 42 | .build() 43 | .unwrap(); 44 | 45 | // Initialize Live2D Cubism logger 46 | core::set_core_logger(|s| println!("{}", s)); 47 | 48 | // Show information 49 | println!("Press X to switch expression, press M to switch motion."); 50 | 51 | // load model 52 | use std::{iter::FromIterator, path::PathBuf}; 53 | let res_path = PathBuf::from_iter(&[env!("CUBISM_CORE"), "Samples/Res"]); 54 | 55 | let textures = load_textures(window); 56 | let mut haru = 57 | core::Model::from_bytes(&std::fs::read(&res_path.join("Haru/Haru.moc3")).unwrap()[..]) 58 | .expect("Failed to load model."); 59 | let motions_path = &[ 60 | "Haru/motions/haru_g_idle.motion3.json", 61 | "Haru/motions/haru_g_m15.motion3.json", 62 | "Haru/motions/haru_g_m06.motion3.json", 63 | "Haru/motions/haru_g_m09.motion3.json", 64 | "Haru/motions/haru_g_m20.motion3.json", 65 | "Haru/motions/haru_g_m26.motion3.json", 66 | ]; 67 | let expressions_path = &[ 68 | "Haru/expressions/F01.exp3.json", 69 | "Haru/expressions/F02.exp3.json", 70 | "Haru/expressions/F03.exp3.json", 71 | "Haru/expressions/F04.exp3.json", 72 | "Haru/expressions/F05.exp3.json", 73 | "Haru/expressions/F06.exp3.json", 74 | "Haru/expressions/F07.exp3.json", 75 | "Haru/expressions/F08.exp3.json", 76 | ]; 77 | 78 | let mut motions_index: usize = 0; 79 | let mut motion = Motion::from_motion3_json(&res_path.join(motions_path[motions_index])) 80 | .expect("Failed to load motion."); 81 | let mut exp_index: usize = 0; 82 | let mut expression = 83 | Expression::from_exp3_json(&haru, &res_path.join(expressions_path[exp_index])) 84 | .expect("Failed to load expression."); 85 | 86 | // Play motion. 87 | motion.play(); 88 | motion.set_looped(false); // just for cosmetic... 89 | 90 | // initialize renderer 91 | let mut renderer = Renderer::new(); 92 | 93 | let mut g2d = Glium2d::new(opengl, window); 94 | 95 | while let Some(e) = window.next() { 96 | if let Some(v) = e.render_args() { 97 | use graphics::*; 98 | 99 | let viewport = v.draw_size; 100 | let mut target = window.draw(); 101 | 102 | motion.update(&mut haru); 103 | expression.apply(&mut haru, 1.0); 104 | 105 | haru.update(); 106 | 107 | g2d.draw(&mut target, v.viewport(), |c, g| { 108 | let t = c 109 | .transform 110 | .trans(viewport[0] as f64 * 0.5, viewport[1] as f64 * 0.5) 111 | .scale(0.2, 0.2); 112 | clear([1.0, 1.0, 1.0, 1.0], g); 113 | renderer.draw_model(g, t, &haru, &textures); 114 | }); 115 | 116 | target.finish().unwrap(); 117 | } 118 | 119 | if let Some(t) = e.update_args() { 120 | motion.tick(t.dt); 121 | } 122 | 123 | if let Some(b) = e.press_args() { 124 | if let Button::Keyboard(key) = b { 125 | match key { 126 | Key::X => { 127 | // switch expression 128 | exp_index = exp_index + 1; 129 | if exp_index == expressions_path.len() { 130 | exp_index = 0; 131 | } 132 | 133 | println!("Switched expression to {}", expressions_path[exp_index]); 134 | 135 | expression = Expression::from_exp3_json( 136 | &haru, 137 | &res_path.join(expressions_path[exp_index]), 138 | ) 139 | .expect("Failed to load expression."); 140 | }, 141 | Key::M => { 142 | // switch motion 143 | motions_index = motions_index + 1; 144 | if motions_index == motions_path.len() { 145 | motions_index = 0; 146 | } 147 | 148 | println!("Switched motion to {}", motions_path[motions_index]); 149 | 150 | motion = 151 | Motion::from_motion3_json(&res_path.join(motions_path[motions_index])) 152 | .expect("Failed to load motion."); 153 | motion.play(); 154 | motion.set_looped(false); 155 | }, 156 | _ => {}, 157 | } 158 | } 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /cubism-core-piston2d-renderer/examples/haru.rs: -------------------------------------------------------------------------------- 1 | use cubism_core as core; 2 | use cubism_core_piston2d_renderer::*; 3 | use glium::backend::Facade; 4 | use glium_graphics::{Flip, Glium2d, GliumWindow, OpenGL, Texture, TextureSettings}; 5 | use piston::{event_loop::EventLoop, input::*, window::WindowSettings}; 6 | use sdl2_window::*; 7 | 8 | fn load_textures(window: &mut F) -> [Texture; 2] { 9 | use std::{iter::FromIterator, path::PathBuf}; 10 | 11 | let res_path = PathBuf::from_iter(&[env!("CUBISM_CORE"), "Samples/Res"]); 12 | 13 | let tex0 = Texture::from_path( 14 | window, 15 | &res_path.join("Haru/Haru.2048/texture_00.png"), 16 | Flip::None, 17 | &TextureSettings::new(), 18 | ) 19 | .expect("Failed to load texture"); 20 | let tex1 = Texture::from_path( 21 | window, 22 | &res_path.join("Haru/Haru.2048/texture_01.png"), 23 | Flip::None, 24 | &TextureSettings::new(), 25 | ) 26 | .expect("Failed to load texture"); 27 | 28 | [tex0, tex1] 29 | } 30 | 31 | fn main() { 32 | let width = 600.0; 33 | let height = 850.0; 34 | let opengl = OpenGL::V3_2; 35 | 36 | let ref mut window: GliumWindow = WindowSettings::new("Haru", [width, height]) 37 | .exit_on_esc(true) 38 | .graphics_api(opengl) 39 | .samples(4) 40 | .build() 41 | .unwrap(); 42 | 43 | // Initialize Live2D Cubism logger 44 | core::set_core_logger(|s| println!("{}", s)); 45 | 46 | // load model 47 | use std::{iter::FromIterator, path::PathBuf}; 48 | let res_path = PathBuf::from_iter(&[env!("CUBISM_CORE"), "Samples/Res"]); 49 | 50 | let textures = load_textures(window); 51 | let mut haru = 52 | core::Model::from_bytes(&std::fs::read(&res_path.join("Haru/Haru.moc3")).unwrap()[..]) 53 | .expect("Failed to load model."); 54 | 55 | // initialize renderer 56 | let mut renderer = Renderer::new(); 57 | 58 | let mut g2d = Glium2d::new(opengl, window); 59 | window.set_lazy(true); 60 | 61 | while let Some(e) = window.next() { 62 | if let Some(v) = e.render_args() { 63 | use graphics::*; 64 | 65 | let viewport = v.draw_size; 66 | let mut target = window.draw(); 67 | 68 | haru.update(); 69 | 70 | g2d.draw(&mut target, v.viewport(), |c, g| { 71 | let t = c 72 | .transform 73 | .trans(viewport[0] as f64 * 0.5, viewport[1] as f64 * 0.5) 74 | .scale(0.2, 0.2); 75 | clear([1.0, 1.0, 1.0, 1.0], g); 76 | renderer.draw_model(g, t, &haru, &textures); 77 | }); 78 | 79 | target.finish().unwrap(); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /cubism-core-piston2d-renderer/examples/ver4.rs: -------------------------------------------------------------------------------- 1 | use cubism::motion::Motion; 2 | use cubism_core as core; 3 | use cubism_core_piston2d_renderer::*; 4 | use glium::backend::Facade; 5 | use glium_graphics::{Flip, Glium2d, GliumWindow, OpenGL, Texture, TextureSettings}; 6 | use piston::{input::*, window::WindowSettings}; 7 | use sdl2_window::*; 8 | 9 | fn load_textures(window: &mut F) -> [Texture; 2] { 10 | use std::{iter::FromIterator, path::PathBuf}; 11 | 12 | let res_path = PathBuf::from_iter(&[env!("CUBISM_CORE"), "Samples/Res"]); 13 | 14 | let tex0 = Texture::from_path( 15 | window, 16 | &res_path.join("Rice/Rice.2048/texture_00.png"), 17 | Flip::None, 18 | &TextureSettings::new(), 19 | ) 20 | .expect("Failed to load texture"); 21 | let tex1 = Texture::from_path( 22 | window, 23 | &res_path.join("Rice/Rice.2048/texture_01.png"), 24 | Flip::None, 25 | &TextureSettings::new(), 26 | ) 27 | .expect("Failed to load texture"); 28 | 29 | [tex0, tex1] 30 | } 31 | 32 | fn main() { 33 | let width = 800.0; 34 | let height = 800.0; 35 | let opengl = OpenGL::V3_2; 36 | 37 | let ref mut window: GliumWindow = 38 | WindowSettings::new("Animation Test: Rice", [width, height]) 39 | .exit_on_esc(true) 40 | .graphics_api(opengl) 41 | .samples(4) 42 | .build() 43 | .unwrap(); 44 | 45 | // Initialize Live2D Cubism logger 46 | core::set_core_logger(|s| println!("{}", s)); 47 | 48 | // Show information 49 | println!("Press X to switch expression."); 50 | 51 | // load model 52 | use std::{iter::FromIterator, path::PathBuf}; 53 | let res_path = PathBuf::from_iter(&[env!("CUBISM_CORE"), "Samples/Res"]); 54 | 55 | let textures = load_textures(window); 56 | let mut rice = 57 | core::Model::from_bytes(&std::fs::read(&res_path.join("Rice/Rice.moc3")).unwrap()[..]) 58 | .expect("Failed to load model."); 59 | let motions_path = &[ 60 | "Rice/motions/mtn_00.motion3.json", 61 | "Rice/motions/mtn_01.motion3.json", 62 | "Rice/motions/mtn_02.motion3.json", 63 | "Rice/motions/mtn_03.motion3.json", 64 | ]; 65 | 66 | let mut motions_index: usize = 0; 67 | let mut motion = Motion::from_motion3_json(&res_path.join(motions_path[motions_index])) 68 | .expect("Failed to load motion."); 69 | // Play motion. 70 | motion.play(); 71 | motion.set_looped(false); // just for cosmetic... 72 | 73 | // initialize renderer 74 | let mut renderer = Renderer::new(); 75 | 76 | let mut g2d = Glium2d::new(opengl, window); 77 | 78 | while let Some(e) = window.next() { 79 | if let Some(v) = e.render_args() { 80 | use graphics::*; 81 | 82 | let viewport = v.draw_size; 83 | let mut target = window.draw(); 84 | 85 | motion.update(&mut rice).unwrap(); 86 | 87 | rice.update(); 88 | 89 | g2d.draw(&mut target, v.viewport(), |c, g| { 90 | let t = c 91 | .transform 92 | .trans(viewport[0] as f64 * 0.5, viewport[1] as f64 * 0.5) 93 | .scale(0.25, 0.25); 94 | clear([1.0, 1.0, 1.0, 1.0], g); 95 | renderer.draw_model(g, t, &rice, &textures); 96 | }); 97 | 98 | target.finish().unwrap(); 99 | } 100 | 101 | if let Some(t) = e.update_args() { 102 | motion.tick(t.dt); 103 | } 104 | 105 | if let Some(b) = e.press_args() { 106 | if let Button::Keyboard(key) = b { 107 | match key { 108 | Key::M => { 109 | // switch motion 110 | motions_index = motions_index + 1; 111 | if motions_index == motions_path.len() { 112 | motions_index = 0; 113 | } 114 | 115 | println!("Switched motion to {}", motions_path[motions_index]); 116 | 117 | motion = 118 | Motion::from_motion3_json(&res_path.join(motions_path[motions_index])) 119 | .expect("Failed to load motion."); 120 | motion.play(); 121 | motion.set_looped(false); 122 | }, 123 | _ => {}, 124 | } 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /cubism-core-piston2d-renderer/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A Live2D Cubism renderer for [Piston](https://www.piston.rs/). 2 | //! 3 | //! Example: 4 | //! ``` 5 | //! use cubism_core as core; 6 | //! use cubism_core_piston2d_renderer::*; 7 | //! use piston_window::*; 8 | //! use sdl2_window::*; 9 | //! 10 | //! fn main() { 11 | //! let width = 600.0; 12 | //! let height = 850.0; 13 | //! let opengl = OpenGL::V3_2; 14 | //! 15 | //! let mut window: PistonWindow = WindowSettings::new("Haru", [width, height]) 16 | //! .exit_on_esc(true) 17 | //! .graphics_api(opengl) 18 | //! .samples(16) 19 | //! .build() 20 | //! .unwrap(); 21 | //! 22 | //! let mut t = window.create_texture_context(); 23 | //! 24 | //! // Initialize Live2D Cubism logger 25 | //! core::set_core_logger(|s| println!("{}", s)); 26 | //! 27 | //! // load model 28 | //! use std::{iter::FromIterator, path::PathBuf}; 29 | //! 30 | //! let res_path = PathBuf::from_iter(&[env!("CUBISM_CORE"), "Samples/Res"]); 31 | //! 32 | //! let tex0 = Texture::from_path( 33 | //! &mut t, 34 | //! &res_path.join("Haru/Haru.2048/texture_00.png"), 35 | //! Flip::None, 36 | //! &TextureSettings::new(), 37 | //! ) 38 | //! .expect("Failed to load texture"); 39 | //! let tex1 = Texture::from_path( 40 | //! &mut t, 41 | //! &res_path.join("Haru/Haru.2048/texture_01.png"), 42 | //! Flip::None, 43 | //! &TextureSettings::new(), 44 | //! ) 45 | //! .expect("Failed to load texture"); 46 | //! let textures = [tex0, tex1]; 47 | //! 48 | //! let mut haru = 49 | //! core::Model::from_bytes(&std::fs::read(&res_path.join("Haru/Haru.moc3")).unwrap()[..]) 50 | //! .expect("Failed to load model."); 51 | //! 52 | //! // initialize renderer 53 | //! let mut renderer = Renderer::new(); 54 | //! 55 | //! while let Some(e) = window.next() { 56 | //! if let Some(v) = e.render_args() { 57 | //! let viewport = v.draw_size; 58 | //! 59 | //! haru.update(); 60 | //! 61 | //! window.draw_2d(&e, |c, g, _d| { 62 | //! let t = c 63 | //! .transform 64 | //! .trans(viewport[0] as f64 * 0.5, viewport[1] as f64 * 0.5) 65 | //! .scale(0.2, 0.2); 66 | //! clear([1.0, 1.0, 1.0, 1.0], g); 67 | //! renderer.draw_model(g, t, &haru, &textures); 68 | //! }); 69 | //! } 70 | //! } 71 | //! } 72 | //! ``` 73 | 74 | #![deny(missing_docs)] 75 | 76 | use cubism_core::Model; 77 | use graphics::{math::Matrix2d, DrawState, Graphics, ImageSize}; 78 | 79 | /// Live2D Cubism renderer for [Piston](https://www.piston.rs/). 80 | pub struct Renderer {} 81 | 82 | impl Default for Renderer { 83 | fn default() -> Self { 84 | Self::new() 85 | } 86 | } 87 | 88 | impl Renderer { 89 | /// Initializes a renderer. 90 | pub fn new() -> Renderer { 91 | Renderer {} 92 | } 93 | 94 | /// Draws a model. 95 | /// 96 | /// A model is drawn at (0, 0) pixel-by-pixel. In order to resize, relocate, 97 | /// or rotate, manipulate `transform`. 98 | pub fn draw_model( 99 | &mut self, 100 | g: &mut G, 101 | transform: Matrix2d, 102 | model: &Model, 103 | textures: &[T], 104 | ) where 105 | G: Graphics, 106 | T: ImageSize, 107 | { 108 | let mut sorted_draw_indices = vec![0; model.drawable_count()]; 109 | 110 | for (idx, order) in model.drawable_render_orders().iter().enumerate() { 111 | sorted_draw_indices[*order as usize] = idx; 112 | } 113 | 114 | for draw_idx in sorted_draw_indices { 115 | self.draw_mesh(g, transform, model, draw_idx, textures, None); 116 | } 117 | } 118 | 119 | fn draw_mesh( 120 | &mut self, 121 | g: &mut G, 122 | transform: Matrix2d, 123 | model: &Model, 124 | index: usize, 125 | textures: &[T], 126 | draw_state: Option, 127 | ) where 128 | G: Graphics, 129 | T: ImageSize, 130 | { 131 | use cubism_core::{ConstantFlags, DynamicFlags}; 132 | use graphics::draw_state::Blend; 133 | 134 | let opacity = model.drawable_opacities()[index]; 135 | let dyn_flags = model.drawable_dynamic_flags()[index]; 136 | 137 | if draw_state.is_none() 138 | && (opacity <= 0.0 || !dyn_flags.intersects(DynamicFlags::IS_VISIBLE)) 139 | { 140 | return; 141 | } 142 | 143 | let blend_mode = model.drawable_constant_flags()[index]; 144 | 145 | let draw_state = if let Some(draw_state) = draw_state { 146 | draw_state 147 | } else { 148 | // generate masks 149 | let masks = model.drawable_masks()[index]; 150 | 151 | let state = if masks.is_empty() { 152 | DrawState::new_alpha() 153 | } else { 154 | let state = DrawState::new_clip(); 155 | for i in masks { 156 | self.draw_mesh(g, transform, model, *i as usize, textures, Some(state)); 157 | } 158 | 159 | if blend_mode.intersects(ConstantFlags::IS_INVERTED_MASK) { 160 | DrawState::new_outside() 161 | } else { 162 | DrawState::new_inside() 163 | } 164 | }; 165 | 166 | if blend_mode.intersects(ConstantFlags::BLEND_ADDITIVE) { 167 | state.blend(Blend::Lighter) 168 | } else if blend_mode.intersects(ConstantFlags::BLEND_MULTIPLICATIVE) { 169 | state.blend(Blend::Multiply) 170 | } else { 171 | state.blend(Blend::Alpha) 172 | } 173 | }; 174 | 175 | let draw_state = &draw_state; 176 | 177 | // obtain pixel-per-unit information 178 | let (_, _, ppu) = model.canvas_info(); 179 | 180 | let vtx_pos = model.drawable_vertex_positions(index); 181 | let vtx_uv = model.drawable_vertex_uvs(index); 182 | let idx_buffer = model.drawable_indices()[index]; 183 | 184 | let tex = &textures[model.drawable_texture_indices()[index] as usize]; 185 | 186 | let mut pos = Vec::with_capacity(idx_buffer.len()); 187 | let mut uv = Vec::with_capacity(idx_buffer.len()); 188 | 189 | use graphics::triangulation::{tx, ty}; 190 | 191 | // extracts positions and UVs since Piston does not support index buffer objects 192 | for i in idx_buffer { 193 | let i = usize::from(*i); 194 | 195 | let [x, y] = vtx_pos[i]; 196 | let (x, y) = (f64::from(x * ppu), f64::from(-y * ppu)); 197 | 198 | let tp = [tx(transform, x, y), ty(transform, x, y)]; 199 | pos.push(tp); 200 | uv.push([vtx_uv[i][0], 1.0 - vtx_uv[i][1]]); 201 | } 202 | 203 | let mut pos = &pos[0..]; 204 | let mut uv = &uv[0..]; 205 | 206 | // split by maximum vertex count and draw triangles 207 | use graphics::BACK_END_MAX_VERTEX_COUNT; 208 | while pos.len() >= BACK_END_MAX_VERTEX_COUNT { 209 | g.tri_list_uv(draw_state, &[1.0, 1.0, 1.0, opacity], tex, |f| { 210 | f( 211 | &pos[0..BACK_END_MAX_VERTEX_COUNT], 212 | &uv[0..BACK_END_MAX_VERTEX_COUNT], 213 | ); 214 | }); 215 | 216 | pos = &pos[BACK_END_MAX_VERTEX_COUNT..]; 217 | uv = &uv[BACK_END_MAX_VERTEX_COUNT..]; 218 | } 219 | 220 | // then, draw the rest 221 | g.tri_list_uv(draw_state, &[1.0, 1.0, 1.0, opacity], tex, |f| { 222 | f(&pos, &uv); 223 | }); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /cubism-core-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cubism-core-sys" 3 | version = "0.1.0" 4 | authors = ["Lukas Wirth "] 5 | edition = "2018" 6 | repository = "https://github.com/Veykril/cubism-rs" 7 | homepage = "https://github.com/Veykril/cubism-rs" 8 | license = "MIT OR Apache-2.0" 9 | description = "Raw ffi bindings for Live2d Cubism Core" 10 | keywords = ["live2d", "api-bindings", "cubism"] 11 | categories = ["external-ffi-bindings"] 12 | build = "build.rs" 13 | links = "Live2DCubismCore" 14 | 15 | [features] 16 | default = ["static-link"] 17 | static-link = [] 18 | -------------------------------------------------------------------------------- /cubism-core-sys/build.rs: -------------------------------------------------------------------------------- 1 | use std::{env, path::PathBuf}; 2 | 3 | fn lookup_cubism_core(arch: &str, vendor: &str, sys: &str, abi: &str, linking_strategy: &str) { 4 | let mut lib_dir = if let Ok(lib_dir) = env::var("CUBISM_CORE").map(PathBuf::from) { 5 | lib_dir 6 | } else { 7 | // TODO: this won't appear unless the compilation fails. 8 | println!( 9 | "cargo:warning=it seems that the CUBISM_CORE environment variable is not set. \ 10 | Please set it to your Live2DCubismCore directory before compiling, \ 11 | or specify the Live2DCubismCore library manually. \ 12 | Check out https://github.com/Veykril/cubism-rs for more information." 13 | ); 14 | return; 15 | }; 16 | 17 | if linking_strategy == "static" { 18 | lib_dir.push("Core/lib"); 19 | } else { 20 | lib_dir.push("Core/dll"); 21 | } 22 | 23 | match (vendor, sys) { 24 | ("pc", "windows") => { 25 | lib_dir.push("windows"); 26 | lib_dir.push(match arch { 27 | "x86_64" => "x86_64", 28 | "i686" => "x86", 29 | _ => panic!("unknown windows architecture: {}", arch), 30 | }); 31 | lib_dir.push("140"); 32 | }, 33 | ("apple", "darwin") => { 34 | if linking_strategy != "static" { 35 | panic!( 36 | "since Live2DCubismCore is in MH_BUNDLE format (which is deprecated), \ 37 | dynamic linking on macOS is not supported. \ 38 | See https://github.com/Veykril/cubism-rs for more information." 39 | ); 40 | } 41 | if arch == "i686" { 42 | panic!("no 32-bit support for macOS."); 43 | } 44 | 45 | lib_dir.push("macos"); 46 | }, 47 | ("apple", "ios") => { 48 | unimplemented!("TODO: implement ios linking"); 49 | }, 50 | ("unknown", "linux") => { 51 | lib_dir.push("linux"); 52 | lib_dir.push(match arch { 53 | "x86_64" => "x86_64", 54 | _ => panic!("linux is only supported on x86_64"), 55 | }); 56 | }, 57 | ("linux", "android") | ("linux", "androideabi") => { 58 | lib_dir.push("android"); 59 | lib_dir.push(match arch { 60 | "i686" => "x86", 61 | "armv7" => "armeabi-v7a", 62 | "aarch64" => "arm64-v8a", 63 | _ => panic!("unsupported android architecture: {}", arch), 64 | }); 65 | }, 66 | _ => panic!( 67 | "unsupported target triple: {}-{}-{}-{}", 68 | arch, vendor, sys, abi 69 | ), 70 | } 71 | println!("cargo:rustc-link-search=all={}", lib_dir.display()); 72 | } 73 | 74 | fn main() { 75 | println!("cargo:rerun-if-env-changed=CUBISM_CORE"); 76 | let target = env::var("TARGET").unwrap(); 77 | let (arch, vendor, sys, abi) = { 78 | let mut target_s = target.split('-'); 79 | ( 80 | target_s.next().unwrap_or(""), 81 | target_s.next().unwrap_or(""), 82 | target_s.next().unwrap_or(""), 83 | target_s.next().unwrap_or(""), 84 | ) 85 | }; 86 | 87 | let linking_strategy = if cfg!(feature = "static-link") { 88 | "static" 89 | } else { 90 | "dylib" 91 | }; 92 | 93 | lookup_cubism_core(arch, vendor, sys, abi, linking_strategy); 94 | 95 | let profile = env::var("PROFILE").unwrap_or_default(); 96 | 97 | match (vendor, sys, &*profile) { 98 | ("pc", "windows", "debug") => println!( 99 | "cargo:rustc-link-lib={}=Live2DCubismCore_MTd", 100 | linking_strategy 101 | ), 102 | ("pc", "windows", "release") => println!( 103 | "cargo:rustc-link-lib={}=Live2DCubismCore_MT", 104 | linking_strategy 105 | ), 106 | _ => println!("cargo:rustc-link-lib={}=Live2DCubismCore", linking_strategy), 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /cubism-core-sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_camel_case_types, non_snake_case, non_upper_case_globals)] 2 | #![allow(clippy::identity_op)] 3 | 4 | pub mod moc; 5 | pub mod model; 6 | 7 | pub use self::{moc::*, model::*}; 8 | 9 | use std::os::raw::{c_char, c_float, c_uint}; 10 | 11 | pub type csmVersion = c_uint; 12 | 13 | #[repr(C)] 14 | #[derive(Copy, Clone, Debug)] 15 | pub struct csmVector2 { 16 | pub x: c_float, 17 | pub y: c_float, 18 | } 19 | 20 | extern "C" { 21 | pub fn csmGetVersion() -> csmVersion; 22 | } 23 | 24 | pub type csmLogFunction = Option; 25 | 26 | extern "C" { 27 | pub fn csmGetLogFunction() -> csmLogFunction; 28 | pub fn csmSetLogFunction(handler: csmLogFunction); 29 | } 30 | -------------------------------------------------------------------------------- /cubism-core-sys/src/moc.rs: -------------------------------------------------------------------------------- 1 | use std::os::raw::{c_uint, c_void}; 2 | 3 | pub const csmAlignofMoc: usize = 64; 4 | 5 | pub const csmMocVersion_Unknown: csmMocVersion = 0; 6 | pub const csmMocVersion_30: csmMocVersion = 1; 7 | pub const csmMocVersion_33: csmMocVersion = 2; 8 | pub const csmMocVersion_40: csmMocVersion = 3; 9 | 10 | pub type csmMocVersion = c_uint; 11 | 12 | #[repr(C, align(64))] 13 | #[derive(Copy, Clone, Debug)] 14 | pub struct csmMoc { 15 | _unused: [u64; 0], 16 | } 17 | 18 | extern "C" { 19 | pub fn csmGetLatestMocVersion() -> csmMocVersion; 20 | pub fn csmGetMocVersion(address: *const c_void, size: c_uint) -> csmMocVersion; 21 | pub fn csmReviveMocInPlace(aligned_address: *mut c_void, size: c_uint) -> *mut csmMoc; 22 | } 23 | 24 | #[test] 25 | fn alignment() { 26 | assert_eq!(::std::mem::align_of::(), csmAlignofMoc); 27 | } 28 | -------------------------------------------------------------------------------- /cubism-core-sys/src/model.rs: -------------------------------------------------------------------------------- 1 | use std::os::raw::{c_char, c_float, c_int, c_uchar, c_uint, c_ushort, c_void}; 2 | 3 | use crate::{csmVector2, moc::csmMoc}; 4 | 5 | pub const csmAlignofModel: usize = 16; 6 | 7 | pub const csmBlendAdditive: csmFlags = 1 << 0; 8 | pub const csmBlendMultiplicative: csmFlags = 1 << 1; 9 | pub const csmIsDoubleSided: csmFlags = 1 << 2; 10 | pub const csmIsInvertedMask: csmFlags = 1 << 3; 11 | 12 | pub const csmIsVisible: csmFlags = 1 << 0; 13 | pub const csmVisibilityDidChange: csmFlags = 1 << 1; 14 | pub const csmOpacityDidChange: csmFlags = 1 << 2; 15 | pub const csmDrawOrderDidChange: csmFlags = 1 << 3; 16 | pub const csmRenderOrderDidChange: csmFlags = 1 << 4; 17 | pub const csmVertexPositionsDidChange: csmFlags = 1 << 5; 18 | 19 | pub type csmFlags = c_uchar; 20 | 21 | #[repr(C, align(16))] 22 | #[derive(Copy, Clone, Debug)] 23 | pub struct csmModel { 24 | _unused: [u16; 0], 25 | } 26 | 27 | extern "C" { 28 | pub fn csmGetSizeofModel(moc: *const csmMoc) -> c_uint; 29 | pub fn csmInitializeModelInPlace( 30 | moc: *const csmMoc, 31 | aligned_address: *mut c_void, 32 | size: c_uint, 33 | ) -> *mut csmModel; 34 | pub fn csmUpdateModel(model: *mut csmModel); 35 | pub fn csmReadCanvasInfo( 36 | model: *const csmModel, 37 | outSizeInPixels: *mut csmVector2, 38 | outOriginalInPixels: *mut csmVector2, 39 | outPixelsPerUnit: *mut c_float, 40 | ); 41 | 42 | pub fn csmGetParameterCount(model: *const csmModel) -> c_int; 43 | pub fn csmGetParameterIds(model: *const csmModel) -> *mut *const c_char; 44 | pub fn csmGetParameterMinimumValues(model: *const csmModel) -> *const c_float; 45 | pub fn csmGetParameterMaximumValues(model: *const csmModel) -> *const c_float; 46 | pub fn csmGetParameterDefaultValues(model: *const csmModel) -> *const c_float; 47 | pub fn csmGetParameterValues(model: *mut csmModel) -> *mut c_float; 48 | 49 | pub fn csmGetPartCount(model: *const csmModel) -> c_int; 50 | pub fn csmGetPartIds(model: *const csmModel) -> *mut *const c_char; 51 | pub fn csmGetPartOpacities(model: *mut csmModel) -> *mut c_float; 52 | pub fn csmGetPartParentPartIndices(model: *const csmModel) -> *const c_int; 53 | 54 | pub fn csmGetDrawableCount(model: *const csmModel) -> c_int; 55 | pub fn csmGetDrawableIds(model: *const csmModel) -> *mut *const c_char; 56 | pub fn csmGetDrawableConstantFlags(model: *const csmModel) -> *const csmFlags; 57 | pub fn csmGetDrawableDynamicFlags(model: *const csmModel) -> *const csmFlags; 58 | pub fn csmGetDrawableTextureIndices(model: *const csmModel) -> *const c_int; 59 | pub fn csmGetDrawableDrawOrders(model: *const csmModel) -> *const c_int; 60 | pub fn csmGetDrawableRenderOrders(model: *const csmModel) -> *const c_int; 61 | pub fn csmGetDrawableOpacities(model: *const csmModel) -> *const c_float; 62 | pub fn csmGetDrawableMaskCounts(model: *const csmModel) -> *const c_int; 63 | pub fn csmGetDrawableMasks(model: *const csmModel) -> *mut *const c_int; 64 | pub fn csmGetDrawableVertexCounts(model: *const csmModel) -> *const c_int; 65 | pub fn csmGetDrawableVertexPositions(model: *const csmModel) -> *mut *const csmVector2; 66 | pub fn csmGetDrawableVertexUvs(model: *const csmModel) -> *mut *const csmVector2; 67 | pub fn csmGetDrawableIndexCounts(model: *const csmModel) -> *const c_int; 68 | pub fn csmGetDrawableIndices(model: *const csmModel) -> *mut *const c_ushort; 69 | pub fn csmResetDrawableDynamicFlags(model: *mut csmModel); 70 | } 71 | 72 | #[test] 73 | fn model_alignment() { 74 | assert_eq!(::std::mem::align_of::(), csmAlignofModel); 75 | } 76 | -------------------------------------------------------------------------------- /cubism-core-wgpu-renderer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cubism-core-wgpu-renderer" 3 | version = "0.1.0" 4 | authors = ["Lukas Wirth "] 5 | edition = "2018" 6 | repository = "https://github.com/Veykril/cubism-rs" 7 | homepage = "https://github.com/Veykril/cubism-rs" 8 | license = "MIT OR Apache-2.0" 9 | 10 | [dependencies] 11 | cubism-core = { path = "../cubism-core", version = "0.1.0" } 12 | wgpu = "0.4" -------------------------------------------------------------------------------- /cubism-core-wgpu-renderer/shader/default.frag: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | layout (location = 0) in vec2 i_uv; 4 | 5 | layout(set = 1, binding = 0) uniform texture2D u_tex; 6 | layout(set = 1, binding = 1) uniform sampler s_tex; 7 | 8 | layout (location = 0) out vec4 o_target; 9 | 10 | void main() { 11 | o_target = texture(sampler2D(u_tex, s_tex), i_uv); 12 | } 13 | -------------------------------------------------------------------------------- /cubism-core-wgpu-renderer/shader/default.frag.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Veykril/cubism-rs/dada83d8ed43fcdd059f8dd2a802bc18ac52e4a8/cubism-core-wgpu-renderer/shader/default.frag.spv -------------------------------------------------------------------------------- /cubism-core-wgpu-renderer/shader/default.vert: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | layout (location = 0) in vec2 in_pos; 4 | layout (location = 1) in vec2 in_tex_coords; 5 | 6 | layout(set = 0, binding = 0) uniform View { mat4 u_mvp; }; 7 | 8 | layout (location = 0) out vec2 out_tex_coords; 9 | 10 | void main() { 11 | gl_Position = u_mvp * vec4(in_pos, 0.0, 1.0); 12 | out_tex_coords = in_tex_coords; 13 | } 14 | -------------------------------------------------------------------------------- /cubism-core-wgpu-renderer/shader/default.vert.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Veykril/cubism-rs/dada83d8ed43fcdd059f8dd2a802bc18ac52e4a8/cubism-core-wgpu-renderer/shader/default.vert.spv -------------------------------------------------------------------------------- /cubism-core-wgpu-renderer/src/lib.rs: -------------------------------------------------------------------------------- 1 | use wgpu::*; 2 | 3 | #[derive(Copy, Clone, Debug)] 4 | struct Vertex { 5 | pos: [f32; 2], 6 | uv: [f32; 2], 7 | } 8 | 9 | struct BoundTexture { 10 | bind_group: wgpu::BindGroup, 11 | } 12 | 13 | impl BoundTexture { 14 | pub fn new( 15 | texture: wgpu::Texture, 16 | sampler: &wgpu::Sampler, 17 | layout: &wgpu::BindGroupLayout, 18 | device: &wgpu::Device, 19 | ) -> Self { 20 | let view = texture.create_default_view(); 21 | 22 | // Create the texture bind group from the layout. 23 | let bind_group = device.create_bind_group(&BindGroupDescriptor { 24 | layout, 25 | bindings: &[ 26 | Binding { 27 | binding: 0, 28 | resource: BindingResource::TextureView(&view), 29 | }, 30 | Binding { 31 | binding: 1, 32 | resource: BindingResource::Sampler(sampler), 33 | }, 34 | ], 35 | }); 36 | 37 | BoundTexture { bind_group } 38 | } 39 | 40 | pub fn make_sampler(device: &wgpu::Device) -> wgpu::Sampler { 41 | device.create_sampler(&wgpu::SamplerDescriptor { 42 | address_mode_u: wgpu::AddressMode::ClampToEdge, 43 | address_mode_v: wgpu::AddressMode::ClampToEdge, 44 | address_mode_w: wgpu::AddressMode::ClampToEdge, 45 | mag_filter: wgpu::FilterMode::Nearest, 46 | min_filter: wgpu::FilterMode::Linear, 47 | mipmap_filter: wgpu::FilterMode::Nearest, 48 | lod_min_clamp: -100.0, 49 | lod_max_clamp: 100.0, 50 | compare_function: wgpu::CompareFunction::Always, 51 | }) 52 | } 53 | 54 | pub fn layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { 55 | device.create_bind_group_layout(&BindGroupLayoutDescriptor { 56 | bindings: &[ 57 | BindGroupLayoutBinding { 58 | binding: 0, 59 | visibility: wgpu::ShaderStage::FRAGMENT, 60 | ty: BindingType::SampledTexture { 61 | multisampled: false, 62 | dimension: TextureViewDimension::D2, 63 | }, 64 | }, 65 | BindGroupLayoutBinding { 66 | binding: 1, 67 | visibility: wgpu::ShaderStage::FRAGMENT, 68 | ty: BindingType::Sampler, 69 | }, 70 | ], 71 | }) 72 | } 73 | } 74 | 75 | pub struct Renderer { 76 | pipeline: RenderPipeline, 77 | uniform_buffer: Buffer, 78 | uniform_bind_group: BindGroup, 79 | textures: Vec, 80 | texture_layout: BindGroupLayout, 81 | vertex_buffers: Vec, 82 | index_buffers: Vec<(wgpu::Buffer, usize)>, 83 | } 84 | 85 | impl Renderer { 86 | /// Initializes a renderer. 87 | pub fn new( 88 | model: &cubism_core::Model, 89 | device: &Device, 90 | queue: &mut Queue, 91 | format: TextureFormat, 92 | textures: impl IntoIterator, 93 | ) -> Renderer { 94 | let vert = wgpu::read_spirv(std::io::Cursor::new( 95 | &include_bytes!("../shader/default.vert.spv")[..], 96 | )) 97 | .expect("vert"); 98 | let frag = wgpu::read_spirv(std::io::Cursor::new( 99 | &include_bytes!("../shader/default.frag.spv")[..], 100 | )) 101 | .expect("frag"); 102 | let vs_module = device.create_shader_module(&vert); 103 | let fs_module = device.create_shader_module(&frag); 104 | 105 | // Create the uniform matrix buffer. 106 | let uniform_buffer = device 107 | .create_buffer_mapped::( 108 | 16, 109 | wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST, 110 | ) 111 | .fill_from_slice(&[ 112 | 1.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 113 | ]); 114 | let uniform_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor { 115 | bindings: &[BindGroupLayoutBinding { 116 | binding: 0, 117 | visibility: wgpu::ShaderStage::VERTEX, 118 | ty: BindingType::UniformBuffer { dynamic: false }, 119 | }], 120 | }); 121 | let uniform_bind_group = device.create_bind_group(&BindGroupDescriptor { 122 | layout: &uniform_layout, 123 | bindings: &[Binding { 124 | binding: 0, 125 | resource: BindingResource::Buffer { 126 | buffer: &uniform_buffer, 127 | range: 0..64, 128 | }, 129 | }], 130 | }); 131 | 132 | let texture_layout = BoundTexture::layout(device); 133 | let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { 134 | bind_group_layouts: &[&uniform_layout, &texture_layout], 135 | }); 136 | 137 | // Create the render pipeline. 138 | let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor { 139 | layout: &pipeline_layout, 140 | vertex_stage: ProgrammableStageDescriptor { 141 | module: &vs_module, 142 | entry_point: "main", 143 | }, 144 | fragment_stage: Some(ProgrammableStageDescriptor { 145 | module: &fs_module, 146 | entry_point: "main", 147 | }), 148 | rasterization_state: Some(RasterizationStateDescriptor { 149 | front_face: FrontFace::Cw, 150 | cull_mode: CullMode::None, 151 | depth_bias: 0, 152 | depth_bias_slope_scale: 0.0, 153 | depth_bias_clamp: 0.0, 154 | }), 155 | primitive_topology: PrimitiveTopology::TriangleList, 156 | color_states: &[ColorStateDescriptor { 157 | format, 158 | color_blend: BlendDescriptor { 159 | src_factor: BlendFactor::SrcAlpha, 160 | dst_factor: BlendFactor::OneMinusSrcAlpha, 161 | operation: BlendOperation::Add, 162 | }, 163 | alpha_blend: BlendDescriptor { 164 | src_factor: BlendFactor::OneMinusDstAlpha, 165 | dst_factor: BlendFactor::One, 166 | operation: BlendOperation::Add, 167 | }, 168 | write_mask: ColorWrite::ALL, 169 | }], 170 | depth_stencil_state: None, 171 | index_format: IndexFormat::Uint16, 172 | vertex_buffers: &[ 173 | // pos 174 | VertexBufferDescriptor { 175 | stride: std::mem::size_of::() as BufferAddress, 176 | step_mode: InputStepMode::Vertex, 177 | attributes: &[ 178 | VertexAttributeDescriptor { 179 | format: VertexFormat::Float2, 180 | shader_location: 0, 181 | offset: 0, 182 | }, 183 | VertexAttributeDescriptor { 184 | format: VertexFormat::Float2, 185 | shader_location: 1, 186 | offset: 8, 187 | }, 188 | ], 189 | }, 190 | ], 191 | sample_count: 1, 192 | sample_mask: !0, 193 | alpha_to_coverage_enabled: false, 194 | }); 195 | 196 | let mut vertex_buffers = Vec::with_capacity(model.drawable_count()); 197 | let mut index_buffers = Vec::with_capacity(model.drawable_count()); 198 | let mut temp = Vec::new(); 199 | for cubism_core::Drawable { 200 | vertex_positions, 201 | vertex_uvs, 202 | indices, 203 | .. 204 | } in model.drawables() 205 | { 206 | temp.extend( 207 | vertex_positions 208 | .iter() 209 | .zip(vertex_uvs) 210 | .map(|(&pos, &uv)| Vertex { pos, uv }), 211 | ); 212 | vertex_buffers.push( 213 | device 214 | .create_buffer_mapped(temp.len(), wgpu::BufferUsage::VERTEX) 215 | .fill_from_slice(&temp), 216 | ); 217 | index_buffers.push(( 218 | device 219 | .create_buffer_mapped(indices.len(), wgpu::BufferUsage::INDEX) 220 | .fill_from_slice(indices), 221 | indices.len(), 222 | )); 223 | temp.clear(); 224 | } 225 | 226 | let sampler = BoundTexture::make_sampler(&device); 227 | 228 | Renderer { 229 | pipeline, 230 | uniform_buffer, 231 | uniform_bind_group, 232 | textures: textures 233 | .into_iter() 234 | .map(|tex| BoundTexture::new(tex, &sampler, &texture_layout, &device)) 235 | .collect(), 236 | texture_layout, 237 | vertex_buffers, 238 | index_buffers, 239 | } 240 | } 241 | 242 | /// Draws a model. 243 | pub fn draw_model( 244 | &mut self, 245 | device: &Device, 246 | view: &TextureView, 247 | encoder: &mut CommandEncoder, 248 | model: &cubism_core::Model, 249 | ) { 250 | let mut drawables: Vec<_> = model.drawables().collect(); 251 | drawables.sort_unstable_by_key(|d| d.render_order); 252 | 253 | let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 254 | color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { 255 | attachment: view, 256 | resolve_target: None, 257 | load_op: wgpu::LoadOp::Load, 258 | store_op: wgpu::StoreOp::Store, 259 | clear_color: wgpu::Color { 260 | r: 0.0, 261 | g: 0.0, 262 | b: 0.0, 263 | a: 0.0, 264 | }, 265 | }], 266 | depth_stencil_attachment: None, 267 | }); 268 | 269 | rpass.set_pipeline(&self.pipeline); 270 | rpass.set_bind_group(0, &self.uniform_bind_group, &[]); 271 | // pass by ref or value? Drawable is quite a big structure 272 | for drawable in &drawables { 273 | self.draw_drawable(device, &mut rpass, drawable).unwrap(); 274 | } 275 | } 276 | 277 | fn update_buffers( 278 | &mut self, 279 | device: &Device, 280 | drawable: &cubism_core::Drawable, 281 | ) -> Result<(), ()> { 282 | let vtx_pos = drawable.vertex_positions; 283 | let vtx_uv = drawable.vertex_uvs; 284 | let vtx_buffer = vtx_pos 285 | .iter() 286 | .zip(vtx_uv) 287 | .map(|(&pos, &uv)| Vertex { pos, uv }) 288 | .collect::>(); 289 | self.vertex_buffers[drawable.index] = device 290 | .create_buffer_mapped(vtx_buffer.len(), wgpu::BufferUsage::VERTEX) 291 | .fill_from_slice(&vtx_buffer); 292 | Ok(()) 293 | } 294 | 295 | fn draw_drawable( 296 | &mut self, 297 | device: &Device, 298 | rpass: &mut RenderPass, 299 | drawable: &cubism_core::Drawable, 300 | ) -> Result<(), ()> { 301 | let dflags = drawable.dynamic_flags; 302 | if drawable.opacity <= 0.0 || !dflags.intersects(cubism_core::DynamicFlags::IS_VISIBLE) { 303 | return Ok(()); 304 | } 305 | if dflags.intersects(cubism_core::DynamicFlags::VERTEX_POSITIONS_CHANGED) { 306 | self.update_buffers(device, drawable)?; 307 | } 308 | rpass.set_index_buffer(&self.index_buffers[drawable.index].0, 0); 309 | rpass.set_vertex_buffers(0, &[(&self.vertex_buffers[drawable.index], 0)]); 310 | rpass.set_bind_group( 311 | 1, 312 | &self.textures[drawable.texture_index as usize].bind_group, 313 | &[], 314 | ); 315 | rpass.draw_indexed(0..self.index_buffers[drawable.index].1 as u32, 0, 0..1); 316 | Ok(()) 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /cubism-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cubism-core" 3 | version = "0.1.0" 4 | authors = ["Lukas Wirth "] 5 | edition = "2018" 6 | repository = "https://github.com/Veykril/cubism-rs" 7 | homepage = "https://github.com/Veykril/cubism-rs" 8 | license = "MIT OR Apache-2.0" 9 | description = "Rust bindings for Live2d Cubism Core" 10 | keywords = ["live2d", "api-bindings", "cubism"] 11 | categories = ["api-bindings"] 12 | 13 | [dependencies] 14 | ffi = { package = "cubism-core-sys", version = "0.1.0", path = "../cubism-core-sys", default-features = false } 15 | bitflags = "1" 16 | 17 | [features] 18 | static-link = ["ffi/static-link"] -------------------------------------------------------------------------------- /cubism-core/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{error, fmt}; 2 | 3 | /// The result type, returned by this library. 4 | pub type MocResult = std::result::Result; 5 | 6 | /// An error returned by [`Model::from_bytes`]. 7 | /// 8 | /// [`Model::from_bytes`]: ../struct.Model.html#method.from_bytes 9 | #[derive(Copy, Clone, Debug)] 10 | pub enum MocError { 11 | /// The moc version of the data passed is too old and therefore cannot be 12 | /// loaded. 13 | MocVersionMismatch(u32), 14 | /// The moc data passed is not a valid moc file. 15 | InvalidMocData, 16 | } 17 | 18 | impl error::Error for MocError {} 19 | 20 | impl fmt::Display for MocError { 21 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 22 | match self { 23 | MocError::MocVersionMismatch(v) => write!( 24 | fmt, 25 | "the moc version of the file is too old for the cubism core lib, found {}", 26 | v 27 | ), 28 | MocError::InvalidMocData => write!(fmt, "the moc data is invalid"), 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /cubism-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs, rust_2018_idioms)] 2 | #![warn( 3 | clippy::all, 4 | missing_copy_implementations, 5 | missing_debug_implementations 6 | )] 7 | 8 | //! Rust bindings for Live2D's cubism sdk 9 | 10 | mod error; 11 | mod log; 12 | mod mem; 13 | mod moc; 14 | mod model; 15 | 16 | pub use crate::{error::*, log::*, moc::*, model::*}; 17 | 18 | /// Returns the linked library version in a (major, minor, patch) tuple 19 | pub fn version() -> (u8, u8, u16) { 20 | let version = unsafe { ffi::csmGetVersion() }; 21 | let major = (version & 0xFF00_0000) >> 24; 22 | let minor = (version & 0x00FF_0000) >> 16; 23 | let patch = version & 0xFFFF; 24 | (major as u8, minor as u8, patch as u16) 25 | } 26 | 27 | bitflags::bitflags! { 28 | /// The constant flags of a [Model](model/struct.Model.html)'s drawable. 29 | pub struct ConstantFlags: u8 { 30 | /// The drawable should be blended additively. 31 | const BLEND_ADDITIVE = ffi::csmBlendAdditive; 32 | /// The drawable should be blended multiplicatively. 33 | const BLEND_MULTIPLICATIVE = ffi::csmBlendMultiplicative; 34 | /// The drawable is double sided and therefore shouldn't be culled. 35 | const IS_DOUBLE_SIDED = ffi::csmIsDoubleSided; 36 | /// Whether the clipping mask is inverted or not. 37 | const IS_INVERTED_MASK = ffi::csmIsInvertedMask; 38 | } 39 | } 40 | 41 | bitflags::bitflags! { 42 | /// The dynamic flags of a [Model](model/struct.Model.html)'s drawable. 43 | pub struct DynamicFlags: u8 { 44 | /// The drawable is visible. 45 | const IS_VISIBLE = ffi::csmIsVisible; 46 | /// The drawable's visibility changed since the last update. 47 | const VISIBILITY_CHANGED = ffi::csmVisibilityDidChange; 48 | /// The drawable's opacity changed since the last update. 49 | const OPACITY_CHANGED = ffi::csmOpacityDidChange; 50 | /// The drawable's drawing order changed since the last update. 51 | const DRAW_ORDER_CHANGED = ffi::csmDrawOrderDidChange; 52 | /// The drawable's render order changed since the last update. 53 | const RENDER_ORDER_CHANGED = ffi::csmRenderOrderDidChange; 54 | /// The drawable's vertex positions changed since the last update. 55 | const VERTEX_POSITIONS_CHANGED = ffi::csmVertexPositionsDidChange; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /cubism-core/src/log.rs: -------------------------------------------------------------------------------- 1 | // Inspired by the log crate 2 | use std::{ 3 | ffi::CStr, 4 | sync::atomic::{AtomicUsize, Ordering}, 5 | }; 6 | 7 | const UNINITIALIZED: usize = 0; 8 | const INITIALIZING: usize = 1; 9 | const INITIALIZED: usize = 2; 10 | 11 | static STATE: AtomicUsize = AtomicUsize::new(UNINITIALIZED); 12 | static mut LOGGER: &'static dyn Fn(&str) = &|_| {}; 13 | 14 | /// Set the function the native cubism core calls for logging. 15 | /// Once set calling this function will do nothing. 16 | pub fn set_core_logger(logger: F) 17 | where 18 | F: Fn(&str) + 'static, 19 | { 20 | match STATE.compare_and_swap(UNINITIALIZED, INITIALIZING, Ordering::SeqCst) { 21 | UNINITIALIZED => unsafe { 22 | LOGGER = Box::leak(Box::new(logger)); 23 | ffi::csmSetLogFunction(Some(core_logger)); 24 | STATE.store(INITIALIZED, Ordering::SeqCst); 25 | }, 26 | INITIALIZING => while STATE.load(Ordering::SeqCst) == INITIALIZING {}, 27 | _ => (), 28 | } 29 | } 30 | 31 | unsafe extern "C" fn core_logger(message: *const std::os::raw::c_char) { 32 | if let Ok(s) = CStr::from_ptr(message).to_str() { 33 | (LOGGER)(s.trim_end()) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cubism-core/src/mem.rs: -------------------------------------------------------------------------------- 1 | use std::{alloc, mem, ptr::NonNull}; 2 | 3 | #[derive(Debug)] 4 | pub struct AlignedMemory { 5 | ptr: NonNull, 6 | layout: alloc::Layout, 7 | } 8 | 9 | impl AlignedMemory { 10 | pub fn alloc(size: usize) -> Self { 11 | let layout = alloc::Layout::from_size_align(size, mem::align_of::()).unwrap(); 12 | let ptr = unsafe { alloc::alloc(layout) as *mut T }; 13 | if let Some(ptr) = NonNull::new(ptr) { 14 | AlignedMemory { ptr, layout } 15 | } else { 16 | alloc::handle_alloc_error(layout) 17 | } 18 | } 19 | 20 | pub fn layout(&self) -> &alloc::Layout { 21 | &self.layout 22 | } 23 | 24 | pub fn as_ptr(&self) -> *mut T { 25 | self.ptr.as_ptr() 26 | } 27 | } 28 | 29 | impl Drop for AlignedMemory { 30 | fn drop(&mut self) { 31 | unsafe { alloc::dealloc(self.ptr.as_ptr() as *mut u8, self.layout) }; 32 | } 33 | } 34 | 35 | #[cfg(test)] 36 | mod test { 37 | #[test] 38 | fn test_mem_alloc() { 39 | crate::mem::AlignedMemory::::alloc(100); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /cubism-core/src/moc.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::CStr, ptr, ptr::NonNull, slice}; 2 | 3 | use ffi::{csmMoc, csmModel}; 4 | 5 | use crate::{ 6 | error::{MocError, MocResult}, 7 | mem::AlignedMemory, 8 | ConstantFlags, 9 | }; 10 | 11 | static INVALID_ID_STR: &str = "NON_UTF8_ID"; 12 | 13 | /// This represents a moc. 14 | /// 15 | /// A moc should never exist without at least one model instance as it 16 | /// owns the parameter, part and drawable ids as well as the minimum, maximum 17 | /// and default parameter values of its [Model](./struct.Model.html). 18 | /// 19 | /// All the data that can be accessed from a moc is static data that will not 20 | /// change over the course of a programs lifetime. 21 | #[derive(Debug)] 22 | pub struct Moc { 23 | mem: AlignedMemory, 24 | pub(in crate) part_ids: Box<[&'static str]>, 25 | pub(in crate) parameter_ids: Box<[&'static str]>, 26 | pub(in crate) drawable_ids: Box<[&'static str]>, 27 | param_def_val: NonNull<[f32]>, 28 | param_max_val: NonNull<[f32]>, 29 | param_min_val: NonNull<[f32]>, 30 | drawable_texture_indices: NonNull<[i32]>, 31 | drawable_constant_flags: NonNull<[ConstantFlags]>, 32 | drawable_indices: Box<[&'static [u16]]>, 33 | drawable_masks: Box<[&'static [i32]]>, 34 | drawable_vertex_counts: NonNull<[i32]>, 35 | } 36 | 37 | impl Moc { 38 | /// Returns the parameter names. 39 | #[inline] 40 | pub fn parameter_ids<'moc>(&'moc self) -> &[&'moc str] { 41 | &self.parameter_ids 42 | } 43 | 44 | /// Returns the part names. 45 | #[inline] 46 | pub fn part_ids<'moc>(&'moc self) -> &[&'moc str] { 47 | &self.part_ids 48 | } 49 | 50 | /// Returns the drawable names. 51 | #[inline] 52 | pub fn drawable_ids<'moc>(&'moc self) -> &[&'moc str] { 53 | &self.drawable_ids 54 | } 55 | 56 | /// Returns the parameter max values. 57 | #[inline] 58 | pub fn parameter_max(&self) -> &[f32] { 59 | unsafe { self.param_max_val.as_ref() } 60 | } 61 | 62 | /// Returns the parameter min values. 63 | #[inline] 64 | pub fn parameter_min(&self) -> &[f32] { 65 | unsafe { self.param_min_val.as_ref() } 66 | } 67 | 68 | /// Returns the parameter default values. 69 | #[inline] 70 | pub fn parameter_default(&self) -> &[f32] { 71 | unsafe { self.param_def_val.as_ref() } 72 | } 73 | 74 | /// Returns the number of parameters this moc has. 75 | #[inline] 76 | pub fn parameter_count(&self) -> usize { 77 | self.parameter_ids.len() 78 | } 79 | 80 | /// Returns the number of parts this moc has. 81 | #[inline] 82 | pub fn part_count(&self) -> usize { 83 | self.part_ids.len() 84 | } 85 | 86 | /// Returns the number of drawables this moc has. 87 | #[inline] 88 | pub fn drawable_count(&self) -> usize { 89 | self.drawable_ids.len() 90 | } 91 | 92 | /// Returns the texture indices of the drawables. 93 | #[inline] 94 | pub fn drawable_texture_indices(&self) -> &[i32] { 95 | unsafe { self.drawable_texture_indices.as_ref() } 96 | } 97 | 98 | /// Returns the [ConstantFlags](./struct.ConstantFlags.html). 99 | #[inline] 100 | pub fn drawable_constant_flags(&self) -> &[ConstantFlags] { 101 | unsafe { self.drawable_constant_flags.as_ref() } 102 | } 103 | 104 | /// Returns the vertex counts of each drawable. 105 | #[inline] 106 | pub fn drawable_vertex_counts(&self) -> &[i32] { 107 | unsafe { self.drawable_vertex_counts.as_ref() } 108 | } 109 | 110 | /// Returns the indices of the drawables. 111 | #[inline] 112 | pub fn drawable_indices<'moc>(&'moc self) -> &[&'moc [u16]] { 113 | &self.drawable_indices 114 | } 115 | 116 | /// Returns the masks of the drawables. 117 | #[inline] 118 | pub fn drawable_masks<'moc>(&'moc self) -> &[&'moc [i32]] { 119 | &self.drawable_masks 120 | } 121 | 122 | /// Returns true if this model is masked. 123 | #[inline] 124 | pub fn is_masked(&self) -> bool { 125 | self.drawable_masks.iter().any(|m| !m.is_empty()) 126 | } 127 | 128 | /// Returns the raw [csmMoc](../cubism_core_sys/moc/struct.csmMoc.html) ptr 129 | #[inline] 130 | pub fn as_ptr(&self) -> *mut csmMoc { 131 | self.mem.as_ptr() 132 | } 133 | } 134 | 135 | impl Moc { 136 | unsafe fn new_moc(data: &[u8]) -> MocResult> { 137 | let moc_ver = ffi::csmGetMocVersion(data.as_ptr() as _, data.len() as _); 138 | if ffi::csmGetLatestMocVersion() < moc_ver { 139 | Err(MocError::MocVersionMismatch(moc_ver)) 140 | } else { 141 | let mem = AlignedMemory::alloc(data.len()); 142 | ptr::copy_nonoverlapping(data.as_ptr(), mem.as_ptr() as *mut u8, data.len()); 143 | let revived = 144 | ffi::csmReviveMocInPlace(mem.as_ptr() as _, mem.layout().size() as u32).is_null(); 145 | if revived { 146 | Err(MocError::InvalidMocData) 147 | } else { 148 | Ok(mem) 149 | } 150 | } 151 | } 152 | 153 | pub(in crate) unsafe fn new(data: &[u8]) -> MocResult<(Self, AlignedMemory)> { 154 | let mem = Self::new_moc(data)?; 155 | let model = Self::init_new_model(mem.as_ptr()); 156 | let model_ptr = model.as_ptr(); 157 | 158 | let id_transform = |ptr, len| { 159 | slice::from_raw_parts_mut(ptr, len) 160 | .iter() 161 | .map(|ptr| CStr::from_ptr(*ptr).to_str().unwrap_or(INVALID_ID_STR)) 162 | }; 163 | 164 | let param_count = ffi::csmGetParameterCount(model_ptr) as usize; 165 | let part_count = ffi::csmGetPartCount(model_ptr) as usize; 166 | let drawable_count = ffi::csmGetDrawableCount(model_ptr) as usize; 167 | 168 | let indices = slice::from_raw_parts(ffi::csmGetDrawableIndices(model_ptr), drawable_count); 169 | let drawable_indices = 170 | slice::from_raw_parts(ffi::csmGetDrawableIndexCounts(model_ptr), drawable_count) 171 | .iter() 172 | .zip(indices) 173 | .map(|(c, indices)| slice::from_raw_parts(*indices, *c as usize)) 174 | .collect(); 175 | let masks = slice::from_raw_parts(ffi::csmGetDrawableMasks(model_ptr), drawable_count); 176 | let drawable_masks = 177 | slice::from_raw_parts(ffi::csmGetDrawableMaskCounts(model_ptr), drawable_count) 178 | .iter() 179 | .zip(masks) 180 | .map(|(c, masks)| slice::from_raw_parts(*masks, *c as usize)) 181 | .collect(); 182 | 183 | Ok(( 184 | Moc { 185 | mem, 186 | part_ids: id_transform(ffi::csmGetPartIds(model_ptr), part_count).collect(), 187 | parameter_ids: id_transform(ffi::csmGetParameterIds(model_ptr), param_count) 188 | .collect(), 189 | drawable_ids: id_transform(ffi::csmGetDrawableIds(model_ptr), drawable_count) 190 | .collect(), 191 | param_def_val: NonNull::from(slice::from_raw_parts( 192 | ffi::csmGetParameterDefaultValues(model_ptr), 193 | param_count, 194 | )), 195 | param_max_val: NonNull::from(slice::from_raw_parts( 196 | ffi::csmGetParameterMaximumValues(model_ptr), 197 | param_count, 198 | )), 199 | param_min_val: NonNull::from(slice::from_raw_parts( 200 | ffi::csmGetParameterMinimumValues(model_ptr), 201 | param_count, 202 | )), 203 | drawable_texture_indices: NonNull::from(slice::from_raw_parts( 204 | ffi::csmGetDrawableTextureIndices(model_ptr), 205 | drawable_count, 206 | )), 207 | drawable_constant_flags: NonNull::from(slice::from_raw_parts( 208 | ffi::csmGetDrawableConstantFlags(model_ptr) as _, 209 | drawable_count, 210 | )), 211 | drawable_indices, 212 | drawable_masks, 213 | drawable_vertex_counts: NonNull::from(slice::from_raw_parts( 214 | ffi::csmGetDrawableVertexCounts(model_ptr), 215 | drawable_count, 216 | )), 217 | }, 218 | model, 219 | )) 220 | } 221 | 222 | pub(in crate) unsafe fn init_new_model(moc: *const csmMoc) -> AlignedMemory { 223 | let model_size = ffi::csmGetSizeofModel(moc); 224 | let model_mem = AlignedMemory::alloc(model_size as usize); 225 | 226 | if ffi::csmInitializeModelInPlace(moc, model_mem.as_ptr() as *mut _, model_size).is_null() { 227 | unreachable!( 228 | "ffi::csmInitializeModelInPlace returned a null pointer, \ 229 | this shouldn't happen unless the alignment is incorrect" 230 | ) 231 | } else { 232 | model_mem 233 | } 234 | } 235 | } 236 | 237 | unsafe impl Send for Moc {} 238 | unsafe impl Sync for Moc {} 239 | -------------------------------------------------------------------------------- /cubism-core/src/model.rs: -------------------------------------------------------------------------------- 1 | use std::{iter, mem, ops, ptr::NonNull, slice, sync::Arc}; 2 | 3 | use ffi::csmModel; 4 | 5 | use crate::{error::MocResult, mem::AlignedMemory, moc::Moc, ConstantFlags, DynamicFlags}; 6 | 7 | /// This represents a model. 8 | /// 9 | /// A model shares its underlying [Moc](./struct.Moc.html) with other models 10 | /// that have been cloned from this one. Because of this it is preferred to 11 | /// clone models, instead of creating new ones from the same data. 12 | /// 13 | /// Slices returned by functions have to be indexed by the drawable, parameter 14 | /// or part index for the individual value. If the functions takes an index 15 | /// argument, then the function index replaces this behaviour and the returned 16 | /// slice are values that all belong to the drawable. 17 | #[derive(Debug)] 18 | pub struct Model { 19 | mem: AlignedMemory, 20 | moc: Arc, 21 | param_val: NonNull<[f32]>, 22 | part_opacities: NonNull<[f32]>, 23 | } 24 | 25 | impl Model { 26 | /// Creates a model instance from bytes. 27 | #[inline] 28 | pub fn from_bytes>(data: R) -> MocResult { 29 | unsafe { Moc::new(data.as_ref()).map(|(moc, mem)| Self::new_impl(Arc::new(moc), mem)) } 30 | } 31 | 32 | /// Returns the first parameter with the given name, or `None` if there is 33 | /// none with the given name. 34 | pub fn parameter(&self, name: &str) -> Option> { 35 | self.parameter_ids() 36 | .iter() 37 | .enumerate() 38 | .find_map(|(idx, id)| { 39 | if *id == name { 40 | Some(self.parameter_at(idx)) 41 | } else { 42 | None 43 | } 44 | }) 45 | } 46 | 47 | /// Returns the first parameter with the given name, or `None` if there is 48 | /// none with the given name. 49 | pub fn parameter_mut(&mut self, name: &str) -> Option> { 50 | if let Some(idx) = self.parameter_ids().iter().position(|id| *id == name) { 51 | Some(self.parameter_at_mut(idx)) 52 | } else { 53 | None 54 | } 55 | } 56 | 57 | /// Returns the parameter at the specified index. 58 | /// 59 | /// # Panics 60 | /// Panics on out of bounds access. 61 | pub fn parameter_at(&self, idx: usize) -> Parameter<'_> { 62 | // Do manual bounds checking since all slices have the same length 63 | assert!(idx < self.parameter_count()); 64 | unsafe { 65 | Parameter { 66 | id: &self.parameter_ids().get_unchecked(idx), 67 | value: *self.parameter_values().get_unchecked(idx), 68 | min_value: *self.parameter_min().get_unchecked(idx), 69 | max_value: *self.parameter_max().get_unchecked(idx), 70 | default_value: *self.parameter_default().get_unchecked(idx), 71 | } 72 | } 73 | } 74 | 75 | /// Returns the parameter at the specified index. 76 | /// 77 | /// # Panics 78 | /// Panics on out of bounds access. 79 | pub fn parameter_at_mut(&mut self, idx: usize) -> ParameterMut<'_> { 80 | // Do manual bounds checking since all slices have the same length 81 | assert!(idx < self.parameter_count()); 82 | unsafe { 83 | let min_value = *self.parameter_min().get_unchecked(idx); 84 | let max_value = *self.parameter_max().get_unchecked(idx); 85 | let default_value = *self.parameter_default().get_unchecked(idx); 86 | ParameterMut { 87 | id: &self.moc.parameter_ids.get_unchecked(idx), 88 | value: self.parameter_values_mut().get_unchecked_mut(idx), 89 | min_value, 90 | max_value, 91 | default_value, 92 | } 93 | } 94 | } 95 | 96 | /// Returns the first part with the given name, or `None` if there is none 97 | /// with the given name. 98 | pub fn part(&self, name: &str) -> Option> { 99 | self.part_ids().iter().enumerate().find_map(|(idx, id)| { 100 | if *id == name { 101 | Some(self.part_at(idx)) 102 | } else { 103 | None 104 | } 105 | }) 106 | } 107 | 108 | /// Returns the first part with the given name, or `None` if there is none 109 | /// with the given name. 110 | pub fn part_mut(&mut self, name: &str) -> Option> { 111 | if let Some(idx) = self.part_ids().iter().position(|id| *id == name) { 112 | Some(self.part_at_mut(idx)) 113 | } else { 114 | None 115 | } 116 | } 117 | 118 | /// Returns the parameter at the specified idx. 119 | /// 120 | /// # Panics 121 | /// Panics on out of bounds access. 122 | #[inline] 123 | pub fn part_at(&self, idx: usize) -> Part<'_> { 124 | Part { 125 | id: &self.moc.part_ids()[idx], 126 | opacity: self.part_opacities()[idx], 127 | } 128 | } 129 | 130 | /// Returns the parameter at the specified idx. 131 | /// 132 | /// # Panics 133 | /// Panics on out of bounds access. 134 | #[inline] 135 | pub fn part_at_mut(&mut self, idx: usize) -> PartMut<'_> { 136 | PartMut { 137 | id: &self.moc.part_ids[idx], 138 | opacity: &mut self.part_opacities_mut()[idx], 139 | } 140 | } 141 | 142 | /// Returns the first drawable with the given name, or `None` if there is 143 | /// none with the given name. 144 | pub fn drawable(&self, name: &str) -> Option> { 145 | self.drawable_ids() 146 | .iter() 147 | .enumerate() 148 | .find_map(|(idx, id)| { 149 | if *id == name { 150 | Some(self.drawable_at(idx)) 151 | } else { 152 | None 153 | } 154 | }) 155 | } 156 | 157 | /// Returns the drawable at the specified index. 158 | /// 159 | /// # Panics 160 | /// Panics on out of bounds access. 161 | pub fn drawable_at(&self, idx: usize) -> Drawable<'_> { 162 | // Do manual bounds checking since all slices have the same length 163 | assert!(idx < self.drawable_count()); 164 | unsafe { 165 | Drawable { 166 | index: idx, 167 | render_order: *self.drawable_render_orders().get_unchecked(idx), 168 | draw_order: *self.drawable_draw_orders().get_unchecked(idx), 169 | texture_index: *self.drawable_texture_indices().get_unchecked(idx), 170 | indices: self.drawable_indices().get_unchecked(idx), 171 | vertex_positions: self.drawable_vertex_positions(idx), 172 | vertex_uvs: self.drawable_vertex_uvs(idx), 173 | opacity: *self.drawable_opacities().get_unchecked(idx), 174 | masks: self.drawable_masks().get_unchecked(idx), 175 | constant_flags: *self.drawable_constant_flags().get_unchecked(idx), 176 | dynamic_flags: *self.drawable_dynamic_flags().get_unchecked(idx), 177 | } 178 | } 179 | } 180 | 181 | /// Returns the model's parameter values. 182 | #[inline] 183 | pub fn parameter_values(&self) -> &[f32] { 184 | unsafe { self.param_val.as_ref() } 185 | } 186 | 187 | /// Returns a mutable slice of the model's parameter values. 188 | #[inline] 189 | pub fn parameter_values_mut(&mut self) -> &mut [f32] { 190 | unsafe { self.param_val.as_mut() } 191 | } 192 | 193 | /// Sets the parameter value at index `idx` to `val`. 194 | /// 195 | /// # Panics 196 | /// Panics on out of bounds access. 197 | #[inline] 198 | pub fn set_parameter_value(&mut self, idx: usize, val: f32) { 199 | self.parameter_values_mut()[idx] = val; 200 | } 201 | 202 | /// Returns the model's part opacities. 203 | #[inline] 204 | pub fn part_opacities(&self) -> &[f32] { 205 | unsafe { self.part_opacities.as_ref() } 206 | } 207 | 208 | /// Returns a mutable slice of the model's part opacities. 209 | /// Opacity changes of a parent part also apply to its children. 210 | #[inline] 211 | pub fn part_opacities_mut(&mut self) -> &mut [f32] { 212 | unsafe { self.part_opacities.as_mut() } 213 | } 214 | 215 | /// Sets the part opacity at index `idx` to `val`. 216 | /// 217 | /// # Panics 218 | /// Panics on out of bounds access. 219 | #[inline] 220 | pub fn set_part_opacity(&mut self, idx: usize, val: f32) { 221 | self.part_opacities_mut()[idx] = val; 222 | } 223 | 224 | /// Returns the parent of the part at the given index. 225 | #[inline] 226 | pub fn part_parent(&self, idx: usize) -> Option> { 227 | self.part_parents() 228 | .get(idx) 229 | .filter(|i| **i != -1) 230 | .map(|i| self.part_at(*i as usize)) 231 | } 232 | 233 | /// Returns the model's part parent relationships. 234 | /// If the value of a parent is -1 it means the part is the root. 235 | #[inline] 236 | pub fn part_parents(&self) -> &[i32] { 237 | unsafe { 238 | slice::from_raw_parts( 239 | ffi::csmGetPartParentPartIndices(self.as_ptr()), 240 | self.part_count(), 241 | ) 242 | } 243 | } 244 | 245 | /// Updates this model and finalizes its parameters and part opacities. 246 | /// This has to be called before accessing the drawables. 247 | #[inline] 248 | pub fn update(&mut self) { 249 | // FIXME: is this order correct? This is what the pdf says, but the framework 250 | // implementation has it reversed 251 | unsafe { ffi::csmResetDrawableDynamicFlags(self.mem.as_ptr()) }; 252 | unsafe { ffi::csmUpdateModel(self.mem.as_ptr()) }; 253 | } 254 | 255 | /// Returns information about this models size, origin and pixels-per-unit. 256 | pub fn canvas_info(&self) -> ([f32; 2], [f32; 2], f32) { 257 | let mut size = [0.0; 2]; 258 | let mut origin = [0.0; 2]; 259 | let mut ppu = 0.0; 260 | unsafe { 261 | ffi::csmReadCanvasInfo( 262 | self.mem.as_ptr(), 263 | &mut size as *mut _ as *mut _, 264 | &mut origin as *mut _ as *mut _, 265 | &mut ppu, 266 | ); 267 | } 268 | (size, origin, ppu) 269 | } 270 | 271 | /// Returns the render orders of the drawables. 272 | #[inline] 273 | pub fn drawable_render_orders(&self) -> &[i32] { 274 | unsafe { 275 | slice::from_raw_parts( 276 | ffi::csmGetDrawableRenderOrders(self.as_ptr()), 277 | self.drawable_count(), 278 | ) 279 | } 280 | } 281 | 282 | /// Returns the draw orders of the drawables. 283 | #[inline] 284 | pub fn drawable_draw_orders(&self) -> &[i32] { 285 | unsafe { 286 | slice::from_raw_parts( 287 | ffi::csmGetDrawableDrawOrders(self.as_ptr()), 288 | self.drawable_count(), 289 | ) 290 | } 291 | } 292 | 293 | /// Returns the vertex positions of the drawable at the specified index. 294 | #[inline] 295 | pub fn drawable_vertex_positions(&self, idx: usize) -> &[[f32; 2]] { 296 | unsafe { 297 | slice::from_raw_parts( 298 | *ffi::csmGetDrawableVertexPositions(self.as_ptr()).add(idx) as *const _, 299 | self.drawable_vertex_counts()[idx] as usize, 300 | ) 301 | } 302 | } 303 | 304 | /// Returns the uv coordinates of the drawable at the specified index. 305 | #[inline] 306 | pub fn drawable_vertex_uvs(&self, idx: usize) -> &[[f32; 2]] { 307 | unsafe { 308 | slice::from_raw_parts( 309 | *ffi::csmGetDrawableVertexUvs(self.as_ptr()).add(idx) as *const _, 310 | self.drawable_vertex_counts()[idx] as usize, 311 | ) 312 | } 313 | } 314 | 315 | /// Returns the drawable opacities. 316 | #[inline] 317 | pub fn drawable_opacities(&self) -> &[f32] { 318 | unsafe { 319 | slice::from_raw_parts( 320 | ffi::csmGetDrawableOpacities(self.as_ptr()), 321 | self.drawable_count(), 322 | ) 323 | } 324 | } 325 | 326 | /// Returns the [DynamicFlags](./struct.DynamicFlags.html). 327 | #[inline] 328 | pub fn drawable_dynamic_flags(&self) -> &[DynamicFlags] { 329 | unsafe { 330 | slice::from_raw_parts( 331 | ffi::csmGetDrawableDynamicFlags(self.as_ptr()) as *const DynamicFlags, 332 | self.drawable_count(), 333 | ) 334 | } 335 | } 336 | 337 | /// Returns a reference to the underlying [Moc](./struct.Moc.html). 338 | #[inline] 339 | pub fn moc(&self) -> &Moc { 340 | &self.moc 341 | } 342 | 343 | /// Clones the arc that holds the underlying [Moc](./struct.Moc.html) and 344 | /// returns it. 345 | #[inline] 346 | #[must_use] 347 | pub fn moc_arc(&self) -> Arc { 348 | self.moc.clone() 349 | } 350 | 351 | /// Returns the raw 352 | /// [csmModel](../cubism_core_sys/model/struct.csmModel.html) ptr. 353 | #[inline] 354 | pub fn as_ptr(&self) -> *mut csmModel { 355 | self.mem.as_ptr() 356 | } 357 | 358 | /// Returns an iterator over the model's parameters. 359 | #[inline] 360 | pub fn parameters(&self) -> ParameterIter<'_> { 361 | ParameterIter { 362 | model: self, 363 | idx: 0, 364 | } 365 | } 366 | 367 | /// Returns an iterator over the model's parameters. 368 | #[inline] 369 | pub fn parameters_mut(&mut self) -> ParameterIterMut<'_> { 370 | ParameterIterMut { 371 | model: self, 372 | idx: 0, 373 | } 374 | } 375 | 376 | /// Returns an iterator over the model's parts. 377 | #[inline] 378 | pub fn parts(&self) -> PartIter<'_> { 379 | PartIter { 380 | model: self, 381 | idx: 0, 382 | } 383 | } 384 | 385 | /// Returns an iterator over the model's parts. 386 | #[inline] 387 | pub fn parts_mut(&mut self) -> PartIterMut<'_> { 388 | PartIterMut { 389 | model: self, 390 | idx: 0, 391 | } 392 | } 393 | 394 | /// Returns an iterator over the model's parts. 395 | #[inline] 396 | pub fn drawables(&self) -> DrawableIter<'_> { 397 | DrawableIter { 398 | model: self, 399 | idx: 0, 400 | } 401 | } 402 | } 403 | 404 | impl Model { 405 | unsafe fn new_impl(moc: Arc, mem: AlignedMemory) -> Model { 406 | let param_values = NonNull::from(slice::from_raw_parts_mut( 407 | ffi::csmGetParameterValues(mem.as_ptr()), 408 | moc.parameter_count(), 409 | )); 410 | let part_opacities = NonNull::from(slice::from_raw_parts_mut( 411 | ffi::csmGetPartOpacities(mem.as_ptr()), 412 | moc.part_count(), 413 | )); 414 | 415 | Model { 416 | mem, 417 | moc, 418 | param_val: param_values, 419 | part_opacities, 420 | } 421 | } 422 | } 423 | 424 | impl Clone for Model { 425 | fn clone(&self) -> Self { 426 | let model_mem = unsafe { Moc::init_new_model(self.moc.as_ptr()) }; 427 | let mut model = unsafe { Self::new_impl(self.moc_arc(), model_mem) }; 428 | model 429 | .parameter_values_mut() 430 | .copy_from_slice(self.parameter_values()); 431 | model 432 | .part_opacities_mut() 433 | .copy_from_slice(self.part_opacities()); 434 | model 435 | } 436 | } 437 | 438 | impl ops::Deref for Model { 439 | type Target = Moc; 440 | #[inline] 441 | fn deref(&self) -> &Self::Target { 442 | &self.moc 443 | } 444 | } 445 | 446 | impl AsRef for Model { 447 | #[inline] 448 | fn as_ref(&self) -> &Moc { 449 | &self.moc 450 | } 451 | } 452 | 453 | unsafe impl Send for Model {} 454 | unsafe impl Sync for Model {} 455 | 456 | /// A parameter of a model. 457 | #[derive(Copy, Clone, Debug)] 458 | pub struct Parameter<'model> { 459 | /// The parameter's identifier 460 | pub id: &'model str, 461 | /// The parameter's current value 462 | pub value: f32, 463 | /// The parameter's minimum value 464 | pub min_value: f32, 465 | /// The parameter's maximum value 466 | pub max_value: f32, 467 | /// The parameter's default value 468 | pub default_value: f32, 469 | } 470 | 471 | /// A parameter of a model. 472 | #[derive(Debug)] 473 | pub struct ParameterMut<'model> { 474 | /// The parameter's identifier 475 | pub id: &'model str, 476 | /// The parameter's current value 477 | pub value: &'model mut f32, 478 | /// The parameter's minimum value 479 | pub min_value: f32, 480 | /// The parameter's maximum value 481 | pub max_value: f32, 482 | /// The parameter's default value 483 | pub default_value: f32, 484 | } 485 | 486 | /// A part of a model. 487 | #[derive(Copy, Clone, Debug)] 488 | pub struct Part<'model> { 489 | /// The part's identifier 490 | pub id: &'model str, 491 | /// The part's current opacity 492 | pub opacity: f32, 493 | } 494 | 495 | /// A part of a model. 496 | #[derive(Debug)] 497 | pub struct PartMut<'model> { 498 | /// The part's identifier 499 | pub id: &'model str, 500 | /// The part's current opacity 501 | pub opacity: &'model mut f32, 502 | } 503 | 504 | /// A drawable of a model. 505 | #[derive(Copy, Clone, Debug)] 506 | pub struct Drawable<'model> { 507 | // mem::size_of:: == 768 bits! 508 | // This seems to be way too big 509 | /// The drawable index. 510 | pub index: usize, 511 | /// The drawable's render order. 512 | pub render_order: i32, 513 | /// The drawable's draw order(where is the difference to the render order?). 514 | pub draw_order: i32, 515 | /// The drawable's texture index. 516 | pub texture_index: i32, 517 | /// The drawable's indices. 518 | pub indices: &'model [u16], 519 | /// The drawable's vertex positions. 520 | pub vertex_positions: &'model [[f32; 2]], 521 | /// The drawable's uvs. 522 | pub vertex_uvs: &'model [[f32; 2]], 523 | /// The drawable's opacity. 524 | pub opacity: f32, 525 | /// The drawable's masks. 526 | pub masks: &'model [i32], 527 | /// The drawable's constant drawing flags. 528 | pub constant_flags: ConstantFlags, 529 | /// The drawable's dynamic drawing flags. 530 | pub dynamic_flags: DynamicFlags, 531 | } 532 | 533 | impl<'model> Drawable<'model> { 534 | /// Returns whether this drawable is masked or not. 535 | pub fn is_masked(&self) -> bool { 536 | !self.masks.is_empty() 537 | } 538 | } 539 | 540 | /// An iterator that iterates over a model's parameters. 541 | #[derive(Clone, Debug)] 542 | pub struct ParameterIter<'model> { 543 | model: &'model Model, 544 | idx: usize, 545 | } 546 | 547 | impl<'model> iter::ExactSizeIterator for ParameterIter<'model> {} 548 | impl<'model> iter::FusedIterator for ParameterIter<'model> {} 549 | impl<'model> Iterator for ParameterIter<'model> { 550 | type Item = Parameter<'model>; 551 | 552 | fn next(&mut self) -> Option { 553 | if self.idx < self.model.parameter_count() { 554 | let param = self.model.parameter_at(self.idx); 555 | self.idx += 1; 556 | Some(param) 557 | } else { 558 | None 559 | } 560 | } 561 | 562 | fn size_hint(&self) -> (usize, Option) { 563 | let len = self.model.parameter_count() - self.idx; 564 | (len, Some(len)) 565 | } 566 | } 567 | 568 | /// An iterator that iterates over a model's parameters. 569 | #[derive(Debug)] 570 | pub struct ParameterIterMut<'model> { 571 | model: &'model mut Model, 572 | idx: usize, 573 | } 574 | 575 | impl<'model> iter::ExactSizeIterator for ParameterIterMut<'model> {} 576 | impl<'model> iter::FusedIterator for ParameterIterMut<'model> {} 577 | impl<'model> Iterator for ParameterIterMut<'model> { 578 | type Item = ParameterMut<'model>; 579 | 580 | fn next(&mut self) -> Option { 581 | if self.idx < self.model.parameter_count() { 582 | // safety: transmuting the lifetimes is safe here, since we only create mutable 583 | // borrows to disjoint objects 584 | let part = unsafe { mem::transmute(self.model.parameter_at_mut(self.idx)) }; 585 | self.idx += 1; 586 | Some(part) 587 | } else { 588 | None 589 | } 590 | } 591 | 592 | fn size_hint(&self) -> (usize, Option) { 593 | let len = self.model.parameter_count() - self.idx; 594 | (len, Some(len)) 595 | } 596 | } 597 | 598 | /// An iterator that iterates over a model's parts. 599 | #[derive(Clone, Debug)] 600 | pub struct PartIter<'model> { 601 | model: &'model Model, 602 | idx: usize, 603 | } 604 | 605 | impl<'model> iter::ExactSizeIterator for PartIter<'model> {} 606 | impl<'model> iter::FusedIterator for PartIter<'model> {} 607 | impl<'model> Iterator for PartIter<'model> { 608 | type Item = Part<'model>; 609 | 610 | fn next(&mut self) -> Option { 611 | if self.idx < self.model.part_count() { 612 | let part = self.model.part_at(self.idx); 613 | self.idx += 1; 614 | Some(part) 615 | } else { 616 | None 617 | } 618 | } 619 | 620 | fn size_hint(&self) -> (usize, Option) { 621 | let len = self.model.part_count() - self.idx; 622 | (len, Some(len)) 623 | } 624 | } 625 | 626 | /// An iterator that iterates over a model's parts. 627 | #[derive(Debug)] 628 | pub struct PartIterMut<'model> { 629 | model: &'model mut Model, 630 | idx: usize, 631 | } 632 | 633 | impl<'model> iter::ExactSizeIterator for PartIterMut<'model> {} 634 | impl<'model> iter::FusedIterator for PartIterMut<'model> {} 635 | impl<'model> Iterator for PartIterMut<'model> { 636 | type Item = PartMut<'model>; 637 | 638 | fn next(&mut self) -> Option> { 639 | if self.idx < self.model.part_count() { 640 | // safety: transmuting the lifetimes is safe here, since we only create mutable 641 | // borrows to disjoint objects 642 | let part = unsafe { mem::transmute(self.model.part_at_mut(self.idx)) }; 643 | self.idx += 1; 644 | Some(part) 645 | } else { 646 | None 647 | } 648 | } 649 | 650 | fn size_hint(&self) -> (usize, Option) { 651 | let len = self.model.part_count() - self.idx; 652 | (len, Some(len)) 653 | } 654 | } 655 | 656 | /// An iterator that iterates over a model's parameters. 657 | #[derive(Clone, Debug)] 658 | pub struct DrawableIter<'model> { 659 | model: &'model Model, 660 | idx: usize, 661 | } 662 | 663 | impl<'model> iter::ExactSizeIterator for DrawableIter<'model> {} 664 | impl<'model> iter::FusedIterator for DrawableIter<'model> {} 665 | impl<'model> Iterator for DrawableIter<'model> { 666 | type Item = Drawable<'model>; 667 | 668 | fn next(&mut self) -> Option { 669 | // TODO: optimize, this implementation does a lot of bounds checking and 670 | // repeated ffi function calls 671 | if self.idx < self.model.drawable_count() { 672 | let drawable = self.model.drawable_at(self.idx); 673 | self.idx += 1; 674 | Some(drawable) 675 | } else { 676 | None 677 | } 678 | } 679 | 680 | fn size_hint(&self) -> (usize, Option) { 681 | let len = self.model.drawable_count() - self.idx; 682 | (len, Some(len)) 683 | } 684 | } 685 | -------------------------------------------------------------------------------- /cubism-examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cubism-examples" 3 | version = "0.1.0" 4 | authors = ["Lukas Wirth "] 5 | edition = "2018" 6 | publish = false 7 | 8 | [[bin]] 9 | name = "display" 10 | path = "src/display.rs" 11 | 12 | [dependencies] 13 | cubism = { path = "..", version = "0.1.0" } 14 | cubism-core-wgpu-renderer = { version = "0.1.0", path = "../cubism-core-wgpu-renderer"} 15 | env_logger = "0.7" 16 | log = "0.4" 17 | image = { version = "^0.22", default-features = false, features = ["png_codec"] } 18 | wgpu = "0.4" 19 | winit = "0.20" 20 | -------------------------------------------------------------------------------- /cubism-examples/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::Cursor, 4 | iter::FromIterator, 5 | path::{Path, PathBuf}, 6 | time::Instant, 7 | }; 8 | use winit::event_loop::EventLoop; 9 | 10 | const SWAPCHAIN_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8UnormSrgb; 11 | 12 | fn load_texture( 13 | device: &wgpu::Device, 14 | encoder: &mut wgpu::CommandEncoder, 15 | path: &Path, 16 | ) -> wgpu::Texture { 17 | let image = image::load(Cursor::new(&std::fs::read(path).unwrap()[..]), image::PNG) 18 | .unwrap() 19 | .flipv() 20 | .to_rgba(); 21 | let (width, height) = image.dimensions(); 22 | let texture_extent = wgpu::Extent3d { 23 | width, 24 | height, 25 | depth: 1, 26 | }; 27 | let texture = device.create_texture(&wgpu::TextureDescriptor { 28 | size: texture_extent, 29 | array_layer_count: 1, 30 | mip_level_count: 1, 31 | sample_count: 1, 32 | dimension: wgpu::TextureDimension::D2, 33 | format: wgpu::TextureFormat::Rgba8UnormSrgb, 34 | usage: wgpu::TextureUsage::SAMPLED | wgpu::TextureUsage::COPY_DST, 35 | }); 36 | let temp_buf = device 37 | .create_buffer_mapped::(image.len(), wgpu::BufferUsage::COPY_SRC) 38 | .fill_from_slice(&image); 39 | encoder.copy_buffer_to_texture( 40 | wgpu::BufferCopyView { 41 | buffer: &temp_buf, 42 | offset: 0, 43 | row_pitch: 4 * texture_extent.width, 44 | image_height: texture_extent.height, 45 | }, 46 | wgpu::TextureCopyView { 47 | texture: &texture, 48 | mip_level: 0, 49 | array_layer: 0, 50 | origin: wgpu::Origin3d { 51 | x: 0.0, 52 | y: 0.0, 53 | z: 0.0, 54 | }, 55 | }, 56 | texture_extent, 57 | ); 58 | texture 59 | } 60 | 61 | fn main() { 62 | env_logger::from_env(env_logger::Env::default().default_filter_or("warn")).init(); 63 | log::warn!("NOTE: The window may freeze for a few seconds due to image loading being very slow in debug"); 64 | cubism::core::set_core_logger(|s| log::warn!("CUBISM: {}", s)); 65 | let res_path = PathBuf::from_iter(&[env!("CUBISM_CORE"), "Samples/Res/Haru"]); 66 | 67 | // Create window 68 | let event_loop = EventLoop::new(); 69 | let window = winit::window::WindowBuilder::new() 70 | .with_resizable(true) 71 | .with_min_inner_size(winit::dpi::LogicalSize { 72 | width: 128.0, 73 | height: 128.0, 74 | }) 75 | .with_inner_size(winit::dpi::LogicalSize { 76 | width: 512.0, 77 | height: 512.0, 78 | }) 79 | .build(&event_loop) 80 | .unwrap(); 81 | let surface = wgpu::Surface::create(&window); 82 | 83 | let winit::dpi::PhysicalSize { width, height } = window.inner_size(); 84 | 85 | let adapter = wgpu::Adapter::request(&wgpu::RequestAdapterOptions { 86 | power_preference: wgpu::PowerPreference::Default, 87 | backends: wgpu::BackendBit::PRIMARY, 88 | }) 89 | .unwrap(); 90 | 91 | let (device, mut queue) = adapter.request_device(&wgpu::DeviceDescriptor { 92 | extensions: wgpu::Extensions { 93 | anisotropic_filtering: false, 94 | }, 95 | limits: wgpu::Limits::default(), 96 | }); 97 | 98 | let mut sc_desc = wgpu::SwapChainDescriptor { 99 | usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT, 100 | format: SWAPCHAIN_FORMAT, 101 | width, 102 | height, 103 | present_mode: wgpu::PresentMode::NoVsync, 104 | }; 105 | let mut swap_chain = device.create_swap_chain(&surface, &sc_desc); 106 | window.request_redraw(); 107 | 108 | // Load model3.json 109 | let haru_json = cubism::json::model::Model3::from_reader( 110 | File::open(&res_path.join("Haru.model3.json")).unwrap(), 111 | ) 112 | .unwrap(); 113 | 114 | // Load our cubism model 115 | let haru = cubism::model::UserModel::from_model3(&res_path, &haru_json).unwrap(); 116 | 117 | let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { todo: 0 }); 118 | // Load textures 119 | let textures = haru_json 120 | .file_references 121 | .textures 122 | .iter() 123 | .map(|texpath| load_texture(&device, &mut encoder, &res_path.join(texpath))) 124 | .collect::>(); 125 | queue.submit(&[encoder.finish()]); 126 | 127 | let mut model_renderer = cubism_core_wgpu_renderer::Renderer::new( 128 | &haru, 129 | &device, 130 | &mut queue, 131 | sc_desc.format, 132 | textures, 133 | ); 134 | let mut last_frame = Instant::now(); 135 | 136 | event_loop.run(move |event, _, control_flow| { 137 | use winit::dpi::PhysicalSize; 138 | use winit::event::{Event, WindowEvent}; 139 | use winit::event_loop::ControlFlow; 140 | match event { 141 | Event::WindowEvent { event, .. } => match event { 142 | WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, 143 | // aka minimized 144 | WindowEvent::Resized(PhysicalSize { 145 | width: 0, 146 | height: 0, 147 | }) => (), 148 | WindowEvent::Resized(PhysicalSize { width, height }) => { 149 | sc_desc.width = width; 150 | sc_desc.height = height; 151 | swap_chain = device.create_swap_chain(&surface, &sc_desc); 152 | } 153 | _ => (), 154 | }, 155 | Event::RedrawRequested(_) => { 156 | let now = Instant::now(); 157 | let delta = now - last_frame; 158 | //let delta_s = delta.as_nanos() as f32 / 1e9; 159 | last_frame = now; 160 | let frame = swap_chain.get_next_texture(); 161 | let mut encoder: wgpu::CommandEncoder = 162 | device.create_command_encoder(&wgpu::CommandEncoderDescriptor { todo: 0 }); 163 | 164 | encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 165 | color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { 166 | attachment: &frame.view, 167 | resolve_target: None, 168 | load_op: wgpu::LoadOp::Clear, 169 | store_op: wgpu::StoreOp::Store, 170 | clear_color: wgpu::Color { 171 | r: 0.8, 172 | g: 0.5, 173 | b: 0.4, 174 | a: 1.0, 175 | }, 176 | }], 177 | depth_stencil_attachment: None, 178 | }); 179 | model_renderer.draw_model(&device, &frame.view, &mut encoder, &haru); 180 | queue.submit(&[encoder.finish()]); 181 | } 182 | Event::MainEventsCleared => (), 183 | _ => (), 184 | } 185 | }) 186 | } 187 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | use_field_init_shorthand = true 2 | reorder_imports = true 3 | match_block_trailing_comma = true 4 | wrap_comments = true -------------------------------------------------------------------------------- /src/controller.rs: -------------------------------------------------------------------------------- 1 | //! Controller definitions. 2 | use fxhash::FxHashMap; 3 | 4 | use std::any::TypeId; 5 | 6 | use cubism_core::Model; 7 | 8 | mod expression; 9 | pub use self::expression::ExpressionController; 10 | mod eye_blink; 11 | pub use self::eye_blink::EyeBlink; 12 | 13 | /// Priorities used by the standard controllers of this crate. 14 | pub mod default_priorities { 15 | /// The eyeblink controller priority. 16 | pub const EYE_BLINK: usize = 100; 17 | /// The eyeblink controller priority. 18 | pub const EXPRESSION: usize = 200; 19 | } 20 | 21 | /// The controller trait. A controller is an object that modifies a models 22 | /// parameter and part values in a defined fashion. 23 | pub trait Controller: 'static { 24 | /// Run the controller on the passed [`Model`]. 25 | fn update_parameters(&mut self, model: &mut Model, delta: f32); 26 | /// The execution priority of this controller. The smallest value has the 27 | /// highest priority. 28 | fn priority(&self) -> usize; 29 | } 30 | 31 | impl dyn Controller { 32 | unsafe fn downcast_ref_unchecked(&self) -> &C { 33 | &*(self as *const Self as *const C) 34 | } 35 | unsafe fn downcast_mut_unchecked(&mut self) -> &mut C { 36 | &mut *(self as *mut Self as *mut C) 37 | } 38 | unsafe fn downcast_unchecked(self: Box) -> Box { 39 | Box::from_raw(Box::into_raw(self) as *mut C) 40 | } 41 | } 42 | 43 | /// A ControllerMap is basically a typemap over [`Controller`]s, it only allows 44 | /// one controller per type to exist and tracks their enabled status. 45 | pub struct ControllerMap { 46 | map: FxHashMap, bool)>, 47 | } 48 | 49 | impl ControllerMap { 50 | /// Creates a new empty controller map. 51 | pub fn new() -> Self { 52 | ControllerMap { 53 | map: FxHashMap::with_hasher(Default::default()), 54 | } 55 | } 56 | 57 | /// Registers a new controller, unregistering and returning back the 58 | /// previous controller of the same type if it exists. 59 | pub fn register(&mut self, controller: C) -> Option> { 60 | let controller: Box = Box::new(controller); 61 | self.map 62 | .insert(TypeId::of::(), (controller, true)) 63 | .map(|(old, _)| unsafe { old.downcast_unchecked() }) 64 | } 65 | 66 | /// Removes and returns the controller of the type if it exists in the map. 67 | pub fn remove(&mut self) -> Option> { 68 | self.map 69 | .remove(&TypeId::of::()) 70 | .map(|(old, _)| unsafe { old.downcast_unchecked() }) 71 | } 72 | 73 | /// Returns a reference to the controller of the type if it exists in the 74 | /// map. 75 | pub fn get(&self) -> Option<&C> { 76 | self.map 77 | .get(&TypeId::of::()) 78 | .map(|(con, _)| unsafe { (&**con).downcast_ref_unchecked() }) 79 | } 80 | 81 | /// Returns a mutable reference to the controller of the type if it exists 82 | /// in the map. 83 | pub fn get_mut(&mut self) -> Option<&mut C> { 84 | self.map 85 | .get_mut(&TypeId::of::()) 86 | .map(|(con, _)| unsafe { (&mut **con).downcast_mut_unchecked() }) 87 | } 88 | 89 | /// Enables or disables the controller of the type. Does nothing if there is 90 | /// no controller registered under the type. 91 | pub fn set_enabled(&mut self, enabled: bool) { 92 | if let Some((_, en)) = self.map.get_mut(&TypeId::of::()) { 93 | *en = enabled; 94 | } 95 | } 96 | 97 | /// Returns true whether the controller is enabled or not. If it doesn't 98 | /// exist it returns false. 99 | pub fn is_enabled(&self) -> bool { 100 | self.map 101 | .get(&TypeId::of::()) 102 | .map(|&(_, enabled)| enabled) 103 | .unwrap_or(false) 104 | } 105 | 106 | /// Checks whether the controller type has been registered or not. 107 | pub fn is_registered(&self) -> bool { 108 | self.map.contains_key(&TypeId::of::()) 109 | } 110 | 111 | /// Returns an iterator over the enabled controllers. 112 | pub fn enabled_controllers<'this>( 113 | &'this self, 114 | ) -> impl Iterator + 'this { 115 | self.map 116 | .values() 117 | .filter_map(|&(ref con, enabled)| if enabled { Some(&**con) } else { None }) 118 | } 119 | 120 | /// Returns an iterator over the enabled controllers. 121 | pub fn enabled_controllers_mut<'this>( 122 | &'this mut self, 123 | ) -> impl Iterator + 'this { 124 | self.map 125 | .values_mut() 126 | .filter_map(|&mut (ref mut con, enabled)| if enabled { Some(&mut **con) } else { None }) 127 | } 128 | 129 | /// Returns an iterator over the controllers in this map. 130 | pub fn controllers<'this>(&'this self) -> impl Iterator + 'this { 131 | self.map.values().map(|(con, _)| &**con) 132 | } 133 | 134 | /// Returns an iterator over the controllers in this map. 135 | pub fn controllers_mut<'this>( 136 | &'this mut self, 137 | ) -> impl Iterator + 'this { 138 | self.map.values_mut().map(|(con, _)| &mut **con) 139 | } 140 | 141 | /// Calls [`update_parameters`] on every enabled controller in the order of 142 | /// their priority. 143 | pub fn update_enabled_controllers(&mut self, model: &mut Model, delta: f32) { 144 | let mut controllers = self.enabled_controllers_mut().collect::>(); 145 | controllers.sort_unstable_by_key(|c| c.priority()); 146 | for con in controllers { 147 | con.update_parameters(model, delta); 148 | } 149 | } 150 | } 151 | 152 | impl Default for ControllerMap { 153 | fn default() -> Self { 154 | Self::new() 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/controller/expression.rs: -------------------------------------------------------------------------------- 1 | use fxhash::FxHashMap; 2 | 3 | use cubism_core::Model; 4 | 5 | use crate::controller::Controller; 6 | use crate::expression::Expression; 7 | use crate::util::SimpleSlab; 8 | 9 | /// An ExpressionController is responsible for properly registering and 10 | /// switching between expressions of a model. 11 | pub struct ExpressionController { 12 | expressions: SimpleSlab, 13 | name_map: FxHashMap, 14 | current_expr: Option, 15 | weight: f32, 16 | } 17 | 18 | impl ExpressionController { 19 | /// Creates a new empty ExpressionController. 20 | pub fn new() -> Self { 21 | Self { 22 | expressions: SimpleSlab::new(), 23 | name_map: FxHashMap::default(), 24 | current_expr: None, 25 | weight: 1.0, 26 | } 27 | } 28 | 29 | /// Inserts a new expression into the map with the name, unregistering 30 | /// and returning back the previous expression under the same name if it 31 | /// exists. 32 | pub fn register(&mut self, name: impl Into, exp: Expression) -> Option { 33 | let index = self.expressions.push(exp); 34 | self.name_map 35 | .insert(name.into(), index) 36 | .and_then(|old| self.expressions.take(old)) 37 | } 38 | 39 | /// Set the current expression, if an expression by the given name doesnt 40 | /// exist it will be set to apply no expression. 41 | pub fn set_expression(&mut self, name: &str) { 42 | self.current_expr = self.name_map.get(name).copied(); 43 | } 44 | 45 | /// Sets the expression weight to apply. 46 | /// Note: Weight will be bound between [0.0,1.0]. 47 | pub fn set_expression_weight(&mut self, weight: f32) { 48 | self.weight = weight.min(1.0).max(0.0); 49 | } 50 | 51 | /// The names of all currently registered expressions. 52 | pub fn names(&self) -> impl Iterator { 53 | self.name_map.keys().map(|s| &**s) 54 | } 55 | 56 | /// An iterator over the currently registered expressions 57 | pub fn expressions(&self) -> impl Iterator { 58 | self.expressions.iter().flatten() 59 | } 60 | } 61 | 62 | impl Controller for ExpressionController { 63 | fn update_parameters(&mut self, model: &mut Model, _: f32) { 64 | self.current_expr.map(|expr| { 65 | self.expressions 66 | .get(expr) 67 | .map(|expr| expr.apply(model, self.weight)) 68 | }); 69 | } 70 | 71 | fn priority(&self) -> usize { 72 | crate::controller::default_priorities::EXPRESSION 73 | } 74 | } 75 | 76 | impl Default for ExpressionController { 77 | fn default() -> Self { 78 | Self::new() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/controller/eye_blink.rs: -------------------------------------------------------------------------------- 1 | use cubism_core::Model; 2 | 3 | use crate::controller::Controller; 4 | 5 | #[derive(Copy, Clone, Debug)] 6 | enum EyeState { 7 | Open, 8 | Closed, 9 | Closing, 10 | Opening, 11 | } 12 | 13 | /// An Eye Blink controller. This Controller emulates eye blinking. 14 | // FIXME: sanitize timing inputs 15 | #[derive(Clone, Debug)] 16 | pub struct EyeBlink { 17 | parameter_ids: Box<[usize]>, 18 | current_state: EyeState, 19 | next_cycle: f32, 20 | blink_interval: f32, 21 | closed_time: f32, 22 | opening_time: f32, 23 | closing_time: f32, 24 | } 25 | 26 | impl Default for EyeBlink { 27 | fn default() -> Self { 28 | EyeBlink { 29 | parameter_ids: Box::new([]), 30 | current_state: EyeState::Open, 31 | next_cycle: 5.0, 32 | blink_interval: 5.0, 33 | closed_time: 0.05, 34 | opening_time: 0.15, 35 | closing_time: 0.1, 36 | } 37 | } 38 | } 39 | 40 | impl EyeBlink { 41 | /// Creates a new EyeBlink Controller acting on the specified parameter ids 42 | /// with the given timings. 43 | /// 44 | /// The controller assumes that the ids belong to the model that is being 45 | /// passed on to [`EyeBlink::update_parameters`], meaning that if this 46 | /// is not the case the application may panic on out of bounds access or 47 | /// move incorrect parts. 48 | pub fn new>>( 49 | parameter_ids: B, 50 | blink_interval: f32, 51 | closed_time: f32, 52 | opening_time: f32, 53 | closing_time: f32, 54 | ) -> Self { 55 | EyeBlink { 56 | parameter_ids: parameter_ids.into(), 57 | current_state: EyeState::Open, 58 | blink_interval, 59 | next_cycle: blink_interval, 60 | closed_time, 61 | opening_time, 62 | closing_time, 63 | } 64 | } 65 | 66 | /// Set the parameters that are affected by this controller. 67 | pub fn set_ids>>(&mut self, parameter_ids: B) { 68 | self.parameter_ids = parameter_ids.into(); 69 | } 70 | 71 | /// Set the timings of this controller. 72 | pub fn set_timings( 73 | &mut self, 74 | blink_interval: f32, 75 | closed_time: f32, 76 | opening_time: f32, 77 | closing_time: f32, 78 | ) { 79 | self.blink_interval = blink_interval.max(closed_time + opening_time + closing_time); 80 | self.next_cycle = self.blink_interval; 81 | self.closed_time = closed_time; 82 | self.opening_time = opening_time; 83 | self.closing_time = closing_time; 84 | } 85 | } 86 | 87 | impl Controller for EyeBlink { 88 | fn update_parameters(&mut self, model: &mut Model, delta: f32) { 89 | self.next_cycle -= delta; 90 | let val = match self.current_state { 91 | EyeState::Open => { 92 | if self.next_cycle <= 0.0 { 93 | self.current_state = EyeState::Closing; 94 | self.next_cycle += self.closing_time; 95 | } 96 | 1.0 97 | }, 98 | EyeState::Closed => { 99 | if self.next_cycle <= 0.0 { 100 | self.current_state = EyeState::Opening; 101 | self.next_cycle += self.opening_time; 102 | } 103 | 0.0 104 | }, 105 | EyeState::Opening => { 106 | if self.next_cycle <= 0.0 { 107 | self.current_state = EyeState::Open; 108 | self.next_cycle += self.blink_interval; 109 | 1.0 110 | } else { 111 | (self.opening_time - self.next_cycle) / self.opening_time 112 | } 113 | }, 114 | EyeState::Closing => { 115 | if self.next_cycle <= 0.0 { 116 | self.current_state = EyeState::Closed; 117 | self.next_cycle += self.closed_time; 118 | 0.0 119 | } else { 120 | self.next_cycle / self.closing_time 121 | } 122 | }, 123 | }; 124 | for par in self.parameter_ids.iter().copied() { 125 | model.parameter_values_mut()[par] = val; 126 | } 127 | } 128 | 129 | fn priority(&self) -> usize { 130 | crate::controller::default_priorities::EYE_BLINK 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Errors returned by cubism. 2 | use std::{error, fmt, io}; 3 | 4 | use cubism_core::MocError; 5 | 6 | /// The result type, returned by this library. 7 | pub type CubismResult = std::result::Result; 8 | 9 | /// An error returned by this library. 10 | #[derive(Debug)] 11 | pub enum CubismError { 12 | /// A moc loading error occurred while loading a model. 13 | Moc(MocError), 14 | /// A json error occurred while serializing or deserializing a json 15 | /// file. 16 | Json(serde_json::Error), 17 | /// An io error occurred. 18 | Io(io::Error), 19 | } 20 | 21 | impl error::Error for CubismError {} 22 | impl fmt::Display for CubismError { 23 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 24 | match self { 25 | CubismError::Moc(e) => (e as &dyn fmt::Display).fmt(fmt), 26 | CubismError::Json(e) => (e as &dyn fmt::Display).fmt(fmt), 27 | CubismError::Io(e) => (e as &dyn fmt::Display).fmt(fmt), 28 | } 29 | } 30 | } 31 | 32 | impl From for CubismError { 33 | fn from(e: MocError) -> Self { 34 | CubismError::Moc(e) 35 | } 36 | } 37 | 38 | impl From for CubismError { 39 | fn from(e: serde_json::Error) -> Self { 40 | CubismError::Json(e) 41 | } 42 | } 43 | 44 | impl From for CubismError { 45 | fn from(e: io::Error) -> Self { 46 | CubismError::Io(e) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/expression.rs: -------------------------------------------------------------------------------- 1 | //! A model expression. 2 | 3 | use std::{fs, path::Path}; 4 | 5 | use crate::error::CubismResult; 6 | use crate::json::expression::{Expression3, ExpressionBlendType, ExpressionParameter}; 7 | 8 | use cubism_core::Model; 9 | 10 | /// A model expression. 11 | #[derive(Clone, Debug)] 12 | pub struct Expression { 13 | fade_in: f32, 14 | fade_out: f32, 15 | parameters: Vec<(usize, ExpressionBlendType, f32)>, 16 | } 17 | 18 | impl Expression { 19 | /// Creates a Expression from a path of .exp3.json file and the 20 | /// corresponding model. 21 | pub fn from_exp3_json>(model: &Model, path: P) -> CubismResult { 22 | let Expression3 { 23 | fade_in_time: fade_in, 24 | fade_out_time: fade_out, 25 | parameters, 26 | .. 27 | } = Expression3::from_reader(fs::File::open(path)?)?; 28 | Ok(Expression { 29 | fade_in, 30 | fade_out, 31 | parameters: parameters 32 | .into_iter() 33 | .flat_map( 34 | |ExpressionParameter { 35 | id, 36 | blend_type, 37 | value, 38 | }| { 39 | model 40 | .parameter_ids() 41 | .iter() 42 | .position(|id2| *id2 == id) 43 | .map(|idx| (idx, blend_type, value)) 44 | }, 45 | ) 46 | .collect::>(), 47 | }) 48 | } 49 | 50 | /// Apply an expression to a model. 51 | pub fn apply(&self, model: &mut Model, mut weight: f32) { 52 | weight = weight.min(1.0).max(0.0); 53 | for (id, blend_type, value) in self.parameters.iter().copied() { 54 | let model_value = &mut model.parameter_values_mut()[id]; 55 | *model_value = match blend_type { 56 | ExpressionBlendType::Add => value.mul_add(weight, *model_value), 57 | ExpressionBlendType::Multiply => *model_value * (value - 1.0).mul_add(weight, 1.0), 58 | ExpressionBlendType::Overwrite => value * weight, 59 | }; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/id.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | // https://docs.live2d.com/cubism-editor-manual/standard-parametor-list/# 3 | 4 | /// Common Group IDs 5 | pub mod groups { 6 | pub static EYE_BLINK: &str = "EyeBlink"; 7 | pub static LIP_SYNC: &str = "LipSync"; 8 | } 9 | 10 | /// Standard Part IDs 11 | pub mod parts { 12 | pub static HIT_AREA_PREFIX: &str = "HitArea"; 13 | pub static HIT_AREA_HEAD: &str = "Head"; 14 | pub static HIT_AREA_BODY: &str = "Body"; 15 | 16 | pub static CORE: &str = "Parts01Core"; 17 | 18 | pub static ARM_PREFIX: &str = "Parts01Arm_"; 19 | pub static ARM_L_PREFIX: &str = "Parts01ArmL_"; 20 | pub static ARM_R_PREFIX: &str = "Parts01ArmR_"; 21 | } 22 | 23 | /// Standard Parameter IDs 24 | pub mod param { 25 | pub static ANGLE_X: &str = "ParamAngleX"; 26 | pub static ANGLE_Y: &str = "ParamAngleY"; 27 | pub static ANGLE_Z: &str = "ParamAngleZ"; 28 | 29 | pub static EYE_L_OPEN: &str = "ParamEyeLOpen"; 30 | pub static EYE_L_SMILE: &str = "ParamEyeLSmile"; 31 | pub static EYE_R_OPEN: &str = "ParamEyeROpen"; 32 | pub static EYE_R_SMILE: &str = "ParamEyeRSmile"; 33 | pub static EYE_BALL_X: &str = "ParamEyeBallX"; 34 | pub static EYE_BALL_Y: &str = "ParamEyeBallY"; 35 | pub static EYE_BALL_FORM: &str = "ParamEyeBallForm"; 36 | 37 | pub static BROW_LY: &str = "ParamBrowLY"; 38 | pub static BROW_RY: &str = "ParamBrowRY"; 39 | pub static BROW_LX: &str = "ParamBrowLX"; 40 | pub static BROW_RX: &str = "ParamBrowRX"; 41 | pub static BROW_L_ANGLE: &str = "ParamBrowLAngle"; 42 | pub static BROW_R_ANGLE: &str = "ParamBrowRAngle"; 43 | pub static BROW_L_FORM: &str = "ParamBrowLForm"; 44 | pub static BROW_R_FORM: &str = "ParamBrowRForm"; 45 | 46 | pub static MOUTH_FORM: &str = "ParamMouthForm"; 47 | pub static MOUTH_OPEN_Y: &str = "ParamMouthOpenY"; 48 | 49 | pub static CHEEK: &str = "ParamCheek"; 50 | 51 | pub static BODY_ANGLE_X: &str = "ParamBodyAngleX"; 52 | pub static BODY_ANGLE_Y: &str = "ParamBodyAngleY"; 53 | pub static BODY_ANGLE_Z: &str = "ParamBodyAngleZ"; 54 | 55 | pub static BREATH: &str = "ParamBreath"; 56 | 57 | pub static ARM_LA: &str = "ParamArmLA"; 58 | pub static ARM_RA: &str = "ParamArmRA"; 59 | pub static ARM_LB: &str = "ParamArmLB"; 60 | pub static ARM_RB: &str = "ParamArmRB"; 61 | 62 | pub static HAND_L: &str = "ParamHandL"; 63 | pub static HAND_R: &str = "ParamHandR"; 64 | 65 | pub static HAIR_FRONT: &str = "ParamHairFront"; 66 | pub static HAIR_SIDE: &str = "ParamHairSide"; 67 | pub static HAIR_BACK: &str = "ParamHairBack"; 68 | pub static HAIR_FLUFFY: &str = "ParamHairFluffy"; 69 | 70 | pub static SHOULDER_Y: &str = "ParamShoulderY"; 71 | 72 | pub static BUST_X: &str = "ParamBustX"; 73 | pub static BUST_Y: &str = "ParamBustY"; 74 | pub static BASE_X: &str = "ParamBaseX"; 75 | pub static BASE_Y: &str = "ParamBaseY"; 76 | 77 | pub static NONE: &str = "NONE:"; 78 | } 79 | -------------------------------------------------------------------------------- /src/json.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | 3 | pub mod cdi; 4 | pub mod expression; 5 | pub mod model; 6 | pub mod motion; 7 | pub mod physics; 8 | pub mod pose; 9 | pub mod user_data; 10 | 11 | /// Utility function to map non-positive floats to 1.0 after deserialization 12 | pub(self) fn de_fade_time<'de, D>(d: D) -> Result 13 | where 14 | D: serde::Deserializer<'de>, 15 | { 16 | use serde::Deserialize; 17 | f32::deserialize(d).map(|val| if val <= 0.0 { 1.0 } else { val }) 18 | } 19 | 20 | pub(self) const fn float_1() -> f32 { 21 | 1.0 22 | } 23 | -------------------------------------------------------------------------------- /src/json/cdi.rs: -------------------------------------------------------------------------------- 1 | /// Parses .cdi3.json. 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use std::str::FromStr; 5 | 6 | /// Rust structure representation for .cdi3.json file. 7 | #[derive(Clone, Debug, Deserialize, Serialize)] 8 | #[serde(rename_all = "PascalCase")] 9 | pub struct Cdi3 { 10 | pub version: usize, 11 | #[serde(default)] 12 | pub parameters: Vec, 13 | #[serde(default)] 14 | pub parameter_groups: Vec, 15 | #[serde(default)] 16 | pub parts: Vec, 17 | } 18 | 19 | #[derive(Clone, Debug, Deserialize, Serialize)] 20 | #[serde(rename_all = "PascalCase")] 21 | pub struct Cdi3Parameter { 22 | id: String, 23 | #[serde(default)] 24 | group_id: String, 25 | name: String, 26 | } 27 | 28 | #[derive(Clone, Debug, Deserialize, Serialize)] 29 | #[serde(rename_all = "PascalCase")] 30 | pub struct Cdi3ParameterGroup { 31 | id: String, 32 | #[serde(default)] 33 | group_id: String, 34 | name: String, 35 | } 36 | 37 | #[derive(Clone, Debug, Deserialize, Serialize)] 38 | #[serde(rename_all = "PascalCase")] 39 | pub struct Cdi3Part { 40 | id: String, 41 | name: String, 42 | } 43 | 44 | impl Cdi3 { 45 | /// Parses a Cdi3 from a .cdi3.json reader. 46 | #[inline] 47 | pub fn from_reader(r: R) -> serde_json::Result { 48 | serde_json::from_reader(r) 49 | } 50 | } 51 | 52 | impl FromStr for Cdi3 { 53 | type Err = serde_json::Error; 54 | 55 | /// Parses a Cdi3 from a .cdi3.json string. 56 | #[inline] 57 | fn from_str(s: &str) -> serde_json::Result { 58 | serde_json::from_str(s) 59 | } 60 | } 61 | 62 | #[test] 63 | fn json_samples_cdi3() { 64 | use std::iter::FromIterator; 65 | let path = 66 | std::path::PathBuf::from_iter(&[env!("CUBISM_CORE"), "Samples/Res/Rice/Rice.cdi3.json"]); 67 | 68 | Cdi3::from_str( 69 | &std::fs::read_to_string(&path) 70 | .unwrap_or_else(|e| panic!("error while reading {:?}: {:?}", &path, e)), 71 | ) 72 | .unwrap_or_else(|e| panic!("error while parsing {:?}: {:?}", &path, e)); 73 | } 74 | -------------------------------------------------------------------------------- /src/json/expression.rs: -------------------------------------------------------------------------------- 1 | // Parses .exp3.json. 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use std::str::FromStr; 6 | 7 | /// Rust structure representation for .exp3.json file. 8 | #[derive(Clone, Debug, Deserialize, Serialize)] 9 | #[serde(rename_all = "PascalCase")] 10 | pub struct Expression3 { 11 | #[serde(rename = "Type")] 12 | pub ty: String, 13 | #[serde(deserialize_with = "super::de_fade_time", default = "super::float_1")] 14 | pub fade_in_time: f32, 15 | #[serde(deserialize_with = "super::de_fade_time", default = "super::float_1")] 16 | pub fade_out_time: f32, 17 | pub parameters: Vec, 18 | } 19 | 20 | impl Expression3 { 21 | /// Parses a Expression3 from a .expression3.json reader. 22 | #[inline] 23 | pub fn from_reader(r: R) -> serde_json::Result { 24 | serde_json::from_reader(r) 25 | } 26 | } 27 | 28 | impl FromStr for Expression3 { 29 | type Err = serde_json::Error; 30 | 31 | /// Parses a Expression3 from a .expression3.json string. 32 | #[inline] 33 | fn from_str(s: &str) -> serde_json::Result { 34 | serde_json::from_str(s) 35 | } 36 | } 37 | 38 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] 39 | pub enum ExpressionBlendType { 40 | Add = 0x00, 41 | Multiply = 0x01, 42 | Overwrite = 0x02, 43 | } 44 | 45 | #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 46 | #[serde(rename_all = "PascalCase")] 47 | pub struct ExpressionParameter { 48 | pub id: String, 49 | #[serde(rename = "Blend")] 50 | pub blend_type: ExpressionBlendType, 51 | pub value: f32, 52 | } 53 | 54 | #[test] 55 | fn json_samples_exp3() { 56 | use std::iter::FromIterator; 57 | let path = std::path::PathBuf::from_iter(&[env!("CUBISM_CORE"), "Samples/Res"]); 58 | for model in &["Haru/expressions", "Natori/exp"] { 59 | let exp_path = path.join(model); 60 | let expressions = std::fs::read_dir(exp_path).unwrap(); 61 | 62 | for exp in expressions { 63 | let exp_path = exp.unwrap().path(); 64 | 65 | if !exp_path.is_file() { 66 | continue; 67 | } 68 | 69 | Expression3::from_str( 70 | &std::fs::read_to_string(&exp_path) 71 | .unwrap_or_else(|e| panic!("error while reading {:?}: {:?}", &exp_path, e)), 72 | ) 73 | .unwrap_or_else(|e| panic!("error while parsing {:?}: {:?}", &exp_path, e)); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/json/model.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use std::{path::PathBuf, str::FromStr}; 4 | 5 | #[derive(Clone, Debug, Deserialize, Serialize)] 6 | #[serde(rename_all = "PascalCase")] 7 | pub struct Model3 { 8 | pub version: usize, 9 | #[serde(default)] 10 | pub file_references: FileReferences, 11 | #[serde(default)] 12 | pub groups: Vec, 13 | #[serde(default)] 14 | pub hit_areas: Vec, 15 | pub layout: Option, 16 | } 17 | 18 | impl Model3 { 19 | /// Parses a Model3 from a .model3.json reader. 20 | #[inline] 21 | pub fn from_reader(r: R) -> serde_json::Result { 22 | serde_json::from_reader(r) 23 | } 24 | } 25 | 26 | impl FromStr for Model3 { 27 | type Err = serde_json::Error; 28 | 29 | /// Parses a Model3 from a .model3.json string. 30 | #[inline] 31 | fn from_str(s: &str) -> serde_json::Result { 32 | serde_json::from_str(s) 33 | } 34 | } 35 | 36 | #[derive(Clone, Debug, Default, Deserialize, Serialize)] 37 | #[serde(rename_all = "PascalCase")] 38 | pub struct FileReferences { 39 | pub moc: Option, 40 | #[serde(default)] 41 | pub textures: Vec, 42 | pub pose: Option, 43 | pub physics: Option, 44 | #[serde(default)] 45 | pub expressions: Vec, 46 | #[serde(default)] 47 | pub motions: Motions, 48 | pub user_data: Option, 49 | } 50 | 51 | #[derive(Clone, Debug, Deserialize, Serialize)] 52 | #[serde(rename_all = "PascalCase")] 53 | pub struct Group { 54 | pub target: GroupTarget, 55 | pub name: String, 56 | pub ids: Vec, 57 | } 58 | 59 | #[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialOrd, PartialEq)] 60 | pub enum GroupTarget { 61 | Parameter, 62 | Part, 63 | //Drawable? 64 | } 65 | 66 | // TODO: Might very well be just a hashmap figure out whether these names should 67 | // be hardcoded or not 68 | #[derive(Clone, Debug, Default, Deserialize, Serialize)] 69 | #[serde(rename_all = "PascalCase")] 70 | pub struct Motions { 71 | #[serde(default)] 72 | pub idle: Vec, 73 | #[serde(default, rename = "TapBody")] 74 | pub tap_body: Vec, 75 | #[serde(default, rename = "PinchIn")] 76 | pub pinch_in: Vec, 77 | #[serde(default, rename = "PinchOut")] 78 | pub pinch_out: Vec, 79 | #[serde(default)] 80 | pub shake: Vec, 81 | #[serde(default, rename = "FlickHead")] 82 | pub flick_head: Vec, 83 | } 84 | 85 | #[derive(Clone, Debug, Deserialize, Serialize)] 86 | #[serde(rename_all = "PascalCase")] 87 | pub struct Motion { 88 | pub file: PathBuf, 89 | #[serde(rename = "FadeInTime", default = "super::float_1")] 90 | pub fade_in_time: f32, 91 | #[serde(rename = "FadeOutTime", default = "super::float_1")] 92 | pub fade_out_time: f32, 93 | } 94 | 95 | #[derive(Clone, Debug, Deserialize, Serialize)] 96 | #[serde(rename_all = "PascalCase")] 97 | pub struct Expression { 98 | pub name: String, 99 | pub file: PathBuf, 100 | } 101 | 102 | #[derive(Clone, Debug, Deserialize, Serialize)] 103 | #[serde(rename_all = "PascalCase")] 104 | pub struct HitArea { 105 | pub name: String, 106 | pub id: String, 107 | } 108 | 109 | #[derive(Copy, Clone, Debug, Default, Deserialize, Serialize)] 110 | #[serde(rename_all = "PascalCase")] 111 | pub struct Layout { 112 | #[serde(rename = "CenterX")] 113 | pub center_x: f32, 114 | #[serde(rename = "CenterY")] 115 | pub center_y: f32, 116 | pub x: f32, 117 | pub y: f32, 118 | pub width: f32, 119 | pub height: f32, 120 | } 121 | 122 | #[test] 123 | fn json_samples_model3() { 124 | use std::iter::FromIterator; 125 | let path = std::path::PathBuf::from_iter(&[env!("CUBISM_CORE"), "Samples/Res"]); 126 | for model in &["Haru", "Hiyori", "Mark", "Natori"] { 127 | let model_path = path.join([model, "/", model, ".model3.json"].concat()); 128 | Model3::from_str( 129 | &std::fs::read_to_string(&model_path) 130 | .unwrap_or_else(|e| panic!("error while reading {:?}: {:?}", &model_path, e)), 131 | ) 132 | .unwrap_or_else(|e| panic!("error while parsing {:?}: {:?}", &model_path, e)); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/json/motion.rs: -------------------------------------------------------------------------------- 1 | //! Parses .motion3.json. 2 | use serde::{self, Deserialize, Serialize}; 3 | 4 | use std::str::FromStr; 5 | 6 | /// Rust structure representation for Motion3 metadata. 7 | #[derive(Copy, Clone, Debug, Serialize, Deserialize)] 8 | #[serde(rename_all = "PascalCase")] 9 | pub struct Meta { 10 | /// Duration of a motion. 11 | pub duration: f32, 12 | /// Frame per second. 13 | pub fps: f32, 14 | #[serde(rename = "Loop")] 15 | /// True if the motion is looped. 16 | pub looped: bool, 17 | /// TODO: 18 | #[serde(rename = "AreBeziersRestricted")] 19 | pub restricted_beziers: bool, 20 | /// A number of curves that the motion3.json file has. 21 | pub curve_count: usize, 22 | /// A number of segments that the motion3.json file has. 23 | pub total_segment_count: usize, 24 | /// A number of points that the motion3.json file has. 25 | pub total_point_count: usize, 26 | /// A number of user data fields that the motion3.json file has. 27 | pub user_data_count: usize, 28 | /// A total size of user data. 29 | pub total_user_data_size: usize, 30 | } 31 | 32 | /// Point. 33 | #[derive(Copy, Clone, Debug, Serialize, Deserialize)] 34 | pub struct SegmentPoint { 35 | /// Time. 36 | pub time: f32, 37 | /// Value. 38 | pub value: f32, 39 | } 40 | 41 | /// Segment. 42 | #[derive(Copy, Clone, Debug)] 43 | pub enum Segment { 44 | /// Linear. 45 | Linear(SegmentPoint, SegmentPoint), 46 | /// Bezier curve. 47 | Bezier([SegmentPoint; 4]), 48 | /// Stepped. 49 | Stepped(SegmentPoint, f32), 50 | /// Inverse stepped. 51 | InverseStepped(f32, SegmentPoint), 52 | } 53 | 54 | mod segment_parser { 55 | use crate::json::motion::{Segment, SegmentPoint}; 56 | use serde::{ 57 | self, 58 | de::{self, SeqAccess, Visitor}, 59 | Deserializer, Serializer, 60 | }; 61 | 62 | struct SegmentVisitor; 63 | 64 | impl<'de> Visitor<'de> for SegmentVisitor { 65 | type Value = Vec; 66 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 67 | formatter.write_str("sequence of integers / numbers") 68 | } 69 | 70 | fn visit_seq>(self, seq: A) -> Result { 71 | let mut seq = seq; 72 | let mut ret = vec![]; 73 | 74 | const SEG_LINEAR: i32 = 0; // リニア 75 | const SEG_BEZIER: i32 = 1; // ベジェ曲線 76 | const SEG_STEPPED: i32 = 2; // ステップ 77 | const SEG_INV: i32 = 3; // インバースステップ 78 | 79 | // parse the first position 80 | let t0: f32 = seq.next_element()?.unwrap(); 81 | let v0: f32 = seq.next_element()?.unwrap(); 82 | 83 | let mut last_point = SegmentPoint { 84 | time: t0, 85 | value: v0, 86 | }; 87 | 88 | // parse positions 89 | while let Some(seg_type) = seq.next_element()? as Option { 90 | match seg_type { 91 | SEG_LINEAR => { 92 | let t0: f32 = seq.next_element()?.unwrap(); 93 | let v0: f32 = seq.next_element()?.unwrap(); 94 | 95 | let next_point = SegmentPoint { 96 | time: t0, 97 | value: v0, 98 | }; 99 | 100 | ret.push(Segment::Linear(last_point, next_point)); 101 | last_point = next_point; 102 | }, 103 | SEG_STEPPED => { 104 | let t0: f32 = seq.next_element()?.unwrap(); 105 | let v0: f32 = seq.next_element()?.unwrap(); 106 | 107 | ret.push(Segment::Stepped(last_point, t0)); 108 | 109 | last_point = SegmentPoint { 110 | time: t0, 111 | value: v0, 112 | }; 113 | }, 114 | SEG_INV => { 115 | let t0: f32 = seq.next_element()?.unwrap(); 116 | let v0: f32 = seq.next_element()?.unwrap(); 117 | 118 | let tn = last_point.time; 119 | 120 | last_point = SegmentPoint { 121 | time: t0, 122 | value: v0, 123 | }; 124 | 125 | ret.push(Segment::InverseStepped(tn, last_point)); 126 | }, 127 | SEG_BEZIER => { 128 | let t0: f32 = seq.next_element()?.unwrap(); 129 | let v0: f32 = seq.next_element()?.unwrap(); 130 | let t1: f32 = seq.next_element()?.unwrap(); 131 | let v1: f32 = seq.next_element()?.unwrap(); 132 | let t2: f32 = seq.next_element()?.unwrap(); 133 | let v2: f32 = seq.next_element()?.unwrap(); 134 | 135 | let next_point = SegmentPoint { 136 | time: t2, 137 | value: v2, 138 | }; 139 | 140 | ret.push(Segment::Bezier([ 141 | last_point, 142 | SegmentPoint { 143 | time: t0, 144 | value: v0, 145 | }, 146 | SegmentPoint { 147 | time: t1, 148 | value: v1, 149 | }, 150 | next_point, 151 | ])); 152 | 153 | last_point = next_point; 154 | }, 155 | _ => return Err(de::Error::custom("invalid segment format.")), 156 | } 157 | } 158 | 159 | Ok(ret) 160 | } 161 | } 162 | 163 | pub fn serialize(_: &[Segment], _: S) -> Result 164 | where 165 | S: Serializer, 166 | { 167 | unimplemented!() 168 | } 169 | 170 | pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> 171 | where 172 | D: Deserializer<'de>, 173 | { 174 | deserializer.deserialize_seq(SegmentVisitor) 175 | } 176 | } 177 | 178 | /// Rust structure representation for Motion3 curve data. 179 | #[derive(Clone, Debug, Serialize, Deserialize)] 180 | #[serde(rename_all = "PascalCase")] 181 | pub struct Curve { 182 | /// Target. 183 | pub target: String, 184 | /// Id. 185 | pub id: String, 186 | /// Segments. 187 | #[serde(with = "segment_parser")] 188 | pub segments: Vec, 189 | /// Fade-in time. 1.0 [sec] as default. 190 | #[serde(default = "super::float_1")] 191 | pub fade_in_time: f32, 192 | /// Fade-out time. 1.0 [sec] as default. 193 | #[serde(default = "super::float_1")] 194 | pub fade_out_time: f32, 195 | } 196 | 197 | /// Rust structure representation for Motion3. 198 | #[derive(Clone, Debug, Serialize, Deserialize)] 199 | #[serde(rename_all = "PascalCase")] 200 | pub struct Motion3 { 201 | /// Version. 202 | pub version: u32, 203 | /// Metadata. 204 | pub meta: Meta, 205 | /// Curves. 206 | pub curves: Vec, 207 | #[serde(default)] 208 | pub user_data: Vec, 209 | } 210 | 211 | impl Motion3 { 212 | /// Parses a Motion3 from a .motion3.json reader. 213 | #[inline] 214 | pub fn from_reader(r: R) -> serde_json::Result { 215 | serde_json::from_reader(r) 216 | } 217 | } 218 | 219 | impl FromStr for Motion3 { 220 | type Err = serde_json::Error; 221 | 222 | /// Parses a Motion3 from a .motion3.json string. 223 | #[inline] 224 | fn from_str(s: &str) -> serde_json::Result { 225 | serde_json::from_str(s) 226 | } 227 | } 228 | 229 | #[derive(Clone, Debug, Serialize, Deserialize)] 230 | #[serde(rename_all = "PascalCase")] 231 | pub struct MotionUserData { 232 | pub time: f32, 233 | pub value: String, 234 | } 235 | 236 | #[test] 237 | fn json_samples_motion3() { 238 | use std::iter::FromIterator; 239 | let path = std::path::PathBuf::from_iter(&[env!("CUBISM_CORE"), "Samples/Res"]); 240 | for model in &["Haru", "Hiyori", "Mark", "Natori"] { 241 | let motion_path = path.join([model, "/motions/"].concat()); 242 | let motions = std::fs::read_dir(motion_path).unwrap(); 243 | 244 | for motion in motions { 245 | let motion_path = motion.unwrap().path(); 246 | 247 | if !motion_path.is_file() { 248 | continue; 249 | } 250 | 251 | Motion3::from_str( 252 | &std::fs::read_to_string(&motion_path) 253 | .unwrap_or_else(|e| panic!("error while reading {:?}: {:?}", &motion_path, e)), 254 | ) 255 | .unwrap_or_else(|e| panic!("error while parsing {:?}: {:?}", &motion_path, e)); 256 | } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/json/physics.rs: -------------------------------------------------------------------------------- 1 | /// Parses .physics3.json. 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use std::str::FromStr; 5 | 6 | /// Rust structure representation for .physics3.json file. 7 | #[derive(Clone, Debug, Deserialize, Serialize)] 8 | #[serde(rename_all = "PascalCase")] 9 | pub struct Physics3 { 10 | version: usize, 11 | meta: Physics3Meta, 12 | physics_settings: Vec, 13 | } 14 | 15 | #[derive(Clone, Debug, Deserialize, Serialize)] 16 | #[serde(rename_all = "PascalCase")] 17 | pub struct PhysicsSetting { 18 | id: String, 19 | #[serde(default)] 20 | input: Vec, 21 | #[serde(default)] 22 | output: Vec, 23 | #[serde(default)] 24 | vertices: Vec, 25 | normalization: Option, 26 | } 27 | 28 | #[derive(Clone, Debug, Deserialize, Serialize)] 29 | #[serde(rename_all = "PascalCase")] 30 | pub struct PhysicsInput { 31 | source: PhysicsTarget, 32 | weight: f32, 33 | #[serde(rename = "Type")] 34 | ty: String, 35 | reflect: bool, 36 | } 37 | 38 | #[derive(Clone, Debug, Deserialize, Serialize)] 39 | #[serde(rename_all = "PascalCase")] 40 | pub struct PhysicsOutput { 41 | destination: PhysicsTarget, 42 | vertex_index: usize, 43 | scale: f32, 44 | weight: f32, 45 | #[serde(rename = "Type")] 46 | ty: String, 47 | reflect: bool, 48 | } 49 | 50 | #[derive(Clone, Copy, Debug, Deserialize, Serialize)] 51 | #[serde(rename_all = "PascalCase")] 52 | pub struct PhysicsVertex { 53 | position: Vec2D, 54 | mobility: f32, 55 | delay: f32, 56 | acceleration: f32, 57 | radius: f32, 58 | } 59 | 60 | #[derive(Clone, Copy, Debug, Deserialize, Serialize)] 61 | #[serde(rename_all = "PascalCase")] 62 | pub struct PhysicsNormalization { 63 | position: PhysicsNormalizationParameter, 64 | angle: PhysicsNormalizationParameter, 65 | } 66 | 67 | #[derive(Clone, Copy, Debug, Deserialize, Serialize)] 68 | #[serde(rename_all = "PascalCase")] 69 | pub struct PhysicsNormalizationParameter { 70 | minimum: f32, 71 | maximum: f32, 72 | default: f32, 73 | } 74 | 75 | #[derive(Clone, Debug, Deserialize, Serialize)] 76 | #[serde(rename_all = "PascalCase")] 77 | pub struct PhysicsTarget { 78 | target: String, 79 | id: String, 80 | } 81 | 82 | #[derive(Clone, Debug, Deserialize, Serialize)] 83 | #[serde(rename_all = "PascalCase")] 84 | pub struct Physics3Meta { 85 | total_input_count: usize, 86 | total_output_count: usize, 87 | vertex_count: usize, 88 | physics_setting_count: usize, 89 | effective_forces: EffectiveForces, 90 | physics_dictionary: Vec, 91 | } 92 | 93 | #[derive(Clone, Debug, Deserialize, Serialize)] 94 | #[serde(rename_all = "PascalCase")] 95 | pub struct PhysicsIdName { 96 | id: String, 97 | name: String, 98 | } 99 | 100 | #[derive(Clone, Copy, Debug, Deserialize, Serialize)] 101 | #[serde(rename_all = "PascalCase")] 102 | pub struct EffectiveForces { 103 | #[serde(default)] 104 | gravity: Vec2D, 105 | #[serde(default)] 106 | wind: Vec2D, 107 | } 108 | 109 | #[derive(Clone, Copy, Default, Debug, Deserialize, Serialize)] 110 | #[serde(rename_all = "PascalCase")] 111 | pub struct Vec2D { 112 | x: f32, 113 | y: f32, 114 | } 115 | 116 | impl Physics3 { 117 | /// Parses a Physics3 from a .physics3.json reader. 118 | #[inline] 119 | pub fn from_reader(r: R) -> serde_json::Result { 120 | serde_json::from_reader(r) 121 | } 122 | } 123 | 124 | impl FromStr for Physics3 { 125 | type Err = serde_json::Error; 126 | 127 | /// Parses a Physics3 from a .physics3.json string. 128 | #[inline] 129 | fn from_str(s: &str) -> serde_json::Result { 130 | serde_json::from_str(s) 131 | } 132 | } 133 | 134 | #[test] 135 | fn json_samples_physics3() { 136 | use std::iter::FromIterator; 137 | let path = std::path::PathBuf::from_iter(&[env!("CUBISM_CORE"), "Samples/Res"]); 138 | for model in &["Rice", "Hiyori", "Mark", "Natori"] { 139 | let path = path.join([model, "/", model, ".physics3.json"].concat()); 140 | 141 | Physics3::from_str( 142 | &std::fs::read_to_string(&path) 143 | .unwrap_or_else(|e| panic!("error while reading {:?}: {:?}", &path, e)), 144 | ) 145 | .unwrap_or_else(|e| panic!("error while parsing {:?}: {:?}", &path, e)); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/json/pose.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use std::str::FromStr; 4 | 5 | #[derive(Clone, Debug, Deserialize, Serialize)] 6 | #[serde(rename_all = "PascalCase")] 7 | pub struct Pose3 { 8 | #[serde(rename = "Type")] 9 | pub ty: String, 10 | pub groups: Vec, 11 | #[serde(default = "Pose3::fade_in_time_default")] 12 | pub fade_in_time: f32, 13 | } 14 | 15 | impl Pose3 { 16 | /// Parses a Pose3 from a .pose3.json reader. 17 | #[inline] 18 | pub fn from_reader(r: R) -> serde_json::Result { 19 | serde_json::from_reader(r) 20 | } 21 | 22 | fn fade_in_time_default() -> f32 { 23 | 0.5 24 | } 25 | } 26 | 27 | impl FromStr for Pose3 { 28 | type Err = serde_json::Error; 29 | 30 | /// Parses a Pose3 from a .pose3.json string. 31 | #[inline] 32 | fn from_str(s: &str) -> serde_json::Result { 33 | serde_json::from_str(s) 34 | } 35 | } 36 | 37 | #[derive(Clone, Debug, Deserialize, Serialize)] 38 | #[serde(rename_all = "PascalCase")] 39 | pub struct PoseItem { 40 | pub id: String, 41 | pub link: Vec, 42 | } 43 | 44 | type PoseGroup = Vec; 45 | 46 | #[test] 47 | fn json_samples_pose3() { 48 | use std::iter::FromIterator; 49 | let path = std::path::PathBuf::from_iter(&[env!("CUBISM_CORE"), "Samples/Res"]); 50 | for model in &["Haru", "Hiyori", "Natori"] { 51 | let pose_path = path.join([model, "/", model, ".pose3.json"].concat()); 52 | Pose3::from_str( 53 | &std::fs::read_to_string(&pose_path) 54 | .unwrap_or_else(|e| panic!("error while reading {:?}: {:?}", &pose_path, e)), 55 | ) 56 | .unwrap_or_else(|e| panic!("error while parsing {:?}: {:?}", &pose_path, e)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/json/user_data.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use std::str::FromStr; 4 | 5 | #[derive(Clone, Debug, Deserialize, Serialize)] 6 | #[serde(rename_all = "PascalCase")] 7 | pub struct UserData3 { 8 | pub version: usize, 9 | pub meta: Meta, 10 | pub user_data: Vec, 11 | } 12 | 13 | impl UserData3 { 14 | /// Parses a UserData3 from a .userdata3.json reader. 15 | #[inline] 16 | pub fn from_reader(r: R) -> serde_json::Result { 17 | serde_json::from_reader(r) 18 | } 19 | } 20 | 21 | impl FromStr for UserData3 { 22 | type Err = serde_json::Error; 23 | 24 | /// Parses a UserData3 from a .userdata3.json string. 25 | #[inline] 26 | fn from_str(s: &str) -> serde_json::Result { 27 | serde_json::from_str(s) 28 | } 29 | } 30 | 31 | #[derive(Copy, Clone, Debug, Deserialize, Serialize)] 32 | #[serde(rename_all = "PascalCase")] 33 | pub struct Meta { 34 | pub user_data_count: usize, 35 | pub total_user_data_size: usize, 36 | } 37 | 38 | #[derive(Clone, Debug, Deserialize, Serialize)] 39 | #[serde(rename_all = "PascalCase")] 40 | pub struct UserData { 41 | pub target: UserDataTarget, 42 | pub id: String, 43 | pub value: String, 44 | } 45 | 46 | #[derive(Clone, Debug, Deserialize, Serialize)] 47 | #[serde(rename_all = "PascalCase")] 48 | pub enum UserDataTarget { 49 | ArtMesh, 50 | } 51 | 52 | #[test] 53 | fn json_samples_userdata3() { 54 | use std::iter::FromIterator; 55 | let path = std::path::PathBuf::from_iter(&[env!("CUBISM_CORE"), "Samples/Res"]); 56 | for model in &["Haru", "Hiyori", "Mark"] { 57 | let userdata_path = path.join([model, "/", model, ".userdata3.json"].concat()); 58 | UserData3::from_str( 59 | &std::fs::read_to_string(&userdata_path) 60 | .unwrap_or_else(|e| panic!("error while parsing {:?}: {:?}", &userdata_path, e)), 61 | ) 62 | .unwrap_or_else(|e| panic!("error while parsing {:?}: {:?}", &userdata_path, e)); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs, bare_trait_objects)] 2 | #![warn(clippy::all)] 3 | 4 | //! A framework for Live2D's cubism sdk 5 | pub use cubism_core as core; 6 | 7 | pub mod controller; 8 | pub mod error; 9 | pub mod expression; 10 | pub mod id; 11 | pub mod json; 12 | pub mod model; 13 | pub mod motion; 14 | pub(crate) mod util; 15 | -------------------------------------------------------------------------------- /src/model.rs: -------------------------------------------------------------------------------- 1 | //! A UserModel that represents a functional parsed model3.json. 2 | use std::{fmt, fs, io, ops, path::Path}; 3 | 4 | use cubism_core::Model; 5 | 6 | use crate::controller::{Controller, ControllerMap, ExpressionController, EyeBlink}; 7 | use crate::error::CubismResult; 8 | use crate::expression::Expression; 9 | use crate::json::model::{GroupTarget, Model3}; 10 | 11 | /// A UserModel that represents a functional parsed model3.json. 12 | pub struct UserModel { 13 | model: Model, 14 | // registered controllers 15 | controller_map: ControllerMap, 16 | // saved snapshot of the models parameter for reloading 17 | parameter_snapshot: Box<[f32]>, 18 | } 19 | 20 | impl UserModel { 21 | /// Creates a new UserModel backed by the given Model. 22 | pub fn new(model: Model) -> Self { 23 | let parameter_snapshot = model.parameter_values().into(); 24 | Self { 25 | model, 26 | controller_map: ControllerMap::new(), 27 | parameter_snapshot, 28 | } 29 | } 30 | 31 | /// Creates a UserModel from a path of a model3.json file 32 | #[inline] 33 | pub fn from_model3_json>(path: P) -> CubismResult { 34 | let path = path.as_ref(); 35 | let model3 = Model3::from_reader(fs::File::open(path)?)?; 36 | Self::from_model3(path.parent().unwrap(), &model3) 37 | } 38 | 39 | /// Creates a UserModel from a Model3 and the parent path of the file it was 40 | /// loaded from. 41 | pub fn from_model3(base: &Path, model3: &Model3) -> CubismResult { 42 | if let Some(moc_path) = model3.file_references.moc.as_ref() { 43 | let model = Model::from_bytes(&fs::read(base.join(moc_path))?)?; 44 | let mut this = Self::new(model); 45 | 46 | let mut expr_con = ExpressionController::new(); 47 | for res in model3.file_references.expressions.iter().map(|exp| { 48 | Expression::from_exp3_json(&this.model, base.join(&exp.file)) 49 | .map(|expr| (exp.name.clone(), expr)) 50 | }) { 51 | let (name, expr) = res?; 52 | expr_con.register(name, expr); 53 | } 54 | this.controller_map.register(expr_con); 55 | 56 | if let Some(eye_blink) = Self::try_create_eye_blink(&this.model, model3) { 57 | this.controller_map.register(eye_blink); 58 | } 59 | 60 | Ok(this) 61 | } else { 62 | Err(io::Error::new(io::ErrorKind::NotFound, "no moc file has been specified").into()) 63 | } 64 | } 65 | 66 | fn try_create_eye_blink(model: &Model, model3: &Model3) -> Option { 67 | let eye_blink_ids: Box<[usize]> = model3 68 | .groups 69 | .iter() 70 | .find(|g| g.target == GroupTarget::Parameter && g.name == "EyeBlink")? 71 | .ids 72 | .iter() 73 | .flat_map(|id| model.parameter_ids().iter().position(|id2| *id2 == *id)) 74 | .collect(); 75 | 76 | let mut eb = EyeBlink::default(); 77 | eb.set_ids(eye_blink_ids); 78 | Some(eb) 79 | } 80 | 81 | /// Saves the current parameter values of this model in a hidden snapshot. 82 | pub fn save_parameters(&mut self) { 83 | self.parameter_snapshot 84 | .copy_from_slice(self.model.parameter_values()); 85 | } 86 | 87 | /// Loads the parameters of the hidden snapshot into the current parameter 88 | /// values of this model. 89 | pub fn load_parameters(&mut self) { 90 | self.model 91 | .parameter_values_mut() 92 | .copy_from_slice(&self.parameter_snapshot); 93 | } 94 | 95 | /// Swaps the parameters of this model and the hidden snapshot. 96 | pub fn swap_parameters(&mut self) { 97 | self.parameter_snapshot 98 | .swap_with_slice(self.model.parameter_values_mut()); 99 | } 100 | 101 | /// Applies the expression(if set), runs the controllers in order and 102 | /// updates the model. 103 | pub fn update(&mut self, delta: f32) { 104 | self.load_parameters(); 105 | // do motion update here 106 | self.save_parameters(); 107 | self.controller_map 108 | .update_enabled_controllers(&mut self.model, delta); 109 | self.model.update(); 110 | } 111 | 112 | /// The controller map of this model. 113 | pub fn controllers_map(&self) -> &ControllerMap { 114 | &self.controller_map 115 | } 116 | 117 | /// The controller map of this model. 118 | pub fn controllers_map_mut(&mut self) -> &mut ControllerMap { 119 | &mut self.controller_map 120 | } 121 | 122 | /// Returns a reference to the controller of the type if it exists in this 123 | /// model. 124 | pub fn controller(&self) -> Option<&C> { 125 | self.controller_map.get::() 126 | } 127 | 128 | /// Returns a mutable reference to the controller of the type if it exists 129 | /// in this model. 130 | pub fn controller_mut(&mut self) -> Option<&mut C> { 131 | self.controller_map.get_mut::() 132 | } 133 | 134 | /// The underlying core model. 135 | pub fn model(&self) -> &Model { 136 | &self.model 137 | } 138 | 139 | /// The underlying core model. 140 | pub fn model_mut(&mut self) -> &mut Model { 141 | &mut self.model 142 | } 143 | } 144 | 145 | impl ops::Deref for UserModel { 146 | type Target = Model; 147 | #[inline] 148 | fn deref(&self) -> &Self::Target { 149 | &self.model 150 | } 151 | } 152 | 153 | impl fmt::Debug for UserModel { 154 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 155 | f.debug_struct("UserModel").finish() 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/motion.rs: -------------------------------------------------------------------------------- 1 | //! Motion. 2 | 3 | use std::fs; 4 | use std::ops::{Deref, DerefMut}; 5 | use std::path::Path; 6 | 7 | use crate::core::Model; 8 | use crate::error::CubismResult; 9 | use crate::json::motion::{Motion3, Segment, SegmentPoint}; 10 | 11 | fn lerp_points(p0: SegmentPoint, p1: SegmentPoint, t: f32) -> SegmentPoint { 12 | SegmentPoint { 13 | time: (p1.time - p0.time).mul_add(t, p0.time), 14 | value: (p1.value - p0.value).mul_add(t, p0.value), 15 | } 16 | } 17 | 18 | fn segment_intersects(seg: &Segment, t: f32) -> bool { 19 | match seg { 20 | Segment::Linear(p0, p1) => p0.time <= t && t <= p1.time, 21 | Segment::Bezier([p0, _, _, p1]) => p0.time <= t && t <= p1.time, 22 | Segment::Stepped(p0, t1) => p0.time <= t && t <= *t1, 23 | Segment::InverseStepped(t0, p1) => *t0 <= t && t <= p1.time, 24 | } 25 | } 26 | 27 | fn segment_interpolate(seg: &Segment, t: f32) -> f32 { 28 | match seg { 29 | Segment::Linear(p0, p1) => { 30 | let k = (t - p0.time) / (p1.time - p0.time); 31 | 32 | if k > 0.0 { 33 | (p1.value - p0.value).mul_add(k, p0.value) 34 | } else { 35 | p0.value 36 | } 37 | }, 38 | Segment::Bezier([p0, p1, p2, p3]) => { 39 | let k = (t - p0.time) / (p3.time - p0.time); 40 | let k = if k < 0.0 { 0.0 } else { k }; 41 | 42 | let (p0, p1, p2, p3) = (*p0, *p1, *p2, *p3); 43 | 44 | let p01 = lerp_points(p0, p1, k); 45 | let p12 = lerp_points(p1, p2, k); 46 | let p23 = lerp_points(p2, p3, k); 47 | 48 | let p012 = lerp_points(p01, p12, k); 49 | let p123 = lerp_points(p12, p23, k); 50 | 51 | lerp_points(p012, p123, k).value 52 | }, 53 | Segment::Stepped(p0, _) => p0.value, 54 | Segment::InverseStepped(_, p1) => p1.value, 55 | } 56 | } 57 | 58 | /// Handles motions and animates a model. 59 | #[derive(Clone, Debug)] 60 | pub struct Motion { 61 | json: Motion3, 62 | duration: f32, 63 | fps: f32, 64 | looped: bool, 65 | playing: bool, 66 | current_time: f64, 67 | } 68 | 69 | impl Motion { 70 | /// Creates a Motion from a Motion3. 71 | pub fn new(motion3: Motion3) -> Motion { 72 | let duration = motion3.meta.duration; 73 | let fps = motion3.meta.fps; 74 | let looped = motion3.meta.looped; 75 | 76 | Motion { 77 | json: motion3, 78 | duration, 79 | fps, 80 | looped, 81 | playing: false, 82 | current_time: 0.0, 83 | } 84 | } 85 | /// Set whether the motion loops. 86 | pub fn set_looped(&mut self, looped: bool) { 87 | self.looped = looped; 88 | } 89 | 90 | /// Plays a motion. 91 | pub fn play(&mut self) { 92 | self.playing = true; 93 | } 94 | 95 | /// Pauses a motion. 96 | pub fn pause(&mut self) { 97 | self.playing = false; 98 | } 99 | 100 | /// Stops a motion. 101 | pub fn stop(&mut self) { 102 | self.playing = false; 103 | self.current_time = 0.0; 104 | } 105 | 106 | /// Return if the motion playing. 107 | pub fn is_playing(&self) -> bool { 108 | self.playing 109 | } 110 | 111 | /// Creates a Motion from a path of .motion3.json file. 112 | pub fn from_motion3_json>(path: P) -> CubismResult { 113 | let json = Motion3::from_reader(fs::File::open(path)?)?; 114 | 115 | Ok(Motion::new(json)) 116 | } 117 | 118 | /// Ticks frames. 119 | pub fn tick(&mut self, delta_time: f64) { 120 | use std::f64; 121 | 122 | if !self.playing { 123 | return; 124 | } 125 | 126 | let duration = f64::from(self.duration); 127 | 128 | self.current_time += delta_time; 129 | 130 | if duration <= self.current_time { 131 | if self.looped { 132 | self.current_time -= (self.current_time / duration).floor() * duration; 133 | } else { 134 | self.current_time = duration; 135 | self.playing = false; 136 | } 137 | } 138 | } 139 | 140 | /// Updates a model. 141 | pub fn update(&self, model: &mut Model) -> CubismResult<()> { 142 | let current = self.current_time as f32; 143 | 144 | let mut lip_sync: Option = None; 145 | let mut eye_blink: Option = None; 146 | 147 | for curve in &self.json.curves { 148 | for seg in &curve.segments { 149 | if !segment_intersects(seg, current) { 150 | continue; 151 | } 152 | 153 | let id: &str = &curve.id; 154 | let target: &str = &curve.target; 155 | let value = segment_interpolate(seg, current); 156 | 157 | match target { 158 | "Model" => { 159 | match id { 160 | "EyeBlink" => { 161 | eye_blink = Some(value); 162 | }, 163 | "LipSync" => { 164 | lip_sync = Some(value); 165 | }, 166 | "Opacity" => { 167 | // TODO: 168 | }, 169 | _ => { 170 | eprintln!("Unhandled id: {}", id); 171 | }, 172 | } 173 | }, 174 | "PartOpacity" => { 175 | let param = model.part_mut(id); 176 | if let Some(param) = param { 177 | *param.opacity = value; 178 | } 179 | }, 180 | "Parameter" => { 181 | let param = model.parameter_mut(id); 182 | if let Some(param) = param { 183 | // TODO: fade-in capability 184 | *param.value = value; 185 | 186 | if let Some(_value) = eye_blink { 187 | // TODO: multiply eye_blink to value if the 188 | // parameter corresponds to 189 | // eye blinking 190 | } 191 | 192 | if let Some(_value) = lip_sync { 193 | // TODO: add eye_blink to value if the parameter 194 | // corresponds to 195 | // lip-sync 196 | } 197 | } 198 | }, 199 | _ => { 200 | eprintln!("Unhandled target: {}", target); 201 | }, 202 | } 203 | 204 | break; 205 | } 206 | } 207 | 208 | if eye_blink.is_none() { 209 | // TODO: handle eye blinking when not overwritten 210 | } 211 | 212 | if lip_sync.is_none() { 213 | // TODO: handle lip syncing when not overwritten 214 | } 215 | 216 | // TODO: Better error handling 217 | Ok(()) 218 | } 219 | } 220 | 221 | impl From for Motion { 222 | fn from(motion: Motion3) -> Self { 223 | Self::new(motion) 224 | } 225 | } 226 | 227 | impl Deref for Motion { 228 | type Target = Motion3; 229 | fn deref(&self) -> &Self::Target { 230 | &self.json 231 | } 232 | } 233 | 234 | impl DerefMut for Motion { 235 | fn deref_mut(&mut self) -> &mut Self::Target { 236 | &mut self.json 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/physics.rs: -------------------------------------------------------------------------------- 1 | use crate::core::Model; 2 | use crate::json::physics::Physics3; 3 | 4 | pub struct Physics { 5 | wind: (f32, f32), 6 | gravity: (f32, f32), 7 | rig: PhysicsRig, 8 | } 9 | 10 | impl Physics { 11 | pub fn from_physics3(phys3: Physics3) -> Self { 12 | Physics { 13 | wind: (0.0, 0.0), 14 | gravity: (0.0, -1.0), 15 | rig: PhysicsRig::from_physics3(phys3), 16 | } 17 | } 18 | 19 | pub fn update(&self, model: &Model, delta: f32) {} 20 | } 21 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | /// A simple wrapper around a vec that returns the index of newly 2 | /// pushed/inserted elements and allows holes to exist. 3 | pub struct SimpleSlab { 4 | buf: Vec>, 5 | last_free: usize, 6 | } 7 | 8 | impl SimpleSlab { 9 | pub fn new() -> Self { 10 | SimpleSlab { 11 | buf: Vec::new(), 12 | last_free: 0, 13 | } 14 | } 15 | 16 | pub fn push(&mut self, t: T) -> usize { 17 | let len = self.buf.len(); 18 | if len <= self.last_free { 19 | let ret = len; 20 | self.buf.push(Some(t)); 21 | self.last_free = self.buf.len(); 22 | ret 23 | } else { 24 | let ret = self.last_free; 25 | self.buf[self.last_free].replace(t); 26 | self.last_free = self.buf[self.last_free..] 27 | .iter() 28 | .position(Option::is_none) 29 | .map(|pos| pos + self.last_free) 30 | .unwrap_or(len); 31 | ret 32 | } 33 | } 34 | 35 | pub fn take(&mut self, idx: usize) -> Option { 36 | if idx < self.last_free { 37 | self.last_free = idx; 38 | } 39 | self.buf.get_mut(idx).and_then(Option::take) 40 | } 41 | 42 | pub fn get(&self, idx: usize) -> Option<&T> { 43 | self.buf.get(idx).and_then(Option::as_ref) 44 | } 45 | 46 | pub fn get_mut(&mut self, idx: usize) -> Option<&mut T> { 47 | self.buf.get_mut(idx).and_then(Option::as_mut) 48 | } 49 | 50 | #[inline] 51 | pub fn iter(&self) -> impl Iterator> { 52 | self.buf.iter() 53 | } 54 | 55 | #[inline] 56 | pub fn iter_mut(&mut self) -> impl Iterator> { 57 | self.buf.iter_mut() 58 | } 59 | } 60 | 61 | #[test] 62 | fn simple_slab_push() { 63 | let mut slab = SimpleSlab::new(); 64 | assert_eq!(0, slab.push(100)); 65 | assert_eq!(1, slab.push(101)); 66 | assert_eq!(slab.last_free, 2); 67 | assert_eq!(slab.buf.len(), 2); 68 | } 69 | 70 | #[test] 71 | fn simple_slab_take() { 72 | let mut slab = SimpleSlab::new(); 73 | assert_eq!(0, slab.push(100)); 74 | assert_eq!(1, slab.push(101)); 75 | assert_eq!(Some(100), slab.take(0)); 76 | assert_eq!(slab.last_free, 0); 77 | assert_eq!(slab.buf.len(), 2); 78 | } 79 | 80 | #[test] 81 | fn simple_slab_take_push() { 82 | let mut slab = SimpleSlab::new(); 83 | assert_eq!(0, slab.push(100)); 84 | assert_eq!(1, slab.push(101)); 85 | assert_eq!(2, slab.push(102)); 86 | assert_eq!(Some(101), slab.take(1)); 87 | assert_eq!(slab.last_free, 1); 88 | assert_eq!(slab.buf.len(), 3); 89 | assert_eq!(1, slab.push(104)); 90 | } 91 | --------------------------------------------------------------------------------