├── .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 | 
7 |
8 | [Skinned mesh animation demo](https://github.com/KhronosGroup/glTF-Sample-Assets/blob/main/Models/CesiumMan/README.md)
9 |
10 | 
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
--------------------------------------------------------------------------------