├── .gitignore ├── Cargo.toml ├── LICENSE ├── deny.toml ├── examples └── print.rs ├── readme.md └── src ├── extensions.rs ├── lib.rs └── primitive_reader.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | examples 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "goth-gltf" 3 | version = "0.1.1" 4 | edition = "2021" 5 | license = "MIT" 6 | description = "A lightweight, low-level reader for gltf (and glb) files" 7 | repository = "https://github.com/expenses/goth-gltf/" 8 | readme = "readme.md" 9 | 10 | [dependencies] 11 | thiserror = { version = "1.0.40", optional = true } 12 | bytemuck = { version = "1.13.1", optional = true } 13 | nanoserde = "0.1.32" 14 | 15 | [features] 16 | primitive_reader = ["bytemuck", "thiserror"] 17 | names = [] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ashley Ruglys 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # This template contains all of the possible sections and their default values 2 | 3 | # Note that all fields that take a lint level have these possible values: 4 | # * deny - An error will be produced and the check will fail 5 | # * warn - A warning will be produced, but the check will not fail 6 | # * allow - No warning or error will be produced, though in some cases a note 7 | # will be 8 | 9 | # The values provided in this template are the default values that will be used 10 | # when any section or field is not specified in your own configuration 11 | 12 | # If 1 or more target triples (and optionally, target_features) are specified, 13 | # only the specified targets will be checked when running `cargo deny check`. 14 | # This means, if a particular package is only ever used as a target specific 15 | # dependency, such as, for example, the `nix` crate only being used via the 16 | # `target_family = "unix"` configuration, that only having windows targets in 17 | # this list would mean the nix crate, as well as any of its exclusive 18 | # dependencies not shared by any other crates, would be ignored, as the target 19 | # list here is effectively saying which targets you are building for. 20 | targets = [ 21 | # The triple can be any string, but only the target triples built in to 22 | # rustc (as of 1.40) can be checked against actual config expressions 23 | #{ triple = "x86_64-unknown-linux-musl" }, 24 | # You can also specify which target_features you promise are enabled for a 25 | # particular target. target_features are currently not validated against 26 | # the actual valid features supported by the target architecture. 27 | #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, 28 | ] 29 | 30 | # This section is considered when running `cargo deny check advisories` 31 | # More documentation for the advisories section can be found here: 32 | # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html 33 | [advisories] 34 | # The path where the advisory database is cloned/fetched into 35 | db-path = "~/.cargo/advisory-db" 36 | # The url(s) of the advisory databases to use 37 | db-urls = ["https://github.com/rustsec/advisory-db"] 38 | # The lint level for security vulnerabilities 39 | vulnerability = "deny" 40 | # The lint level for unmaintained crates 41 | unmaintained = "warn" 42 | # The lint level for crates that have been yanked from their source registry 43 | yanked = "warn" 44 | # The lint level for crates with security notices. Note that as of 45 | # 2019-12-17 there are no security notice advisories in 46 | # https://github.com/rustsec/advisory-db 47 | notice = "warn" 48 | # A list of advisory IDs to ignore. Note that ignored advisories will still 49 | # output a note when they are encountered. 50 | ignore = [ 51 | #"RUSTSEC-0000-0000", 52 | ] 53 | # Threshold for security vulnerabilities, any vulnerability with a CVSS score 54 | # lower than the range specified will be ignored. Note that ignored advisories 55 | # will still output a note when they are encountered. 56 | # * None - CVSS Score 0.0 57 | # * Low - CVSS Score 0.1 - 3.9 58 | # * Medium - CVSS Score 4.0 - 6.9 59 | # * High - CVSS Score 7.0 - 8.9 60 | # * Critical - CVSS Score 9.0 - 10.0 61 | #severity-threshold = 62 | 63 | # If this is true, then cargo deny will use the git executable to fetch advisory database. 64 | # If this is false, then it uses a built-in git library. 65 | # Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support. 66 | # See Git Authentication for more information about setting up git authentication. 67 | #git-fetch-with-cli = true 68 | 69 | # This section is considered when running `cargo deny check licenses` 70 | # More documentation for the licenses section can be found here: 71 | # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html 72 | [licenses] 73 | # The lint level for crates which do not have a detectable license 74 | unlicensed = "deny" 75 | # List of explicitly allowed licenses 76 | # See https://spdx.org/licenses/ for list of possible licenses 77 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. 78 | allow = [ 79 | "MIT", 80 | #"Apache-2.0", 81 | #"Apache-2.0 WITH LLVM-exception", 82 | ] 83 | # List of explicitly disallowed licenses 84 | # See https://spdx.org/licenses/ for list of possible licenses 85 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. 86 | deny = [ 87 | #"Nokia", 88 | ] 89 | # Lint level for licenses considered copyleft 90 | copyleft = "warn" 91 | # Blanket approval or denial for OSI-approved or FSF Free/Libre licenses 92 | # * both - The license will be approved if it is both OSI-approved *AND* FSF 93 | # * either - The license will be approved if it is either OSI-approved *OR* FSF 94 | # * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF 95 | # * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved 96 | # * neither - This predicate is ignored and the default lint level is used 97 | allow-osi-fsf-free = "neither" 98 | # Lint level used when no other predicates are matched 99 | # 1. License isn't in the allow or deny lists 100 | # 2. License isn't copyleft 101 | # 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" 102 | default = "deny" 103 | # The confidence threshold for detecting a license from license text. 104 | # The higher the value, the more closely the license text must be to the 105 | # canonical license text of a valid SPDX license file. 106 | # [possible values: any between 0.0 and 1.0]. 107 | confidence-threshold = 0.8 108 | # Allow 1 or more licenses on a per-crate basis, so that particular licenses 109 | # aren't accepted for every possible crate as with the normal allow list 110 | exceptions = [ 111 | # Each entry is the crate and version constraint, and its specific allow 112 | # list 113 | #{ allow = ["Zlib"], name = "adler32", version = "*" }, 114 | ] 115 | 116 | # Some crates don't have (easily) machine readable licensing information, 117 | # adding a clarification entry for it allows you to manually specify the 118 | # licensing information 119 | #[[licenses.clarify]] 120 | # The name of the crate the clarification applies to 121 | #name = "ring" 122 | # The optional version constraint for the crate 123 | #version = "*" 124 | # The SPDX expression for the license requirements of the crate 125 | #expression = "MIT AND ISC AND OpenSSL" 126 | # One or more files in the crate's source used as the "source of truth" for 127 | # the license expression. If the contents match, the clarification will be used 128 | # when running the license check, otherwise the clarification will be ignored 129 | # and the crate will be checked normally, which may produce warnings or errors 130 | # depending on the rest of your configuration 131 | #license-files = [ 132 | # Each entry is a crate relative path, and the (opaque) hash of its contents 133 | #{ path = "LICENSE", hash = 0xbd0eed23 } 134 | #] 135 | 136 | [licenses.private] 137 | # If true, ignores workspace crates that aren't published, or are only 138 | # published to private registries. 139 | # To see how to mark a crate as unpublished (to the official registry), 140 | # visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. 141 | ignore = false 142 | # One or more private registries that you might publish crates to, if a crate 143 | # is only published to private registries, and ignore is true, the crate will 144 | # not have its license(s) checked 145 | registries = [ 146 | #"https://sekretz.com/registry 147 | ] 148 | 149 | # This section is considered when running `cargo deny check bans`. 150 | # More documentation about the 'bans' section can be found here: 151 | # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html 152 | [bans] 153 | # Lint level for when multiple versions of the same crate are detected 154 | multiple-versions = "warn" 155 | # Lint level for when a crate version requirement is `*` 156 | wildcards = "allow" 157 | # The graph highlighting used when creating dotgraphs for crates 158 | # with multiple versions 159 | # * lowest-version - The path to the lowest versioned duplicate is highlighted 160 | # * simplest-path - The path to the version with the fewest edges is highlighted 161 | # * all - Both lowest-version and simplest-path are used 162 | highlight = "all" 163 | # List of crates that are allowed. Use with care! 164 | allow = [ 165 | #{ name = "ansi_term", version = "=0.11.0" }, 166 | ] 167 | # List of crates to deny 168 | deny = [ 169 | # Each entry the name of a crate and a version range. If version is 170 | # not specified, all versions will be matched. 171 | #{ name = "ansi_term", version = "=0.11.0" }, 172 | # 173 | # Wrapper crates can optionally be specified to allow the crate when it 174 | # is a direct dependency of the otherwise banned crate 175 | #{ name = "ansi_term", version = "=0.11.0", wrappers = [] }, 176 | ] 177 | # Certain crates/versions that will be skipped when doing duplicate detection. 178 | skip = [ 179 | #{ name = "ansi_term", version = "=0.11.0" }, 180 | ] 181 | # Similarly to `skip` allows you to skip certain crates during duplicate 182 | # detection. Unlike skip, it also includes the entire tree of transitive 183 | # dependencies starting at the specified crate, up to a certain depth, which is 184 | # by default infinite 185 | skip-tree = [ 186 | #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, 187 | ] 188 | 189 | # This section is considered when running `cargo deny check sources`. 190 | # More documentation about the 'sources' section can be found here: 191 | # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html 192 | [sources] 193 | # Lint level for what to happen when a crate from a crate registry that is not 194 | # in the allow list is encountered 195 | unknown-registry = "warn" 196 | # Lint level for what to happen when a crate from a git repository that is not 197 | # in the allow list is encountered 198 | unknown-git = "warn" 199 | # List of URLs for allowed crate registries. Defaults to the crates.io index 200 | # if not specified. If it is specified but empty, no registries are allowed. 201 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 202 | # List of URLs for allowed Git repositories 203 | allow-git = ["https://github.com/expenses/nanoserde"] 204 | 205 | [sources.allow-org] 206 | # 1 or more github.com organizations to allow git sources for 207 | github = [] 208 | # 1 or more gitlab.com organizations to allow git sources for 209 | gitlab = [] 210 | # 1 or more bitbucket.org organizations to allow git sources for 211 | bitbucket = [] 212 | -------------------------------------------------------------------------------- /examples/print.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let filename = std::env::args().nth(1).unwrap(); 3 | let bytes = std::fs::read(&filename).unwrap(); 4 | let (gltf, _): ( 5 | goth_gltf::Gltf, 6 | _, 7 | ) = goth_gltf::Gltf::from_bytes(&bytes).unwrap(); 8 | println!("{:#?}", gltf); 9 | } 10 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # goth-gltf 2 | 3 | Goth-gltf aims to be a low-level, unopinionated reader for gltf files. 4 | 5 | Basic example: 6 | ```rust 7 | let filename = std::env::args().nth(1).unwrap(); 8 | let bytes = std::fs::read(&filename).unwrap(); 9 | let (gltf, _): ( 10 | goth_gltf::Gltf, 11 | _, 12 | ) = goth_gltf::Gltf::from_bytes(&bytes).unwrap(); 13 | println!("{:#?}", gltf); 14 | ``` 15 | 16 | ## In comparison with [gltf-rs], it: 17 | 18 | - Represents the gltf JSON structure transparently 19 | - Uses nanoserde instead of serde 20 | - Supports a wider range of extensions 21 | - Has no code specific for loading images or reading attributes out of buffers 22 | 23 | ## Extensions Implemented 24 | 25 | - `KHR_lights_punctual` 26 | - `KHR_materials_emissive_strength` 27 | - `KHR_materials_ior` 28 | - `KHR_materials_sheen` 29 | - `KHR_materials_unlit` 30 | - `KHR_texture_basisu` 31 | - `KHR_texture_transform` 32 | - `EXT_mesh_gpu_instancing` 33 | - `EXT_meshopt_compression` 34 | - `MSFT_lod` 35 | - `MSFT_screencoverage` 36 | 37 | [gltf-rs]: https://github.com/gltf-rs/gltf 38 | 39 | License: MIT 40 | -------------------------------------------------------------------------------- /src/extensions.rs: -------------------------------------------------------------------------------- 1 | use crate::{Extensions, TextureInfo}; 2 | use nanoserde::DeJson; 3 | 4 | #[derive(Debug, DeJson, Clone, Copy)] 5 | pub struct KhrTextureBasisu { 6 | pub source: usize, 7 | } 8 | 9 | #[derive(Debug, DeJson, Clone, Copy)] 10 | pub struct KhrTextureTransform { 11 | #[nserde(default)] 12 | pub offset: [f32; 2], 13 | #[nserde(default)] 14 | pub rotation: f32, 15 | #[nserde(default = "[1.0, 1.0]")] 16 | pub scale: [f32; 2], 17 | #[nserde(rename = "texCoord")] 18 | #[nserde(default)] 19 | pub tex_coord: usize, 20 | } 21 | 22 | #[derive(Debug, DeJson, Clone)] 23 | pub struct KhrMaterialsSheen { 24 | #[nserde(rename = "sheenColorFactor")] 25 | #[nserde(default)] 26 | pub sheen_color_factor: [f32; 3], 27 | #[nserde(rename = "sheenColorTexture")] 28 | pub sheen_color_texture: Option>, 29 | #[nserde(rename = "sheenRoughnessFactor")] 30 | #[nserde(default)] 31 | pub sheen_roughness_factor: f32, 32 | #[nserde(rename = "sheenRoughnessTexture")] 33 | pub sheen_roughness_texture: Option>, 34 | } 35 | 36 | #[derive(Debug, DeJson, Clone, Copy)] 37 | pub struct KhrMaterialsEmissiveStrength { 38 | #[nserde(rename = "emissiveStrength")] 39 | #[nserde(default = "1.0")] 40 | pub emissive_strength: f32, 41 | } 42 | 43 | #[derive(Debug, DeJson, Clone, Copy)] 44 | pub struct KhrMaterialsUnlit {} 45 | 46 | #[derive(Debug, DeJson, Clone)] 47 | pub struct KhrMaterialsSpecular { 48 | #[nserde(rename = "specularFactor")] 49 | #[nserde(default = "1.0")] 50 | pub specular_factor: f32, 51 | #[nserde(rename = "specularTexture")] 52 | pub specular_texture: Option>, 53 | #[nserde(rename = "specularColorFactor", default = "[1.0, 1.0, 1.0]")] 54 | pub specular_color_factor: [f32; 3], 55 | #[nserde(rename = "specularColorTexture")] 56 | pub specular_color_texture: Option>, 57 | } 58 | 59 | #[derive(Debug, DeJson, Clone)] 60 | pub struct KhrMaterialsTransmission { 61 | #[nserde(rename = "transmissionFactor")] 62 | #[nserde(default = "1.0")] 63 | pub transmission_factor: f32, 64 | #[nserde(rename = "transmissionTexture")] 65 | pub transmission_texture: Option>, 66 | } 67 | 68 | #[derive(Debug, DeJson, Clone)] 69 | pub struct KhrLightsPunctual { 70 | #[nserde(default)] 71 | pub lights: Vec, 72 | } 73 | 74 | #[derive(Debug, DeJson, Clone, Copy)] 75 | pub struct Light { 76 | #[nserde(default = "[1.0, 1.0, 1.0]")] 77 | pub color: [f32; 3], 78 | #[nserde(default = "1.0")] 79 | pub intensity: f32, 80 | #[nserde(rename = "type")] 81 | pub ty: LightType, 82 | pub spot: Option, 83 | } 84 | 85 | #[derive(Debug, DeJson, Clone, Copy)] 86 | pub enum LightType { 87 | #[nserde(rename = "point")] 88 | Point, 89 | #[nserde(rename = "directional")] 90 | Directional, 91 | #[nserde(rename = "spot")] 92 | Spot, 93 | } 94 | 95 | #[derive(Debug, DeJson, Clone, Copy)] 96 | pub struct LightSpot { 97 | #[nserde(rename = "innerConeAngle")] 98 | #[nserde(default)] 99 | pub inner_cone_angle: f32, 100 | #[nserde(rename = "outerConeAngle")] 101 | #[nserde(default = "std::f32::consts::FRAC_PI_4")] 102 | pub outer_cone_angle: f32, 103 | } 104 | 105 | #[derive(Debug, DeJson, Clone, Copy)] 106 | pub struct KhrMaterialsIor { 107 | #[nserde(default = "1.5")] 108 | pub ior: f32, 109 | } 110 | 111 | #[derive(Debug, DeJson, Clone, Copy)] 112 | pub struct ExtMeshoptCompression { 113 | pub buffer: usize, 114 | #[nserde(rename = "byteOffset")] 115 | #[nserde(default)] 116 | pub byte_offset: usize, 117 | #[nserde(rename = "byteLength")] 118 | pub byte_length: usize, 119 | #[nserde(rename = "byteStride")] 120 | pub byte_stride: usize, 121 | pub mode: CompressionMode, 122 | pub count: usize, 123 | #[nserde(default)] 124 | pub filter: CompressionFilter, 125 | } 126 | 127 | #[derive(Debug, DeJson, PartialEq, Eq, Clone, Copy)] 128 | pub enum CompressionMode { 129 | #[nserde(rename = "ATTRIBUTES")] 130 | Attributes, 131 | #[nserde(rename = "TRIANGLES")] 132 | Triangles, 133 | #[nserde(rename = "INDICES")] 134 | Indices, 135 | } 136 | 137 | #[derive(Debug, DeJson, PartialEq, Eq, Clone, Copy)] 138 | pub enum CompressionFilter { 139 | #[nserde(rename = "NONE")] 140 | None, 141 | #[nserde(rename = "OCTAHEDRAL")] 142 | Octahedral, 143 | #[nserde(rename = "QUATERNION")] 144 | Quaternion, 145 | #[nserde(rename = "EXPONENTIAL")] 146 | Exponential, 147 | } 148 | 149 | impl Default for CompressionFilter { 150 | fn default() -> Self { 151 | Self::None 152 | } 153 | } 154 | 155 | #[derive(Debug, DeJson, Clone, Copy)] 156 | pub struct ExtMeshoptCompressionBuffer { 157 | #[nserde(default)] 158 | pub fallback: bool, 159 | } 160 | 161 | #[derive(Debug, DeJson, Clone, Copy)] 162 | pub struct ExtMeshGpuInstancing { 163 | pub attributes: ExtMeshGpuInstancingAttributes, 164 | } 165 | 166 | #[derive(Debug, DeJson, Clone, Copy)] 167 | pub struct ExtMeshGpuInstancingAttributes { 168 | #[nserde(rename = "ROTATION")] 169 | pub rotation: usize, 170 | #[nserde(rename = "SCALE")] 171 | pub scale: usize, 172 | #[nserde(rename = "TRANSLATION")] 173 | pub translation: usize, 174 | } 175 | 176 | #[derive(Debug, DeJson, Clone)] 177 | pub struct MsftLod { 178 | pub ids: Vec, 179 | } 180 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Goth-gltf aims to be a low-level, unopinionated reader for gltf files. 2 | //! 3 | //! Basic example: 4 | //! ```no_run 5 | //! let filename = std::env::args().nth(1).unwrap(); 6 | //! let bytes = std::fs::read(&filename).unwrap(); 7 | //! let (gltf, _): ( 8 | //! goth_gltf::Gltf, 9 | //! _, 10 | //! ) = goth_gltf::Gltf::from_bytes(&bytes).unwrap(); 11 | //! println!("{:#?}", gltf); 12 | //! ``` 13 | //! 14 | //! # In comparison with [gltf-rs], it: 15 | //! 16 | //! - Represents the gltf JSON structure transparently 17 | //! - Uses nanoserde instead of serde 18 | //! - Supports a wider range of extensions 19 | //! - Has no code specific for loading images or reading attributes out of buffers 20 | //! 21 | //! # Extensions Implemented 22 | //! 23 | //! - `KHR_lights_punctual` 24 | //! - `KHR_materials_emissive_strength` 25 | //! - `KHR_materials_ior` 26 | //! - `KHR_materials_sheen` 27 | //! - `KHR_materials_unlit` 28 | //! - `KHR_texture_basisu` 29 | //! - `KHR_texture_transform` 30 | //! - `EXT_mesh_gpu_instancing` 31 | //! - `EXT_meshopt_compression` 32 | //! - `MSFT_lod` 33 | //! - `MSFT_screencoverage` 34 | //! 35 | //! [gltf-rs]: https://github.com/gltf-rs/gltf 36 | 37 | #![allow(clippy::question_mark)] 38 | 39 | pub mod extensions; 40 | /// Basic support for reading primitive data from buffer views and accessors. 41 | #[cfg(feature = "primitive_reader")] 42 | pub mod primitive_reader; 43 | 44 | use nanoserde::DeJson; 45 | use std::fmt::Debug; 46 | 47 | pub trait Extensions: DeJson { 48 | type RootExtensions: DeJson + Default + Debug + Clone; 49 | type TextureExtensions: DeJson + Default + Debug + Clone; 50 | type TextureInfoExtensions: DeJson + Default + Debug + Clone; 51 | type MaterialExtensions: DeJson + Default + Debug + Clone; 52 | type BufferExtensions: DeJson + Default + Debug + Clone; 53 | type NodeExtensions: DeJson + Default + Debug + Clone; 54 | type NodeExtras: DeJson + Default + Debug + Clone; 55 | type BufferViewExtensions: DeJson + Default + Debug + Clone; 56 | } 57 | 58 | impl Extensions for () { 59 | type RootExtensions = (); 60 | type TextureExtensions = (); 61 | type TextureInfoExtensions = (); 62 | type MaterialExtensions = (); 63 | type BufferExtensions = (); 64 | type NodeExtensions = (); 65 | type NodeExtras = (); 66 | type BufferViewExtensions = (); 67 | } 68 | 69 | /// A parsed gltf document. 70 | #[derive(Debug, DeJson)] 71 | pub struct Gltf { 72 | #[nserde(default)] 73 | pub images: Vec, 74 | #[nserde(default)] 75 | pub textures: Vec>, 76 | #[nserde(default)] 77 | pub materials: Vec>, 78 | #[nserde(default)] 79 | pub buffers: Vec>, 80 | #[nserde(rename = "bufferViews")] 81 | #[nserde(default)] 82 | pub buffer_views: Vec>, 83 | #[nserde(default)] 84 | pub accessors: Vec, 85 | #[nserde(default)] 86 | pub meshes: Vec, 87 | #[nserde(default)] 88 | pub animations: Vec, 89 | #[nserde(default)] 90 | pub nodes: Vec>, 91 | #[nserde(default)] 92 | pub skins: Vec, 93 | #[nserde(default)] 94 | pub samplers: Vec, 95 | #[nserde(default)] 96 | pub cameras: Vec, 97 | #[nserde(default)] 98 | pub extensions: E::RootExtensions, 99 | #[nserde(default)] 100 | pub scenes: Vec, 101 | #[nserde(default)] 102 | pub scene: usize, 103 | } 104 | 105 | impl Gltf { 106 | /// Load a gltf from either a gltf or a glb file. 107 | /// 108 | /// In the case of a .glb, the binary buffer chunk will be returned as well. 109 | pub fn from_bytes(bytes: &[u8]) -> Result<(Self, Option<&[u8]>), nanoserde::DeJsonErr> { 110 | // Check for the 4-byte magic. 111 | if !bytes.starts_with(b"glTF") { 112 | return Ok((Self::from_json_bytes(bytes)?, None)); 113 | } 114 | 115 | // There's always a json chunk at the start: 116 | // https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#structured-json-content 117 | 118 | let json_chunk_length = u32::from_le_bytes(bytes[12..16].try_into().unwrap()); 119 | 120 | let json_chunk_end = 20 + json_chunk_length as usize; 121 | 122 | let json_chunk_bytes = &bytes[20..20 + json_chunk_length as usize]; 123 | 124 | let json = Self::from_json_bytes(json_chunk_bytes)?; 125 | 126 | let binary_buffer = if bytes.len() != json_chunk_end { 127 | Some(&bytes[json_chunk_end + 8..]) 128 | } else { 129 | None 130 | }; 131 | 132 | Ok((json, binary_buffer)) 133 | } 134 | 135 | pub fn from_json_bytes(bytes: &[u8]) -> Result { 136 | match std::str::from_utf8(bytes) { 137 | Ok(string) => Self::from_json_string(string), 138 | Err(error) => Err(nanoserde::DeJsonState::default().err_parse(&error.to_string())), 139 | } 140 | } 141 | 142 | pub fn from_json_string(string: &str) -> Result { 143 | Self::deserialize_json(string) 144 | } 145 | } 146 | 147 | #[derive(Debug, DeJson)] 148 | pub struct Skin { 149 | #[nserde(rename = "inverseBindMatrices")] 150 | pub inverse_bind_matrices: Option, 151 | pub skeleton: Option, 152 | pub joints: Vec, 153 | #[cfg(feature = "names")] 154 | pub name: Option, 155 | } 156 | 157 | #[derive(Debug, DeJson)] 158 | pub struct Animation { 159 | pub channels: Vec, 160 | pub samplers: Vec, 161 | #[cfg(feature = "names")] 162 | pub name: Option, 163 | } 164 | 165 | #[derive(Debug, DeJson)] 166 | pub struct Channel { 167 | pub sampler: usize, 168 | pub target: Target, 169 | } 170 | 171 | #[derive(Debug, DeJson)] 172 | pub struct Target { 173 | pub node: Option, 174 | pub path: TargetPath, 175 | } 176 | 177 | #[derive(Debug, DeJson)] 178 | pub struct AnimationSampler { 179 | pub input: usize, 180 | #[nserde(default)] 181 | pub interpolation: Interpolation, 182 | pub output: usize, 183 | } 184 | 185 | #[derive(Debug, DeJson, Clone, Copy)] 186 | pub enum Interpolation { 187 | #[nserde(rename = "LINEAR")] 188 | Linear, 189 | #[nserde(rename = "STEP")] 190 | Step, 191 | #[nserde(rename = "CUBICSPLINE")] 192 | CubicSpline, 193 | } 194 | 195 | impl Default for Interpolation { 196 | fn default() -> Self { 197 | Self::Linear 198 | } 199 | } 200 | 201 | #[derive(Debug, DeJson)] 202 | pub enum TargetPath { 203 | #[nserde(rename = "translation")] 204 | Translation, 205 | #[nserde(rename = "rotation")] 206 | Rotation, 207 | #[nserde(rename = "scale")] 208 | Scale, 209 | #[nserde(rename = "weights")] 210 | Weights, 211 | } 212 | 213 | #[derive(Debug, DeJson)] 214 | pub struct Buffer { 215 | pub uri: Option, 216 | #[nserde(rename = "byteLength")] 217 | pub byte_length: usize, 218 | #[cfg(feature = "names")] 219 | pub name: Option, 220 | #[nserde(default)] 221 | pub extensions: E::BufferExtensions, 222 | } 223 | 224 | #[derive(Debug, DeJson)] 225 | pub struct Node { 226 | pub camera: Option, 227 | #[nserde(default)] 228 | pub children: Vec, 229 | pub skin: Option, 230 | pub matrix: Option<[f32; 16]>, 231 | pub mesh: Option, 232 | pub rotation: Option<[f32; 4]>, 233 | pub scale: Option<[f32; 3]>, 234 | pub translation: Option<[f32; 3]>, 235 | #[cfg(feature = "names")] 236 | pub name: Option, 237 | #[nserde(default)] 238 | pub extensions: E::NodeExtensions, 239 | #[nserde(default)] 240 | pub extras: E::NodeExtras, 241 | } 242 | 243 | impl Node { 244 | pub fn transform(&self) -> NodeTransform { 245 | match self.matrix { 246 | Some(matrix) => match (self.translation, self.rotation, self.scale) { 247 | // If both a matrix and a full transform set is specified, then just use the transform. 248 | (Some(translation), Some(rotation), Some(scale)) => NodeTransform::Set { 249 | translation, 250 | rotation, 251 | scale, 252 | }, 253 | _ => NodeTransform::Matrix(matrix), 254 | }, 255 | None => { 256 | let translation = self.translation.unwrap_or([0.0; 3]); 257 | let rotation = self.rotation.unwrap_or([0.0, 0.0, 0.0, 1.0]); 258 | let scale = self.scale.unwrap_or([1.0; 3]); 259 | NodeTransform::Set { 260 | translation, 261 | rotation, 262 | scale, 263 | } 264 | } 265 | } 266 | } 267 | } 268 | 269 | pub enum NodeTransform { 270 | Matrix([f32; 16]), 271 | Set { 272 | translation: [f32; 3], 273 | rotation: [f32; 4], 274 | scale: [f32; 3], 275 | }, 276 | } 277 | 278 | #[derive(Debug, DeJson)] 279 | pub struct Mesh { 280 | pub primitives: Vec, 281 | pub weights: Option>, 282 | #[cfg(feature = "names")] 283 | pub name: Option, 284 | } 285 | 286 | #[derive(Debug, DeJson)] 287 | pub struct Primitive { 288 | pub attributes: Attributes, 289 | pub indices: Option, 290 | pub material: Option, 291 | #[nserde(default)] 292 | pub mode: PrimitiveMode, 293 | pub targets: Option>, 294 | } 295 | 296 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 297 | pub enum PrimitiveMode { 298 | Points, 299 | Lines, 300 | LineLoop, 301 | LineStrip, 302 | Triangles, 303 | TriangleStrip, 304 | TriangleFan, 305 | } 306 | 307 | impl Default for PrimitiveMode { 308 | fn default() -> Self { 309 | Self::Triangles 310 | } 311 | } 312 | 313 | impl DeJson for PrimitiveMode { 314 | fn de_json( 315 | state: &mut nanoserde::DeJsonState, 316 | input: &mut core::str::Chars, 317 | ) -> Result { 318 | let ty = match &state.tok { 319 | nanoserde::DeJsonTok::U64(ty) => match ty { 320 | 0 => Self::Points, 321 | 1 => Self::Lines, 322 | 2 => Self::LineLoop, 323 | 3 => Self::LineStrip, 324 | 4 => Self::Triangles, 325 | 5 => Self::TriangleStrip, 326 | 6 => Self::TriangleFan, 327 | _ => return Err(state.err_range(&ty.to_string())), 328 | }, 329 | _ => return Err(state.err_token("U64")), 330 | }; 331 | 332 | state.next_tok(input)?; 333 | 334 | Ok(ty) 335 | } 336 | } 337 | 338 | #[derive(Debug, DeJson)] 339 | pub struct Attributes { 340 | #[nserde(rename = "POSITION")] 341 | pub position: Option, 342 | #[nserde(rename = "TANGENT")] 343 | pub tangent: Option, 344 | #[nserde(rename = "NORMAL")] 345 | pub normal: Option, 346 | #[nserde(rename = "TEXCOORD_0")] 347 | pub texcoord_0: Option, 348 | #[nserde(rename = "TEXCOORD_1")] 349 | pub texcoord_1: Option, 350 | #[nserde(rename = "JOINTS_0")] 351 | pub joints_0: Option, 352 | #[nserde(rename = "WEIGHTS_0")] 353 | pub weights_0: Option, 354 | } 355 | 356 | #[derive(Debug, DeJson, Clone)] 357 | pub struct Image { 358 | pub uri: Option, 359 | #[nserde(rename = "mimeType")] 360 | pub mime_type: Option, 361 | #[nserde(rename = "bufferView")] 362 | pub buffer_view: Option, 363 | #[cfg(feature = "names")] 364 | pub name: Option, 365 | } 366 | 367 | #[derive(Debug, DeJson)] 368 | pub struct Texture { 369 | pub sampler: Option, 370 | pub source: Option, 371 | #[cfg(feature = "names")] 372 | pub name: Option, 373 | #[nserde(default)] 374 | pub extensions: E::TextureExtensions, 375 | } 376 | 377 | #[derive(Debug, DeJson)] 378 | pub struct BufferView { 379 | pub buffer: usize, 380 | #[nserde(rename = "byteOffset")] 381 | #[nserde(default)] 382 | pub byte_offset: usize, 383 | #[nserde(rename = "byteLength")] 384 | pub byte_length: usize, 385 | #[nserde(rename = "byteStride")] 386 | pub byte_stride: Option, 387 | #[cfg(feature = "names")] 388 | pub name: Option, 389 | #[nserde(default)] 390 | pub extensions: E::BufferViewExtensions, 391 | } 392 | 393 | #[derive(Debug, DeJson)] 394 | pub struct Accessor { 395 | #[nserde(rename = "bufferView")] 396 | pub buffer_view: Option, 397 | #[nserde(rename = "byteOffset")] 398 | #[nserde(default)] 399 | pub byte_offset: usize, 400 | #[nserde(rename = "componentType")] 401 | pub component_type: ComponentType, 402 | #[nserde(default)] 403 | pub normalized: bool, 404 | pub count: usize, 405 | #[nserde(rename = "type")] 406 | pub accessor_type: AccessorType, 407 | pub sparse: Option, 408 | // todo: these could be changed to enum { Int, Float }. 409 | pub min: Option>, 410 | pub max: Option>, 411 | #[cfg(feature = "names")] 412 | pub name: Option, 413 | } 414 | 415 | impl Accessor { 416 | pub fn byte_length(&self, buffer_view: &BufferView) -> usize { 417 | self.count 418 | * buffer_view.byte_stride.unwrap_or_else(|| { 419 | self.component_type.byte_size() * self.accessor_type.num_components() 420 | }) 421 | } 422 | } 423 | 424 | #[derive(Debug, DeJson)] 425 | pub struct Sparse { 426 | pub count: usize, 427 | pub indices: SparseIndices, 428 | pub values: SparseValues, 429 | } 430 | 431 | #[derive(Debug, DeJson)] 432 | pub struct SparseIndices { 433 | #[nserde(rename = "bufferView")] 434 | pub buffer_view: usize, 435 | #[nserde(rename = "byteOffset")] 436 | #[nserde(default)] 437 | pub byte_offset: usize, 438 | #[nserde(rename = "componentType")] 439 | pub component_type: ComponentType, 440 | } 441 | 442 | #[derive(Debug, DeJson)] 443 | pub struct SparseValues { 444 | #[nserde(rename = "bufferView")] 445 | pub buffer_view: usize, 446 | #[nserde(rename = "byteOffset")] 447 | #[nserde(default)] 448 | pub byte_offset: usize, 449 | } 450 | 451 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 452 | pub enum ComponentType { 453 | UnsignedByte, 454 | Byte, 455 | UnsignedShort, 456 | Short, 457 | UnsignedInt, 458 | Float, 459 | } 460 | 461 | impl ComponentType { 462 | pub fn byte_size(&self) -> usize { 463 | match self { 464 | Self::UnsignedByte | Self::Byte => 1, 465 | Self::UnsignedShort | Self::Short => 2, 466 | Self::UnsignedInt | Self::Float => 4, 467 | } 468 | } 469 | } 470 | 471 | impl DeJson for ComponentType { 472 | fn de_json( 473 | state: &mut nanoserde::DeJsonState, 474 | input: &mut core::str::Chars, 475 | ) -> Result { 476 | let ty = match &state.tok { 477 | nanoserde::DeJsonTok::U64(ty) => match ty { 478 | 5120 => Self::Byte, 479 | 5121 => Self::UnsignedByte, 480 | 5122 => Self::Short, 481 | 5123 => Self::UnsignedShort, 482 | 5125 => Self::UnsignedInt, 483 | 5126 => Self::Float, 484 | _ => return Err(state.err_range(&ty.to_string())), 485 | }, 486 | _ => return Err(state.err_token("U64")), 487 | }; 488 | 489 | state.next_tok(input)?; 490 | 491 | Ok(ty) 492 | } 493 | } 494 | 495 | #[derive(Debug, DeJson, PartialEq)] 496 | pub enum AccessorType { 497 | #[nserde(rename = "SCALAR")] 498 | Scalar, 499 | #[nserde(rename = "VEC2")] 500 | Vec2, 501 | #[nserde(rename = "VEC3")] 502 | Vec3, 503 | #[nserde(rename = "VEC4")] 504 | Vec4, 505 | #[nserde(rename = "MAT2")] 506 | Mat2, 507 | #[nserde(rename = "MAT3")] 508 | Mat3, 509 | #[nserde(rename = "MAT4")] 510 | Mat4, 511 | } 512 | 513 | impl AccessorType { 514 | pub fn num_components(&self) -> usize { 515 | match self { 516 | Self::Scalar => 1, 517 | Self::Vec2 => 2, 518 | Self::Vec3 => 3, 519 | Self::Vec4 | Self::Mat2 => 4, 520 | Self::Mat3 => 9, 521 | Self::Mat4 => 16, 522 | } 523 | } 524 | } 525 | 526 | #[derive(Debug, DeJson, Clone)] 527 | pub struct Material { 528 | #[nserde(rename = "pbrMetallicRoughness")] 529 | #[nserde(default)] 530 | pub pbr_metallic_roughness: PbrMetallicRoughness, 531 | #[nserde(rename = "normalTexture")] 532 | pub normal_texture: Option>, 533 | #[nserde(rename = "occlusionTexture")] 534 | pub occlusion_texture: Option>, 535 | #[nserde(rename = "emissiveTexture")] 536 | pub emissive_texture: Option>, 537 | #[nserde(rename = "emissiveFactor")] 538 | #[nserde(default)] 539 | pub emissive_factor: [f32; 3], 540 | #[nserde(rename = "alphaMode")] 541 | #[nserde(default)] 542 | pub alpha_mode: AlphaMode, 543 | #[nserde(rename = "alphaCutoff")] 544 | #[nserde(default = "0.5")] 545 | pub alpha_cutoff: f32, 546 | #[nserde(rename = "doubleSided")] 547 | #[nserde(default)] 548 | pub double_sided: bool, 549 | #[cfg(feature = "names")] 550 | pub name: Option, 551 | #[nserde(default)] 552 | pub extensions: E::MaterialExtensions, 553 | } 554 | 555 | #[derive(Debug, DeJson, Clone, Copy)] 556 | pub enum AlphaMode { 557 | #[nserde(rename = "OPAQUE")] 558 | Opaque, 559 | #[nserde(rename = "MASK")] 560 | Mask, 561 | #[nserde(rename = "BLEND")] 562 | Blend, 563 | } 564 | 565 | impl Default for AlphaMode { 566 | fn default() -> Self { 567 | Self::Opaque 568 | } 569 | } 570 | 571 | #[derive(Debug, DeJson, Clone)] 572 | pub struct PbrMetallicRoughness { 573 | #[nserde(rename = "baseColorFactor")] 574 | #[nserde(default = "[1.0, 1.0, 1.0, 1.0]")] 575 | pub base_color_factor: [f32; 4], 576 | #[nserde(rename = "baseColorTexture")] 577 | pub base_color_texture: Option>, 578 | #[nserde(rename = "metallicFactor")] 579 | #[nserde(default = "1.0")] 580 | pub metallic_factor: f32, 581 | #[nserde(rename = "roughnessFactor")] 582 | #[nserde(default = "1.0")] 583 | pub roughness_factor: f32, 584 | #[nserde(rename = "metallicRoughnessTexture")] 585 | pub metallic_roughness_texture: Option>, 586 | } 587 | 588 | impl Default for PbrMetallicRoughness { 589 | fn default() -> Self { 590 | Self { 591 | base_color_factor: [1.0; 4], 592 | base_color_texture: None, 593 | metallic_factor: 1.0, 594 | roughness_factor: 1.0, 595 | metallic_roughness_texture: None, 596 | } 597 | } 598 | } 599 | 600 | #[derive(Debug, DeJson, Clone)] 601 | pub struct TextureInfo { 602 | pub index: usize, 603 | #[nserde(rename = "texCoord")] 604 | #[nserde(default)] 605 | pub tex_coord: usize, 606 | #[nserde(default)] 607 | pub extensions: E::TextureInfoExtensions, 608 | } 609 | 610 | #[derive(Debug, DeJson, Clone)] 611 | pub struct NormalTextureInfo { 612 | pub index: usize, 613 | #[nserde(rename = "texCoord")] 614 | #[nserde(default)] 615 | pub tex_coord: usize, 616 | #[nserde(default = "1.0")] 617 | pub scale: f32, 618 | #[nserde(default)] 619 | pub extensions: E::TextureInfoExtensions, 620 | } 621 | 622 | #[derive(Debug, DeJson, Clone)] 623 | pub struct OcclusionTextureInfo { 624 | pub index: usize, 625 | #[nserde(rename = "texCoord")] 626 | #[nserde(default)] 627 | pub tex_coord: usize, 628 | #[nserde(default = "1.0")] 629 | pub strength: f32, 630 | #[nserde(default)] 631 | pub extensions: E::TextureInfoExtensions, 632 | } 633 | 634 | #[derive(Debug, DeJson)] 635 | pub struct Sampler { 636 | #[nserde(rename = "magFilter")] 637 | pub mag_filter: Option, 638 | #[nserde(rename = "minFilter")] 639 | pub min_filter: Option, 640 | #[nserde(rename = "wrapS")] 641 | #[nserde(default)] 642 | pub wrap_s: SamplerWrap, 643 | #[nserde(rename = "wrapT")] 644 | #[nserde(default)] 645 | pub wrap_t: SamplerWrap, 646 | #[cfg(feature = "names")] 647 | pub name: Option, 648 | } 649 | 650 | #[derive(Debug)] 651 | pub enum FilterMode { 652 | Nearest, 653 | Linear, 654 | } 655 | 656 | impl DeJson for FilterMode { 657 | fn de_json( 658 | state: &mut nanoserde::DeJsonState, 659 | input: &mut core::str::Chars, 660 | ) -> Result { 661 | let ty = match &state.tok { 662 | nanoserde::DeJsonTok::U64(ty) => match ty { 663 | 9728 => Self::Nearest, 664 | 9729 => Self::Linear, 665 | _ => return Err(state.err_range(&ty.to_string())), 666 | }, 667 | _ => return Err(state.err_token("U64")), 668 | }; 669 | 670 | state.next_tok(input)?; 671 | 672 | Ok(ty) 673 | } 674 | } 675 | 676 | #[derive(Debug)] 677 | pub struct MinFilter { 678 | pub mode: FilterMode, 679 | pub mipmap: Option, 680 | } 681 | 682 | impl DeJson for MinFilter { 683 | fn de_json( 684 | state: &mut nanoserde::DeJsonState, 685 | input: &mut core::str::Chars, 686 | ) -> Result { 687 | let ty = match &state.tok { 688 | nanoserde::DeJsonTok::U64(ty) => match ty { 689 | 9728 => Self { 690 | mode: FilterMode::Nearest, 691 | mipmap: None, 692 | }, 693 | 9729 => Self { 694 | mode: FilterMode::Linear, 695 | mipmap: None, 696 | }, 697 | 9984 => Self { 698 | mode: FilterMode::Nearest, 699 | mipmap: Some(FilterMode::Nearest), 700 | }, 701 | 9985 => Self { 702 | mode: FilterMode::Linear, 703 | mipmap: Some(FilterMode::Nearest), 704 | }, 705 | 9986 => Self { 706 | mode: FilterMode::Nearest, 707 | mipmap: Some(FilterMode::Linear), 708 | }, 709 | 9987 => Self { 710 | mode: FilterMode::Linear, 711 | mipmap: Some(FilterMode::Linear), 712 | }, 713 | _ => return Err(state.err_range(&ty.to_string())), 714 | }, 715 | _ => return Err(state.err_token("U64")), 716 | }; 717 | 718 | state.next_tok(input)?; 719 | 720 | Ok(ty) 721 | } 722 | } 723 | 724 | #[derive(Debug)] 725 | pub enum SamplerWrap { 726 | ClampToEdge, 727 | MirroredRepeat, 728 | Repeat, 729 | } 730 | 731 | impl DeJson for SamplerWrap { 732 | fn de_json( 733 | state: &mut nanoserde::DeJsonState, 734 | input: &mut core::str::Chars, 735 | ) -> Result { 736 | let ty = match &state.tok { 737 | nanoserde::DeJsonTok::U64(ty) => match ty { 738 | 33071 => Self::ClampToEdge, 739 | 33648 => Self::MirroredRepeat, 740 | 10497 => Self::Repeat, 741 | _ => return Err(state.err_range(&ty.to_string())), 742 | }, 743 | _ => return Err(state.err_token("U64")), 744 | }; 745 | 746 | state.next_tok(input)?; 747 | 748 | Ok(ty) 749 | } 750 | } 751 | 752 | impl Default for SamplerWrap { 753 | fn default() -> Self { 754 | Self::Repeat 755 | } 756 | } 757 | 758 | #[derive(Debug, DeJson)] 759 | pub struct Camera { 760 | pub perspective: Option, 761 | pub orthographic: Option, 762 | #[nserde(rename = "type")] 763 | pub ty: CameraType, 764 | #[cfg(feature = "names")] 765 | pub name: Option, 766 | } 767 | 768 | #[derive(Debug, DeJson)] 769 | pub struct CameraPerspective { 770 | pub yfov: f32, 771 | pub znear: f32, 772 | pub zfar: Option, 773 | #[nserde(rename = "aspectRatio")] 774 | pub aspect_ratio: Option, 775 | } 776 | 777 | #[derive(Debug, DeJson, Clone, Copy)] 778 | pub struct CameraOrthographic { 779 | pub xmag: f32, 780 | pub ymag: f32, 781 | pub zfar: f32, 782 | pub znear: f32, 783 | } 784 | 785 | #[derive(Debug, DeJson)] 786 | pub enum CameraType { 787 | #[nserde(rename = "perspective")] 788 | Perspective, 789 | #[nserde(rename = "orthographic")] 790 | Orthographic, 791 | } 792 | 793 | #[derive(Debug, DeJson, Clone)] 794 | pub struct Scene { 795 | pub nodes: Vec, 796 | #[cfg(feature = "names")] 797 | pub name: Option, 798 | } 799 | 800 | pub mod default_extensions { 801 | use crate::extensions; 802 | use nanoserde::DeJson; 803 | 804 | #[derive(Debug, Default, Clone, Copy, DeJson)] 805 | pub struct Extensions; 806 | 807 | impl super::Extensions for Extensions { 808 | type RootExtensions = RootExtensions; 809 | type TextureExtensions = TextureExtensions; 810 | type TextureInfoExtensions = TextureInfoExtensions; 811 | type MaterialExtensions = MaterialExtensions; 812 | type BufferExtensions = BufferExtensions; 813 | type NodeExtensions = NodeExtensions; 814 | type NodeExtras = NodeExtras; 815 | type BufferViewExtensions = BufferViewExtensions; 816 | } 817 | 818 | #[derive(Debug, DeJson, Default, Clone)] 819 | pub struct RootExtensions { 820 | #[nserde(rename = "KHR_lights_punctual")] 821 | pub khr_lights_punctual: Option, 822 | } 823 | 824 | #[derive(Debug, DeJson, Default, Clone)] 825 | pub struct BufferExtensions { 826 | #[nserde(rename = "EXT_meshopt_compression")] 827 | pub ext_meshopt_compression: Option, 828 | } 829 | 830 | #[derive(Debug, DeJson, Default, Clone)] 831 | pub struct NodeExtensions { 832 | #[nserde(rename = "EXT_mesh_gpu_instancing")] 833 | pub ext_mesh_gpu_instancing: Option, 834 | #[nserde(rename = "MSFT_lod")] 835 | pub msft_lod: Option, 836 | } 837 | 838 | #[derive(Debug, DeJson, Default, Clone)] 839 | pub struct NodeExtras { 840 | #[nserde(rename = "MSFT_screencoverage")] 841 | pub msft_screencoverage: Option>, 842 | } 843 | 844 | #[derive(Debug, Default, DeJson, Clone)] 845 | pub struct TextureExtensions { 846 | #[nserde(rename = "KHR_texture_basisu")] 847 | pub khr_texture_basisu: Option, 848 | } 849 | 850 | #[derive(Debug, DeJson, Default, Clone)] 851 | pub struct BufferViewExtensions { 852 | #[nserde(rename = "EXT_meshopt_compression")] 853 | pub ext_meshopt_compression: Option, 854 | } 855 | 856 | #[derive(Debug, DeJson, Default, Clone)] 857 | pub struct MaterialExtensions { 858 | #[nserde(rename = "KHR_materials_sheen")] 859 | pub khr_materials_sheen: Option>, 860 | #[nserde(rename = "KHR_materials_emissive_strength")] 861 | pub khr_materials_emissive_strength: Option, 862 | #[nserde(rename = "KHR_materials_unlit")] 863 | pub khr_materials_unlit: Option, 864 | #[nserde(rename = "KHR_materials_ior")] 865 | pub khr_materials_ior: Option, 866 | #[nserde(rename = "KHR_materials_specular")] 867 | pub khr_materials_specular: Option>, 868 | #[nserde(rename = "KHR_materials_transmission")] 869 | pub khr_materials_transmission: Option>, 870 | } 871 | 872 | #[derive(Debug, DeJson, Default, Clone, Copy)] 873 | pub struct TextureInfoExtensions { 874 | #[nserde(rename = "KHR_texture_transform")] 875 | pub khr_texture_transform: Option, 876 | } 877 | } 878 | -------------------------------------------------------------------------------- /src/primitive_reader.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use std::borrow::Cow; 3 | use std::collections::HashMap; 4 | use thiserror::Error; 5 | 6 | pub trait MeshOptCompressionExtension { 7 | fn ext_meshopt_compression(&self) -> Option; 8 | } 9 | 10 | impl MeshOptCompressionExtension for crate::default_extensions::BufferViewExtensions { 11 | fn ext_meshopt_compression(&self) -> Option { 12 | self.ext_meshopt_compression 13 | } 14 | } 15 | 16 | impl MeshOptCompressionExtension for () { 17 | fn ext_meshopt_compression(&self) -> Option { 18 | None 19 | } 20 | } 21 | 22 | fn unsigned_short_to_float(short: u16) -> f32 { 23 | short as f32 / 65535.0 24 | } 25 | 26 | fn unsigned_byte_to_float(byte: u8) -> f32 { 27 | byte as f32 / 255.0 28 | } 29 | 30 | fn signed_byte_to_float(byte: i8) -> f32 { 31 | (byte as f32 / 127.0).max(-1.0) 32 | } 33 | 34 | fn signed_short_to_float(short: i16) -> f32 { 35 | (short as f32 / 32767.0).max(-1.0) 36 | } 37 | 38 | fn byte_stride( 39 | accessor: &crate::Accessor, 40 | buffer_view: &crate::BufferView, 41 | ) -> usize 42 | where 43 | E::BufferViewExtensions: MeshOptCompressionExtension, 44 | { 45 | buffer_view 46 | .extensions 47 | .ext_meshopt_compression() 48 | .map(|ext| ext.byte_stride) 49 | .or(buffer_view.byte_stride) 50 | .unwrap_or_else(|| { 51 | accessor.component_type.byte_size() * accessor.accessor_type.num_components() 52 | }) 53 | } 54 | 55 | #[derive(Error, Debug)] 56 | pub enum Error { 57 | #[error("Accessor is missing buffer view")] 58 | AccessorMissingBufferView, 59 | #[error("Buffer view index {0} out of bounds")] 60 | BufferViewIndexOutOfBounds(usize), 61 | #[error("Accessor index {0} out of bounds")] 62 | AccessorIndexOutOfBounds(usize), 63 | #[error("{0}: Unsupported combination of component type, normalized and byte stride: {1:?}")] 64 | UnsupportedCombination(u32, (ComponentType, bool, Option)), 65 | } 66 | 67 | pub fn read_buffer_with_accessor<'a, E: Extensions>( 68 | buffer_view_map: &'a HashMap>, 69 | gltf: &'a crate::Gltf, 70 | accessor: &crate::Accessor, 71 | ) -> Result<(&'a [u8], Option), Error> 72 | where 73 | E::BufferViewExtensions: MeshOptCompressionExtension, 74 | { 75 | let buffer_view_index = accessor 76 | .buffer_view 77 | .ok_or(Error::AccessorMissingBufferView)?; 78 | let buffer_view = gltf 79 | .buffer_views 80 | .get(buffer_view_index) 81 | .ok_or(Error::BufferViewIndexOutOfBounds(buffer_view_index))?; 82 | 83 | let start = accessor.byte_offset; 84 | let end = start + accessor.count * byte_stride(accessor, buffer_view); 85 | 86 | let buffer_view_bytes = buffer_view_map 87 | .get(&buffer_view_index) 88 | .ok_or(Error::BufferViewIndexOutOfBounds(buffer_view_index))?; 89 | 90 | // Force the end of the slice to be in-bounds as either the maths for calculating 91 | // `end` is wrong or some files are a little odd. 92 | let end = end.min(buffer_view_bytes.len()); 93 | 94 | let slice = &buffer_view_bytes[start..end]; 95 | 96 | Ok((slice, buffer_view.byte_stride)) 97 | } 98 | 99 | pub fn read_f32<'a>( 100 | slice: &'a [u8], 101 | byte_stride: Option, 102 | accessor: &crate::Accessor, 103 | ) -> Result, Error> { 104 | Ok( 105 | match (accessor.component_type, accessor.normalized, byte_stride) { 106 | (ComponentType::Float, false, None) => Cow::Borrowed(bytemuck::cast_slice(slice)), 107 | other => return Err(Error::UnsupportedCombination(std::line!(), other)), 108 | }, 109 | ) 110 | } 111 | 112 | pub fn read_f32x3<'a>( 113 | slice: &'a [u8], 114 | byte_stride: Option, 115 | accessor: &crate::Accessor, 116 | ) -> Result, Error> { 117 | Ok( 118 | match (accessor.component_type, accessor.normalized, byte_stride) { 119 | (ComponentType::Float, false, None | Some(12)) => { 120 | let slice: &[f32] = bytemuck::cast_slice(slice); 121 | Cow::Owned( 122 | slice 123 | .chunks(3) 124 | .map(|slice| <[f32; 3]>::try_from(slice).unwrap()) 125 | .collect(), 126 | ) 127 | } 128 | (ComponentType::Short, true, Some(stride)) => { 129 | let slice: &[i16] = bytemuck::cast_slice(slice); 130 | Cow::Owned( 131 | slice 132 | .chunks(stride / 2) 133 | .map(|slice| std::array::from_fn(|i| signed_short_to_float(slice[i]))) 134 | .collect(), 135 | ) 136 | } 137 | (ComponentType::UnsignedShort, false, Some(8)) => { 138 | let slice: &[u16] = bytemuck::cast_slice(slice); 139 | Cow::Owned( 140 | slice 141 | .chunks(4) 142 | .map(move |slice| std::array::from_fn(|i| slice[i] as f32)) 143 | .collect(), 144 | ) 145 | } 146 | (ComponentType::UnsignedShort, true, Some(8)) => { 147 | let slice: &[u16] = bytemuck::cast_slice(slice); 148 | Cow::Owned( 149 | slice 150 | .chunks(4) 151 | .map(|slice| std::array::from_fn(|i| unsigned_short_to_float(slice[i]))) 152 | .collect(), 153 | ) 154 | } 155 | (ComponentType::Byte, true, Some(stride)) => Cow::Owned( 156 | slice 157 | .chunks(stride) 158 | .map(move |slice| std::array::from_fn(|i| signed_byte_to_float(slice[i] as i8))) 159 | .collect(), 160 | ), 161 | other => return Err(Error::UnsupportedCombination(std::line!(), other)), 162 | }, 163 | ) 164 | } 165 | 166 | fn read_f32x2<'a>( 167 | slice: &'a [u8], 168 | byte_stride: Option, 169 | accessor: &crate::Accessor, 170 | ) -> Result, Error> { 171 | Ok( 172 | match (accessor.component_type, accessor.normalized, byte_stride) { 173 | (ComponentType::Float, false, None | Some(8)) => { 174 | Cow::Borrowed(bytemuck::cast_slice(slice)) 175 | } 176 | (ComponentType::Float, false, Some(stride)) => { 177 | let slice: &[f32] = bytemuck::cast_slice(slice); 178 | Cow::Owned( 179 | slice 180 | .chunks(stride / 4) 181 | .map(move |slice| std::array::from_fn(|i| slice[i])) 182 | .collect(), 183 | ) 184 | } 185 | (ComponentType::UnsignedShort, true, Some(stride)) => { 186 | let slice: &[u16] = bytemuck::cast_slice(slice); 187 | Cow::Owned( 188 | slice 189 | .chunks(stride / 2) 190 | .map(move |slice| { 191 | std::array::from_fn(|i| unsigned_short_to_float(slice[i])) 192 | }) 193 | .collect(), 194 | ) 195 | } 196 | other => return Err(Error::UnsupportedCombination(std::line!(), other)), 197 | }, 198 | ) 199 | } 200 | 201 | unsafe fn cast_slice(bytes: &[u8]) -> &[T] { 202 | std::slice::from_raw_parts( 203 | bytes.as_ptr() as *const T, 204 | bytes.len() / std::mem::size_of::(), 205 | ) 206 | } 207 | 208 | pub fn read_f32x4<'a>( 209 | slice: &'a [u8], 210 | byte_stride: Option, 211 | accessor: &crate::Accessor, 212 | ) -> Result, Error> { 213 | Ok( 214 | match (accessor.component_type, accessor.normalized, byte_stride) { 215 | (ComponentType::Float, false, None) => { 216 | // bytemuck::cast_slice panics with an alignment issue on wasm so we just use unsafe for this. 217 | // todo: might be wrong. 218 | Cow::Borrowed(unsafe { cast_slice(slice) }) 219 | } 220 | (ComponentType::UnsignedByte, true, Some(4)) => Cow::Owned( 221 | slice 222 | .chunks(4) 223 | .map(move |slice| std::array::from_fn(|i| unsigned_byte_to_float(slice[i]))) 224 | .collect(), 225 | ), 226 | (ComponentType::Short, true, None) => { 227 | let slice: &[[i16; 4]] = bytemuck::cast_slice(slice); 228 | Cow::Owned( 229 | slice 230 | .iter() 231 | .map(|slice| std::array::from_fn(|i| signed_short_to_float(slice[i]))) 232 | .collect(), 233 | ) 234 | } 235 | other => return Err(Error::UnsupportedCombination(std::line!(), other)), 236 | }, 237 | ) 238 | } 239 | 240 | fn read_u32<'a>( 241 | slice: &'a [u8], 242 | byte_stride: Option, 243 | accessor: &crate::Accessor, 244 | ) -> Result, Error> { 245 | Ok( 246 | match (accessor.component_type, accessor.normalized, byte_stride) { 247 | (ComponentType::UnsignedShort, false, None) => { 248 | let slice: &[u16] = bytemuck::cast_slice(slice); 249 | Cow::Owned(slice.iter().map(|&i| i as u32).collect()) 250 | } 251 | (ComponentType::UnsignedInt, false, None) => Cow::Borrowed(bytemuck::cast_slice(slice)), 252 | other => return Err(Error::UnsupportedCombination(std::line!(), other)), 253 | }, 254 | ) 255 | } 256 | 257 | fn read_u32x4<'a>( 258 | slice: &'a [u8], 259 | byte_stride: Option, 260 | accessor: &crate::Accessor, 261 | ) -> Result, Error> { 262 | Ok( 263 | match (accessor.component_type, accessor.normalized, byte_stride) { 264 | (ComponentType::UnsignedByte, false, Some(4) | None) => Cow::Owned( 265 | slice 266 | .chunks(4) 267 | .map(|slice| std::array::from_fn(|i| slice[i] as u32)) 268 | .collect(), 269 | ), 270 | other => return Err(Error::UnsupportedCombination(std::line!(), other)), 271 | }, 272 | ) 273 | } 274 | 275 | pub struct PrimitiveReader<'a, E: Extensions> { 276 | gltf: &'a crate::Gltf, 277 | pub primitive: &'a crate::Primitive, 278 | buffer_view_map: &'a HashMap>, 279 | } 280 | 281 | impl<'a, E: Extensions> PrimitiveReader<'a, E> 282 | where 283 | E::BufferViewExtensions: MeshOptCompressionExtension, 284 | { 285 | pub fn new( 286 | gltf: &'a crate::Gltf, 287 | primitive: &'a crate::Primitive, 288 | buffer_view_map: &'a HashMap>, 289 | ) -> Self { 290 | Self { 291 | gltf, 292 | primitive, 293 | buffer_view_map, 294 | } 295 | } 296 | 297 | pub fn read_indices(&self) -> Result>, Error> { 298 | let accessor_index = match self.primitive.indices { 299 | Some(index) => index, 300 | None => return Ok(None), 301 | }; 302 | 303 | let accessor = self 304 | .gltf 305 | .accessors 306 | .get(accessor_index) 307 | .ok_or(Error::AccessorIndexOutOfBounds(accessor_index))?; 308 | let (slice, byte_stride) = 309 | read_buffer_with_accessor(self.buffer_view_map, self.gltf, accessor)?; 310 | 311 | Ok(Some(read_u32(slice, byte_stride, accessor)?)) 312 | } 313 | 314 | pub fn read_positions(&self) -> Result>, Error> { 315 | let accessor_index = match self.primitive.attributes.position { 316 | Some(index) => index, 317 | None => return Ok(None), 318 | }; 319 | 320 | let accessor = self 321 | .gltf 322 | .accessors 323 | .get(accessor_index) 324 | .ok_or(Error::AccessorIndexOutOfBounds(accessor_index))?; 325 | let (slice, byte_stride) = 326 | read_buffer_with_accessor(self.buffer_view_map, self.gltf, accessor)?; 327 | 328 | Ok(Some(read_f32x3(slice, byte_stride, accessor)?)) 329 | } 330 | 331 | pub fn read_normals(&self) -> Result>, Error> { 332 | let accessor_index = match self.primitive.attributes.normal { 333 | Some(index) => index, 334 | None => return Ok(None), 335 | }; 336 | 337 | let accessor = self 338 | .gltf 339 | .accessors 340 | .get(accessor_index) 341 | .ok_or(Error::AccessorIndexOutOfBounds(accessor_index))?; 342 | let (slice, byte_stride) = 343 | read_buffer_with_accessor(self.buffer_view_map, self.gltf, accessor)?; 344 | 345 | Ok(Some(read_f32x3(slice, byte_stride, accessor)?)) 346 | } 347 | 348 | pub fn read_uvs(&self) -> Result>, Error> { 349 | let accessor_index = match self.primitive.attributes.texcoord_0 { 350 | Some(index) => index, 351 | None => return Ok(None), 352 | }; 353 | 354 | let accessor = self 355 | .gltf 356 | .accessors 357 | .get(accessor_index) 358 | .ok_or(Error::AccessorIndexOutOfBounds(accessor_index))?; 359 | let (slice, byte_stride) = 360 | read_buffer_with_accessor(self.buffer_view_map, self.gltf, accessor)?; 361 | 362 | Ok(Some(read_f32x2(slice, byte_stride, accessor)?)) 363 | } 364 | 365 | pub fn read_second_uvs(&self) -> Result>, Error> { 366 | let accessor_index = match self.primitive.attributes.texcoord_1 { 367 | Some(index) => index, 368 | None => return Ok(None), 369 | }; 370 | 371 | let accessor = self 372 | .gltf 373 | .accessors 374 | .get(accessor_index) 375 | .ok_or(Error::AccessorIndexOutOfBounds(accessor_index))?; 376 | let (slice, byte_stride) = 377 | read_buffer_with_accessor(self.buffer_view_map, self.gltf, accessor)?; 378 | 379 | Ok(Some(read_f32x2(slice, byte_stride, accessor)?)) 380 | } 381 | 382 | pub fn read_joints(&self) -> Result>, Error> { 383 | let accessor_index = match self.primitive.attributes.joints_0 { 384 | Some(index) => index, 385 | None => return Ok(None), 386 | }; 387 | 388 | let accessor = self 389 | .gltf 390 | .accessors 391 | .get(accessor_index) 392 | .ok_or(Error::AccessorIndexOutOfBounds(accessor_index))?; 393 | 394 | let (slice, byte_stride) = 395 | read_buffer_with_accessor(self.buffer_view_map, self.gltf, accessor)?; 396 | 397 | Ok(Some(read_u32x4(slice, byte_stride, accessor)?)) 398 | } 399 | 400 | pub fn read_weights(&self) -> Result>, Error> { 401 | let accessor_index = match self.primitive.attributes.weights_0 { 402 | Some(index) => index, 403 | None => return Ok(None), 404 | }; 405 | 406 | let accessor = self 407 | .gltf 408 | .accessors 409 | .get(accessor_index) 410 | .ok_or(Error::AccessorIndexOutOfBounds(accessor_index))?; 411 | let (slice, byte_stride) = 412 | read_buffer_with_accessor(self.buffer_view_map, self.gltf, accessor)?; 413 | 414 | Ok(Some(read_f32x4(slice, byte_stride, accessor)?)) 415 | } 416 | } 417 | --------------------------------------------------------------------------------