├── .editorconfig ├── .github └── workflows │ └── rolling-release.yaml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── frontispiece.gif └── src ├── cli ├── mod.rs └── parse.rs ├── connection.rs ├── convert ├── collada │ ├── make_invertible.rs │ ├── mod.rs │ └── xml.rs ├── gltf │ ├── curve.rs │ ├── gltf.rs │ ├── mod.rs │ ├── object_trs.rs │ └── primitive.rs ├── image_namer.rs └── mod.rs ├── db.rs ├── decompress └── mod.rs ├── errors.rs ├── extract └── mod.rs ├── info └── mod.rs ├── logger.rs ├── main.rs ├── nds ├── decode_texture.rs ├── gpu_cmds.rs ├── mod.rs ├── texture_formats.rs └── texture_params.rs ├── nitro ├── animation.rs ├── container.rs ├── info_block.rs ├── material_animation.rs ├── mod.rs ├── model.rs ├── name.rs ├── pattern.rs ├── render_cmds.rs ├── rotation.rs └── tex.rs ├── primitives └── mod.rs ├── skeleton ├── joint_tree.rs ├── mod.rs ├── symbolic_matrix.rs └── vertex_record.rs ├── util ├── bimap.rs ├── bits.rs ├── bivec.rs ├── cur.rs ├── fields.rs ├── fixed.rs ├── mod.rs ├── namers.rs ├── out_dir.rs ├── tree.rs └── view.rs ├── version.rs └── viewer ├── fps.rs ├── main_loop.rs ├── mod.rs ├── model_viewer ├── eye.rs ├── mod.rs ├── shaders │ ├── frag.glsl │ ├── vert_lit.glsl │ └── vert_unlit.glsl └── texture_cache.rs └── viewer.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 10 | -------------------------------------------------------------------------------- /.github/workflows/rolling-release.yaml: -------------------------------------------------------------------------------- 1 | name: Rolling Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: windows-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Version Info 16 | run: | 17 | rustc -Vv 18 | cargo -Vv 19 | 20 | - name: Build 21 | run: | 22 | $env:APICULA_BUILD_COMMIT_HASH=(git log --pretty=format:'%h' -n 1) 23 | $env:APICULA_BUILD_COMMIT_DATE=(git log --pretty=format:'%cs' -n 1) 24 | cargo b --release 25 | target/release/apicula.exe -V 26 | 27 | - name: Zip 28 | run: | 29 | cd target/release 30 | 7z a ../../apicula-latest-windows.zip apicula.exe 31 | 32 | - name: Tag 33 | run: | 34 | git tag --force continuous ${{ github.sha }} 35 | git push --tags --force 36 | 37 | - name: Release 38 | uses: ncipollo/release-action@v1 39 | with: 40 | allowUpdates: true 41 | artifacts: "apicula-latest-windows.zip" 42 | omitBodyDuringUpdate: true 43 | omitNameDuringUpdate: true 44 | tag: continuous 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "apicula" 3 | version = "0.1.1-dev" 4 | authors = ["scurest "] 5 | license = "0BSD" 6 | edition = "2021" 7 | 8 | [dependencies] 9 | cgmath = "0.16.0" 10 | glium = { git = "https://github.com/scurest/glium.git", branch = "vsync" } 11 | json = "0.12.4" 12 | log = { version = "0.4.6", features = ["std"] } 13 | png = "0.17" 14 | termcolor = "1" 15 | time = "0.1.36" 16 | wild = "2.0.2" 17 | 18 | [profile.release] 19 | panic = "abort" 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2019 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose 4 | with or without fee is hereby granted. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 8 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 10 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 11 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 12 | THIS SOFTWARE. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Ore ga Omae o Mamoru model in Blender

2 |

apicula

3 |

4 | Lines of code 5 | license 0BSD 6 |
7 | Rip models from DS games. 8 |

