├── .gitignore ├── Pipfile ├── Pipfile.lock ├── README.md ├── banner-camera.gltf ├── cesium_demo.gif ├── cgfx.hexpat ├── cgfx ├── __init__.py ├── animation.py ├── canm.py ├── cenv.py ├── cflt.py ├── cgfx.py ├── cmdl.py ├── dict.py ├── luts.py ├── mtob.py ├── patricia.py ├── primitives.py ├── shared.py ├── sobj.py ├── swizzler.py └── txob.py ├── main.py └── normal_demo.gif /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | __pycache__/ 3 | *.gltf 4 | *.glb 5 | *.bin 6 | *.cgfx 7 | *.png 8 | *.jpg 9 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | gltflib = "*" 8 | pillow = "*" 9 | 10 | [dev-packages] 11 | 12 | [requires] 13 | python_version = "3.12" 14 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "69e57b23c860242fcf725b44826128d3594cf632679249adde12b0e64197d9ba" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.13" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "dataclasses-json": { 20 | "hashes": [ 21 | "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", 22 | "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0" 23 | ], 24 | "markers": "python_version >= '3.7' and python_version < '4.0'", 25 | "version": "==0.6.7" 26 | }, 27 | "gltflib": { 28 | "hashes": [ 29 | "sha256:8a09ac0159bf7b957c04e634e500e099ac2765a55025f531d6a2bd546b8a02d4", 30 | "sha256:8d18bf90045e05d3a93022cb3e37774a542bde9f8609090b66006da677646184" 31 | ], 32 | "index": "pypi", 33 | "markers": "python_full_version >= '3.6.0'", 34 | "version": "==1.0.13" 35 | }, 36 | "marshmallow": { 37 | "hashes": [ 38 | "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", 39 | "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6" 40 | ], 41 | "markers": "python_version >= '3.9'", 42 | "version": "==3.26.1" 43 | }, 44 | "mypy-extensions": { 45 | "hashes": [ 46 | "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", 47 | "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558" 48 | ], 49 | "markers": "python_version >= '3.8'", 50 | "version": "==1.1.0" 51 | }, 52 | "packaging": { 53 | "hashes": [ 54 | "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", 55 | "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" 56 | ], 57 | "markers": "python_version >= '3.8'", 58 | "version": "==25.0" 59 | }, 60 | "pillow": { 61 | "hashes": [ 62 | "sha256:014ca0050c85003620526b0ac1ac53f56fc93af128f7546623cc8e31875ab928", 63 | "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b", 64 | "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", 65 | "sha256:0c3e6d0f59171dfa2e25d7116217543310908dfa2770aa64b8f87605f8cacc97", 66 | "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4", 67 | "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193", 68 | "sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95", 69 | "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941", 70 | "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f", 71 | "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", 72 | "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3", 73 | "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044", 74 | "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", 75 | "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", 76 | "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d", 77 | "sha256:2728567e249cdd939f6cc3d1f049595c66e4187f3c34078cbc0a7d21c47482d2", 78 | "sha256:2b490402c96f907a166615e9a5afacf2519e28295f157ec3a2bb9bd57de638cb", 79 | "sha256:312c77b7f07ab2139924d2639860e084ec2a13e72af54d4f08ac843a5fc9c79d", 80 | "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", 81 | "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70", 82 | "sha256:3692b68c87096ac6308296d96354eddd25f98740c9d2ab54e1549d6c8aea9d79", 83 | "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", 84 | "sha256:39ad2e0f424394e3aebc40168845fee52df1394a4673a6ee512d840d14ab3013", 85 | "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d", 86 | "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", 87 | "sha256:4b835d89c08a6c2ee7781b8dd0a30209a8012b5f09c0a665b65b0eb3560b6f36", 88 | "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7", 89 | "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", 90 | "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", 91 | "sha256:562d11134c97a62fe3af29581f083033179f7ff435f78392565a1ad2d1c2c45c", 92 | "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c", 93 | "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", 94 | "sha256:6ebce70c3f486acf7591a3d73431fa504a4e18a9b97ff27f5f47b7368e4b9dd1", 95 | "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd", 96 | "sha256:7491cf8a79b8eb867d419648fff2f83cb0b3891c8b36da92cc7f1931d46108c8", 97 | "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", 98 | "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", 99 | "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b", 100 | "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f", 101 | "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", 102 | "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed", 103 | "sha256:85d27ea4c889342f7e35f6d56e7e1cb345632ad592e8c51b693d7b7556043ce0", 104 | "sha256:8b02d8f9cb83c52578a0b4beadba92e37d83a4ef11570a8688bbf43f4ca50909", 105 | "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", 106 | "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788", 107 | "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", 108 | "sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156", 109 | "sha256:9bc7ae48b8057a611e5fe9f853baa88093b9a76303937449397899385da06fad", 110 | "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076", 111 | "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", 112 | "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e", 113 | "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", 114 | "sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772", 115 | "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", 116 | "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", 117 | "sha256:b10428b3416d4f9c61f94b494681280be7686bda15898a3a9e08eb66a6d92d67", 118 | "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4", 119 | "sha256:b4ba4be812c7a40280629e55ae0b14a0aafa150dd6451297562e1764808bbe61", 120 | "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8", 121 | "sha256:bf2c33d6791c598142f00c9c4c7d47f6476731c31081331664eb26d6ab583e01", 122 | "sha256:c27476257b2fdcd7872d54cfd119b3a9ce4610fb85c8e32b70b42e3680a29a1e", 123 | "sha256:c8bd62331e5032bc396a93609982a9ab6b411c05078a52f5fe3cc59234a3abd1", 124 | "sha256:c97209e85b5be259994eb5b69ff50c5d20cca0f458ef9abd835e262d9d88b39d", 125 | "sha256:cc1c3bc53befb6096b84165956e886b1729634a799e9d6329a0c512ab651e579", 126 | "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6", 127 | "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", 128 | "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7", 129 | "sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047", 130 | "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443", 131 | "sha256:dd6b20b93b3ccc9c1b597999209e4bc5cf2853f9ee66e3fc9a400a78733ffc9a", 132 | "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf", 133 | "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", 134 | "sha256:e616e7154c37669fc1dfc14584f11e284e05d1c650e1c0f972f281c4ccc53193", 135 | "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600", 136 | "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c", 137 | "sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363", 138 | "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e", 139 | "sha256:f781dcb0bc9929adc77bad571b8621ecb1e4cdef86e940fe2e5b5ee24fd33b35", 140 | "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", 141 | "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", 142 | "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b" 143 | ], 144 | "index": "pypi", 145 | "markers": "python_version >= '3.9'", 146 | "version": "==11.2.1" 147 | }, 148 | "typing-extensions": { 149 | "hashes": [ 150 | "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", 151 | "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef" 152 | ], 153 | "markers": "python_version >= '3.8'", 154 | "version": "==4.13.2" 155 | }, 156 | "typing-inspect": { 157 | "hashes": [ 158 | "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", 159 | "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78" 160 | ], 161 | "version": "==0.9.0" 162 | } 163 | }, 164 | "develop": {} 165 | } 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## pycgfx 2 | `pycgfx` is a tool for converting glTF models into the CGFX (aka BCRES) format supported by the Nintendo 3DS's home menu banners, as well as the Python library backing it. 3 | 4 | [Normal texture demo](https://github.com/KhronosGroup/glTF-Sample-Assets/blob/main/Models/CompareNormal/README.md) 5 |
6 | ![The Compare Normal sample model](normal_demo.gif) 7 | 8 | [Skinned mesh animation demo](https://github.com/KhronosGroup/glTF-Sample-Assets/blob/main/Models/CesiumMan/README.md) 9 |
10 | ![The Cesium Man sample model](cesium_demo.gif) 11 | 12 | The tool supports a number of glTF features: 13 | * .gltf and .glb files 14 | * Diffuse and normal textures 15 | * Constant roughness factor, for specular lighting 16 | * Double-sided materials and alpha blend settings 17 | * Animations and skinning 18 | * Diffuse colour animation using `KHR_animation_pointer` 19 | 20 | All animations are played at once. 21 | 22 | ## Usage 23 | 24 | The `gltflib` and `pillow` libraries are required. They can be installed via `pip`, or by running `pipenv sync`. 25 | 26 | Following this, simply run the `main.py` script through the command line, with your input file as an argument: 27 | ```bash 28 | # With pipenv 29 | pipenv run python main.py input.glb 30 | # Windows, no pipenv 31 | python main.py input.glb 32 | # macOS / Linux, no pipenv 33 | python3 main.py input.glb 34 | ``` 35 | CGFX files larger than 512KB are not supported by the 3DS, and this tool will print a warning if one is generated. 36 | 37 | Some features useful for banners, such as billboarding (useful for logos), are not supported by the glTF specification. 38 | These can be enabled after processing using ImHex (see below), or by modifying the script. 39 | If there is demand, I'm open to working with users to figure out some kind of config file for configuring lights or marking bones as billboards. 40 | 41 | ## Additional assets 42 | Also included in the repository is an [ImHex](https://imhex.werwolv.net/) pattern ([cgfx.hexpat](https://github.com/skyfloogle/pycgfx/blob/main/cgfx.hexpat)), with which CGFX files can be viewed and edited. 43 | Be warned that reading it uses several gigabytes of RAM. 44 | 45 | There is also a small glTF ([banner.gltf](https://github.com/skyfloogle/pycgfx/blob/main/banner-camera.gltf)) specifying the camera used in the 3DS home menu, for use during creation of banner models. 46 | -------------------------------------------------------------------------------- /banner-camera.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset": { 3 | "version": "2.0" 4 | }, 5 | "cameras": [ 6 | { 7 | "name": "Banner Camera", 8 | "type": "perspective", 9 | "perspective": { 10 | "aspectRatio": 1.66666666667, 11 | "yfov": 0.523599, 12 | "zfar": 1000, 13 | "znear": 26.5 14 | } 15 | } 16 | ], 17 | "nodes": [ 18 | { 19 | "name": "Banner Camera", 20 | "camera": 0, 21 | "translation": [0, 1, 44.786] 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /cesium_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyfloogle/pycgfx/1f78850086f3a77c41e07162e842f97a5bf3c18a/cesium_demo.gif -------------------------------------------------------------------------------- /cgfx.hexpat: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | // This pattern allows for inspecting and modifying CGFX files using ImHex. 6 | // WARNING: Due to the large number of nested templates, evaluating this pattern uses 7 | // several gigabytes of memory. 8 | 9 | // https://github.com/PabloMK7/EveryFileExplorer/blob/43c08c11af58567f06a7118bb9af88ba74e810ba/3DS/NintendoWare/GFX/CMDL.cs#L763 10 | // https://github.com/gdkchan/Ohana3DS-Rebirth/blob/master/Ohana3DS%20Rebirth/Ohana/Models/CGFX.cs 11 | 12 | struct RelPtr { 13 | u32 ptr [[no_unique_address, hidden]]; 14 | if (ptr == 0) { 15 | padding[4]; 16 | } else { 17 | T *data: s32 [[inline, pointer_base("std::ptr::relative_to_pointer")]]; 18 | } 19 | }; 20 | 21 | struct ListPtr { 22 | u32 count; 23 | T *entries[count]: s32 [[inline, pointer_base("std::ptr::relative_to_pointer")]]; 24 | }; 25 | 26 | using Matrix = std::Array, 3>; 27 | using OrientationMatrix = std::Array, 3>; 28 | using NullString = std::string::NullString; 29 | 30 | bitfield MTOBFlags { 31 | bool isFragmentLightEnabled: 1; 32 | bool iVertexLightEnabled: 1; 33 | bool isHemiSphereLighEnabled: 1; 34 | bool isHemiSphereOcclusionEnabled: 1; 35 | bool isFogEnabled: 1; 36 | bool iPolygonOffsetEnabled: 1; 37 | padding: 26; 38 | }; 39 | 40 | struct ColorFloat { 41 | float r, g, b, a; 42 | }; 43 | struct ColorByte { 44 | u8 r, g, b, a; 45 | }; 46 | 47 | enum TextureFormat: u32 { 48 | RGBA8, 49 | RGB8, 50 | RGBA5551, 51 | RGB565, 52 | RGBA4, 53 | LA8, 54 | HILO8, 55 | L8, 56 | A8, 57 | LA4, 58 | L4, 59 | A4, 60 | ETC1, 61 | ETC1A4, 62 | }; 63 | 64 | enum LoopMode: u32 { 65 | OneTime, 66 | Loop, 67 | }; 68 | 69 | enum CullMode: u32 { 70 | Never, 71 | FrontFace, 72 | BackFace, 73 | }; 74 | 75 | enum TevSrc: u8 { 76 | PrimaryColor, 77 | FragmentPrimaryColor, 78 | FragmentSecondaryColor, 79 | Texture0, 80 | Texture1, 81 | Texture2, 82 | Texture3, 83 | PreviousBuffer = 0xd, 84 | Constant = 0xe, 85 | Previous = 0xf, 86 | }; 87 | 88 | bitfield TevSources { 89 | TevSrc src0: 4; 90 | TevSrc src1: 4; 91 | TevSrc src2: 4; 92 | }; 93 | 94 | enum TevOpRGB: u8 { 95 | SrcColor = 0, 96 | OneMinusSrcColor = 1, 97 | SrcAlpha = 2, 98 | OneMinusSrcAlpha = 3, 99 | SrcR = 4, 100 | OneMinusSrcR = 5, 101 | SrcG = 8, 102 | OneMinusSrcG = 9, 103 | SrcB = 12, 104 | OneMinusSrcB = 13, 105 | }; 106 | 107 | enum TevOpA: u8 { 108 | SrcA, 109 | OneMinusSrcA, 110 | SrcR, 111 | OneMinusSrcR, 112 | SrcG, 113 | OneMinusSrcG, 114 | SrcB, 115 | OneMinusSrcB, 116 | }; 117 | 118 | bitfield TevOps { 119 | TevOpRGB rgb0: 4; 120 | TevOpRGB rgb1: 4; 121 | TevOpRGB rgb2: 4; 122 | TevOpA alpha0: 4; 123 | TevOpA alpha1: 4; 124 | TevOpA alpha2: 4; 125 | padding: 8; 126 | }; 127 | 128 | enum TevCombine: u16 { 129 | Replace, 130 | Modulate, 131 | Add, 132 | AddSigned, 133 | Interpolate, 134 | Subtract, 135 | Dot3RGB, 136 | Dot3RGBA, 137 | MultiplyAdd, 138 | AddMultiply, 139 | }; 140 | 141 | enum TevScale: u16 { 142 | x1, 143 | x2, 144 | x4, 145 | }; 146 | 147 | enum TextureProjection: u32 { 148 | UVMap, 149 | CameraCubeMap, 150 | CameraSphereMap, 151 | ProjectionMap, 152 | ShadowMap, 153 | ShadowCubeMap, 154 | }; 155 | 156 | enum FresnelConfig: u32 { 157 | None, 158 | Primary, 159 | Secondary, 160 | PrimarySecondary, 161 | }; 162 | 163 | enum BumpMode: u32 { 164 | NotUsed, 165 | AsBump, 166 | AsTangent, 167 | }; 168 | 169 | enum SkeletonScalingRule: u32 { 170 | Standard, 171 | Maya, 172 | SoftImage, 173 | }; 174 | 175 | enum BillboardMode: u32 { 176 | Off, 177 | World, 178 | WorldViewpoint, 179 | Screen, 180 | ScreenViewpoint, 181 | YAxial, 182 | YAxialViewpoint, 183 | }; 184 | 185 | enum DataType: u32 { 186 | Byte = 0x1400, 187 | UnsignedByte = 0x1401, 188 | Short = 0x1402, 189 | UnsignedShort = 0x1403, 190 | Float = 0x1406, 191 | }; 192 | 193 | struct CGFXHeader { 194 | char magic[4]; 195 | u16 endianness; 196 | u16 headerSize; 197 | u32 version; 198 | u32 fileSize; 199 | u32 nrBlocks; 200 | }; 201 | 202 | struct DictNode { 203 | u32 refBit; 204 | u16 leftIndex, rightIndex; 205 | RelPtr name; 206 | RelPtr data; 207 | }; 208 | 209 | struct DICT { 210 | char magic[4]; 211 | u32 sectionSize; 212 | u32 nrEntries; 213 | DictNode rootNode; 214 | DictNode nodes[nrEntries] [[inline]]; 215 | }; 216 | 217 | 218 | struct DictInfo { 219 | u32 nrItems; 220 | u32 offset [[hidden]]; 221 | if (offset != 0) { 222 | DICT dict @ $ + offset - 4; 223 | } 224 | }; 225 | 226 | struct MTOBTextureCoordinator { 227 | u32 sourceCoordinate; 228 | TextureProjection projection; 229 | u32 referenceCamera; 230 | u32 matrixMode; 231 | float scaleU; 232 | float scaleV; 233 | float rotate; 234 | float translateU; 235 | float translateV; 236 | u32 enabled; 237 | Matrix transformMatrix; 238 | }; 239 | 240 | struct SHDR { 241 | u32 type; 242 | char magic[4]; 243 | u32 revision; 244 | RelPtr name; 245 | DictInfo userData; 246 | if (type == 0x80000001) { 247 | RelPtr referenceShaderName; 248 | padding[4]; 249 | } else if (type == 0x80000002) { 250 | RelPtr; 251 | RelPtr dvlb; 252 | } 253 | } [[inline]]; 254 | 255 | bitfield PicaCommandHeader { 256 | cmd_id: 16; 257 | param_mask: 4; 258 | extra_data_length: 8; 259 | group_commands: 1; 260 | }; 261 | 262 | struct PicaCommand { 263 | u32 param; 264 | PicaCommandHeader header; 265 | //u32 extra_values[header.extra_data_length]; 266 | }; 267 | 268 | struct TextureCombinerCtr { 269 | u32 constant; 270 | TevSources srcRgb; 271 | TevSources srcAlpha; 272 | PicaCommandHeader header; 273 | TevOps opr; 274 | TevCombine combineRgb; 275 | TevCombine combineAlpha; 276 | ColorByte constRgba; 277 | TevScale scaleRgb; 278 | TevScale scaleAlpha; 279 | }; 280 | 281 | bitfield FragmentLightingFlags { 282 | bool ClampHighLight: 1; 283 | bool UseDistribution0: 1; 284 | bool UseDistribution1: 1; 285 | bool UseGeometricFactor0: 1; 286 | bool UseGeometricFactor1: 1; 287 | bool UseReflection: 1; 288 | padding: 26; 289 | }; 290 | 291 | struct FragmentLightingCtr { 292 | FragmentLightingFlags flags; 293 | u32 layerConfig; 294 | FresnelConfig fresnelConfig; 295 | u32 bumpTexture; 296 | BumpMode bumpMode; 297 | u32 isBumpRenormalize; 298 | }; 299 | 300 | struct LutTable { 301 | u32 type; 302 | RelPtr name; 303 | u32 someBoolean; 304 | ListPtr lutCommands; 305 | } [[inline]]; 306 | 307 | struct LUTS { 308 | u32 type; 309 | char magic[4]; 310 | u32 revision; 311 | RelPtr name; 312 | DictInfo userData; 313 | DictInfo tables; 314 | } [[inline]]; 315 | 316 | struct ReferenceLookupTableCtr { 317 | u32 type; 318 | RelPtr binaryPath; 319 | RelPtr tableName; 320 | RelPtr targetLut; // runtime 321 | } [[inline]]; 322 | 323 | struct LightingLookupTableCtr { 324 | u32 inputCommand; 325 | u32 scaleCommand; 326 | RelPtr sampler; 327 | } [[inline]]; 328 | 329 | struct FragmentLightingTableCtr { 330 | RelPtr reflectanceRSampler; 331 | RelPtr reflectanceGSampler; 332 | RelPtr reflectanceBSampler; 333 | RelPtr distribution0Sampler; 334 | RelPtr distribution1Sampler; 335 | RelPtr fresnelSampler; 336 | } [[inline]]; 337 | 338 | struct FragmentShader { 339 | ColorFloat bufferColor; 340 | FragmentLightingCtr fragmentLighting; 341 | RelPtr fragmentLightingTable; 342 | TextureCombinerCtr textureCombiners[6]; 343 | PicaCommand alphaTestCommand; 344 | PicaCommand bufferCommands[3]; 345 | } [[inline]]; 346 | 347 | struct ReferenceTexture { 348 | RelPtr linkedName; 349 | } [[inline]]; 350 | 351 | struct PixelBasedImage { 352 | u32 height; 353 | u32 width; 354 | ListPtr data; 355 | u32 dynamicAllocator; 356 | u32 bitsPerPixel; 357 | u32 locationAddress; 358 | u32 memoryAddress; 359 | } [[inline]]; 360 | 361 | struct ImageTexture { 362 | u32 height; 363 | u32 width; 364 | u32 glFormat; 365 | u32 glType; 366 | u32 mipmapLevels; 367 | u32 textureObject; 368 | u32 locationFlags; 369 | TextureFormat texFormat; 370 | RelPtr image; 371 | } [[inline]]; 372 | 373 | struct TXOB { 374 | u32 type; 375 | char magic[4]; 376 | u32 revision; 377 | RelPtr name; 378 | DictInfo userData; 379 | if (type == 0x20000004) { 380 | ReferenceTexture referenceTexture; 381 | } else if (type == 0x20000011) { 382 | ImageTexture imageTexture; 383 | } 384 | } [[inline]]; 385 | 386 | struct Sampler { 387 | u32 type; 388 | char *ownerOffset: s32 [[pointer_base("std::ptr::relative_to_pointer")]]; 389 | u32 minFilter; 390 | ColorFloat borderColor; 391 | } [[inline]]; 392 | 393 | struct TexInfo { 394 | u32 type; 395 | u32 dynamicAllocator; 396 | RelPtr txobOffset; 397 | RelPtr samplerOffset; 398 | PicaCommand cmd[2]; 399 | u16 height, width; 400 | padding[9*4]; 401 | u32 commandSizeToSend; 402 | } [[inline]]; 403 | 404 | struct MaterialColorCtr { 405 | ColorFloat emissionF; 406 | ColorFloat ambientF; 407 | ColorFloat diffuseF; 408 | ColorFloat specularF[2]; 409 | ColorFloat constantF[6]; 410 | ColorByte emissionB; 411 | ColorByte ambientB; 412 | ColorByte diffuseB; 413 | ColorByte specularB[2]; 414 | ColorByte constantB[6]; 415 | u32 commandCache; 416 | }; 417 | 418 | struct RasterizationCtr { 419 | u32 isPolygonOffsetEnabled; 420 | CullMode cullMode; 421 | float polygonOffsetUnit; 422 | PicaCommand rasterizationCommand; 423 | }; 424 | 425 | struct DepthOperationCtr { 426 | u32 depthFlags; 427 | PicaCommand depthCommands[2]; 428 | }; 429 | 430 | struct BlendOperationCtr { 431 | u32 blendMode; 432 | ColorFloat blendColor; 433 | PicaCommand blendCommands[3]; 434 | }; 435 | 436 | struct FragmentOperationCtr { 437 | DepthOperationCtr depthOperation; 438 | BlendOperationCtr blendOperation; 439 | PicaCommand stencilCommands[2]; 440 | }; 441 | 442 | struct MTOB { 443 | u32 type; 444 | char magic[4]; 445 | u32 revision; 446 | RelPtr name; 447 | DictInfo userData; 448 | MTOBFlags flags; 449 | u32 textureCoordinatesConfig; 450 | u32 transluscencyKind; 451 | MaterialColorCtr materialColor; 452 | RasterizationCtr rasterization; 453 | FragmentOperationCtr fragmentOperations; 454 | u32 usedTexureCoordinates; 455 | MTOBTextureCoordinator textureCoordinators[3]; 456 | RelPtr texMappers[4]; 457 | RelPtr shader; 458 | RelPtr fragmentShader; 459 | u32 shaderProgramDescriptionIndex; 460 | u32 shaderParametersCount; 461 | RelPtr shaderParametersPointerTable; 462 | u32 lightSetIndex; 463 | u32 fogIndex; 464 | 465 | u32 shadingParametersHash, shaderParametersHash, 466 | textureCoordinatorsHash, textureSamplersHash, 467 | textureMappersHash, materialColorHash, 468 | rasterizationHash, fragmentLightingHash, 469 | fragmentLightingTableHash, fragmentLightingTableParametersHash, 470 | textureCombinersHash, alphaTestHash, 471 | fragmentOperationsHash; 472 | u32 materialId; 473 | } [[inline]]; 474 | 475 | enum AnimationGroupMemberType: u32 { 476 | MeshNodeVisibility = 0x00080000, 477 | Mesh = 0x01000000, 478 | TextureSampler = 0x02000000, 479 | BlendOperation = 0x04000000, 480 | MaterialColor = 0x08000000, 481 | Model = 0x10000000, 482 | TextureMapper = 0x20000000, 483 | Bone = 0x40000000, 484 | TextureCoordinator = 0x80000000 485 | }; 486 | 487 | enum AGMFieldType: u32 { 488 | MaterialColor = 1, 489 | Sampler = 2, 490 | TextureMapper = 3, 491 | BlendOperation = 4, 492 | TextureCoordinator = 5, 493 | }; 494 | 495 | struct AnimationGroupMember { 496 | AnimationGroupMemberType objectType; 497 | RelPtr path; 498 | RelPtr member; 499 | RelPtr objectId; 500 | u32 valueOffset; 501 | u32 valueSize; 502 | u32 unknown; 503 | AGMFieldType fieldType; 504 | u32 valueIndex; 505 | padding[4]; 506 | if (fieldType <= 5) { 507 | RelPtr parentName; 508 | u32 fieldIndex; 509 | } else { 510 | u32 parentIndex; 511 | } 512 | } [[inline]]; 513 | 514 | bitfield GraphicsAnimGroupFlags { 515 | bool IsTransform: 1; 516 | padding: 31; 517 | }; 518 | 519 | enum GraphicsMemberType: u32 { 520 | None, 521 | Bone, 522 | Material, 523 | Model, 524 | Light, 525 | Camera, 526 | Fog, 527 | }; 528 | 529 | enum AnimGroupEvaluationTiming: u32 { 530 | BeforeWorldUpdate, 531 | AfterSceneCulling, 532 | }; 533 | 534 | struct GraphicsAnimationGroup { 535 | u32 type; 536 | GraphicsAnimGroupFlags flags; 537 | RelPtr name; 538 | GraphicsMemberType memberType; 539 | DictInfo members; 540 | ListPtr blendOperations; 541 | AnimGroupEvaluationTiming evaluationTiming; 542 | } [[inline]]; 543 | 544 | bitfield BoneFlags { 545 | bool isIdentity: 1; 546 | bool isTranslateZero: 1; 547 | bool isRotateZero: 1; 548 | bool isScaleOne: 1; 549 | bool isUniformScale: 1; 550 | bool isSegmentScaleCompensate: 1; 551 | bool isNeedRendering: 1; 552 | bool isLocalMatrixCalculate: 1; 553 | bool isWorldMatrixCalculate: 1; 554 | bool hasSkinningMatrix: 1; 555 | padding: 22; 556 | }; 557 | 558 | struct Bone { 559 | RelPtr name; 560 | BoneFlags flags; 561 | u32 jointId; 562 | u32 parentId; 563 | RelPtr parentOffset; 564 | RelPtr childOffset; 565 | RelPtr previousSiblingOffset; 566 | RelPtr nextSiblingOffset; 567 | float scale[3]; 568 | float rotation[3]; 569 | float translation[3]; 570 | Matrix localMatrix; 571 | Matrix worldMatrix; 572 | Matrix inverseBaseMatrix; 573 | BillboardMode billboardMode; 574 | padding[8]; 575 | } [[inline]]; 576 | 577 | struct IndexStream { 578 | DataType formatType; 579 | u8 primitiveMode; 580 | bool isVisible; 581 | padding[2]; 582 | ListPtr faceData; 583 | u32 bufferObject; 584 | u32 locationFlag; 585 | u32 commandCache; 586 | u32 commandCacheSize; 587 | u32 locationAddress; 588 | u32 memoryArea; 589 | RelPtr boundingBoxOffset; 590 | } [[inline]]; 591 | 592 | struct Primitive { 593 | ListPtr> indexStreams; 594 | u32 nrBufferObjects; 595 | RelPtr bufferObjectArrayOffset; 596 | u32 flags; 597 | u32 commandAllocator; 598 | } [[inline]]; 599 | 600 | struct PrimitiveSet { 601 | ListPtr relatedBones; 602 | u32 skinningMode; 603 | ListPtr> primitives; 604 | } [[inline]]; 605 | 606 | enum VertexAttributeType: u32 { 607 | VertexStream = 0x40000001, 608 | InterleavedVertexStream = 0x40000002, 609 | VertexParamAttribute = 0x80000000, 610 | }; 611 | 612 | enum VertexAttributeUsage: u32 { 613 | Position, 614 | Normal, 615 | Tangent, 616 | Color, 617 | TextureCoordinate0, 618 | TextureCoordinate1, 619 | TextureCoordinate2, 620 | BoneIndex, 621 | BoneWeight, 622 | UserAttribute0, 623 | UserAttribute1, 624 | UserAttribute2, 625 | UserAttribute3, 626 | UserAttribute4, 627 | UserAttribute5, 628 | UserAttribute6, 629 | UserAttribute7, 630 | UserAttribute8, 631 | UserAttribute9, 632 | UserAttribute10, 633 | UserAttribute11, 634 | Interleave, 635 | Quantity 636 | }; 637 | 638 | enum VertexAttributeFlags: u32 { 639 | None, 640 | VertexParam, 641 | Interleave, 642 | VertexParamAndInterleave, 643 | }; 644 | 645 | using VertexAttribute; 646 | 647 | struct VertexAttribute { 648 | VertexAttributeType type; 649 | VertexAttributeUsage usage; 650 | VertexAttributeFlags flags; 651 | if (type == VertexAttributeType::InterleavedVertexStream) { 652 | u32 bufferObject; 653 | u32 locationFlag; 654 | ListPtr vertexStreamData; 655 | u32 locationAddress; 656 | u32 memoryArea; 657 | u32 vertexDataEntrySize; 658 | ListPtr> vertexStreams; 659 | } else if (type == VertexAttributeType::VertexStream) { 660 | u32 bufferObject; 661 | u32 locationFlag; 662 | ListPtr vertexStreamData; 663 | u32 locationAddress; 664 | u32 memoryArea; 665 | DataType formatType; 666 | u32 nrComponents; 667 | float scale; 668 | u32 offset; 669 | } else if (type == VertexAttributeType::VertexParamAttribute) { 670 | DataType formatType; 671 | u32 nrComponents; 672 | float scale; 673 | ListPtr attributes; 674 | } 675 | } [[inline]]; 676 | 677 | struct OrientedBoundingBox { 678 | u32 type; 679 | float centerPosition[3]; 680 | OrientationMatrix orientationMatrix; 681 | float size[3]; 682 | } [[inline]]; 683 | 684 | struct SOBJMesh { 685 | u32 type; 686 | char magic[4]; 687 | u32 revision; 688 | RelPtr name; 689 | DictInfo userData; 690 | u32 shapeIndex; 691 | u32 materialIndex; 692 | char *ownerModel: s32 [[pointer_base("std::ptr::relative_to_pointer")]]; 693 | bool isVisible; 694 | u8 priority; 695 | u16 meshNodeVisibilityIndex; 696 | padding[4*18]; 697 | RelPtr meshNodeName; 698 | } [[inline]]; 699 | 700 | bitfield SkeletonFlags { 701 | bool isModelCoordinate: 1; 702 | bool isTranslateAnimationEnabled: 1; 703 | padding: 30; 704 | }; 705 | 706 | struct SOBJSkeleton { 707 | u32 type; 708 | char magic[4]; 709 | u32 revision; 710 | RelPtr name; 711 | DictInfo userData; 712 | DictInfo bones; 713 | RelPtr rootBone; 714 | SkeletonScalingRule scalingRule; 715 | SkeletonFlags flags; 716 | } [[inline]]; 717 | 718 | struct SOBJShape { 719 | u32 type; 720 | char magic[4]; 721 | u32 revision; 722 | RelPtr name; 723 | DictInfo userData; 724 | u32 flags; 725 | RelPtr orientedBoundingBox; 726 | float positionOffset[3]; 727 | u32 nrPrimitiveSets; 728 | RelPtr *primitiveSets[nrPrimitiveSets]: u32 [[pointer_base("std::ptr::relative_to_pointer")]]; 729 | u32 baseAddress; 730 | u32 nrVertexAttributes; 731 | RelPtr *vertexAttributes[nrVertexAttributes]: u32 [[pointer_base("std::ptr::relative_to_pointer")]]; 732 | RelPtr blendShapeOffset; 733 | } [[inline]]; 734 | 735 | struct MeshNodeVisibilityCtr { 736 | RelPtr name; 737 | u32 visible; 738 | } [[inline]]; 739 | 740 | bitfield CMDLType { 741 | padding: 7; 742 | bool hasSkeleton: 1; 743 | padding: 24; 744 | }; 745 | 746 | bitfield CMDLFlags1 { 747 | padding: 32; 748 | }; 749 | 750 | bitfield CMDLFlags2 { 751 | bool isBranchVisible: 1; 752 | padding: 31; 753 | }; 754 | 755 | bitfield CMDLFlags3 { 756 | bool isVisible: 1; 757 | padding: 8; 758 | bool isNonUniformScalable: 1; 759 | padding: 22; 760 | }; 761 | 762 | struct CMDL { 763 | CMDLType type; 764 | char magic[4]; 765 | u32 revision; 766 | RelPtr modelName; 767 | DictInfo userData; 768 | CMDLFlags1 flags1; 769 | CMDLFlags2 flags2; 770 | u32 childCount; 771 | padding[4]; 772 | DictInfo animationGroups; 773 | float transformScale[3]; 774 | float transformRotate[3]; 775 | float transformTranslate[3]; 776 | Matrix localMatrix; 777 | Matrix worldMatrix; 778 | ListPtr> meshes; 779 | DictInfo materials; 780 | ListPtr> shapes; 781 | DictInfo meshNodes; 782 | CMDLFlags3 flags3; 783 | CullMode cullMode; 784 | u32 layerId; 785 | if (type.hasSkeleton) { 786 | RelPtr skeleton; 787 | } 788 | } [[inline]]; 789 | 790 | enum PrimitiveType: u32 { 791 | Float, 792 | Int, 793 | Boolean, 794 | Vector2, 795 | Vector3, 796 | Transform, 797 | RgbaColor, 798 | Texture, 799 | BakedTransform, 800 | TransformMatrix, 801 | }; 802 | 803 | enum RepeatMethod: u8 { 804 | Clamp, 805 | Repeat, 806 | MirroredRepeat, 807 | Repeat2, // functions exactly the same as Repeat 808 | }; 809 | 810 | struct AnimationCurve { 811 | float startFrame, endFrame; 812 | RepeatMethod preRepeatMethod, postRepeatMethod; 813 | padding[2]; 814 | u32 flags; 815 | } [[inline]]; 816 | 817 | enum Interpolation: u32 { 818 | Nearest, 819 | Linear, 820 | CSpline, 821 | }; 822 | 823 | enum QuantizationType: u32 { 824 | Hermite128, 825 | Hermite64, 826 | Hermite48, 827 | UnifiedHermite96, 828 | UnifiedHermite48, 829 | UnifiedHermite32, 830 | StepLinear64, 831 | StepLinear32 832 | }; 833 | 834 | bitfield FloatSegmentFlags { 835 | bool single: 1; 836 | padding: 1; 837 | Interpolation interp: 3; 838 | QuantizationType quanti: 3; 839 | padding: 24; 840 | }; 841 | 842 | struct Hermite128Key { 843 | float frame; 844 | float value; 845 | float inSlope; 846 | float outSlope; 847 | }; 848 | 849 | struct Hermite64Key { 850 | u32 frameValue; 851 | u16 inSlope; // 8 fractional bits 852 | u16 outSlope; // 8 fractional bits 853 | }; 854 | 855 | struct Hermite48Key { 856 | u8 frameValue[3]; 857 | u8 inOutSlope[3]; 858 | }; 859 | 860 | struct UnifiedHermite96Key { 861 | float frame; 862 | float value; 863 | float inOutSlope; 864 | }; 865 | 866 | struct UnifiedHermite48Key { 867 | u16 frame; // 8 fractional bits 868 | u16 value; 869 | u16 inOutSlope; // 8 fractional bits 870 | }; 871 | 872 | struct UnifiedHermite32Key { 873 | u8 frame; 874 | u8 valueSlope[3]; 875 | }; 876 | 877 | struct StepLinear64Key { 878 | float frame; 879 | float value; 880 | }; 881 | 882 | struct StepLinear32Key { 883 | u24 frame; 884 | u8 value; 885 | }; 886 | 887 | struct FloatSegment { 888 | float startFrame, endFrame; 889 | FloatSegmentFlags flags; 890 | if (flags.single) { 891 | float singleValue; 892 | } else { 893 | u32 keyCount; 894 | float speed; 895 | if (flags.quanti != QuantizationType::Hermite128 && flags.quanti != QuantizationType::StepLinear64 && flags.quanti != QuantizationType::UnifiedHermite96) { 896 | float scale; 897 | float offset; 898 | float frame_scale; 899 | } 900 | match (flags.quanti) { 901 | (QuantizationType::Hermite128): Hermite128Key keys[keyCount]; 902 | (QuantizationType::Hermite64): Hermite64Key keys[keyCount]; 903 | (QuantizationType::Hermite48): Hermite48Key keys[keyCount]; 904 | (QuantizationType::UnifiedHermite96): UnifiedHermite96Key keys[keyCount]; 905 | (QuantizationType::UnifiedHermite48): UnifiedHermite48Key keys[keyCount]; 906 | (QuantizationType::UnifiedHermite32): UnifiedHermite32Key keys[keyCount]; 907 | (QuantizationType::StepLinear64): StepLinear64Key keys[keyCount]; 908 | (QuantizationType::StepLinear32): StepLinear32Key keys[keyCount]; 909 | } 910 | } 911 | } [[inline]]; 912 | 913 | struct FloatAnimationCurve { 914 | AnimationCurve curve; 915 | u32 nrSegments; 916 | RelPtr segments[nrSegments]; 917 | } [[inline]]; 918 | 919 | struct Vec3 { 920 | float x, y, z; 921 | }; 922 | 923 | struct Vector3AndFlags { 924 | Vec3 value; 925 | u32 flags; 926 | }; 927 | 928 | struct Vector3Curve { 929 | AnimationCurve curve; 930 | if (curve.flags & 1) { 931 | float constantValue[3]; 932 | u32 constantFlags; 933 | } else { 934 | Vector3AndFlags values[curve.endFrame - curve.startFrame]; 935 | } 936 | } [[inline]]; 937 | 938 | struct QuaternionAndFlags { 939 | float value[4]; 940 | u32 flags; 941 | }; 942 | 943 | struct QuaternionCurve { 944 | AnimationCurve curve; 945 | // TODO: what if flags & 1? 946 | QuaternionAndFlags values[curve.endFrame - curve.startFrame]; 947 | } [[inline]]; 948 | 949 | struct CANMBone { 950 | u32 flags; 951 | RelPtr bonePath; 952 | // these next two appear to be unused 953 | RelPtr unknown1; 954 | RelPtr unknown2; 955 | PrimitiveType primitiveType; 956 | if (primitiveType == PrimitiveType::Vector2) { 957 | if (flags & 1) float xConst; 958 | else if (flags & 4) padding[4]; 959 | else RelPtr xVal; 960 | if (flags & 2) float yConst; 961 | else if (flags & 8) padding[4]; 962 | else RelPtr yVal; 963 | } else if (primitiveType == PrimitiveType::Transform) { 964 | if (flags & 0x10000) padding[4]; 965 | else if (flags & 0x40) float scale_x; 966 | else RelPtr scale_x; 967 | 968 | if (flags & 0x20000) padding[4]; 969 | else if (flags & 0x80) float scale_y; 970 | else RelPtr scale_y; 971 | 972 | if (flags & 0x40000) padding[4]; 973 | else if (flags & 0x100) float scale_z; 974 | else RelPtr scale_z; 975 | 976 | if (flags & 0x80000) padding[4]; 977 | else if (flags & 0x200) float rot_x; 978 | else RelPtr rot_x; 979 | 980 | if (flags & 0x100000) padding[4]; 981 | else if (flags & 0x400) float rot_y; 982 | else RelPtr rot_y; 983 | 984 | if (flags & 0x200000) padding[4]; 985 | else if (flags & 0x800) float rot_z; 986 | else RelPtr rot_z; 987 | 988 | padding[4]; 989 | 990 | if (flags & 0x800000) padding[4]; 991 | else if (flags & 0x2000) float pos_x; 992 | else RelPtr pos_x; 993 | 994 | if (flags & 0x1000000) padding[4]; 995 | else if (flags & 0x4000) float pos_y; 996 | else RelPtr pos_y; 997 | 998 | if (flags & 0x2000000) padding[4]; 999 | else if (flags & 0x8000) float pos_z; 1000 | else RelPtr pos_z; 1001 | } else if (primitiveType == PrimitiveType::BakedTransform) { 1002 | if (flags & 0x10) padding[4]; 1003 | else RelPtr rotationOffset; 1004 | if (flags & 0x08) padding[4]; 1005 | else RelPtr translation; 1006 | if (flags & 0x20) padding[4]; 1007 | else RelPtr scale; 1008 | } else if (primitiveType == PrimitiveType::RgbaColor) { 1009 | if (flags & 0x10) { 1010 | padding[4]; 1011 | } else { 1012 | RelPtr redCurve; 1013 | } 1014 | if (flags & 0x20) { 1015 | padding[4]; 1016 | } else { 1017 | RelPtr greenCurve; 1018 | } 1019 | if (flags & 0x40) { 1020 | padding[4]; 1021 | } else { 1022 | RelPtr blueCurve; 1023 | } 1024 | if (flags & 0x80) { 1025 | padding[4]; 1026 | } else { 1027 | RelPtr alphaCurve; 1028 | } 1029 | } 1030 | } [[inline]]; 1031 | 1032 | struct CANM { 1033 | char magic[4]; 1034 | u32 revision; 1035 | RelPtr name; 1036 | RelPtr targetAnimationGroupName; 1037 | LoopMode loopMode; 1038 | float frameSize; 1039 | DictInfo memberAnimationsData; 1040 | DictInfo userData; 1041 | } [[inline]]; 1042 | 1043 | struct LightAnimationChannel { 1044 | padding[4]; 1045 | RelPtr name; 1046 | }[[inline]]; 1047 | 1048 | struct LightAnimation { 1049 | u32 flags; 1050 | padding[4]; 1051 | RelPtr name; 1052 | padding[4]; 1053 | DictInfo channels; 1054 | ListPtr some_enum; 1055 | } [[inline]]; 1056 | 1057 | struct LightLutOrSomething { 1058 | u32 flags; 1059 | padding[8]; 1060 | RelPtr info; 1061 | } [[inline]]; 1062 | 1063 | struct SomeMoreLightData { 1064 | padding[8]; 1065 | RelPtr lut; 1066 | }; 1067 | 1068 | enum LightType: u32 { 1069 | Positional, 1070 | Directional, 1071 | }; 1072 | 1073 | struct CFLT { 1074 | u32 type; 1075 | char magic[4]; 1076 | u32 revision; 1077 | RelPtr name; 1078 | DictInfo userData; 1079 | u32 someFlags; 1080 | padding[3*4]; 1081 | DictInfo animation; 1082 | Vec3 scale; 1083 | Vec3 rotation; 1084 | Vec3 transform; 1085 | Matrix localMatrix; 1086 | Matrix worldMatrix; 1087 | u32 enabled; 1088 | LightType lightType; 1089 | ColorFloat ambientF; 1090 | ColorFloat diffuseF; 1091 | ColorFloat specularF[2]; 1092 | u32 ambientB; 1093 | u32 diffuseB; 1094 | u32 specularB[2]; 1095 | float positionOrDirection[3]; 1096 | RelPtr attenutationLut; 1097 | RelPtr spotlightLut; 1098 | padding[2*4]; 1099 | u32 attenuationScale; // float1.7.12 1100 | u32 attenuationBias; // float1.7.12 1101 | } [[inline]]; 1102 | 1103 | struct ViewMatrixData { 1104 | u32; 1105 | u32 flags; 1106 | Vec3 lookAt; 1107 | float angle; 1108 | } [[inline]]; 1109 | 1110 | struct ProjectionMatrixData { 1111 | u32; 1112 | float znear; 1113 | float zfar; 1114 | float aspect; 1115 | float fov; 1116 | } [[inline]]; 1117 | 1118 | struct CCAM { 1119 | u32 type; 1120 | char magic[4]; 1121 | u32 revision; 1122 | RelPtr name; 1123 | DictInfo userData; 1124 | padding[16]; 1125 | DictInfo animationGroups; 1126 | Vec3 scale; 1127 | Vec3 rotation; 1128 | Vec3 transform; 1129 | Matrix localMatrix; 1130 | Matrix worldMatrix; 1131 | u32; 1132 | u32; 1133 | RelPtr; 1134 | RelPtr; 1135 | } [[inline]]; 1136 | 1137 | struct CENVCamera { 1138 | u32; 1139 | RelPtr; 1140 | u32; 1141 | } [[inline]]; 1142 | 1143 | struct CENVLight { 1144 | u32; 1145 | RelPtr; 1146 | u32; 1147 | } [[inline]]; 1148 | 1149 | struct CENVLightSet { 1150 | u32; 1151 | ListPtr> lights; 1152 | } [[inline]]; 1153 | 1154 | struct CENV { 1155 | u32 type; 1156 | char magic[4]; 1157 | u32 revision; 1158 | RelPtr name; 1159 | DictInfo userData; 1160 | ListPtr> cameras; 1161 | ListPtr> lightSets; 1162 | ListPtr; 1163 | } [[inline]]; 1164 | 1165 | struct DATA { 1166 | char magic[4]; 1167 | u32 sectionSize; 1168 | DictInfo models; 1169 | DictInfo textures; 1170 | DictInfo lookUpTables; 1171 | DictInfo materials; 1172 | DictInfo shaders; 1173 | DictInfo cameras; 1174 | DictInfo lights; 1175 | DictInfo fogs; 1176 | DictInfo scenes; 1177 | DictInfo skeletalAnimations; 1178 | DictInfo materialAnimations; 1179 | DictInfo visibilityAnimations; 1180 | DictInfo cameraAnimations; 1181 | DictInfo lightAnimations; 1182 | DictInfo emitters; 1183 | }; 1184 | 1185 | struct CGFX { 1186 | CGFXHeader header; 1187 | DATA data; 1188 | }; 1189 | 1190 | CGFX cgfx @ 0; 1191 | -------------------------------------------------------------------------------- /cgfx/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyfloogle/pycgfx/1f78850086f3a77c41e07162e842f97a5bf3c18a/cgfx/__init__.py -------------------------------------------------------------------------------- /cgfx/animation.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | from .shared import StandardObject, List 3 | from .dict import DictInfo 4 | from struct import Struct 5 | 6 | 7 | class AnimationGroupMemberType(IntEnum): 8 | MeshNodeVisibility = 0x00080000 9 | Mesh = 0x01000000 10 | TextureSampler = 0x02000000 11 | BlendOperation = 0x04000000 12 | MaterialColor = 0x08000000 13 | Model = 0x10000000 14 | TextureMapper = 0x20000000 15 | Bone = 0x40000000 16 | TextureCoordinator = 0x80000000 17 | 18 | 19 | class AnimationGroupMember(StandardObject): 20 | # these names mainly apply to material members, not sure how others work 21 | # this is sort of accessing a hierarchy, so here's the naming convention: 22 | # object has fields, and those have values 23 | object_type = AnimationGroupMemberType.Mesh 24 | path: str = None 25 | member: str = None 26 | blend_operation_index: str = None 27 | value_offset = 0 28 | value_size = 0 29 | unknown = 0 30 | field_type = 0 31 | value_index = 0 32 | parent_name = "" 33 | field_index = 0 34 | parent_index = 0 35 | 36 | def refresh_struct(self): 37 | # padding is used at runtime 38 | fmt = "Iiiiiiiiixxxxi" 39 | if self.field_type <= 5: 40 | fmt += "i" 41 | self.struct = Struct(fmt) 42 | 43 | def values(self): 44 | return ( 45 | self.object_type, 46 | self.path, 47 | self.member, 48 | self.blend_operation_index, 49 | self.value_offset, 50 | self.value_size, 51 | self.unknown, 52 | self.field_type, 53 | self.value_index, 54 | ) + ( 55 | (self.parent_name, self.field_index) 56 | if self.field_type <= 5 57 | else (self.parent_index,) 58 | ) 59 | 60 | 61 | class GraphicsAnimationGroup(StandardObject): 62 | struct = Struct("Iiiiiiiii") 63 | type = 0x80000000 64 | flags = 0 65 | name = "" 66 | member_type = 0 67 | members: DictInfo[AnimationGroupMember] 68 | blend_operations: List[int] 69 | evalution_timing = 0 70 | 71 | def __init__(self): 72 | self.members = DictInfo() 73 | self.blend_operations = List() 74 | 75 | def values(self): 76 | return ( 77 | self.type, 78 | self.flags, 79 | self.name, 80 | self.member_type, 81 | self.members, 82 | self.blend_operations, 83 | self.evalution_timing, 84 | ) 85 | -------------------------------------------------------------------------------- /cgfx/canm.py: -------------------------------------------------------------------------------- 1 | from .shared import ( 2 | ColorFloat, 3 | InlineObject, 4 | StandardObject, 5 | Signature, 6 | Vector3, 7 | Vector4, 8 | ) 9 | from .dict import DictInfo 10 | from enum import IntEnum, IntFlag 11 | from struct import Struct 12 | 13 | 14 | class Vector2Flag(IntFlag): 15 | XConst = 1 16 | YConst = 2 17 | XIgnore = 4 18 | YIgnore = 8 19 | 20 | 21 | class TransformFlag(IntFlag): 22 | ScaleXConst = 0x40 23 | ScaleYConst = 0x80 24 | ScaleZConst = 0x100 25 | RotXConst = 0x200 26 | RotYConst = 0x400 27 | RotZConst = 0x800 28 | PosXConst = 0x2000 29 | PosYConst = 0x4000 30 | PosZConst = 0x8000 31 | ScaleXIgnore = 0x10000 32 | ScaleYIgnore = 0x20000 33 | ScaleZIgnore = 0x40000 34 | RotXIgnore = 0x80000 35 | RotYIgnore = 0x100000 36 | RotZIgnore = 0x200000 37 | PosXIgnore = 0x800000 38 | PosYIgnore = 0x1000000 39 | PosZIgnore = 0x2000000 40 | 41 | 42 | class BakedTransformFlag(IntFlag): 43 | TranslationIgnore = 8 44 | RotationIgnore = 16 45 | ScaleIgnore = 32 46 | 47 | 48 | class RgbaColorFlags(IntFlag): 49 | RConst = 0x1 50 | BConst = 0x2 51 | GConst = 0x4 52 | AConst = 0x8 53 | RIgnore = 0x10 54 | GIgnore = 0x20 55 | BIgnore = 0x40 56 | AIgnore = 0x80 57 | 58 | 59 | class PrimitiveType(IntEnum): 60 | Float = 0 61 | Int = 1 62 | Boolean = 2 63 | Vector2 = 3 64 | Vector3 = 4 65 | Transform = 5 66 | RgbaColor = 6 67 | Texture = 7 68 | BakedTransform = 8 69 | TransformMatrix = 9 70 | 71 | 72 | class RepeatMethod(IntEnum): 73 | Clamp = 0 74 | Repeat = 1 75 | MirroredRepeat = 2 76 | 77 | 78 | class AnimationCurve(StandardObject): 79 | struct = Struct("ffbbxxi") 80 | start_frame = 0.0 81 | end_frame = 600.0 82 | pre_repeat_method = RepeatMethod.Clamp 83 | post_repeat_method = RepeatMethod.Clamp 84 | flags = 0 85 | 86 | def values(self) -> tuple: 87 | return ( 88 | self.start_frame, 89 | self.end_frame, 90 | self.pre_repeat_method, 91 | self.post_repeat_method, 92 | self.flags, 93 | ) 94 | 95 | 96 | class InterpolationType(IntEnum): 97 | Nearest = 0 98 | Linear = 1 99 | CubicSpline = 2 100 | 101 | 102 | class InterpolationKey(InlineObject): 103 | frame = 0.0 104 | value = 0.0 105 | 106 | 107 | class UnifiedHermiteKey(InterpolationKey): 108 | in_out_slope = 0.0 109 | 110 | 111 | class HermiteKey(InterpolationKey): 112 | in_slope = 0.0 113 | out_slope = 0.0 114 | 115 | 116 | class Hermite128Key(HermiteKey): 117 | struct = Struct("ffff") 118 | 119 | def __init__(self, frame=0, value=0, in_slope=0, out_slope=0): 120 | self.frame = frame 121 | self.value = value 122 | self.in_slope = in_slope 123 | self.out_slope = out_slope 124 | 125 | def values(self): 126 | return (self.frame, self.value, self.in_slope, self.out_slope) 127 | 128 | 129 | class UnifiedHermite96Key(UnifiedHermiteKey): 130 | struct = Struct("fff") 131 | 132 | def values(self): 133 | return (self.frame, self.value, self.in_out_slope) 134 | 135 | 136 | class StepLinear64Key(InterpolationKey): 137 | struct = Struct("ff") 138 | 139 | def __init__(self, frame=0, value=0): 140 | self.frame = frame 141 | self.value = value 142 | 143 | def values(self): 144 | return (self.frame, self.value) 145 | 146 | 147 | class QuantizationType(IntEnum): 148 | Hermite128 = 0 149 | Hermite64 = 1 150 | Hermite48 = 2 151 | UnifiedHermite96 = 3 152 | UnifiedHermite48 = 4 153 | UnifiedHermite32 = 5 154 | StepLinear64 = 6 155 | StepLinear32 = 7 156 | 157 | 158 | class FloatSegment(StandardObject): 159 | start_frame = 0.0 160 | end_frame = 0.0 161 | single_value: float | None = None 162 | interpolation = InterpolationType.Linear 163 | quantization = QuantizationType.Hermite128 164 | keys: list[InterpolationKey] 165 | scale = 1 166 | offset = 0 167 | frame_scale = 1 168 | 169 | def __init__(self): 170 | self.keys = [] 171 | 172 | def refresh_struct(self): 173 | struct = "ffi" 174 | if self.single_value is not None: 175 | struct += "f" 176 | else: 177 | struct += "if" 178 | if self.quantization not in ( 179 | QuantizationType.Hermite128, 180 | QuantizationType.UnifiedHermite96, 181 | QuantizationType.StepLinear64, 182 | ): 183 | struct += "fff" 184 | for k in self.keys: 185 | k.refresh_struct() 186 | struct += k.struct.format 187 | self.struct = Struct(struct) 188 | 189 | def values(self): 190 | return ( 191 | self.start_frame, 192 | self.end_frame, 193 | ( 194 | (self.single_value is not None) 195 | | (self.interpolation << 2) 196 | | (self.quantization << 5) 197 | ), 198 | ) + ( 199 | (self.single_value,) 200 | if self.single_value is not None 201 | else ( 202 | len(self.keys), 203 | # speed is used to accelerate lookups 204 | # for this, time * speed * (num_keys-1) must < num_keys 205 | # so speed < 1 / time 206 | 1 / (self.end_frame - self.start_frame), 207 | ) 208 | + ( 209 | (self.scale, self.offset, self.frame_scale) 210 | if self.quantization 211 | not in ( 212 | QuantizationType.Hermite128, 213 | QuantizationType.UnifiedHermite96, 214 | QuantizationType.StepLinear64, 215 | ) 216 | else () 217 | ) 218 | + tuple(self.keys) 219 | ) 220 | 221 | 222 | class FloatAnimationCurve(AnimationCurve): 223 | segments: list[FloatSegment] 224 | 225 | def __init__(self) -> None: 226 | self.segments = [] 227 | 228 | def refresh_struct(self): 229 | self.struct = Struct( 230 | AnimationCurve.struct.format + "i" + "i" * len(self.segments) 231 | ) 232 | 233 | def values(self) -> tuple: 234 | return super().values() + (len(self.segments), *self.segments) 235 | 236 | 237 | class Vector3AndFlags(InlineObject): 238 | struct = Struct("fffi") 239 | value: Vector3 240 | flags = 0 241 | 242 | def __init__(self) -> None: 243 | self.value = Vector3() 244 | 245 | def values(self) -> tuple: 246 | return (self.value, self.flags) 247 | 248 | 249 | class Vector3AnimationCurve(AnimationCurve): 250 | frames: list[Vector3AndFlags] 251 | 252 | def __init__(self) -> None: 253 | self.frames = [] 254 | 255 | def refresh_struct(self): 256 | self.struct = Struct( 257 | AnimationCurve.struct.format + Vector3AndFlags.format * len(self.frames) 258 | ) 259 | 260 | def values(self) -> tuple: 261 | if (self.flags[0] & 1) == 0: 262 | assert len(self.frames) == self.end_frame - self.start_frame 263 | return super().values() + ( 264 | (self.frames[0],) if self.flags[0] & 1 else tuple(self.frames) 265 | ) 266 | 267 | 268 | class QuaternionAndFlags(InlineObject): 269 | struct = Struct("ffffi") 270 | value: Vector4 271 | flags = 0 272 | 273 | def __init__(self) -> None: 274 | self.value = Vector4() 275 | 276 | def values(self) -> tuple: 277 | return (self.value, self.flags) 278 | 279 | 280 | class QuaternionAnimationCurve(AnimationCurve): 281 | frames: list[QuaternionAndFlags] 282 | 283 | def __init__(self) -> None: 284 | self.frames = [] 285 | 286 | def refresh_struct(self): 287 | self.struct = Struct( 288 | AnimationCurve.struct.format + QuaternionAndFlags.format * len(self.frames) 289 | ) 290 | 291 | def values(self) -> tuple: 292 | if (self.flags[0] & 1) == 0: 293 | assert len(self.frames) == self.end_frame - self.start_frame 294 | return super.values() + ( 295 | (self.frames[0],) if self.flags[0] & 1 else tuple(self.frames) 296 | ) 297 | 298 | 299 | class CANMBone(StandardObject): 300 | struct = Struct("iiiii") 301 | flags = 0 302 | bone_path = "" 303 | unknown1 = "" 304 | unknown2 = "" 305 | primitive_type = 0 306 | 307 | def values(self): 308 | return ( 309 | self.flags, 310 | self.bone_path, 311 | self.unknown1, 312 | self.unknown2, 313 | self.primitive_type, 314 | ) 315 | 316 | 317 | class CANMBoneVector2(CANMBone): 318 | flags = Vector2Flag(0) 319 | primitive_type = PrimitiveType.Vector2 320 | x: None | float | FloatAnimationCurve = None 321 | y: None | float | FloatAnimationCurve = None 322 | 323 | def refresh_struct(self): 324 | flags = Vector2Flag(0) 325 | struct = "" 326 | values = (self.x, self.y) 327 | ignore_flags = (Vector2Flag.XIgnore, Vector2Flag.YIgnore) 328 | const_flags = (Vector2Flag.XConst, Vector2Flag.YConst) 329 | for i in range(len(values)): 330 | if isinstance(values[i], float): 331 | struct += "f" 332 | flags |= const_flags[i] 333 | else: 334 | struct += "i" 335 | if values[i] is None: 336 | flags |= ignore_flags[i] 337 | self.flags = flags 338 | self.flags = flags 339 | self.struct = Struct(CANMBone.struct.format + struct) 340 | 341 | def values(self): 342 | return super().values() + (self.x, self.y) 343 | 344 | 345 | class CANMBoneTransform(CANMBone): 346 | flags = TransformFlag(0) 347 | primitive_type = PrimitiveType.Transform 348 | scale_x: None | float | FloatAnimationCurve = None 349 | scale_y: None | float | FloatAnimationCurve = None 350 | scale_z: None | float | FloatAnimationCurve = None 351 | rot_x: None | float | FloatAnimationCurve = None 352 | rot_y: None | float | FloatAnimationCurve = None 353 | rot_z: None | float | FloatAnimationCurve = None 354 | pos_x: None | float | FloatAnimationCurve = None 355 | pos_y: None | float | FloatAnimationCurve = None 356 | pos_z: None | float | FloatAnimationCurve = None 357 | 358 | def refresh_struct(self): 359 | flags = TransformFlag(0) 360 | struct = "" 361 | values = ( 362 | self.scale_x, 363 | self.scale_y, 364 | self.scale_z, 365 | self.rot_x, 366 | self.rot_y, 367 | self.rot_z, 368 | self.pos_x, 369 | self.pos_y, 370 | self.pos_z, 371 | ) 372 | ignore_flags = ( 373 | TransformFlag.ScaleXIgnore, 374 | TransformFlag.ScaleYIgnore, 375 | TransformFlag.ScaleZIgnore, 376 | TransformFlag.RotXIgnore, 377 | TransformFlag.RotYIgnore, 378 | TransformFlag.RotZIgnore, 379 | TransformFlag.PosXIgnore, 380 | TransformFlag.PosYIgnore, 381 | TransformFlag.PosZIgnore, 382 | ) 383 | const_flags = ( 384 | TransformFlag.ScaleXConst, 385 | TransformFlag.ScaleYConst, 386 | TransformFlag.ScaleZConst, 387 | TransformFlag.RotXConst, 388 | TransformFlag.RotYConst, 389 | TransformFlag.RotZConst, 390 | TransformFlag.PosXConst, 391 | TransformFlag.PosYConst, 392 | TransformFlag.PosZConst, 393 | ) 394 | for i in range(len(values)): 395 | if isinstance(values[i], float): 396 | struct += "f" 397 | flags |= const_flags[i] 398 | else: 399 | struct += "i" 400 | if values[i] is None: 401 | flags |= ignore_flags[i] 402 | if const_flags[i] == TransformFlag.RotZConst: 403 | struct += "xxxx" 404 | self.flags = flags 405 | self.struct = Struct(CANMBone.struct.format + struct) 406 | 407 | def values(self): 408 | return super().values() + ( 409 | self.scale_x, 410 | self.scale_y, 411 | self.scale_z, 412 | self.rot_x, 413 | self.rot_y, 414 | self.rot_z, 415 | self.pos_x, 416 | self.pos_y, 417 | self.pos_z, 418 | ) 419 | 420 | 421 | class CANMBoneBakedTransform(CANMBone): 422 | struct = Struct(CANMBone.struct.format + "iii") 423 | primitive_type = PrimitiveType.BakedTransform 424 | flags = BakedTransformFlag(0) 425 | rotation: None | QuaternionAnimationCurve = None 426 | translation: None | Vector3AnimationCurve = None 427 | scale: None | Vector3AnimationCurve = None 428 | 429 | def refresh_struct(self): 430 | flags = BakedTransformFlag(0) 431 | values = (self.rotation, self.translation, self.scale) 432 | ignore_flags = ( 433 | BakedTransformFlag.RotationIgnore, 434 | BakedTransformFlag.TranslationIgnore, 435 | BakedTransformFlag.ScaleIgnore, 436 | ) 437 | for i in range(len(values)): 438 | if values[i] is None: 439 | flags |= ignore_flags[i] 440 | self.flags = flags 441 | 442 | def values(self): 443 | return super().values() + (self.rotation, self.translation, self.scale) 444 | 445 | 446 | class CANMBoneRgbaColor(CANMBone): 447 | struct = Struct(CANMBone.struct.format + "iiii") 448 | primitive_type = PrimitiveType.RgbaColor 449 | flags = RgbaColorFlags(0) 450 | red: None | float | FloatAnimationCurve = None 451 | green: None | float | FloatAnimationCurve = None 452 | blue: None | float | FloatAnimationCurve = None 453 | alpha: None | float | FloatAnimationCurve = None 454 | 455 | def refresh_struct(self): 456 | flags = RgbaColorFlags(0) 457 | struct = "" 458 | values = (self.red, self.green, self.blue, self.alpha) 459 | ignore_flags = ( 460 | RgbaColorFlags.RIgnore, 461 | RgbaColorFlags.GIgnore, 462 | RgbaColorFlags.BIgnore, 463 | RgbaColorFlags.AIgnore, 464 | ) 465 | const_flags = ( 466 | RgbaColorFlags.RConst, 467 | RgbaColorFlags.GConst, 468 | RgbaColorFlags.BConst, 469 | RgbaColorFlags.AConst, 470 | ) 471 | for i in range(len(values)): 472 | if isinstance(values[i], float): 473 | struct += "f" 474 | flags |= const_flags[i] 475 | else: 476 | struct += "i" 477 | if values[i] is None: 478 | flags |= ignore_flags[i] 479 | if const_flags[i] == TransformFlag.RotZConst: 480 | struct += "xxxx" 481 | self.flags = flags 482 | self.struct = Struct(CANMBone.struct.format + struct) 483 | 484 | def values(self): 485 | return super().values() + (self.red, self.green, self.blue, self.alpha) 486 | 487 | 488 | class CANM(StandardObject): 489 | struct = Struct("4siiiifiiii") 490 | signature = Signature("CANM") 491 | revision = 0x05000000 492 | name = "" 493 | target_animation_group_name = "" 494 | looping = True 495 | frame_size = 600 496 | member_animations_data: DictInfo[CANMBone] 497 | user_data: DictInfo 498 | 499 | def __init__(self): 500 | self.member_animations_data: DictInfo[CANMBone] = DictInfo() 501 | self.user_data = DictInfo() 502 | 503 | def values(self) -> tuple: 504 | return ( 505 | self.signature, 506 | self.revision, 507 | self.name, 508 | self.target_animation_group_name, 509 | self.looping, 510 | self.frame_size, 511 | self.member_animations_data, 512 | self.user_data, 513 | ) 514 | -------------------------------------------------------------------------------- /cgfx/cenv.py: -------------------------------------------------------------------------------- 1 | from .shared import StandardObject, Signature, List 2 | from .dict import DictInfo 3 | from struct import Struct 4 | 5 | 6 | class CENVCamera(StandardObject): 7 | struct = Struct("iii") 8 | unk1 = 0 9 | name = "" 10 | unk2 = 0 11 | 12 | def values(self): 13 | return (self.unk1, self.name, self.unk2) 14 | 15 | 16 | class CENVLight(StandardObject): 17 | struct = Struct("iii") 18 | unk1 = 0 19 | name = "" 20 | unk2 = 0 21 | 22 | def values(self): 23 | return (self.unk1, self.name, self.unk2) 24 | 25 | 26 | class CENVLightSet(StandardObject): 27 | struct = Struct("iii") 28 | unk = 0 29 | lights: List[CENVLight] 30 | 31 | def __init__(self): 32 | self.lights = List() 33 | 34 | def values(self): 35 | return (self.unk, self.lights) 36 | 37 | 38 | class CENV(StandardObject): 39 | struct = Struct("i4siiiiiiiiii") 40 | type = 0x800000 41 | signature = Signature("CENV") 42 | revision = 0x6000000 43 | name = "" 44 | user_data: DictInfo 45 | cameras: List[CENVCamera] 46 | light_sets: List[CENVLightSet] 47 | other_list: List 48 | 49 | def __init__(self): 50 | self.user_data = DictInfo() 51 | self.cameras = List() 52 | self.light_sets = List() 53 | self.other_list = List() 54 | 55 | def values(self): 56 | return ( 57 | self.type, 58 | self.signature, 59 | self.revision, 60 | self.name, 61 | self.user_data, 62 | self.cameras, 63 | self.light_sets, 64 | self.other_list, 65 | ) 66 | -------------------------------------------------------------------------------- /cgfx/cflt.py: -------------------------------------------------------------------------------- 1 | from .shared import ( 2 | Vector3, 3 | Vector4, 4 | Matrix, 5 | ColorByte, 6 | ColorFloat, 7 | StandardObject, 8 | Signature, 9 | ) 10 | from .dict import DictInfo 11 | from .animation import GraphicsAnimationGroup 12 | from struct import Struct 13 | 14 | 15 | def float_to_20bit(f: float) -> int: 16 | s = Struct("f") 17 | data = s.pack(f) 18 | casted = int.from_bytes(data, "little", signed=False) 19 | mantissa = casted & 0x7FFFFF 20 | exponent = max(-0x3F, min(0x40, (((casted >> 23) & 0xFF) - 0x7F))) 21 | sign = casted >> 31 22 | return (sign << 19) | (((exponent + 0x3F) & 0x7F) << 12) | (mantissa >> 13) 23 | 24 | 25 | class CFLT(StandardObject): 26 | struct = Struct( 27 | "i4siiiiiiixxxxiifffffffff" 28 | + "f" * 12 * 2 29 | + "ii" 30 | + "f" * 16 31 | + "B" * 16 32 | + "fffiixxxxxxxxiixxxx" 33 | ) 34 | type = 0x400000A2 35 | signature = Signature("CFLT") 36 | revision = 0x6000000 37 | name = "" 38 | user_data: DictInfo 39 | flags = 1 40 | branch_visible = False 41 | nr_children = 0 42 | animation_group_descriptions: DictInfo[GraphicsAnimationGroup] 43 | scale: Vector3 44 | rotation: Vector3 45 | translation: Vector3 46 | local: Matrix 47 | world: Matrix 48 | enabled = True 49 | light_type = 0 50 | ambient: ColorFloat 51 | diffuse: ColorFloat 52 | specular: list[ColorFloat] 53 | position_or_direction: Vector3 54 | attenuation_lut = None 55 | spotlight_lut = None 56 | attenuation_scale = 1 57 | attenuation_bias = -0.0 58 | 59 | def __init__(self) -> None: 60 | super().__init__() 61 | self.scale = Vector3(1, 1, 1) 62 | self.rotation = Vector3(0, 0, 0) 63 | self.translation = Vector3(0, 0, 0) 64 | self.local = Matrix( 65 | Vector4(1, 0, 0, 0), Vector4(0, 1, 0, 0), Vector4(0, 0, 1, 0) 66 | ) 67 | self.world = Matrix( 68 | Vector4(1, 0, 0, 0), Vector4(0, 1, 0, 0), Vector4(0, 0, 1, 0) 69 | ) 70 | self.user_data = DictInfo() 71 | self.animation_group_descriptions = DictInfo() 72 | self.position_or_direction = Vector3(0, 0, -1) 73 | self.ambient = ColorFloat(1, 1, 1, 1) 74 | self.diffuse = ColorFloat(1, 1, 1, 1) 75 | self.specular = [ColorFloat(1, 1, 1, 1), ColorFloat(1, 1, 1, 1)] 76 | 77 | def values(self) -> tuple: 78 | return ( 79 | self.type, 80 | self.signature, 81 | self.revision, 82 | self.name, 83 | self.user_data, 84 | self.flags, 85 | self.branch_visible, 86 | self.nr_children, 87 | self.animation_group_descriptions, 88 | self.scale, 89 | self.rotation, 90 | self.translation, 91 | self.local, 92 | self.world, 93 | self.enabled, 94 | self.light_type, 95 | self.ambient, 96 | self.diffuse, 97 | *self.specular, 98 | self.ambient.as_byte(), 99 | self.diffuse.as_byte(), 100 | *(x.as_byte() for x in self.specular), 101 | self.position_or_direction, 102 | self.attenuation_lut, 103 | self.spotlight_lut, 104 | float_to_20bit(self.attenuation_scale), 105 | float_to_20bit(self.attenuation_bias), 106 | ) 107 | -------------------------------------------------------------------------------- /cgfx/cgfx.py: -------------------------------------------------------------------------------- 1 | from .dict import DICT, DictInfo 2 | from .shared import InlineObject, Signature 3 | from struct import Struct 4 | from .cmdl import CMDL 5 | from .txob import TXOB 6 | from .mtob import MTOB 7 | from .cflt import CFLT 8 | from .canm import CANM 9 | 10 | 11 | class CGFXHeader(InlineObject): 12 | struct = Struct("4sHhiii") 13 | offset = 0 14 | signature = Signature("CGFX") 15 | endianness = 0xFEFF 16 | header_size = 0x14 17 | version = 0x5000000 18 | file_size = 0 19 | nr_blocks = 1 20 | 21 | def values(self): 22 | return ( 23 | self.signature, 24 | self.endianness, 25 | self.header_size, 26 | self.version, 27 | self.file_size, 28 | self.nr_blocks, 29 | ) 30 | 31 | 32 | class CGFXData(InlineObject): 33 | struct = Struct("4si" + DictInfo.struct.format * 15) 34 | offset = CGFXHeader.offset + CGFXHeader.struct.size 35 | signature = Signature("DATA") 36 | section_size = 0 37 | models: DictInfo[CMDL] 38 | textures: DictInfo[TXOB] 39 | materials: DictInfo[MTOB] 40 | lights: DictInfo[CFLT] 41 | skeletal_animations: DictInfo[CANM] 42 | material_animations: DictInfo[CANM] 43 | visibility_animations: DictInfo[CANM] 44 | 45 | def __init__(self) -> None: 46 | super().__init__() 47 | self.models = DictInfo() 48 | self.textures = DictInfo() 49 | self.lookup_tables = DictInfo() 50 | self.materials = DictInfo() 51 | self.shaders = DictInfo() 52 | self.cameras = DictInfo() 53 | self.lights = DictInfo() 54 | self.fogs = DictInfo() 55 | self.scenes = DictInfo() 56 | self.skeletal_animations = DictInfo() 57 | self.material_animations = DictInfo() 58 | self.visibility_animations = DictInfo() 59 | self.camera_animations = DictInfo() 60 | self.light_animations = DictInfo() 61 | self.emitters = DictInfo() 62 | 63 | def values(self) -> tuple: 64 | return ( 65 | self.signature, 66 | self.section_size, 67 | self.models, 68 | self.textures, 69 | self.lookup_tables, 70 | self.materials, 71 | self.shaders, 72 | self.cameras, 73 | self.lights, 74 | self.fogs, 75 | self.scenes, 76 | self.skeletal_animations, 77 | self.material_animations, 78 | self.visibility_animations, 79 | self.camera_animations, 80 | self.light_animations, 81 | self.emitters, 82 | ) 83 | 84 | 85 | class CGFX(InlineObject): 86 | struct = Struct(CGFXHeader.struct.format + CGFXData.struct.format) 87 | offset = 0 88 | header: CGFXHeader 89 | data: CGFXData 90 | 91 | def __init__(self) -> None: 92 | super().__init__() 93 | self.header = CGFXHeader() 94 | self.data = CGFXData() 95 | 96 | def values(self) -> tuple: 97 | return (self.header, self.data) 98 | -------------------------------------------------------------------------------- /cgfx/cmdl.py: -------------------------------------------------------------------------------- 1 | from .dict import DictInfo 2 | from .shared import Signature, StandardObject, Vector3, Vector4, Matrix, List 3 | from .sobj import SOBJMesh, SOBJShape, SOBJSkeleton 4 | from struct import Struct 5 | from .mtob import MTOB 6 | from enum import IntEnum 7 | from .animation import GraphicsAnimationGroup 8 | 9 | 10 | class CMDL(StandardObject): 11 | struct = Struct("i4siiiiiiixxxxiifffffffff" + "f" * 12 * 2 + "i" * 11) 12 | type = 0x40000012 13 | signature = Signature("CMDL") 14 | revision = 0x7000000 15 | name = "" 16 | user_data: DictInfo 17 | flags = 1 # looks transform-related, specific meaning unknown 18 | branch_visible = False # unused 19 | nr_children = 0 20 | animation_group_descriptions: DictInfo[GraphicsAnimationGroup] 21 | scale: Vector3 22 | rotation: Vector3 23 | translation: Vector3 24 | local: Matrix 25 | world: Matrix 26 | meshes: List[SOBJMesh] 27 | materials: DictInfo[MTOB] 28 | shapes: List[SOBJShape] 29 | mesh_nodes: DictInfo 30 | visible = True 31 | cull_mode = 0 32 | layer_id = 0 33 | 34 | def __init__(self) -> None: 35 | super().__init__() 36 | self.scale = Vector3(1, 1, 1) 37 | self.rotation = Vector3(0, 0, 0) 38 | self.translation = Vector3(0, 0, 0) 39 | self.local = Matrix( 40 | Vector4(1, 0, 0, 0), Vector4(0, 1, 0, 0), Vector4(0, 0, 1, 0) 41 | ) 42 | self.world = Matrix( 43 | Vector4(1, 0, 0, 0), Vector4(0, 1, 0, 0), Vector4(0, 0, 1, 0) 44 | ) 45 | self.user_data = DictInfo() 46 | self.animation_group_descriptions = DictInfo() 47 | self.meshes = List() 48 | self.materials = DictInfo() 49 | self.shapes = List() 50 | self.mesh_nodes = DictInfo() 51 | 52 | def values(self) -> tuple: 53 | return ( 54 | self.type, 55 | self.signature, 56 | self.revision, 57 | self.name, 58 | self.user_data, 59 | self.flags, 60 | self.branch_visible, 61 | self.nr_children, 62 | self.animation_group_descriptions, 63 | self.scale, 64 | self.rotation, 65 | self.translation, 66 | self.local, 67 | self.world, 68 | self.meshes, 69 | self.materials, 70 | self.shapes, 71 | self.mesh_nodes, 72 | self.visible, 73 | self.cull_mode, 74 | self.layer_id, 75 | ) 76 | 77 | 78 | class CMDLWithSkeleton(CMDL): 79 | struct = Struct(CMDL.struct.format + "i") 80 | type = CMDL.type | 0x80 81 | skeleton: SOBJSkeleton = None 82 | 83 | def values(self) -> tuple: 84 | return super().values() + (self.skeleton,) 85 | -------------------------------------------------------------------------------- /cgfx/dict.py: -------------------------------------------------------------------------------- 1 | from .shared import InlineObject, Signature, StandardObject, StringTable 2 | from typing import TypeVar, Generic 3 | from struct import Struct 4 | from . import patricia 5 | 6 | T = TypeVar("T", bound=StandardObject) 7 | 8 | 9 | class Node(InlineObject, Generic[T]): 10 | struct = Struct("ihhii") 11 | refbit: int 12 | left_index: int 13 | right_index: int 14 | name: str 15 | content: T 16 | 17 | def __init__(self, name: str, content: T) -> None: 18 | super().__init__() 19 | self.name = name 20 | self.content = content 21 | self.refbit = -1 22 | self.left_index = 0 23 | self.right_index = 0 24 | 25 | def values(self) -> tuple: 26 | return (self.refbit, self.left_index, self.right_index, self.name, self.content) 27 | 28 | def get_name(self) -> str: 29 | return self.name or "" 30 | 31 | 32 | class DICT(StandardObject, Generic[T]): 33 | signature = Signature("DICT") 34 | nodes: list[Node] 35 | 36 | def __init__(self) -> None: 37 | super().__init__() 38 | self.nodes = [Node(None, None)] 39 | 40 | def refresh_struct(self): 41 | self.struct = Struct("4sii" + Node.struct.format * len(self.nodes)) 42 | 43 | def values(self) -> tuple: 44 | self.refresh_struct() 45 | return (self.signature, self.struct.size, self.len()) + tuple(self.nodes) 46 | 47 | def len(self): 48 | return len(self.nodes) - 1 49 | 50 | def __getitem__(self, name: str) -> T: 51 | if isinstance(name, int): 52 | return self.nodes[name + 1].content 53 | # could be smarter but eh 54 | for n in self.nodes: 55 | if n.name == name: 56 | return n.content 57 | return None 58 | 59 | def __iter__(self): 60 | for n in self.nodes[1:]: 61 | yield n.name 62 | 63 | def get_index(self, name: str) -> int: 64 | for i in range(len(self.nodes)): 65 | if self.nodes[i].name == name: 66 | return i - 1 67 | 68 | def add(self, name: str, data: T): 69 | self.nodes.append(Node(name, data)) 70 | self.regenerate() 71 | 72 | def regenerate(self): 73 | tree = patricia.generate( 74 | [n.get_name() for n in self.nodes if n != self.nodes[0]] 75 | ) 76 | tree.root.idx_entry = -1 77 | for n in self.nodes: 78 | p = tree[n.get_name()] 79 | assert p.name == n.get_name().ljust( 80 | tree.string_length, "\0" 81 | ), f"{p.name}, {n.get_name()}" 82 | if n != self.nodes[0]: 83 | n.refbit = p.refbit 84 | n.left_index = p.left.idx_entry + 1 85 | n.right_index = p.right.idx_entry + 1 86 | 87 | 88 | class DictInfo(InlineObject, Generic[T]): 89 | struct = Struct("ii") 90 | dict: DICT 91 | 92 | def __init__(self) -> None: 93 | super().__init__() 94 | self.dict = DICT() 95 | 96 | def values(self) -> tuple: 97 | return (self.dict.len(), self.dict if self.dict.len() else None) 98 | 99 | def add(self, name: str, data: T): 100 | self.dict.add(name, data) 101 | 102 | def __getitem__(self, name: str) -> T: 103 | return self.dict[name] 104 | 105 | def len(self) -> int: 106 | return self.dict.len() 107 | 108 | def __iter__(self): 109 | return iter(self.dict) 110 | 111 | def get_index(self, name: str) -> int: 112 | return self.dict.get_index(name) 113 | -------------------------------------------------------------------------------- /cgfx/luts.py: -------------------------------------------------------------------------------- 1 | from .shared import StandardObject, Signature 2 | from .dict import DictInfo 3 | from struct import Struct 4 | 5 | 6 | def generate_lut_commands(lut: list[float]) -> bytes: 7 | assert len(lut) == 256 8 | command = b"\xc8\x01\xff\x07" 9 | values = [min(int(i * 0x1000), 0xFFF) for i in lut] 10 | diffs = [min(int(abs(j - i) * 0x800), 0x7FF) for i, j in zip(lut, lut[1:])] + [0] 11 | fixed = [((d << 12) | v).to_bytes(4, "little") for v, d in zip(values, diffs)] 12 | assert len(fixed) == 256 13 | return ( 14 | fixed[0] 15 | + command 16 | + b"".join(fixed[1:128]) 17 | + b"\0" * 4 18 | + fixed[128] 19 | + command 20 | + b"".join(fixed[129:]) 21 | + b"\0" * 4 22 | ) 23 | 24 | 25 | class LutTable(StandardObject): 26 | struct = Struct("Iiiii") 27 | type = 0x80000000 28 | name = "" 29 | some_bool = True 30 | lut: list[float] 31 | 32 | def __init__(self, lut=None): 33 | self.lut = lut or [1 - abs(i / 128) for i in range(-128, 128)] 34 | 35 | def values(self): 36 | return (self.type, self.name, self.some_bool, generate_lut_commands(self.lut)) 37 | 38 | @staticmethod 39 | def phong(shininess) -> "LutTable": 40 | return LutTable([pow(i / 256, shininess) for i in range(256)]) 41 | 42 | 43 | class LUTS(StandardObject): 44 | struct = Struct("I4siiiiii") 45 | type = 0x04000000 46 | signature = Signature("LUTS") 47 | revision = 0x04000000 48 | name = "" 49 | user_data: DictInfo 50 | tables: DictInfo[LutTable] 51 | 52 | def __init__(self): 53 | self.user_data = DictInfo() 54 | self.tables = DictInfo() 55 | 56 | def values(self): 57 | return ( 58 | self.type, 59 | self.signature, 60 | self.revision, 61 | self.name, 62 | self.user_data, 63 | self.tables, 64 | ) 65 | -------------------------------------------------------------------------------- /cgfx/mtob.py: -------------------------------------------------------------------------------- 1 | from .shared import ( 2 | InlineObject, 3 | StandardObject, 4 | Signature, 5 | Matrix, 6 | Vector3, 7 | Vector4, 8 | Reference, 9 | ColorByte, 10 | ColorFloat, 11 | ) 12 | from .dict import DictInfo 13 | from struct import Struct 14 | from .txob import TXOB 15 | from enum import IntEnum, IntFlag 16 | import itertools 17 | 18 | 19 | class MTOBFlag(IntFlag): 20 | FragmentLight = 1 21 | VertexLight = 2 22 | HemisphereLight = 4 23 | HemisphereOcclusion = 8 24 | Fog = 16 25 | PolygonOffset = 32 26 | 27 | 28 | class CullMode(IntEnum): 29 | Never = 0 30 | FrontFace = 1 31 | BackFace = 2 32 | 33 | 34 | class TextureProjection(IntEnum): 35 | UVMap = 0 36 | CameraCubeMap = 1 37 | CameraSphereMap = 2 38 | ProjectionMap = 3 39 | ShadowMap = 4 40 | ShadowCubeMap = 5 41 | 42 | 43 | class FresnelConfig(IntFlag): 44 | Primary = 1 45 | Secondary = 2 46 | 47 | 48 | class BumpMode(IntEnum): 49 | NotUsed = 0 50 | AsBump = 1 51 | AsTangent = 2 52 | 53 | 54 | class DepthFlag(IntFlag): 55 | TestEnabled = 1 # depth read 56 | MaskEnabled = 2 # depth write 57 | 58 | 59 | class PicaCommand(InlineObject): 60 | struct = Struct("II") 61 | 62 | def __init__(self, param, head): 63 | self.param = param 64 | self.head = head 65 | 66 | def values(self): 67 | return (self.param, self.head) 68 | 69 | 70 | class MaterialColor(InlineObject): 71 | struct = Struct("ffff" * (3 + 2 + 6) + "BBBB" * (3 + 2 + 6) + "i") 72 | emission = ColorFloat(0, 0, 0, 0) 73 | ambient = ColorFloat(0, 0, 0, 1) 74 | diffuse = ColorFloat(1, 1, 1, 1) 75 | specular: list[ColorFloat] 76 | constant: list[ColorFloat] 77 | command_cache = 0 78 | 79 | def __init__(self): 80 | self.specular = [ColorFloat(1, 1, 1, 0), ColorFloat(0, 0, 0, 0)] 81 | self.constant = [ColorFloat(0, 0, 0, 0) for _ in range(6)] 82 | 83 | def values(self): 84 | return ( 85 | self.emission, 86 | self.ambient, 87 | self.diffuse, 88 | *self.specular, 89 | *self.constant, 90 | self.emission.as_byte(), 91 | self.ambient.as_byte(), 92 | self.diffuse.as_byte(), 93 | *(x.as_byte() for x in self.specular + self.constant), 94 | self.command_cache, 95 | ) 96 | 97 | 98 | class Rasterization(InlineObject): 99 | struct = Struct("iifii") 100 | flags = 0 101 | cull_mode = CullMode.BackFace # ignored by the home menu 102 | polygon_offset_unit = 0 103 | command: PicaCommand 104 | 105 | def __init__(self): 106 | # this command actually defines the cull mode 107 | self.command = PicaCommand(CullMode.BackFace, 0x00010040) 108 | 109 | def values(self): 110 | return (self.flags, self.cull_mode, self.polygon_offset_unit, self.command) 111 | 112 | 113 | class DepthOperation(InlineObject): 114 | struct = Struct("iiiii") 115 | flags = DepthFlag.MaskEnabled | DepthFlag.TestEnabled 116 | commands: list[PicaCommand] 117 | 118 | def __init__(self): 119 | self.commands = [PicaCommand(0x41, 0x10107), PicaCommand(0x3000000, 0x80126)] 120 | 121 | def values(self): 122 | return (self.flags, *self.commands) 123 | 124 | 125 | class BlendEquation(IntEnum): 126 | Add = 0 127 | Subtract = 1 128 | RevSubtract = 2 129 | Minimum = 3 130 | Maximum = 4 131 | 132 | 133 | class BlendFunction(IntEnum): 134 | Zero = 0 135 | One = 1 136 | SrcColor = 2 137 | InvSrccolor = 3 138 | DstColor = 4 139 | InvDstColor = 5 140 | SrcAlpha = 6 141 | InvSrcAlpha = 7 142 | DstAlpha = 8 143 | InvDstAlpha = 9 144 | ConstColor = 10 145 | InvConstColor = 11 146 | ConstAlpha = 12 147 | InvConstAlpha = 13 148 | SrcAlphaSaturate = 14 149 | 150 | 151 | class BlendOperation(InlineObject): 152 | struct = Struct("iffffIIIIII") 153 | mode = 1 154 | blend = ColorFloat(0, 0, 0, 1) 155 | equation_color = BlendEquation.Add 156 | equation_alpha = BlendEquation.Add 157 | src_color = BlendFunction.One 158 | dst_color = BlendFunction.Zero 159 | src_alpha = BlendFunction.One 160 | dst_alpha = BlendFunction.Zero 161 | 162 | def values(self): 163 | return ( 164 | self.mode, 165 | self.blend, 166 | PicaCommand(0xE40100, 0x803F0100), 167 | PicaCommand( 168 | self.equation_color 169 | | (self.equation_alpha << 8) 170 | | (self.src_color << 16) 171 | | (self.dst_color << 20) 172 | | (self.src_alpha << 24) 173 | | (self.dst_alpha << 28), 174 | 0, 175 | ), 176 | PicaCommand( 177 | int.from_bytes(bytes(self.blend.as_byte().values()), "little"), 0 178 | ), 179 | ) 180 | 181 | 182 | class FragmentOperation(InlineObject): 183 | struct = Struct( 184 | DepthOperation.struct.format + BlendOperation.struct.format + "IIII" 185 | ) 186 | depth_operation: DepthOperation 187 | blend_operation: BlendOperation 188 | # stencil commands may be modified at runtime 189 | stencil_commands: list[PicaCommand] 190 | 191 | def __init__(self): 192 | self.depth_operation = DepthOperation() 193 | self.blend_operation = BlendOperation() 194 | self.stencil_commands = [PicaCommand(0, 0xD0105), PicaCommand(0, 0xF0106)] 195 | 196 | def values(self): 197 | return (self.depth_operation, self.blend_operation, *self.stencil_commands) 198 | 199 | 200 | class TextureCoordinator(InlineObject): 201 | # first byte of padding is modified at runtime 202 | struct = Struct("iiiifffff?xxx" + "f" * 12) 203 | source_coordinate = 0 204 | projection = TextureProjection.UVMap 205 | reference_camera = 0 206 | matrix_mode = 0 207 | scale_u = 1 208 | scale_v = 1 209 | rotate = 0 210 | translate_u = 0 211 | translate_v = 0 212 | # if true, matrix is generated at runtime based on matrix mode 213 | should_generate_matrix = False 214 | transform_matrix: Matrix 215 | 216 | def __init__(self): 217 | self.transform_matrix = Matrix( 218 | Vector4(1, 0, 0, 0), Vector4(0, 1, 0, 0), Vector4(0, 0, 1, 0) 219 | ) 220 | 221 | def values(self): 222 | return ( 223 | self.source_coordinate, 224 | self.projection, 225 | self.reference_camera, 226 | self.matrix_mode, 227 | self.scale_u, 228 | self.scale_v, 229 | self.rotate, 230 | self.translate_u, 231 | self.translate_v, 232 | self.should_generate_matrix, 233 | self.transform_matrix, 234 | ) 235 | 236 | 237 | class TextureSampler(StandardObject): 238 | struct = Struct("Iiiffff") 239 | type = 0x80000000 240 | owner: "TexInfo" 241 | min_filter = 0 242 | border_color: ColorFloat 243 | 244 | def __init__(self, owner): 245 | self.owner = owner 246 | self.border_color = ColorFloat(0, 0, 0, 0) 247 | 248 | def values(self): 249 | return (self.type, Reference(self.owner), self.min_filter, self.border_color) 250 | 251 | 252 | class TexInfo(StandardObject): 253 | struct = Struct("Iiii" + "I" * 14 + "i") 254 | type = 0x80000000 255 | dynamic_allocator = 0 256 | txob: TXOB 257 | sampler: TextureSampler 258 | # commands + count are modified at runtime 259 | commands: list[PicaCommand] 260 | command_size_to_send = 0x38 261 | 262 | def __init__(self, txob: TXOB): 263 | self.txob = txob 264 | self.sampler = TextureSampler(self) 265 | self.commands = [ 266 | PicaCommand(0, 0x1008E), 267 | PicaCommand(0xFF000000, 0x809F0081), 268 | PicaCommand(0, 0), 269 | PicaCommand(0, 0), 270 | PicaCommand(0, 0), 271 | PicaCommand(0, 0), 272 | PicaCommand(0, 0), 273 | ] 274 | 275 | def values(self): 276 | return ( 277 | self.type, 278 | self.dynamic_allocator, 279 | self.txob, 280 | self.sampler, 281 | *self.commands, 282 | self.command_size_to_send, 283 | ) 284 | 285 | 286 | class SHDR(StandardObject): 287 | struct = Struct("I4siiii") 288 | type: int 289 | signature = Signature("SHDR") 290 | revision = 0x5000000 291 | name = "" 292 | user_data: DictInfo 293 | 294 | def __init__(self): 295 | self.user_data = DictInfo() 296 | 297 | def values(self): 298 | return (self.type, self.signature, self.revision, self.name, self.user_data) 299 | 300 | 301 | class LinkedShader(SHDR): 302 | # padding is modified at runtime 303 | struct = Struct(SHDR.struct.format + "ixxxx") 304 | type = 0x80000001 305 | reference_shader_name = "DefaultShader" 306 | 307 | def values(self): 308 | return super().values() + (self.reference_shader_name,) 309 | 310 | 311 | class FragmentLightingFlags(IntFlag): 312 | ClampHighLight = 1 313 | UseDistribution0 = 2 314 | UseDistribution1 = 4 315 | UseGeometricFactor0 = 8 316 | UseGeometricFactor1 = 16 317 | UseReflection = 32 318 | 319 | 320 | class FragmentLighting(InlineObject): 321 | struct = Struct("iiiiii") 322 | flags = FragmentLightingFlags(0) 323 | layer_config = 0 # bits 4-7 of GPUREG_LIGHTING_CONFIG0 324 | fresnel_config = FresnelConfig(0) 325 | bump_texture = 0 326 | bump_mode = BumpMode.NotUsed 327 | is_bump_renormalize = False 328 | 329 | def values(self): 330 | return ( 331 | self.flags, 332 | self.layer_config, 333 | self.fresnel_config, 334 | self.bump_texture, 335 | self.bump_mode, 336 | self.is_bump_renormalize, 337 | ) 338 | 339 | 340 | class ReferenceLookupTable(StandardObject): 341 | struct = Struct("iiixxxx") 342 | type = 0x40000000 343 | # used to look up the LUTS in the DATA block 344 | binary_path = "" 345 | # used to look up the table within the LUTS 346 | table_name = "" 347 | 348 | def values(self): 349 | return (self.type, self.binary_path, self.table_name) 350 | 351 | 352 | class LightingLookupTable(StandardObject): 353 | struct = Struct("iii") 354 | input_command = 0 # GPUREG_LIGHTING_LUTINPUT_SELECT value 355 | scale_command = 0 # GPUREG_LIGHTING_LUTINPUT_SCALE value 356 | sampler: ReferenceLookupTable 357 | 358 | def __init__(self): 359 | self.sampler = ReferenceLookupTable() 360 | 361 | def values(self): 362 | return (self.input_command, self.scale_command, self.sampler) 363 | 364 | 365 | class FragmentLightingTable(StandardObject): 366 | struct = Struct("iiiiii") 367 | reflectance_r_sampler: LightingLookupTable = None 368 | reflectance_g_sampler: LightingLookupTable = None 369 | reflectance_b_sampler: LightingLookupTable = None 370 | distribution_0_sampler: LightingLookupTable = None 371 | distribution_1_sampler: LightingLookupTable = None 372 | fresnel_sampler: LightingLookupTable = None 373 | 374 | def values(self): 375 | return ( 376 | self.reflectance_r_sampler, 377 | self.reflectance_g_sampler, 378 | self.reflectance_b_sampler, 379 | self.distribution_0_sampler, 380 | self.distribution_1_sampler, 381 | self.fresnel_sampler, 382 | ) 383 | 384 | 385 | class ConstantColorSource(IntEnum): 386 | Constant0 = 0 387 | Constant1 = 1 388 | Constant2 = 2 389 | Constant3 = 3 390 | Constant4 = 4 391 | Constant5 = 5 392 | Emission = 6 393 | Ambient = 7 394 | Diffuse = 8 395 | Specular0 = 9 396 | Specular1 = 10 397 | 398 | 399 | class TextureCombiner(InlineObject): 400 | struct = Struct("ihhIihhxxxxhh") 401 | constant = ConstantColorSource.Constant0 402 | src_rgb = 0xFFF 403 | src_alpha = 0xFFF 404 | header: int 405 | tev_ops = 0 406 | combine_rgb = 0 407 | combine_alpha = 0 408 | scale_rgb = 0 409 | scale_alpha = 0 410 | 411 | def __init__(self, i): 412 | self.header = 0x804F0000 | ((0xC0 if i < 4 else 0xD0) + i * 8) 413 | self.const_rgba = ColorByte(0, 0, 0, 255) 414 | 415 | def values(self): 416 | return ( 417 | self.constant, 418 | self.src_rgb, 419 | self.src_alpha, 420 | self.header, 421 | self.tev_ops, 422 | self.combine_rgb, 423 | self.combine_alpha, 424 | self.scale_rgb, 425 | self.scale_alpha, 426 | ) 427 | 428 | 429 | class AlphaTestFunction(IntEnum): 430 | Never = 0 431 | Always = 1 432 | Equal = 2 433 | NotEqual = 3 434 | Less = 4 435 | LessEqual = 5 436 | Greater = 6 437 | GreaterEqual = 7 438 | 439 | 440 | class AlphaTest(InlineObject): 441 | enabled = False 442 | function = AlphaTestFunction.GreaterEqual 443 | cutoff = 128 444 | 445 | def values(self): 446 | return ( 447 | PicaCommand( 448 | self.enabled | (self.function << 4) | (self.cutoff << 8), 0xF0104 449 | ), 450 | ) 451 | 452 | 453 | class FragmentShader(StandardObject): 454 | struct = Struct( 455 | "ffff" 456 | + FragmentLighting.struct.format 457 | + "i" 458 | + TextureCombiner.struct.format * 6 459 | + "IIIIIIII" 460 | ) 461 | buffer_color: ColorFloat 462 | fragment_lighting: FragmentLighting 463 | fragment_lighting_table: FragmentLightingTable 464 | texture_combiners: list[TextureCombiner] 465 | alpha_test: AlphaTest 466 | buffer_commands: list[PicaCommand] 467 | 468 | def __init__(self): 469 | self.buffer_color = ColorFloat(0, 0, 0, 0) 470 | self.fragment_lighting = FragmentLighting() 471 | self.fragment_lighting_table = FragmentLightingTable() 472 | self.texture_combiners = [TextureCombiner(i) for i in range(6)] 473 | self.alpha_test = AlphaTest() 474 | self.buffer_commands = [ 475 | PicaCommand(0xFF000000, 0xF00FD), 476 | PicaCommand(0, 0x200E0), 477 | PicaCommand(0x400, 0x201C3), 478 | ] 479 | 480 | def values(self): 481 | return ( 482 | self.buffer_color, 483 | self.fragment_lighting, 484 | self.fragment_lighting_table, 485 | *self.texture_combiners, 486 | self.alpha_test, 487 | *self.buffer_commands, 488 | ) 489 | 490 | 491 | class MTOB(StandardObject): 492 | # padding is written to at runtime 493 | struct = Struct( 494 | "i4siiiiiii" 495 | + MaterialColor.struct.format 496 | + Rasterization.struct.format 497 | + FragmentOperation.struct.format 498 | + "i" 499 | + TextureCoordinator.struct.format * 3 500 | + "iii" 501 | + "iiiiiiii" 502 | + "IIIIIIIIIIIII" 503 | + "xxxx" 504 | ) 505 | type = 0x8000000 506 | signature = Signature("MTOB") 507 | revision = 0x6000000 508 | name = "" 509 | user_data: DictInfo 510 | flags = MTOBFlag(0) 511 | texture_coordinates_config = 0 512 | transluscency_kind = 0 513 | material_color: MaterialColor 514 | rasterization: Rasterization 515 | fragment_operations: FragmentOperation 516 | used_texture_coordinates_count = 0 517 | texture_coordinators: list[TextureCoordinator] 518 | texture_mappers: list[TexInfo] 519 | shader: SHDR 520 | fragment_shader: FragmentShader 521 | shader_program_description_index = 0 522 | shader_parameters_count = 0 523 | shader_parameters_pointer_table = 0 524 | light_set_index = 0 525 | fog_index = 0 526 | 527 | def __init__(self): 528 | self.user_data = DictInfo() 529 | self.material_color = MaterialColor() 530 | self.rasterization = Rasterization() 531 | self.fragment_operations = FragmentOperation() 532 | self.texture_coordinators = [TextureCoordinator() for _ in range(3)] 533 | self.texture_mappers: list[TexInfo] = [None, None, None, None] 534 | self.shader = LinkedShader() 535 | self.fragment_shader = FragmentShader() 536 | 537 | def values(self): 538 | return ( 539 | self.type, 540 | self.signature, 541 | self.revision, 542 | self.name, 543 | self.user_data, 544 | self.flags, 545 | self.texture_coordinates_config, 546 | self.transluscency_kind, 547 | self.material_color, 548 | self.rasterization, 549 | self.fragment_operations, 550 | self.used_texture_coordinates_count, 551 | *self.texture_coordinators, 552 | *self.texture_mappers, 553 | self.shader, 554 | self.fragment_shader, 555 | self.shader_program_description_index, 556 | self.shader_parameters_count, 557 | self.shader_parameters_pointer_table, 558 | self.light_set_index, 559 | self.fog_index, 560 | self.shading_parameters_hash(), 561 | self.shader_parameters_hash(), 562 | self.texture_coordinators_hash(), 563 | self.texture_samplers_hash(), 564 | # texture mappers hash is calculated at runtime 565 | 0, 566 | self.material_color_hash(), 567 | self.rasterization_hash(), 568 | self.fragment_lighting_hash(), 569 | # fragment lighting table hash is calculated at runtime 570 | 0, 571 | self.fragmnent_lighting_table_parameters_hash(), 572 | self.texture_combiners_hash(), 573 | self.alpha_test_hash(), 574 | self.fragment_operations_hash(), 575 | ) 576 | 577 | def shading_parameters_hash(self): 578 | # TODO 579 | return 0 580 | 581 | def shader_parameters_hash(self): 582 | # TODO 583 | return 0 584 | 585 | def texture_coordinators_hash(self): 586 | # TODO 587 | return 0 588 | 589 | def texture_samplers_hash(self): 590 | # used to calculate texture mappers hash 591 | return ( 592 | hash( 593 | tuple( 594 | v 595 | for v in itertools.chain.from_iterable( 596 | m.flat_values() for m in self.texture_mappers if m is not None 597 | ) 598 | if not isinstance(v, StandardObject) 599 | ) 600 | ) 601 | & 0xFFFFFFFF 602 | ) 603 | 604 | def material_color_hash(self): 605 | # TODO 606 | return 0 607 | 608 | def rasterization_hash(self): 609 | # TODO 610 | return 0 611 | 612 | def fragment_lighting_hash(self): 613 | # TODO 614 | return 0 615 | 616 | def fragmnent_lighting_table_parameters_hash(self): 617 | # used to calculate fragment lighting table hash 618 | return ( 619 | hash( 620 | tuple( 621 | v 622 | for v in itertools.chain.from_iterable( 623 | m.values() 624 | for m in self.fragment_shader.fragment_lighting_table.values() 625 | if m 626 | ) 627 | if not isinstance(v, StandardObject) 628 | ) 629 | ) 630 | & 0xFFFFFFFF 631 | ) 632 | 633 | def texture_combiners_hash(self): 634 | # TODO 635 | return 0 636 | 637 | def alpha_test_hash(self): 638 | # TODO 639 | return 0 640 | 641 | def fragment_operations_hash(self): 642 | # TODO 643 | return 0 644 | -------------------------------------------------------------------------------- /cgfx/patricia.py: -------------------------------------------------------------------------------- 1 | # straight translation from https://github.com/Gericom/EveryFileExplorer/blob/master/3DS/NintendoWare/GFX/PatriciaTreeGenerator.cs 2 | 3 | 4 | class Node: 5 | refbit: int 6 | left: "Node" 7 | right: "Node" 8 | idx_entry: int 9 | name: str 10 | 11 | 12 | def get_bit(name: str, bit: int) -> bool: 13 | return ((ord(name[bit // 8]) >> (bit & 7)) & 1) != 0 14 | 15 | 16 | class PatTree: 17 | root: Node 18 | string_length: int 19 | 20 | def __init__(self, maxlen) -> None: 21 | self.string_length = maxlen 22 | root = Node() 23 | root.refbit = maxlen * 8 - 1 24 | root.left = root 25 | root.right = root 26 | root.idx_entry = 0 27 | root.name = "\0" * maxlen 28 | self.root = root 29 | 30 | def add(self, name: str, index: int): 31 | name = name.ljust(self.string_length, "\0") 32 | new_node = Node() 33 | new_node.name = name 34 | new_node.idx_entry = index 35 | 36 | left = self[name] 37 | 38 | bit = self.string_length * 8 - 1 39 | while get_bit(left.name, bit) == get_bit(name, bit): 40 | bit -= 1 41 | 42 | left, current = self.get_with_parent(name, bit) 43 | 44 | new_node.refbit = bit 45 | new_node.left = left if get_bit(name, bit) else new_node 46 | new_node.right = new_node if get_bit(name, bit) else left 47 | if get_bit(name, current.refbit): 48 | current.right = new_node 49 | else: 50 | current.left = new_node 51 | return new_node 52 | 53 | def __getitem__(self, name: str) -> Node: 54 | return self.get_with_parent(name)[0] 55 | 56 | def get_with_parent(self, name: str, minbit=-1) -> tuple[Node]: 57 | name = name.ljust(self.string_length, "\0") 58 | current = self.root 59 | left = current.left 60 | while current.refbit > left.refbit and left.refbit > minbit: 61 | current = left 62 | left = current.right if get_bit(name, current.refbit) else current.left 63 | return (left, current) 64 | 65 | 66 | def generate(names: list[str]) -> PatTree: 67 | tree = PatTree(max(len(n) for n in names)) 68 | for i, n in sorted(enumerate(names), key=lambda k: -len(k[1])): 69 | tree.add(n, i) 70 | return tree 71 | -------------------------------------------------------------------------------- /cgfx/primitives.py: -------------------------------------------------------------------------------- 1 | from .shared import StandardObject, List 2 | from struct import Struct 3 | from enum import IntEnum, IntFlag 4 | 5 | 6 | class DataType(IntEnum): 7 | Byte = 0x1400 8 | UByte = 0x1401 9 | Short = 0x1402 10 | Float = 0x1406 11 | 12 | 13 | class VertexAttributeUsage(IntEnum): 14 | Position = 0 15 | Normal = 1 16 | Tangent = 2 17 | Color = 3 18 | TextureCoordinate0 = 4 19 | TextureCoordinate1 = 5 20 | TextureCoordinate2 = 6 21 | BoneIndex = 7 22 | BoneWeight = 8 23 | UserAttribute0 = 9 24 | UserAttribute1 = 10 25 | UserAttribute2 = 11 26 | UserAttribute3 = 12 27 | UserAttribute4 = 13 28 | UserAttribute5 = 14 29 | UserAttribute6 = 15 30 | UserAttribute7 = 16 31 | UserAttribute8 = 17 32 | UserAttribute9 = 18 33 | UserAttribute10 = 19 34 | UserAttribute11 = 20 35 | Interlave = 21 36 | Quantity = 22 37 | 38 | 39 | class VertexAttributeFlag(IntFlag): 40 | VertexParam = 1 41 | Interleave = 2 42 | 43 | 44 | class IndexStream(StandardObject): 45 | struct = Struct("ib?xxiiiiiiiii") 46 | data_type = DataType.UByte 47 | primitive_mode = 0 48 | visible = True 49 | face_data: b"" 50 | buffer_object = 0 51 | location_flag = 0 52 | # command cache + size are modified at runtime 53 | command_cache = 0 54 | command_cache_size = 0 55 | location_address = 0 56 | memory_area = 0 57 | bounding_box_offset = 0 58 | 59 | def values(self) -> tuple: 60 | return ( 61 | self.data_type, 62 | self.primitive_mode, 63 | self.visible, 64 | self.face_data, 65 | self.buffer_object, 66 | self.location_flag, 67 | self.command_cache, 68 | self.command_cache_size, 69 | self.location_address, 70 | self.memory_area, 71 | self.bounding_box_offset, 72 | ) 73 | 74 | 75 | class Primitive(StandardObject): 76 | struct = Struct("iiiiii") 77 | index_streams: List[IndexStream] 78 | # buffer objects are written to at runtime, but are allocated in advance 79 | buffer_objects: List[int] 80 | flags = 0 # appears unused 81 | command_allocator = 0 82 | 83 | def __init__(self): 84 | self.index_streams = List() 85 | self.buffer_objects = List() 86 | 87 | def values(self) -> tuple: 88 | return ( 89 | self.index_streams, 90 | self.buffer_objects, 91 | self.flags, 92 | self.command_allocator, 93 | ) 94 | 95 | 96 | class PrimitiveSet(StandardObject): 97 | struct = Struct("iiiii") 98 | related_bones: List[int] 99 | skinning_mode = 0 100 | primitives: List[Primitive] 101 | 102 | def __init__(self): 103 | self.related_bones = List() 104 | self.primitives = List() 105 | 106 | def values(self) -> tuple: 107 | return (self.related_bones, self.skinning_mode, self.primitives) 108 | 109 | 110 | class VertexAttribute(StandardObject): 111 | type: int 112 | usage = VertexAttributeUsage.Position 113 | flags = VertexAttributeFlag(0) 114 | 115 | 116 | class InterleavedVertexStream(VertexAttribute): 117 | # padding is written to at runtime 118 | struct = Struct("iiixxxxiiiiiiii") 119 | type = 0x40000002 120 | flags = VertexAttributeFlag.Interleave 121 | location_flag = 0 122 | vertex_stream_data = b"" 123 | location_address = 0 124 | memory_area = 0 125 | vertex_data_entry_size = 0 126 | vertex_streams: List[VertexAttribute] 127 | 128 | def __init__(self): 129 | self.vertex_streams = List() 130 | 131 | def values(self): 132 | return ( 133 | self.type, 134 | self.usage, 135 | self.flags, 136 | self.location_flag, 137 | self.vertex_stream_data, 138 | self.location_address, 139 | self.memory_area, 140 | self.vertex_data_entry_size, 141 | self.vertex_streams, 142 | ) 143 | 144 | 145 | class VertexStream(VertexAttribute): 146 | struct = Struct("iiiiiiiiiiifi") 147 | type = 0x40000001 148 | buffer_object = 0 149 | location_flag = 0 150 | vertex_stream_data = b"" 151 | location_address = 0 152 | memory_area = 0 153 | format_type = DataType.Float 154 | components_count = 0 155 | scale = 1 156 | vert_offset = 0 157 | 158 | def values(self): 159 | return ( 160 | self.type, 161 | self.usage, 162 | self.flags, 163 | self.buffer_object, 164 | self.location_flag, 165 | self.vertex_stream_data, 166 | self.location_address, 167 | self.memory_area, 168 | self.format_type, 169 | self.components_count, 170 | self.scale, 171 | self.vert_offset, 172 | ) 173 | 174 | 175 | class VertexParamAttribute(VertexAttribute): 176 | struct = Struct("iiiiifii") 177 | type = 0x80000000 178 | flags = VertexAttributeFlag.VertexParam 179 | format_type = DataType.Float 180 | components_count = 0 181 | scale = 0 182 | attributes: List[float] 183 | 184 | def values(self): 185 | return ( 186 | self.type, 187 | self.usage, 188 | self.flags, 189 | self.format_type, 190 | self.components_count, 191 | self.scale, 192 | self.attributes, 193 | ) 194 | -------------------------------------------------------------------------------- /cgfx/shared.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | import struct 3 | from collections import OrderedDict 4 | from typing import Generic, TypeVar 5 | 6 | T = TypeVar("T") 7 | 8 | 9 | class Signature: 10 | data: str 11 | 12 | def __init__(self, data) -> None: 13 | self.data = data 14 | 15 | 16 | class StringTable: 17 | table: OrderedDict[bytes, int] 18 | total = 0 19 | padding: int 20 | offset: int 21 | 22 | def __init__(self) -> None: 23 | self.table = OrderedDict() 24 | 25 | @staticmethod 26 | def correct(s: bytes | str) -> bytes: 27 | if isinstance(s, str): 28 | return s.encode() + b"\0" 29 | else: 30 | # textures are aligned to 16 bytes 31 | # vertex buffers aren't but aligning everything is easier 32 | return s + b"\0" * (-len(s) % 16) 33 | return s 34 | 35 | def add(self, s: bytes | str): 36 | s = self.correct(s) 37 | if s not in self.table: 38 | self.table[s] = self.total 39 | self.total += len(s) 40 | 41 | def prepare(self, offset: int) -> int: 42 | self.offset = offset 43 | self.padding = -(offset + self.total) % 16 44 | self.padding ^= 8 # align content to 16, aka header to halfway through 45 | self.total += self.padding 46 | return offset + self.total 47 | 48 | def size(self) -> int: 49 | return self.total 50 | 51 | def empty(self) -> bool: 52 | return len(self.table) == 0 53 | 54 | def get(self, s: str) -> int: 55 | return self.offset + self.table[self.correct(s)] 56 | 57 | def write(self) -> bytes: 58 | return b"".join(self.table.keys()) + b"\0" * self.padding 59 | 60 | 61 | class BaseObject(ABC): 62 | struct: struct.Struct 63 | offset: int 64 | inline = False 65 | 66 | def refresh_struct(self): 67 | pass 68 | 69 | @abstractmethod 70 | def values(self) -> tuple: 71 | pass 72 | 73 | def flat_values(self): 74 | self.refresh_struct() 75 | for v in self.values(): 76 | if isinstance(v, InlineObject): 77 | # v.refresh_struct() 78 | # vals = list(v.flat_values()) 79 | # bufs = [] 80 | # for i in range(len(vals)): 81 | # if vals[i] is None: vals[i] = 0 82 | # if isinstance(vals[i], bytes): bufs.append(i) 83 | # if isinstance(vals[i], Signature): vals[i] = vals[i].data.encode() 84 | # if isinstance(vals[i], StandardObject): vals[i] = 0 85 | # if isinstance(vals[i], str): vals[i] = 0 86 | # for i in bufs[::-1]: 87 | # vals[i:i+1] = [0, 0] 88 | # v.struct.pack(*vals) 89 | yield from v.flat_values() 90 | else: 91 | yield v 92 | 93 | def real_values(self, strings, imag) -> tuple: 94 | values = [] 95 | fmt_pos = 0 96 | offset = self.offset 97 | for v in self.flat_values(): 98 | # add values 99 | if isinstance(v, StandardObject): 100 | values.append(v.offset - offset) 101 | elif isinstance(v, InlineObject): 102 | values += list(v.real_values(strings, imag)) 103 | elif isinstance(v, Reference): 104 | if v.obj is None: 105 | values.append(0) 106 | else: 107 | values.append(v.obj.offset - offset) 108 | elif isinstance(v, Signature): 109 | values.append(v.data.encode()) 110 | elif isinstance(v, str): 111 | # string 112 | values.append(strings.get(v) - offset) 113 | elif isinstance(v, bytes): 114 | # data 115 | values.append(len(v)) 116 | offset += 4 117 | fmt_pos += 1 118 | values.append(imag.get(v) - offset if v else 0) 119 | elif v is None: 120 | # null 121 | values.append(0) 122 | else: 123 | values.append(v) 124 | # update offset 125 | if isinstance(v, InlineObject): 126 | fmt_pos += v.size() 127 | else: 128 | fmt_pos += 1 129 | while fmt_pos < len(self.struct.format): 130 | if self.struct.format[fmt_pos] == "x": 131 | fmt_pos += 1 132 | continue 133 | try: 134 | offset = self.offset + struct.calcsize( 135 | self.struct.format[:fmt_pos] 136 | ) 137 | break 138 | except struct.error: 139 | if self.struct.format[fmt_pos - 1 : fmt_pos + 1] != "4s": 140 | raise RuntimeError( 141 | f"can't use numbers other than 4s (found {self.struct.format[fmt_pos:fmt_pos+2]})" 142 | ) 143 | fmt_pos += 1 144 | continue 145 | return values 146 | 147 | def prepare(self, offset: int, strings: StringTable, imag: StringTable) -> int: 148 | """offset is current offset, returns new offset""" 149 | self.refresh_struct() 150 | self.offset = offset 151 | values = self.values() 152 | old_len = 0 153 | while old_len != len(values): 154 | old_len = len(values) 155 | values = [ 156 | vv 157 | for v in values 158 | for vv in (v.values() if isinstance(v, InlineObject) else [v]) 159 | ] 160 | offset = self.offset + self.size() 161 | for v in values: 162 | if isinstance(v, StandardObject): 163 | offset = v.prepare(offset, strings, imag) 164 | elif isinstance(v, str): 165 | # string (not signature) 166 | strings.add(v) 167 | elif isinstance(v, bytes): 168 | imag.add(v) 169 | return offset 170 | 171 | def write(self, strings: StringTable, imag: StringTable) -> bytes: 172 | values = self.real_values(strings, imag) 173 | data = self.struct.pack(*values) 174 | for v in self.flat_values(): 175 | if isinstance(v, StandardObject): 176 | data += v.write(strings, imag) 177 | return data 178 | 179 | def size(self) -> int: 180 | self.refresh_struct() 181 | return self.struct.size 182 | 183 | def __eq__(self, other) -> bool: 184 | return isinstance(other, type(self)) and self.values() == other.values() 185 | 186 | 187 | class StandardObject(BaseObject): 188 | pass 189 | 190 | 191 | class InlineObject(BaseObject): 192 | pass 193 | 194 | 195 | class Reference: 196 | def __init__(self, obj: StandardObject): 197 | self.obj = obj 198 | 199 | 200 | class ListData(StandardObject, Generic[T]): 201 | contents: list[T] 202 | 203 | def __init__(self, list=None) -> None: 204 | super().__init__() 205 | self.contents = list if list else [] 206 | 207 | def refresh_struct(self): 208 | self.struct = struct.Struct("i" * len(self.contents)) 209 | 210 | def values(self) -> tuple: 211 | return tuple(self.contents) 212 | 213 | def __len__(self): 214 | return len(self.contents) 215 | 216 | def add(self, value: T): 217 | self.contents.append(value) 218 | 219 | 220 | class List(InlineObject, Generic[T]): 221 | data: ListData 222 | 223 | def __init__(self, list=None) -> None: 224 | super().__init__() 225 | self.data = ListData(list) 226 | 227 | def refresh_struct(self): 228 | self.struct = struct.Struct("ii") 229 | 230 | def values(self) -> tuple: 231 | return (len(self.data), self.data if len(self.data) else None) 232 | 233 | def add(self, value: T): 234 | self.data.add(value) 235 | 236 | def __len__(self): 237 | return len(self.data) 238 | 239 | 240 | class Vector3(InlineObject): 241 | struct = struct.Struct("fff") 242 | x: float 243 | y: float 244 | z: float 245 | 246 | def __init__(self, x, y, z): 247 | self.x = x 248 | self.y = y 249 | self.z = z 250 | 251 | def values(self) -> tuple: 252 | return (self.x, self.y, self.z) 253 | 254 | 255 | class Vector4(Vector3): 256 | struct = struct.Struct("ffff") 257 | w: float 258 | 259 | def __init__(self, x, y, z, w): 260 | super().__init__(x, y, z) 261 | self.w = w 262 | 263 | def values(self): 264 | return (self.x, self.y, self.z, self.w) 265 | 266 | 267 | class Matrix(InlineObject): 268 | struct = struct.Struct("f" * 12) 269 | columns: list[Vector4] 270 | 271 | def __init__(self, col1, col2, col3): 272 | self.columns = [col1, col2, col3] 273 | 274 | def values(self) -> tuple: 275 | return tuple(self.columns) 276 | 277 | 278 | class OrientationMatrix(InlineObject): 279 | struct = struct.Struct("f" * 9) 280 | columns: list[Vector3] 281 | 282 | def __init__(self, col1, col2, col3): 283 | self.columns = [col1, col2, col3] 284 | 285 | def values(self) -> tuple: 286 | return tuple(self.columns) 287 | 288 | 289 | class Color(InlineObject): 290 | def __init__(self, r, g, b, a): 291 | self.r = r 292 | self.g = g 293 | self.b = b 294 | self.a = a 295 | 296 | def values(self): 297 | return (self.r, self.g, self.b, self.a) 298 | 299 | 300 | class ColorByte(Color): 301 | struct = struct.Struct("BBBB") 302 | 303 | 304 | class ColorFloat(Color): 305 | struct = struct.Struct("ffff") 306 | 307 | def as_byte(self) -> ColorByte: 308 | return ColorByte( 309 | int(self.r * 255), int(self.g * 255), int(self.b * 255), int(self.a * 255) 310 | ) 311 | -------------------------------------------------------------------------------- /cgfx/sobj.py: -------------------------------------------------------------------------------- 1 | from struct import Struct 2 | 3 | from enum import IntEnum, IntFlag 4 | 5 | from .dict import DictInfo 6 | from .shared import ( 7 | Signature, 8 | StandardObject, 9 | List, 10 | Vector3, 11 | Vector4, 12 | OrientationMatrix, 13 | Reference, 14 | Matrix, 15 | ) 16 | 17 | from .primitives import VertexAttribute, PrimitiveSet 18 | 19 | from typing import TYPE_CHECKING 20 | 21 | if TYPE_CHECKING: 22 | from .cmdl import CMDL 23 | 24 | 25 | class SkeletonScalingRule(IntEnum): 26 | Standard = 0 27 | Maya = 1 28 | SoftImage = 2 29 | 30 | 31 | class BillboardMode(IntEnum): 32 | # When the non-viewpoint modes are in use, the bone always faces the camera identically. 33 | # With a viewpoint mode, the bone may appear to tilt as it moves towards the side. 34 | # Other than that, the true meaning of these modes is unclear, despite obvious 35 | # mathematical differences. 36 | Off = 0 37 | World = 1 38 | WorldViewpoint = 2 39 | Screen = 3 40 | ScreenViewpoint = 4 41 | YAxial = 5 42 | YAxialViewpoint = 6 43 | 44 | 45 | class SOBJMesh(StandardObject): 46 | # padding is used at runtime 47 | struct = Struct("i4siiiiiii?BH" + "x" * 4 * 18 + "i") 48 | type = 0x1000000 49 | signature = Signature("SOBJ") 50 | revision = 0 51 | name = "" 52 | user_data: DictInfo 53 | shape_index = 0 54 | material_index = 0 55 | owner: "CMDL" 56 | is_visible = True 57 | priority = 0 58 | mesh_node_visibility_index = 0 59 | mesh_node_name = "" 60 | 61 | def __init__(self, owner) -> None: 62 | super().__init__() 63 | self.user_data = DictInfo() 64 | self.owner = owner 65 | 66 | def values(self) -> tuple: 67 | return ( 68 | self.type, 69 | self.signature, 70 | self.revision, 71 | self.name, 72 | self.user_data, 73 | self.shape_index, 74 | self.material_index, 75 | Reference(self.owner), 76 | self.is_visible, 77 | self.priority, 78 | self.mesh_node_visibility_index, 79 | self.mesh_node_name, 80 | ) 81 | 82 | 83 | class OrientedBoundingBox(StandardObject): 84 | struct = Struct("I" + "f" * (3 + 9 + 3)) 85 | type = 0x80000000 86 | center_pos: Vector3 87 | orientation: Matrix 88 | bb_size: Vector3 89 | 90 | def __init__(self): 91 | self.center_pos = Vector3(0, 0, 0) 92 | self.orientation = OrientationMatrix( 93 | Vector3(1, 0, 0), Vector3(0, 1, 0), Vector3(0, 0, 1) 94 | ) 95 | self.bb_size = Vector3(1, 1, 1) 96 | 97 | def values(self) -> tuple: 98 | return (self.type, self.center_pos, self.orientation, self.bb_size) 99 | 100 | 101 | class SOBJShape(StandardObject): 102 | struct = Struct("i4siiiiiifffiiiiii") 103 | type = 0x10000001 104 | signature = Signature("SOBJ") 105 | revision = 0 106 | name = "" 107 | user_data: DictInfo 108 | # flags are modified at runtime 109 | flags = 0 110 | # the bounding box is unused, it gets read but nothing is done with it 111 | oriented_bounding_box: OrientedBoundingBox 112 | position_offset: Vector3 113 | primitive_sets: List[PrimitiveSet] 114 | # base address is modified at runtime 115 | base_address = 0 116 | vertex_attributes: List[VertexAttribute] 117 | blend_shape = 0 118 | 119 | def __init__(self) -> None: 120 | super().__init__() 121 | self.user_data = DictInfo() 122 | self.position_offset = Vector3(0, 0, 0) 123 | self.primitive_sets = List() 124 | self.vertex_attributes = List() 125 | self.oriented_bounding_box = OrientedBoundingBox() 126 | 127 | def values(self) -> tuple: 128 | return ( 129 | self.type, 130 | self.signature, 131 | self.revision, 132 | self.name, 133 | self.user_data, 134 | self.flags, 135 | self.oriented_bounding_box, 136 | self.position_offset, 137 | self.primitive_sets, 138 | self.base_address, 139 | self.vertex_attributes, 140 | self.blend_shape, 141 | ) 142 | 143 | 144 | class BoneFlag(IntFlag): 145 | IsIdentity = 1 146 | IsTranslateZero = 2 147 | IsRotateZero = 4 148 | IsScaleOne = 8 149 | IsUniformScale = 16 150 | IsSegmentScaleCompensate = 32 151 | IsNeedRendering = 64 152 | IsLocalMatrixCalculate = 128 153 | IsWorldMatrixCalculate = 256 154 | HasSkinningMatrix = 512 155 | 156 | 157 | class Bone(StandardObject): 158 | struct = Struct("iiiiiiiifffffffff" + "f" * (12 * 3) + "ixxxxxxxx") 159 | name = "" 160 | flags = BoneFlag(0) 161 | joint_id = 0 162 | parent_id = -1 163 | parent = None 164 | child = None 165 | previous_sibling = None 166 | next_sibling = None 167 | scale: Vector3 168 | rotation: Vector3 169 | position: Vector3 170 | local: Matrix 171 | world: Matrix 172 | inverse_base: Matrix 173 | billboard_mode = BillboardMode.Off 174 | 175 | def __init__(self): 176 | self.scale = Vector3(1, 1, 1) 177 | self.rotation = Vector3(0, 0, 0) 178 | self.position = Vector3(0, 0, 0) 179 | self.local = Matrix( 180 | Vector4(1, 0, 0, 0), Vector4(0, 1, 0, 0), Vector4(0, 0, 1, 0) 181 | ) 182 | self.world = Matrix( 183 | Vector4(1, 0, 0, 0), Vector4(0, 1, 0, 0), Vector4(0, 0, 1, 0) 184 | ) 185 | self.inverse_base = Matrix( 186 | Vector4(1, 0, 0, 0), Vector4(0, 1, 0, 0), Vector4(0, 0, 1, 0) 187 | ) 188 | 189 | def values(self): 190 | return ( 191 | self.name, 192 | self.flags, 193 | self.joint_id, 194 | self.parent_id, 195 | Reference(self.parent), 196 | Reference(self.child), 197 | Reference(self.previous_sibling), 198 | Reference(self.next_sibling), 199 | self.scale, 200 | self.rotation, 201 | self.position, 202 | self.local, 203 | self.world, 204 | self.inverse_base, 205 | self.billboard_mode, 206 | ) 207 | 208 | 209 | class SkeletonFlag(IntFlag): 210 | IsModelCoordinate = 1 211 | IsTranslateAnimationEnabled = 2 212 | 213 | 214 | class SOBJSkeleton(StandardObject): 215 | struct = Struct("i4siiiiiiiii") 216 | type = 0x2000000 217 | signature = Signature("SOBJ") 218 | revision = 0 219 | name = "" 220 | user_data: DictInfo 221 | bones: DictInfo[Bone] 222 | root_bone = None 223 | scaling_rule = SkeletonScalingRule.Standard 224 | flags = SkeletonFlag(0) 225 | 226 | def __init__(self): 227 | self.user_data = DictInfo() 228 | self.bones: DictInfo[Bone] = DictInfo() 229 | 230 | def values(self): 231 | return ( 232 | self.type, 233 | self.signature, 234 | self.revision, 235 | self.name, 236 | self.user_data, 237 | self.bones, 238 | Reference(self.root_bone), 239 | self.scaling_rule, 240 | self.flags, 241 | ) 242 | -------------------------------------------------------------------------------- /cgfx/swizzler.py: -------------------------------------------------------------------------------- 1 | from PIL.Image import Image 2 | from .txob import PixelBasedImage, TXOB, ImageTexture, TextureFormat 3 | 4 | 5 | def swizzle(im: Image, format: TextureFormat) -> bytes: 6 | swizzle_map = [ 7 | 0x00, 8 | 0x01, 9 | 0x04, 10 | 0x05, 11 | 0x10, 12 | 0x11, 13 | 0x14, 14 | 0x15, 15 | 0x02, 16 | 0x03, 17 | 0x06, 18 | 0x07, 19 | 0x12, 20 | 0x13, 21 | 0x16, 22 | 0x17, 23 | 0x08, 24 | 0x09, 25 | 0x0C, 26 | 0x0D, 27 | 0x18, 28 | 0x19, 29 | 0x1C, 30 | 0x1D, 31 | 0x0A, 32 | 0x0B, 33 | 0x0E, 34 | 0x0F, 35 | 0x1A, 36 | 0x1B, 37 | 0x1E, 38 | 0x1F, 39 | 0x20, 40 | 0x21, 41 | 0x24, 42 | 0x25, 43 | 0x30, 44 | 0x31, 45 | 0x34, 46 | 0x35, 47 | 0x22, 48 | 0x23, 49 | 0x26, 50 | 0x27, 51 | 0x32, 52 | 0x33, 53 | 0x36, 54 | 0x37, 55 | 0x28, 56 | 0x29, 57 | 0x2C, 58 | 0x2D, 59 | 0x38, 60 | 0x39, 61 | 0x3C, 62 | 0x3D, 63 | 0x2A, 64 | 0x2B, 65 | 0x2E, 66 | 0x2F, 67 | 0x3A, 68 | 0x3B, 69 | 0x3E, 70 | 0x3F, 71 | ] 72 | 73 | bpp_input = 4 74 | 75 | input = im.tobytes() 76 | output = b"" 77 | 78 | for ty in range(im.height // 8): 79 | for tx in range(im.width // 8): 80 | tile = bytearray(8 * 8 * format.bytes_per_pixel()) 81 | for y in range(8): 82 | for x in range(8): 83 | input_pixel_offset = ( 84 | (ty * 8 + y) * im.width + (tx * 8 + x) 85 | ) * bpp_input 86 | pixel = input[input_pixel_offset : input_pixel_offset + bpp_input][ 87 | ::-1 88 | ] 89 | match format: 90 | case TextureFormat.RGB8: 91 | pixel = pixel[1:] 92 | case TextureFormat.RGBA5551: 93 | pixel = ( 94 | (pixel[0] >> 7) 95 | | ((pixel[1] & 0xF8) >> 2) 96 | | ((pixel[2] & 0xF8) << 3) 97 | | ((pixel[3] & 0xF8) << 8) 98 | ).to_bytes(2, "little") 99 | case TextureFormat.RGB565: 100 | pixel = ( 101 | (pixel[1] >> 3) 102 | | ((pixel[2] & 0xFC) << 3) 103 | | ((pixel[3] & 0xF8) << 8) 104 | ).to_bytes(2, "little") 105 | case TextureFormat.RGBA4: 106 | pixel = bytes( 107 | [ 108 | (pixel[0] >> 4) | (pixel[1] & 0xF0), 109 | (pixel[2] >> 4) | (pixel[3] & 0xF0), 110 | ] 111 | ) 112 | case _: 113 | raise RuntimeError( 114 | f"Unsupported pixel format {format.name}" 115 | ) 116 | output_pixel_offset = swizzle_map[y * 8 + x] * len(pixel) 117 | tile[output_pixel_offset : output_pixel_offset + len(pixel)] = pixel 118 | output += tile 119 | return output 120 | 121 | 122 | def to_txob( 123 | im: Image, format: TextureFormat = TextureFormat.RGBA4, mipmaps=1 124 | ) -> ImageTexture: 125 | txob = ImageTexture() 126 | txob.width = txob.pixel_based_image.width = im.width 127 | txob.height = txob.pixel_based_image.height = im.height 128 | im = im.convert("RGBA") 129 | txob.hw_format = format 130 | txob.pixel_based_image.data = swizzle(im, format) 131 | txob.mipmap_level_count = 1 132 | return txob 133 | -------------------------------------------------------------------------------- /cgfx/txob.py: -------------------------------------------------------------------------------- 1 | from .shared import StandardObject, Signature, Reference 2 | from .dict import DictInfo 3 | from struct import Struct 4 | from enum import IntEnum 5 | 6 | 7 | class TextureFormat(IntEnum): 8 | RGBA8 = 0 # Broken on 3DS home menu - do not use 9 | RGB8 = 1 10 | RGBA5551 = 2 11 | RGB565 = 3 12 | RGBA4 = 4 13 | LA8 = 5 14 | HILO8 = 6 15 | L8 = 7 16 | A8 = 8 17 | LA4 = 9 18 | L4 = 10 19 | A4 = 11 20 | ETC1 = 12 21 | ETC1A4 = 13 22 | 23 | def bytes_per_pixel(self): 24 | match self: 25 | case self.RGBA8: 26 | return 4 27 | case self.RGB8: 28 | return 3 29 | case f if f >= self.RGBA5551 and f <= self.HILO8: 30 | return 2 31 | case self.L8 | self.A8 | self.LA4: 32 | return 1 33 | case self.L4 | self.A4: 34 | return 0.5 35 | # TODO ETC1 / ETC1A4 36 | 37 | 38 | class TXOB(StandardObject): 39 | struct = Struct("i4siiii") 40 | type: int 41 | signature = Signature("TXOB") 42 | revision = 0x5000000 43 | name = "" 44 | user_data: DictInfo 45 | 46 | def __init__(self): 47 | self.user_data = DictInfo() 48 | 49 | def values(self): 50 | return (self.type, self.signature, self.revision, self.name, self.user_data) 51 | 52 | 53 | class ReferenceTexture(TXOB): 54 | struct = Struct(TXOB.struct.format + "ii") 55 | type = 0x20000004 56 | txob: TXOB 57 | 58 | def __init__(self, txob: TXOB): 59 | super().__init__() 60 | self.txob = txob 61 | 62 | def values(self): 63 | return (*super().values(), self.txob.name, Reference(self.txob)) 64 | 65 | 66 | class PixelBasedTexture(TXOB): 67 | # padding is written to at runtime 68 | struct = Struct(TXOB.struct.format + "iiiiixxxxii") 69 | height = 0 70 | width = 0 71 | gl_format = 0 # unused 72 | gl_type = 0 # unused 73 | mipmap_level_count = 0 74 | location_flag = 0 75 | hw_format = TextureFormat.RGBA4 76 | 77 | def values(self): 78 | return ( 79 | *super().values(), 80 | self.height, 81 | self.width, 82 | self.gl_format, 83 | self.gl_type, 84 | self.mipmap_level_count, 85 | self.location_flag, 86 | self.hw_format, 87 | ) 88 | 89 | 90 | class PixelBasedImage(StandardObject): 91 | struct = Struct("iiiiiiii") 92 | height = 0 93 | width = 0 94 | data = b"" 95 | dynamic_allocator = 0 96 | bits_per_pixel = 0 # unused 97 | location_address = 0 98 | memory_address = 0 99 | 100 | def values(self): 101 | return ( 102 | self.height, 103 | self.width, 104 | self.data, 105 | self.dynamic_allocator, 106 | self.bits_per_pixel, 107 | self.location_address, 108 | self.memory_address, 109 | ) 110 | 111 | 112 | class ImageTexture(PixelBasedTexture): 113 | struct = Struct(PixelBasedTexture.struct.format + "i") 114 | type = 0x20000011 115 | pixel_based_image: PixelBasedImage 116 | 117 | def __init__(self): 118 | super().__init__() 119 | self.pixel_based_image = PixelBasedImage() 120 | 121 | def values(self): 122 | return (*super().values(), self.pixel_based_image) 123 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from cgfx.cgfx import CGFX 4 | from cgfx.cmdl import CMDL, CMDLWithSkeleton 5 | from cgfx.shared import StringTable, Vector3, Vector4, Matrix 6 | from cgfx.dict import DictInfo 7 | from cgfx.txob import ImageTexture, PixelBasedImage, ReferenceTexture 8 | from cgfx.sobj import ( 9 | SOBJMesh, 10 | SOBJShape, 11 | SOBJSkeleton, 12 | Bone, 13 | BillboardMode, 14 | BoneFlag, 15 | SkeletonFlag, 16 | ) 17 | from cgfx.primitives import ( 18 | Primitive, 19 | PrimitiveSet, 20 | InterleavedVertexStream, 21 | IndexStream, 22 | VertexStream, 23 | VertexAttributeUsage, 24 | VertexAttributeFlag, 25 | DataType, 26 | VertexParamAttribute, 27 | ) 28 | from cgfx.mtob import ( 29 | MTOB, 30 | BlendEquation, 31 | BlendFunction, 32 | ColorFloat, 33 | DepthFlag, 34 | TexInfo, 35 | PicaCommand, 36 | LinkedShader, 37 | LightingLookupTable, 38 | MTOBFlag, 39 | ConstantColorSource, 40 | BumpMode, 41 | FragmentLightingFlags, 42 | ) 43 | from cgfx.animation import ( 44 | GraphicsAnimationGroup, 45 | AnimationGroupMember, 46 | AnimationGroupMemberType, 47 | ) 48 | from cgfx.luts import LUTS, LutTable 49 | from cgfx.cenv import CENV, CENVLight, CENVLightSet 50 | from cgfx.cflt import CFLT 51 | from cgfx.canm import ( 52 | CANM, 53 | FloatAnimationCurve, 54 | FloatSegment, 55 | CANMBoneTransform, 56 | InterpolationType, 57 | QuantizationType, 58 | StepLinear64Key, 59 | Hermite128Key, 60 | CANMBoneRgbaColor, 61 | ) 62 | import itertools 63 | import struct 64 | from cgfx import swizzler 65 | from PIL import Image 66 | import gltflib 67 | from io import BytesIO 68 | import math 69 | import argparse 70 | import os.path 71 | 72 | 73 | def quat_to_euler(x: float, y: float, z: float, w: float) -> Vector3: 74 | t0 = 2 * (w * x + y * z) 75 | t1 = 1 - 2 * (x * x + y * y) 76 | ex = math.atan2(t0, t1) 77 | 78 | t2 = max(-1, min(1, 2 * (w * y - z * x))) 79 | ey = math.asin(t2) 80 | 81 | t3 = 2 * (w * z + x * y) 82 | t4 = 1 - 2 * (y * y + z * z) 83 | ez = math.atan2(t3, t4) 84 | 85 | return Vector3(ex, ey, ez) 86 | 87 | 88 | def make_material_animation(material_animation: GraphicsAnimationGroup, mtob: MTOB): 89 | member = AnimationGroupMember() 90 | material_animation.members.add( 91 | f'Materials["{mtob.name}"].MaterialColor.Emission', member 92 | ) 93 | member.object_type = AnimationGroupMemberType.MaterialColor 94 | member.path = f'Materials["{mtob.name}"].MaterialColor.Emission' 95 | member.member = mtob.name 96 | member.blend_operation_index = "MaterialColor" 97 | member.value_offset = 16 * 0 98 | member.value_size = 16 99 | member.field_type = 1 100 | member.parent_name = mtob.name 101 | 102 | member = AnimationGroupMember() 103 | material_animation.members.add( 104 | f'Materials["{mtob.name}"].MaterialColor.Ambient', member 105 | ) 106 | member.object_type = AnimationGroupMemberType.MaterialColor 107 | member.path = f'Materials["{mtob.name}"].MaterialColor.Ambient' 108 | member.member = mtob.name 109 | member.blend_operation_index = "MaterialColor" 110 | member.value_offset = 16 * 1 111 | member.value_size = 16 112 | member.field_type = 1 113 | member.value_index = 1 114 | member.parent_name = mtob.name 115 | 116 | member = AnimationGroupMember() 117 | material_animation.members.add( 118 | f'Materials["{mtob.name}"].MaterialColor.Diffuse', member 119 | ) 120 | member.object_type = AnimationGroupMemberType.MaterialColor 121 | member.path = f'Materials["{mtob.name}"].MaterialColor.Diffuse' 122 | member.member = mtob.name 123 | member.blend_operation_index = "MaterialColor" 124 | member.value_offset = 16 * 2 125 | member.value_size = 16 126 | member.field_type = 1 127 | member.value_index = 2 128 | member.parent_name = mtob.name 129 | 130 | member = AnimationGroupMember() 131 | material_animation.members.add( 132 | f'Materials["{mtob.name}"].MaterialColor.Specular0', member 133 | ) 134 | member.object_type = AnimationGroupMemberType.MaterialColor 135 | member.path = f'Materials["{mtob.name}"].MaterialColor.Specular0' 136 | member.member = mtob.name 137 | member.blend_operation_index = "MaterialColor" 138 | member.value_offset = 16 * 3 139 | member.value_size = 16 140 | member.field_type = 1 141 | member.value_index = 3 142 | member.parent_name = mtob.name 143 | 144 | member = AnimationGroupMember() 145 | material_animation.members.add( 146 | f'Materials["{mtob.name}"].MaterialColor.Specular1', member 147 | ) 148 | member.object_type = AnimationGroupMemberType.MaterialColor 149 | member.path = f'Materials["{mtob.name}"].MaterialColor.Specular1' 150 | member.member = mtob.name 151 | member.blend_operation_index = "MaterialColor" 152 | member.value_offset = 16 * 4 153 | member.value_size = 16 154 | member.field_type = 1 155 | member.value_index = 4 156 | member.parent_name = mtob.name 157 | 158 | member = AnimationGroupMember() 159 | material_animation.members.add( 160 | f'Materials["{mtob.name}"].MaterialColor.Constant0', member 161 | ) 162 | member.object_type = AnimationGroupMemberType.MaterialColor 163 | member.path = f'Materials["{mtob.name}"].MaterialColor.Constant0' 164 | member.member = mtob.name 165 | member.blend_operation_index = "MaterialColor" 166 | member.value_offset = 16 * 5 167 | member.value_size = 16 168 | member.field_type = 1 169 | member.value_index = 5 170 | member.parent_name = mtob.name 171 | 172 | member = AnimationGroupMember() 173 | material_animation.members.add( 174 | f'Materials["{mtob.name}"].MaterialColor.Constant1', member 175 | ) 176 | member.object_type = AnimationGroupMemberType.MaterialColor 177 | member.path = f'Materials["{mtob.name}"].MaterialColor.Constant1' 178 | member.member = mtob.name 179 | member.blend_operation_index = "MaterialColor" 180 | member.value_offset = 16 * 6 181 | member.value_size = 16 182 | member.field_type = 1 183 | member.value_index = 6 184 | member.parent_name = mtob.name 185 | 186 | member = AnimationGroupMember() 187 | material_animation.members.add( 188 | f'Materials["{mtob.name}"].MaterialColor.Constant2', member 189 | ) 190 | member.object_type = AnimationGroupMemberType.MaterialColor 191 | member.path = f'Materials["{mtob.name}"].MaterialColor.Constant2' 192 | member.member = mtob.name 193 | member.blend_operation_index = "MaterialColor" 194 | member.value_offset = 16 * 7 195 | member.value_size = 16 196 | member.field_type = 1 197 | member.value_index = 7 198 | member.parent_name = mtob.name 199 | 200 | member = AnimationGroupMember() 201 | material_animation.members.add( 202 | f'Materials["{mtob.name}"].MaterialColor.Constant3', member 203 | ) 204 | member.object_type = AnimationGroupMemberType.MaterialColor 205 | member.path = f'Materials["{mtob.name}"].MaterialColor.Constant3' 206 | member.member = mtob.name 207 | member.blend_operation_index = "MaterialColor" 208 | member.value_offset = 16 * 8 209 | member.value_size = 16 210 | member.field_type = 1 211 | member.value_index = 8 212 | member.parent_name = mtob.name 213 | 214 | member = AnimationGroupMember() 215 | material_animation.members.add( 216 | f'Materials["{mtob.name}"].MaterialColor.Constant4', member 217 | ) 218 | member.object_type = AnimationGroupMemberType.MaterialColor 219 | member.path = f'Materials["{mtob.name}"].MaterialColor.Constant4' 220 | member.member = mtob.name 221 | member.blend_operation_index = "MaterialColor" 222 | member.value_offset = 16 * 9 223 | member.value_size = 16 224 | member.field_type = 1 225 | member.value_index = 9 226 | member.parent_name = mtob.name 227 | 228 | member = AnimationGroupMember() 229 | material_animation.members.add( 230 | f'Materials["{mtob.name}"].MaterialColor.Constant5', member 231 | ) 232 | member.object_type = AnimationGroupMemberType.MaterialColor 233 | member.path = f'Materials["{mtob.name}"].MaterialColor.Constant5' 234 | member.member = mtob.name 235 | member.blend_operation_index = "MaterialColor" 236 | member.value_offset = 16 * 10 237 | member.value_size = 16 238 | member.field_type = 1 239 | member.value_index = 10 240 | member.parent_name = mtob.name 241 | 242 | for i in range(len(mtob.texture_mappers)): 243 | if mtob.texture_mappers[i] is None: 244 | continue 245 | 246 | # this one seems to crash somtimes when uncommenting it 247 | # member = AnimationGroupMember() 248 | # material_animation.members.add(f'Materials["{mtob.name}"].TextureMappers[{i}].Sampler.BorderColor', member) 249 | # member.object_type = AnimationGroupMemberType.TextureSampler 250 | # member.path = f'Materials["{mtob.name}"].TextureMappers[{i}].Sampler.BorderColor' 251 | # member.member = mtob.name 252 | # member.blend_operation_index = f'TextureMappers[{i}].Sampler' 253 | # member.value_offset = 12 254 | # member.value_size = 16 255 | # member.field_type = 2 256 | # member.field_index = i 257 | # member.value_index = 0 258 | # member.parent_name = mtob.name 259 | 260 | member = AnimationGroupMember() 261 | material_animation.members.add( 262 | f'Materials["{mtob.name}"].TextureMappers[{i}].Texture', member 263 | ) 264 | member.object_type = AnimationGroupMemberType.TextureMapper 265 | member.path = f'Materials["{mtob.name}"].TextureMappers[{i}].Texture' 266 | member.member = mtob.name 267 | member.blend_operation_index = f"TextureMappers[{i}]" 268 | member.value_offset = 8 269 | member.value_size = 4 270 | member.unknown = 1 271 | member.field_type = 3 272 | member.field_index = i 273 | member.value_index = 0 274 | member.parent_name = mtob.name 275 | 276 | member = AnimationGroupMember() 277 | material_animation.members.add( 278 | f'Materials["{mtob.name}"].TextureCoordinators[{i}].Scale', member 279 | ) 280 | member.object_type = AnimationGroupMemberType.TextureCoordinator 281 | member.path = ( 282 | f'Materials["{mtob.name}"].FragmentOperation.TextureCoordinators[{i}].Scale' 283 | ) 284 | member.member = mtob.name 285 | member.blend_operation_index = f"TextureCoordinators[{i}]" 286 | member.value_offset = 16 287 | member.value_size = 8 288 | member.unknown = 2 289 | member.field_type = 5 290 | member.field_index = i 291 | member.value_index = 0 292 | member.parent_name = mtob.name 293 | 294 | member = AnimationGroupMember() 295 | material_animation.members.add( 296 | f'Materials["{mtob.name}"].TextureCoordinators[{i}].Rotate', member 297 | ) 298 | member.object_type = AnimationGroupMemberType.TextureCoordinator 299 | member.path = f'Materials["{mtob.name}"].FragmentOperation.TextureCoordinators[{i}].Rotate' 300 | member.member = mtob.name 301 | member.blend_operation_index = f"TextureCoordinators[{i}]" 302 | member.value_offset = 24 303 | member.value_size = 4 304 | member.unknown = 3 305 | member.field_type = 5 306 | member.field_index = i 307 | member.value_index = 1 308 | member.parent_name = mtob.name 309 | 310 | member = AnimationGroupMember() 311 | material_animation.members.add( 312 | f'Materials["{mtob.name}"].TextureCoordinators[{i}].Translate', member 313 | ) 314 | member.object_type = AnimationGroupMemberType.TextureCoordinator 315 | member.path = f'Materials["{mtob.name}"].FragmentOperation.TextureCoordinators[{i}].Translate' 316 | member.member = mtob.name 317 | member.blend_operation_index = f"TextureCoordinators[{i}]" 318 | member.value_offset = 28 319 | member.value_size = 8 320 | member.unknown = 2 321 | member.field_type = 5 322 | member.field_index = i 323 | member.value_index = 2 324 | member.parent_name = mtob.name 325 | 326 | member = AnimationGroupMember() 327 | material_animation.members.add( 328 | f'Materials["{mtob.name}"].FragmentOperation.BlendOperation.BlendColor', member 329 | ) 330 | member.object_type = AnimationGroupMemberType.BlendOperation 331 | member.path = ( 332 | f'Materials["{mtob.name}"].FragmentOperation.BlendOperation.BlendColor' 333 | ) 334 | member.member = mtob.name 335 | member.blend_operation_index = "FragmentOperation.BlendOperation" 336 | member.value_offset = 4 337 | member.value_size = 16 338 | member.field_type = 4 339 | member.value_index = 0 340 | member.parent_name = mtob.name 341 | 342 | 343 | def gltf_get_bv_data(gltf: gltflib.GLTF, bv_id: int) -> bytes: 344 | bv = gltf.model.bufferViews[bv_id] 345 | buf = gltf.model.buffers[bv.buffer] 346 | if buf.uri is None: 347 | buf_res = gltf.get_glb_resource() 348 | else: 349 | buf_res = gltf.get_resource(buf.uri) 350 | return buf_res.data[bv.byteOffset : bv.byteOffset + bv.byteLength] 351 | 352 | 353 | def gltf_get_accessor_data_vertices( 354 | gltf: gltflib.GLTF, acc: int | gltflib.Accessor 355 | ) -> list[bytes]: 356 | if isinstance(acc, int): 357 | acc = gltf.model.accessors[acc] 358 | bv = gltf.model.bufferViews[acc.bufferView] 359 | bv_data = gltf_get_bv_data(gltf, acc.bufferView) 360 | start = acc.byteOffset or 0 361 | component_sizes = {5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4} 362 | type_sizes = { 363 | "SCALAR": 1, 364 | "VEC2": 2, 365 | "VEC3": 3, 366 | "VEC4": 4, 367 | "MAT2": 4, 368 | "MAT3": 9, 369 | "MAT4": 16, 370 | } 371 | element_size = component_sizes[acc.componentType] * type_sizes[acc.type] 372 | stride = bv.byteStride or element_size 373 | return list( 374 | bv_data[i : i + element_size] 375 | for i in range(start, start + acc.count * stride, stride) 376 | ) 377 | 378 | 379 | def gltf_get_accessor_data_raw(gltf: gltflib.GLTF, acc: gltflib.Accessor) -> bytes: 380 | return b"".join(gltf_get_accessor_data_vertices(gltf, acc)) 381 | 382 | 383 | def gltf_get_texture( 384 | cgfx: CGFX, gltf: gltflib.GLTF, image_id: int, normal: bool = False 385 | ) -> ImageTexture: 386 | image = gltf.model.images[image_id] 387 | tex_name = image.name or image.uri or f"image{image_id}" 388 | if normal: 389 | tex_name = f"NORM~{tex_name}" 390 | if tex_name in cgfx.data.textures: 391 | return cgfx.data.textures[tex_name] 392 | 393 | if image.uri is not None: 394 | image_data = gltf.get_resource(image.uri).data 395 | elif image.bufferView is not None: 396 | image_data = gltf_get_bv_data(gltf, image.bufferView) 397 | 398 | im: Image.Image = Image.open(BytesIO(image_data)) 399 | if im.width > 256: 400 | im = im.resize((256, im.height)) 401 | if im.height > 256: 402 | im = im.resize((im.width, 256)) 403 | if normal: 404 | im = im.convert("RGBA") 405 | for x in range(im.width): 406 | for y in range(im.height): 407 | px = im.getpixel((x, y)) 408 | im.putpixel((x, y), (255 - px[0], 255 - px[1], px[2])) 409 | txob = swizzler.to_txob(im.transpose(Image.Transpose.FLIP_TOP_BOTTOM)) 410 | txob.name = tex_name 411 | cgfx.data.textures.add(tex_name, txob) 412 | return txob 413 | 414 | 415 | def make_bones( 416 | gltf: gltflib.GLTF, node_ids: list[int], bone_dict: DictInfo[Bone] 417 | ) -> list[Bone]: 418 | bones = [] 419 | for node_id in node_ids: 420 | node = gltf.model.nodes[node_id] 421 | bone = Bone() 422 | bone.name = node.name or f"Node {node_id}" 423 | bone_dict.add(bone.name, bone) 424 | bone.joint_id = node_id 425 | bone.flags = ( 426 | BoneFlag.IsNeedRendering 427 | | BoneFlag.IsLocalMatrixCalculate 428 | | BoneFlag.IsWorldMatrixCalculate 429 | ) 430 | if bones: 431 | bone.previous_sibling = bones[-1] 432 | bones[-1].next_sibling = bone 433 | bones.append(bone) 434 | 435 | if node.matrix: 436 | bone.position = Vector3(node.matrix[12], node.matrix[13], node.matrix[14]) 437 | bone.scale = Vector3( 438 | math.hypot(*node.matrix[:3]), 439 | math.hypot(*node.matrix[4:7]), 440 | math.hypot(*node.matrix[8:11]), 441 | ) 442 | 443 | if abs(node.matrix[2]) != 1: 444 | rot_y = -math.asin(node.matrix[2] / bone.scale.x) 445 | rot_x = math.atan2( 446 | node.matrix[6] / math.cos(rot_y), node.matrix[10] / math.cos(rot_y) 447 | ) 448 | rot_z = math.atan2( 449 | node.matrix[1] / math.cos(rot_y), node.matrix[0] / math.cos(rot_y) 450 | ) 451 | else: 452 | rot_z = 0 453 | if node.matrix[2] == -1: 454 | rot_y = math.pi / 2 455 | rot_x = math.atan(node.matrix[4], node.matrix[8]) 456 | bone.rotation = Vector3(rot_x, rot_y, rot_z) 457 | 458 | else: 459 | if node.translation: 460 | bone.position = Vector3(*node.translation) 461 | if node.scale: 462 | bone.scale = Vector3(*node.scale) 463 | if node.rotation: 464 | bone.rotation = quat_to_euler(*node.rotation) 465 | 466 | if bone.position == Vector3(0, 0, 0): 467 | bone.flags |= BoneFlag.IsTranslateZero 468 | if bone.scale == Vector3(1, 1, 1): 469 | bone.flags |= BoneFlag.IsScaleOne 470 | if bone.scale.x == bone.scale.y == bone.scale.z: 471 | bone.flags |= BoneFlag.IsUniformScale 472 | if bone.rotation == Vector3(0, 0, 0): 473 | bone.flags |= BoneFlag.IsRotateZero 474 | 475 | all_flags = ( 476 | BoneFlag.IsTranslateZero | BoneFlag.IsRotateZero | BoneFlag.IsScaleOne 477 | ) 478 | if (bone.flags & all_flags) == all_flags: 479 | bone.flags |= BoneFlag.IsIdentity 480 | 481 | if node.children: 482 | child_bones = make_bones(gltf, node.children, bone_dict) 483 | if child_bones: 484 | bone.child = child_bones[0] 485 | for c in child_bones: 486 | c.parent = bone 487 | return bones 488 | 489 | 490 | def convert_gltf(gltf: gltflib.GLTF) -> CGFX: 491 | default_sampler = gltflib.Sampler( 492 | magFilter=9729, minFilter=9729, wrapS=10497, wrapT=10497 493 | ) 494 | default_material = gltflib.Material(name="glTF default material") 495 | 496 | cgfx = CGFX() 497 | 498 | luts = LUTS() 499 | luts.name = "LutSet" 500 | cgfx.data.lookup_tables.add("LutSet", luts) 501 | 502 | cmdl = CMDLWithSkeleton() 503 | cgfx.data.models.add("COMMON", cmdl) 504 | cmdl.name = "COMMON" 505 | 506 | cmdl.skeleton = SOBJSkeleton() 507 | node_ids = gltf.model.scenes[gltf.model.scene].nodes 508 | root_bone = Bone() 509 | root_bone.name = "Scene root" 510 | # ensure name is unique 511 | while any(n.name == root_bone.name for n in gltf.model.nodes): 512 | root_bone.name += "_" 513 | root_bone.joint_id = len(gltf.model.nodes) 514 | root_bone.flags = BoneFlag.IsIdentity | BoneFlag.IsNeedRendering 515 | cmdl.skeleton.root_bone = root_bone 516 | cmdl.skeleton.bones.add(root_bone.name, root_bone) 517 | initial_bones = make_bones(gltf, node_ids, cmdl.skeleton.bones) 518 | 519 | # sort bones based on how many transluscent materials they use 520 | def sort_key(node): 521 | node = gltf.model.nodes[node.content.joint_id] 522 | if node.mesh is None: 523 | return 0 524 | mesh = gltf.model.meshes[node.mesh] 525 | return sum( 526 | 1 527 | for p in mesh.primitives 528 | if p.material is not None 529 | and gltf.model.materials[p.material].alphaMode == "BLEND" 530 | ) / len(mesh.primitives) 531 | 532 | cmdl.skeleton.bones.dict.nodes[2:] = sorted( 533 | cmdl.skeleton.bones.dict.nodes[2:], key=sort_key 534 | ) 535 | 536 | # clean up joint ids 537 | node_to_bone = {} 538 | bone_to_node = {} 539 | 540 | for b in cmdl.skeleton.bones: 541 | bone = cmdl.skeleton.bones[b] 542 | if bone.joint_id >= 0: 543 | bone_to_node[cmdl.skeleton.bones.get_index(b)] = bone.joint_id 544 | node_to_bone[bone.joint_id] = cmdl.skeleton.bones.get_index(b) 545 | bone.joint_id = cmdl.skeleton.bones.get_index(b) 546 | 547 | for b in initial_bones: 548 | b.parent = root_bone 549 | root_bone.child = initial_bones[0] 550 | 551 | for b in cmdl.skeleton.bones: 552 | bone = cmdl.skeleton.bones[b] 553 | if bone.parent: 554 | bone.parent_id = bone.parent.joint_id 555 | 556 | mesh_nodes = [ 557 | bone_to_node[cmdl.skeleton.bones[bone_name].joint_id] 558 | for bone_name in cmdl.skeleton.bones 559 | if bone_to_node[cmdl.skeleton.bones[bone_name].joint_id] < len(gltf.model.nodes) 560 | and gltf.model.nodes[bone_to_node[cmdl.skeleton.bones[bone_name].joint_id]].mesh 561 | is not None 562 | ] 563 | 564 | for node_id in mesh_nodes: 565 | node = gltf.model.nodes[node_id] 566 | mesh = gltf.model.meshes[node.mesh] 567 | bone_id = node_to_bone[node_id] 568 | bone = cmdl.skeleton.bones[bone_id] 569 | 570 | skin = None 571 | if node.skin is not None: 572 | # bone.flags |= BoneFlag.HasSkinningMatrix 573 | skin = gltf.model.skins[node.skin] 574 | if skin.inverseBindMatrices is not None: 575 | ibms = gltf_get_accessor_data_vertices(gltf, skin.inverseBindMatrices) 576 | for i, joint_id in enumerate(skin.joints): 577 | ibm = [ 578 | struct.unpack("f", bytes(s))[0] 579 | for s in itertools.batched(ibms[i], 4) 580 | ] 581 | sub_bone = cmdl.skeleton.bones[node_to_bone[joint_id]] 582 | sub_bone.flags |= BoneFlag.HasSkinningMatrix 583 | sub_bone.inverse_base = Matrix( 584 | Vector4(*ibm[::4]), Vector4(*ibm[1::4]), Vector4(*ibm[2::4]) 585 | ) 586 | 587 | for material in ( 588 | ( 589 | gltf.model.materials[p.material] 590 | if p.material is not None 591 | else default_material 592 | ) 593 | for p in mesh.primitives 594 | ): 595 | if material.name not in cmdl.materials: 596 | mtob = MTOB() 597 | cmdl.materials.add(material.name, mtob) 598 | mtob.name = material.name 599 | 600 | match material.alphaMode: 601 | case "MASK": 602 | mtob.fragment_operations.blend_operation.src_color = ( 603 | BlendFunction.One 604 | ) 605 | mtob.fragment_operations.blend_operation.dst_color = ( 606 | BlendFunction.Zero 607 | ) 608 | mtob.fragment_operations.blend_operation.src_alpha = ( 609 | BlendFunction.One 610 | ) 611 | mtob.fragment_operations.blend_operation.dst_alpha = ( 612 | BlendFunction.Zero 613 | ) 614 | mtob.fragment_shader.alpha_test.enabled = True 615 | if material.alphaCutoff is not None: 616 | mtob.fragment_shader.alpha_test.cutoff = int( 617 | material.alphaCutoff * 255 618 | ) 619 | case "BLEND": 620 | mtob.fragment_operations.blend_operation.src_color = ( 621 | BlendFunction.SrcAlpha 622 | ) 623 | mtob.fragment_operations.blend_operation.dst_color = ( 624 | BlendFunction.InvSrcAlpha 625 | ) 626 | mtob.fragment_operations.blend_operation.src_alpha = ( 627 | BlendFunction.SrcAlpha 628 | ) 629 | mtob.fragment_operations.blend_operation.dst_alpha = ( 630 | BlendFunction.InvSrcAlpha 631 | ) 632 | mtob.fragment_operations.depth_operation.flags = ( 633 | DepthFlag.TestEnabled 634 | ) 635 | case "OPAQUE" | _: 636 | mtob.fragment_operations.blend_operation.src_color = ( 637 | BlendFunction.One 638 | ) 639 | mtob.fragment_operations.blend_operation.dst_color = ( 640 | BlendFunction.Zero 641 | ) 642 | mtob.fragment_operations.blend_operation.src_alpha = ( 643 | BlendFunction.One 644 | ) 645 | mtob.fragment_operations.blend_operation.dst_alpha = ( 646 | BlendFunction.Zero 647 | ) 648 | 649 | # multiply base texture with primary color 650 | mtob.fragment_shader.texture_combiners[0].src_rgb = 0x030 651 | mtob.fragment_shader.texture_combiners[0].src_alpha = 0x030 652 | mtob.fragment_shader.texture_combiners[0].combine_rgb = 1 653 | mtob.fragment_shader.texture_combiners[0].combine_alpha = 1 654 | # multiply with diffuse lighting 655 | mtob.fragment_shader.texture_combiners[1].src_rgb = 0x0F1 656 | mtob.fragment_shader.texture_combiners[1].combine_rgb = 1 657 | # add specular lighting 658 | mtob.fragment_shader.texture_combiners[2].src_rgb = 0xFE2 659 | mtob.fragment_shader.texture_combiners[2].combine_rgb = 8 660 | mtob.fragment_shader.texture_combiners[2].constant = ( 661 | ConstantColorSource.Constant0 662 | ) 663 | 664 | mtob.flags = MTOBFlag.FragmentLight 665 | pmr = material.pbrMetallicRoughness or gltflib.PBRMetallicRoughness() 666 | base_tex = pmr.baseColorTexture 667 | if pmr.baseColorFactor: 668 | mtob.material_color.diffuse = ColorFloat(*pmr.baseColorFactor) 669 | 670 | # specular 671 | mtob.fragment_shader.fragment_lighting.flags = ( 672 | FragmentLightingFlags.UseDistribution0 673 | ) 674 | mtob.fragment_shader.fragment_lighting_table.distribution_0_sampler = ( 675 | LightingLookupTable() 676 | ) 677 | mtob.fragment_shader.fragment_lighting_table.distribution_0_sampler.sampler.binary_path = ( 678 | "LutSet" 679 | ) 680 | mtob.fragment_shader.fragment_lighting_table.distribution_0_sampler.sampler.table_name = ( 681 | mtob.name 682 | ) 683 | roughness_factor = ( 684 | pmr.roughnessFactor if pmr.roughnessFactor is not None else 1 685 | ) 686 | mtob.material_color.constant[0] = ColorFloat( 687 | *([1 - 0.9 * roughness_factor] * 3), 1 688 | ) 689 | lut = LutTable.phong(4 * 200 / (1 + roughness_factor * 100)) 690 | luts.tables.add(mtob.name, lut) 691 | 692 | if base_tex: 693 | tex = gltf.model.textures[base_tex.index] 694 | sampler = ( 695 | gltf.model.samplers[tex.sampler] 696 | if tex.sampler is not None 697 | else default_sampler 698 | ) 699 | tex_info = TexInfo( 700 | ReferenceTexture( 701 | gltf_get_texture(cgfx, gltf, tex.source, False) 702 | ) 703 | ) 704 | tex_param = 0 705 | tex_param |= 1 * ((sampler.magFilter or 9729) & 1) 706 | tex_param |= 2 * ((sampler.minFilter or 9729) & 1) 707 | tex_param |= [33071, 0, 10497, 33648].index( 708 | (sampler.wrapS or 10497) 709 | ) << 12 710 | tex_param |= [33071, 0, 10497, 33648].index( 711 | (sampler.wrapT or 10497) 712 | ) << 8 713 | tex_info.commands[2].head |= tex_param 714 | mtob.texture_mappers[mtob.used_texture_coordinates_count] = tex_info 715 | mtob.texture_coordinators[ 716 | mtob.used_texture_coordinates_count 717 | ].source_coordinate = (base_tex.texCoord or 0) 718 | mtob.used_texture_coordinates_count += 1 719 | else: 720 | mtob.fragment_shader.texture_combiners[0].combine_rgb = 0 721 | mtob.fragment_shader.texture_combiners[0].combine_alpha = 0 722 | if material.normalTexture: 723 | tex = gltf.model.textures[material.normalTexture.index] 724 | 725 | sampler = ( 726 | gltf.model.samplers[tex.sampler] 727 | if tex.sampler is not None 728 | else default_sampler 729 | ) 730 | tex_info = TexInfo( 731 | ReferenceTexture(gltf_get_texture(cgfx, gltf, tex.source, True)) 732 | ) 733 | tex_info.commands[0].head += 8 * mtob.used_texture_coordinates_count 734 | tex_info.commands[ 735 | 1 736 | ].head += 8 * mtob.used_texture_coordinates_count + 8 * bool( 737 | mtob.used_texture_coordinates_count 738 | ) 739 | tex_param = 0 740 | tex_param |= 1 * (sampler.magFilter & 1) 741 | tex_param |= 2 * (sampler.minFilter & 1) 742 | tex_param |= [33071, 0, 10497, 33648].index(sampler.wrapS) << 12 743 | tex_param |= [33071, 0, 10497, 33648].index(sampler.wrapT) << 8 744 | tex_info.commands[2].head |= tex_param 745 | mtob.texture_mappers[mtob.used_texture_coordinates_count] = tex_info 746 | mtob.texture_coordinators[ 747 | mtob.used_texture_coordinates_count 748 | ].source_coordinate = (material.normalTexture.texCoord or 0) 749 | mtob.fragment_shader.fragment_lighting.bump_texture = ( 750 | mtob.used_texture_coordinates_count 751 | ) 752 | mtob.used_texture_coordinates_count += 1 753 | mtob.fragment_shader.fragment_lighting.bump_mode = BumpMode.AsBump 754 | mtob.fragment_shader.fragment_lighting.is_bump_renormalize = True 755 | 756 | for i, p in enumerate(mesh.primitives): 757 | if p.mode is not None and p.mode != 4: 758 | raise RuntimeError( 759 | "only triangle list primitives are currently supported" 760 | ) 761 | sobj_mesh = SOBJMesh(cmdl) 762 | cmdl.meshes.add(sobj_mesh) 763 | sobj_mesh.name = ( 764 | (mesh.name or cmdl.skeleton.bones[node_to_bone[node_id]]) 765 | + "_" 766 | + mtob.name 767 | ) 768 | sobj_mesh.mesh_node_visibility_index = 65535 769 | sobj_mesh.material_index = cmdl.materials.get_index( 770 | gltf.model.materials[p.material].name 771 | if p.material is not None 772 | else default_material.name 773 | ) 774 | sobj_mesh.shape_index = len(cmdl.shapes) 775 | shape = SOBJShape() 776 | cmdl.shapes.add(shape) 777 | shape.name = sobj_mesh.name 778 | primitive_set = PrimitiveSet() 779 | shape.primitive_sets.add(primitive_set) 780 | primitive_set.related_bones.add(bone_id) 781 | if node.skin is not None: 782 | primitive_set.skinning_mode = 2 783 | for joint_id in skin.joints: 784 | primitive_set.related_bones.add(node_to_bone[joint_id]) 785 | primitive = Primitive() 786 | primitive_set.primitives.add(primitive) 787 | if p.indices is not None: 788 | indices = gltf.model.accessors[p.indices] 789 | index_stream = IndexStream() 790 | index_stream.data_type = indices.componentType 791 | index_stream.face_data = gltf_get_accessor_data_raw(gltf, indices) 792 | if material.doubleSided: 793 | # duplicate all vertices backwards 794 | rev = reversed(gltf_get_accessor_data_vertices(gltf, indices)) 795 | acc_id = list( 796 | v for k, v in p.attributes.__dict__.items() if v is not None 797 | )[0] 798 | count = len( 799 | gltf_get_accessor_data_vertices( 800 | gltf, gltf.model.accessors[acc_id] 801 | ) 802 | ) 803 | index_stream.face_data += b"".join( 804 | (int.from_bytes(b, "little") + count).to_bytes(len(b), "little") 805 | for b in rev 806 | ) 807 | primitive.index_streams.add(index_stream) 808 | primitive.buffer_objects.add(0) 809 | for ty, acc_id in p.attributes.__dict__.items(): 810 | if acc_id is None: 811 | continue 812 | acc = gltf.model.accessors[acc_id] 813 | vs = VertexStream() 814 | vs.usage = { 815 | "POSITION": VertexAttributeUsage.Position, 816 | "NORMAL": VertexAttributeUsage.Normal, 817 | "TANGENT": VertexAttributeUsage.Tangent, 818 | "TEXCOORD_0": VertexAttributeUsage.TextureCoordinate0, 819 | "TEXCOORD_1": VertexAttributeUsage.TextureCoordinate1, 820 | "COLOR_0": VertexAttributeUsage.Color, 821 | "JOINTS_0": VertexAttributeUsage.BoneIndex, 822 | "WEIGHTS_0": VertexAttributeUsage.BoneWeight, 823 | }.get(ty) 824 | if vs.usage is None: 825 | continue 826 | shape.vertex_attributes.add(vs) 827 | vs.components_count = {"SCALAR": 1, "VEC2": 2, "VEC3": 3, "VEC4": 4}[ 828 | acc.type 829 | ] 830 | vs.format_type = acc.componentType 831 | if acc.componentType == 5123: # unsigned short doesn't work 832 | vs.format_type = 5122 # turn it into signed short 833 | vs.vertex_stream_data = gltf_get_accessor_data_raw(gltf, acc) 834 | if ty == "JOINTS_0": 835 | vs.vertex_stream_data = b"".join( 836 | (int.from_bytes(bytes(b), "little") + 1).to_bytes( 837 | len(b), "little" 838 | ) 839 | for b in itertools.batched( 840 | vs.vertex_stream_data, 841 | {5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4}[ 842 | acc.componentType 843 | ], 844 | ) 845 | ) 846 | if material.doubleSided: 847 | # duplicate all vertices but with the normals reversed 848 | verts = vs.vertex_stream_data 849 | if ty != "NORMAL": 850 | vs.vertex_stream_data += verts 851 | else: 852 | vs.vertex_stream_data += bytes( 853 | [v ^ (0x80 * (i % 4 == 3)) for i, v in enumerate(verts)] 854 | ) 855 | 856 | visibility_animation = GraphicsAnimationGroup() 857 | cmdl.animation_group_descriptions.add("VisibilityAnimation", visibility_animation) 858 | visibility_animation.name = "VisibilityAnimation" 859 | visibility_animation.member_type = 3 860 | visibility_animation.blend_operations.add(0) 861 | 862 | is_visible = AnimationGroupMember() 863 | visibility_animation.members.add("IsVisible", is_visible) 864 | is_visible.object_type = AnimationGroupMemberType.Model 865 | is_visible.path = "IsVisible" 866 | is_visible.value_offset = 212 867 | is_visible.value_size = 1 868 | is_visible.field_type = 6 869 | is_visible.value_index = 1 870 | 871 | for i in range(len(cmdl.meshes)): 872 | mesh_is_visible = AnimationGroupMember() 873 | visibility_animation.members.add(f"Meshes[{i}].IsVisible", mesh_is_visible) 874 | mesh_is_visible.object_type = AnimationGroupMemberType.Mesh 875 | mesh_is_visible.path = f"Meshes[{i}].IsVisible" 876 | mesh_is_visible.member = str(i) 877 | mesh_is_visible.value_offset = 36 878 | mesh_is_visible.value_size = 1 879 | mesh_is_visible.field_type = 7 880 | mesh_is_visible.parent_index = i 881 | 882 | skeletal_animation = GraphicsAnimationGroup() 883 | cmdl.animation_group_descriptions.add("SkeletalAnimation", skeletal_animation) 884 | skeletal_animation.flags = 1 885 | skeletal_animation.name = "SkeletalAnimation" 886 | skeletal_animation.member_type = 1 887 | skeletal_animation.blend_operations.add(8) 888 | skeletal_animation.evalution_timing = 1 889 | 890 | for b in cmdl.skeleton.bones: 891 | member = AnimationGroupMember() 892 | skeletal_animation.members.add(b, member) 893 | member.object_type = AnimationGroupMemberType.Bone 894 | member.path = b 895 | member.member = b 896 | member.field_type = 0 897 | member.parent_name = b 898 | 899 | material_animation = GraphicsAnimationGroup() 900 | cmdl.animation_group_descriptions.add("MaterialAnimation", material_animation) 901 | material_animation.name = "MaterialAnimation" 902 | material_animation.member_type = 2 903 | material_animation.evalution_timing = 1 904 | material_animation.blend_operations.add(3) 905 | material_animation.blend_operations.add(7) 906 | material_animation.blend_operations.add(5) 907 | material_animation.blend_operations.add(2) 908 | 909 | for m in cmdl.materials: 910 | make_material_animation(material_animation, cmdl.materials[m]) 911 | 912 | cenv = CENV() 913 | cenv.name = "Scene" 914 | cgfx.data.scenes.add(cenv.name, cenv) 915 | light_set = CENVLightSet() 916 | cenv.light_sets.add(light_set) 917 | cenv_light = CENVLight() 918 | light_set.lights.add(cenv_light) 919 | cenv_light.name = "TheLight" 920 | 921 | cflt = CFLT() 922 | cflt.name = "TheLight" 923 | cgfx.data.lights.add(cflt.name, cflt) 924 | 925 | if gltf.model.animations: 926 | skeletal_animation = CANM() 927 | skeletal_animation.name = "COMMON" 928 | cgfx.data.skeletal_animations.add(skeletal_animation.name, skeletal_animation) 929 | skeletal_animation.target_animation_group_name = "SkeletalAnimation" 930 | skeletal_animation.frame_size = 60 * max( 931 | struct.unpack("f", t)[0] 932 | for a in gltf.model.animations or [] 933 | for c in a.channels 934 | for t in gltf_get_accessor_data_vertices(gltf, a.samplers[c.sampler].input) 935 | ) 936 | for anim in gltf.model.animations or []: 937 | for node_id, channels in itertools.groupby( 938 | sorted( 939 | (c for c in anim.channels if c.target.node is not None), 940 | key=lambda c: c.target.node, 941 | ), 942 | key=lambda c: c.target.node, 943 | ): 944 | bone = CANMBoneTransform() 945 | bone.bone_path = cmdl.skeleton.bones[node_to_bone[node_id]].name 946 | skeletal_animation.member_animations_data.add(bone.bone_path, bone) 947 | for c in channels: 948 | sampler = anim.samplers[c.sampler] 949 | interpolation = ( 950 | InterpolationType( 951 | ("STEP", "LINEAR", "CUBICSPLINE").index( 952 | sampler.interpolation 953 | ) 954 | ) 955 | if sampler.interpolation 956 | else InterpolationType.Linear 957 | ) 958 | inputs = tuple( 959 | struct.unpack("f", t)[0] 960 | for t in gltf_get_accessor_data_vertices(gltf, sampler.input) 961 | ) 962 | outputs = tuple( 963 | tuple( 964 | struct.unpack("f", bytes(c))[0] 965 | for c in itertools.batched(v, 4) 966 | ) 967 | for v in gltf_get_accessor_data_vertices(gltf, sampler.output) 968 | ) 969 | match c.target.path: 970 | case "weights": 971 | print("WARNING: morph target animations are not supported") 972 | continue 973 | case "translation": 974 | bone.pos_x = FloatAnimationCurve() 975 | bone.pos_y = FloatAnimationCurve() 976 | bone.pos_z = FloatAnimationCurve() 977 | bone.pos_x.segments.append(FloatSegment()) 978 | bone.pos_y.segments.append(FloatSegment()) 979 | bone.pos_z.segments.append(FloatSegment()) 980 | bone.pos_x.start_frame = bone.pos_y.start_frame = ( 981 | bone.pos_z.end_frame 982 | ) = bone.pos_x.segments[ 983 | 0 984 | ].start_frame = bone.pos_y.segments[ 985 | 0 986 | ].start_frame = bone.pos_z.segments[ 987 | 0 988 | ].start_frame = ( 989 | min(inputs) * 60 990 | ) 991 | bone.pos_x.end_frame = bone.pos_y.end_frame = ( 992 | bone.pos_z.end_frame 993 | ) = bone.pos_x.segments[0].end_frame = bone.pos_y.segments[ 994 | 0 995 | ].end_frame = bone.pos_z.segments[ 996 | 0 997 | ].end_frame = ( 998 | max(inputs) * 60 999 | ) 1000 | bone.pos_x.segments[0].interpolation = bone.pos_y.segments[ 1001 | 0 1002 | ].interpolation = bone.pos_z.segments[0].interpolation = ( 1003 | interpolation 1004 | ) 1005 | bone.pos_x.segments[0].quantization = bone.pos_y.segments[ 1006 | 0 1007 | ].quantization = bone.pos_z.segments[0].quantization = ( 1008 | QuantizationType.StepLinear64 1009 | if interpolation != InterpolationType.CubicSpline 1010 | else QuantizationType.Hermite128 1011 | ) 1012 | if interpolation != InterpolationType.CubicSpline: 1013 | for time, (x, y, z) in zip(inputs, outputs): 1014 | bone.pos_x.segments[0].keys.append( 1015 | StepLinear64Key(time * 60, x) 1016 | ) 1017 | bone.pos_y.segments[0].keys.append( 1018 | StepLinear64Key(time * 60, y) 1019 | ) 1020 | bone.pos_z.segments[0].keys.append( 1021 | StepLinear64Key(time * 60, z) 1022 | ) 1023 | else: 1024 | for time, ( 1025 | (xa, ya, za), 1026 | (xv, yv, zv), 1027 | (xb, yb, zb), 1028 | ) in zip(inputs, itertools.batched(outputs, 3)): 1029 | bone.pos_x.segments[0].keys.append( 1030 | Hermite128Key(time * 60, xv, xa, xb) 1031 | ) 1032 | bone.pos_y.segments[0].keys.append( 1033 | Hermite128Key(time * 60, yv, ya, yb) 1034 | ) 1035 | bone.pos_z.segments[0].keys.append( 1036 | Hermite128Key(time * 60, zv, za, zb) 1037 | ) 1038 | case "scale": 1039 | bone.scale_x = FloatAnimationCurve() 1040 | bone.scale_y = FloatAnimationCurve() 1041 | bone.scale_z = FloatAnimationCurve() 1042 | bone.scale_x.segments.append(FloatSegment()) 1043 | bone.scale_y.segments.append(FloatSegment()) 1044 | bone.scale_z.segments.append(FloatSegment()) 1045 | bone.scale_x.start_frame = bone.scale_y.start_frame = ( 1046 | bone.scale_z.end_frame 1047 | ) = bone.scale_x.segments[ 1048 | 0 1049 | ].start_frame = bone.scale_y.segments[ 1050 | 0 1051 | ].start_frame = bone.scale_z.segments[ 1052 | 0 1053 | ].start_frame = ( 1054 | min(inputs) * 60 1055 | ) 1056 | bone.scale_x.end_frame = bone.scale_y.end_frame = ( 1057 | bone.scale_z.end_frame 1058 | ) = bone.scale_x.segments[ 1059 | 0 1060 | ].end_frame = bone.scale_y.segments[ 1061 | 0 1062 | ].end_frame = bone.scale_z.segments[ 1063 | 0 1064 | ].end_frame = ( 1065 | max(inputs) * 60 1066 | ) 1067 | bone.scale_x.segments[0].interpolation = ( 1068 | bone.scale_y.segments[0].interpolation 1069 | ) = bone.scale_z.segments[0].interpolation = interpolation 1070 | bone.scale_x.segments[0].quantization = ( 1071 | bone.scale_y.segments[0].quantization 1072 | ) = bone.scale_z.segments[0].quantization = ( 1073 | QuantizationType.StepLinear64 1074 | if interpolation != InterpolationType.CubicSpline 1075 | else QuantizationType.Hermite128 1076 | ) 1077 | if interpolation != InterpolationType.CubicSpline: 1078 | for time, (x, y, z) in zip(inputs, outputs): 1079 | bone.scale_x.segments[0].keys.append( 1080 | StepLinear64Key(time * 60, x) 1081 | ) 1082 | bone.scale_y.segments[0].keys.append( 1083 | StepLinear64Key(time * 60, y) 1084 | ) 1085 | bone.scale_z.segments[0].keys.append( 1086 | StepLinear64Key(time * 60, z) 1087 | ) 1088 | else: 1089 | for time, ( 1090 | (xa, ya, za), 1091 | (xv, yv, zv), 1092 | (xb, yb, zb), 1093 | ) in zip(inputs, itertools.batched(outputs, 3)): 1094 | bone.scale_x.segments[0].keys.append( 1095 | Hermite128Key(time * 60, xv, xa, xb) 1096 | ) 1097 | bone.scale_y.segments[0].keys.append( 1098 | Hermite128Key(time * 60, yv, ya, yb) 1099 | ) 1100 | bone.scale_z.segments[0].keys.append( 1101 | Hermite128Key(time * 60, zv, za, zb) 1102 | ) 1103 | case "rotation": 1104 | if interpolation == InterpolationType.CubicSpline: 1105 | print( 1106 | "WARNING: spline rotation animations are currently not supported" 1107 | ) 1108 | continue 1109 | bone.rot_x = FloatAnimationCurve() 1110 | bone.rot_y = FloatAnimationCurve() 1111 | bone.rot_z = FloatAnimationCurve() 1112 | bone.rot_x.segments.append(FloatSegment()) 1113 | bone.rot_y.segments.append(FloatSegment()) 1114 | bone.rot_z.segments.append(FloatSegment()) 1115 | bone.rot_x.start_frame = bone.rot_y.start_frame = ( 1116 | bone.rot_z.end_frame 1117 | ) = bone.rot_x.segments[ 1118 | 0 1119 | ].start_frame = bone.rot_y.segments[ 1120 | 0 1121 | ].start_frame = bone.rot_z.segments[ 1122 | 0 1123 | ].start_frame = ( 1124 | min(inputs) * 60 1125 | ) 1126 | bone.rot_x.end_frame = bone.rot_y.end_frame = ( 1127 | bone.rot_z.end_frame 1128 | ) = bone.rot_x.segments[0].end_frame = bone.rot_y.segments[ 1129 | 0 1130 | ].end_frame = bone.rot_z.segments[ 1131 | 0 1132 | ].end_frame = ( 1133 | max(inputs) * 60 1134 | ) 1135 | bone.rot_x.segments[0].interpolation = bone.rot_y.segments[ 1136 | 0 1137 | ].interpolation = bone.rot_z.segments[0].interpolation = ( 1138 | interpolation 1139 | ) 1140 | bone.rot_x.segments[0].quantization = bone.rot_y.segments[ 1141 | 0 1142 | ].quantization = bone.rot_z.segments[0].quantization = ( 1143 | QuantizationType.StepLinear64 1144 | if interpolation != InterpolationType.CubicSpline 1145 | else QuantizationType.Hermite128 1146 | ) 1147 | for time, (x, y, z, w) in zip(inputs, outputs): 1148 | euler = quat_to_euler(x, y, z, w) 1149 | # normalize rotations to smallest distance 1150 | if ( 1151 | interpolation == InterpolationType.Linear 1152 | and len(bone.rot_x.segments[0].keys) > 0 1153 | ): 1154 | while ( 1155 | euler.x 1156 | < bone.rot_x.segments[0].keys[-1].value 1157 | - math.pi 1158 | ): 1159 | euler.x += 2 * math.pi 1160 | while ( 1161 | euler.x 1162 | > bone.rot_x.segments[0].keys[-1].value 1163 | + math.pi 1164 | ): 1165 | euler.x -= 2 * math.pi 1166 | while ( 1167 | euler.y 1168 | < bone.rot_y.segments[0].keys[-1].value 1169 | - math.pi 1170 | ): 1171 | euler.y += 2 * math.pi 1172 | while ( 1173 | euler.y 1174 | > bone.rot_y.segments[0].keys[-1].value 1175 | + math.pi 1176 | ): 1177 | euler.y -= 2 * math.pi 1178 | while ( 1179 | euler.z 1180 | < bone.rot_z.segments[0].keys[-1].value 1181 | - math.pi 1182 | ): 1183 | euler.z += 2 * math.pi 1184 | while ( 1185 | euler.z 1186 | > bone.rot_z.segments[0].keys[-1].value 1187 | + math.pi 1188 | ): 1189 | euler.z -= 2 * math.pi 1190 | bone.rot_x.segments[0].keys.append( 1191 | StepLinear64Key(time * 60, euler.x) 1192 | ) 1193 | bone.rot_y.segments[0].keys.append( 1194 | StepLinear64Key(time * 60, euler.y) 1195 | ) 1196 | bone.rot_z.segments[0].keys.append( 1197 | StepLinear64Key(time * 60, euler.z) 1198 | ) 1199 | if ( 1200 | gltf.model.extensionsUsed 1201 | and "KHR_animation_pointer" in gltf.model.extensionsUsed 1202 | ): 1203 | material_animation = CANM() 1204 | material_animation.name = "COMMON" 1205 | cgfx.data.material_animations.add( 1206 | material_animation.name, material_animation 1207 | ) 1208 | material_animation.target_animation_group_name = "MaterialAnimation" 1209 | material_animation.frame_size = 60 * max( 1210 | struct.unpack("f", t)[0] 1211 | for a in gltf.model.animations or [] 1212 | for c in a.channels 1213 | for t in gltf_get_accessor_data_vertices( 1214 | gltf, a.samplers[c.sampler].input 1215 | ) 1216 | ) 1217 | for anim in gltf.model.animations or []: 1218 | for base, channels in itertools.groupby( 1219 | sorted( 1220 | ( 1221 | c 1222 | for c in anim.channels 1223 | if c.target.path == "pointer" 1224 | and "KHR_animation_pointer" in c.target.extensions 1225 | ), 1226 | key=lambda c: c.target.extensions["KHR_animation_pointer"][ 1227 | "pointer" 1228 | ], 1229 | ), 1230 | key=lambda c: c.target.extensions["KHR_animation_pointer"][ 1231 | "pointer" 1232 | ].split("/")[:3], 1233 | ): 1234 | if base[1] != "materials": 1235 | print( 1236 | "WARNING: pointer animations currently only supported for materials" 1237 | ) 1238 | continue 1239 | material = gltf.model.materials[int(base[2])] 1240 | if material.name not in cmdl.materials: 1241 | continue 1242 | for c in channels: 1243 | sampler = anim.samplers[c.sampler] 1244 | interpolation = ( 1245 | InterpolationType( 1246 | ("STEP", "LINEAR", "CUBICSPLINE").index( 1247 | sampler.interpolation 1248 | ) 1249 | ) 1250 | if sampler.interpolation 1251 | else InterpolationType.Linear 1252 | ) 1253 | inputs = tuple( 1254 | struct.unpack("f", t)[0] 1255 | for t in gltf_get_accessor_data_vertices( 1256 | gltf, sampler.input 1257 | ) 1258 | ) 1259 | outputs = tuple( 1260 | tuple( 1261 | struct.unpack("f", bytes(c))[0] 1262 | for c in itertools.batched(v, 4) 1263 | ) 1264 | for v in gltf_get_accessor_data_vertices( 1265 | gltf, sampler.output 1266 | ) 1267 | ) 1268 | 1269 | bone = CANMBoneRgbaColor() 1270 | path = c.target.extensions["KHR_animation_pointer"][ 1271 | "pointer" 1272 | ].split("/") 1273 | if path[3:] == ["pbrMetallicRoughness", "baseColorFactor"]: 1274 | bone.bone_path = ( 1275 | f'Materials["{material.name}"].MaterialColor.Diffuse' 1276 | ) 1277 | else: 1278 | print( 1279 | "WARNING: pointer " 1280 | + "/".join(path) 1281 | + " is currently not supported" 1282 | ) 1283 | continue 1284 | material_animation.member_animations_data.add( 1285 | bone.bone_path, bone 1286 | ) 1287 | bone.red = FloatAnimationCurve() 1288 | bone.green = FloatAnimationCurve() 1289 | bone.blue = FloatAnimationCurve() 1290 | bone.alpha = FloatAnimationCurve() 1291 | bone.red.segments.append(FloatSegment()) 1292 | bone.green.segments.append(FloatSegment()) 1293 | bone.blue.segments.append(FloatSegment()) 1294 | bone.alpha.segments.append(FloatSegment()) 1295 | bone.red.start_frame = bone.green.start_frame = ( 1296 | bone.blue.start_frame 1297 | ) = bone.alpha.end_frame = bone.red.segments[ 1298 | 0 1299 | ].start_frame = bone.green.segments[ 1300 | 0 1301 | ].start_frame = bone.blue.segments[ 1302 | 0 1303 | ].start_frame = bone.alpha.segments[ 1304 | 0 1305 | ].start_frame = ( 1306 | min(inputs) * 60 1307 | ) 1308 | bone.red.end_frame = bone.green.end_frame = ( 1309 | bone.blue.end_frame 1310 | ) = bone.alpha.end_frame = bone.red.segments[ 1311 | 0 1312 | ].end_frame = bone.green.segments[ 1313 | 0 1314 | ].end_frame = bone.blue.segments[ 1315 | 0 1316 | ].end_frame = bone.alpha.segments[ 1317 | 0 1318 | ].end_frame = ( 1319 | max(inputs) * 60 1320 | ) 1321 | bone.red.segments[0].interpolation = bone.green.segments[ 1322 | 0 1323 | ].interpolation = bone.blue.segments[ 1324 | 0 1325 | ].interpolation = bone.alpha.segments[ 1326 | 0 1327 | ].interpolation = interpolation 1328 | bone.red.segments[0].quantization = bone.green.segments[ 1329 | 0 1330 | ].quantization = bone.blue.segments[ 1331 | 0 1332 | ].quantization = bone.alpha.segments[ 1333 | 0 1334 | ].quantization = ( 1335 | QuantizationType.StepLinear64 1336 | if interpolation != InterpolationType.CubicSpline 1337 | else QuantizationType.Hermite128 1338 | ) 1339 | 1340 | if interpolation != InterpolationType.CubicSpline: 1341 | for time, (r, g, b, a) in zip(inputs, outputs): 1342 | bone.red.segments[0].keys.append( 1343 | StepLinear64Key(time * 60, r) 1344 | ) 1345 | bone.green.segments[0].keys.append( 1346 | StepLinear64Key(time * 60, g) 1347 | ) 1348 | bone.blue.segments[0].keys.append( 1349 | StepLinear64Key(time * 60, b) 1350 | ) 1351 | bone.alpha.segments[0].keys.append( 1352 | StepLinear64Key(time * 60, a) 1353 | ) 1354 | else: 1355 | for time, ( 1356 | (ra, ga, ba, aa), 1357 | (rv, gv, bv, av), 1358 | (rb, gb, bb, ab), 1359 | ) in zip(inputs, itertools.batched(outputs, 3)): 1360 | bone.red.segments[0].keys.append( 1361 | Hermite128Key(time * 60, rv, ra, rb) 1362 | ) 1363 | bone.green.segments[0].keys.append( 1364 | Hermite128Key(time * 60, gv, ga, gb) 1365 | ) 1366 | bone.blue.segments[0].keys.append( 1367 | Hermite128Key(time * 60, bv, ba, bb) 1368 | ) 1369 | bone.alpha.segments[0].keys.append( 1370 | Hermite128Key(time * 60, av, aa, ab) 1371 | ) 1372 | 1373 | # optional lighting stuff 1374 | 1375 | # light_animation = GraphicsAnimationGroup() 1376 | # light_animation.name = 'LightAnimation' 1377 | # cflt.animation_group_descriptions.add(light_animation.name, light_animation) 1378 | # light_animation.member_type = 4 1379 | # light_animation.blend_operations.add(8) 1380 | # light_animation.blend_operations.add(3) 1381 | # light_animation.blend_operations.add(6) 1382 | # light_animation.blend_operations.add(2) 1383 | # light_animation.blend_operations.add(0) 1384 | 1385 | # member = AnimationGroupMember() 1386 | # light_animation.members.add('Transform', member) 1387 | # member.object_type = 0x800000 1388 | # member.path = 'Transform' 1389 | # member.value_offset = 48 1390 | # member.value_size = 36 1391 | # member.field_type = 9 1392 | # member.parent_index = 9 1393 | 1394 | # member = AnimationGroupMember() 1395 | # light_animation.members.add('Ambient', member) 1396 | # member.object_type = 0x100000 1397 | # member.path = 'Ambient' 1398 | # member.value_offset = 188 1399 | # member.value_size = 16 1400 | # member.unknown = 1 1401 | # member.field_type = 13 1402 | # member.value_index = 0 1403 | 1404 | # member = AnimationGroupMember() 1405 | # light_animation.members.add('Diffuse', member) 1406 | # member.object_type = 0x100000 1407 | # member.path = 'Diffuse' 1408 | # member.value_offset = 204 1409 | # member.value_size = 16 1410 | # member.unknown = 1 1411 | # member.field_type = 13 1412 | # member.value_index = 1 1413 | 1414 | # member = AnimationGroupMember() 1415 | # light_animation.members.add('Specular0', member) 1416 | # member.object_type = 0x100000 1417 | # member.path = 'Specular0' 1418 | # member.value_offset = 220 1419 | # member.value_size = 16 1420 | # member.unknown = 1 1421 | # member.field_type = 13 1422 | # member.value_index = 2 1423 | 1424 | # member = AnimationGroupMember() 1425 | # light_animation.members.add('Specular1', member) 1426 | # member.object_type = 0x100000 1427 | # member.path = 'Specular1' 1428 | # member.value_offset = 236 1429 | # member.value_size = 16 1430 | # member.unknown = 1 1431 | # member.field_type = 13 1432 | # member.value_index = 3 1433 | 1434 | # member = AnimationGroupMember() 1435 | # light_animation.members.add('Direction', member) 1436 | # member.object_type = 0x100000 1437 | # member.path = 'Direction' 1438 | # member.value_offset = 268 1439 | # member.value_size = 12 1440 | # member.unknown = 2 1441 | # member.field_type = 13 1442 | # member.value_index = 4 1443 | 1444 | # member = AnimationGroupMember() 1445 | # light_animation.members.add('DistanceAttenuationStart', member) 1446 | # member.object_type = 0x100000 1447 | # member.path = 'DistanceAttenuationStart' 1448 | # member.value_offset = 288 1449 | # member.value_size = 4 1450 | # member.unknown = 3 1451 | # member.field_type = 13 1452 | # member.value_index = 5 1453 | 1454 | # member = AnimationGroupMember() 1455 | # light_animation.members.add('DistanceAttenuationEnd', member) 1456 | # member.object_type = 0x100000 1457 | # member.path = 'DistanceAttenuationEnd' 1458 | # member.value_offset = 292 1459 | # member.value_size = 4 1460 | # member.unknown = 3 1461 | # member.field_type = 13 1462 | # member.value_index = 6 1463 | 1464 | # member = AnimationGroupMember() 1465 | # light_animation.members.add('IsLightEnabled', member) 1466 | # member.object_type = 0x100000 1467 | # member.path = 'IsLightEnabled' 1468 | # member.value_offset = 180 1469 | # member.value_size = 1 1470 | # member.unknown = 4 1471 | # member.field_type = 12 1472 | 1473 | return cgfx 1474 | 1475 | 1476 | def write(cgfx: CGFX) -> bytes: 1477 | strings = StringTable() 1478 | imag = StringTable() 1479 | offset = cgfx.prepare(0, strings, imag) 1480 | offset = strings.prepare(offset) 1481 | cgfx.data.section_size = offset - cgfx.data.offset 1482 | if not imag.empty(): 1483 | cgfx.header.nr_blocks = 2 1484 | offset += 8 # IMAG header 1485 | offset = imag.prepare(offset) 1486 | cgfx.header.file_size = offset 1487 | data = cgfx.write(strings, imag) 1488 | data += strings.write() 1489 | if not imag.empty(): 1490 | data += b"IMAG" + imag.size().to_bytes(4, "little") + imag.write() 1491 | if len(data) > 0x80000: 1492 | print(f"WARNING: CGFX is too big ({len(data)} bytes, max is {0x80000} bytes)") 1493 | return data 1494 | 1495 | 1496 | def main(): 1497 | parser = argparse.ArgumentParser(description="Convert a glTF model to CGFX.") 1498 | parser.add_argument("in_gltf", type=str, help="The input glTF (.gltf or .glb)") 1499 | parser.add_argument( 1500 | "out_cgfx", type=str, help="The output CGFX (.cgfx)", nargs="?", default=None 1501 | ) 1502 | args = parser.parse_args() 1503 | if args.out_cgfx is None: 1504 | args.out_cgfx = os.path.splitext(args.in_gltf)[0] + ".cgfx" 1505 | 1506 | gltf = gltflib.GLTF.load(args.in_gltf, load_file_resources=True) 1507 | cgfx = convert_gltf(gltf) 1508 | with open(args.out_cgfx, "wb") as f: 1509 | f.write(write(cgfx)) 1510 | 1511 | 1512 | if __name__ == "__main__": 1513 | main() 1514 | -------------------------------------------------------------------------------- /normal_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyfloogle/pycgfx/1f78850086f3a77c41e07162e842f97a5bf3c18a/normal_demo.gif --------------------------------------------------------------------------------