9 | 10 | ----- 11 | 12 | apicula can rip, view, and convert the [NSBMD model 13 | files](https://github.com/scurest/apicula/wiki/FILETYPES) found in many Nintendo 14 | DS games. 15 | 16 | * [Tutorial](https://github.com/scurest/apicula/wiki/TUTORIAL) 17 | * [Hallow's tutorial on VG Resource](https://www.vg-resource.com/thread-32332.html) 18 | * [Common Blender issues](https://github.com/scurest/apicula/wiki/IMPORT:-Blender) 19 | * [Programmer's documentation on .nsbXX files](https://raw.githubusercontent.com/scurest/nsbmd_docs/master/nsbmd_docs.txt) 20 | 21 | 22 | ### Compatibility 23 | 24 | apicula recognized these file types (called Nitro files). See [the 25 | wiki](https://github.com/scurest/apicula/wiki/FILETYPES) for more info. 26 | 27 | * `.nsbmd`, `.BMD`, or `.BMD0`: 3D models, textures, palettes 28 | * `.nsbtx`, `.BTX`, or `.BTX0`: textures, palettes 29 | * `.nsbca`, `.BCA`, or `.BCA0`: joint animations 30 | * `.nsbtp`, `.BTP`, or `.BTP0`: pattern animations (flipbook-type) 31 | * `.nsbta`, `.BTA`, or `.BTA0`: material animations (experimental!!) 32 | 33 | Models can be converted to COLLADA or glTF. 34 | 35 | Pattern animations are supported in the viewer and extractor, but not in the 36 | converter (neither COLLADA nor glTF support animations that change a material's 37 | textures). 38 | 39 | Material animations are supported in the viewer and extractor, but not in the 40 | converter. 41 | 42 | Importing apicula's COLLADA files has been tested in Blender and Maya. 43 | 44 | 45 | ### Downloads 46 | 47 | * [Download for Windows, 64-bit](https://github.com/scurest/apicula/releases/download/continuous/apicula-latest-windows.zip) 48 | 49 | This is built automatically off the latest `master`. You may need one of the Visual Studio 50 | Redistributable packages installed. 51 | 52 | 53 | ### Building 54 | 55 | Make sure [Rust (1.34+) is installed](https://rustup.rs/) and [build the usual 56 | way](https://doc.rust-lang.org/cargo/guide/working-on-an-existing-project.html) 57 | 58 | $ git clone https://github.com/scurest/apicula.git 59 | $ cd apicula 60 | $ cargo b --release 61 | $ target/release/apicula -V 62 | 63 | 64 | ### Usage 65 | 66 | To search a ROM (or any other file) for Nitro files and extract them 67 | 68 | apicula extract -o 69 | 70 | To view models 71 | 72 | apicula view 73 | 74 | To convert models to COLLADA `.dae` files 75 | 76 | apicula convert -o 77 | 78 | To convert models to glTF `.glb` files 79 | 80 | apicula convert -f=glb -o 81 | 82 | To get technical information about the given Nitro files 83 | 84 | apicula info 85 | 86 | To receive further help 87 | 88 | apicula help 89 | 90 | See also the [tutorial](https://github.com/scurest/apicula/wiki/TUTORIAL) on the 91 | process of extracting .nsbXX files from a ROM, converting them to COLLADA, and 92 | importing them into Blender. 93 | 94 | 95 | ### Special Thanks 96 | 97 | * **kiwi.ds**, for models and documentation for Nitro formats. All NDS model viewers seem to be 98 | derived from this one. Now defunct. 99 | 100 | * **Gericom's [MKDS Course Modifier](https://gbatemp.net/threads/mkds-course-modifier.299444/)**, 101 | for animation information, especially for the meaning of the basis rotations. 102 | 103 | * **Lowlines' [Console 104 | Tool](https://web.archive.org/web/20180319005030/http://llref.emutalk.net/projects/ctool/)**, 105 | for animations and documentation for Nitro formats. I also used Console Tool 106 | for extracting files from ROMs. 107 | 108 | * **Barubary's [DSDecmp](https://github.com/Barubary/dsdecmp)** for NDS 109 | decompression algorithms. 110 | 111 | * **[GBATEK](http://problemkaputt.de/gbatek.htm#ds3dvideo)**, for DS hardware documentation. 112 | 113 | * **[deSmuME](http://desmume.org/)**, for the DS debugger. `_3D_LOG_EXEC` and the GDB stub were 114 | invaluable. 115 | 116 | 117 | ### License 118 | 119 | 0BSD 120 | -------------------------------------------------------------------------------- /frontispiece.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scurest/apicula/3d4e91e14045392a49c89e86dab8cb936225588c/frontispiece.gif -------------------------------------------------------------------------------- /src/cli/mod.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsString; 2 | use std::path::Path; 3 | use std::process::exit; 4 | use wild; 5 | 6 | mod parse; 7 | use self::parse::*; 8 | pub use self::parse::Args; 9 | use crate::version::print_version_info; 10 | 11 | 12 | pub fn parse_cli_args() -> Args { 13 | let args_os: Vec = wild::args_os().collect(); 14 | let mut p = Parse::new(args_os); 15 | 16 | let _exe_name = p.argv.next(); 17 | 18 | let arg = p.argv.next(); 19 | let arg = match arg { 20 | Some(x) => x, 21 | None => show_usage_and_exit(), 22 | }; 23 | let arg = match arg.to_str() { 24 | Some(x) => x, 25 | None => { 26 | error!("don't understand {:?}", arg); 27 | info!("use `apicula help` for help"); 28 | exit(1); 29 | } 30 | }; 31 | match arg { 32 | "x" | "extract" => { 33 | p.args.subcommand = "extract"; 34 | extract(&mut p) 35 | } 36 | "v" | "view" => { 37 | p.args.subcommand = "view"; 38 | view(&mut p); 39 | } 40 | "c" | "convert" => { 41 | p.args.subcommand = "convert"; 42 | convert(&mut p); 43 | } 44 | "i" | "info" => { 45 | p.args.subcommand = "info"; 46 | info(&mut p); 47 | } 48 | "help" => { 49 | p.args.subcommand = "help"; 50 | help(&mut p); 51 | } 52 | "-h" | "--help" => show_usage_and_exit(), 53 | "-V" | "--version" => version(), 54 | _ => { 55 | error!("don't understand {}", arg); 56 | info!("use `apicula help` for help"); 57 | exit(1); 58 | } 59 | } 60 | 61 | p.args 62 | } 63 | 64 | fn show_opts_help(opts: &[&Opt]) { 65 | println!(" Options:"); 66 | for opt in opts { 67 | if !opt.help.is_empty() { 68 | println!(" {}", opt.help); 69 | } 70 | } 71 | } 72 | 73 | static HELP_OPT: Opt = Opt { 74 | short: "h", long: "help", flag: true, 75 | help: "-h, --help show help", 76 | }; 77 | static OUTPUT_OPT: Opt = Opt { 78 | short: "o", long: "output", flag: false, 79 | help: "-o, --output place output files here (will be created)", 80 | }; 81 | static OVERWRITE_OPT: Opt = Opt { 82 | short: "", long: "overwrite", flag: true, 83 | help: "--overwrite allow overwriting files in the output dir", 84 | }; 85 | static ALL_ANIMATIONS_OPT: Opt = Opt { 86 | short: "", long: "all-animations", flag: true, 87 | help: "--all-animations don't guess which joint anims go with a model, just try them all", 88 | }; 89 | static MORE_TEXTURES_OPT: Opt = Opt { 90 | short: "", long: "more-textures", flag: true, 91 | help: "--more-textures try to dump images for unused textures too", 92 | }; 93 | static FORMAT_OPT: Opt = Opt { 94 | short: "f", long: "format", flag: false, 95 | help: "-f, --format output model format (dae, glb, gltf)", 96 | }; 97 | 98 | 99 | fn version() -> ! { 100 | print_version_info(); 101 | exit(0) 102 | } 103 | 104 | fn show_usage_and_exit() -> ! { 105 | print!(concat!( 106 | "\n", 107 | " Usage: apicula ...\n", 108 | "\n", 109 | " Viewer/converter for Nintendo DS Nitro model files (.nsbmd).\n", 110 | "\n", 111 | " Example:\n", 112 | "\n", 113 | " # extract files from a ROM\n", 114 | " apicula extract rom.nds -o nitro-files\n", 115 | " # view all extracted models\n", 116 | " apicula view nitro-files\n", 117 | " # convert model to collada\n", 118 | " apicula convert nitro-files/my-model.nsbmd -o my-model\n", 119 | "\n", 120 | " Commands:\n", 121 | "\n", 122 | " extract Extract Nitro files\n", 123 | " view Nitro model viewer\n", 124 | " convert Convert Nitro models to .dae/.gltf\n", 125 | " info Display debugging info for Nitro files\n", 126 | " help Display help\n", 127 | "\n", 128 | " Run `apicula help COMMAND` for more information on specific commands.\n", 129 | " Visit https://github.com/scurest/apicula/wiki for more info.\n", 130 | "\n", 131 | )); 132 | exit(0); 133 | } 134 | 135 | fn help(p: &mut Parse) -> ! { 136 | let arg = p.argv.next(); 137 | let arg = arg.as_ref().and_then(|arg| arg.to_str()); 138 | match arg { 139 | Some("extract") => show_extract_help_and_exit(), 140 | Some("view") => show_view_help_and_exit(), 141 | Some("convert") => show_convert_help_and_exit(), 142 | Some("info") => show_info_help_and_exit(), 143 | _ => show_usage_and_exit(), 144 | } 145 | } 146 | 147 | 148 | static EXTRACT_OPTS: &[&Opt] = &[&OUTPUT_OPT, &OVERWRITE_OPT, &HELP_OPT]; 149 | 150 | fn extract(p: &mut Parse) { 151 | parse_opts(p, EXTRACT_OPTS); 152 | if p.args.flags.contains(&"help") { show_extract_help_and_exit(); } 153 | if p.args.free_args.len() == 0 { 154 | error!("pass the file you want to extract from"); 155 | exit(1); 156 | } 157 | if p.args.free_args.len() > 1 { 158 | error!("too many input files! I only need one"); 159 | exit(1); 160 | } 161 | check_output_dir(p); 162 | } 163 | 164 | fn show_extract_help_and_exit() -> ! { 165 | print!(concat!( 166 | "\n", 167 | " Usage: apicula extract -o \n", 168 | "\n", 169 | " Extract Nitro files (models, textures, animations, etc.) from .\n", 170 | " Try it on an .nds rom.\n", 171 | "\n", 172 | )); 173 | show_opts_help(EXTRACT_OPTS); 174 | println!(); 175 | exit(0) 176 | } 177 | 178 | 179 | static INFO_OPTS: &[&Opt] = &[&HELP_OPT]; 180 | 181 | fn info(p: &mut Parse) { 182 | parse_opts(p, INFO_OPTS); 183 | if p.args.flags.contains(&"help") { show_info_help_and_exit(); } 184 | check_nitro_input(p); 185 | } 186 | 187 | fn show_info_help_and_exit() -> ! { 188 | print!(concat!( 189 | "\n", 190 | " Usage: apicula info \n", 191 | "\n", 192 | " Display debugging info for the given set of Nitro files.\n", 193 | " You can lookup how textures and palettes were resolved here.\n", 194 | " But this is mostly useful for developers.\n", 195 | "\n", 196 | )); 197 | show_opts_help(INFO_OPTS); 198 | exit(0); 199 | } 200 | 201 | 202 | static VIEW_OPTS: &[&Opt] = &[&ALL_ANIMATIONS_OPT, &HELP_OPT]; 203 | 204 | fn view(p: &mut Parse) { 205 | parse_opts(p, VIEW_OPTS); 206 | if p.args.flags.contains(&"help") { show_view_help_and_exit(); } 207 | check_nitro_input(p); 208 | } 209 | 210 | fn show_view_help_and_exit() -> ! { 211 | print!(concat!( 212 | "\n", 213 | " Usage: apicula view \n", 214 | "\n", 215 | " Open the 3D model viewer.\n", 216 | " Each can be either a Nitro file or a directory of Nitro files.\n", 217 | "\n", 218 | )); 219 | show_opts_help(VIEW_OPTS); 220 | println!(); 221 | exit(0) 222 | } 223 | 224 | 225 | static CONVERT_OPTS: &[&Opt] = &[&OUTPUT_OPT, &FORMAT_OPT, &OVERWRITE_OPT, &MORE_TEXTURES_OPT, &ALL_ANIMATIONS_OPT, &HELP_OPT]; 226 | 227 | fn convert(p: &mut Parse) { 228 | parse_opts(p, CONVERT_OPTS); 229 | if p.args.flags.contains(&"help") { show_convert_help_and_exit(); } 230 | check_nitro_input(p); 231 | check_format(p); 232 | check_output_dir(p); 233 | } 234 | 235 | fn show_convert_help_and_exit() -> ! { 236 | print!(concat!( 237 | "\n", 238 | " Usage: apicula convert ... -o \n", 239 | "\n", 240 | " Converts Nitro models to .dae/.gltf. Default is .dae.\n", 241 | " The textures and animations on each model will be the same as with `apicula view`.\n", 242 | "\n", 243 | )); 244 | show_opts_help(CONVERT_OPTS); 245 | println!(); 246 | exit(0); 247 | } 248 | 249 | 250 | fn check_nitro_input(p: &Parse) { 251 | if p.args.free_args.is_empty() { 252 | error!("give me some input files"); 253 | exit(1); 254 | } 255 | } 256 | 257 | fn check_format(p: &Parse) { 258 | let format = p.args.get_opt("format"); 259 | if let Some(format) = format { 260 | match format.to_str() { 261 | Some("dae") | Some("glb") | Some("gltf") => (), 262 | _ => { 263 | error!("bad output format, should be one of: dae glb gltf"); 264 | exit(1); 265 | } 266 | } 267 | } 268 | } 269 | 270 | fn check_output_dir(p: &Parse) { 271 | let output = p.args.get_opt("output"); 272 | if output.is_none() { 273 | error!("where do I put the output files? Pass it with --output"); 274 | exit(1); 275 | } 276 | let output = output.unwrap(); 277 | if Path::new(output).exists() { 278 | if !p.args.flags.contains(&"overwrite") { 279 | error!("output directory already exists, choose a different one or \ 280 | pass --overwrite if you're okay with files in that dir possibly \ 281 | being overwritten"); 282 | exit(1); 283 | } 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /src/cli/parse.rs: -------------------------------------------------------------------------------- 1 | //! Mini-lib for CLI argument parsing. 2 | //! Handles flags: -x, --x 3 | //! And options with args: -xfoo, -x=foo, -x foo, --x=foo, --x foo 4 | use std; 5 | use std::ffi::{OsString, OsStr}; 6 | 7 | 8 | pub struct Args { 9 | pub subcommand: &'static str, 10 | pub free_args: Vec, 11 | pub opt_args: Vec<(&'static str, OsString)>, 12 | pub flags: Vec<&'static str>, 13 | } 14 | 15 | impl Args { 16 | pub fn get_opt(&self, long: &'static str) -> Option<&OsStr> { 17 | self.opt_args.iter().find(|p| p.0 == long).map(|p| p.1.as_os_str()) 18 | } 19 | } 20 | 21 | pub struct Opt { 22 | pub short: &'static str, 23 | pub long: &'static str, 24 | pub flag: bool, 25 | pub help: &'static str, 26 | } 27 | 28 | pub struct Parse { 29 | pub argv: std::vec::IntoIter, 30 | pub args: Args, 31 | } 32 | 33 | impl Parse { 34 | pub fn new(args_os: Vec) -> Parse { 35 | Parse { 36 | argv: args_os.into_iter(), 37 | args: Args { 38 | subcommand: "", 39 | free_args: vec![], 40 | opt_args: vec![], 41 | flags: vec![], 42 | } 43 | } 44 | } 45 | } 46 | 47 | pub fn parse_opts(p: &mut Parse, opts: &[&Opt]) { 48 | 'argv: while let Some(os_arg) = p.argv.next() { 49 | if let Some(arg) = os_arg.to_str() { 50 | 51 | let mut arg_opt_name; // "x" in -x or --x 52 | let mut arg_param = None; // "param" in -x=param or -xparam 53 | let arg_is_long; // --x vs -x 54 | 55 | if arg.starts_with("--") { 56 | arg_opt_name = &arg[2..]; 57 | arg_is_long = true; 58 | if let Some(i) = arg_opt_name.find('=') { 59 | arg_param = Some(&arg_opt_name[i+1..]); 60 | arg_opt_name = &arg_opt_name[..i]; 61 | } 62 | } else if arg.starts_with("-") { 63 | arg_opt_name = &arg[1..]; 64 | arg_is_long = false; 65 | match arg_opt_name.char_indices().nth(1) { 66 | Some((i, c)) if c == '=' => { // -x=foo 67 | arg_param = Some(&arg_opt_name[i+1..]); 68 | arg_opt_name = &arg_opt_name[..i]; 69 | } 70 | Some((i, _)) => { // -xfoo 71 | if !&arg_opt_name[i..].is_empty() { 72 | arg_param = Some(&arg_opt_name[i..]); 73 | } 74 | arg_opt_name = &arg_opt_name[..i]; 75 | } 76 | None => (), 77 | } 78 | } else { 79 | p.args.free_args.push(os_arg); 80 | continue 'argv; 81 | } 82 | 83 | for opt in opts { 84 | let matches = match arg_is_long { 85 | true => arg_opt_name == opt.long, 86 | false => !opt.short.is_empty() && arg_opt_name == opt.short, 87 | }; 88 | if !matches { continue; } 89 | 90 | if opt.flag { 91 | 92 | if arg_param.is_some() { 93 | error!("flag --{} doesn't take an argument", opt.long); 94 | suggest_help_and_exit(); 95 | } 96 | p.args.flags.push(opt.long); 97 | continue 'argv; 98 | 99 | } else { 100 | 101 | let param: OsString = match arg_param { 102 | Some(s) => s.into(), 103 | None => { 104 | match p.argv.next() { 105 | Some(s) => s, 106 | None => { 107 | error!("expected an argument after --{}", opt.long); 108 | suggest_help_and_exit(); 109 | } 110 | } 111 | } 112 | }; 113 | if p.args.get_opt(opt.long).is_some() { 114 | error!("you already passed --{}", opt.long); 115 | suggest_help_and_exit(); 116 | } 117 | p.args.opt_args.push((opt.long, param)); 118 | continue 'argv; 119 | 120 | } 121 | } 122 | 123 | error!("don't understand option {}{}", 124 | if arg_is_long { "--" } else { "-" }, 125 | arg_opt_name, 126 | ); 127 | suggest_help_and_exit(); 128 | 129 | } else { 130 | p.args.free_args.push(os_arg); 131 | } 132 | } 133 | } 134 | 135 | pub fn suggest_help_and_exit() -> ! { 136 | info!("Pass --help if you need help."); 137 | std::process::exit(1) 138 | } 139 | -------------------------------------------------------------------------------- /src/convert/collada/make_invertible.rs: -------------------------------------------------------------------------------- 1 | use cgmath::Matrix4; 2 | 3 | /// Slightly perturb a matrix's diagonal to create a non-singular matrix. 4 | /// 5 | /// For example, if an object matrix is zero (to eg. hide a part of a model 6 | /// for some portion of an animation) this will make a new object matrix 7 | /// that "hides" it by making it very small. 8 | pub fn make_invertible(m: &Matrix4) -> Matrix4 { 9 | use cgmath::{SquareMatrix, One}; 10 | 11 | if m.is_invertible() { 12 | return m.clone(); 13 | } 14 | 15 | // Try making the matrix invertible by bumping it slightly along the 16 | // diagonal. 17 | for &epsilon in &[0.000001, 0.00001, 0.0001, 0.001f64] { 18 | let m2 = m + Matrix4::from_scale(epsilon); 19 | if m2.is_invertible() { 20 | return m2; 21 | } 22 | } 23 | 24 | // Fuck this, I give up. 25 | warn!("found singular object matrix (COLLADA requires an invertible \ 26 | matrix here); proceeding with the identity. Your model may look wrong."); 27 | debug!("namely, the matrix {:#?}", m); 28 | Matrix4::one() 29 | } 30 | -------------------------------------------------------------------------------- /src/convert/collada/xml.rs: -------------------------------------------------------------------------------- 1 | //! Helpers to make writing XML less unpleasant. 2 | 3 | use cgmath::Matrix4; 4 | use std::fmt::{Display, Write}; 5 | 6 | pub struct Xml { 7 | s: String, 8 | cur_indent: u32 9 | } 10 | 11 | static INDENT_SIZE: u32 = 2; 12 | 13 | impl Xml { 14 | pub fn with_capacity(capacity: usize) -> Xml { 15 | Xml { 16 | s: String::with_capacity(capacity), 17 | cur_indent: 0, 18 | } 19 | } 20 | 21 | pub fn string(self) -> String { 22 | self.s 23 | } 24 | 25 | pub fn start_open_tag(&mut self) { 26 | self.s.push_str("<"); 27 | self.cur_indent += 1; 28 | } 29 | 30 | pub fn start_close_tag(&mut self) { 31 | self.s.push_str(""); 54 | } 55 | 56 | pub fn end_empty_tag(&mut self) { 57 | self.s.push_str("/>"); 58 | self.cur_indent -= 1; 59 | } 60 | 61 | pub fn push_str(&mut self, s: &str) { 62 | self.s.push_str(s); 63 | } 64 | 65 | pub fn push_text(&mut self, x: &T) { 66 | write!(&mut self.s, "{}", x).unwrap(); 67 | } 68 | 69 | pub fn matrix(&mut self, m: &Matrix4) { 70 | let m: &[f64; 16] = m.as_ref(); 71 | // COLLADA wants row-major order 72 | write!(&mut self.s, "{} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {}", 73 | m[0], m[4], m[8], m[12], 74 | m[1], m[5], m[9], m[13], 75 | m[2], m[6], m[10], m[14], 76 | m[3], m[7], m[11], m[15], 77 | ).unwrap(); 78 | } 79 | } 80 | 81 | macro_rules! xml_attr { 82 | ($x:ident; ($e:expr) $($rest:tt)*) => { 83 | $x.push_text(&$e); 84 | xml_attr!($x; $($rest)*); 85 | }; 86 | ($x:ident; $strlit:tt $($rest:tt)*) => { 87 | $x.push_str($strlit); 88 | xml_attr!($x; $($rest)*); 89 | }; 90 | ($x:ident;) => {}; 91 | } 92 | 93 | macro_rules! xml { 94 | ($x:ident; { 95 | $x.start_close_tag(); 96 | xml!($x; $($rest)*) 97 | }; 98 | ($x:ident; < $($rest:tt)*) => { 99 | $x.start_open_tag(); 100 | xml!($x; $($rest)*) 101 | }; 102 | ($x:ident; /> $($rest:tt)*) => { 103 | $x.end_empty_tag(); 104 | xml!($x; $($rest)*) 105 | }; 106 | ($x:ident; > $($rest:tt)*) => { 107 | $x.end_tag(); 108 | xml!($x; $($rest)*) 109 | }; 110 | ($x:ident; ; $($rest:tt)*) => { 111 | $x.nl(); 112 | xml!($x; $($rest)*) 113 | }; 114 | ($x:ident; / $($rest:tt)*) => { 115 | $x.deindent_and_start_close_tag(); 116 | xml!($x; $($rest)*) 117 | }; 118 | ($x:ident; $attr:ident = [ $($val:tt)* ] $($rest:tt)*) => { 119 | $x.push_str(" "); 120 | $x.push_str(stringify!($attr)); 121 | $x.push_str("=\""); 122 | xml_attr!($x; $($val)*); 123 | $x.push_str("\""); 124 | xml!($x; $($rest)*) 125 | }; 126 | ($x:ident; if ($cond:expr) { $($then:tt)* } else { $($els:tt)* } $($rest:tt)*) => { 127 | if $cond { 128 | xml!($x; $($then)*); 129 | } else { 130 | xml!($x; $($els)*); 131 | } 132 | xml!($x; $($rest)*); 133 | }; 134 | ($x:ident; if ($cond:expr) { $($then:tt)* } $($rest:tt)*) => { 135 | if $cond { 136 | xml!($x; $($then)*); 137 | } 138 | xml!($x; $($rest)*); 139 | }; 140 | ($x:ident; for $p:pat in ($it:expr) { $($body:tt)* } $($rest:tt)*) => { 141 | for $p in $it { 142 | xml!($x; $($body)*); 143 | } 144 | xml!($x; $($rest)*); 145 | }; 146 | ($x:ident; MATRIX($e:expr) $($rest:tt)*) => { 147 | $x.matrix($e); 148 | xml!($x; $($rest)*); 149 | }; 150 | ($x:ident; $word:ident $($rest:tt)*) => { 151 | $x.push_str(stringify!($word)); 152 | xml!($x; $($rest)*) 153 | }; 154 | ($x:ident; ($e:expr) $($rest:tt)*) => { 155 | $x.push_text(&$e); 156 | xml!($x; $($rest)*) 157 | }; 158 | ($x:ident; $strlit:tt $($rest:tt)*) => { 159 | $x.push_str(&$strlit); 160 | xml!($x; $($rest)*) 161 | }; 162 | ($x:ident;) => {}; 163 | } 164 | -------------------------------------------------------------------------------- /src/convert/gltf/curve.rs: -------------------------------------------------------------------------------- 1 | /// Converts a set of Nitro TRS curves into a form suitable for glTF. 2 | /// 3 | /// Nitro files contain one curve for each translation/scale component, while 4 | /// glTF contains one curve for the whole 3-vector, so we need to resample the 5 | /// three component curve onto their common domain and join them together. And 6 | /// we need to convert the curve of rotation matrices to a curve of quaternions. 7 | 8 | use crate::nitro::animation::{TRSCurves, Curve}; 9 | use cgmath::{Vector3, Matrix3, Quaternion, InnerSpace, vec3}; 10 | 11 | /// Represents the domain of a Curve. 12 | /// 13 | /// NOTE: we interpret the domain of a constant curve as being a curve with a 14 | /// single sample at 0. IOW we basically forget about constant curves in this 15 | /// module. 16 | #[derive(Copy, Clone)] 17 | pub enum CurveDomain { 18 | None, 19 | Sampled { 20 | start_frame: u16, 21 | end_frame: u16, 22 | sampling_rate: u16, 23 | } 24 | } 25 | 26 | impl Curve { 27 | pub fn domain(&self) -> CurveDomain { 28 | match *self { 29 | Curve::None => CurveDomain::None, 30 | Curve::Constant(_) => CurveDomain::Sampled { 31 | start_frame: 0, 32 | end_frame: 1, 33 | sampling_rate: 1, 34 | }, 35 | Curve::Samples { 36 | start_frame, 37 | end_frame, 38 | ref values 39 | } => CurveDomain::Sampled { 40 | start_frame, 41 | end_frame, 42 | sampling_rate: (end_frame - start_frame) / values.len() as u16, 43 | } 44 | } 45 | } 46 | } 47 | 48 | impl CurveDomain { 49 | pub fn union(self, other: CurveDomain) -> CurveDomain { 50 | match (self, other) { 51 | (CurveDomain::None, d) => d, 52 | (d, CurveDomain::None) => d, 53 | ( 54 | CurveDomain::Sampled { start_frame: s1, end_frame: e1, sampling_rate: r1 }, 55 | CurveDomain::Sampled { start_frame: s2, end_frame: e2, sampling_rate: r2 }, 56 | ) => CurveDomain::Sampled { 57 | start_frame: s1.min(s2), 58 | end_frame: e1.max(e2), 59 | sampling_rate: r1.min(r2), 60 | } 61 | } 62 | } 63 | } 64 | 65 | pub struct GlTFObjectCurves { 66 | pub translation: Curve>, 67 | pub rotation: Curve>, 68 | pub scale: Curve>, 69 | } 70 | 71 | impl GlTFObjectCurves { 72 | pub fn for_trs_curves(trs_curves: &TRSCurves) -> GlTFObjectCurves { 73 | let translation = resample_vec3(&trs_curves.trans, 0.0); 74 | let rotation = rotation_curve(&trs_curves.rotation); 75 | let scale = resample_vec3(&trs_curves.scale, 1.0); 76 | 77 | GlTFObjectCurves { translation, rotation, scale } 78 | } 79 | } 80 | 81 | /// Turns an array of three curves of reals into one curve of 3-vectors by 82 | /// re-sampling the curves onto their common domain. 83 | fn resample_vec3(curves: &[Curve; 3], default_value: f64) -> Curve> { 84 | let resampled_domain = 85 | curves[0].domain() 86 | .union(curves[1].domain()) 87 | .union(curves[2].domain()); 88 | 89 | match resampled_domain { 90 | CurveDomain::None => Curve::None, 91 | CurveDomain::Sampled { start_frame, end_frame, sampling_rate } => { 92 | let num_samples = (end_frame - start_frame) / sampling_rate; 93 | let mut values = Vec::with_capacity(num_samples as usize); 94 | 95 | let mut frame = start_frame; 96 | while frame < end_frame { 97 | let x = curves[0].sample_at(default_value, frame); 98 | let y = curves[1].sample_at(default_value, frame); 99 | let z = curves[2].sample_at(default_value, frame); 100 | values.push(vec3(x, y, z)); 101 | 102 | frame += sampling_rate; 103 | } 104 | 105 | Curve::Samples { start_frame, end_frame, values } 106 | } 107 | } 108 | } 109 | 110 | /// Turns a curve of rotation matrices into a curve of quaternions. 111 | fn rotation_curve(matrix_curve: &Curve>) -> Curve> { 112 | fn to_quat(m: Matrix3) -> Quaternion { 113 | Quaternion::from(m).normalize() 114 | } 115 | 116 | match *matrix_curve { 117 | Curve::None => Curve::None, 118 | Curve::Constant(m) => Curve::Samples { 119 | start_frame: 0, 120 | end_frame: 1, 121 | values: vec![to_quat(m)], 122 | }, 123 | Curve::Samples { start_frame, end_frame, ref values } => { 124 | let mut quats = values.iter() 125 | .map(|&m| to_quat(m)) 126 | .collect::>>(); 127 | 128 | // Ensure the quaternions go on the shortest path through the 129 | // hypersphere 130 | for i in 1..quats.len() { 131 | if quats[i].dot(quats[i-1]) < 0.0 { 132 | quats[i] = -quats[i]; 133 | } 134 | } 135 | 136 | Curve::Samples { start_frame, end_frame, values: quats } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/convert/gltf/gltf.rs: -------------------------------------------------------------------------------- 1 | use json::JsonValue; 2 | use std::io::Write; 3 | 4 | pub struct GlTF { 5 | pub buffers: Vec, 6 | pub json: JsonValue, 7 | } 8 | 9 | pub struct Buffer { 10 | pub bytes: Vec, 11 | pub alignment: usize, 12 | } 13 | 14 | impl GlTF { 15 | pub fn new() -> GlTF { 16 | GlTF { 17 | buffers: vec![], 18 | json: object!( 19 | "asset" => object!( 20 | "version" => "2.0", 21 | ), 22 | "bufferViews" => array!(), 23 | "accessors" => array!(), 24 | "extensionsUsed" => array!(), 25 | "extensionsRequired" => array!(), 26 | ) 27 | } 28 | } 29 | 30 | /// Remove empty top-level collections from the glTF JSON. 31 | pub fn cleanup(&mut self) { 32 | for &key in &["accessors", "bufferViews", "extensionsUsed", "extensionsRequired"] { 33 | if self.json[key].is_empty() { 34 | self.json.remove(key); 35 | } 36 | } 37 | } 38 | 39 | /// Updates all the buffer views to point to the correct location when the 40 | /// buffers are all joined into one single json["buffers"][0]. Returns the 41 | /// size the joined buffer will have. 42 | pub fn update_buffer_views(&mut self) -> usize { 43 | // Record the offset to the start of each buffer when they are all laid 44 | // out end-to-end. 45 | let mut buf_offsets = Vec::with_capacity(self.buffers.len()); 46 | let buf_byte_len = { 47 | let mut l = 0_usize; 48 | for buffer in &self.buffers { 49 | // Pad to alignment 50 | if l % buffer.alignment != 0 { 51 | l += buffer.alignment - (l % buffer.alignment); 52 | } 53 | 54 | buf_offsets.push(l); 55 | 56 | l += buffer.bytes.len(); 57 | } 58 | // Pad buffer to 4-byte alignment (required for GLBs) 59 | if l % 4 != 0 { 60 | l += 4 - (l % 4); 61 | } 62 | l 63 | }; 64 | 65 | // Add the glTF buffer 66 | self.json["buffers"] = array!( 67 | object!( 68 | "byteLength" => buf_byte_len, 69 | ) 70 | ); 71 | 72 | // Fixup bufferView pointers 73 | for buf_view in self.json["bufferViews"].members_mut() { 74 | let old_buf_idx = buf_view["buffer"].as_usize().unwrap(); 75 | buf_view["buffer"] = 0.into(); 76 | let offset = if buf_view.has_key("byteOffset") { 77 | buf_view["byteOffset"].as_usize().unwrap() 78 | } else { 79 | 0 80 | }; 81 | let new_offset = offset + buf_offsets[old_buf_idx]; 82 | buf_view["byteOffset"] = new_offset.into(); 83 | } 84 | 85 | buf_byte_len 86 | } 87 | 88 | /// Write the joined buffer to w. 89 | pub fn write_buffer(&mut self, w: &mut W) -> std::io::Result<()> { 90 | let mut scratch = Vec::::with_capacity(4); 91 | 92 | let mut l = 0; 93 | for buffer in &self.buffers { 94 | // Pad to alignment 95 | if l % buffer.alignment != 0 { 96 | scratch.resize(buffer.alignment - (l % buffer.alignment), 0); 97 | w.write_all(&scratch)?; 98 | l += buffer.alignment - (l % buffer.alignment); 99 | } 100 | 101 | w.write_all(&buffer.bytes)?; 102 | l += buffer.bytes.len(); 103 | } 104 | if l % 4 != 0 { 105 | scratch.resize(4 - (l % 4), 0); 106 | w.write_all(&scratch)?; 107 | } 108 | Ok(()) 109 | } 110 | 111 | pub fn write_glb(mut self, w: &mut W) -> std::io::Result<()> { 112 | let bin_len = self.update_buffer_views(); 113 | 114 | // JSON -> String 115 | let mut s = self.json.dump(); 116 | while s.len() % 4 != 0 { 117 | s.push(' '); 118 | } 119 | 120 | // Calculate total filesize 121 | let filesize = 122 | 12 + // GLB Header 123 | 8 + // JSON Chunk Header 124 | s.len() + // JSON Chunk Data 125 | 8 + // BIN Chunk Header 126 | bin_len; // BIN Chunk Data 127 | 128 | // Scratch buffer 129 | let mut scratch = Vec::::with_capacity(24); 130 | 131 | // GLB Header 132 | scratch.extend_from_slice(b"glTF"); 133 | scratch.push_u32(2); 134 | scratch.push_u32(filesize as u32); 135 | // JSON Chunk Header 136 | scratch.push_u32(s.len() as u32); 137 | scratch.extend_from_slice(b"JSON"); 138 | w.write_all(&scratch)?; 139 | // JSON Chunk Data 140 | w.write_all(s.as_bytes())?; 141 | // BIN Chunk Header 142 | scratch.clear(); 143 | scratch.push_u32(bin_len as u32); 144 | scratch.extend_from_slice(b"BIN\0"); 145 | w.write_all(&scratch)?; 146 | // Write all the buffer into the BIN data 147 | self.write_buffer(w)?; 148 | 149 | Ok(()) 150 | } 151 | 152 | pub fn write_gltf_bin( 153 | mut self, 154 | gltf_w: &mut W1, 155 | bin_w: &mut W2, 156 | buffer_uri: &str, 157 | ) -> std::io::Result<()> { 158 | self.update_buffer_views(); 159 | self.json["buffers"][0]["uri"] = buffer_uri.into(); 160 | self.json.write_pretty(gltf_w, 2)?; 161 | self.write_buffer(bin_w)?; 162 | Ok(()) 163 | } 164 | } 165 | 166 | 167 | pub trait ByteVec { 168 | fn push_u16(&mut self, x: u16); 169 | fn push_u32(&mut self, x: u32); 170 | fn push_f32(&mut self, x: f32); 171 | fn push_normalized_u8(&mut self, x: f32); 172 | } 173 | 174 | impl ByteVec for Vec { 175 | fn push_u16(&mut self, x: u16) { 176 | self.extend_from_slice(&x.to_le_bytes()) 177 | } 178 | fn push_u32(&mut self, x: u32) { 179 | self.extend_from_slice(&x.to_le_bytes()) 180 | } 181 | fn push_f32(&mut self, x: f32) { 182 | self.push_u32(x.to_bits()) 183 | } 184 | fn push_normalized_u8(&mut self, x: f32) { 185 | self.push((x * 255.0).round() as u8); 186 | } 187 | } 188 | 189 | pub trait VecExt { 190 | /// Pushes an element to a list and returns its index. 191 | fn add(&mut self, x: T) -> usize; 192 | } 193 | 194 | impl VecExt for Vec { 195 | fn add(&mut self, x: T) -> usize { 196 | self.push(x); 197 | self.len() - 1 198 | } 199 | } 200 | 201 | impl VecExt for JsonValue { 202 | fn add(&mut self, x: JsonValue) -> usize { 203 | self.push(x).unwrap(); 204 | self.len() - 1 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/convert/gltf/object_trs.rs: -------------------------------------------------------------------------------- 1 | use crate::nitro::Model; 2 | use cgmath::{Vector3, Quaternion, InnerSpace, One, vec3, Matrix4}; 3 | 4 | pub struct TRS { 5 | pub translation: Option>, 6 | pub rotation_quaternion: Option>, 7 | pub scale: Option>, 8 | } 9 | 10 | pub struct ObjectTRSes { 11 | pub objects: Vec, 12 | } 13 | 14 | impl ObjectTRSes { 15 | pub fn for_model_at_rest(model: &Model) -> ObjectTRSes { 16 | let objects = model.objects.iter().map(|obj| { 17 | let translation = obj.trans.clone(); 18 | 19 | let rotation_quaternion = obj.rot.map(|rotation_matrix| { 20 | Quaternion::from(rotation_matrix) 21 | .normalize() 22 | }); 23 | 24 | let scale = obj.scale.map(|scale| { 25 | // Bump scalings that are too close to zero. 26 | let adjust_scale_factor = |s| { 27 | // The smallest number on the DS was 2^{-12} (~0.0002). 28 | static SMALL: f64 = 0.000_002; 29 | if s >= 0.0 && s < SMALL { 30 | SMALL 31 | } else if s <= 0.0 && s > -SMALL { 32 | -SMALL 33 | } else { 34 | s 35 | } 36 | }; 37 | 38 | vec3( 39 | adjust_scale_factor(scale.x), 40 | adjust_scale_factor(scale.y), 41 | adjust_scale_factor(scale.z), 42 | ) 43 | }); 44 | 45 | TRS { translation, rotation_quaternion, scale } 46 | }).collect::>(); 47 | 48 | ObjectTRSes { objects } 49 | } 50 | } 51 | 52 | impl<'a> std::convert::From<&'a TRS> for Matrix4 { 53 | fn from(trs: &'a TRS) -> Matrix4 { 54 | let mut m: Matrix4; 55 | if let Some(s) = trs.scale { 56 | m = Matrix4::from_nonuniform_scale(s.x, s.y, s.z); 57 | } else { 58 | m = Matrix4::one(); 59 | } 60 | if let Some(r) = trs.rotation_quaternion { 61 | m = Matrix4::from(r) * m; 62 | } 63 | if let Some(t) = trs.translation { 64 | m = Matrix4::from_translation(t) * m; 65 | } 66 | m 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/convert/gltf/primitive.rs: -------------------------------------------------------------------------------- 1 | use crate::primitives::{Primitives, PolyType}; 2 | 3 | /// Triangulates the quads in a Primitive in the correct way for 4 | /// FB_ngon_encoding to reconstruct them. 5 | pub fn encode_ngons(mut prims: Primitives) -> Primitives { 6 | assert!(prims.poly_type == PolyType::TrisAndQuads); 7 | 8 | let mut tris = Vec::::with_capacity(prims.indices.len()); 9 | for call in prims.draw_calls.iter_mut() { 10 | let mut tris_range = tris.len()..tris.len(); 11 | 12 | // Pick an index in each face and emit a triangulation of the face with 13 | // that index as the first index of each tri. Don't pick the same index 14 | // for two subsequent faces. 15 | // TODO: handle degenerate faces? Probably low priority... 16 | let mut last_face_index: u16 = 0xffff; 17 | for face in prims.indices[call.index_range.clone()].chunks_exact(4) { 18 | tris.reserve(6); 19 | if last_face_index != face[0] { 20 | last_face_index = face[0]; 21 | 22 | tris.push(face[0]); 23 | tris.push(face[1]); 24 | tris.push(face[2]); 25 | if face[3] != 0xffff { 26 | tris.push(face[0]); 27 | tris.push(face[2]); 28 | tris.push(face[3]); 29 | } 30 | } else { 31 | last_face_index = face[2]; 32 | 33 | tris.push(face[2]); 34 | tris.push(face[0]); 35 | tris.push(face[1]); 36 | if face[3] != 0xffff { 37 | tris.push(face[2]); 38 | tris.push(face[3]); 39 | tris.push(face[0]); 40 | } 41 | } 42 | } 43 | 44 | tris_range.end = tris.len(); 45 | 46 | call.index_range = tris_range; 47 | } 48 | 49 | prims.indices = tris; 50 | prims.poly_type = PolyType::Tris; 51 | 52 | prims 53 | } 54 | -------------------------------------------------------------------------------- /src/convert/image_namer.rs: -------------------------------------------------------------------------------- 1 | //! Discovers images in a Connection and assigns them names. We use these for 2 | //! image filenames so that models know what the path to a specific image it 3 | //! uses will be. 4 | use crate::db::{Database, TextureId, PaletteId}; 5 | use crate::nitro::Name; 6 | use std::collections::HashMap; 7 | use crate::util::namers::UniqueNamer; 8 | use crate::connection::Connection; 9 | 10 | type ImageId = (TextureId, Option); 11 | 12 | pub struct ImageNamer { 13 | pub namer: UniqueNamer, 14 | pub names: HashMap, 15 | pub used_texture_ids: Vec, // TODO: BitVec 16 | } 17 | 18 | impl ImageNamer { 19 | pub fn build(db: &Database, conn: &Connection) -> ImageNamer { 20 | let mut image_namer = ImageNamer { 21 | namer: UniqueNamer::new(), 22 | names: HashMap::new(), 23 | used_texture_ids: vec![false; db.textures.len()], 24 | }; 25 | 26 | // Discovery images from model materials 27 | for mdl_conn in &conn.models { 28 | for mat_conn in &mdl_conn.materials { 29 | match mat_conn.image_id() { 30 | Ok(Some(image_id)) => 31 | image_namer.insert_image_id(db, image_id), 32 | _ => continue, 33 | } 34 | } 35 | } 36 | 37 | // Discover images from pattern animations 38 | for mdl_conn in &conn.models { 39 | for pat_conn in &mdl_conn.patterns { 40 | let pat = &db.patterns[pat_conn.pattern_id]; 41 | for track in &pat.material_tracks { 42 | for keyframe in &track.keyframes { 43 | let tex_idx = keyframe.texture_idx as usize; 44 | let texture_id = match pat_conn.texture_ids[tex_idx] { 45 | Some(id) => id, 46 | None => continue, 47 | }; 48 | let pal_idx = keyframe.palette_idx as usize; 49 | let palette_id = match pat_conn.palette_ids[pal_idx] { 50 | Some(id) => id, 51 | None => continue, 52 | }; 53 | let image_id = (texture_id, Some(palette_id)); 54 | image_namer.insert_image_id(db, image_id); 55 | } 56 | } 57 | } 58 | } 59 | 60 | image_namer 61 | } 62 | 63 | pub fn insert_image_id(&mut self, db: &Database, image_id: ImageId) { 64 | let texture_name = db.textures[image_id.0].name; 65 | let namer = &mut self.namer; 66 | self.names.entry(image_id).or_insert_with(|| { 67 | namer.get_fresh_name(texture_name.print_safe().to_string()) 68 | }); 69 | self.used_texture_ids[image_id.0] = true; 70 | } 71 | 72 | /// Discover even more images by guessing, based on their names, which 73 | /// palettes go with which textures. 74 | pub fn add_more_images(&mut self, db: &Database) { 75 | let mut num_guesses = 0; 76 | let mut still_unextracted = false; 77 | 78 | for (texture_id, texture) in db.textures.iter().enumerate() { 79 | if self.used_texture_ids[texture_id] { 80 | continue; 81 | } 82 | 83 | // Direct color textures don't need a palette. 84 | if !texture.params.format().desc().requires_palette { 85 | self.insert_image_id(db, (texture_id, None)); 86 | num_guesses += 1; 87 | continue; 88 | } 89 | 90 | // If there's one palette, guess it 91 | if db.palettes.len() == 1 { 92 | self.insert_image_id(db, (texture_id, Some(0))); 93 | num_guesses += 1; 94 | continue; 95 | } 96 | 97 | // Guess palette name == texture name 98 | if let Some(ids) = db.palettes_by_name.get(&texture.name) { 99 | self.insert_image_id(db, (texture_id, Some(ids[0]))); 100 | num_guesses += 1; 101 | continue; 102 | } 103 | 104 | // Guess palette name == texture_name + "_pl" 105 | if let Some(ids) = db.palettes_by_name.get(&append_pl(&texture.name)) { 106 | self.insert_image_id(db, (texture_id, Some(ids[0]))); 107 | num_guesses += 1; 108 | continue; 109 | } 110 | 111 | still_unextracted = true; 112 | } 113 | 114 | info!("Guessed {} new images (for --more-images)", num_guesses); 115 | if still_unextracted { 116 | info!("There are still unextracted textures though"); 117 | } 118 | } 119 | } 120 | 121 | /// Append "_pl" to the end of a name. 122 | fn append_pl(name: &Name) -> Name { 123 | let mut res = name.clone(); 124 | 125 | // Find the index of the first NUL byte in the suffix of NUL bytes. 126 | let mut idx = res.0.iter().rposition(|&x| x != b'\0') 127 | .map(|pos| pos + 1) 128 | .unwrap_or(0); 129 | 130 | // Append as much of b"_pl" as will fit. 131 | for &b in b"_pl" { 132 | if idx == res.0.len() { break; } 133 | res.0[idx] = b; 134 | idx += 1; 135 | } 136 | 137 | res 138 | } 139 | -------------------------------------------------------------------------------- /src/convert/mod.rs: -------------------------------------------------------------------------------- 1 | mod collada; 2 | mod image_namer; 3 | mod gltf; 4 | 5 | use crate::cli::Args; 6 | use crate::errors::Result; 7 | use std::fs::File; 8 | use std::io::Write; 9 | use std::path::PathBuf; 10 | use crate::util::namers::UniqueNamer; 11 | use crate::util::OutDir; 12 | use crate::db::Database; 13 | use crate::convert::image_namer::ImageNamer; 14 | use crate::connection::{Connection, ConnectionOptions}; 15 | 16 | pub fn main(args: &Args) -> Result<()> { 17 | let out_dir_path = PathBuf::from(args.get_opt("output").unwrap()); 18 | let mut out_dir = OutDir::new(out_dir_path)?; 19 | 20 | let db = Database::from_cli_args(args)?; 21 | 22 | db.print_status(); 23 | 24 | let conn_options = ConnectionOptions::from_cli_args(args); 25 | let conn = Connection::build(&db, conn_options); 26 | 27 | let format = args.get_opt("format").map(|s| s.to_str().unwrap()) 28 | .unwrap_or("dae"); 29 | 30 | let mut image_namer = ImageNamer::build(&db, &conn); 31 | if args.flags.contains(&"more-textures") { 32 | image_namer.add_more_images(&db); 33 | } 34 | 35 | let mut models_written = 0; 36 | let mut pngs_written = 0; 37 | 38 | // Gives unique names to each model file to avoid name clashes. 39 | let mut model_file_namer = UniqueNamer::new(); 40 | 41 | for (model_id, model) in db.models.iter().enumerate() { 42 | debug!("Converting model {} ({})...", model.name, model_id); 43 | 44 | let name = model_file_namer.get_fresh_name(format!("{}", model.name.print_safe())); 45 | let mut f = out_dir.create_file(&format!("{}.{}", name, format))?; 46 | 47 | let res = if format == "dae" { 48 | let s = collada::write(&db, &conn, &image_namer, model_id); 49 | f.write_all(s.as_bytes()).and_then(|_| f.flush()) 50 | } else if format == "glb" || format == "gltf" { 51 | let gltf = gltf::to_gltf(&db, &conn, &image_namer, model_id); 52 | if format == "glb" { 53 | gltf.write_glb(&mut f) 54 | } else { 55 | let bin_file_name = format!("{}.bin", name); 56 | let mut bin_f = out_dir.create_file(&bin_file_name)?; 57 | gltf.write_gltf_bin(&mut f, &mut bin_f, &bin_file_name) 58 | } 59 | } else { 60 | unreachable!() 61 | }; 62 | 63 | match res { 64 | Ok(()) => { models_written += 1; }, 65 | Err(e) => error!("failed to write {}: {}", name, e), 66 | } 67 | } 68 | 69 | // Save PNGs for all the images 70 | for ((texture_id, palette_id), image_name) in image_namer.names.drain() { 71 | let texture = &db.textures[texture_id]; 72 | let palette = palette_id.map(|id| &db.palettes[id]); 73 | 74 | use crate::nds::decode_texture; 75 | let rgba = match decode_texture(texture, palette) { 76 | Ok(rgba) => rgba, 77 | Err(e) => { 78 | error!("error generating image {}, error: {}", image_name, e); 79 | continue; 80 | } 81 | }; 82 | 83 | let dim = (texture.params.width(), texture.params.height()); 84 | let mut png_file = out_dir.create_file(&format!("{}.png", image_name))?; 85 | match write_rgba(&mut png_file, &rgba.0[..], dim) { 86 | Ok(()) => { pngs_written += 1; } 87 | Err(e) => error!("failed writing PNG: {}", e), 88 | } 89 | } 90 | 91 | // Print results 92 | let plural = |x| if x != 1 { "s" } else { "" }; 93 | let model_file_name = match format { 94 | "dae" => "DAE", 95 | "glb" => "GLB", 96 | "gltf" => "glTF", 97 | _ => unreachable!(), 98 | }; 99 | println!("Wrote {} {}{}, {} PNG{}.", 100 | models_written, model_file_name, plural(models_written), 101 | pngs_written, plural(pngs_written)); 102 | 103 | Ok(()) 104 | } 105 | 106 | pub fn write_rgba(f: &mut File, rgba: &[u8], dim: (u32, u32)) -> Result<()> { 107 | use png::{Encoder, ColorType, BitDepth}; 108 | 109 | let mut encoder = Encoder::new(f, dim.0, dim.1); 110 | encoder.set_color(ColorType::Rgba); 111 | encoder.set_depth(BitDepth::Eight); 112 | 113 | let mut writer = encoder.write_header()?; 114 | writer.write_image_data(rgba)?; 115 | 116 | Ok(()) 117 | } 118 | -------------------------------------------------------------------------------- /src/db.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::Args; 2 | use std::path::PathBuf; 3 | use std::fs; 4 | use std::collections::HashMap; 5 | use crate::nitro::{ 6 | Name, Model, Texture, Palette, Animation, Pattern, 7 | MaterialAnimation, Container 8 | }; 9 | use crate::errors::Result; 10 | use crate::util::cur::Cur; 11 | 12 | pub type FileId = usize; 13 | pub type ModelId = usize; 14 | pub type TextureId = usize; 15 | pub type PaletteId = usize; 16 | pub type AnimationId = usize; 17 | pub type PatternId = usize; 18 | pub type MatAnimId = usize; 19 | 20 | #[derive(Default)] 21 | pub struct Database { 22 | pub file_paths: Vec, 23 | 24 | pub models: Vec, 25 | pub textures: Vec, 26 | pub palettes: Vec, 27 | pub animations: Vec, 28 | pub patterns: Vec, 29 | pub mat_anims: Vec, 30 | 31 | pub models_found_in: Vec, 32 | pub textures_found_in: Vec, 33 | pub palettes_found_in: Vec, 34 | pub animations_found_in: Vec, 35 | pub patterns_found_in: Vec, 36 | pub mat_anims_found_in: Vec, 37 | 38 | pub textures_by_name: HashMap>, 39 | pub palettes_by_name: HashMap>, 40 | } 41 | 42 | impl Database { 43 | pub fn from_cli_args(args: &Args) -> Result { 44 | let user_paths = 45 | args.free_args.iter() 46 | .map(PathBuf::from); 47 | let file_paths = expand_directories(user_paths); 48 | 49 | let mut db: Database = Default::default(); 50 | db.build(file_paths)?; 51 | Ok(db) 52 | } 53 | 54 | pub fn print_status(&self) { 55 | let num_models = self.models.len(); 56 | let num_textures = self.textures.len(); 57 | let num_palettes = self.palettes.len(); 58 | let num_animations = self.animations.len(); 59 | let num_patterns = self.patterns.len(); 60 | let num_mat_anims = self.mat_anims.len(); 61 | 62 | let plural = |x| if x != 1 { "s" } else { "" }; 63 | println!( 64 | "Got {} model{}, {} texture{}, {} palette{}, {} animation{}, {} pattern animation{}, {} material animation{}.", 65 | num_models, plural(num_models), 66 | num_textures, plural(num_textures), 67 | num_palettes, plural(num_palettes), 68 | num_animations, plural(num_animations), 69 | num_patterns, plural(num_patterns), 70 | num_mat_anims, plural(num_mat_anims), 71 | ); 72 | 73 | if num_mat_anims > 0 { 74 | info!("Material animation support is experimental!") 75 | } 76 | } 77 | 78 | fn build(&mut self, file_paths: Vec) -> Result<()> { 79 | self.file_paths = file_paths; 80 | 81 | debug!("Building database..."); 82 | 83 | for file_id in 0..self.file_paths.len() { 84 | debug!("Processing {:?}...", self.file_paths[file_id]); 85 | 86 | let buf = match std::fs::read(&self.file_paths[file_id]) { 87 | Ok(buf) => buf, 88 | Err(e) =>{ 89 | error!("file-system error reading {}: {}", 90 | self.file_paths[file_id].to_string_lossy(), e, 91 | ); 92 | continue; 93 | } 94 | }; 95 | 96 | use crate::nitro::container::read_container; 97 | match read_container(Cur::new(&buf)) { 98 | Ok(cont) => { 99 | self.add_container(file_id, cont); 100 | } 101 | Err(e) => { 102 | error!("couldn't parse as a Nitro file: {}: {}", 103 | self.file_paths[file_id].to_string_lossy(), e); 104 | } 105 | } 106 | } 107 | 108 | self.build_by_name_maps(); 109 | Ok(()) 110 | } 111 | 112 | fn add_container(&mut self, file_id: FileId, cont: Container) { 113 | use std::iter::repeat; 114 | 115 | macro_rules! move_from_cont { 116 | ($kind:ident, $kind_found_in:ident) => { 117 | let num = cont.$kind.len(); 118 | self.$kind.extend(cont.$kind.into_iter()); 119 | self.$kind_found_in.extend(repeat(file_id).take(num)); 120 | }; 121 | } 122 | 123 | move_from_cont!(models, models_found_in); 124 | move_from_cont!(textures, textures_found_in); 125 | move_from_cont!(palettes, palettes_found_in); 126 | move_from_cont!(animations, animations_found_in); 127 | move_from_cont!(patterns, patterns_found_in); 128 | move_from_cont!(mat_anims, mat_anims_found_in); 129 | } 130 | 131 | /// Fill out `textures_by_name` and `palettes_by_name`. 132 | fn build_by_name_maps(&mut self) { 133 | for (id, texture) in self.textures.iter().enumerate() { 134 | self.textures_by_name.entry(texture.name) 135 | .or_insert(vec![]) 136 | .push(id); 137 | } 138 | 139 | for (id, palette) in self.palettes.iter().enumerate() { 140 | self.palettes_by_name.entry(palette.name) 141 | .or_insert(vec![]) 142 | .push(id); 143 | } 144 | } 145 | } 146 | 147 | /// Collects the user's provided paths, expanding any that are directories into 148 | /// their children (only expand once, not recursively). 149 | fn expand_directories>(paths: I) -> Vec { 150 | let mut file_paths = vec![]; 151 | for path in paths { 152 | if path.is_dir() { 153 | let start_idx = file_paths.len(); 154 | 155 | let entries = match fs::read_dir(path) { 156 | Ok(entries) => entries, 157 | Err(e) => { 158 | warn!("error reading directory: {}", e); 159 | continue; 160 | } 161 | }; 162 | for entry in entries { 163 | let entry = match entry { 164 | Ok(entry) => entry, 165 | Err(_) => continue, 166 | }; 167 | file_paths.push(entry.path()); 168 | } 169 | 170 | let end_idx = file_paths.len(); 171 | file_paths[start_idx..end_idx].sort(); 172 | } else { 173 | file_paths.push(path); 174 | } 175 | } 176 | file_paths 177 | } 178 | -------------------------------------------------------------------------------- /src/decompress/mod.rs: -------------------------------------------------------------------------------- 1 | //! Decompress NDS data. 2 | //! 3 | //! The compressions methods are the LZ77 methods included in the NDS bios. 4 | //! 5 | //! See: http://problemkaputt.de/gbatek.htm#biosdecompressionfunctions 6 | //! See: DSDecmp (https://github.com/Barubary/dsdecmp) 7 | 8 | use std::{fmt, error, result}; 9 | use crate::util::bits::BitField; 10 | use crate::util::cur::{self, Cur}; 11 | 12 | pub struct DecompressResult<'a> { 13 | /// The decompressed data. 14 | pub data: Vec, 15 | /// A cursor at the end of the compressed data stream. 16 | pub end_cur: Cur<'a>, 17 | } 18 | 19 | /// Try to decompress data at `cur`. 20 | pub fn decompress(cur: Cur) -> Result { 21 | match cur.peek::() { 22 | Ok(0x10) => de_lz77_0x10(cur), 23 | Ok(0x11) => de_lz77_0x11(cur), 24 | _ => Err(Error::DecompressFailed), 25 | } 26 | } 27 | 28 | fn de_lz77_0x10(mut cur: Cur) -> Result { 29 | let header = cur.next::()?; 30 | let ty = header.bits(0,8); 31 | if ty != 0x10 { 32 | return Err(Error::DecompressFailed); 33 | } 34 | let mut decompressed_size = header.bits(8, 32) as usize; 35 | if decompressed_size == 0 { 36 | decompressed_size = cur.next::()? as usize; 37 | } 38 | 39 | // Too short to contain anything interesting 40 | if decompressed_size < 40 { 41 | return Err(Error::DecompressFailed); 42 | } 43 | // Too big (> 4 MiB) 44 | if decompressed_size > (1 << 19) * 4 { 45 | return Err(Error::DecompressFailed); 46 | } 47 | 48 | let mut out = Vec::with_capacity(decompressed_size); 49 | 50 | while out.len() < decompressed_size { 51 | let mut flags = cur.next::()?; 52 | for _ in 0..8 { 53 | let compressed = flags & 0x80 != 0; 54 | flags <<= 1; 55 | if !compressed { 56 | // Uncompressed byte 57 | out.push(cur.next::()?); 58 | } else { 59 | // LZ backreference 60 | let (ofs_sub_1, n_sub_3) = { 61 | let x = cur.next::()?.swap_bytes(); // stored big-endian 62 | (x.bits(0, 12) as usize, x.bits(12, 16) as usize) 63 | }; 64 | let (ofs, n) = (ofs_sub_1 + 1, n_sub_3 + 3); 65 | 66 | if out.len() + n > decompressed_size { // too much data 67 | return Err(Error::DecompressFailed); 68 | } 69 | if out.len() < ofs { // not enough data 70 | return Err(Error::DecompressFailed); 71 | } 72 | 73 | for _ in 0 .. n { 74 | let x = out[out.len() - ofs]; 75 | out.push(x); 76 | } 77 | } 78 | 79 | if out.len() >= decompressed_size { 80 | break; 81 | } 82 | } 83 | } 84 | 85 | Ok(DecompressResult { 86 | data: out, 87 | end_cur: cur, 88 | }) 89 | } 90 | 91 | fn de_lz77_0x11(mut cur: Cur) -> Result { 92 | let header = cur.next::()?; 93 | let ty = header.bits(0,8); 94 | if ty != 0x11 { 95 | return Err(Error::DecompressFailed); 96 | } 97 | let mut decompressed_size = header.bits(8, 32) as usize; 98 | if decompressed_size == 0 { 99 | decompressed_size = cur.next::()? as usize; 100 | } 101 | 102 | // Too short to contain anything interesting 103 | if decompressed_size < 40 { 104 | return Err(Error::DecompressFailed); 105 | } 106 | // Too big (> 4 MiB) 107 | if decompressed_size > (1 << 19) * 4 { 108 | return Err(Error::DecompressFailed); 109 | } 110 | 111 | let mut out = Vec::with_capacity(decompressed_size); 112 | 113 | while out.len() < decompressed_size { 114 | let mut flags = cur.next::()?; 115 | for _ in 0..8 { 116 | let compressed = flags & 0x80 != 0; 117 | flags <<= 1; 118 | if !compressed { 119 | // Uncompressed byte 120 | out.push(cur.next::()?); 121 | } else { 122 | let (ofs, n); 123 | 124 | fn nibbles(x: u8) -> (u8, u8) { (x >> 4, x & 0xf) } 125 | let (a,b) = nibbles(cur.next::()?); 126 | match a { 127 | 0 => { 128 | // ab cd ef 129 | // => 130 | // n = abc + 0x11 = bc + 0x11 131 | // ofs = def + 1 132 | let (c,d) = nibbles(cur.next::()?); 133 | let ef = cur.next::()?; 134 | 135 | n = (((b as usize) << 4) | (c as usize)) + 0x11; 136 | ofs = (((d as usize) << 8) | ef as usize) + 1; 137 | } 138 | 1 => { 139 | // ab cd ef gh 140 | // => 141 | // n = bcde + 0x111 142 | // ofs = fgh + 1 143 | let cd = cur.next::()?; 144 | let (e,f) = nibbles(cur.next::()?); 145 | let gh = cur.next::()?; 146 | 147 | n = (((b as usize) << 12) | ((cd as usize) << 4) | (e as usize)) + 0x111; 148 | ofs = (((f as usize) << 8) | (gh as usize)) + 1; 149 | } 150 | _ => { 151 | // ab cd 152 | // => 153 | // n = a + 1 154 | // ofs = bcd + 1 155 | let cd = cur.next::()?; 156 | 157 | n = (a as usize) + 1; 158 | ofs = (((b as usize) << 8) | (cd as usize)) + 1; 159 | } 160 | } 161 | 162 | if out.len() + n > decompressed_size { // too much data 163 | return Err(Error::DecompressFailed); 164 | } 165 | if out.len() < ofs { // not enough data 166 | return Err(Error::DecompressFailed); 167 | } 168 | 169 | for _ in 0 .. n { 170 | let x = out[out.len() - ofs]; 171 | out.push(x); 172 | } 173 | } 174 | 175 | if out.len() >= decompressed_size { 176 | break; 177 | } 178 | } 179 | } 180 | 181 | Ok(DecompressResult { 182 | data: out, 183 | end_cur: cur, 184 | }) 185 | } 186 | 187 | 188 | type Result = result::Result; 189 | 190 | // Don't bother storing any info in the error type. 191 | #[derive(Debug)] 192 | pub enum Error { 193 | DecompressFailed, 194 | } 195 | 196 | impl fmt::Display for Error { 197 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 198 | write!(f, "{:?}", self) 199 | } 200 | } 201 | 202 | impl error::Error for Error {} 203 | 204 | impl From for Error { 205 | fn from(_: cur::Error) -> Error { Error::DecompressFailed } 206 | } 207 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt; 3 | 4 | pub type Result = std::result::Result>; 5 | 6 | /// Error message. 7 | #[derive(Debug)] 8 | pub struct ErrorMsg { 9 | pub msg: String, 10 | } 11 | 12 | impl fmt::Display for ErrorMsg { 13 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 14 | f.write_str(&self.msg) 15 | } 16 | } 17 | 18 | impl Error for ErrorMsg {} 19 | 20 | macro_rules! errmsg { 21 | ($msg:expr) => { 22 | crate::errors::ErrorMsg { msg: $msg.into() } 23 | }; 24 | ($fmt:expr, $($arg:tt)+) => { 25 | crate::errors::ErrorMsg { msg: format!($fmt, $($arg)+) } 26 | }; 27 | } 28 | 29 | macro_rules! bail { 30 | ($($arg:tt)+) => { 31 | return Err(errmsg!($($arg)+).into()) 32 | }; 33 | } 34 | 35 | macro_rules! check { 36 | ($b:expr) => { 37 | if !$b { 38 | Err(errmsg!(concat!("expected: ", stringify!($b)))) 39 | } else { 40 | Ok(()) 41 | } 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /src/extract/mod.rs: -------------------------------------------------------------------------------- 1 | //! Extract recognized container files from ROMs or other packed files. 2 | 3 | use crate::cli::Args; 4 | use crate::decompress; 5 | use crate::errors::Result; 6 | use crate::nitro::Container; 7 | use crate::nitro::container::read_container; 8 | use std::collections::HashSet; 9 | use std::fs; 10 | use std::io::Write; 11 | use std::path::PathBuf; 12 | use crate::util::cur::Cur; 13 | use crate::util::OutDir; 14 | 15 | pub fn main(args: &Args) -> Result<()> { 16 | let input_file = &args.free_args[0]; 17 | let input = fs::read(input_file) 18 | .map_err(|e| errmsg!("couldn't read input file: {}", e))?; 19 | let cur = Cur::new(&input[..]); 20 | 21 | let out_dir_path = PathBuf::from(args.get_opt("output").unwrap()); 22 | let out_dir = OutDir::new(out_dir_path)?; 23 | let mut output = ExtractOutput::new(out_dir); 24 | 25 | scan_for_nitro_files(&mut output, cur); 26 | scan_for_compressed_nitro_files(&mut output, cur); 27 | 28 | output.print_report(); 29 | 30 | Ok(()) 31 | } 32 | 33 | fn scan_for_nitro_files(output: &mut ExtractOutput, mut cur: Cur) { 34 | while let Some(start_idx) = find_next_stamp(cur.slice_from_cur_to_end()) { 35 | cur.jump_forward(start_idx); 36 | scan_for_file_at(output, &mut cur); 37 | } 38 | } 39 | 40 | /// Scans for a Nitro file at cur and outputs it if it exists. 41 | /// Also moves cur to where you should resume searching from. 42 | fn scan_for_file_at(output: &mut ExtractOutput, cur: &mut Cur) { 43 | if let Ok(cont) = read_container(*cur) { 44 | if let Ok(file_bytes) = cur.next_n_u8s(cont.file_size as usize) { 45 | output.save_file(file_bytes, &cont); 46 | return; 47 | } 48 | } 49 | cur.jump_forward(1); 50 | } 51 | 52 | fn scan_for_compressed_nitro_files(output: &mut ExtractOutput, mut cur: Cur) { 53 | while let Some(start_idx) = find_next_compression_start_byte(cur.slice_from_cur_to_end()) { 54 | cur.jump_forward(start_idx); 55 | if let Ok(result) = decompress::decompress(cur) { 56 | scan_for_nitro_files(output, Cur::new(&result.data)); 57 | } 58 | cur.jump_forward(1); 59 | } 60 | } 61 | 62 | fn find_next_stamp(bytes: &[u8]) -> Option { 63 | // find BMD0|BTX0|BCA0|BTP0|BTA0 64 | let mut i = 0; 65 | while i + 3 < bytes.len() { 66 | if bytes[i] == b'B' && bytes[i+3] == b'0' { 67 | if (bytes[i+1] == b'M' && bytes[i+2] == b'D') || 68 | (bytes[i+1] == b'T' && bytes[i+2] == b'X') || 69 | (bytes[i+1] == b'C' && bytes[i+2] == b'A') || 70 | (bytes[i+1] == b'T' && bytes[i+2] == b'P') || 71 | (bytes[i+1] == b'T' && bytes[i+2] == b'A') { 72 | return Some(i); 73 | } 74 | } 75 | i += 1; 76 | } 77 | None 78 | } 79 | 80 | fn find_next_compression_start_byte(bytes: &[u8]) -> Option { 81 | // find 0x10|0x11 82 | let mut i = 0; 83 | while i != bytes.len() { 84 | if bytes[i] == 0x10 || bytes[i] == 0x11 { 85 | return Some(i); 86 | } 87 | i += 1; 88 | } 89 | None 90 | } 91 | 92 | struct ExtractOutput { 93 | taken_file_names: HashSet, 94 | out_dir: OutDir, 95 | num_bmds: u32, 96 | num_btxs: u32, 97 | num_bcas: u32, 98 | num_btps: u32, 99 | num_btas: u32, 100 | } 101 | 102 | impl ExtractOutput { 103 | fn new(out_dir: OutDir) -> ExtractOutput { 104 | ExtractOutput { 105 | taken_file_names: HashSet::new(), 106 | out_dir, 107 | num_bmds: 0, 108 | num_btxs: 0, 109 | num_bcas: 0, 110 | num_btps: 0, 111 | num_btas: 0, 112 | } 113 | } 114 | 115 | /// Print report on extraction results. 116 | fn print_report(&self) { 117 | let plural = |x| if x != 1 { "s" } else { "" }; 118 | println!("Found {} BMD{}, {} BTX{}, {} BCA{}, {} BTP{}, {} BTA{}.", 119 | self.num_bmds, plural(self.num_bmds), 120 | self.num_btxs, plural(self.num_btxs), 121 | self.num_bcas, plural(self.num_bcas), 122 | self.num_btps, plural(self.num_btps), 123 | self.num_btas, plural(self.num_btas), 124 | ); 125 | } 126 | 127 | /// Given the slice `bytes` that successfully parsed as the Nitro container 128 | /// `container`, save the slice to a file in the output directory. 129 | fn save_file(&mut self, bytes: &[u8], container: &Container) { 130 | let file_name = guess_container_name(container); 131 | let file_extension = match container.stamp { 132 | b"BMD0" => "nsbmd", 133 | b"BTX0" => "nsbtx", 134 | b"BCA0" => "nsbca", 135 | b"BTP0" => "nsbtp", 136 | b"BTA0" => "nsbta", 137 | _ => "nsbxx", 138 | }; 139 | 140 | // Find an available filename 141 | let mut save_path = format!("{}.{}", file_name, file_extension); 142 | let mut cntr = 1; 143 | while self.taken_file_names.contains(&save_path) { 144 | save_path.clear(); 145 | use std::fmt::Write; 146 | write!(save_path, "{}.{:03}.{}", file_name, cntr, file_extension).unwrap(); 147 | cntr += 1; 148 | } 149 | self.taken_file_names.insert(save_path.clone()); 150 | 151 | let result = self.out_dir 152 | .create_file(&save_path) 153 | .and_then(|mut f| Ok(f.write_all(bytes)?)); 154 | match result { 155 | Ok(()) => { 156 | match container.stamp { 157 | b"BMD0" => self.num_bmds += 1, 158 | b"BTX0" => self.num_btxs += 1, 159 | b"BCA0" => self.num_bcas += 1, 160 | b"BTP0" => self.num_btps += 1, 161 | b"BTA0" => self.num_btas += 1, 162 | _ => (), 163 | } 164 | } 165 | Err(e) => { 166 | error!("failed to write {}: {:?}", file_name, e); 167 | } 168 | } 169 | } 170 | } 171 | 172 | /// Guess a name for `cont` using the name of its first item. 173 | fn guess_container_name(cont: &Container) -> String { 174 | if !cont.models.is_empty() { 175 | format!("{}", cont.models[0].name.print_safe()) 176 | } else if !cont.textures.is_empty() { 177 | format!("{}", cont.textures[0].name.print_safe()) 178 | } else if !cont.palettes.is_empty() { 179 | format!("{}", cont.palettes[0].name.print_safe()) 180 | } else if !cont.animations.is_empty() { 181 | format!("{}", cont.animations[0].name.print_safe()) 182 | } else if !cont.patterns.is_empty() { 183 | format!("{}", cont.patterns[0].name.print_safe()) 184 | } else if !cont.mat_anims.is_empty() { 185 | format!("{}", cont.mat_anims[0].name.print_safe()) 186 | } else { 187 | match cont.stamp { 188 | b"BMD0" => "empty_model_file", 189 | b"BTX0" => "empty_texture_file", 190 | b"BCA0" => "empty_animation_file", 191 | b"BTP0" => "empty_pattern_file", 192 | b"BTA0" => "empty_material_anim_file", 193 | _ => "empty_unknown_file", 194 | }.to_string() 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/info/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::Args; 2 | use crate::errors::Result; 3 | use crate::db::Database; 4 | use crate::connection::{Connection, ConnectionOptions, MaterialConnection, Match}; 5 | 6 | pub fn main(args: &Args) -> Result<()> { 7 | let db = Database::from_cli_args(args)?; 8 | 9 | let conn_options = ConnectionOptions::from_cli_args(args); 10 | let conn = Connection::build(&db, conn_options); 11 | 12 | db.print_status(); 13 | 14 | println!(); 15 | 16 | for model_id in 0..db.models.len() { 17 | model_info(&db, &conn, model_id); 18 | } 19 | for texture_id in 0..db.textures.len() { 20 | texture_info(&db, texture_id); 21 | } 22 | for palette_id in 0..db.palettes.len() { 23 | palette_info(&db, palette_id); 24 | } 25 | for animation_id in 0..db.animations.len() { 26 | animation_info(&db, animation_id); 27 | } 28 | for pattern_id in 0..db.patterns.len() { 29 | pattern_info(&db, pattern_id); 30 | } 31 | for mat_anim_id in 0..db.mat_anims.len() { 32 | mat_anim_info(&db, mat_anim_id); 33 | } 34 | 35 | Ok(()) 36 | } 37 | 38 | 39 | fn model_info(db: &Database, conn: &Connection, model_id: usize) { 40 | let model = &db.models[model_id]; 41 | println!("Model {}:", model_id); 42 | println!(" Name: {:?}", model.name); 43 | println!(" Found In: {}", 44 | db.file_paths[db.models_found_in[model_id]].to_string_lossy()); 45 | println!(" Num Pieces: {}", model.pieces.len()); 46 | println!(" Objects ({} total):", model.objects.len()); 47 | for (i, object) in model.objects.iter().enumerate() { 48 | print!(" Object {}: {:?} ", i, object.name); 49 | println!("({}{}{})", 50 | object.trans.map(|_| "T").unwrap_or("-"), 51 | object.rot.map(|_| "R").unwrap_or("-"), 52 | object.scale.map(|_| "S").unwrap_or("-"), 53 | ); 54 | } 55 | println!(" Materials ({} total):", model.materials.len()); 56 | for (i, material) in model.materials.iter().enumerate() { 57 | println!(" Material {}:", i); 58 | println!(" Name: {:?}", material.name); 59 | if let Some(name) = material.texture_name { 60 | print!(" Texture: {:?} ", name); 61 | 62 | match conn.models[model_id].materials[i].texture() { 63 | None => print!("(not found)"), 64 | Some(Match { id, best }) => { 65 | print!("(matched texture {}", id); 66 | if !best { 67 | print!(", but tentatively"); 68 | } 69 | print!(")") 70 | } 71 | } 72 | println!(); 73 | } 74 | if let Some(name) = material.palette_name { 75 | print!(" Palette: {:?} ", name); 76 | 77 | match conn.models[model_id].materials[i] { 78 | MaterialConnection::NoTexture => 79 | print!("(palette but no texture!?)"), 80 | MaterialConnection::TextureMissing { .. } => 81 | print!("(skipped; texture missing)"), 82 | MaterialConnection::TextureOkNoPalette { .. } => 83 | unreachable!(), 84 | MaterialConnection::TextureOkPaletteMissing { .. } => 85 | print!("(not found)"), 86 | MaterialConnection::TextureOkPaletteOk { 87 | palette: Match { id, best }, .. 88 | } => { 89 | print!("(matched palette {}", id); 90 | if !best { 91 | print!(", but tentatively"); 92 | } 93 | print!(")"); 94 | } 95 | } 96 | println!(); 97 | } 98 | 99 | let params = material.params; 100 | println!(" Dimensions: {}x{}", material.width, material.height); 101 | println!(" Repeat (s,t): ({}, {})", params.repeat_s(), params.repeat_t()); 102 | println!(" Mirror (s,t): ({}, {})", params.mirror_s(), params.mirror_t()); 103 | println!(" Texcoord Transform Mode: {}", params.texcoord_transform_mode()); 104 | 105 | println!(" Diffuse Color: {:?}", material.diffuse); 106 | println!(" Diffuse is Default Vertex Color: {}", material.diffuse_is_default_vertex_color); 107 | println!(" Ambient Color: {:?}", material.ambient); 108 | println!(" Specular Color: {:?}", material.specular); 109 | println!(" Enable Shininess Table: {}", material.enable_shininess_table); 110 | println!(" Emission: {:?}", material.emission); 111 | println!(" Alpha: {:?}", material.alpha); 112 | 113 | println!(" Cull: {}", 114 | match (material.cull_backface, material.cull_frontface) { 115 | (true, true) => "All (y tho?)", 116 | (true, false) => "Backfacing", 117 | (false, true) => "Frontfacing", 118 | (false, false) => "None", 119 | } 120 | ); 121 | } 122 | println!(); 123 | } 124 | 125 | 126 | fn texture_info(db: &Database, texture_id: usize) { 127 | let texture = &db.textures[texture_id]; 128 | println!("Texture {}:", texture_id); 129 | println!(" Name: {:?}", texture.name); 130 | println!(" Found In: {}", 131 | db.file_paths[db.textures_found_in[texture_id]].to_string_lossy()); 132 | 133 | let params = texture.params; 134 | println!(" Offset: {:#x}", params.offset()); 135 | println!(" Dimensions: {}x{}", params.width(), params.height()); 136 | println!(" Format: {} ({})", params.format().0, params.format().desc().name); 137 | println!(" Color 0 Transparent?: {}", params.is_color0_transparent()); 138 | println!(); 139 | } 140 | 141 | fn palette_info(db: &Database, palette_id: usize) { 142 | let palette = &db.palettes[palette_id]; 143 | println!("Palette {}:", palette_id); 144 | println!(" Name: {:?}", palette.name); 145 | println!(" Found In: {}", 146 | db.file_paths[db.palettes_found_in[palette_id]].to_string_lossy()); 147 | println!(" Offset: {:#x}", palette.off); 148 | println!(); 149 | } 150 | 151 | fn animation_info(db: &Database, anim_id: usize) { 152 | let anim = &db.animations[anim_id]; 153 | println!("Animation {}:", anim_id); 154 | println!(" Name: {:?}", anim.name); 155 | println!(" Found In: {}", 156 | db.file_paths[db.animations_found_in[anim_id]].to_string_lossy()); 157 | println!(" Frames: {}", anim.num_frames); 158 | println!(" Num Objects: {}", anim.objects_curves.len()); 159 | println!(" TRS Curves:",); 160 | for (i, trs_curves) in anim.objects_curves.iter().enumerate() { 161 | println!(" Object {}:", i); 162 | 163 | use crate::nitro::animation::Curve; 164 | fn curve_info(name: &'static str, curve: &Curve) { 165 | match *curve { 166 | Curve::None => { } 167 | Curve::Constant(_) => { 168 | println!(" {}: Constant", name) 169 | } 170 | Curve::Samples { start_frame, end_frame, ref values } => { 171 | println!(" {}: {} samples (frames {} to {})", 172 | name, values.len(), start_frame, end_frame) 173 | } 174 | } 175 | } 176 | 177 | curve_info("Trans X", &trs_curves.trans[0]); 178 | curve_info("Trans Y", &trs_curves.trans[1]); 179 | curve_info("Trans Z", &trs_curves.trans[2]); 180 | curve_info("Rotation", &trs_curves.rotation); 181 | curve_info("Scale X", &trs_curves.scale[0]); 182 | curve_info("Scale Y", &trs_curves.scale[1]); 183 | curve_info("Scale Z", &trs_curves.scale[2]); 184 | } 185 | println!(); 186 | } 187 | 188 | fn pattern_info(db: &Database, pat_id: usize) { 189 | let pat = &db.patterns[pat_id]; 190 | println!("Pattern Animation {}:", pat_id); 191 | println!(" Name: {:?}", pat.name); 192 | println!(" Found In: {}", 193 | db.file_paths[db.patterns_found_in[pat_id]].to_string_lossy()); 194 | println!(" Frames: {}", pat.num_frames); 195 | println!(" Tracks ({} total):", pat.material_tracks.len()); 196 | for (i, track) in pat.material_tracks.iter().enumerate() { 197 | println!(" Track {}: {}", i, track.name); 198 | for key in &track.keyframes { 199 | println!(" {}: {:?} / {:?}", 200 | key.frame, 201 | pat.texture_names[key.texture_idx as usize], 202 | pat.palette_names[key.palette_idx as usize], 203 | ); 204 | } 205 | } 206 | println!(); 207 | } 208 | 209 | fn mat_anim_info(db: &Database, mat_anim_id: usize) { 210 | let mat_anim = &db.mat_anims[mat_anim_id]; 211 | println!("Material Animation {}:", mat_anim_id); 212 | println!(" Name: {:?}", mat_anim.name); 213 | println!(" Found In: {}", 214 | db.file_paths[db.mat_anims_found_in[mat_anim_id]].to_string_lossy()); 215 | println!(" Frames: {}", mat_anim.num_frames); 216 | println!(" Tracks ({} total):", mat_anim.tracks.len()); 217 | for (i, track) in mat_anim.tracks.iter().enumerate() { 218 | println!(" Track {}:", i); 219 | println!(" Name: {}", track.name); 220 | } 221 | println!(); 222 | } 223 | -------------------------------------------------------------------------------- /src/logger.rs: -------------------------------------------------------------------------------- 1 | //! Logger that prints messages like `[WARN] Lorem ipsum`. 2 | 3 | use log::{self, Log, Level, Metadata, Record}; 4 | use std::io::Write; 5 | use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; 6 | 7 | struct Logger { 8 | level: Level, 9 | use_color: bool, 10 | } 11 | 12 | impl Log for Logger { 13 | fn enabled(&self, metadata: &Metadata) -> bool { 14 | metadata.level() <= self.level 15 | } 16 | 17 | fn log(&self, record: &Record) { 18 | if !self.enabled(record.metadata()) { 19 | return; 20 | } 21 | 22 | let color_choice = match self.use_color { 23 | true => ColorChoice::Auto, 24 | false => ColorChoice::Never, 25 | }; 26 | let mut stderr = StandardStream::stderr(color_choice); 27 | let _ = stderr.set_color(ColorSpec::new().set_fg(Some(Color::Green))); 28 | let _ = writeln!(&mut stderr, "[{}] {}", 29 | record.level().to_string(), 30 | record.args(), 31 | ); 32 | let _ = stderr.reset(); 33 | } 34 | 35 | fn flush(&self) { } 36 | } 37 | 38 | pub fn init(level: Level) { 39 | use std::io::IsTerminal; 40 | let use_color = std::io::stderr().is_terminal(); 41 | 42 | let logger = Logger { level, use_color }; 43 | let _ = log::set_boxed_logger(Box::new(logger)); 44 | log::set_max_level(level.to_level_filter()); 45 | } 46 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! apicula, NDS model viewer/converter 2 | 3 | #![recursion_limit="128"] 4 | 5 | #[macro_use] 6 | extern crate log; 7 | #[macro_use] 8 | extern crate glium; 9 | #[macro_use] 10 | extern crate json; 11 | 12 | #[macro_use] 13 | mod errors; 14 | #[macro_use] 15 | mod util; 16 | mod cli; 17 | mod convert; 18 | mod decompress; 19 | mod extract; 20 | mod nds; 21 | mod nitro; 22 | mod viewer; 23 | mod db; 24 | mod info; 25 | mod primitives; 26 | mod skeleton; 27 | mod logger; 28 | mod connection; 29 | mod version; 30 | 31 | use errors::Result; 32 | 33 | fn main() { 34 | let ret_code = match main2() { 35 | Ok(()) => 0, 36 | Err(e) => { 37 | error!("{}", e); 38 | 1 39 | } 40 | }; 41 | std::process::exit(ret_code); 42 | } 43 | 44 | fn main2() -> Result<()> { 45 | init_logger(0); 46 | let args = cli::parse_cli_args(); 47 | match args.subcommand { 48 | "extract" => extract::main(&args)?, 49 | "view" => viewer::main(&args)?, 50 | "convert" => convert::main(&args)?, 51 | "info" => info::main(&args)?, 52 | _ => unimplemented!(), 53 | } 54 | Ok(()) 55 | } 56 | 57 | pub fn init_logger(verbosity: u64) { 58 | use log::Level; 59 | let max_log_level = match verbosity { 60 | 0 => Level::Info, 61 | 1 => Level::Debug, 62 | _ => Level::Trace, 63 | }; 64 | logger::init(max_log_level); 65 | } 66 | -------------------------------------------------------------------------------- /src/nds/decode_texture.rs: -------------------------------------------------------------------------------- 1 | use crate::nitro::{Texture, Palette}; 2 | use crate::errors::Result; 3 | use crate::util::bits::BitField; 4 | use crate::util::cur::Cur; 5 | 6 | /// Pixel data stored in R8G8B8A8 format. 7 | pub struct RGBABuf(pub Vec); 8 | 9 | impl RGBABuf { 10 | pub fn for_dimensions((width, height): (u32, u32)) -> RGBABuf { 11 | let (w, h) = (width as usize, height as usize); 12 | RGBABuf(Vec::with_capacity(4 * w * h)) 13 | } 14 | 15 | fn pixel(&mut self, pixel: [u8; 4]) { 16 | self.0.extend_from_slice(&pixel); 17 | } 18 | } 19 | 20 | /// Decodes a texture/palette combo to RGBA. 21 | pub fn decode_texture(texture: &Texture, palette: Option<&Palette>) -> Result { 22 | let params = texture.params; 23 | let (w, h) = params.dim(); 24 | let requires_palette = params.format().desc().requires_palette; 25 | 26 | if requires_palette && palette.is_none() { 27 | bail!("texture required a palette"); 28 | } 29 | 30 | let mut buf = RGBABuf::for_dimensions((w, h)); 31 | 32 | use super::TextureFormat as F; 33 | match params.format() { 34 | F(0) => bail!("texture had format 0"), 35 | F(1) => decode_format1(&mut buf, texture, palette.unwrap()), 36 | F(2) => decode_format2(&mut buf, texture, palette.unwrap()), 37 | F(3) => decode_format3(&mut buf, texture, palette.unwrap()), 38 | F(4) => decode_format4(&mut buf, texture, palette.unwrap()), 39 | F(5) => decode_format5(&mut buf, texture, palette.unwrap()), 40 | F(6) => decode_format6(&mut buf, texture, palette.unwrap()), 41 | F(7) => decode_format7(&mut buf, texture), 42 | _ => unreachable!(), 43 | } 44 | 45 | Ok(buf) 46 | } 47 | 48 | fn decode_format1(buf: &mut RGBABuf, tex: &Texture, pal: &Palette) { 49 | // A3I5 Translucent Texture (3-bit Alpha, 5-bit Color Index) 50 | let (w, h) = tex.params.dim(); 51 | let num_texels = (w * h) as usize; 52 | let data = &tex.data1[..]; 53 | let pal_cur = Cur::from_buf_pos(&pal.pal_block[..], pal.off as usize); 54 | for n in 0..num_texels { 55 | let x = data[n]; 56 | let rgb = pal_cur.nth::(x.bits(0,5) as usize).unwrap_or(0); 57 | let a = a3_to_a5(x.bits(5,8)); 58 | buf.pixel(rgb555a5(rgb, a)); 59 | } 60 | } 61 | 62 | fn decode_format2(buf: &mut RGBABuf, tex: &Texture, pal: &Palette) { 63 | // 4-Color Palette Texture 64 | let (w, h) = tex.params.dim(); 65 | let num_bytes = tex.params.format().byte_len((w, h)); 66 | let color0_is_transparent = tex.params.is_color0_transparent(); 67 | let data = &tex.data1; 68 | let pal_cur = Cur::from_buf_pos(&pal.pal_block[..], pal.off as usize); 69 | for n in 0..num_bytes { 70 | let x = data[n]; 71 | 72 | macro_rules! do_pixel { 73 | ($lo:expr, $hi:expr) => { 74 | let u = x.bits($lo, $hi); 75 | let rgb = pal_cur.nth::(u as usize).unwrap_or(0); 76 | let transparent = u == 0 && color0_is_transparent; 77 | let a = if transparent { 0 } else { 31 }; 78 | buf.pixel(rgb555a5(rgb, a)); 79 | }; 80 | } 81 | 82 | do_pixel!(0, 2); 83 | do_pixel!(2, 4); 84 | do_pixel!(4, 6); 85 | do_pixel!(6, 8); 86 | } 87 | } 88 | 89 | fn decode_format3(buf: &mut RGBABuf, tex: &Texture, pal: &Palette) { 90 | // 16-Color Palette Texture 91 | let (w, h) = tex.params.dim(); 92 | let num_bytes = tex.params.format().byte_len((w, h)); 93 | let color0_is_transparent = tex.params.is_color0_transparent(); 94 | let data = &tex.data1; 95 | let pal_cur = Cur::from_buf_pos(&pal.pal_block[..], pal.off as usize); 96 | for n in 0..num_bytes { 97 | let x = data[n]; 98 | 99 | macro_rules! do_pixel { 100 | ($lo:expr, $hi:expr) => { 101 | let u = x.bits($lo, $hi); 102 | let rgb = pal_cur.nth::(u as usize).unwrap_or(0); 103 | let transparent = u == 0 && color0_is_transparent; 104 | let a = if transparent { 0 } else { 31 }; 105 | buf.pixel(rgb555a5(rgb, a)); 106 | }; 107 | } 108 | 109 | do_pixel!(0, 4); 110 | do_pixel!(4, 8); 111 | } 112 | } 113 | 114 | fn decode_format4(buf: &mut RGBABuf, tex: &Texture, pal: &Palette) { 115 | // 256-Color Palette Texture 116 | let (w, h) = tex.params.dim(); 117 | let num_bytes = tex.params.format().byte_len((w, h)); 118 | let color0_is_transparent = tex.params.is_color0_transparent(); 119 | let data = &tex.data1; 120 | let pal_cur = Cur::from_buf_pos(&pal.pal_block[..], pal.off as usize); 121 | for n in 0..num_bytes { 122 | let x = data[n]; 123 | 124 | let rgb = pal_cur.nth::(x as usize).unwrap_or(0); 125 | let transparent = x == 0 && color0_is_transparent; 126 | let a = if transparent { 0 } else { 31 }; 127 | buf.pixel(rgb555a5(rgb, a)); 128 | } 129 | } 130 | 131 | fn decode_format5(buf: &mut RGBABuf, tex: &Texture, pal: &Palette) { 132 | // Block-compressed Texture 133 | let (w, h) = tex.params.dim(); 134 | let num_blocks_x = w / 4; 135 | let num_blocks = (w * h / 16) as usize; 136 | 137 | let data1 = Cur::new(&tex.data1).next_n::(num_blocks).unwrap(); 138 | let data2 = Cur::new(&tex.data2).next_n::(num_blocks).unwrap(); 139 | let pal_cur = Cur::from_buf_pos(&pal.pal_block[..], pal.off as usize); 140 | 141 | for y in 0..h { 142 | for x in 0..w { 143 | // Find the block containing (x, y) 144 | let block_idx = num_blocks_x * (y/4) + (x/4); 145 | let block = data1.nth(block_idx as usize); 146 | let extra = data2.nth(block_idx as usize); 147 | 148 | // Find the bits for this texel within the block 149 | let texel_off = 2 * (4 * (y%4) + (x%4)) as u32; 150 | let texel = block.bits(texel_off, texel_off+2); 151 | 152 | let mode = extra.bits(14,16); 153 | let pal_addr = (extra.bits(0,14) as usize) << 1; 154 | 155 | let pixel = { 156 | let color = |n| { 157 | let rgb = pal_cur.nth::(pal_addr+n).unwrap_or(0); 158 | rgb555a5(rgb, 31) 159 | }; 160 | 161 | match (mode, texel) { 162 | (0, 0) => color(0), 163 | (0, 1) => color(1), 164 | (0, 2) => color(2), 165 | (0, 3) => [0, 0, 0, 0], 166 | 167 | (1, 0) => color(0), 168 | (1, 1) => color(1), 169 | (1, 2) => avg(color(0), color(1)), 170 | (1, 3) => [0, 0, 0, 0], 171 | 172 | (2, 0) => color(0), 173 | (2, 1) => color(1), 174 | (2, 2) => color(2), 175 | (2, 3) => color(3), 176 | 177 | (3, 0) => color(0), 178 | (3, 1) => color(1), 179 | (3, 2) => avg358(color(1), color(0)), 180 | (3, 3) => avg358(color(0), color(1)), 181 | 182 | _ => unreachable!(), 183 | } 184 | }; 185 | 186 | buf.pixel(pixel); 187 | } 188 | } 189 | } 190 | 191 | fn decode_format6(buf: &mut RGBABuf, tex: &Texture, pal: &Palette) { 192 | // A5I3 Translucent Texture (5-bit Alpha, 3-bit Color Index) 193 | let (w, h) = tex.params.dim(); 194 | let num_texels = (w * h) as usize; 195 | let data = &tex.data1[..]; 196 | let pal_cur = Cur::from_buf_pos(&pal.pal_block[..], pal.off as usize); 197 | for n in 0..num_texels { 198 | let x = data[n]; 199 | let rgb = pal_cur.nth::(x.bits(0,3) as usize).unwrap_or(0); 200 | let a = x.bits(3,8); 201 | buf.pixel(rgb555a5(rgb, a)); 202 | } 203 | } 204 | 205 | fn decode_format7(buf: &mut RGBABuf, tex: &Texture) { 206 | // Direct Color Texture 207 | // Holds actual 16-bit color values (no palette) 208 | let (w, h) = tex.params.dim(); 209 | let num_texels = (w * h) as usize; 210 | let data = Cur::new(&tex.data1).next_n::(num_texels).unwrap(); 211 | for n in 0..num_texels { 212 | let texel = data.nth(n); 213 | let alpha_bit = texel.bits(15,16); 214 | buf.pixel(rgb555a5( 215 | texel, 216 | if alpha_bit == 0 { 0 } else { 31 }, 217 | )); 218 | } 219 | } 220 | 221 | 222 | /// Converts RGB555 color and A5 alpha into RGBA8888. 223 | fn rgb555a5(rgb555: u16, a5: u8) -> [u8; 4] { 224 | let r5 = rgb555.bits(0,5) as u8; 225 | let g5 = rgb555.bits(5,10) as u8; 226 | let b5 = rgb555.bits(10,15) as u8; 227 | let r8 = extend_5bit_to_8bit(r5); 228 | let g8 = extend_5bit_to_8bit(g5); 229 | let b8 = extend_5bit_to_8bit(b5); 230 | let a8 = extend_5bit_to_8bit(a5); 231 | [r8, g8, b8, a8] 232 | } 233 | 234 | fn a3_to_a5(x: u8) -> u8 { 235 | (x << 2) | (x >> 1) 236 | } 237 | 238 | fn extend_5bit_to_8bit(x: u8) -> u8 { 239 | (x << 3) | (x >> 2) 240 | } 241 | 242 | /// (c1 + c2) / 2 243 | fn avg(c1: [u8; 4], c2: [u8; 4]) -> [u8; 4] { 244 | [ 245 | ((c1[0] as u32 + c2[0] as u32) / 2) as u8, 246 | ((c1[1] as u32 + c2[1] as u32) / 2) as u8, 247 | ((c1[2] as u32 + c2[2] as u32) / 2) as u8, 248 | ((c1[3] as u32 + c2[3] as u32) / 2) as u8, 249 | ] 250 | } 251 | 252 | /// (3*c1 + 5*c2) / 8 253 | fn avg358(c1: [u8; 4], c2: [u8; 4]) -> [u8; 4] { 254 | [ 255 | ((3*c1[0] as u32 + 5*c2[0] as u32) / 8) as u8, 256 | ((3*c1[1] as u32 + 5*c2[1] as u32) / 8) as u8, 257 | ((3*c1[2] as u32 + 5*c2[2] as u32) / 8) as u8, 258 | ((3*c1[3] as u32 + 5*c2[3] as u32) / 8) as u8, 259 | ] 260 | } 261 | -------------------------------------------------------------------------------- /src/nds/gpu_cmds.rs: -------------------------------------------------------------------------------- 1 | //! NDS GPU command parsing. 2 | //! 3 | //! A `CmdParser` allows consumers to iterate over NDS GPU commands stored in 4 | //! a buffer. This modules doesn't actually do anything with the commands, it 5 | //! just knows how they're stored in memory and provides them in a more easily- 6 | //! consumable form. 7 | //! 8 | //! See the [GBATEK documentation](http://problemkaputt.de/gbatek.htm#ds3dvideo) 9 | //! for a reference on the DS's GPU. 10 | 11 | use cgmath::{Point2, Point3, Vector3, vec3}; 12 | use crate::errors::Result; 13 | use crate::util::bits::BitField; 14 | use crate::util::fixed::fix16; 15 | use crate::util::fixed::fix32; 16 | use crate::util::view::View; 17 | 18 | /// DS GPU command. 19 | pub enum GpuCmd { 20 | /// Do nothing. 21 | Nop, 22 | 23 | /// Load the matrix from stack slot `idx` to the current matrix. 24 | Restore { idx: u32 }, 25 | 26 | /// Precompose the current matrix with a scaling. 27 | Scale { scale: (f64, f64, f64) }, 28 | 29 | /// Begin a new primitive group of the given type (tris, quads, etc). 30 | Begin { prim_type: u32 }, 31 | 32 | /// End the current primitive group. 33 | End, 34 | 35 | /// Send a vertex with the given position. 36 | /// 37 | /// Note that the position is _untransformed_. It must be multiplied 38 | /// by the current matrix to get its final position. The implementer 39 | /// is responsible for tracking the current matrix and doing the 40 | /// transformation. 41 | Vertex { position: Point3 }, 42 | 43 | /// Set the texture coordinate for subsequent vertices. 44 | /// 45 | /// Texture coordinate on the DS are measured in texels. The top-left 46 | /// corner of an image is (0,0) and the bottom-right is (w,h), where 47 | /// w and h are the width and height of the image. 48 | TexCoord { texcoord: Point2 }, 49 | 50 | /// Set the color for subsequent vertices. 51 | Color { color: Point3 }, 52 | 53 | /// Set the normal vector for subsequent vertices. 54 | Normal { normal: Vector3 }, 55 | } 56 | 57 | /// Parses the memory representation of GPU commands, yielding them as 58 | /// an iterator. 59 | /// 60 | /// A GPU command consists of a one-byte opcode and a sequence of 32-words 61 | /// for parameters. The commands are packed in memory as follows: four 62 | /// commands are packed as a sequence of words 63 | /// 64 | /// 1. the four opcodes into the first four bytes 65 | /// 2. then the parameters for the first command, then the parameters for 66 | /// the second command, then the third, then the fourth. 67 | pub struct CmdParser<'a> { 68 | /// The unprocessed opcodes (never more than four at one time). 69 | pub opcode_fifo: &'a [u8], 70 | 71 | /// The buffer, starting with the parameters for the next opcode in 72 | /// `opcode_fifo`, or starting with the next group of opcodes if that's 73 | /// empty. 74 | pub buf: &'a [u8], 75 | 76 | /// The last vertex position sent; this is needed for the relative GPU 77 | /// commands that specify a vertex position by a displacement from the 78 | /// last one. 79 | pub vertex: Point3, 80 | 81 | /// Whether we're done. Always set after an error. 82 | done: bool, 83 | } 84 | 85 | impl<'a> CmdParser<'a> { 86 | pub fn new(cmds: &[u8]) -> CmdParser { 87 | CmdParser { 88 | opcode_fifo: &cmds[0..0], 89 | buf: cmds, 90 | vertex: Point3::new(0.0, 0.0, 0.0), 91 | done: false, 92 | } 93 | } 94 | } 95 | 96 | impl<'a> Iterator for CmdParser<'a> { 97 | type Item = Result; 98 | 99 | fn next(&mut self) -> Option { 100 | if self.done { 101 | return None; 102 | } 103 | 104 | // Fill up the opcode FIFO if it has run out. 105 | if self.opcode_fifo.len() == 0 { 106 | if self.buf.len() == 0 { 107 | // Finished successfully. 108 | self.done = true; 109 | return None; 110 | } 111 | 112 | if self.buf.len() < 4 { 113 | self.done = true; 114 | return Some(Err("GPU has too few opcodes".into())) 115 | } 116 | 117 | self.opcode_fifo = &self.buf[0..4]; 118 | self.buf = &self.buf[4..]; 119 | } 120 | 121 | let opcode = self.opcode_fifo[0]; 122 | self.opcode_fifo = &self.opcode_fifo[1..]; 123 | 124 | let num_params = match num_params(opcode) { 125 | Ok(count) => count, 126 | Err(e) => { 127 | self.done = true; 128 | return Some(Err(e)); 129 | } 130 | }; 131 | let num_param_bytes = 4 * num_params; 132 | if self.buf.len() < num_param_bytes { 133 | self.done = true; 134 | return Some(Err("buffer too short for GPU opcode parameters".into())) 135 | } 136 | let params = View::from_buf(&self.buf[0 .. num_param_bytes]); 137 | self.buf = &self.buf[num_param_bytes ..]; 138 | 139 | Some(parse(self, opcode, params)) 140 | } 141 | } 142 | 143 | fn parse(state: &mut CmdParser, opcode: u8, params: View) -> Result { 144 | Ok(match opcode { 145 | // NOP 146 | 0x00 => GpuCmd::Nop, 147 | 148 | // MTX_RESTORE - Restore Current Matrix from Stack 149 | 0x14 => { 150 | let idx = params.nth(0) & 31; 151 | GpuCmd::Restore { idx } 152 | } 153 | 154 | // MTX_SCALE - Multiply Current Matrix by Scale Matrix 155 | 0x1b => { 156 | let sx = fix32(params.nth(0), 1, 19, 12); 157 | let sy = fix32(params.nth(1), 1, 19, 12); 158 | let sz = fix32(params.nth(2), 1, 19, 12); 159 | GpuCmd::Scale { scale: (sx, sy, sz) } 160 | } 161 | 162 | // BEGIN_VTXS - Start of Vertex List 163 | 0x40 => { 164 | let prim_type = params.nth(0) & 3; 165 | GpuCmd::Begin { prim_type } 166 | } 167 | 168 | // END_VTXS - End of Vertex List 169 | 0x41 => GpuCmd::End, 170 | 171 | // VTX_16 - Set Vertex XYZ Coordinates 172 | 0x23 => { 173 | let p0 = params.nth(0); 174 | let p1 = params.nth(1); 175 | let x = fix16(p0.bits(0, 16) as u16, 1, 3, 12); 176 | let y = fix16(p0.bits(16, 32) as u16, 1, 3, 12); 177 | let z = fix16(p1.bits(0, 16) as u16, 1, 3, 12); 178 | let position = Point3::new(x, y, z); 179 | state.vertex = position; 180 | GpuCmd::Vertex { position } 181 | } 182 | 183 | // VTX_10 - Set Vertex XYZ Coordinates 184 | 0x24 => { 185 | let p = params.nth(0); 186 | let x = fix16(p.bits(0, 10) as u16, 1, 3, 6); 187 | let y = fix16(p.bits(10, 20) as u16, 1, 3, 6); 188 | let z = fix16(p.bits(20, 30) as u16, 1, 3, 6); 189 | let position = Point3::new(x, y, z); 190 | state.vertex = position; 191 | GpuCmd::Vertex { position } 192 | } 193 | 194 | // VTX_XY - Set Vertex XY Coordinates 195 | 0x25 => { 196 | let p = params.nth(0); 197 | let x = fix16(p.bits(0, 16) as u16, 1, 3, 12); 198 | let y = fix16(p.bits(16, 32) as u16, 1, 3, 12); 199 | let position = Point3::new(x, y, state.vertex.z); 200 | state.vertex = position; 201 | GpuCmd::Vertex { position } 202 | } 203 | 204 | // VTX_XZ - Set Vertex XZ Coordinates 205 | 0x26 => { 206 | let p = params.nth(0); 207 | let x = fix16(p.bits(0, 16) as u16, 1, 3, 12); 208 | let z = fix16(p.bits(16, 32) as u16, 1, 3, 12); 209 | let position = Point3::new(x, state.vertex.y, z); 210 | state.vertex = position; 211 | GpuCmd::Vertex { position } 212 | } 213 | 214 | // VTX_YZ - Set Vertex YZ Coordinates 215 | 0x27 => { 216 | let p = params.nth(0); 217 | let y = fix16(p.bits(0, 16) as u16, 1, 3, 12); 218 | let z = fix16(p.bits(16, 32) as u16, 1, 3, 12); 219 | let position = Point3::new(state.vertex.x, y, z); 220 | state.vertex = position; 221 | GpuCmd::Vertex { position } 222 | } 223 | 224 | // VTX_DIFF - Set Relative Vertex Coordinates 225 | 0x28 => { 226 | let p = params.nth(0); 227 | // Differences are 10-bit numbers, scaled by 1/2^3 to put them 228 | // in the same 1,3,12 format as the others VTX commands. 229 | let scale = (0.5f64).powi(3); 230 | let dx = scale * fix16(p.bits(0, 10) as u16, 1, 0, 9); 231 | let dy = scale * fix16(p.bits(10, 20) as u16, 1, 0, 9); 232 | let dz = scale * fix16(p.bits(20, 30) as u16, 1, 0, 9); 233 | let position = state.vertex + vec3(dx, dy, dz); 234 | state.vertex = position; 235 | GpuCmd::Vertex { position } 236 | } 237 | 238 | // TEXCOORD - Set Texture Coordinates 239 | 0x22 => { 240 | let p = params.nth(0); 241 | let s = fix16(p.bits(0, 16) as u16, 1, 11, 4); 242 | let t = fix16(p.bits(16, 32) as u16, 1, 11, 4); 243 | let texcoord = Point2::new(s, t); 244 | GpuCmd::TexCoord { texcoord } 245 | } 246 | 247 | // COLOR - Set Vertex Color 248 | 0x20 => { 249 | let p = params.nth(0); 250 | let r = p.bits(0, 5) as f32 / 31.0; 251 | let g = p.bits(5, 10) as f32 / 31.0; 252 | let b = p.bits(10, 15) as f32 / 31.0; 253 | let color = Point3::new(r, g, b); 254 | GpuCmd::Color { color } 255 | } 256 | 257 | // NORMAL - Set Normal Vector 258 | 0x21 => { 259 | let p = params.nth(0); 260 | let x = fix32(p.bits(0, 10), 1, 0, 9); 261 | let y = fix32(p.bits(10, 20), 1, 0, 9); 262 | let z = fix32(p.bits(20, 30), 1, 0, 9); 263 | let normal = vec3(x, y, z); 264 | GpuCmd::Normal { normal } 265 | } 266 | 267 | _ => { 268 | bail!("unimplented GPU ocpode: {:#x}", opcode); 269 | } 270 | }) 271 | } 272 | 273 | /// Number of u32 parameters `opcode` takes. 274 | fn num_params(opcode: u8) -> Result { 275 | static SIZES: [i8; 66] = [ 276 | 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 277 | -1, -1, -1, -1, -1, 1, 0, 1, 1, 1, 0, 278 | 16, 12, 16, 12, 9, 3, 3, -1, -1, -1, 1, 279 | 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 280 | -1, -1, -1, -1, 1, 1, 1, 1, 1, -1, -1, 281 | -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 0, 282 | ]; 283 | let opcode = opcode as usize; 284 | if opcode >= SIZES.len() || SIZES[opcode] == -1 { 285 | bail!("unknown GPU opcode: {:#x}", opcode); 286 | } 287 | Ok(SIZES[opcode] as usize) 288 | } 289 | -------------------------------------------------------------------------------- /src/nds/mod.rs: -------------------------------------------------------------------------------- 1 | //! NDS hardware functions. 2 | 3 | pub mod gpu_cmds; 4 | pub mod texture_formats; 5 | pub mod texture_params; 6 | pub mod decode_texture; 7 | 8 | pub use self::texture_formats::{TextureFormat, Alpha}; 9 | pub use self::texture_params::TextureParams; 10 | pub use self::decode_texture::decode_texture; 11 | -------------------------------------------------------------------------------- /src/nds/texture_formats.rs: -------------------------------------------------------------------------------- 1 | //! NDS texture formats info. 2 | 3 | use super::TextureParams; 4 | 5 | #[derive(Copy, Clone)] 6 | pub struct TextureFormat(pub u8); 7 | 8 | pub enum Alpha { 9 | // Alpha = 1 10 | Opaque, 11 | // Alpha = 0 or 1 12 | Transparent, 13 | // 0 <= Alpha <= 1 14 | Translucent, 15 | } 16 | 17 | impl TextureFormat { 18 | pub fn desc(self) -> &'static FormatDesc { 19 | &DESCS[self.0 as usize] 20 | } 21 | 22 | /// How many bytes a texture of the given size takes up in this format. 23 | pub fn byte_len(self, (width, height): (u32, u32)) -> usize { 24 | let bit_len = width * height * self.desc().bpp as u32; 25 | bit_len as usize / 8 26 | } 27 | 28 | /// Whether this format can have transparent or translucent texels when 29 | /// drawn with the given parameters. 30 | pub fn alpha_type(self, params: TextureParams) -> Alpha { 31 | let is_color0_transparent = params.is_color0_transparent(); 32 | match self.desc().alpha_desc { 33 | AlphaDesc::Opaque => Alpha::Opaque, 34 | AlphaDesc::Transparent => Alpha::Transparent, 35 | AlphaDesc::Translucent => Alpha::Translucent, 36 | AlphaDesc::TransparentDependingOnParams => { 37 | if is_color0_transparent { 38 | Alpha::Transparent 39 | } else { 40 | Alpha::Opaque 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | /// Describes properties of an NDS texture format. 48 | pub struct FormatDesc { 49 | pub name: &'static str, 50 | pub requires_palette: bool, 51 | pub bpp: u8, 52 | pub alpha_desc: AlphaDesc, 53 | } 54 | 55 | pub enum AlphaDesc { 56 | Opaque, 57 | Transparent, 58 | TransparentDependingOnParams, 59 | Translucent, 60 | } 61 | 62 | pub static DESCS: [FormatDesc; 8] = [ 63 | // 0, not really a real texture format 64 | FormatDesc { 65 | name: "None", 66 | requires_palette: false, 67 | bpp: 0, 68 | alpha_desc: AlphaDesc::Opaque, 69 | }, 70 | // 1 71 | FormatDesc { 72 | name: "A3I5 Translucent Texture", 73 | requires_palette: true, 74 | bpp: 8, 75 | alpha_desc: AlphaDesc::Translucent, 76 | }, 77 | // 2 78 | FormatDesc { 79 | name: "4-Color Palette Texture", 80 | requires_palette: true, 81 | bpp: 2, 82 | alpha_desc: AlphaDesc::TransparentDependingOnParams, 83 | }, 84 | // 3 85 | FormatDesc { 86 | name: "16-Color Palette Texture", 87 | requires_palette: true, 88 | bpp: 4, 89 | alpha_desc: AlphaDesc::TransparentDependingOnParams, 90 | }, 91 | // 4 92 | FormatDesc { 93 | name: "256-Color Palette Texture", 94 | requires_palette: true, 95 | bpp: 8, 96 | alpha_desc: AlphaDesc::TransparentDependingOnParams, 97 | }, 98 | // 5 99 | FormatDesc { 100 | name: "Block-Compressed Texture", 101 | requires_palette: true, 102 | bpp: 2, 103 | alpha_desc: AlphaDesc::Transparent, 104 | }, 105 | // 6 106 | FormatDesc { 107 | name: "A5I3 Translucent Texture", 108 | requires_palette: true, 109 | bpp: 8, 110 | alpha_desc: AlphaDesc::Translucent, 111 | }, 112 | // 7 113 | FormatDesc { 114 | name: "Direct RGBA Texture", 115 | requires_palette: false, 116 | bpp: 16, 117 | alpha_desc: AlphaDesc::Transparent, 118 | }, 119 | ]; 120 | -------------------------------------------------------------------------------- /src/nds/texture_params.rs: -------------------------------------------------------------------------------- 1 | use super::TextureFormat; 2 | use crate::util::bits::BitField; 3 | 4 | #[derive(Copy, Clone)] 5 | pub struct TextureParams(pub u32); 6 | 7 | impl TextureParams { 8 | pub fn offset(self) -> u32 { self.0.bits(0,16) << 3 } 9 | pub fn repeat_s(self) -> bool { self.0.bits(16,17) != 0 } 10 | pub fn repeat_t(self) -> bool { self.0.bits(17,18) != 0 } 11 | pub fn mirror_s(self) -> bool { self.0.bits(18,19) != 0 } 12 | pub fn mirror_t(self) -> bool { self.0.bits(19,20) != 0 } 13 | pub fn width(self) -> u32 { 8 << self.0.bits(20,23) } 14 | pub fn height(self) -> u32 { 8 << self.0.bits(23,26) } 15 | pub fn dim(self) -> (u32, u32) { (self.width(), self.height()) } 16 | pub fn format(self) -> TextureFormat { TextureFormat(self.0.bits(26,29) as u8) } 17 | pub fn is_color0_transparent(self) -> bool { self.0.bits(29,30) != 0 } 18 | pub fn texcoord_transform_mode(self) -> u8 { self.0.bits(30,32) as u8 } 19 | } 20 | 21 | impl std::fmt::Debug for TextureParams { 22 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 23 | f.debug_struct("TextureParams") 24 | .field("dim", &(self.width(), self.height())) 25 | .field("format", &self.format().0) 26 | .field("offset", &self.offset()) 27 | .field("repeat", &(self.repeat_s(), self.repeat_t())) 28 | .field("mirror", &(self.mirror_s(), self.mirror_t())) 29 | .field("color0_transparent", &self.is_color0_transparent()) 30 | .field("texcoord_transform_mode", &self.texcoord_transform_mode()) 31 | .finish() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/nitro/animation.rs: -------------------------------------------------------------------------------- 1 | use cgmath::{Matrix3, Matrix4}; 2 | use crate::util::bits::BitField; 3 | use crate::util::cur::Cur; 4 | use crate::util::fixed::{fix16, fix32}; 5 | use crate::nitro::Name; 6 | use crate::nitro::rotation::{pivot_mat, basis_mat}; 7 | use std::ops::{Mul, Add}; 8 | use crate::errors::Result; 9 | 10 | pub struct Animation { 11 | pub name: Name, 12 | pub num_frames: u16, 13 | pub objects_curves: Vec, 14 | } 15 | 16 | pub struct TRSCurves { 17 | pub trans: [Curve; 3], 18 | pub rotation: Curve>, 19 | pub scale: [Curve; 3], 20 | } 21 | 22 | pub enum Curve { 23 | // A curve which is everywhere undefined. (Sampling at an undefined point 24 | // should produce an appropriate default value, like 0.0 for a translation). 25 | None, 26 | 27 | // A curve which has a constant value for all time. 28 | // 29 | // | 30 | // |------------- 31 | // | 32 | // |_____________ 33 | // 34 | Constant(T), 35 | 36 | // A curve defined by sampling at a fixed rate on the interval [start_frame, 37 | // end_frame]. 38 | // 39 | // | ,-, _ 40 | // | ,'| |\ ,'|`, 41 | // | | | | `-| | | 42 | // |__|_|_|_|_|_|_|_ 43 | // | | 44 | // start_frame end_frame 45 | // 46 | Samples { 47 | start_frame: u16, 48 | end_frame: u16, 49 | values: Vec, 50 | } 51 | } 52 | 53 | 54 | pub fn read_animation(base_cur: Cur, name: Name) -> Result { 55 | fields!(base_cur, animation { 56 | stamp: [u8; 4], 57 | num_frames: u16, 58 | num_objects: u16, 59 | unknown: u32, 60 | pivot_data_off: u32, 61 | basis_data_off: u32, 62 | object_offs: [u16; num_objects], 63 | }); 64 | 65 | check!(stamp == b"J\0AC")?; // wtf NUL 66 | 67 | if num_frames == 0 { 68 | bail!("ignoring animation with 0 frames"); 69 | } 70 | 71 | let pivot_data = base_cur + pivot_data_off; 72 | let basis_data = base_cur + basis_data_off; 73 | 74 | let objects_curves = object_offs.map(|curves_off| { 75 | let mut cur = base_cur + curves_off; 76 | fields!(cur, object_curves { 77 | flags: u16, 78 | dummy: u8, 79 | index: u8, 80 | end: Cur, 81 | }); 82 | cur = end; 83 | 84 | // The flags tells us if anything is anything, which of the TRS 85 | // properties are animated, and for each animated property, whether its 86 | // curve is constant or not. 87 | 88 | #[derive(Debug)] 89 | struct AnimationFlags { 90 | animated: bool, 91 | trans_animated: bool, 92 | trans_xyz_const: [bool; 3], 93 | rot_animated: bool, 94 | rot_const: bool, 95 | scale_animated: bool, 96 | scale_xyz_const: [bool; 3], 97 | } 98 | 99 | let flags = AnimationFlags { 100 | animated: flags.bits(0,1) == 0, 101 | trans_animated: flags.bits(1,3) == 0, 102 | trans_xyz_const: [ 103 | flags.bits(3,4) != 0, 104 | flags.bits(4,5) != 0, 105 | flags.bits(5,6) != 0, 106 | ], 107 | rot_animated: flags.bits(6,8) == 0, 108 | rot_const: flags.bits(8,9) != 0, 109 | scale_animated: flags.bits(9,11) == 0, 110 | scale_xyz_const: [ 111 | flags.bits(11,12) != 0, 112 | flags.bits(12,13) != 0, 113 | flags.bits(13,14) != 0, 114 | ] 115 | }; 116 | 117 | trace!("flags: {:?}", flags); 118 | 119 | let mut trs_curves = TRSCurves { 120 | trans: [Curve::None, Curve::None, Curve::None], 121 | rotation: Curve::None, 122 | scale: [Curve::None, Curve::None, Curve::None], 123 | }; 124 | 125 | if !flags.animated { 126 | return Ok(trs_curves); 127 | } 128 | 129 | //////////////// 130 | // Translation 131 | //////////////// 132 | 133 | if flags.trans_animated { 134 | for i in 0..3 { 135 | let is_const = flags.trans_xyz_const[i]; 136 | if is_const { 137 | let v = fix32(cur.next::()?, 1, 19, 12); 138 | trs_curves.trans[i as usize] = Curve::Constant(v); 139 | } else { 140 | let info = CurveInfo::from_u32(cur.next::()?)?; 141 | let off = cur.next::()?; 142 | 143 | let start_frame = info.start_frame; 144 | let end_frame = info.end_frame; 145 | let values = match info.data_width { 146 | 0 => (base_cur + off) 147 | .next_n::(info.num_samples())? 148 | .map(|x| fix32(x, 1, 19, 12)) 149 | .collect::>(), 150 | 151 | _ => (base_cur + off) 152 | .next_n::(info.num_samples())? 153 | .map(|x| fix16(x, 1, 3, 12)) 154 | .collect::>(), 155 | }; 156 | 157 | trs_curves.trans[i as usize] = Curve::Samples { 158 | start_frame, end_frame, values, 159 | }; 160 | } 161 | } 162 | } 163 | 164 | 165 | ///////////// 166 | // Rotation 167 | ///////////// 168 | 169 | // In this case, the data at base_cur + off doesn't store the actual 170 | // curve values, it stores references into pivot_data and basis_data 171 | // (see above, these were stored in the parent J0AC) where the values 172 | // are located. This lambda is used to get the actual values. 173 | let fetch_matrix = |x: u16| -> Result> { 174 | let mode = x.bits(15, 16); 175 | let idx = x.bits(0, 15) as usize; 176 | Ok(match mode { 177 | 1 => { 178 | // Pivot data, just like in the MDL model files. 179 | let (selneg, a, b) = pivot_data.nth::<(u16, u16, u16)>(idx)?; 180 | let sel = selneg.bits(0, 4); 181 | let neg = selneg.bits(4, 8); 182 | let a = fix16(a, 1, 3, 12); 183 | let b = fix16(b, 1, 3, 12); 184 | pivot_mat(sel, neg, a, b) 185 | } 186 | _ => { 187 | let d = basis_data.nth::<(u16, u16, u16, u16, u16)>(idx as usize)?; 188 | basis_mat(d) 189 | } 190 | }) 191 | 192 | }; 193 | 194 | if flags.rot_animated { 195 | if flags.rot_const { 196 | let v = cur.next::()?; 197 | let _ = cur.next::()?; // Skipped? For alignment? 198 | trs_curves.rotation = Curve::Constant(fetch_matrix(v)?); 199 | } else { 200 | let info = CurveInfo::from_u32(cur.next::()?)?; 201 | let off = cur.next::()?; 202 | 203 | let start_frame = info.start_frame; 204 | let end_frame = info.end_frame; 205 | let values = { 206 | // Do this with an explicit with_capacity + push loop 207 | // because collecting an iterator into a Result doesn't 208 | // reserve the capacity in advance. 209 | // See rust-lang/rust/#48994. 210 | let num_samples = info.num_samples(); 211 | let mut samples: Vec> = 212 | Vec::with_capacity(num_samples); 213 | for v in (base_cur + off).next_n::(num_samples)? { 214 | samples.push(fetch_matrix(v)?); 215 | } 216 | samples 217 | }; 218 | 219 | trs_curves.rotation = Curve::Samples { 220 | start_frame, end_frame, values, 221 | }; 222 | } 223 | } 224 | 225 | 226 | ////////// 227 | // Scale 228 | ////////// 229 | 230 | // These are just like translations except there are two values per 231 | // curve instead of one. I ignore the second one because I don't know 232 | // what it's for. 233 | 234 | if flags.scale_animated { 235 | for i in 0..3 { 236 | let is_const = flags.scale_xyz_const[i]; 237 | if is_const { 238 | let v = fix32(cur.next::<(u32, u32)>()?.0, 1, 19, 12); 239 | trs_curves.scale[i as usize] = Curve::Constant(v); 240 | } else { 241 | let info = CurveInfo::from_u32(cur.next::()?)?; 242 | let off = cur.next::()?; 243 | 244 | let start_frame = info.start_frame; 245 | let end_frame = info.end_frame; 246 | let values = match info.data_width { 247 | 0 => (base_cur + off) 248 | .next_n::<(u32, u32)>(info.num_samples())? 249 | .map(|(x, _)| fix32(x, 1, 19, 12)) 250 | .collect::>(), 251 | 252 | _ => (base_cur + off) 253 | .next_n::<(u16, u16)>(info.num_samples())? 254 | .map(|(x, _)| fix16(x, 1, 3, 12)) 255 | .collect::>(), 256 | }; 257 | 258 | trs_curves.scale[i as usize] = Curve::Samples { 259 | start_frame, end_frame, values, 260 | }; 261 | } 262 | } 263 | } 264 | 265 | // Finally finished! TT_TT 266 | 267 | Ok(trs_curves) 268 | 269 | }).collect::>>()?; 270 | 271 | Ok(Animation { name, num_frames, objects_curves }) 272 | } 273 | 274 | 275 | struct CurveInfo { 276 | start_frame: u16, 277 | end_frame: u16, 278 | rate: u8, 279 | data_width: u8, 280 | } 281 | 282 | impl CurveInfo { 283 | fn from_u32(x: u32) -> Result { 284 | let start_frame = x.bits(0, 16) as u16; 285 | let end_frame = x.bits(16, 28) as u16; 286 | let rate = x.bits(30, 32) as u8; 287 | let data_width = x.bits(28, 30) as u8; 288 | 289 | check!(start_frame < end_frame)?; 290 | 291 | Ok(CurveInfo { start_frame, end_frame, rate, data_width }) 292 | } 293 | 294 | fn num_samples(&self) -> usize { 295 | // XXX check this (I literally just made it up) 296 | ((self.end_frame - self.start_frame) >> self.rate) as usize 297 | } 298 | } 299 | 300 | 301 | impl Curve 302 | where T: Copy + Mul + Add { 303 | pub fn sample_at(&self, default: T, frame: u16) -> T { 304 | match *self { 305 | Curve::None => default, 306 | Curve::Constant(v) => { v }, 307 | Curve::Samples { start_frame, end_frame, ref values } => { 308 | if values.is_empty() { return default; } 309 | 310 | // XXX what's the behavior outside the defined bounds? 311 | // We're currently using "hold value". 312 | // TODO Worry about end_frame == 0? 313 | if frame <= start_frame { return values[0]; } 314 | if frame >= end_frame - 1 { return values[values.len() - 1]; } 315 | 316 | // Linearly interpolate between the two nearest values 317 | // XXX I made this up too :b 318 | let lam = (frame - start_frame) as f64 / (end_frame - 1 - start_frame) as f64; 319 | let idx = lam * (values.len() - 1) as f64; 320 | let lo_idx = idx.floor(); 321 | let hi_idx = idx.ceil(); 322 | let gamma = idx - lo_idx; 323 | values[lo_idx as usize] * (1.0 - gamma) + values[hi_idx as usize] * gamma 324 | } 325 | } 326 | } 327 | } 328 | 329 | 330 | impl TRSCurves { 331 | pub fn sample_at(&self, frame: u16) -> Matrix4 { 332 | use cgmath::{One, vec3}; 333 | 334 | let tx = self.trans[0].sample_at(0.0, frame); 335 | let ty = self.trans[1].sample_at(0.0, frame); 336 | let tz = self.trans[2].sample_at(0.0, frame); 337 | let rot = self.rotation.sample_at(Matrix3::one(), frame); 338 | let sx = self.scale[0].sample_at(1.0, frame); 339 | let sy = self.scale[1].sample_at(1.0, frame); 340 | let sz = self.scale[2].sample_at(1.0, frame); 341 | 342 | let translation = Matrix4::from_translation(vec3(tx, ty, tz)); 343 | let rotation = Matrix4::from(rot); 344 | let scale = Matrix4::from_nonuniform_scale(sx, sy, sz); 345 | 346 | translation * rotation * scale 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /src/nitro/container.rs: -------------------------------------------------------------------------------- 1 | //! Nitro containers. 2 | //! 3 | //! A Nitro container holds... well, subcontainers, but inside of _those_ are 4 | //! the model, texture, or animation files. A Nitro container is recognized by 5 | //! an identifying stamp: eg. b"BMD0". The subcontainers also contain a stamp 6 | //! eg. b"MDL0". An MDL0 contains models, a TEX0 contains textures and palettes, 7 | //! and a JNT0 contains animations. 8 | //! 9 | //! The different types of containers are supposed to only contain certain kinds 10 | //! of subcontainers. For example, a BMD0 (a "Nitro model") typically only 11 | //! contains MDL0s and TEX0s (ie. models, textures and palettes) and a BCA0 12 | //! usually only contains JNT0s (animations), but we don't do anything to 13 | //! enforce this. We'll read any kind of file we can get our hands on! 14 | 15 | use crate::errors::Result; 16 | use crate::nitro::{Model, Texture, Palette, Animation, Pattern, MaterialAnimation}; 17 | use crate::nitro::info_block; 18 | use crate::util::cur::Cur; 19 | 20 | const STAMPS: [&[u8]; 5] = [b"BMD0", b"BTX0", b"BCA0", b"BTP0", b"BTA0"]; 21 | 22 | pub struct Container { 23 | pub stamp: &'static [u8], 24 | pub file_size: u32, 25 | pub models: Vec, 26 | pub textures: Vec, 27 | pub palettes: Vec, 28 | pub animations: Vec, 29 | pub patterns: Vec, 30 | pub mat_anims: Vec, 31 | } 32 | 33 | pub fn read_container(cur: Cur) -> Result { 34 | fields!(cur, container { 35 | stamp: [u8; 4], 36 | bom: u16, 37 | version: u16, 38 | file_size: u32, 39 | header_size: u16, 40 | num_sections: u16, 41 | section_offs: [u32; num_sections], 42 | }); 43 | 44 | let stamp = 45 | match STAMPS.iter().find(|&s| s == &stamp) { 46 | Some(x) => x, 47 | None => bail!("unrecognized Nitro container: expected \ 48 | the first four bytes to be one of: BMD0, BTX0, BCA0, BTP0, BTA0"), 49 | }; 50 | 51 | check!(bom == 0xfeff)?; 52 | check!(header_size == 16)?; 53 | check!(file_size > 16)?; 54 | 55 | let mut cont = Container { 56 | stamp, file_size, models: vec![], textures: vec![], 57 | palettes: vec![], animations: vec![], patterns: vec![], 58 | mat_anims: vec![], 59 | }; 60 | 61 | for section_off in section_offs { 62 | let section_cur = cur + section_off; 63 | if let Err(e) = read_section(&mut cont, section_cur) { 64 | debug!("skipping Nitro section: {}", e); 65 | } 66 | } 67 | 68 | Ok(cont) 69 | } 70 | 71 | fn read_section(cont: &mut Container, cur: Cur) -> Result<()> { 72 | let stamp = cur.clone().next_n_u8s(4)?; 73 | match stamp { 74 | b"MDL0" => add_mdl(cont, cur), 75 | b"TEX0" => add_tex(cont, cur), 76 | b"JNT0" => add_jnt(cont, cur), 77 | b"PAT0" => add_pat(cont, cur), 78 | b"SRT0" => add_srt(cont, cur), 79 | _ => bail!("unrecognized Nitro format: expected the first four \ 80 | bytes to be one of: MDL0, TEX0, JNT0, PAT0, SRT0"), 81 | } 82 | } 83 | 84 | // An MDL is a container for models. 85 | fn add_mdl(cont: &mut Container, cur: Cur) -> Result<()> { 86 | use crate::nitro::model::read_model; 87 | 88 | fields!(cur, MDL0 { 89 | stamp: [u8; 4], 90 | section_size: u32, 91 | end: Cur, 92 | }); 93 | check!(stamp == b"MDL0")?; 94 | 95 | for (off, name) in info_block::read::(end)? { 96 | match read_model(cur + off, name) { 97 | Ok(model) => cont.models.push(model), 98 | Err(e) => { 99 | error!("error on model {}: {}", name, e); 100 | } 101 | } 102 | } 103 | Ok(()) 104 | } 105 | 106 | // This work is already done for us in read_tex; see that module for why. 107 | fn add_tex(cont: &mut Container, cur: Cur) -> Result<()> { 108 | use crate::nitro::tex::read_tex; 109 | 110 | let (textures, palettes) = read_tex(cur)?; 111 | cont.textures.extend(textures.into_iter()); 112 | cont.palettes.extend(palettes.into_iter()); 113 | Ok(()) 114 | } 115 | 116 | 117 | // A JNT is a container for animations. 118 | fn add_jnt(cont: &mut Container, cur: Cur) -> Result<()> { 119 | use crate::nitro::animation::read_animation; 120 | 121 | fields!(cur, JNT0 { 122 | stamp: [u8; 4], 123 | section_size: u32, 124 | end: Cur, 125 | }); 126 | check!(stamp == b"JNT0")?; 127 | 128 | for (off, name) in info_block::read::(end)? { 129 | match read_animation(cur + off, name) { 130 | Ok(animation) => cont.animations.push(animation), 131 | Err(e) => { 132 | error!("error on animation {}: {}", name, e); 133 | } 134 | } 135 | } 136 | Ok(()) 137 | } 138 | 139 | // A PAT is a container for pattern animations. 140 | fn add_pat(cont: &mut Container, cur: Cur) -> Result<()> { 141 | use crate::nitro::pattern::read_pattern; 142 | 143 | fields!(cur, PAT0 { 144 | stamp: [u8; 4], 145 | section_size: u32, 146 | end: Cur, 147 | }); 148 | check!(stamp == b"PAT0")?; 149 | 150 | for (off, name) in info_block::read::(end)? { 151 | match read_pattern(cur + off, name) { 152 | Ok(pattern) => cont.patterns.push(pattern), 153 | Err(e) => { 154 | error!("error on pattern {}: {}", name, e); 155 | } 156 | } 157 | } 158 | Ok(()) 159 | } 160 | 161 | // An SRT is a container for material animations. 162 | fn add_srt(cont: &mut Container, cur: Cur) -> Result<()> { 163 | use crate::nitro::material_animation::read_mat_anim; 164 | 165 | fields!(cur, SRT0 { 166 | stamp: [u8; 4], 167 | section_size: u32, 168 | end: Cur, 169 | }); 170 | check!(stamp == b"SRT0")?; 171 | 172 | for (off, name) in info_block::read::(end)? { 173 | match read_mat_anim(cur + off, name) { 174 | Ok(mat_anim) => cont.mat_anims.push(mat_anim), 175 | Err(e) => { 176 | error!("error on material animation {}: {}", name, e); 177 | } 178 | } 179 | } 180 | Ok(()) 181 | } 182 | -------------------------------------------------------------------------------- /src/nitro/info_block.rs: -------------------------------------------------------------------------------- 1 | //! Common pattern in Nitro files. 2 | //! 3 | //! An info block is a common structure that contains a sequence of 4 | //! data-name pairs, where the data is usually an offset to the location 5 | //! of some struct with the given name. 6 | 7 | use crate::errors::Result; 8 | use crate::nitro::name::Name; 9 | use std::fmt::Debug; 10 | use std::iter::Zip; 11 | use crate::util::cur::Cur; 12 | use crate::util::view::{View, Viewable}; 13 | 14 | pub type Iterator<'a, T> = Zip, View<'a, Name>>; 15 | 16 | /// Returns an iterator over (`T`, name) pairs in an info block. 17 | pub fn read(cur: Cur) -> Result> where 18 | T: Viewable + Debug 19 | { 20 | fields!(cur, info_block { 21 | dummy: u8, 22 | count: u8, 23 | header_size: u16, 24 | 25 | unknown_subheader_size: u16, 26 | unknown_section_size: u16, 27 | unknown_constant: u32, 28 | unknown_data: [u32; count], 29 | 30 | size_of_datum: u16, 31 | data_section_size: u16, 32 | data: [T; count], 33 | 34 | names: [Name; count], 35 | }); 36 | 37 | check!(dummy == 0)?; 38 | check!(size_of_datum as usize == ::size())?; 39 | 40 | Ok(data.zip(names)) 41 | } 42 | -------------------------------------------------------------------------------- /src/nitro/material_animation.rs: -------------------------------------------------------------------------------- 1 | // HIGHLY INCOMPLETE!!! 2 | 3 | use super::animation::Curve; 4 | use crate::util::cur::Cur; 5 | use crate::util::view::Viewable; 6 | use crate::util::fixed::fix16; 7 | use cgmath::{Matrix4, vec3}; 8 | use crate::nitro::Name; 9 | use crate::errors::Result; 10 | use super::info_block; 11 | 12 | /// Material animation. Does things like UV matrix animation. 13 | pub struct MaterialAnimation { 14 | pub name: Name, 15 | pub num_frames: u16, 16 | pub tracks: Vec, 17 | } 18 | 19 | /// Targets one material in a model and animates it. 20 | pub struct MaterialTrack { 21 | pub name: Name, 22 | pub channels: [MaterialChannel; 5], 23 | } 24 | 25 | /// Targets one material property for animation???? 26 | pub struct MaterialChannel { 27 | pub num_frames: u16, 28 | pub target: MatChannelTarget, 29 | pub curve: Curve, 30 | } 31 | 32 | #[derive(Copy, Clone, PartialEq, Eq)] 33 | pub enum MatChannelTarget { 34 | TranslationU, 35 | TranslationV, 36 | Unknown, 37 | } 38 | 39 | pub fn read_mat_anim(cur: Cur, name: Name) -> Result { 40 | debug!("material animation: {:?}", name); 41 | 42 | fields!(cur, mat_anim { 43 | _unknown: [u8; 4], // b'M\0AT' ?? 44 | num_frames: u16, 45 | unknown: u16, 46 | end: Cur, 47 | }); 48 | 49 | let tracks = info_block::read::<[ChannelData; 5]>(end)? 50 | .map(|(chans, name)| { 51 | let c0 = read_channel(cur, chans[0], MatChannelTarget::Unknown)?; 52 | let c1 = read_channel(cur, chans[1], MatChannelTarget::Unknown)?; 53 | let c2 = read_channel(cur, chans[2], MatChannelTarget::Unknown)?; 54 | let c3 = read_channel(cur, chans[3], MatChannelTarget::TranslationU)?; 55 | let c4 = read_channel(cur, chans[4], MatChannelTarget::TranslationV)?; 56 | let channels = [c0, c1, c2, c3, c4]; 57 | Ok(MaterialTrack { name, channels }) 58 | }) 59 | .collect::>>()?; 60 | 61 | Ok(MaterialAnimation { 62 | name, 63 | num_frames, 64 | tracks, 65 | }) 66 | } 67 | 68 | #[derive(Debug, Copy, Clone)] 69 | struct ChannelData { 70 | num_frames: u16, 71 | flags: u8, // some sort of flags 72 | offset: u32, // meaning appears to depend on flags 73 | } 74 | impl Viewable for ChannelData { 75 | fn size() -> usize { 8 } 76 | fn view(buf: &[u8]) -> ChannelData { 77 | let mut cur = Cur::new(buf); 78 | let num_frames = cur.next::().unwrap(); 79 | let _dummy = cur.next::().unwrap(); // always 0? 80 | let flags = cur.next::().unwrap(); 81 | let offset = cur.next::().unwrap(); 82 | ChannelData { num_frames, flags, offset } 83 | } 84 | } 85 | 86 | fn read_channel(base_cur: Cur, data: ChannelData, target: MatChannelTarget) -> Result { 87 | if !((data.flags == 16) && target != MatChannelTarget::Unknown) { 88 | // That's the only case I understand; otherwise bail. 89 | return Ok(MaterialChannel { 90 | num_frames: 0, 91 | target, 92 | curve: Curve::None, 93 | }); 94 | } 95 | 96 | let values = (base_cur + data.offset) 97 | .next_n::(data.num_frames as usize)? 98 | .map(|n| fix16(n, 1, 10, 5)) 99 | .collect::>(); 100 | let curve = Curve::Samples { 101 | start_frame: 0, 102 | end_frame: data.num_frames, 103 | values, 104 | }; 105 | 106 | Ok(MaterialChannel { 107 | num_frames: data.num_frames, 108 | target, 109 | curve, 110 | }) 111 | } 112 | 113 | impl MaterialTrack { 114 | pub fn eval_uv_mat(&self, frame: u16) -> Matrix4 { 115 | let (mut u, mut v) = (0.0, 0.0); 116 | for chan in &self.channels { 117 | if chan.target == MatChannelTarget::TranslationU { 118 | u = chan.curve.sample_at(u, frame); 119 | } 120 | if chan.target == MatChannelTarget::TranslationV { 121 | v = chan.curve.sample_at(v, frame); 122 | } 123 | } 124 | Matrix4::from_translation(vec3(u, v, 0.0)) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/nitro/mod.rs: -------------------------------------------------------------------------------- 1 | //! Load Nitro files. 2 | //! 3 | //! Nitro is the SDK used for many Nintendo DS games. These modules parse 4 | //! the binary format for Nitro files into domain objects and provide other 5 | //! tools specific to these formats. 6 | //! 7 | //! Partial documentation on these formats can be found here. 8 | //! 9 | //! * 10 | //! * 11 | //! * 12 | //! 13 | //! The code in this module should be more complete. 14 | 15 | pub mod model; 16 | pub mod tex; 17 | pub mod animation; 18 | pub mod container; 19 | pub mod name; 20 | pub mod render_cmds; 21 | pub mod pattern; 22 | pub mod material_animation; 23 | mod info_block; 24 | mod rotation; 25 | 26 | pub use self::name::Name; 27 | pub use self::container::Container; 28 | pub use self::model::Model; 29 | pub use self::tex::Texture; 30 | pub use self::tex::Palette; 31 | pub use self::animation::Animation; 32 | pub use self::pattern::Pattern; 33 | pub use self::material_animation::MaterialAnimation; 34 | -------------------------------------------------------------------------------- /src/nitro/name.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Write}; 2 | use crate::util::view::Viewable; 3 | 4 | /// Sixteen-byte NUL-padded ASCII(?) string, used as human-readable names 5 | /// in Nitro files. 6 | #[derive(Copy, Clone, PartialEq, Eq, Hash)] 7 | pub struct Name(pub [u8; 16]); 8 | 9 | 10 | impl Name { 11 | pub fn from_bytes(buf: &[u8]) -> Name { 12 | let mut name = Name([0; 16]); 13 | name.0.copy_from_slice(buf); 14 | name 15 | } 16 | 17 | /// Returns an object that formats the name as a non-empty string 18 | /// of letters, digits, and underscores. 19 | pub fn print_safe(&self) -> NameSafePrinter { 20 | NameSafePrinter(self) 21 | } 22 | } 23 | 24 | impl Viewable for Name { 25 | fn size() -> usize { 16 } 26 | fn view(buf: &[u8]) -> Name { 27 | Name::from_bytes(buf) 28 | } 29 | } 30 | 31 | impl fmt::Display for Name { 32 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 33 | for &b in trim_trailing_nuls(&self.0[..]) { 34 | // Convert non-printable characters to periods (which is what 35 | // hex editors usually do). 36 | f.write_char(if b < 0x20 { '.' } else { b as char })?; 37 | } 38 | Ok(()) 39 | } 40 | } 41 | 42 | impl fmt::Debug for Name { 43 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 44 | let trimmed = trim_trailing_nuls(&self.0); 45 | 46 | // Print "normal" strings without quotes or escaping. 47 | 48 | let normal = 49 | !trimmed.is_empty() && 50 | trimmed.iter().all(|&b| { 51 | let is_letter_or_digit = 52 | (b >= b'a' && b <= b'z') || 53 | (b >= b'A' && b <= b'Z') || 54 | (b >= b'0' && b <= b'9') || 55 | b == b'_' || b == b'-'; 56 | is_letter_or_digit 57 | }); 58 | 59 | if normal { 60 | for &b in trimmed { 61 | f.write_char(b as char)?; 62 | } 63 | } else { 64 | f.write_char('"')?; 65 | for &b in trim_trailing_nuls(&self.0[..]) { 66 | for c in (b as char).escape_default() { 67 | f.write_char(c)?; 68 | } 69 | } 70 | f.write_char('"')?; 71 | } 72 | Ok(()) 73 | } 74 | } 75 | 76 | /// Wrapper produces by `Name::print_safe`. 77 | pub struct NameSafePrinter<'a>(pub &'a Name); 78 | 79 | impl<'a> fmt::Display for NameSafePrinter<'a> { 80 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 81 | let trimmed = trim_trailing_nuls(&(self.0).0[..]); 82 | 83 | if trimmed.len() == 0 { 84 | f.write_char('_')?; 85 | return Ok(()); 86 | } 87 | 88 | for &b in trimmed { 89 | let is_letter_or_digit = 90 | (b >= b'a' && b <= b'z') || 91 | (b >= b'A' && b <= b'Z') || 92 | (b >= b'0' && b <= b'9'); 93 | let c = if is_letter_or_digit { b as char } else { '_' }; 94 | f.write_char(c)?; 95 | } 96 | Ok(()) 97 | } 98 | } 99 | 100 | /// Slice off any NUL bytes from the end of `buf`. 101 | fn trim_trailing_nuls(mut buf: &[u8]) -> &[u8] { 102 | while let Some((&0, rest)) = buf.split_last() { 103 | buf = rest; 104 | } 105 | buf 106 | } 107 | -------------------------------------------------------------------------------- /src/nitro/pattern.rs: -------------------------------------------------------------------------------- 1 | use crate::util::cur::Cur; 2 | use crate::nitro::Name; 3 | use crate::errors::Result; 4 | use super::info_block; 5 | 6 | /// A pattern animation changes the images that the materials of a model use as 7 | /// it plays. 8 | pub struct Pattern { 9 | pub name: Name, 10 | pub num_frames: u16, 11 | pub texture_names: Vec, 12 | pub palette_names: Vec, 13 | pub material_tracks: Vec, 14 | } 15 | 16 | /// Gives the keyframes at which an image should change. 17 | pub struct PatternTrack { 18 | pub name: Name, 19 | pub keyframes: Vec, 20 | } 21 | 22 | pub struct PatternKeyframe { 23 | pub frame: u16, 24 | /// Index into the Pattern.texture_names array. 25 | pub texture_idx: u8, 26 | /// Index into the Pattern.palette_names array. 27 | pub palette_idx: u8, 28 | } 29 | 30 | pub fn read_pattern(cur: Cur, name: Name) -> Result { 31 | debug!("pattern: {:?}", name); 32 | 33 | fields!(cur, pattern { 34 | _unknown: [u8; 4], 35 | num_frames: u16, 36 | num_texture_names: u8, 37 | num_palette_names: u8, 38 | texture_names_off: u16, 39 | palette_names_off: u16, 40 | end: Cur, 41 | }); 42 | 43 | let texture_names = (cur + texture_names_off) 44 | .next_n::(num_texture_names as usize)? 45 | .collect::>(); 46 | 47 | let palette_names = (cur + palette_names_off) 48 | .next_n::(num_palette_names as usize)? 49 | .collect::>(); 50 | 51 | let material_tracks = info_block::read::<(u32, u16, u16)>(end)? 52 | .map(|((num_keyframes, _, off), name)| { 53 | let keyframes = 54 | (cur + off) 55 | .next_n::<(u16, u8, u8)>(num_keyframes as usize)? 56 | .map(|(frame, texture_idx, palette_idx)| { 57 | if texture_idx >= num_texture_names || palette_idx >= num_palette_names { 58 | bail!("OOB index in pattern animation"); 59 | } 60 | 61 | Ok(PatternKeyframe { frame, texture_idx, palette_idx }) 62 | }) 63 | .collect::>>()?; 64 | 65 | Ok(PatternTrack { name, keyframes }) 66 | }) 67 | .collect::>>()?; 68 | 69 | Ok(Pattern { 70 | name, 71 | num_frames, 72 | texture_names, 73 | palette_names, 74 | material_tracks, 75 | }) 76 | } 77 | 78 | impl PatternTrack { 79 | pub fn sample(&self, frame: u16) -> (u8, u8) { 80 | // Linear search for the first keyframe after the given frame. The 81 | // desired keyframe is the one before that. 82 | let next_pos = self.keyframes.iter().position(|key| key.frame > frame); 83 | let keyframe = match next_pos { 84 | Some(0) => &self.keyframes[0], 85 | Some(n) => &self.keyframes[n - 1], 86 | None => &self.keyframes[self.keyframes.len() - 1], 87 | }; 88 | (keyframe.texture_idx, keyframe.palette_idx) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/nitro/render_cmds.rs: -------------------------------------------------------------------------------- 1 | //! Render commands for model files. 2 | 3 | use crate::errors::Result; 4 | use crate::util::cur::Cur; 5 | 6 | pub struct SkinTerm { 7 | pub weight: f32, 8 | pub stack_pos: u8, 9 | pub inv_bind_idx: u8, 10 | } 11 | 12 | /// A render command is analyzed into zero or more render ops (think micro-ops 13 | /// to a CPU instruction). 14 | pub enum Op { 15 | /// cur_matrix = matrix_stack[stack_pos] 16 | LoadMatrix { stack_pos: u8 }, 17 | /// matrix_stack[stack_pos] = cur_matrix 18 | StoreMatrix { stack_pos: u8 }, 19 | /// cur_matrix = cur_matrix * object_matrices[object_idx] 20 | MulObject { object_idx: u8 }, 21 | /// cur_matrix = ∑_{term} 22 | /// term.weight * 23 | /// matrix_stack[term.stack_pos] * 24 | /// inv_bind_matrices[term.inv_bind_idx] 25 | /// (ie. the skinning equation) 26 | Skin { terms: Box<[SkinTerm]> }, 27 | /// cur_matrix = cur_matrix * scale(up_scale) 28 | ScaleUp, 29 | /// cur_matrix = cur_matrix * scale(model.down_scale) 30 | ScaleDown, 31 | 32 | /// Bind materials[material_idx] for subsequent draw calls. 33 | BindMaterial { material_idx: u8 }, 34 | 35 | /// Draw pieces[piece_idx]. 36 | Draw { piece_idx: u8 }, 37 | } 38 | 39 | /// Parses a bytestream of render commands into a list of render ops. 40 | pub fn parse_render_cmds(mut cur: Cur) -> Result> { 41 | trace!("render commands @ {:#x}", cur.pos()); 42 | 43 | let mut ops: Vec = vec![]; 44 | 45 | loop { 46 | let (opcode, params) = next_opcode_params(&mut cur)?; 47 | trace!("cmd {:#2x} {:?}", opcode, params); 48 | 49 | match opcode { 50 | 0x00 => { 51 | // NOP 52 | } 53 | 0x01 => { 54 | // End render commands 55 | return Ok(ops); 56 | } 57 | 0x02 => { 58 | // Unknown 59 | // Here's what I know about it: 60 | // * there is always one of them in a model, after the initial matrix 61 | // stack setup but before the 0x03/0x04/0x05 command sequences 62 | // * the first parameter is num_objects - 1 (ie. the index of the last 63 | // object) 64 | // * the second parameter is always 1; if this 1 is changed to a zero 65 | // using a debugger, the model is not drawn (this is probably why 66 | // other people have called this "visibility") 67 | // * running it emits no GPU commands 68 | } 69 | 0x03 => { 70 | // Load a matrix from the stack 71 | ops.push(Op::LoadMatrix { stack_pos: params[0] }); 72 | } 73 | 0x04 | 0x24 | 0x44 => { 74 | // Bind a material 75 | ops.push(Op::BindMaterial { material_idx: params[0] }); 76 | } 77 | 0x05 => { 78 | // Draw a piece of the model 79 | ops.push(Op::Draw { piece_idx: params[0] }); 80 | } 81 | 0x06 | 0x26 | 0x46 | 0x66 => { 82 | // Multiply the current matrix by an object matrix, possibly 83 | // loading a matrix from the stack beforehand, and possibly 84 | // storing the result to a stack location after. 85 | let object_idx = params[0]; 86 | 87 | let _parent_id = params[1]; 88 | let _unknown = params[2]; 89 | 90 | let (store_pos, load_pos) = match opcode { 91 | 0x06 => (None, None), 92 | 0x26 => (Some(params[3]), None), 93 | 0x46 => (None, Some(params[3])), 94 | 0x66 => (Some(params[3]), Some(params[4])), 95 | _ => unreachable!(), 96 | }; 97 | 98 | if let Some(stack_pos) = load_pos { 99 | ops.push(Op::LoadMatrix { stack_pos }); 100 | } 101 | ops.push(Op::MulObject { object_idx }); 102 | if let Some(stack_pos) = store_pos { 103 | ops.push(Op::StoreMatrix { stack_pos }); 104 | } 105 | } 106 | 0x09 => { 107 | // Creates a matrix from the skinning equation and stores it to 108 | // a stack slot. The skinning equation is 109 | // 110 | // ∑ weight * matrix_stack[stack_pos] * inv_binds[inv_bind_idx] 111 | // 112 | // Note that vertices to which a skinning matrix are applied are 113 | // given in model space -- the inverse bind matrices bring it 114 | // into the local space of each of its influencing objects. 115 | // Normal vertices are given directly in the local space of the 116 | // object that gets applied to them. 117 | let store_pos = params[0]; 118 | 119 | let num_terms = params[1] as usize; 120 | let mut i = 2; 121 | let terms = (0..num_terms) 122 | .map(|_| { 123 | let stack_pos = params[i]; 124 | let inv_bind_idx = params[i + 1]; 125 | let weight = params[i + 2] as f32 / 256.0; // denormalize 126 | i += 3; 127 | 128 | SkinTerm { weight, stack_pos, inv_bind_idx } 129 | }) 130 | .collect::>() 131 | .into_boxed_slice(); 132 | 133 | ops.push(Op::Skin { terms }); 134 | ops.push(Op::StoreMatrix { stack_pos: store_pos }); 135 | } 136 | 0x0b => { 137 | // Scale up by a per-model constant. 138 | ops.push(Op::ScaleUp); 139 | } 140 | 0x2b => { 141 | // Scale down by a per-model constant. 142 | ops.push(Op::ScaleDown); 143 | } 144 | _ => { 145 | debug!("skipping unknown render command {:#x}", opcode); 146 | } 147 | } 148 | } 149 | } 150 | 151 | /// Fetch the next opcode and its parameters from the bytestream. 152 | fn next_opcode_params<'a>(cur: &mut Cur<'a>) -> Result<(u8, &'a [u8])> { 153 | let opcode = cur.next::()?; 154 | 155 | // The only variable-length command 156 | if opcode == 0x09 { 157 | // 1 byte + 1 byte (count) + count u8[3]s 158 | let count = cur.nth::(1)?; 159 | let params_len = 1 + 1 + 3 * count; 160 | let params = cur.next_n_u8s(params_len as usize)?; 161 | return Ok((opcode, params)); 162 | } 163 | 164 | let params_len = match opcode { 165 | 0x00 => 0, 166 | 0x01 => 0, 167 | 0x02 => 2, 168 | 0x03 => 1, 169 | 0x04 => 1, 170 | 0x05 => 1, 171 | 0x06 => 3, 172 | 0x07 => 1, 173 | 0x08 => 1, 174 | 0x0b => 0, 175 | 0x0c => 2, 176 | 0x0d => 2, 177 | 0x24 => 1, 178 | 0x26 => 4, 179 | 0x2b => 0, 180 | 0x40 => 0, 181 | 0x44 => 1, 182 | 0x46 => 4, 183 | 0x47 => 2, 184 | 0x66 => 5, 185 | 0x80 => 0, 186 | _ => bail!("unknown render command opcode: {:#x}", opcode), 187 | }; 188 | let params = cur.next_n_u8s(params_len)?; 189 | Ok((opcode, params)) 190 | } 191 | -------------------------------------------------------------------------------- /src/nitro/rotation.rs: -------------------------------------------------------------------------------- 1 | //! Decodes formats for rotations. 2 | //! 3 | //! Rotations are always stored as a 3x3 matrix (probably for speed; the NDS 4 | //! probably wasn't fast enough to convert Euler angles or quaternions to 5 | //! matrices on the fly). This also means that a "rotation" matrix might not 6 | //! actually be a rotation (ie. orthogonal of determinant +1). 7 | 8 | use cgmath::{Matrix3, vec3}; 9 | use crate::util::bits::BitField; 10 | use crate::util::fixed::fix16; 11 | 12 | pub fn pivot_mat(select: u16, neg: u16, a: f64, b: f64) -> Matrix3 { 13 | if select >= 9 { 14 | // Does this actually happen? 15 | debug!("pivot with select={} actually happened! :O", select); 16 | return Matrix3::new( 17 | -a, 0.0, 0.0, 18 | 0.0, 0.0, 0.0, 19 | 0.0, 0.0, 0.0, 20 | ); 21 | } 22 | 23 | let o = if neg.bits(0,1) == 0 { 1.0 } else { -1.0 }; 24 | let c = if neg.bits(1,2) == 0 { b } else { -b }; 25 | let d = if neg.bits(2,3) == 0 { a } else { -a }; 26 | 27 | // Consider eg. a = cos θ, b = sin θ. 28 | // Nb. the pattern here. 29 | match select { 30 | 0 => Matrix3::new( o , 0.0, 0.0, 0.0, a , b , 0.0, c , d ), 31 | 1 => Matrix3::new(0.0, o , 0.0, a , 0.0, b , c , 0.0, d ), 32 | 2 => Matrix3::new(0.0, 0.0, o , a , b , 0.0, c , d , 0.0), 33 | 34 | 3 => Matrix3::new(0.0, a , b , o , 0.0, 0.0, 0.0, c , d ), 35 | 4 => Matrix3::new( a , 0.0, b , 0.0, o , 0.0, c , 0.0, d ), 36 | 5 => Matrix3::new( a , b , 0.0, 0.0, 0.0, o , c , d , 0.0), 37 | 38 | 6 => Matrix3::new(0.0, a , b , 0.0, c , d , o , 0.0, 0.0), 39 | 7 => Matrix3::new( a , 0.0, b , c , 0.0, d , 0.0, o , 0.0), 40 | 8 => Matrix3::new( a , b , 0.0, c , d , 0.0, 0.0, 0.0, o ), 41 | 42 | _ => unreachable!(), 43 | } 44 | } 45 | 46 | pub fn basis_mat((in0,in1,in2,in3,in4): (u16,u16,u16,u16,u16)) -> Matrix3 { 47 | // Credit for figuring this out goes to MKDS Course Modifier. 48 | // 49 | // Braindump for this function follows: 50 | // 51 | // The matrix is specified by giving an orthonormal right-hand basis to transform 52 | // to. This is 9 numbers (3 entries for 3 vectors). Since the basis is orthonormal 53 | // and right-handed, 3 of these are redundant (given by the cross-product), so we 54 | // only need to store 6 numbers. Since the vectors are unit length, the components 55 | // are all <1 in magnitude (=+1 does not appear to be possible?) and the DS uses 56 | // 12 bits for the fractional part of its numbers, so we need 13 bits (+1 for sign) 57 | // for each number. 13 * 6 = 78 bits. Give one of the numbers an extra 2 bits, and 58 | // we get exactly five u16s. 59 | // 60 | // The obvious way to store each 14-bit number is packed densely one after the other. 61 | // But extracting them this way requires a different mask/shift/accumulate for every 62 | // one. Instead, five of the numbers are stored in the high 13 bits of each u16 and 63 | // the last number is built by xoring the low three bits together in sequence. This 64 | // can be done in a simple loop. 65 | // 66 | // in4 is handled strangely. The first three entries come from (in0,in1,in2), but 67 | // the last entry, is made from a sequence of xors 68 | // (in4 << 12) | (in0 << 9) | (in1 << 6) | (in2 << 3) | in3 69 | // which is where the weird permutation 4,0,1,2,3 of the input comes from. I don't 70 | // have a good explanation for this part :( 71 | // 72 | // I'd like to check this code against the disassembly from a DS ROM, but I can't 73 | // find a way to locate it with the debugger. 74 | 75 | let input = [in4, in0, in1, in2, in3]; 76 | let mut out = [0u16; 6]; 77 | 78 | for i in 0..5 { 79 | out[i] = input[i].bits(3,16); 80 | out[5] = (out[5] << 3) | input[i].bits(0,3); 81 | } 82 | 83 | let f = |x| fix16(x, 1, 0, 12); 84 | let a = vec3(f(out[1]), f(out[2]), f(out[3])); 85 | let b = vec3(f(out[4]), f(out[0]), f(out[5])); 86 | let c = a.cross(b); 87 | 88 | Matrix3::from_cols(a, b, c) 89 | } 90 | -------------------------------------------------------------------------------- /src/nitro/tex.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | use crate::nitro::Name; 3 | use crate::nitro::info_block; 4 | use crate::errors::Result; 5 | use crate::util::cur::Cur; 6 | use crate::nds::{TextureParams, TextureFormat}; 7 | 8 | pub struct Texture { 9 | pub name: Name, 10 | pub params: TextureParams, 11 | pub data1: Vec, 12 | /// Only used by block-compressed textures. 13 | pub data2: Vec, 14 | } 15 | 16 | pub struct Palette { 17 | pub name: Name, 18 | pub off: u32, 19 | /// Since we don't know how large a palette is 20 | pub pal_block: Rc>, 21 | } 22 | 23 | pub fn read_tex(cur: Cur) -> Result<(Vec, Vec)> { 24 | fields!(cur, TEX0 { 25 | stamp: [u8; 4], 26 | section_size: u32, 27 | _unknown: u32, 28 | tex_block_len_shr_3: u16, 29 | texture_off: u16, 30 | _unknown: u32, 31 | tex_block_off: u32, 32 | _unknown: u32, 33 | compressed_block_len_shr_3: u16, 34 | compressed_info_off: u16, 35 | _unknown: u32, 36 | compressed1_block_off: u32, 37 | compressed2_block_off: u32, 38 | _unknown: u32, 39 | pal_block_len_shr_3: u16, 40 | _unknown: u16, 41 | palette_off: u32, 42 | pal_block_off: u32, 43 | }); 44 | 45 | check!(stamp == b"TEX0")?; 46 | 47 | // Stores palette data. 48 | let pal_block_len = (pal_block_len_shr_3 as usize) << 3; 49 | let pal_block = (cur + pal_block_off).next_n_u8s(pal_block_len)?.to_vec().into_boxed_slice(); 50 | let pal_block = Rc::new(pal_block); 51 | 52 | // Stores regular texture data. 53 | let tex_cur = cur + tex_block_off; 54 | // Stores texture data for block-compressed formats. One block compressed 55 | // texture consists of two parallel arrays, one in the compressed1 block and 56 | // one half the length in the compressed2 block. 57 | let compressed1_cur = cur + compressed1_block_off; 58 | let compressed2_cur = cur + compressed2_block_off; 59 | 60 | 61 | let textures = 62 | info_block::read::<(u32, u32)>(cur + texture_off)? 63 | .map(|((params, _unknown), name)| { 64 | debug!("texture: {:?}", name); 65 | 66 | let params = TextureParams(params); 67 | trace!("params: {:?}", params); 68 | let off = params.offset(); 69 | let len = params.format().byte_len((params.width(), params.height())); 70 | 71 | let (data1, data2); 72 | match params.format() { 73 | TextureFormat(5) => { 74 | data1 = (compressed1_cur + off).next_n_u8s(len)?.to_vec(); 75 | data2 = (compressed2_cur + off/2).next_n_u8s(len/2)?.to_vec(); 76 | }, 77 | _ => { 78 | data1 = (tex_cur + off).next_n_u8s(len)?.to_vec(); 79 | data2 = vec![]; 80 | } 81 | } 82 | 83 | Ok(Texture { name, params, data1, data2 }) 84 | }) 85 | .collect::>>()?; 86 | 87 | let palettes = 88 | info_block::read::<(u16, u16)>(cur + palette_off)? 89 | .map(|((off_shr_3, _unknown), name)| { 90 | debug!("palette: {:?}", name); 91 | let off = (off_shr_3 as u32) << 3; 92 | Palette { name, off, pal_block: Rc::clone(&pal_block) } 93 | }) 94 | .collect::>(); 95 | 96 | 97 | Ok((textures, palettes)) 98 | } 99 | -------------------------------------------------------------------------------- /src/skeleton/mod.rs: -------------------------------------------------------------------------------- 1 | //! Build a skeleton for a model. 2 | //! 3 | //! A brief review of how skinning works. The skeleton consists of a _tree of 4 | //! joints_ and a _skin vertex_ for every vertex. 5 | //! 6 | //! Each joint in the tree has a local-to-parent transform. The composition of 7 | //! the local-to-parent of a joint with its parent's local-to-parent transform, 8 | //! and so on all the way up the tree is a the joint's local-to-world transform. 9 | //! Example: 10 | //! 11 | //! A A's local-to-parent = a, B's = b, etc. 12 | //! / \ D's local-to-world = a c d 13 | //! B C D's world-to-local = d^{-1} c^{-1} a^{-1} 14 | //! / 15 | //! D 16 | //! 17 | //! A skin vertex consists of a list of influences, each influence containing a 18 | //! joint that influences the vertex and a weight controlling how great the 19 | //! influence is. The vertex's final position is determined by the skinning 20 | //! equation 21 | //! 22 | //! (vertex final pos) = 23 | //! ∑_{influence} 24 | //! (weight) (pose local-to-world) (rest world-to-local) (vertex rest pos) 25 | //! 26 | //! See eg. section X in the COLLADA 1.4 spec for more details. 27 | //! 28 | //! In NSBMD models, this skeleton data has been compiled into an imperative 29 | //! list of rendering commands. Example for the above tree: 30 | //! 31 | //! multiply cur matrix by a 32 | //! multiply cur matrix by c 33 | //! multiply cur matrix by d 34 | //! store to stack slot 1 35 | //! ... later ... 36 | //! restore stack slot 1 37 | //! draw vertices for D 38 | //! etc. 39 | //! 40 | //! Our job here is to essentially reverse this procedure, reconstructing the 41 | //! skeleton data from this list of imperative commands. 42 | //! 43 | //! We do this by doing abstract interpretation of the rendering commands, 44 | //! recording what symbolic matrix is in which slot at each point in time. So we 45 | //! would know that slot 1 contained a c d, and we would then use that to 46 | //! reconstruct a chain of joints A -> C -> D and use D as the joint for 47 | //! vertices drawn after restoring stack slot 1. 48 | //! 49 | //! This is the simplest case. A more detailed analysis of the math is in 50 | //! joint_tree.rs. 51 | 52 | pub mod symbolic_matrix; 53 | mod vertex_record; 54 | mod joint_tree; 55 | 56 | pub use self::symbolic_matrix::{SMatrix, AMatrix}; 57 | 58 | use cgmath::Matrix4; 59 | use crate::nitro::Model; 60 | use crate::util::tree::{Tree, NodeIdx}; 61 | 62 | /// Skeleton (or skin) for a model. 63 | pub struct Skeleton { 64 | pub tree: Tree, 65 | pub root: NodeIdx, 66 | pub max_num_weights: u8, // max weights on any vertex 67 | 68 | pub weights: Vec, // weights for all verts packed together 69 | verts: Vec, // verts[vi] points to the weights for vertex vi in weights 70 | } 71 | 72 | pub struct Joint { 73 | pub local_to_parent: Transform, 74 | // Ie. inverse bind matrix. Cached for convenience. 75 | pub rest_world_to_local: Matrix4, 76 | } 77 | 78 | // A joint's local-to-parent transform. 79 | pub enum Transform { 80 | /// Same value as an SMatrix (usually an object matrix). 81 | SMatrix(SMatrix), 82 | /// If the graph of joints is a forest and not a tree, it may be necessary 83 | /// to insert a node to turn it into a tree. That node (called the universal 84 | /// root) will have this transform. Treat as the identity. 85 | Root, 86 | } 87 | 88 | #[derive(Copy, Clone)] 89 | pub struct Weight { 90 | pub weight: f32, 91 | pub joint: NodeIdx, 92 | } 93 | 94 | /// Points to a slice skel.weights[start..start+len]. 95 | /// start is the low 24 bits, len is the high 8 bits. 96 | #[derive(Copy, Clone)] 97 | struct WeightsOfs(u32); 98 | 99 | 100 | impl Skeleton { 101 | pub fn vert_weights(&self, vi: usize) -> &[Weight] { 102 | let ofs = self.verts[vi]; 103 | let start = (ofs.0 & 0xffffff) as usize; 104 | let len = (ofs.0 >> 24) as usize; 105 | &self.weights[start .. start + len] 106 | } 107 | 108 | pub fn build(model: &Model, objects: &[Matrix4]) -> Skeleton { 109 | // First play back rendering commands recording the symbolic value of 110 | // the matrix applied to every vertex. 111 | let vr = self::vertex_record::VertexRecord::build_for_model(model); 112 | // Then build a joint tree for those matrices. 113 | let skel = self::joint_tree::build_skeleton(&vr, model, objects); 114 | skel 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/skeleton/symbolic_matrix.rs: -------------------------------------------------------------------------------- 1 | //! Symbolic matrices and their algebra. 2 | 3 | use std::ops; 4 | 5 | /// The simplest possible symbolic matrix, out of which the others are built. 6 | #[derive(Copy, Clone, PartialEq, Eq)] 7 | pub enum SMatrix { 8 | /// An object matrix from the model file. Their value depends on the pose 9 | /// (and in fact a pose is exactly the set of values of the object matrices; 10 | /// everything else is static). 11 | Object { object_idx: u8 }, 12 | /// An inverse bind matrix from the model file. 13 | InvBind { inv_bind_idx: u8 }, 14 | /// The contents of an uninitialized matrix slot, ie. one that we haven't 15 | /// stored to yet. Theoretically possible, but shouldn't show up. 16 | Uninitialized { stack_pos: u8 }, 17 | } 18 | 19 | /// Composition of SMatrices. 20 | #[derive(Clone)] 21 | pub struct CMatrix { 22 | pub factors: Vec, 23 | } 24 | 25 | /// Linear combination of CMatrices. 26 | #[derive(Clone)] 27 | pub struct AMatrix { 28 | pub terms: Vec, 29 | } 30 | 31 | #[derive(Clone)] 32 | pub struct ATerm { 33 | pub weight: f32, 34 | pub cmat: CMatrix, 35 | } 36 | 37 | // SMatrix -> CMatrix 38 | impl From for CMatrix { 39 | fn from(smat: SMatrix) -> CMatrix { 40 | CMatrix { factors: vec![smat] } 41 | } 42 | } 43 | 44 | // CMatrix -> AMatrix 45 | impl From for AMatrix { 46 | fn from(cmat: CMatrix) -> AMatrix { 47 | AMatrix { terms: vec![ATerm { weight: 1.0, cmat }] } 48 | } 49 | } 50 | 51 | // SMatrix -> AMatrix 52 | impl From for AMatrix { 53 | fn from(smat: SMatrix) -> AMatrix { 54 | let cmat: CMatrix = smat.into(); 55 | cmat.into() 56 | } 57 | } 58 | 59 | impl CMatrix { 60 | /// Identity CMatrix 61 | pub fn one() -> CMatrix { CMatrix { factors: vec![] } } 62 | } 63 | 64 | impl AMatrix { 65 | /// Identity AMatrix 66 | pub fn one() -> AMatrix { CMatrix::one().into() } 67 | /// Zero AMatrix 68 | pub fn zero() -> AMatrix { AMatrix { terms: vec![] } } 69 | } 70 | 71 | // AMatrix *= SMatrix 72 | impl<'a> ops::MulAssign for AMatrix { 73 | fn mul_assign(&mut self, smat: SMatrix) { 74 | // Distribute it over the sum 75 | for term in &mut self.terms { 76 | term.cmat.factors.push(smat); 77 | } 78 | } 79 | } 80 | 81 | // AMatrix *= f32 82 | impl<'a> ops::MulAssign for AMatrix { 83 | fn mul_assign(&mut self, t: f32) { 84 | if t == 0.0 { 85 | self.terms.clear(); 86 | return; 87 | } 88 | // Distribute it over the sum 89 | for term in &mut self.terms { 90 | term.weight *= t; 91 | } 92 | } 93 | } 94 | 95 | // AMatrix += AMatrix 96 | impl<'a> ops::AddAssign for AMatrix { 97 | fn add_assign(&mut self, amat: AMatrix) { 98 | // Don't group like terms; we'll do that after making them into 99 | // SkinVertices since it's easier to compare NodeIndices than CMatrices. 100 | // Note that it is impossible for terms to cancel out, since there is no 101 | // way to encode a negative scalar in the skinning rendering command. 102 | self.terms.extend(amat.terms) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/skeleton/vertex_record.rs: -------------------------------------------------------------------------------- 1 | //! Abstract interpretation of the render commands to discover what symbolic 2 | //! matrix is applied to each vertex. 3 | 4 | use super::symbolic_matrix::{SMatrix, AMatrix}; 5 | use crate::nitro::Model; 6 | use crate::nitro::render_cmds::SkinTerm; 7 | 8 | type MatrixIdx = u16; 9 | 10 | /// Records what symbolic matrix applies to each vertex. 11 | pub struct VertexRecord { 12 | /// A list of all the symbolic matrices computed in the course of drawing 13 | /// the model. 14 | pub matrices: Vec, 15 | /// For each vertex, which matrix in the above list is applied to it. 16 | pub vertices: Vec, 17 | } 18 | 19 | impl VertexRecord { 20 | pub fn build_for_model(model: &Model) -> VertexRecord { 21 | let mut b = Builder::new(model); 22 | use crate::nitro::render_cmds::Op; 23 | for op in &model.render_ops { 24 | match *op { 25 | Op::LoadMatrix { stack_pos } => b.load_matrix(stack_pos), 26 | Op::StoreMatrix { stack_pos } => b.store_matrix(stack_pos), 27 | Op::MulObject { object_idx } => b.mul_object(object_idx), 28 | Op::Skin { ref terms } => b.skin(&*terms), 29 | Op::ScaleUp => b.scale_up(), 30 | Op::ScaleDown => b.scale_down(), 31 | Op::BindMaterial { .. } => (), 32 | Op::Draw { piece_idx } => b.draw(piece_idx), 33 | } 34 | } 35 | b.vr 36 | } 37 | } 38 | 39 | struct Builder<'a> { 40 | model: &'a Model, 41 | vr: VertexRecord, 42 | 43 | cur_matrix: MatrixIdx, 44 | matrix_stack: Vec>, 45 | } 46 | 47 | impl<'a> Builder<'a> { 48 | fn new(model: &Model) -> Builder { 49 | Builder { 50 | model, 51 | vr: VertexRecord { 52 | matrices: vec![AMatrix::one()], 53 | vertices: vec![], 54 | }, 55 | cur_matrix: 0, 56 | matrix_stack: vec![None; 32], 57 | } 58 | } 59 | 60 | /// Add a new AMatrix to the record, returning its index. 61 | fn add_matrix(&mut self, mat: AMatrix) -> MatrixIdx { 62 | self.vr.matrices.push(mat); 63 | (self.vr.matrices.len() - 1) as MatrixIdx 64 | } 65 | 66 | fn fetch_from_stack(&mut self, stack_pos: u8) -> MatrixIdx { 67 | // If the slot is uninitialized, make a new Uninitialized SMatrix for 68 | // it. 69 | if self.matrix_stack[stack_pos as usize].is_none() { 70 | let uninit = SMatrix::Uninitialized { stack_pos }.into(); 71 | let uninit_idx = self.add_matrix(uninit); 72 | self.matrix_stack[stack_pos as usize] = Some(uninit_idx); 73 | } 74 | self.matrix_stack[stack_pos as usize].unwrap() 75 | } 76 | 77 | fn load_matrix(&mut self, stack_pos: u8) { 78 | let idx = self.fetch_from_stack(stack_pos); 79 | self.cur_matrix = idx; 80 | } 81 | 82 | fn store_matrix(&mut self, stack_pos: u8) { 83 | self.matrix_stack[stack_pos as usize] = Some(self.cur_matrix); 84 | } 85 | 86 | fn mul_object(&mut self, object_idx: u8) { 87 | let mut mat = self.vr.matrices[self.cur_matrix as usize].clone(); 88 | mat *= SMatrix::Object { object_idx }; 89 | self.cur_matrix = self.add_matrix(mat); 90 | } 91 | 92 | fn skin(&mut self, terms: &[SkinTerm]) { 93 | let mut acc = AMatrix::zero(); 94 | for term in terms { 95 | // weight * stack[stack_pos] * inv_binds[inv_bind_idx] 96 | let mat_idx = self.fetch_from_stack(term.stack_pos); 97 | let mut mat = self.vr.matrices[mat_idx as usize].clone(); 98 | mat *= SMatrix::InvBind { inv_bind_idx: term.inv_bind_idx }; 99 | mat *= term.weight; 100 | 101 | acc += mat 102 | } 103 | let mat_idx = self.add_matrix(acc); 104 | self.cur_matrix = mat_idx; 105 | } 106 | 107 | // NOTE: Ignored for now, which is incorrect, but they IME don't end up 108 | // affecting the final skeleton (because they end up in the "longest suffix 109 | // of constant factors"; see joint_tree) so it doesn't matter much. 110 | fn scale_up(&mut self) { } 111 | fn scale_down(&mut self) { } 112 | 113 | fn draw(&mut self, piece_idx: u8) { 114 | let piece = &self.model.pieces[piece_idx as usize]; 115 | use crate::nds::gpu_cmds::{CmdParser, GpuCmd}; 116 | let interpreter = CmdParser::new(&piece.gpu_commands); 117 | 118 | for cmd_res in interpreter { 119 | if cmd_res.is_err() { break; } 120 | match cmd_res.unwrap() { 121 | GpuCmd::Restore { idx } => self.load_matrix(idx as u8), 122 | // Again, ignore scalings. 123 | GpuCmd::Scale { .. } => (), 124 | GpuCmd::Vertex { .. } => { 125 | let cur_matrix = self.cur_matrix; 126 | self.vr.vertices.push(cur_matrix) 127 | } 128 | _ => (), 129 | } 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/util/bimap.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::hash::Hash; 3 | use std::collections::HashMap; 4 | 5 | /// Bijective mapping between a set of Ks (on the left) and a set of Vs (on the 6 | /// right). 7 | pub struct BiMap 8 | where 9 | K: Hash + Eq + Clone, 10 | V: Hash + Eq + Clone, 11 | { 12 | fwd: HashMap, 13 | rev: HashMap, 14 | } 15 | 16 | impl BiMap 17 | where 18 | K: Hash + Eq + Clone, 19 | V: Hash + Eq + Clone, 20 | { 21 | pub fn new() -> BiMap { 22 | BiMap { 23 | fwd: HashMap::new(), 24 | rev: HashMap::new(), 25 | } 26 | } 27 | 28 | /// Go from a K to a V (from left to right). 29 | #[allow(dead_code)] 30 | pub fn forward(&self, k: &K) -> &V { 31 | &self.fwd[k] 32 | } 33 | 34 | /// Go from a V to a K (from right to left). 35 | pub fn backward(&self, v: &V) -> &K { 36 | &self.rev[v] 37 | } 38 | 39 | pub fn insert(&mut self, (k, v): (K, V)) { 40 | self.fwd.insert(k.clone(), v.clone()); 41 | self.rev.insert(v, k); 42 | } 43 | 44 | /// Checks if the given V exists in the map. 45 | pub fn right_contains(&self, v: &V) -> bool { 46 | self.rev.contains_key(v) 47 | } 48 | 49 | pub fn iter(&self) -> std::collections::hash_map::Iter { 50 | self.fwd.iter() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/util/bits.rs: -------------------------------------------------------------------------------- 1 | //! Extracting bitfields from integers. 2 | //! 3 | //! If `x` is an unsigned integer, `x.bits(lo, hi)` is a number of 4 | //! the same type with the bits of `x` in the range [`lo`, `hi`) in 5 | //! its low part. 6 | //! 7 | //! # Examples 8 | //! ``` 9 | //! let x = 0xabcdef00u32; 10 | //! assert_eq!(x.bits(8, 16), 0xef); 11 | //! assert_eq!(x.bits(16, 28), 0xbcd); 12 | //! ``` 13 | 14 | pub trait BitField { 15 | fn bits(self, lo: u32, hi: u32) -> Self; 16 | } 17 | 18 | macro_rules! def_bitfield { 19 | ($t:ty, $bitwidth:expr) => { 20 | impl BitField for $t { 21 | #[inline(always)] 22 | fn bits(self, lo: u32, hi: u32) -> $t { 23 | assert!(lo <= hi); 24 | assert!(hi <= $bitwidth); 25 | (self >> lo) & (!0 >> ($bitwidth - (hi - lo) as $t)) 26 | } 27 | } 28 | } 29 | } 30 | 31 | def_bitfield!(u8, 8); 32 | def_bitfield!(u16, 16); 33 | def_bitfield!(u32, 32); 34 | 35 | #[test] 36 | fn test() { 37 | let x = 0xabcdef00u32; 38 | assert_eq!(x.bits(8, 16), 0xef); 39 | assert_eq!(x.bits(16, 28), 0xbcd); 40 | } 41 | -------------------------------------------------------------------------------- /src/util/bivec.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | use std::collections::HashMap; 3 | use std::hash::Hash; 4 | 5 | /// If a Vec is a map {0,1,...,n} -> T, a BiVec is a bijective Vec. 6 | /// Can lookup elements by index, or indices by element. 7 | pub struct BiVec 8 | where 9 | T: Clone + Eq + Hash, 10 | { 11 | vec: Vec, 12 | reverse: HashMap, // vec[reverse[x]] == x 13 | } 14 | 15 | impl BiVec 16 | where 17 | T: Clone + Eq + Hash, 18 | { 19 | pub fn new() -> BiVec { 20 | BiVec { 21 | vec: vec![], 22 | reverse: HashMap::new(), 23 | } 24 | } 25 | 26 | pub fn len(&self) -> usize { 27 | self.vec.len() 28 | } 29 | 30 | pub fn clear(&mut self) { 31 | self.vec.clear(); 32 | self.reverse.clear(); 33 | } 34 | 35 | /// Push an element to the vec (if not already in it). 36 | /// Returns the index of the element. 37 | pub fn push(&mut self, x: T) -> usize { 38 | let vec = &mut self.vec; 39 | *self.reverse 40 | .entry(x.clone()) 41 | .or_insert_with(|| { 42 | vec.push(x); 43 | vec.len() - 1 44 | }) 45 | } 46 | 47 | pub fn idx(&self, x: &T) -> usize { 48 | self.reverse[x] 49 | } 50 | 51 | pub fn iter(&self) -> std::slice::Iter { 52 | self.vec.iter() 53 | } 54 | } 55 | 56 | impl std::ops::Index for BiVec 57 | where 58 | T: Clone + Eq + Hash, 59 | { 60 | type Output = T; 61 | fn index(&self, idx: usize) -> &T { 62 | &self.vec[idx] 63 | } 64 | } 65 | 66 | #[test] 67 | fn lut_test() { 68 | let xs = [12, 0, 12, 1, 4, 7, -2, 3, 7]; 69 | 70 | let mut lut = BiVec::new(); 71 | for &x in &xs { 72 | lut.push(x); 73 | } 74 | 75 | for &x in &xs { 76 | assert_eq!(lut[lut.idx(&x)], x); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/util/cur.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, error}; 2 | use std::ops::Add; 3 | use crate::util::view::{View, Viewable}; 4 | 5 | /// A pointer into a buffer of bytes. Used for binary file parsing. 6 | #[derive(Copy, Clone)] 7 | pub struct Cur<'a> { 8 | buf_: &'a [u8], 9 | pos_: usize, 10 | } 11 | 12 | impl<'a> Cur<'a> { 13 | pub fn new(buf: &[u8]) -> Cur { 14 | Cur { buf_: buf, pos_: 0 } 15 | } 16 | 17 | pub fn from_buf_pos(buf: &[u8], pos: usize) -> Cur { 18 | Cur { buf_: buf, pos_: pos } 19 | } 20 | 21 | pub fn pos(&self) -> usize { 22 | self.pos_ 23 | } 24 | 25 | pub fn bytes_remaining(&self) -> usize { 26 | self.buf_.len().saturating_sub(self.pos_) 27 | } 28 | 29 | pub fn peek(&self) -> Result { 30 | let size = ::size(); 31 | if self.bytes_remaining() < size { 32 | return Err(Error::TooShort); 33 | } 34 | Ok(::view(&self.buf_[self.pos_..self.pos_ + size])) 35 | } 36 | 37 | pub fn next(&mut self) -> Result { 38 | let size = ::size(); 39 | if self.bytes_remaining() < size { 40 | return Err(Error::TooShort); 41 | } 42 | let next = ::view(&self.buf_[self.pos_..self.pos_ + size]); 43 | self.pos_ += size; 44 | Ok(next) 45 | } 46 | 47 | pub fn nth(&self, n: usize) -> Result { 48 | Ok(self.clone().next_n::(n+1)?.nth(n)) 49 | } 50 | 51 | pub fn next_n(&mut self, n: usize) -> Result, Error> { 52 | let size = ::size(); 53 | let buf = self.next_n_u8s(size * n)?; 54 | Ok(View::from_buf(buf)) 55 | } 56 | 57 | pub fn next_n_u8s(&mut self, n: usize) -> Result<&'a [u8], Error> { 58 | if self.pos_.saturating_add(n) > self.buf_.len() { 59 | return Err(Error::TooShort); 60 | } 61 | let next_n = &self.buf_[self.pos_..self.pos_ + n]; 62 | self.pos_ += n; 63 | Ok(next_n) 64 | } 65 | 66 | pub fn slice_from_cur_to_end(&self) -> &'a [u8] { 67 | &self.buf_[self.pos_ ..] 68 | } 69 | 70 | pub fn jump_forward(&mut self, amt: usize) { 71 | let pos = self.pos_; 72 | self.jump_to(pos + amt); 73 | } 74 | 75 | pub fn jump_to(&mut self, pos: usize) { 76 | self.pos_ = pos; 77 | } 78 | } 79 | 80 | impl<'a> Add for Cur<'a> { 81 | type Output = Cur<'a>; 82 | 83 | fn add(self, amt: usize) -> Cur<'a> { 84 | let mut cur = self; 85 | cur.jump_forward(amt); 86 | cur 87 | } 88 | } 89 | 90 | impl<'a> Add for Cur<'a> { 91 | type Output = Cur<'a>; 92 | fn add(self, amt: u32) -> Cur<'a> { self + amt as usize } 93 | } 94 | 95 | impl<'a> Add for Cur<'a> { 96 | type Output = Cur<'a>; 97 | fn add(self, amt: u16) -> Cur<'a> { self + amt as usize } 98 | } 99 | 100 | impl<'a> fmt::Debug for Cur<'a> { 101 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 102 | write!(f, "Cur {{ pos: {} }}", self.pos()) 103 | } 104 | } 105 | 106 | #[derive(Debug)] 107 | pub enum Error { 108 | TooShort, 109 | } 110 | 111 | impl fmt::Display for Error { 112 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 113 | write!(f, "ran out of data") 114 | } 115 | } 116 | 117 | impl error::Error for Error {} 118 | -------------------------------------------------------------------------------- /src/util/fields.rs: -------------------------------------------------------------------------------- 1 | //! Macro for reading struct-like binary data. 2 | //! 3 | //! Reads a sequence of adjacent fields from a `Cur`, as though by `cur.next::()?`. 4 | //! Also logs the values read and their locations with `trace!` for debugging. 5 | //! 6 | //! # Examples 7 | //! 8 | //! ``` 9 | //! fields!(cur, MyName { // "MyName" is a human-readable name used only for logging 10 | //! // Read a u32, placing its value in the variable x. 11 | //! x: u32, 12 | //! // Read a u32 just after the end of the last one. 13 | //! y: u32, 14 | //! // Fields can depend on previous variables; read x u8s. z will be 15 | //! // a byte slice, &[u8]. 16 | //! z: [u8; x], 17 | //! // This doesn't read anything, but stores a pointer to the current 18 | //! // location to c. 19 | //! c: Cur, 20 | //! // Read a 16-bit fixed-point number in (1,3,12) format. The extra 21 | //! // outer brackets are needed for syntactic reasons. 22 | //! f: (fix16(1,3,12)), 23 | //! }); 24 | //! // If control reaches here, `cur` now points just past the end of the last field. 25 | //! ``` 26 | 27 | macro_rules! field_helper2 { 28 | ($cur:ident, [u8; $n:expr]) => { $cur.next_n_u8s($n as usize)? }; 29 | ($cur:ident, [$t:ty; $n:expr]) => { $cur.next_n::<$t>($n as usize)? }; 30 | ($cur:ident, (fix16($s:expr,$i:expr,$f:expr))) => { 31 | { 32 | let x = $cur.next::()?; 33 | crate::util::fixed::fix16(x, $s, $i, $f) 34 | } 35 | }; 36 | ($cur:ident, (fix32($s:expr,$i:expr,$f:expr))) => { 37 | { 38 | let x = $cur.next::()?; 39 | crate::util::fixed::fix32(x, $s, $i, $f) 40 | } 41 | }; 42 | ($cur:ident, Cur) => { $cur.clone() }; 43 | ($cur:ident, $t:ty) => { $cur.next::<$t>()? }; 44 | } 45 | 46 | macro_rules! field_helper { 47 | ($c:ident, $name:ident, $field:ident, Cur) => { 48 | let $field = field_helper2!($c, Cur); 49 | }; 50 | ($c:ident, $name:ident, $field:ident, $ty:tt) => { 51 | let pos = $c.pos(); 52 | let $field = field_helper2!($c, $ty); 53 | trace!("{}.{}@{:#x}: {:?}", 54 | stringify!($name), 55 | stringify!($field), 56 | pos, 57 | $field, 58 | ); 59 | } 60 | } 61 | 62 | macro_rules! fields { 63 | ($cur:expr, $name:ident { $($field:ident : $ty:tt,)* }) => { 64 | let mut c = $cur; 65 | $(field_helper!(c, $name, $field, $ty);)* 66 | }; 67 | ($cur:ident, $name:ident { $($field:ident : $ty:tt),* }) => { 68 | fields!($cur, $name { $($field : $ty,)* }); 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /src/util/fixed.rs: -------------------------------------------------------------------------------- 1 | //! Fixed-point to `f64` conversions. 2 | 3 | use crate::util::bits::BitField; 4 | 5 | /// Reads a fixed-point number from a `u32`. 6 | /// 7 | /// A fixed-point number represents a fractional value (eg. 5.3) as an integer 8 | /// in smaller units (eg. 53 tenths). Ie. in contrast to floating-point numbers, 9 | /// there are a fixed number of bits after the decimal point. The format 10 | /// (`sign_bits`,`int_bits`,`frac_bits`) determines how many bits are in the 11 | /// fractional part, the integer part, and whether the number is signed. 12 | /// 13 | /// Precisely, the low `sign_bits + int_bits + frac_bits` bits of `x` are interpreted 14 | /// as an integer (unsigned if `sign_bits` is 0, twos-complement if it is 1), 15 | /// and the result is this integer times 2^(-`frac_bits`). 16 | pub fn fix32(x: u32, sign_bits: u32, int_bits: u32, frac_bits: u32) -> f64 { 17 | assert!(sign_bits <= 1); 18 | assert!(int_bits + frac_bits > 0); 19 | assert!(sign_bits + int_bits + frac_bits <= 32); 20 | 21 | let x = x.bits(0, sign_bits + int_bits + frac_bits); 22 | 23 | let y = if sign_bits == 0 { 24 | x as f64 25 | } else { 26 | // sign extend 27 | let sign_mask = (1 << (int_bits + frac_bits)) as u32; 28 | if x & sign_mask != 0 { 29 | (x | !(sign_mask - 1)) as i32 as f64 30 | } else { 31 | x as f64 32 | } 33 | }; 34 | y * 0.5f64.powi(frac_bits as i32) 35 | } 36 | 37 | /// Like `fix32` but for `u16`s. For convenience. 38 | pub fn fix16(x: u16, sign_bits: u32, int_bits: u32, frac_bits: u32) -> f64 { 39 | assert!(sign_bits + int_bits + frac_bits <= 16); 40 | fix32(x as u32, sign_bits, int_bits, frac_bits) 41 | } 42 | -------------------------------------------------------------------------------- /src/util/mod.rs: -------------------------------------------------------------------------------- 1 | //! More-or-less general-purpose utility functions. 2 | 3 | pub mod bits; 4 | pub mod cur; 5 | #[macro_use] 6 | pub mod fields; 7 | pub mod bivec; 8 | pub mod bimap; 9 | pub mod fixed; 10 | pub mod namers; 11 | pub mod view; 12 | pub mod out_dir; 13 | pub mod tree; 14 | 15 | pub use self::bivec::BiVec; 16 | pub use self::bimap::BiMap; 17 | pub use self::out_dir::OutDir; 18 | -------------------------------------------------------------------------------- /src/util/namers.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::string::ToString; 3 | 4 | pub struct UniqueNamer { 5 | taken_names: HashSet, 6 | } 7 | 8 | /// Returns unique names. 9 | impl UniqueNamer { 10 | pub fn new() -> UniqueNamer { 11 | UniqueNamer { taken_names: HashSet::new() } 12 | } 13 | 14 | /// Returns a name, either `desired_name` or something "close" to it, which 15 | /// has never been returned by a prior call to this function on the same 16 | /// `UniqueNamer` receiver. 17 | pub fn get_fresh_name>(&mut self, desired_name: S) -> String { 18 | let desired_name = desired_name.as_ref(); 19 | let chosen_name = 20 | if !self.taken_names.contains(desired_name) { 21 | desired_name.to_string() 22 | } else { 23 | let mut name = String::new(); 24 | for i in 1.. { 25 | name = format!("{}{}", desired_name, i); 26 | if !self.taken_names.contains(&name) { 27 | break; 28 | } 29 | } 30 | name 31 | }; 32 | self.taken_names.insert(chosen_name.clone()); 33 | chosen_name 34 | } 35 | } 36 | 37 | #[test] 38 | fn test_unique_namer() { 39 | let mut un = UniqueNamer::new(); 40 | assert_eq!(un.get_fresh_name("A"), "A"); 41 | assert_eq!(un.get_fresh_name("A"), "A1"); 42 | assert_eq!(un.get_fresh_name("A"), "A2"); 43 | assert_eq!(un.get_fresh_name("B"), "B"); 44 | assert_eq!(un.get_fresh_name("A"), "A3"); 45 | } 46 | -------------------------------------------------------------------------------- /src/util/out_dir.rs: -------------------------------------------------------------------------------- 1 | use std::io::ErrorKind; 2 | use std::path::PathBuf; 3 | use std::fs; 4 | use crate::errors::Result; 5 | 6 | /// Directory for putting output files in. Will be created lazily when the first 7 | /// file is created. 8 | pub struct OutDir { 9 | path: PathBuf, 10 | created: bool, 11 | } 12 | 13 | impl OutDir { 14 | pub fn new(path: PathBuf) -> Result { 15 | Ok(OutDir { path, created: false }) 16 | } 17 | 18 | pub fn create_file(&mut self, filename: &str) -> Result { 19 | if !self.created { 20 | match fs::create_dir(&self.path) { 21 | Ok(()) => (), 22 | Err(e) if e.kind() == ErrorKind::AlreadyExists => (), 23 | Err(e) => Err(e)?, 24 | } 25 | self.created = true; 26 | } 27 | Ok(fs::File::create(self.path.join(filename))?) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/util/tree.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | 3 | /// A tree (or forest). Each node stores a T. 4 | /// Nodes are stored in a flat array and referenced by indices. 5 | pub struct Tree { 6 | nodes: Vec>, 7 | } 8 | 9 | pub type NodeIdx = u16; 10 | static NONE: NodeIdx = !0; // marks no children/siblings 11 | 12 | struct Node { 13 | val: T, 14 | parent: NodeIdx, 15 | first_child: NodeIdx, 16 | next_sibling: NodeIdx, 17 | prev_sibling: NodeIdx, 18 | } 19 | 20 | impl Tree { 21 | pub fn with_capacity(num_nodes: usize) -> Tree { 22 | Tree { 23 | nodes: Vec::with_capacity(num_nodes), 24 | } 25 | } 26 | 27 | pub fn node_count(&self) -> usize { 28 | self.nodes.len() 29 | } 30 | 31 | pub fn add_node(&mut self, val: T) -> NodeIdx { 32 | self.nodes.push(Node { 33 | val, 34 | parent: NONE, 35 | first_child: NONE, 36 | next_sibling: NONE, 37 | prev_sibling: NONE, 38 | }); 39 | let node = (self.nodes.len() - 1) as NodeIdx; 40 | assert!(node != NONE); 41 | node 42 | } 43 | 44 | pub fn reparent(&mut self, node: NodeIdx, new_parent: NodeIdx) { 45 | // Remove from current position 46 | let parent = self.nodes[node as usize].parent; 47 | let prev_sibling = self.nodes[node as usize].prev_sibling; 48 | let next_sibling = self.nodes[node as usize].next_sibling; 49 | if prev_sibling != NONE { 50 | self.nodes[prev_sibling as usize].next_sibling = next_sibling; 51 | } 52 | if next_sibling != NONE { 53 | self.nodes[next_sibling as usize].prev_sibling = prev_sibling; 54 | } 55 | if parent != NONE && prev_sibling == NONE { 56 | assert!(self.nodes[parent as usize].first_child == node); 57 | self.nodes[parent as usize].first_child = next_sibling; 58 | } 59 | 60 | // Insert at new position (as last child) 61 | let last_child = self.last_child(new_parent); 62 | if last_child == NONE { 63 | self.nodes[new_parent as usize].first_child = node; 64 | } else { 65 | self.nodes[last_child as usize].next_sibling = node; 66 | } 67 | self.nodes[node as usize].prev_sibling = last_child; 68 | self.nodes[node as usize].next_sibling = NONE; 69 | self.nodes[node as usize].parent = new_parent; 70 | } 71 | 72 | fn last_child(&self, node: NodeIdx) -> NodeIdx { 73 | let mut child = self.nodes[node as usize].first_child; 74 | if child == NONE { 75 | return NONE; 76 | } 77 | 78 | loop { 79 | let next_child = self.nodes[child as usize].next_sibling; 80 | if next_child == NONE { 81 | return child; 82 | } else { 83 | child = next_child; 84 | } 85 | } 86 | } 87 | 88 | // Iterator over all nodes indices (in insertion order). 89 | pub fn node_idxs(&self) -> std::ops::Range { 90 | 0..self.nodes.len() as NodeIdx 91 | } 92 | 93 | /// Iterator over all the children of node. 94 | pub fn children(&self, node: NodeIdx) -> Children { 95 | Children { 96 | tree: self, 97 | next_child: self.nodes[node as usize].first_child, 98 | } 99 | } 100 | } 101 | 102 | impl std::ops::Index for Tree { 103 | type Output = T; 104 | 105 | fn index(&self, node: NodeIdx) -> &T { 106 | &self.nodes[node as usize].val 107 | } 108 | } 109 | 110 | impl std::ops::IndexMut for Tree { 111 | fn index_mut(&mut self, node: NodeIdx) -> &mut T { 112 | &mut self.nodes[node as usize].val 113 | } 114 | } 115 | 116 | pub struct Children<'a, T> { 117 | tree: &'a Tree, 118 | next_child: NodeIdx, 119 | } 120 | 121 | impl std::iter::Iterator for Children<'_, T> { 122 | type Item = NodeIdx; 123 | 124 | fn next(&mut self) -> Option { 125 | if self.next_child == NONE { 126 | None 127 | } else { 128 | let next_child = self.next_child; 129 | self.next_child = self.tree.nodes[self.next_child as usize].next_sibling; 130 | Some(next_child) 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/util/view.rs: -------------------------------------------------------------------------------- 1 | //! Handling for types that can be viewed as byte arrays. 2 | 3 | use std::fmt; 4 | use std::fmt::Debug; 5 | use std::fmt::Formatter; 6 | use std::fmt::Write; 7 | use std::iter::Iterator; 8 | use std::marker::PhantomData; 9 | 10 | /// Types that can be constructed from a constant-length byte array. 11 | pub trait Viewable: Sized { 12 | /// Size of the byte array. 13 | fn size() -> usize; 14 | /// View the byte-array (of length `size()`) as a `Self`. 15 | fn view(buf: &[u8]) -> Self; 16 | } 17 | 18 | impl Viewable for u8 { 19 | fn size() -> usize { 1 } 20 | fn view(buf: &[u8]) -> u8 { buf[0] } 21 | } 22 | 23 | impl Viewable for u16 { 24 | fn size() -> usize { 2 } 25 | fn view(buf: &[u8]) -> u16 { 26 | use std::convert::TryFrom; 27 | let bytes = <[u8; 2]>::try_from(&buf[0..2]).unwrap(); 28 | u16::from_le_bytes(bytes) 29 | } 30 | } 31 | 32 | impl Viewable for u32 { 33 | fn size() -> usize { 4 } 34 | fn view(buf: &[u8]) -> u32 { 35 | use std::convert::TryFrom; 36 | let bytes = <[u8; 4]>::try_from(&buf[0..4]).unwrap(); 37 | u32::from_le_bytes(bytes) 38 | } 39 | } 40 | 41 | impl Viewable for [T; 5] where 42 | T: Viewable, 43 | { 44 | fn size() -> usize { 5 * ::size() } 45 | fn view(buf: &[u8]) -> [T; 5] { 46 | let sz = ::size(); 47 | 48 | let t0 = ::view(&buf[0*sz..1*sz]); 49 | let t1 = ::view(&buf[1*sz..2*sz]); 50 | let t2 = ::view(&buf[2*sz..3*sz]); 51 | let t3 = ::view(&buf[3*sz..4*sz]); 52 | let t4 = ::view(&buf[4*sz..5*sz]); 53 | [t0, t1, t2, t3, t4] 54 | } 55 | } 56 | 57 | impl Viewable for (T,S) where 58 | T: Viewable, 59 | S: Viewable 60 | { 61 | fn size() -> usize { ::size() + ::size() } 62 | fn view(buf: &[u8]) -> (T,S) { 63 | let split = ::size(); 64 | let t = ::view(&buf[..split]); 65 | let s = ::view(&buf[split..]); 66 | (t,s) 67 | } 68 | } 69 | 70 | impl Viewable for (T,S,P) where 71 | T: Viewable, 72 | S: Viewable, 73 | P: Viewable, 74 | { 75 | fn size() -> usize { <(T,(S,P)) as Viewable>::size() } 76 | fn view(buf: &[u8]) -> (T,S,P) { 77 | let (t,(s,p)) = <(T,(S,P)) as Viewable>::view(buf); 78 | (t,s,p) 79 | } 80 | } 81 | 82 | impl Viewable for (T,S,P,Q,R) where 83 | T: Viewable, 84 | S: Viewable, 85 | P: Viewable, 86 | Q: Viewable, 87 | R: Viewable, 88 | { 89 | fn size() -> usize { <(T,S,(P,Q,R)) as Viewable>::size() } 90 | fn view(buf: &[u8]) -> (T,S,P,Q,R) { 91 | let (t,s,(p,q,r)) = <(T,S,(P,Q,R)) as Viewable>::view(buf); 92 | (t,s,p,q,r) 93 | } 94 | } 95 | 96 | 97 | /// A byte buffer interpreted as an array of Viewable elements. 98 | #[derive(Copy, Clone)] 99 | pub struct View<'a, T> { 100 | buf: &'a [u8], 101 | _marker: PhantomData<*const T>, // TODO: Is this the right type? 102 | } 103 | 104 | impl<'a, T: Viewable> View<'a, T> { 105 | /// Constructs a `View` from its underlying buffer. 106 | /// 107 | /// # Panics 108 | /// The view must contain an even number of `T`s; that is, the length 109 | /// of `buf` in bytes must be a multiple of the `Viewable::size()` of 110 | /// `T`. 111 | pub fn from_buf(buf: &[u8]) -> View { 112 | let size = ::size(); 113 | assert!(size == 0 || buf.len() % size == 0); 114 | View { buf, _marker: PhantomData } 115 | } 116 | 117 | /// Number of `T`s in the view. 118 | pub fn len(&self) -> usize { 119 | let size = ::size(); 120 | self.buf.len() / size 121 | } 122 | 123 | pub fn get(&self, pos: usize) -> Option { 124 | let size = ::size(); 125 | let begin = size * pos; 126 | let end = begin + size; 127 | if end > self.buf.len() { 128 | return None; 129 | } 130 | let bytes = &self.buf[begin..end]; 131 | Some(::view(bytes)) 132 | } 133 | 134 | pub fn nth(&self, pos: usize) -> T { 135 | match self.get(pos) { 136 | Some(x) => x, 137 | None => { 138 | panic!("index {} out of range for view of length {}", 139 | pos, self.len()); 140 | } 141 | } 142 | } 143 | } 144 | 145 | impl<'a, T: Viewable + Debug> Debug for View<'a, T> { 146 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 147 | write!(f, "View [")?; 148 | if self.len() != 0 { 149 | write!(f, "{:?}", self.nth(0))?; 150 | for i in 1..self.len() { 151 | write!(f, ", {:?}", self.nth(i))?; 152 | } 153 | } 154 | f.write_char(']') 155 | } 156 | } 157 | 158 | impl<'a, T: Viewable> Iterator for View<'a, T> { 159 | type Item = T; 160 | 161 | fn next(&mut self) -> Option { 162 | if self.buf.len() == 0 { 163 | None 164 | } else { 165 | let size = ::size(); 166 | let item = ::view(&self.buf[0..size]); 167 | self.buf = &self.buf[size..]; 168 | Some(item) 169 | } 170 | } 171 | 172 | fn size_hint(&self) -> (usize, Option) { 173 | let len = self.len(); 174 | (len, Some(len)) 175 | } 176 | } 177 | 178 | impl<'a, T: Viewable> DoubleEndedIterator for View<'a, T> { 179 | fn next_back(&mut self) -> Option { 180 | if self.buf.len() == 0 { 181 | None 182 | } else { 183 | let size = ::size(); 184 | let idx = self.buf.len() - size; 185 | let item = ::view(&self.buf[idx..]); 186 | self.buf = &self.buf[..idx]; 187 | Some(item) 188 | } 189 | } 190 | } 191 | 192 | impl<'a, T: Viewable> ExactSizeIterator for View<'a, T> {} 193 | -------------------------------------------------------------------------------- /src/version.rs: -------------------------------------------------------------------------------- 1 | pub fn print_version_info() { 2 | println!("apicula {}", env!("CARGO_PKG_VERSION")); 3 | 4 | // These variables can optionally be set during the build process 5 | if let Some(info) = option_env!("APICULA_BUILD_COMMIT_HASH") { 6 | println!("build commit: {}", info); 7 | } 8 | if let Some(info) = option_env!("APICULA_BUILD_COMMIT_DATE") { 9 | println!("build commit date: {}", info); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/viewer/fps.rs: -------------------------------------------------------------------------------- 1 | use super::FPS_INTERVAL; 2 | 3 | /// Tracks frames per second. 4 | pub struct FpsCounter { 5 | fps: f64, 6 | time_acc: f64, 7 | frames_acc: f64, 8 | } 9 | 10 | impl FpsCounter { 11 | pub fn new() -> FpsCounter { 12 | FpsCounter { 13 | fps: 0.0, 14 | time_acc: 0.0, 15 | frames_acc: 0.0, 16 | } 17 | } 18 | 19 | pub fn fps(&self) -> f64 { 20 | self.fps 21 | } 22 | 23 | pub fn update(&mut self, dt: f64) { 24 | self.time_acc += dt; 25 | self.frames_acc += 1.0; 26 | 27 | if self.time_acc > FPS_INTERVAL { 28 | self.fps = self.frames_acc / self.time_acc; 29 | self.time_acc = 0.0; 30 | self.frames_acc = 0.0; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/viewer/main_loop.rs: -------------------------------------------------------------------------------- 1 | use glium::winit; 2 | use winit::dpi::{PhysicalSize, PhysicalPosition}; 3 | use winit::keyboard::ModifiersState; 4 | use super::viewer::Viewer; 5 | use crate::db::Database; 6 | use crate::connection::Connection; 7 | 8 | pub fn main_loop(db: Database, conn: Connection) { 9 | let event_loop = winit::event_loop::EventLoop::builder() 10 | .build() 11 | .expect("event loop building"); 12 | let (window, display) = glium::backend::glutin::SimpleWindowBuilder::new() 13 | .with_inner_size(super::WINDOW_WIDTH, super::WINDOW_HEIGHT) 14 | .with_vsync(true) 15 | .build(&event_loop); 16 | 17 | let mut viewer = Viewer::new(&display, db, conn); 18 | 19 | struct State { 20 | last_mouse_xy: PhysicalPosition, 21 | mouse_grabbed: bool, 22 | modifiers: ModifiersState, 23 | win_title: String, 24 | cur_time: u64, 25 | last_time: u64, 26 | } 27 | 28 | let mut state = State { 29 | last_mouse_xy: PhysicalPosition { x: 0.0, y: 0.0 }, 30 | mouse_grabbed: false, 31 | modifiers: Default::default(), 32 | win_title: String::with_capacity(512), 33 | cur_time: time::precise_time_ns(), 34 | last_time: time::precise_time_ns(), 35 | }; 36 | 37 | let _ = event_loop.run(move |ev, window_target| { 38 | use winit::event::Event as Ev; 39 | use winit::event::WindowEvent as WEv; 40 | use winit::event::DeviceEvent as DEv; 41 | 42 | match ev { 43 | Ev::WindowEvent { event, .. } => match event { 44 | WEv::RedrawRequested => { 45 | state.last_time = state.cur_time; 46 | state.cur_time = time::precise_time_ns(); 47 | let dt_in_ns = state.cur_time.wrapping_sub(state.last_time); 48 | let dt = dt_in_ns as f64 / 1_000_000_000.0; 49 | 50 | viewer.update(&display, dt); 51 | 52 | let PhysicalSize { width, height } = window.inner_size(); 53 | if width > 0 && height > 0 { 54 | viewer.set_aspect_ratio(width as f64 / height as f64); 55 | } 56 | 57 | let mut frame = display.draw(); 58 | viewer.draw(&mut frame); 59 | frame.finish().expect("rendering error"); 60 | 61 | state.win_title.clear(); 62 | viewer.title(&mut state.win_title); 63 | window.set_title(&state.win_title); 64 | } 65 | WEv::CloseRequested => { 66 | window_target.exit(); 67 | } 68 | WEv::KeyboardInput { event: e, .. } => { 69 | if let winit::keyboard::PhysicalKey::Code(code) = e.physical_key { 70 | viewer.key( 71 | &display, 72 | code, 73 | e.state == winit::event::ElementState::Pressed, 74 | state.modifiers, 75 | ); 76 | } 77 | } 78 | WEv::ModifiersChanged(m) => { 79 | state.modifiers = m.state(); 80 | } 81 | WEv::MouseInput { state: mouse_state, button, .. } => { 82 | use winit::event::ElementState as Es; 83 | use winit::event::MouseButton as MB; 84 | 85 | match (mouse_state, button) { 86 | (Es::Pressed, MB::Left) => { 87 | state.mouse_grabbed = true; 88 | let _ = window.set_cursor_grab(winit::window::CursorGrabMode::Locked); 89 | window.set_cursor_visible(false); 90 | } 91 | (Es::Released, MB::Left) => { 92 | state.mouse_grabbed = false; 93 | let _ = window.set_cursor_grab(winit::window::CursorGrabMode::None); 94 | window.set_cursor_visible(true); 95 | } 96 | _ => (), 97 | } 98 | } 99 | WEv::CursorMoved { position, .. } => { 100 | state.last_mouse_xy = position; 101 | } 102 | WEv::Focused(false) => { 103 | viewer.blur(); 104 | 105 | // Release the mouse 106 | state.mouse_grabbed = false; 107 | let _ = window.set_cursor_grab(winit::window::CursorGrabMode::None); 108 | window.set_cursor_visible(true); 109 | } 110 | _ => () 111 | }, 112 | Ev::DeviceEvent { event, .. } => match event { 113 | DEv::MouseMotion { delta } => { 114 | // delta is in an "unspecified coordinate system" but 115 | // appears to be pixels on my machine 116 | if state.mouse_grabbed { 117 | viewer.mouse_drag(delta); 118 | } 119 | } 120 | _ => (), 121 | }, 122 | Ev::AboutToWait => { 123 | window.request_redraw(); 124 | }, 125 | _ => (), 126 | } 127 | }); 128 | } 129 | -------------------------------------------------------------------------------- /src/viewer/mod.rs: -------------------------------------------------------------------------------- 1 | mod model_viewer; 2 | mod main_loop; 3 | mod viewer; 4 | mod fps; 5 | 6 | use crate::cli::Args; 7 | use crate::db::Database; 8 | use crate::connection::{Connection, ConnectionOptions}; 9 | use crate::errors::Result; 10 | 11 | /// Initial window width. 12 | pub static WINDOW_WIDTH: u32 = 640; 13 | /// Initial window height. 14 | pub static WINDOW_HEIGHT: u32 = 480; 15 | /// Window background color. 16 | pub static BG_COLOR: (f32, f32, f32, f32) = (0.3, 0.3, 0.3, 1.0); 17 | /// Near-plane distance for perspective. 18 | pub static Z_NEAR: f32 = 0.01; 19 | /// Far-plane distance for perspective. 20 | pub static Z_FAR: f32 = 4000.0; 21 | /// Vertical field-of-view for perspective (radians). 22 | pub static FOV_Y: f32 = 1.1; 23 | /// Animation framerate (seconds/frame) 24 | pub static FRAMERATE: f64 = 1.0 / 60.0; 25 | /// Calculate FPS over intervals of this length (seconds). 26 | pub static FPS_INTERVAL: f64 = 2.0; 27 | 28 | pub fn main(args: &Args) -> Result<()> { 29 | let db = Database::from_cli_args(args)?; 30 | db.print_status(); 31 | 32 | if db.models.len() == 0 { 33 | println!("No models, nothing to do!"); 34 | return Ok(()); 35 | } 36 | 37 | let conn_options = ConnectionOptions::from_cli_args(args); 38 | let conn = Connection::build(&db, conn_options); 39 | 40 | // Print the controls 41 | println!("{}", viewer::CONTROL_HELP); 42 | 43 | main_loop::main_loop(db, conn); 44 | 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /src/viewer/model_viewer/eye.rs: -------------------------------------------------------------------------------- 1 | use cgmath::{EuclideanSpace, Matrix4, Point3, 2 | Rad, Transform, vec3, Vector2, Vector3}; 3 | use std::default::Default; 4 | use std::f32::consts::PI; 5 | 6 | #[derive(Clone)] 7 | pub struct Eye { 8 | pub position: Point3, 9 | pub azimuth: f32, 10 | pub altitude: f32, 11 | } 12 | 13 | impl Eye { 14 | /// Model-view matrix. 15 | pub fn model_view(&self) -> Matrix4 { 16 | let mv = 17 | Matrix4::from_angle_x(Rad(-self.altitude)) * 18 | Matrix4::from_angle_y(Rad(-self.azimuth)) * 19 | Matrix4::from_translation(-self.position.to_vec()); 20 | mv 21 | } 22 | 23 | /// Move in the direction of dv. X = forward, Y = right-side, Z = up. 24 | pub fn move_by(&mut self, dv: Vector3) { 25 | // Treating the eye as if it were inclined neither up nor down, 26 | // transform the forward/side/up basis in camera space into 27 | // world space. 28 | let t = Matrix4::from_angle_y(Rad(self.azimuth)); 29 | let forward = t.transform_vector(vec3(0.0, 0.0, -1.0)); 30 | let side = t.transform_vector(vec3(1.0, 0.0, 0.0)); 31 | let up = t.transform_vector(vec3(0.0, 1.0, 0.0)); 32 | 33 | self.position += forward * dv.x + side * dv.y + up * dv.z; 34 | } 35 | 36 | pub fn free_look(&mut self, dv: Vector2) { 37 | self.azimuth -= dv.x; 38 | self.altitude -= dv.y; 39 | 40 | // Wrap once (expect dv to be small) for azimuth 41 | static PI_2: f32 = PI * 2.0; 42 | if self.azimuth >= PI_2 { 43 | self.azimuth -= PI_2; 44 | } else if self.azimuth < 0.0 { 45 | self.azimuth += PI_2; 46 | } 47 | 48 | // Clamp into allowable altitude range to avoid singularities 49 | // at the poles. 50 | let max_alt = 0.499 * PI; 51 | let min_alt = -max_alt; 52 | self.altitude = 53 | if self.altitude < min_alt { min_alt } 54 | else if self.altitude > max_alt { max_alt } 55 | else { self.altitude }; 56 | } 57 | } 58 | 59 | impl Default for Eye { 60 | fn default() -> Eye { 61 | Eye { 62 | position: Point3::new(0.0, 0.0, 0.0), 63 | azimuth: 0.0, 64 | altitude: 0.0, 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/viewer/model_viewer/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod eye; 2 | pub mod texture_cache; 3 | 4 | pub use self::eye::Eye; 5 | 6 | use cgmath::{PerspectiveFov, Rad, Matrix4}; 7 | use crate::db::Database; 8 | use glium::{VertexBuffer, IndexBuffer, Frame, Surface, Program}; 9 | use crate::nitro::Model; 10 | use crate::primitives::{Primitives, DrawCall, Vertex}; 11 | use self::texture_cache::{TextureCache, ImageId}; 12 | use super::{Z_NEAR, Z_FAR, FOV_Y}; 13 | 14 | type Display = glium::Display; 15 | 16 | /// Model viewer. 17 | /// 18 | /// Handles the GPU data needed to draw a model. Typical use is: 19 | /// 20 | /// * use change_model to begin drawing a new model 21 | /// * use update_vertices after, eg, you move to a new animation frame of the 22 | /// same model 23 | /// * use update_materials when the textures on the materials change 24 | pub struct ModelViewer { 25 | pub eye: Eye, 26 | pub aspect_ratio: f32, 27 | pub light_on: bool, 28 | 29 | /// Program for unlit materials (using vertex colors) 30 | unlit_program: Program, 31 | /// Program for lit materials (using normals) 32 | lit_program: Program, 33 | 34 | vertex_buffer: Option>, 35 | index_buffer: Option>, 36 | draw_calls: Vec, 37 | texture_cache: TextureCache, 38 | material_map: Vec, 39 | } 40 | 41 | /// Tells you what GL texture to use for a given material. 42 | #[derive(Clone)] 43 | pub enum MaterialTextureBinding { 44 | /// Used when the material had not texture. 45 | None, 46 | /// Used when the material had a texture but we couldn't resolve it. 47 | Missing, 48 | /// Use the given image. 49 | ImageId(ImageId), 50 | } 51 | 52 | impl ModelViewer { 53 | pub fn new(display: &Display) -> ModelViewer { 54 | let unlit_vertex_shader = include_str!("shaders/vert_unlit.glsl"); 55 | let lit_vertex_shader = include_str!("shaders/vert_lit.glsl"); 56 | let fragment_shader = include_str!("shaders/frag.glsl"); 57 | let program_args = 58 | glium::program::ProgramCreationInput::SourceCode { 59 | vertex_shader: unlit_vertex_shader, 60 | fragment_shader, 61 | geometry_shader: None, 62 | tessellation_control_shader: None, 63 | tessellation_evaluation_shader: None, 64 | transform_feedback_varyings: None, 65 | outputs_srgb: true, 66 | uses_point_size: false, 67 | }; 68 | let unlit_program = Program::new(display, program_args).unwrap(); 69 | let program_args = 70 | glium::program::ProgramCreationInput::SourceCode { 71 | vertex_shader: lit_vertex_shader, 72 | fragment_shader, 73 | geometry_shader: None, 74 | tessellation_control_shader: None, 75 | tessellation_evaluation_shader: None, 76 | transform_feedback_varyings: None, 77 | outputs_srgb: true, 78 | uses_point_size: false, 79 | }; 80 | let lit_program = Program::new(display, program_args).unwrap(); 81 | 82 | ModelViewer { 83 | eye: Default::default(), 84 | aspect_ratio: 1.0, 85 | light_on: true, 86 | unlit_program, 87 | lit_program, 88 | vertex_buffer: None, 89 | index_buffer: None, 90 | texture_cache: TextureCache::new(display), 91 | draw_calls: vec![], 92 | material_map: vec![], 93 | } 94 | } 95 | 96 | /// Changes the viewed model. 97 | pub fn change_model( 98 | &mut self, 99 | display: &Display, 100 | db: &Database, 101 | prim: Primitives, 102 | material_map: Vec, 103 | ) { 104 | use glium::index::PrimitiveType; 105 | 106 | let vb = VertexBuffer::dynamic(display, &prim.vertices).unwrap(); 107 | let ib = IndexBuffer::new( 108 | display, 109 | PrimitiveType::TrianglesList, 110 | &prim.indices).unwrap(); 111 | self.vertex_buffer = Some(vb); 112 | self.index_buffer = Some(ib); 113 | 114 | self.draw_calls = prim.draw_calls; 115 | 116 | // Simple cache size management: clear everything when we change models 117 | self.texture_cache.clear(); 118 | 119 | self.material_map = material_map; 120 | self.populate_texture_cache(display, db); 121 | } 122 | 123 | /// Update the vertices of the model (eg. when the position change because 124 | /// it is being animated). Cannot change the number of vertices, 125 | /// connectivity, etc. 126 | pub fn update_vertices(&mut self, vertices: &[Vertex]) { 127 | self.vertex_buffer.as_mut().unwrap().write(vertices); 128 | } 129 | 130 | /// Updates the list of material-image bindings. 131 | pub fn update_materials( 132 | &mut self, 133 | display: &Display, 134 | db: &Database, 135 | material_map: Vec, 136 | ) { 137 | self.material_map = material_map; 138 | 139 | self.populate_texture_cache(display, db) 140 | } 141 | 142 | /// Ensures all the images used by the material_map are in the texture 143 | /// cache. 144 | fn populate_texture_cache(&mut self, display: &Display, db: &Database) { 145 | for binding in &self.material_map { 146 | if let MaterialTextureBinding::ImageId(ref image_id) = binding { 147 | self.texture_cache.create(display, db, image_id.clone()); 148 | } 149 | } 150 | } 151 | 152 | pub fn draw(&self, target: &mut Frame, model: &Model) { 153 | // Do nothing if there isn't vertex/index data 154 | let vertex_buffer = match self.vertex_buffer { 155 | Some(ref vb) => vb, 156 | None => return, 157 | }; 158 | let index_buffer = match self.index_buffer { 159 | Some(ref ib) => ib, 160 | None => return, 161 | }; 162 | 163 | // Model-view-projection matrix 164 | let model_view = self.eye.model_view(); 165 | let persp: Matrix4 = PerspectiveFov { 166 | fovy: Rad(FOV_Y), 167 | aspect: self.aspect_ratio, 168 | near: Z_NEAR, 169 | far: Z_FAR, 170 | }.into(); 171 | let model_view_persp = persp * model_view; 172 | let model_view_persp: [[f32; 4]; 4] = model_view_persp.into(); 173 | 174 | // Do each draw call 175 | for call in &self.draw_calls { 176 | let material = &model.materials[call.mat_id as usize]; 177 | 178 | let texture = match self.material_map.get(call.mat_id as usize) { 179 | Some(&MaterialTextureBinding::None) => 180 | self.texture_cache.white_texture(), 181 | Some(&MaterialTextureBinding::Missing) => 182 | self.texture_cache.error_texture(), 183 | Some(&MaterialTextureBinding::ImageId(ref image_id)) => 184 | self.texture_cache.lookup(image_id.clone()), 185 | None => self.texture_cache.error_texture(), 186 | }; 187 | let sampler = { 188 | use glium::uniforms::*; 189 | let mut s = Sampler::new(texture); 190 | 191 | s.1.minify_filter = MinifySamplerFilter::Nearest; 192 | s.1.magnify_filter = MagnifySamplerFilter::Nearest; 193 | 194 | // Texture wrapping/mirroring/clamping 195 | let wrap_fn = |repeat, mirror| { 196 | match (repeat, mirror) { 197 | (false, _) => SamplerWrapFunction::Clamp, 198 | (true, false) => SamplerWrapFunction::Repeat, 199 | (true, true) => SamplerWrapFunction::Mirror, 200 | } 201 | }; 202 | let params = &material.params; 203 | s.1.wrap_function.0 = wrap_fn(params.repeat_s(), params.mirror_s()); 204 | s.1.wrap_function.1 = wrap_fn(params.repeat_t(), params.mirror_t()); 205 | 206 | s 207 | }; 208 | 209 | let indices = &index_buffer.slice(call.index_range.clone()).unwrap(); 210 | 211 | let draw_params = glium::DrawParameters { 212 | depth: glium::Depth { 213 | test: glium::draw_parameters::DepthTest::IfLess, 214 | write: true, 215 | .. Default::default() 216 | }, 217 | backface_culling: { 218 | use glium::draw_parameters::BackfaceCullingMode as Mode; 219 | match (material.cull_backface, material.cull_frontface) { 220 | (false, false) => Mode::CullingDisabled, 221 | (true, false) => Mode::CullClockwise, 222 | (false, true) => Mode::CullCounterClockwise, 223 | (true, true) => continue, 224 | } 225 | }, 226 | blend: glium::Blend::alpha_blending(), 227 | .. Default::default() 228 | }; 229 | 230 | if !call.used_normals || !self.light_on { 231 | let uniforms = uniform! { 232 | matrix: model_view_persp, 233 | alpha: material.alpha, 234 | tex: sampler, 235 | }; 236 | target.draw( 237 | vertex_buffer, 238 | indices, 239 | &self.unlit_program, 240 | &uniforms, 241 | &draw_params, 242 | ).unwrap(); 243 | } else { 244 | let uniforms = uniform! { 245 | matrix: model_view_persp, 246 | light_vec: [0.0, -0.624695, -0.78086877f32], 247 | light_color: [1.0, 1.0, 1.0f32], 248 | diffuse_color: material.diffuse, 249 | ambient_color: material.ambient, 250 | emission_color: material.emission, 251 | alpha: material.alpha, 252 | tex: sampler, 253 | }; 254 | target.draw( 255 | vertex_buffer, 256 | indices, 257 | &self.lit_program, 258 | &uniforms, 259 | &draw_params, 260 | ).unwrap(); 261 | } 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/viewer/model_viewer/shaders/frag.glsl: -------------------------------------------------------------------------------- 1 | #version 140 2 | 3 | uniform sampler2D tex; 4 | 5 | in vec2 v_texcoord; 6 | in vec4 v_color; 7 | 8 | out vec4 color; 9 | 10 | void main() { 11 | vec4 c = texture(tex, v_texcoord) * v_color; 12 | if (c.w == 0.0) discard; 13 | color = c; 14 | } 15 | -------------------------------------------------------------------------------- /src/viewer/model_viewer/shaders/vert_lit.glsl: -------------------------------------------------------------------------------- 1 | #version 140 2 | 3 | uniform mat4 matrix; 4 | uniform vec3 light_vec; 5 | uniform vec3 light_color; 6 | 7 | uniform float alpha; 8 | uniform vec3 diffuse_color; 9 | uniform vec3 emission_color; 10 | uniform vec3 ambient_color; 11 | 12 | in vec3 position; 13 | in vec2 texcoord; 14 | in vec3 normal; 15 | in vec3 color; 16 | 17 | out vec2 v_texcoord; 18 | out vec4 v_color; 19 | 20 | void main() { 21 | v_texcoord = texcoord; 22 | 23 | vec3 c; 24 | float diff_level = max(0.0, -dot(light_vec, normal)); 25 | c = emission_color; 26 | c += diffuse_color * light_color * diff_level; 27 | //c += specular_color * light_color * shine_level; 28 | c += ambient_color * light_color; 29 | c *= color; 30 | v_color = vec4(c, alpha); 31 | 32 | gl_Position = matrix * vec4(position, 1.0); 33 | } 34 | -------------------------------------------------------------------------------- /src/viewer/model_viewer/shaders/vert_unlit.glsl: -------------------------------------------------------------------------------- 1 | #version 140 2 | 3 | uniform mat4 matrix; 4 | 5 | uniform float alpha; 6 | 7 | in vec3 position; 8 | in vec2 texcoord; 9 | in vec3 color; 10 | 11 | out vec2 v_texcoord; 12 | out vec4 v_color; 13 | 14 | void main() { 15 | v_texcoord = texcoord; 16 | v_color = vec4(color, alpha); 17 | gl_Position = matrix * vec4(position, 1.0); 18 | } 19 | -------------------------------------------------------------------------------- /src/viewer/model_viewer/texture_cache.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use crate::db::{Database, TextureId, PaletteId}; 3 | use glium::{Texture2d, texture::RawImage2d}; 4 | use crate::nds; 5 | 6 | type Display = glium::Display; 7 | 8 | // TODO: move somewhere more important. 9 | pub type ImageId = (TextureId, Option); 10 | 11 | /// Maintains cache of all the GL texture we use. 12 | pub struct TextureCache { 13 | /// Caches GL texture for image ids. 14 | table: HashMap, 15 | /// Texture used for materials with no texture. 16 | white_texture_: Texture2d, 17 | /// Texture used when textures are missing, etc. 18 | error_texture_: Texture2d, 19 | } 20 | 21 | impl TextureCache { 22 | pub fn new(display: &Display) -> TextureCache { 23 | // 1x1 white texture 24 | let white_image = RawImage2d::from_raw_rgba( 25 | vec![255, 255, 255, 255u8], 26 | (1, 1), 27 | ); 28 | let white_texture_ = Texture2d::new(display, white_image).unwrap(); 29 | 30 | // 1x1 magenta texture 31 | let error_image = RawImage2d::from_raw_rgba( 32 | vec![255, 0, 255, 255u8], 33 | (1, 1), 34 | ); 35 | let error_texture_ = Texture2d::new(display, error_image).unwrap(); 36 | 37 | TextureCache { 38 | table: HashMap::with_capacity(10), 39 | white_texture_, 40 | error_texture_, 41 | } 42 | } 43 | 44 | pub fn lookup(&self, image_id: ImageId) -> &Texture2d { 45 | self.table.get(&image_id).unwrap_or(self.error_texture()) 46 | } 47 | 48 | /// Ensure the given image_id is in the cache. 49 | pub fn create(&mut self, display: &Display, db: &Database, image_id: ImageId) { 50 | use std::collections::hash_map::Entry; 51 | match self.table.entry(image_id) { 52 | Entry::Occupied(_) => (), 53 | Entry::Vacant(ve) => { 54 | let texture = &db.textures[image_id.0]; 55 | let palette = image_id.1.map(|pal_id| &db.palettes[pal_id]); 56 | let rgba = match nds::decode_texture(texture, palette) { 57 | Ok(rgba) => rgba, 58 | Err(_) => return, 59 | }; 60 | let dim = texture.params.dim(); 61 | let image = RawImage2d::from_raw_rgba_reversed(&rgba.0, dim); 62 | ve.insert(Texture2d::new(display, image).unwrap()); 63 | } 64 | } 65 | } 66 | 67 | pub fn clear(&mut self) { 68 | self.table.clear(); 69 | } 70 | 71 | pub fn white_texture(&self) -> &Texture2d { 72 | &self.white_texture_ 73 | } 74 | 75 | pub fn error_texture(&self) -> &Texture2d { 76 | &self.error_texture_ 77 | } 78 | } 79 | --------------------------------------------------------------------------------