Repository navigation
Expand file tree
/
Copy pathm3_bindings_1.cpp
More file actions
1042 lines (914 loc) · 87.3 KB
/
Copy pathm3_bindings_1.cpp
File metadata and controls
1042 lines (914 loc) · 87.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 Fernando Sahmkow
//
// AUTOGENERATED by tools/codegen — do not edit by hand.
// Regenerate via: python -m tools.codegen.codegen m3 --backend pybind11
//
// Source headers and `@bind` annotations live under include/whiteout/.
//
// Include order matters here:
// 1. pybind11/pybind11.h sets up the base library
// 2. project headers define whiteout::u32 et al used inside MAKE_OPAQUE
// 3. PYBIND11_MAKE_OPAQUE opts out of stl.h's auto-conversion for our vectors
// 4. pybind11/stl.h, stl_bind.h honor the opaque declarations
#include <pybind11/pybind11.h>
#include <array>
#include <cstdint>
#include <optional>
#include <sstream>
#include <string>
#include <vector>
#include <whiteout/vector_types.h>
#include <whiteout/models/m3/types.h>
#include <whiteout/models/m3/structures/anim.h>
#include <whiteout/models/m3/structures/base.h>
#include <whiteout/models/m3/structures/effect.h>
#include <whiteout/models/m3/structures/material.h>
#include <whiteout/models/m3/structures/mesh.h>
#include <whiteout/models/m3/structures/misc.h>
#include <whiteout/models/m3/structures/physics.h>
#include <whiteout/models/m3/structures/scene.h>
#include <whiteout/models/m3/structures.h>
#include <whiteout/models/m3/parser.h>
#include <whiteout/models/m3/writer.h>
#include <whiteout/interfaces.h>
PYBIND11_MAKE_OPAQUE(std::vector<std::string>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::u8>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::u32>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::u64>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::u16>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::Vector3f>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::Vector4f>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::AnimRef<whiteout::f32>>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::AnimRef<whiteout::Vector3f>>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::AnimationGroup>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::AnimationState>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::AttachmentPoint>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::AttachmentVolume>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::Batch>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::BillboardBehavior>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::Bone>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::BoneAnimationSet>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::Camera>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::ClothCollider>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::ClothPhysics>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::ClothProxy>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::CompositeMaterial>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::CompositeSection>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::ConvexHullHalfEdge>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::CreepMaterial>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::DataDrivenGroup>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::DataDrivenMaterial>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::DataDrivenProperty>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::DisplacementMaterial>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::Force>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::HairMaterial>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::HitTestShape>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::IKCCD>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::IKJoint>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::IKTwoJoint>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::InitialReference>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::LensFlare>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::Light>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::MaterialMap>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::MeshDivision>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::MeshSection>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::OneBoneSolver>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::ParticleEmitter>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::ParticleEmitterCopy>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::PhysicsJoint>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::PhysicsMeshBvhNode>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::PhysicsShape>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::Projector>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::ReflectionMaterial>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::Region>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::RibbonEmitter>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::RigidBody>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::STBMaterial>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::Sequence>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::ShadowBox>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::SplineRibbon>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::StandardMaterial>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::SubFlare>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::SubTrackContainer>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::TerrainMaterial>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::TrailingModel>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::TriggerData>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::TurretBehavior>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::ViewVolume>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::VolumeMaterial>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::VolumeNoiseMaterial>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::m3::Warp>);
#include <pybind11/stl.h>
#include <pybind11/stl_bind.h>
#include <pybind11/operators.h>
#include <pybind11/numpy.h>
namespace py = pybind11;
namespace {
// Buffer-protocol vector wrapper for std::vector<Elem> where Elem is laid
// out as `Components` contiguous Scalars (Vector3f, Quaternion, ColorBGRA…).
//
// pybind11's bind_vector<> wires up py::buffer_protocol() automatically,
// but stl_bind.h's auto-buffer-info path static-asserts on element types
// lacking a format_descriptor — Vector3f/Quaternion don't have one.
// We provide the same Python surface (append, extend, clear, __getitem__,
// __setitem__, __iter__, __len__, __bool__) plus a 2D buffer view.
template <typename Elem, typename Scalar, py::ssize_t Components>
auto bindBufferVector(py::module_& m, const char* name) {
using Vec = std::vector<Elem>;
py::class_<Vec> cls(m, name, py::buffer_protocol());
cls.def(py::init<>());
cls.def("__len__", [](const Vec& v) { return v.size(); });
cls.def("__bool__", [](const Vec& v) { return !v.empty(); });
cls.def("__getitem__", [](const Vec& v, std::size_t i) -> Elem {
if (i >= v.size()) throw py::index_error();
return v[i];
});
cls.def("__setitem__", [](Vec& v, std::size_t i, const Elem& val) {
if (i >= v.size()) throw py::index_error();
v[i] = val;
});
cls.def("__iter__", [](Vec& v) {
return py::make_iterator(v.begin(), v.end());
}, py::keep_alive<0, 1>());
cls.def("append", [](Vec& v, const Elem& val) { v.push_back(val); });
cls.def("extend", [](Vec& v, const Vec& o) {
v.insert(v.end(), o.begin(), o.end());
});
cls.def("clear", &Vec::clear);
cls.def_buffer([](Vec& v) -> py::buffer_info {
if constexpr (Components == 1) {
return py::buffer_info(
v.data(),
static_cast<py::ssize_t>(sizeof(Scalar)),
py::format_descriptor<Scalar>::format(),
1,
{ static_cast<py::ssize_t>(v.size()) },
{ static_cast<py::ssize_t>(sizeof(Scalar)) });
} else {
return py::buffer_info(
v.data(),
static_cast<py::ssize_t>(sizeof(Scalar)),
py::format_descriptor<Scalar>::format(),
2,
{ static_cast<py::ssize_t>(v.size()), Components },
{ static_cast<py::ssize_t>(sizeof(Elem)),
static_cast<py::ssize_t>(sizeof(Scalar)) });
}
});
return cls;
}
} // namespace
// Part 1 of bind_m3(), which calls the parts in order.
void bind_m3_1(py::module_& m) {
py::class_<whiteout::m3::StandardMaterial>(m, "StandardMaterial", R"doc(MAT_ — Standard material (v0–v20, 268–352 bytes)
The primary material type with up to 18 texture layers (diffuse, specular, emissive, normal, height, etc.), blend mode, HDR multipliers, and per-version extensions for normal-blend and gloss layers.)doc")
.def(py::init<>())
.def_readwrite("name", &whiteout::m3::StandardMaterial::name, R"doc(Material name (Ref<CHAR>))doc")
.def_readwrite("additional_flags", &whiteout::m3::StandardMaterial::additionalFlags, R"doc(Additional flags)doc")
.def_readwrite("flags", &whiteout::m3::StandardMaterial::flags, R"doc(Material rendering flags)doc")
.def_readwrite("blend_mode", &whiteout::m3::StandardMaterial::blendMode, R"doc(Alpha blend mode)doc")
.def_readwrite("priority", &whiteout::m3::StandardMaterial::priority, R"doc(Render priority (lower = earlier))doc")
.def_readwrite("rtt_channels", &whiteout::m3::StandardMaterial::rttChannels, R"doc(RTT channel mask)doc")
.def_readwrite("specular_exponent", &whiteout::m3::StandardMaterial::specularExponent, R"doc(Specular highlight exponent)doc")
.def_readwrite("depth_blend_falloff", &whiteout::m3::StandardMaterial::depthBlendFalloff, R"doc(Depth blend falloff distance)doc")
.def_readwrite("alpha_test_threshold", &whiteout::m3::StandardMaterial::alphaTestThreshold, R"doc(Alpha test cut-off value)doc")
.def_readwrite("hdr_specular_multiplier", &whiteout::m3::StandardMaterial::hdrSpecularMultiplier, R"doc(HDR specular multiplier)doc")
.def_readwrite("hdr_emissive_multiplier", &whiteout::m3::StandardMaterial::hdrEmissiveMultiplier, R"doc(HDR emissive multiplier)doc")
.def_readwrite("hdr_environment_constant", &whiteout::m3::StandardMaterial::hdrEnvironmentConstant, R"doc(HDR environment constant (v20))doc")
.def_readwrite("hdr_environment_diffuse", &whiteout::m3::StandardMaterial::hdrEnvironmentDiffuse, R"doc(HDR environment diffuse (v20))doc")
.def_readwrite("hdr_environment_specular", &whiteout::m3::StandardMaterial::hdrEnvironmentSpecular, R"doc(HDR environment specular (v20))doc")
.def_readwrite("diffuse_layer", &whiteout::m3::StandardMaterial::diffuseLayer, R"doc(Diffuse / albedo texture)doc")
.def_readwrite("decal_layer", &whiteout::m3::StandardMaterial::decalLayer, R"doc(Decal overlay texture)doc")
.def_readwrite("specular_layer", &whiteout::m3::StandardMaterial::specularLayer, R"doc(Specular map texture)doc")
.def_readwrite("gloss_layer", &whiteout::m3::StandardMaterial::glossLayer, R"doc(Gloss map texture (v16+))doc")
.def_readwrite("emissive_layer1", &whiteout::m3::StandardMaterial::emissiveLayer1, R"doc(Emissive layer 1)doc")
.def_readwrite("emissive_layer2", &whiteout::m3::StandardMaterial::emissiveLayer2, R"doc(Emissive layer 2)doc")
.def_readwrite("environment_layer", &whiteout::m3::StandardMaterial::environmentLayer, R"doc(Environment reflection map)doc")
.def_readwrite("environment_mask_layer", &whiteout::m3::StandardMaterial::environmentMaskLayer, R"doc(Environment mask)doc")
.def_readwrite("alpha_layer1", &whiteout::m3::StandardMaterial::alphaLayer1, R"doc(Alpha mask layer 1)doc")
.def_readwrite("alpha_layer2", &whiteout::m3::StandardMaterial::alphaLayer2, R"doc(Alpha mask layer 2)doc")
.def_readwrite("normal_layer", &whiteout::m3::StandardMaterial::normalLayer, R"doc(Normal / bump map)doc")
.def_readwrite("height_layer", &whiteout::m3::StandardMaterial::heightLayer, R"doc(Height / parallax map)doc")
.def_readwrite("light_map_layer", &whiteout::m3::StandardMaterial::lightMapLayer, R"doc(Light map)doc")
.def_readwrite("ambient_occlusion_layer", &whiteout::m3::StandardMaterial::ambientOcclusionLayer, R"doc(Ambient occlusion map)doc")
.def_readwrite("normal_blend1_mask_layer", &whiteout::m3::StandardMaterial::normalBlend1MaskLayer, R"doc(Normal blend 1 mask (v19+))doc")
.def_readwrite("normal_blend2_mask_layer", &whiteout::m3::StandardMaterial::normalBlend2MaskLayer, R"doc(Normal blend 2 mask (v19+))doc")
.def_readwrite("normal_blend1_layer", &whiteout::m3::StandardMaterial::normalBlend1Layer, R"doc(Normal blend 1 map (v19+))doc")
.def_readwrite("normal_blend2_layer", &whiteout::m3::StandardMaterial::normalBlend2Layer, R"doc(Normal blend 2 map (v19+))doc")
.def_readwrite("material_class", &whiteout::m3::StandardMaterial::materialClass, R"doc(Material class (unit, building, etc.))doc")
.def_readwrite("layer_blend_mode", &whiteout::m3::StandardMaterial::layerBlendMode, R"doc(Layer blend operation)doc")
.def_readwrite("emissive_blend_mode1", &whiteout::m3::StandardMaterial::emissiveBlendMode1, R"doc(Emissive layer 1 blend mode)doc")
.def_readwrite("emissive_blend_mode2", &whiteout::m3::StandardMaterial::emissiveBlendMode2, R"doc(Emissive layer 2 blend mode)doc")
.def_readwrite("specular_mode", &whiteout::m3::StandardMaterial::specularMode, R"doc(Specular computation mode)doc")
.def_readwrite("parallax_height", &whiteout::m3::StandardMaterial::parallaxHeight, R"doc(Animated parallax height)doc")
.def_readwrite("motion_blur_amount", &whiteout::m3::StandardMaterial::motionBlurAmount, R"doc(Animated motion blur amount)doc")
.def_readwrite("normal_blend_factors", &whiteout::m3::StandardMaterial::normalBlendFactors, R"doc(Normal blend factors (v19+))doc")
;
py::class_<whiteout::m3::DisplacementMaterial>(m, "DisplacementMaterial", R"doc(DIS_ — Displacement material (v0–v4, 68 bytes)
Applies vertex displacement via a normal map and animated strength.)doc")
.def(py::init<>())
.def_readwrite("name", &whiteout::m3::DisplacementMaterial::name, R"doc(Material name (Ref<CHAR>))doc")
.def_readwrite("unknown", &whiteout::m3::DisplacementMaterial::unknown, R"doc(Unknown field)doc")
.def_readwrite("strength", &whiteout::m3::DisplacementMaterial::strength, R"doc(Animated displacement strength)doc")
.def_readwrite("normal_map", &whiteout::m3::DisplacementMaterial::normalMap, R"doc(Normal / displacement direction map)doc")
.def_readwrite("strength_map", &whiteout::m3::DisplacementMaterial::strengthMap, R"doc(Strength mask texture)doc")
.def_readwrite("priority", &whiteout::m3::DisplacementMaterial::priority, R"doc(Render priority)doc")
;
py::class_<whiteout::m3::CompositeSection>(m, "CompositeSection", R"doc(CMS_ — Composite material section (v0, 24 bytes)
A single section within a composite material, referencing another material index with an animated blend multiplier.)doc")
.def(py::init<>())
.def_readwrite("material_index", &whiteout::m3::CompositeSection::materialIndex, R"doc(Index into MATM array)doc")
.def_readwrite("map_multiplier", &whiteout::m3::CompositeSection::mapMultiplier, R"doc(Animated blend weight)doc")
;
py::class_<whiteout::m3::CompositeMaterial>(m, "CompositeMaterial", R"doc(CMP_ — Composite material (v0–v2, 28 bytes)
Blends multiple sub-materials via CompositeSection entries.)doc")
.def(py::init<>())
.def_readwrite("name", &whiteout::m3::CompositeMaterial::name, R"doc(Material name (Ref<CHAR>))doc")
.def_readwrite("priority", &whiteout::m3::CompositeMaterial::priority, R"doc(Render priority)doc")
.def_readwrite("sections", &whiteout::m3::CompositeMaterial::sections, R"doc(Sub-material sections (CMS_))doc")
;
py::class_<whiteout::m3::TerrainMaterial>(m, "TerrainMaterial", R"doc(TER_ — Terrain material (v0–v1, 28 bytes)
Simple terrain-specific material with a single texture layer.)doc")
.def(py::init<>())
.def_readwrite("name", &whiteout::m3::TerrainMaterial::name, R"doc(Material name (Ref<CHAR>))doc")
.def_readwrite("terrain_map", &whiteout::m3::TerrainMaterial::terrainMap, R"doc(Terrain texture layer)doc")
.def_readwrite("unknown", &whiteout::m3::TerrainMaterial::unknown, R"doc(Unknown field)doc")
;
py::class_<whiteout::m3::VolumeMaterial>(m, "VolumeMaterial", R"doc(VOL_ — Volume material (v0, 84 bytes)
Volumetric rendering material with density falloff, color map, and two noise maps for procedural volumetric effects.)doc")
.def(py::init<>())
.def_readwrite("name", &whiteout::m3::VolumeMaterial::name, R"doc(Material name (Ref<CHAR>))doc")
.def_readwrite("blend_mode", &whiteout::m3::VolumeMaterial::blendMode, R"doc(Blend mode)doc")
.def_readwrite("falloff_type", &whiteout::m3::VolumeMaterial::falloffType, R"doc(Density falloff type)doc")
.def_readwrite("density", &whiteout::m3::VolumeMaterial::density, R"doc(Animated density)doc")
.def_readwrite("color_map", &whiteout::m3::VolumeMaterial::colorMap, R"doc(Color map texture)doc")
.def_readwrite("noise_map1", &whiteout::m3::VolumeMaterial::noiseMap1, R"doc(Noise map 1)doc")
.def_readwrite("noise_map2", &whiteout::m3::VolumeMaterial::noiseMap2, R"doc(Noise map 2)doc")
.def_readwrite("alpha_threshold", &whiteout::m3::VolumeMaterial::alphaThreshold, R"doc(Alpha test threshold)doc")
;
py::class_<whiteout::m3::HairMaterial>(m, "HairMaterial", R"doc(HAI_ — Hair material (defunct, v0, 116 bytes)
Anisotropic hair rendering material with specular shift and AO. Always null in observed corpus data.)doc")
.def(py::init<>())
.def_readwrite("name", &whiteout::m3::HairMaterial::name, R"doc(Material name (Ref<CHAR>))doc")
.def_readwrite("layer_base", &whiteout::m3::HairMaterial::layerBase, R"doc(Base color / diffuse texture)doc")
.def_readwrite("layer_spec_shift", &whiteout::m3::HairMaterial::layerSpecShift, R"doc(Anisotropic specular shift map)doc")
.def_readwrite("layer_spec_noise", &whiteout::m3::HairMaterial::layerSpecNoise, R"doc(Specular noise / break-up map)doc")
.def_readwrite("layer_ao", &whiteout::m3::HairMaterial::layerAO, R"doc(Ambient occlusion map)doc")
.def_readwrite("shift_primary", &whiteout::m3::HairMaterial::shiftPrimary, R"doc(Primary specular shift)doc")
.def_readwrite("shift_secondary", &whiteout::m3::HairMaterial::shiftSecondary, R"doc(Secondary specular shift)doc")
.def_readwrite("color_diffuse", &whiteout::m3::HairMaterial::colorDiffuse, R"doc(Animated diffuse tint)doc")
.def_readwrite("color_spec", &whiteout::m3::HairMaterial::colorSpec, R"doc(Animated specular tint)doc")
.def_readwrite("spec_exponent0", &whiteout::m3::HairMaterial::specExponent0, R"doc(Primary specular exponent)doc")
.def_readwrite("spec_exponent1", &whiteout::m3::HairMaterial::specExponent1, R"doc(Secondary specular exponent)doc")
;
py::class_<whiteout::m3::VolumeNoiseMaterial>(m, "VolumeNoiseMaterial", R"doc(VON_ — Volume noise material (v0, 268 bytes)
Volumetric noise-based rendering material with animated density, falloff, scroll rate, position, scale, and rotation. Used for gas/smoke/cloud effects.)doc")
.def(py::init<>())
.def_readwrite("name", &whiteout::m3::VolumeNoiseMaterial::name, R"doc(Material name (Ref<CHAR>))doc")
.def_readwrite("falloff_type", &whiteout::m3::VolumeNoiseMaterial::falloffType, R"doc(Density falloff type)doc")
.def_readwrite("draw_transparency", &whiteout::m3::VolumeNoiseMaterial::drawTransparency, R"doc(Camera position mode (inside/outside))doc")
.def_readwrite("density", &whiteout::m3::VolumeNoiseMaterial::density, R"doc(Animated density)doc")
.def_readwrite("near_plane", &whiteout::m3::VolumeNoiseMaterial::nearPlane, R"doc(Animated near-plane clip)doc")
.def_readwrite("falloff", &whiteout::m3::VolumeNoiseMaterial::falloff, R"doc(Animated falloff distance)doc")
.def_readwrite("color_map", &whiteout::m3::VolumeNoiseMaterial::colorMap, R"doc(Color map texture)doc")
.def_readwrite("noise_map1", &whiteout::m3::VolumeNoiseMaterial::noiseMap1, R"doc(Noise map 1)doc")
.def_readwrite("noise_map2", &whiteout::m3::VolumeNoiseMaterial::noiseMap2, R"doc(Noise map 2)doc")
.def_readwrite("scroll_rate", &whiteout::m3::VolumeNoiseMaterial::scrollRate, R"doc(Animated noise scroll rate)doc")
.def_readwrite("position", &whiteout::m3::VolumeNoiseMaterial::position, R"doc(Animated volume position)doc")
.def_readwrite("scale", &whiteout::m3::VolumeNoiseMaterial::scale, R"doc(Animated volume scale)doc")
.def_readwrite("rotation", &whiteout::m3::VolumeNoiseMaterial::rotation, R"doc(Animated volume rotation)doc")
.def_readwrite("alpha_threshold", &whiteout::m3::VolumeNoiseMaterial::alphaThreshold, R"doc(Alpha test threshold)doc")
.def_readwrite("flags", &whiteout::m3::VolumeNoiseMaterial::flags, R"doc(Volume noise material flags)doc")
;
py::class_<whiteout::m3::CreepMaterial>(m, "CreepMaterial", R"doc(CREP — Creep material (v0–v1, 28 bytes)
Material for Zerg creep rendering with a mask map and creep-low parameter.)doc")
.def(py::init<>())
.def_readwrite("name", &whiteout::m3::CreepMaterial::name, R"doc(Material name (Ref<CHAR>))doc")
.def_readwrite("mask_map", &whiteout::m3::CreepMaterial::maskMap, R"doc(Creep mask texture)doc")
.def_readwrite("creep_low", &whiteout::m3::CreepMaterial::creepLow, R"doc(Creep low parameter)doc")
;
py::class_<whiteout::m3::STBMaterial>(m, "STBMaterial", R"doc(STBM — Splat terrain bake material (v0, 48 bytes)
Material for baked terrain splat rendering with diffuse, normal, and specular texture layers.)doc")
.def(py::init<>())
.def_readwrite("name", &whiteout::m3::STBMaterial::name, R"doc(Material name (Ref<CHAR>))doc")
.def_readwrite("diffuse_map", &whiteout::m3::STBMaterial::diffuseMap, R"doc(Diffuse / albedo map)doc")
.def_readwrite("normal_map", &whiteout::m3::STBMaterial::normalMap, R"doc(Normal map)doc")
.def_readwrite("specular_map", &whiteout::m3::STBMaterial::specularMap, R"doc(Specular map)doc")
;
py::class_<whiteout::m3::ReflectionMaterial>(m, "ReflectionMaterial", R"doc(REF_ — Reflection material (v0–v3, 84–160 bytes)
Planar or cube-map reflection material with animated reflection/displacement strength, blur, and multiple texture layers.)doc")
.def(py::init<>())
.def_readwrite("name", &whiteout::m3::ReflectionMaterial::name, R"doc(Material name (Ref<CHAR>))doc")
.def_readwrite("unknown", &whiteout::m3::ReflectionMaterial::unknown, R"doc(Unknown field)doc")
.def_readwrite("reflection_strength", &whiteout::m3::ReflectionMaterial::reflectionStrength, R"doc(Animated reflection strength (v2+))doc")
.def_readwrite("displacement_strength", &whiteout::m3::ReflectionMaterial::displacementStrength, R"doc(Animated displacement strength (v2+))doc")
.def_readwrite("reflection_offset", &whiteout::m3::ReflectionMaterial::reflectionOffset, R"doc(Animated reflection offset (v2+))doc")
.def_readwrite("blur_angle", &whiteout::m3::ReflectionMaterial::blurAngle, R"doc(Animated blur angle (v2+))doc")
.def_readwrite("blur_distance_max", &whiteout::m3::ReflectionMaterial::blurDistanceMax, R"doc(Animated max blur distance (v2+))doc")
.def_readwrite("reflection_map", &whiteout::m3::ReflectionMaterial::reflectionMap, R"doc(Reflection map texture)doc")
.def_readwrite("displacement_map", &whiteout::m3::ReflectionMaterial::displacementMap, R"doc(Displacement map texture)doc")
.def_readwrite("blur_map", &whiteout::m3::ReflectionMaterial::blurMap, R"doc(Blur map texture)doc")
.def_readwrite("flags", &whiteout::m3::ReflectionMaterial::flags, R"doc(Reflection flags (v2+))doc")
.def_readwrite("unknown2", &whiteout::m3::ReflectionMaterial::unknown2, R"doc(Index of the DataDrivenMaterial this was converted into, 0xFFFFFFFF if none (v3+). Written by the Heroes load-time conversion pass, not a material parameter; meaningless in a model that carries no MADD chunk. v3 exists only to hold it. Defaulted because an invented REF_ has no link to name, and a v3 record that says anything else points the Heroes loader at a MADD index.)doc")
;
py::class_<whiteout::m3::SubFlare>(m, "SubFlare", R"doc(LFSB — Sub-flare element (v0–v2, 56 bytes)
A single flare element within a LensFlare material, with position, size, scale, fade, color, and offset parameters.)doc")
.def(py::init<>())
.def_readwrite("index", &whiteout::m3::SubFlare::index, R"doc(Flare element index)doc")
.def_readwrite("position", &whiteout::m3::SubFlare::position, R"doc(Position along the flare axis (0–1))doc")
.def_readwrite("size_xy", &whiteout::m3::SubFlare::sizeXY, R"doc(Base size (width, height))doc")
.def_readwrite("scale_xy", &whiteout::m3::SubFlare::scaleXY, R"doc(Scale multiplier (width, height))doc")
.def_readwrite("fade_in", &whiteout::m3::SubFlare::fadeIn, R"doc(Fade-in range (start, end))doc")
.def_readwrite("fade_out", &whiteout::m3::SubFlare::fadeOut, R"doc(Fade-out range (start, end))doc")
.def_readwrite("color_alpha", &whiteout::m3::SubFlare::colorAlpha, R"doc(Flare color and alpha)doc")
.def_readwrite("face_center", &whiteout::m3::SubFlare::faceCenter, R"doc(Whether to face the flare center)doc")
.def_readwrite("offset", &whiteout::m3::SubFlare::offset, R"doc(Offset from flare center)doc")
;
py::class_<whiteout::m3::LensFlare>(m, "LensFlare", R"doc(LFLR — Lens flare material (v0–v3, 152 bytes)
Lens flare effect with animated intensity, color, HDR, size, sub-flare elements, and flipbook texture grid parameters.)doc")
.def(py::init<>())
.def_readwrite("name", &whiteout::m3::LensFlare::name, R"doc(Flare name (Ref<CHAR>))doc")
.def_readwrite("flare_map", &whiteout::m3::LensFlare::flareMap, R"doc(Flare texture atlas)doc")
.def_readwrite("mask_map", &whiteout::m3::LensFlare::maskMap, R"doc(Flare mask texture)doc")
.def_readwrite("sub_flares", &whiteout::m3::LensFlare::subFlares, R"doc(Sub-flare elements (LFSB))doc")
.def_readwrite("columns", &whiteout::m3::LensFlare::columns, R"doc(Flipbook grid columns)doc")
.def_readwrite("rows", &whiteout::m3::LensFlare::rows, R"doc(Flipbook grid rows)doc")
.def_readwrite("distance_fade", &whiteout::m3::LensFlare::distanceFade, R"doc(Distance fade start)doc")
.def_readwrite("lib_name", &whiteout::m3::LensFlare::libName, R"doc(Library name (Ref<CHAR>))doc")
.def_readwrite("intensity", &whiteout::m3::LensFlare::intensity, R"doc(Animated intensity)doc")
.def_readwrite("color", &whiteout::m3::LensFlare::color, R"doc(Animated color)doc")
.def_readwrite("hdr", &whiteout::m3::LensFlare::hdr, R"doc(Animated HDR multiplier)doc")
.def_readwrite("size", &whiteout::m3::LensFlare::size, R"doc(Animated size)doc")
;
py::class_<whiteout::m3::DataDrivenProperty>(m, "DataDrivenProperty", R"doc(One decoded property of a data-driven material
`data` is the raw value; its shape depends on `size`: 4 = scalar (f32, u32 enum, or BGRA colour — depends on the property), 8 = `{u32 index into DataDrivenMaterial::texturePaths, u32 texture source}`, 12 = 3 floats, 16 = 4 floats, 20 = `{u32 ColorChannelSelect, f32 multiply, f32 add, f32, u16 flags}`, 32 = `{f32 offsetU/V, tilingU/V, angleU/V/W, u32 flags}`, 48 = fresnel `{u32 FresnelMode, f32 exponent, min, max, rotation, mask}`.)doc")
.def(py::init<>())
.def_readwrite("name_hash", &whiteout::m3::DataDrivenProperty::nameHash, R"doc(crc32 of the property name)doc")
.def_readwrite("name", &whiteout::m3::DataDrivenProperty::name, R"doc(Resolved name, empty when the hash is unknown)doc")
.def_readwrite("data", &whiteout::m3::DataDrivenProperty::data, R"doc(Raw value bytes)doc")
.def("data_view",
[](const whiteout::m3::DataDrivenProperty& self) {
return py::memoryview::from_memory(
static_cast<const void*>(self.data.data()),
static_cast<py::ssize_t>(self.data.size()));
})
;
py::class_<whiteout::m3::DataDrivenGroup>(m, "DataDrivenGroup", R"doc(One shader fragment of a data-driven material, with its properties)doc")
.def(py::init<>())
.def_readwrite("name_hash", &whiteout::m3::DataDrivenGroup::nameHash, R"doc(crc32 of the fragment name)doc")
.def_readwrite("name", &whiteout::m3::DataDrivenGroup::name, R"doc(Resolved name, empty when unknown)doc")
.def_readwrite("properties", &whiteout::m3::DataDrivenGroup::properties, R"doc(Properties, in stored order)doc")
;
py::class_<whiteout::m3::DataDrivenProperties>(m, "DataDrivenProperties", R"doc(The decoded contents of DataDrivenMaterial::propertyBlob)doc")
.def(py::init<>())
.def_readwrite("groups", &whiteout::m3::DataDrivenProperties::groups, R"doc(Fragment groups, in stored order)doc")
;
py::class_<whiteout::m3::StandardMaterialConversion>(m, "StandardMaterialConversion", R"doc(Outcome of rebuilding a StandardMaterial from a DataDrivenMaterial
Not every data-driven material has a standard equivalent: some were authored directly against the shader-graph vocabulary, and others are the converted form of a DisplacementMaterial or ReflectionMaterial. `blocker` says which.
Conversion is lossy even when it succeeds, because the forward direction is: variant fragments collapse onto one layer slot, several fragments are derived from the model rather than the material, and per-field animation links are not carried in the blob. `lossy` lists what was dropped for this material.)doc")
.def(py::init<>())
.def_readwrite("converted", &whiteout::m3::StandardMaterialConversion::converted, R"doc(Whether `material` was produced)doc")
.def_readwrite("blocker", &whiteout::m3::StandardMaterialConversion::blocker, R"doc(Why not, when `converted` is false)doc")
.def_readwrite("lossy", &whiteout::m3::StandardMaterialConversion::lossy, R"doc(What the standard form cannot represent)doc")
.def_readwrite("material", &whiteout::m3::StandardMaterialConversion::material, R"doc(Only meaningful when `converted`)doc")
;
py::class_<whiteout::m3::DataDrivenMaterial>(m, "DataDrivenMaterial", R"doc(MADD — Data-driven material (v0–v3, 140–160 bytes)
The engine's own name for this chunk is `SDataDrivenMaterialData`. It is not "additional" data: at load the renderer converts every StandardMaterial (1), DisplacementMaterial (2) and ReflectionMaterial (10) in the model into one of these and rewrites the MaterialMap to MaterialType::DataDriven, so this is the only material representation the renderer actually consumes.
`fragmentHashes` names the shader fragments the material links, in order. Concatenating `shaderType`'s token with those names gives the shader permutation name, and its crc32 is the effect-cache lookup key.
`propertyBlob` is a self-describing two-level dictionary keyed by crc32 of unprefixed names — decode it with decodeProperties().
@see M3_FILE_FORMAT_SPECIFICATION.md §11 Materials)doc")
.def(py::init<>())
.def_readwrite("material_name", &whiteout::m3::DataDrivenMaterial::materialName, R"doc(Material name (Ref<CHAR>))doc")
.def_readwrite("fragment_hashes", &whiteout::m3::DataDrivenMaterial::fragmentHashes, R"doc(crc32 of each shader fragment name, in link order (U32_))doc")
.def_readwrite("extra_hashes", &whiteout::m3::DataDrivenMaterial::extraHashes, R"doc(Secondary hash list (U32_, v2+))doc")
.def_readwrite("property_blob", &whiteout::m3::DataDrivenMaterial::propertyBlob, R"doc(Property dictionary (Ref<CHAR>))doc")
.def("property_blob_view",
[](const whiteout::m3::DataDrivenMaterial& self) {
return py::memoryview::from_memory(
static_cast<const void*>(self.propertyBlob.data()),
static_cast<py::ssize_t>(self.propertyBlob.size()));
})
.def_readwrite("texture_paths", &whiteout::m3::DataDrivenMaterial::texturePaths, R"doc(Texture paths, indexed by the Tex* properties (SCHR))doc")
.def_readwrite("unknown108", &whiteout::m3::DataDrivenMaterial::unknown108, R"doc(1.0, 1.5 or 2.0 across the corpus)doc")
.def_readwrite("unknown112", &whiteout::m3::DataDrivenMaterial::unknown112, R"doc(1.0 in every known record)doc")
.def_readwrite("unknown116", &whiteout::m3::DataDrivenMaterial::unknown116)
.def_readwrite("effect_name_hash", &whiteout::m3::DataDrivenMaterial::effectNameHash, R"doc(crc32 of the shader permutation name; 0 = compute it at load)doc")
.def_readwrite("unknown124", &whiteout::m3::DataDrivenMaterial::unknown124)
.def_readwrite("padding128", &whiteout::m3::DataDrivenMaterial::padding128, R"doc(Zero in every known record)doc")
.def_readwrite("unknown132", &whiteout::m3::DataDrivenMaterial::unknown132)
.def_readwrite("unknown136", &whiteout::m3::DataDrivenMaterial::unknown136, R"doc(Packed bit field)doc")
.def_readwrite("unknown140", &whiteout::m3::DataDrivenMaterial::unknown140)
.def_readwrite("unknown144", &whiteout::m3::DataDrivenMaterial::unknown144)
.def_readwrite("unknown148", &whiteout::m3::DataDrivenMaterial::unknown148)
.def_readwrite("alpha_fresnel_flags", &whiteout::m3::DataDrivenMaterial::alphaFresnelFlags, R"doc(Derived cache the loader recomputes; do not trust over the blob)doc")
.def_readwrite("shader_type", &whiteout::m3::DataDrivenMaterial::shaderType, R"doc(Shader family prefix for the permutation name)doc")
.def_readwrite("unknown151", &whiteout::m3::DataDrivenMaterial::unknown151)
.def_readwrite("effect_name_hash2", &whiteout::m3::DataDrivenMaterial::effectNameHash2, R"doc(Second permutation hash, 0xFFFFFFFF = none (v3+))doc")
.def_readwrite("effect_name_hash3", &whiteout::m3::DataDrivenMaterial::effectNameHash3, R"doc(Third permutation hash, 0xFFFFFFFF = none (v3+))doc")
.def("decode_properties", &whiteout::m3::DataDrivenMaterial::decodeProperties, R"doc(Decode propertyBlob into fragment groups and named properties)doc")
.def("to_standard_material", &whiteout::m3::DataDrivenMaterial::toStandardMaterial, R"doc(Rebuild the StandardMaterial this was converted from, where possible
The engine only converts in the other direction, and does so lossily, so this reverses what it can and reports the rest. See StandardMaterialConversion.)doc")
.def("approximate_standard_material", &whiteout::m3::DataDrivenMaterial::approximateStandardMaterial, R"doc(Best-effort StandardMaterial for a material that has no exact one
toStandardMaterial() refuses shader-graph materials, which were authored in the node editor and never had a StandardMaterial form. This infers one anyway, from the node types, the per-node names in extraHashes, and the texture filenames. The blob stores nodes but not the edges between them, so the graph topology cannot be recovered and the result is a likeness, not a conversion — `lossy` always says so. Materials that are already fixed-function are forwarded to toStandardMaterial() unchanged.)doc")
.def("get_version", &whiteout::m3::DataDrivenMaterial::getVersion)
.def("set_version", &whiteout::m3::DataDrivenMaterial::setVersion, py::arg("newVersion"))
.def("force_version", &whiteout::m3::DataDrivenMaterial::forceVersion, py::arg("newVersion"))
;
py::class_<whiteout::m3::Bone>(m, "Bone", R"doc(BONE — Skeleton bone (v0–v1, 160 bytes)
Each bone has a parent index, animated position/rotation/scale/visibility, and flags controlling inheritance, billboard mode, and IK.)doc")
.def(py::init<>())
.def_readwrite("unknown", &whiteout::m3::Bone::unknown, R"doc(Unknown field)doc")
.def_readwrite("name", &whiteout::m3::Bone::name, R"doc(Bone name (Ref<CHAR>))doc")
.def_readwrite("flags", &whiteout::m3::Bone::flags, R"doc(Bone flags (inherit, billboard, IK, skin))doc")
.def_readwrite("parent_index", &whiteout::m3::Bone::parentIndex, R"doc(Parent bone index (0xFFFF = root))doc")
.def_readwrite("padding", &whiteout::m3::Bone::padding, R"doc(Alignment padding)doc")
.def_readwrite("position", &whiteout::m3::Bone::position, R"doc(Animated translation (36 bytes))doc")
.def_readwrite("rotation", &whiteout::m3::Bone::rotation, R"doc(Animated rotation (44 bytes))doc")
.def_readwrite("scale", &whiteout::m3::Bone::scale, R"doc(Animated scale (36 bytes))doc")
.def_readwrite("visibility", &whiteout::m3::Bone::visibility, R"doc(Animated visibility flag (20 bytes))doc")
;
py::class_<whiteout::m3::Region>(m, "Region", R"doc(REGN — Region / submesh (v0–v5, 48 bytes)
Describes a contiguous range of vertices and indices forming a submesh, with bone lookup info for skinning and UV scale/offset for texturing.)doc")
.def(py::init<>())
.def_readwrite("index", &whiteout::m3::Region::index, R"doc(Region index)doc")
.def_readwrite("unknown", &whiteout::m3::Region::unknown, R"doc(Unknown field)doc")
.def_readwrite("first_vertex", &whiteout::m3::Region::firstVertex, R"doc(First vertex in the vertex buffer)doc")
.def_readwrite("vertex_count", &whiteout::m3::Region::vertexCount, R"doc(Number of vertices)doc")
.def_readwrite("first_index", &whiteout::m3::Region::firstIndex, R"doc(First index in the index buffer)doc")
.def_readwrite("index_count", &whiteout::m3::Region::indexCount, R"doc(Number of indices (triangles × 3))doc")
.def_readwrite("unknown2", &whiteout::m3::Region::unknown2, R"doc(Repeats boneLookupCount (874 of 874 shipped regions))doc")
.def_readwrite("first_bone_lookup", &whiteout::m3::Region::firstBoneLookup, R"doc(First entry in bone lookup table)doc")
.def_readwrite("bone_lookup_count", &whiteout::m3::Region::boneLookupCount, R"doc(Number of bone lookup entries)doc")
.def_readwrite("padding", &whiteout::m3::Region::padding, R"doc(Alignment padding)doc")
.def_readwrite("bone_weight_pairs", &whiteout::m3::Region::boneWeightPairs, R"doc(Number of bone weight pairs per vertex)doc")
.def_readwrite("bone_index_pairs", &whiteout::m3::Region::boneIndexPairs, R"doc(Number of bone index pairs per vertex)doc")
.def_readwrite("root_bone", &whiteout::m3::Region::rootBone, R"doc(Root bone for this region)doc")
.def_readwrite("flags", &whiteout::m3::Region::flags, R"doc(Region flags (hidden, cloth, etc.))doc")
.def_readwrite("uv_scale", &whiteout::m3::Region::uvScale, R"doc(UV coordinate scale factor)doc")
.def_readwrite("uv_offset", &whiteout::m3::Region::uvOffset, R"doc(UV coordinate offset)doc")
;
py::class_<whiteout::m3::Batch>(m, "Batch", R"doc(BAT_ — Batch / draw call (v0–v1, 14 bytes)
Associates a Region with a material for rendering. Multiple batches may reference the same region with different materials.)doc")
.def(py::init<>())
.def_readwrite("unknown", &whiteout::m3::Batch::unknown, R"doc(Unknown field)doc")
.def_readwrite("region_index", &whiteout::m3::Batch::regionIndex, R"doc(Index into REGN array)doc")
.def_readwrite("unknown2", &whiteout::m3::Batch::unknown2, R"doc(Unknown field)doc")
.def_readwrite("material_index", &whiteout::m3::Batch::materialIndex, R"doc(Index into MATM material map array)doc")
.def_readwrite("bone_count", &whiteout::m3::Batch::boneCount, R"doc(Bone whose animated visibility gates this batch's draw (0xFFFF = always drawn). Misnamed — it is a bone index, not a count: the engine's submit loop reads it and skips the batch when that bone is invisible.)doc")
;
py::class_<whiteout::m3::MeshSection>(m, "MeshSection", R"doc(MSEC — Mesh section bounds (v0–v1, 80 bytes)
Per-node animated bounding extent used for culling and LOD.)doc")
.def(py::init<>())
.def_readwrite("node_index", &whiteout::m3::MeshSection::nodeIndex, R"doc(Index into BONE array)doc")
.def_readwrite("bounds", &whiteout::m3::MeshSection::bounds, R"doc(Animated bounding volume (76 bytes))doc")
;
py::class_<whiteout::m3::MeshDivision>(m, "MeshDivision", R"doc(DIV_ — Mesh division (v0–v2, 52 bytes)
Top-level mesh container grouping face indices, regions, batches, and mesh sections. Most models have a single division.)doc")
.def(py::init<>())
.def_readwrite("faces", &whiteout::m3::MeshDivision::faces, R"doc(Triangle indices (U16_))doc")
.def_readwrite("regions", &whiteout::m3::MeshDivision::regions, R"doc(Regions / submeshes (REGN))doc")
.def_readwrite("batches", &whiteout::m3::MeshDivision::batches, R"doc(Draw call batches (BAT_))doc")
.def_readwrite("msec", &whiteout::m3::MeshDivision::msec, R"doc(Per-node mesh section bounds (MSEC))doc")
.def_readwrite("instances", &whiteout::m3::MeshDivision::instances, R"doc(Instance count)doc")
;
py::class_<whiteout::m3::InitialReference>(m, "InitialReference", R"doc(IREF — Initial reference / inverse bind-pose (v0, 64 bytes)
Stores the 4×4 inverse bind-pose matrix for a bone, used to transform vertices from model space into bone-local space for skinning.)doc")
.def(py::init<>())
;
py::class_<whiteout::m3::AttachmentPoint>(m, "AttachmentPoint", R"doc(ATT_ — Attachment point (v0–v1, 20 bytes)
Named bone location used by the engine to attach effects, weapons, or other models to specific skeleton bones.)doc")
.def(py::init<>())
.def_readwrite("unknown", &whiteout::m3::AttachmentPoint::unknown, R"doc(Unknown field)doc")
.def_readwrite("name", &whiteout::m3::AttachmentPoint::name, R"doc(Attachment point name (Ref<CHAR>))doc")
.def_readwrite("bone_index", &whiteout::m3::AttachmentPoint::boneIndex, R"doc(Index into BONE array)doc")
;
py::class_<whiteout::m3::HitTestShape>(m, "HitTestShape", R"doc(SSGS — Hit-test shape (v0–v1, 108 bytes)
Defines a collision / selection volume (box, sphere, capsule, cylinder, or mesh) attached to a bone. Used for both tight and fuzzy hit testing.)doc")
.def(py::init<>())
.def_readwrite("shape_type", &whiteout::m3::HitTestShape::shapeType, R"doc(Shape type (box/sphere/capsule/cylinder/mesh). Defaulted because a conversion builds `MODL.tightHitTestObject` without ever assigning it, and an indeterminate enum wrote junk shape types into every export (`reference_m3_layer_stack_junk`, the same defect one field over). Sphere is what 2,222 of 2,448 shipped models state.)doc")
.def_readwrite("bone_index", &whiteout::m3::HitTestShape::boneIndex, R"doc(Index into BONE array)doc")
.def_readwrite("padding", &whiteout::m3::HitTestShape::padding, R"doc(Alignment padding)doc")
.def_readwrite("vertex_positions", &whiteout::m3::HitTestShape::vertexPositions, R"doc(Mesh vertex positions (VEC3, mesh type only))doc")
.def_readwrite("face_indices", &whiteout::m3::HitTestShape::faceIndices, R"doc(Mesh triangle indices (U16_, mesh type only))doc")
.def_readwrite("size_x", &whiteout::m3::HitTestShape::sizeX, R"doc(X dimension (radius for sphere/capsule))doc")
.def_readwrite("size_y", &whiteout::m3::HitTestShape::sizeY, R"doc(Y dimension (height for capsule/cylinder))doc")
.def_readwrite("size_z", &whiteout::m3::HitTestShape::sizeZ, R"doc(Z dimension)doc")
;
py::class_<whiteout::m3::AttachmentVolume>(m, "AttachmentVolume", R"doc(ATVL — Attachment volume (v0, 116 bytes)
Like HitTestShape but with two bone indices for attachment-point volumes.)doc")
.def(py::init<>())
.def_readwrite("bone1", &whiteout::m3::AttachmentVolume::bone1, R"doc(First bone index)doc")
.def_readwrite("bone2", &whiteout::m3::AttachmentVolume::bone2, R"doc(Second bone index)doc")
.def_readwrite("shape_type", &whiteout::m3::AttachmentVolume::shapeType, R"doc(Shape type)doc")
.def_readwrite("bone_index", &whiteout::m3::AttachmentVolume::boneIndex, R"doc(Primary bone index)doc")
.def_readwrite("padding", &whiteout::m3::AttachmentVolume::padding, R"doc(Alignment padding)doc")
.def_readwrite("vertex_positions", &whiteout::m3::AttachmentVolume::vertexPositions, R"doc(Mesh vertex positions (VEC3))doc")
.def_readwrite("face_indices", &whiteout::m3::AttachmentVolume::faceIndices, R"doc(Mesh triangle indices (U16_))doc")
.def_readwrite("size_x", &whiteout::m3::AttachmentVolume::sizeX, R"doc(X dimension)doc")
.def_readwrite("size_y", &whiteout::m3::AttachmentVolume::sizeY, R"doc(Y dimension)doc")
.def_readwrite("size_z", &whiteout::m3::AttachmentVolume::sizeZ, R"doc(Z dimension)doc")
;
py::class_<whiteout::m3::TriggerData>(m, "TriggerData", R"doc(TRGD — Trigger data (v0, 24 bytes)
Named trigger with associated data indices for gameplay events.)doc")
.def(py::init<>())
.def_readwrite("data_indices", &whiteout::m3::TriggerData::dataIndices, R"doc(Data index array (U32_))doc")
.def_readwrite("name", &whiteout::m3::TriggerData::name, R"doc(Trigger name (Ref<CHAR>))doc")
;
py::class_<whiteout::m3::TurretBehavior>(m, "TurretBehavior", R"doc(PATU — Turret behavior (v0–v4, 152 bytes)
Configures turret rotation constraints for a bone with yaw/pitch limits, weights, and an optional main-turret flag.)doc")
.def(py::init<>())
.def_readwrite("unknown1", &whiteout::m3::TurretBehavior::unknown1, R"doc(Unknown vector 1)doc")
.def_readwrite("unknown2", &whiteout::m3::TurretBehavior::unknown2, R"doc(Unknown vector 2)doc")
.def_readwrite("bone_index", &whiteout::m3::TurretBehavior::boneIndex, R"doc(Index into BONE array)doc")
.def_readwrite("use_as_main_turret", &whiteout::m3::TurretBehavior::useAsMainTurret, R"doc(Non-zero if this is the main turret)doc")
.def_readwrite("turret_group_id", &whiteout::m3::TurretBehavior::turretGroupId, R"doc(Turret group identifier)doc")
.def_readwrite("yaw_limited", &whiteout::m3::TurretBehavior::yawLimited, R"doc(Enable yaw limits)doc")
.def_readwrite("yaw_min", &whiteout::m3::TurretBehavior::yawMin, R"doc(Minimum yaw angle (radians))doc")
.def_readwrite("yaw_max", &whiteout::m3::TurretBehavior::yawMax, R"doc(Maximum yaw angle (radians))doc")
.def_readwrite("yaw_weight", &whiteout::m3::TurretBehavior::yawWeight, R"doc(Yaw rotation weight)doc")
.def_readwrite("pitch_limited", &whiteout::m3::TurretBehavior::pitchLimited, R"doc(Enable pitch limits)doc")
.def_readwrite("pitch_min", &whiteout::m3::TurretBehavior::pitchMin, R"doc(Minimum pitch angle (radians))doc")
.def_readwrite("pitch_max", &whiteout::m3::TurretBehavior::pitchMax, R"doc(Maximum pitch angle (radians))doc")
.def_readwrite("pitch_weight", &whiteout::m3::TurretBehavior::pitchWeight, R"doc(Pitch rotation weight)doc")
.def_readwrite("unknown3", &whiteout::m3::TurretBehavior::unknown3, R"doc(Unknown field)doc")
.def_readwrite("unknown4", &whiteout::m3::TurretBehavior::unknown4, R"doc(Unknown field)doc")
.def_readwrite("main_bone_offset", &whiteout::m3::TurretBehavior::mainBoneOffset, R"doc(Offset from main bone)doc")
;
py::class_<whiteout::m3::BillboardBehavior>(m, "BillboardBehavior", R"doc(BBSC — Billboard behavior (v0, 48 bytes)
Turns one bone to face the camera. The only source of billboarding there is: `BoneFlag::Billboard1/2` are set on no bone in the whole corpus.
StarCraft II applies these at draw time, per view, from `sub_10290A890` — `CBBSolver::Solve` is a stub — and writes the bone's *local* rotation, so the subtree follows.)doc")
.def(py::init<>())
.def_readwrite("dependents", &whiteout::m3::BillboardBehavior::dependents, R"doc(Dependent bone indices (U16_). Empty in every shipped record; the engine never reads them)doc")
.def_readwrite("bone_index", &whiteout::m3::BillboardBehavior::boneIndex, R"doc(Index into BONE array)doc")
.def_readwrite("billboard_type", &whiteout::m3::BillboardBehavior::billboardType, R"doc(Which axes may turn — see BillboardType)doc")
.def_readwrite("camera_look_at", &whiteout::m3::BillboardBehavior::cameraLookAt, R"doc(Non-zero: aim from this bone at the eye. Zero: aim along the camera's view direction instead, so every such bone shares one orientation)doc")
.def_readwrite("up", &whiteout::m3::BillboardBehavior::up, R"doc(MISNAMED: not a direction. A rotation applied *before* the billboard basis, and only by the axis-locked types 0/1/2)doc")
.def_readwrite("forward", &whiteout::m3::BillboardBehavior::forward, R"doc(MISNAMED likewise: the same kind of pre-rotation, taken only by type 6, and only on a bone whose parent is another bone. Types 3/4/5 take neither)doc")
;
py::class_<whiteout::m3::IKJoint>(m, "IKJoint", R"doc(IKJT — IK joint (v0, 32 bytes)
Inverse kinematics joint with raycast up/down range, max speed, and goal threshold for terrain-following or foot-planting.)doc")
.def(py::init<>())
.def_readwrite("dependents", &whiteout::m3::IKJoint::dependents, R"doc(Dependent bone indices (U16_))doc")
.def_readwrite("bone_index1", &whiteout::m3::IKJoint::boneIndex1, R"doc(First bone index)doc")
.def_readwrite("bone_index2", &whiteout::m3::IKJoint::boneIndex2, R"doc(Second bone index)doc")
.def_readwrite("raycast_up", &whiteout::m3::IKJoint::raycastUp, R"doc(Raycast upward distance (positive; shipped 1.5 / 3.0))doc")
.def_readwrite("raycast_down", &whiteout::m3::IKJoint::raycastDown, R"doc(Raycast downward offset, SIGNED (shipped -4.0 / -3.0): the surface window is [z + raycastDown, z + raycastUp])doc")
.def_readwrite("max_speed", &whiteout::m3::IKJoint::maxSpeed, R"doc(Maximum IK solving speed)doc")
.def_readwrite("goal_threshold", &whiteout::m3::IKJoint::goalThreshold, R"doc(Goal distance threshold)doc")
;
py::class_<whiteout::m3::IKTwoJoint>(m, "IKTwoJoint", R"doc(IK2J — Two-joint IK solver (v0, 48 bytes)
Classic two-bone IK (e.g. elbow/knee) with hinge axis, angle limits, and search range for target acquisition.)doc")
.def(py::init<>())
.def_readwrite("dependents", &whiteout::m3::IKTwoJoint::dependents, R"doc(Dependent bone indices (U16_))doc")
.def_readwrite("bone_base", &whiteout::m3::IKTwoJoint::boneBase, R"doc(Base bone (e.g. upper arm/thigh))doc")
.def_readwrite("bone_target", &whiteout::m3::IKTwoJoint::boneTarget, R"doc(Target bone (e.g. forearm/shin))doc")
.def_readwrite("bone_end", &whiteout::m3::IKTwoJoint::boneEnd, R"doc(End effector bone (e.g. hand/foot))doc")
.def_readwrite("padding", &whiteout::m3::IKTwoJoint::padding, R"doc(Alignment padding)doc")
.def_readwrite("hinge_axis", &whiteout::m3::IKTwoJoint::hingeAxis, R"doc(Hinge rotation axis)doc")
.def_readwrite("max_angle_inner", &whiteout::m3::IKTwoJoint::maxAngleInner, R"doc(Maximum inner angle)doc")
.def_readwrite("max_angle_outer", &whiteout::m3::IKTwoJoint::maxAngleOuter, R"doc(Maximum outer angle)doc")
.def_readwrite("search_up", &whiteout::m3::IKTwoJoint::searchUp, R"doc(Search range upward)doc")
.def_readwrite("search_down", &whiteout::m3::IKTwoJoint::searchDown, R"doc(Search range downward)doc")
;
py::class_<whiteout::m3::IKCCD>(m, "IKCCD", R"doc(IKCC — CCD IK solver (v0, 24 bytes)
Cyclic Coordinate Descent IK solver with base/target bones and vertical search range.)doc")
.def(py::init<>())
.def_readwrite("dependents", &whiteout::m3::IKCCD::dependents, R"doc(Dependent bone indices (U16_))doc")
.def_readwrite("bone_base", &whiteout::m3::IKCCD::boneBase, R"doc(Base bone index)doc")
.def_readwrite("bone_target", &whiteout::m3::IKCCD::boneTarget, R"doc(Target bone index)doc")
.def_readwrite("search_up", &whiteout::m3::IKCCD::searchUp, R"doc(Search range upward)doc")
.def_readwrite("search_down", &whiteout::m3::IKCCD::searchDown, R"doc(Search range downward)doc")
;
py::class_<whiteout::m3::OneBoneSolver>(m, "OneBoneSolver", R"doc(PAOB — One-bone IK solver (v0, 24 bytes)
Simple single-bone orientation solver with angle limit and fallback bone.)doc")
.def(py::init<>())
.def_readwrite("dependents", &whiteout::m3::OneBoneSolver::dependents, R"doc(Dependent bone indices (U16_))doc")
.def_readwrite("bone", &whiteout::m3::OneBoneSolver::bone, R"doc(Primary bone index)doc")
.def_readwrite("bone_fallback", &whiteout::m3::OneBoneSolver::boneFallback, R"doc(Fallback bone index)doc")
.def_readwrite("max_angle", &whiteout::m3::OneBoneSolver::maxAngle, R"doc(Maximum rotation angle)doc")
;
py::class_<whiteout::m3::ShadowBox>(m, "ShadowBox", R"doc(SHBX — Shadow box (v0, 64 bytes)
Axis-aligned shadow volume defined by a 4×4 transform matrix.)doc")
.def(py::init<>())
;
py::class_<whiteout::m3::ViewVolume>(m, "ViewVolume", R"doc(VVOL — View volume (v0, 40 bytes)
Animated visibility volume bound to a bone, used for culling decisions.)doc")
.def(py::init<>())
.def_readwrite("node_index", &whiteout::m3::ViewVolume::nodeIndex, R"doc(Index into BONE array)doc")
.def_readwrite("size", &whiteout::m3::ViewVolume::size, R"doc(Animated half-extents (36 bytes))doc")
;
py::class_<whiteout::m3::TrailingModel>(m, "TrailingModel", R"doc(TMD_ — Trailing model (v0–v1, defunct)
Legacy trailing model data. Observed in older files but no longer actively used by the engine.)doc")
.def(py::init<>())
.def_readwrite("vectors", &whiteout::m3::TrailingModel::vectors, R"doc(Control vectors (VEC3))doc")
.def_readwrite("param0", &whiteout::m3::TrailingModel::param0, R"doc(Parameter 0 (observed: 5.0))doc")
.def_readwrite("param1", &whiteout::m3::TrailingModel::param1, R"doc(Parameter 1 (observed: 1.0))doc")
.def_readwrite("anim_float0", &whiteout::m3::TrailingModel::animFloat0, R"doc(Animated float 0 (init 0.5))doc")
.def_readwrite("anim_float1", &whiteout::m3::TrailingModel::animFloat1, R"doc(Animated float 1 (init 1.0))doc")
.def_readwrite("flag", &whiteout::m3::TrailingModel::flag, R"doc(Flag (observed: 1))doc")
.def_readwrite("reserved0", &whiteout::m3::TrailingModel::reserved0, R"doc(Reserved)doc")
.def_readwrite("reserved1", &whiteout::m3::TrailingModel::reserved1, R"doc(Reserved)doc")
;
py::class_<whiteout::m3::Force>(m, "Force", R"doc(FOR_ — Force field (v0–v2, 104 bytes)
Pushes particles and ribbons (flag 0x8) and rigid bodies (flag 0x10) inside an influence volume. A body is affected when its `localForces | worldForces << 16` mask shares a bit with `localChannels`.)doc")
.def(py::init<>())
.def_readwrite("force_type", &whiteout::m3::Force::forceType, R"doc(Force kind)doc")
.def_readwrite("force_shape", &whiteout::m3::Force::forceShape, R"doc(Influence volume shape)doc")
.def_readwrite("unknown", &whiteout::m3::Force::unknown, R"doc(Read as local/world scope; no reader traced yet)doc")
.def_readwrite("bone_index", &whiteout::m3::Force::boneIndex, R"doc(Index into BONE array)doc")
.def_readwrite("flags", &whiteout::m3::Force::flags, R"doc(Falloff, height gradient, unbounded, targets)doc")
.def_readwrite("local_channels", &whiteout::m3::Force::localChannels, R"doc(Channel mask matched against body and emitter masks)doc")
.def_readwrite("strength", &whiteout::m3::Force::strength, R"doc(Animated force strength)doc")
.def_readwrite("width", &whiteout::m3::Force::width, R"doc(Animated influence width)doc")
.def_readwrite("height", &whiteout::m3::Force::height, R"doc(Animated influence height)doc")
.def_readwrite("length", &whiteout::m3::Force::length, R"doc(Animated influence length)doc")
;
py::class_<whiteout::m3::Warp>(m, "Warp", R"doc(WRP_ — Vertex warp (v1, 132 bytes)
A vertex-shader deformation particles and ribbons opt into. The client refuses a v0 record, so the parser drops one.)doc")
.def(py::init<>())
.def_readwrite("warp_type", &whiteout::m3::Warp::warpType, R"doc(Warp type)doc")
.def_readwrite("bone_index", &whiteout::m3::Warp::boneIndex, R"doc(Index into BONE array)doc")
.def_readwrite("unknown", &whiteout::m3::Warp::unknown, R"doc(Unknown field)doc")
.def_readwrite("radius", &whiteout::m3::Warp::radius, R"doc(Animated warp radius)doc")
.def_readwrite("height", &whiteout::m3::Warp::height, R"doc(Animated warp height)doc")
.def_readwrite("strength", &whiteout::m3::Warp::strength, R"doc(Animated warp strength)doc")
.def_readwrite("angular", &whiteout::m3::Warp::angular, R"doc(Animated angular component)doc")
.def_readwrite("axial", &whiteout::m3::Warp::axial, R"doc(Animated axial component)doc")
.def_readwrite("radial", &whiteout::m3::Warp::radial, R"doc(Animated radial component)doc")
;
py::class_<whiteout::m3::ConvexHullHalfEdge>(m, "ConvexHullHalfEdge", R"doc(DMSE — Convex hull half-edge (v0, 4 bytes)
Entries come in consecutive twin pairs: an even entry's twin is the next one (`twinOffset` +1), an odd entry's the previous (-1).)doc")
.def(py::init<>())
.def_readwrite("twin_offset", &whiteout::m3::ConvexHullHalfEdge::twinOffset, R"doc(+1 on the even entry of a pair, -1 on the odd one)doc")
.def_readwrite("origin_vertex", &whiteout::m3::ConvexHullHalfEdge::originVertex, R"doc(Vertex the half-edge leaves)doc")
.def_readwrite("face", &whiteout::m3::ConvexHullHalfEdge::face, R"doc(Face the half-edge borders)doc")
.def_readwrite("next_in_face", &whiteout::m3::ConvexHullHalfEdge::nextInFace, R"doc(Next half-edge around the same face)doc")
;
py::class_<whiteout::m3::PhysicsMeshBvhNode>(m, "PhysicsMeshBvhNode", R"doc(DMMN — Physics mesh BVH node (v0: 12 bytes, v1: 8 bytes)
The SC2 5.0 client never reads DMMN: it rebuilds each mesh's tree at load and then takes the tree's centre, extent, tolerance and height from the PHSH. The cooker (physics_cook.h) writes none. Kept so a shipped v3 mesh reads and writes back whole.
**v1** (8 bytes per node) — octahedral-encoded normal + quantized slab bounds: - i16 octX, octY: octahedral-mapped slab normal (snorm16 pair) - u16 slabMin, slabMax: quantized bounding-slab distances along the normal
**v0** (Havok era, 12 bytes per node) is a plain normal; only v2 meshes carry it, and those are rebuilt by the upgrade.)doc")
.def(py::init<>())
;
py::class_<whiteout::m3::PhysicsMeshTriangle>(m, "PhysicsMeshTriangle", R"doc(DMMT — Havok-era mesh triangle (v0, 28 bytes)
Only a v2 PHSH references it; the upgrade keeps the three vertex indices.)doc")
.def(py::init<>())
.def_readwrite("vertex_index0", &whiteout::m3::PhysicsMeshTriangle::vertexIndex0, R"doc(First vertex index)doc")
.def_readwrite("vertex_index1", &whiteout::m3::PhysicsMeshTriangle::vertexIndex1, R"doc(Second vertex index)doc")
.def_readwrite("vertex_index2", &whiteout::m3::PhysicsMeshTriangle::vertexIndex2, R"doc(Third vertex index)doc")
.def_readwrite("edge_index0", &whiteout::m3::PhysicsMeshTriangle::edgeIndex0, R"doc(First edge index)doc")
.def_readwrite("edge_index1", &whiteout::m3::PhysicsMeshTriangle::edgeIndex1, R"doc(Second edge index)doc")
.def_readwrite("edge_index2", &whiteout::m3::PhysicsMeshTriangle::edgeIndex2, R"doc(Third edge index)doc")
.def_readwrite("reserved", &whiteout::m3::PhysicsMeshTriangle::reserved, R"doc(Reserved)doc")
.def_readwrite("flags", &whiteout::m3::PhysicsMeshTriangle::flags, R"doc(Triangle flags)doc")
;
py::class_<whiteout::m3::PhysicsMeshEdge>(m, "PhysicsMeshEdge", R"doc(DMME — Havok-era mesh edge (v0, 20 bytes)
Only a v2 PHSH references it, and the upgrade discards it.)doc")
.def(py::init<>())
.def_readwrite("edge_type", &whiteout::m3::PhysicsMeshEdge::edgeType, R"doc(Edge type)doc")
.def_readwrite("vertex_a", &whiteout::m3::PhysicsMeshEdge::vertexA, R"doc(First vertex index)doc")
.def_readwrite("vertex_b", &whiteout::m3::PhysicsMeshEdge::vertexB, R"doc(Second vertex index)doc")
.def_readwrite("face_a", &whiteout::m3::PhysicsMeshEdge::faceA, R"doc(First adjacent face)doc")
.def_readwrite("face_b", &whiteout::m3::PhysicsMeshEdge::faceB, R"doc(Second adjacent face)doc")
;
py::class_<whiteout::m3::PhysicsShape>(m, "PhysicsShape", R"doc(PHSH — Physics shape (v3, 300 bytes; v0 96, v1 132 and v2 292 read)
Bytes 0–103 are common: the matrix, the kind, the two source Refs and the dimensions. Bytes 104–183 are the cooked convex hull (kind 4) and 184–299 the cooked mesh (kind 5).
**Source vs cooked.** `sourcePoints`/`sourceTriangles` (+68/+80) are raw input the client cooks at load, with the matrix baked in: a hull from the points, a mesh from both. Only the upgrade of a v0/v1 shape fills them, and `UpgradePhysics` cooks them the same way, so a parsed shape carries the cooked tables and empty sources.
**Hull tables** are used directly as a Domino polytope: the counts at +164/+168/+172 rather than the Ref counts, the volume and surface area as cached mass data (buoyancy; mass under a physics-material override).
**Mesh tables.** The client rebuilds the tree from the vertices and the three indices of each triangle (plus the low byte of its seventh value), then overwrites the tree's centre, extent, tolerance and height with this record's, so those four must be what its builder computes (`CookMesh`). DMMN and the adjacency are never read.)doc")
.def(py::init<>())
.def_readwrite("shape_type", &whiteout::m3::PhysicsShape::shapeType, R"doc(Shape type (box/sphere/capsule/cylinder/hull/mesh))doc")
.def_readwrite("source_points", &whiteout::m3::PhysicsShape::sourcePoints, R"doc(Uncooked points (VEC3, +68), matrix not yet applied)doc")
.def_readwrite("source_triangles", &whiteout::m3::PhysicsShape::sourceTriangles, R"doc(Uncooked triangle list (U16_, +80), three per face)doc")
.def_readwrite("shape_dimensions", &whiteout::m3::PhysicsShape::shapeDimensions, R"doc(Box half-extents; sphere radius; capsule/cylinder radius, length)doc")
.def_readwrite("hull_vertices", &whiteout::m3::PhysicsShape::hullVertices, R"doc(Vertex positions (VEC3))doc")
.def_readwrite("hull_planes", &whiteout::m3::PhysicsShape::hullPlanes, R"doc(Face planes (n, d), n unit length (VEC4))doc")
.def_readwrite("hull_half_edges", &whiteout::m3::PhysicsShape::hullHalfEdges, R"doc(Half-edge table (DMSE), twin pairs)doc")
.def_readwrite("hull_face_first_edges", &whiteout::m3::PhysicsShape::hullFaceFirstEdges, R"doc(Each face's first half-edge (U8__))doc")
.def_readwrite("hull_centroid", &whiteout::m3::PhysicsShape::hullCentroid, R"doc(Volume centroid)doc")
.def_readwrite("hull_vertex_count", &whiteout::m3::PhysicsShape::hullVertexCount, R"doc(Vertices the client reads)doc")
.def_readwrite("hull_face_count", &whiteout::m3::PhysicsShape::hullFaceCount, R"doc(Faces the client reads)doc")
.def_readwrite("hull_half_edge_count", &whiteout::m3::PhysicsShape::hullHalfEdgeCount, R"doc(Half-edges the client reads)doc")
.def_readwrite("hull_volume", &whiteout::m3::PhysicsShape::hullVolume, R"doc(Enclosed volume)doc")
.def_readwrite("hull_surface_area", &whiteout::m3::PhysicsShape::hullSurfaceArea, R"doc(Surface area)doc")
.def_readwrite("mesh_bvh_nodes", &whiteout::m3::PhysicsShape::meshBvhNodes, R"doc(BVH tree nodes (DMMN), never read)doc")
.def_readwrite("mesh_vertex_positions", &whiteout::m3::PhysicsShape::meshVertexPositions, R"doc(Vertex positions, w=0 (VEC4))doc")
.def_readwrite("mesh_bounds_center", &whiteout::m3::PhysicsShape::meshBoundsCenter, R"doc(Tree centre, as the client's builder computes it)doc")
.def_readwrite("mesh_bounds_extent", &whiteout::m3::PhysicsShape::meshBoundsExtent, R"doc(Tree half-extent, likewise)doc")
.def_readwrite("mesh_tolerance", &whiteout::m3::PhysicsShape::meshTolerance, R"doc(Per-axis quantization step (= extent / 32767))doc")
.def_readwrite("mesh_normal_count", &whiteout::m3::PhysicsShape::meshNormalCount, R"doc(DMMN count)doc")
.def_readwrite("mesh_vertex_count", &whiteout::m3::PhysicsShape::meshVertexCount, R"doc(Number of mesh vertices)doc")
.def_readwrite("mesh_face_index16_count", &whiteout::m3::PhysicsShape::meshFaceIndex16Count, R"doc(MT16 face count (0 when MT32); the client reads this, not the Ref)doc")
.def_readwrite("mesh_face_index32_count", &whiteout::m3::PhysicsShape::meshFaceIndex32Count, R"doc(MT32 face count (0 when MT16))doc")
.def_readwrite("mesh_unknown1", &whiteout::m3::PhysicsShape::meshUnknown1, R"doc(Never read)doc")
.def_readwrite("mesh_reserved", &whiteout::m3::PhysicsShape::meshReserved, R"doc(Never read)doc")
.def_readwrite("mesh_tree_depth", &whiteout::m3::PhysicsShape::meshTreeDepth, R"doc(Tree height, as the client's builder computes it)doc")
.def_readwrite("mesh_collision_margin", &whiteout::m3::PhysicsShape::meshCollisionMargin, R"doc(Never read)doc")
;
py::class_<whiteout::m3::RigidBody>(m, "RigidBody", R"doc(PHRB — Rigid body (v4, 80 bytes; v0 72, v1 96, v2 104 and v3 56 read)
A Domino body on `parentBoneIndex`, with its shapes. `simulationType` is how the body is created; `dynamicState` whether it simulates at a moment.)doc")
.def(py::init<>())
.def_readwrite("simulation_type", &whiteout::m3::RigidBody::simulationType, R"doc(Creation type: 0 dynamic, 1 kinematic, 2 static)doc")
.def_readwrite("parent_bone_index", &whiteout::m3::RigidBody::parentBoneIndex, R"doc(Parent bone index)doc")
.def_readwrite("physics_type", &whiteout::m3::RigidBody::physicsType, R"doc(Physics-material id game data may override)doc")
.def_readwrite("density", &whiteout::m3::RigidBody::density, R"doc(Body density)doc")
.def_readwrite("friction", &whiteout::m3::RigidBody::friction, R"doc(Surface friction)doc")
.def_readwrite("restitution", &whiteout::m3::RigidBody::restitution, R"doc(Elasticity / bounciness)doc")
.def_readwrite("linear_damping", &whiteout::m3::RigidBody::linearDamping, R"doc(Linear velocity damping)doc")
.def_readwrite("angular_damping", &whiteout::m3::RigidBody::angularDamping, R"doc(Angular velocity damping)doc")
.def_readwrite("inertia_scale", &whiteout::m3::RigidBody::inertiaScale, R"doc(Domino inertia scale (gravity scale is fixed at 1))doc")
.def_readwrite("dynamic_state", &whiteout::m3::RigidBody::dynamicState, R"doc(Simulates now; sampled only when flag bit 1 is set)doc")
.def_readwrite("dynamic_blend_out", &whiteout::m3::RigidBody::dynamicBlendOut, R"doc(Never read)doc")
.def_readwrite("rigid_body_shape", &whiteout::m3::RigidBody::rigidBodyShape, R"doc(Collision shapes (PHSH))doc")
.def_readwrite("flags", &whiteout::m3::RigidBody::flags, R"doc(Rigid body flags)doc")
.def_readwrite("local_forces", &whiteout::m3::RigidBody::localForces, R"doc(Local force channel bitmask)doc")
.def_readwrite("world_forces", &whiteout::m3::RigidBody::worldForces, R"doc(World force channel bitmask)doc")
.def_readwrite("priority", &whiteout::m3::RigidBody::priority, R"doc(Never read)doc")
;
py::class_<whiteout::m3::PhysicsJoint>(m, "PhysicsJoint", R"doc(PHYJ — Physics joint (v0, 180 bytes)
Joins the first body on each of two bones. Angles are radians. `enableLimits` and `enableFriction` are bytes to the client; the upper three bytes are never read.)doc")
.def(py::init<>())
.def_readwrite("joint_type", &whiteout::m3::PhysicsJoint::jointType, R"doc(0 spherical, 1 revolute, 2 cone-twist, 3 weld)doc")
.def_readwrite("bone_index1", &whiteout::m3::PhysicsJoint::boneIndex1, R"doc(First bone index)doc")
.def_readwrite("bone_index2", &whiteout::m3::PhysicsJoint::boneIndex2, R"doc(Second bone index)doc")
.def_readwrite("enable_limits", &whiteout::m3::PhysicsJoint::enableLimits, R"doc(Enable angular limits (low byte))doc")
.def_readwrite("limit_min", &whiteout::m3::PhysicsJoint::limitMin, R"doc(Minimum limit angle)doc")
.def_readwrite("limit_max", &whiteout::m3::PhysicsJoint::limitMax, R"doc(Maximum limit angle)doc")
.def_readwrite("cone_angle", &whiteout::m3::PhysicsJoint::coneAngle, R"doc(Cone constraint angle)doc")
.def_readwrite("enable_friction", &whiteout::m3::PhysicsJoint::enableFriction, R"doc(Enable joint friction (low byte))doc")
.def_readwrite("friction", &whiteout::m3::PhysicsJoint::friction, R"doc(Multiplier on an estimated gravity-holding torque)doc")
.def_readwrite("damping_ratio", &whiteout::m3::PhysicsJoint::dampingRatio, R"doc(Weld spring damping ratio)doc")
.def_readwrite("angular_frequency", &whiteout::m3::PhysicsJoint::angularFrequency, R"doc(Weld spring frequency)doc")
.def_readwrite("break_threshold", &whiteout::m3::PhysicsJoint::breakThreshold, R"doc(Never read)doc")
.def_readwrite("enable_shape", &whiteout::m3::PhysicsJoint::enableShape, R"doc(Collide connected)doc")
;
py::class_<whiteout::m3::ClothCollider>(m, "ClothCollider", R"doc(PHCC — Cloth collider (v0, 76 bytes)
A capsule along its own +Z, centred, on `bone`.)doc")
.def(py::init<>())
.def_readwrite("radius", &whiteout::m3::ClothCollider::radius, R"doc(Capsule radius)doc")
.def_readwrite("height", &whiteout::m3::ClothCollider::height, R"doc(Capsule full length)doc")
.def_readwrite("bone", &whiteout::m3::ClothCollider::bone, R"doc(Bone index; 0xFFFF is the model root)doc")
;
py::class_<whiteout::m3::ClothProxy>(m, "ClothProxy", R"doc(PHAC — Cloth proxy (v0, 32 bytes)
Binds one cloth-influenced region to its cage: per vertex of `clothIndex`, four cage-local `u16` lanes packed in a `u64` and four byte weights (/255) packed in a `u32`.)doc")
.def(py::init<>())
.def_readwrite("proxy_index", &whiteout::m3::ClothProxy::proxyIndex, R"doc(The cage's region (REGN index))doc")
.def_readwrite("cloth_index", &whiteout::m3::ClothProxy::clothIndex, R"doc(The bound region (REGN index))doc")
.def_readwrite("proxy_vertices", &whiteout::m3::ClothProxy::proxyVertices, R"doc(Four cage vertices per bound vertex (U64_))doc")
.def_readwrite("proxy_weights", &whiteout::m3::ClothProxy::proxyWeights, R"doc(Four byte weights per bound vertex (U32_))doc")
;
py::class_<whiteout::m3::ClothPhysics>(m, "ClothPhysics", R"doc(PHCL — Cloth physics (v4, 192 bytes; v0 140, v1 116, v2 128 and v3 192 read)
One cloth: the cage region its particles are, per-particle anchors and movability, colliders, the regions it drives (PHAC) and the solver parameters. A record with colliders and no cage exports them to other models. Added in MODL v28.)doc")
.def(py::init<>())
.def_readwrite("cage_region", &whiteout::m3::ClothPhysics::cageRegion, R"doc(The cage's REGN index)doc")
.def_readwrite("skin_bone_count", &whiteout::m3::ClothPhysics::skinBoneCount, R"doc(Never read)doc")
.def_readwrite("skin_bones", &whiteout::m3::ClothPhysics::skinBones, R"doc(Bones the anchors and colliders use (U16_))doc")
.def_readwrite("sim_enabled", &whiteout::m3::ClothPhysics::simEnabled, R"doc(Per-particle flags, bit 0 movable (U8__))doc")
.def_readwrite("vertex_bones", &whiteout::m3::ClothPhysics::vertexBones, R"doc(Per-particle anchor bones, four bytes (U32_))doc")
.def_readwrite("vertex_weights", &whiteout::m3::ClothPhysics::vertexWeights, R"doc(Per-particle anchor weights, four bytes (U32_))doc")
.def_readwrite("colliders", &whiteout::m3::ClothPhysics::colliders, R"doc(Cloth colliders (PHCC))doc")
.def_readwrite("proxies", &whiteout::m3::ClothPhysics::proxies, R"doc(Cloth proxies (PHAC))doc")
.def_readwrite("density", &whiteout::m3::ClothPhysics::density, R"doc(Cloth density)doc")
.def_readwrite("tracking", &whiteout::m3::ClothPhysics::tracking, R"doc(Tracking factor)doc")
.def_readwrite("stretch_stiffness", &whiteout::m3::ClothPhysics::stretchStiffness, R"doc(Stretch stiffness)doc")
.def_readwrite("horizontal_stiffness", &whiteout::m3::ClothPhysics::horizontalStiffness, R"doc(Horizontal stiffness)doc")
.def_readwrite("bending_stiffness", &whiteout::m3::ClothPhysics::bendingStiffness, R"doc(Bending stiffness)doc")
.def_readwrite("damping", &whiteout::m3::ClothPhysics::damping, R"doc(Damping coefficient)doc")
.def_readwrite("friction", &whiteout::m3::ClothPhysics::friction, R"doc(Friction coefficient)doc")
.def_readwrite("gravity", &whiteout::m3::ClothPhysics::gravity, R"doc(Gravity influence)doc")
.def_readwrite("explosion_scale", &whiteout::m3::ClothPhysics::explosionScale, R"doc(Explosion force scale)doc")
.def_readwrite("wind_scale", &whiteout::m3::ClothPhysics::windScale, R"doc(Wind force scale)doc")
.def_readwrite("shear_stiffness", &whiteout::m3::ClothPhysics::shearStiffness, R"doc(Shear stiffness)doc")
.def_readwrite("drag_factor", &whiteout::m3::ClothPhysics::dragFactor, R"doc(Drag factor)doc")
.def_readwrite("lift_factor", &whiteout::m3::ClothPhysics::liftFactor, R"doc(Lift factor)doc")
.def_readwrite("sphere_stiffness", &whiteout::m3::ClothPhysics::sphereStiffness, R"doc(Sphere collider stiffness)doc")
.def_readwrite("flatten", &whiteout::m3::ClothPhysics::flatten, R"doc(Flatten mode)doc")
.def_readwrite("active", &whiteout::m3::ClothPhysics::active, R"doc(Animated active state; sampled only when flag bit 1 is set)doc")
.def_readwrite("use_skin_collision", &whiteout::m3::ClothPhysics::useSkinCollision, R"doc(Use skin mesh for collision)doc")
.def_readwrite("skin_offset", &whiteout::m3::ClothPhysics::skinOffset, R"doc(Skin collision offset)doc")
.def_readwrite("skin_exponent", &whiteout::m3::ClothPhysics::skinExponent, R"doc(Skin collision exponent)doc")
.def_readwrite("skin_stiffness", &whiteout::m3::ClothPhysics::skinStiffness, R"doc(Skin collision stiffness)doc")
.def_readwrite("local_channels", &whiteout::m3::ClothPhysics::localChannels, R"doc(Never read)doc")
.def_readwrite("local_wind", &whiteout::m3::ClothPhysics::localWind, R"doc(Local wind direction and magnitude)doc")
;
py::class_<whiteout::m3::Light>(m, "Light", R"doc(LITE — Light source (v0–v7, 212 bytes)
Omni, spot, or directional light with animated diffuse/specular colors, intensity, decay, attenuation start/end, and spot-light hot-spot/falloff.)doc")
.def(py::init<>())
.def_readwrite("light_type", &whiteout::m3::Light::lightType, R"doc(Light type (omni/spot/directional))doc")
.def_readwrite("bone_index", &whiteout::m3::Light::boneIndex, R"doc(Index into BONE array)doc")
.def_readwrite("flags", &whiteout::m3::Light::flags, R"doc(Light flags (shadows, specular, AO, etc.))doc")
.def_readwrite("lod_cut", &whiteout::m3::Light::lodCut, R"doc(LOD cut-off level)doc")
.def_readwrite("shadow_lod_cut", &whiteout::m3::Light::shadowLodCut, R"doc(Shadow LOD cut-off level)doc")
.def_readwrite("diffuse_color", &whiteout::m3::Light::diffuseColor, R"doc(Animated diffuse color (RGB))doc")
.def_readwrite("intensity_multiplier", &whiteout::m3::Light::intensityMultiplier, R"doc(Animated intensity multiplier)doc")
.def_readwrite("specular_color", &whiteout::m3::Light::specularColor, R"doc(Animated specular color (RGB))doc")
.def_readwrite("specular_multiplier", &whiteout::m3::Light::specularMultiplier, R"doc(Animated specular multiplier)doc")
.def_readwrite("decay", &whiteout::m3::Light::decay, R"doc(Animated distance decay exponent)doc")
.def_readwrite("attenuation_end", &whiteout::m3::Light::attenuationEnd, R"doc(Attenuation end distance)doc")
.def_readwrite("attenuation_start", &whiteout::m3::Light::attenuationStart, R"doc(Animated attenuation start distance)doc")
.def_readwrite("hot_spot", &whiteout::m3::Light::hotSpot, R"doc(Animated spot inner cone angle)doc")
.def_readwrite("falloff", &whiteout::m3::Light::falloff, R"doc(Animated spot outer cone falloff)doc")
;
py::class_<whiteout::m3::Camera>(m, "Camera", R"doc(CAM_ — Camera (v2–v5, 144–264 bytes)
Bone-attached camera with animated FOV, clip planes, shadow clip distance, depth-of-field parameters, and version-dependent bokeh settings.)doc")
.def(py::init<>())
.def_readwrite("bone_index", &whiteout::m3::Camera::boneIndex, R"doc(Index into BONE array)doc")
.def_readwrite("name", &whiteout::m3::Camera::name, R"doc(Camera name (Ref<CHAR>))doc")
.def_readwrite("field_of_view", &whiteout::m3::Camera::fieldOfView, R"doc(Animated FOV in radians (v2+))doc")
.def_readwrite("use_vertical_fov", &whiteout::m3::Camera::useVerticalFOV, R"doc(Use vertical FOV (0 or 1, v2+))doc")
.def_readwrite("dof_type", &whiteout::m3::Camera::dofType, R"doc(DOF type (v5 only, default 3))doc")
.def_readwrite("far_clip", &whiteout::m3::Camera::farClip, R"doc(Animated far clip plane (v3+))doc")
.def_readwrite("near_clip", &whiteout::m3::Camera::nearClip, R"doc(Animated near clip plane (v3+))doc")
.def_readwrite("shadow_clip_distance", &whiteout::m3::Camera::shadowClipDistance, R"doc(Animated shadow clip distance (v2+))doc")
.def_readwrite("focus_distance", &whiteout::m3::Camera::focusDistance, R"doc(Animated DOF focal point distance (v2+))doc")
.def_readwrite("far_focus_range", &whiteout::m3::Camera::farFocusRange, R"doc(Animated DOF far focus range (v2+))doc")
.def_readwrite("near_focus_range", &whiteout::m3::Camera::nearFocusRange, R"doc(Animated DOF near focus range (v2+))doc")
.def_readwrite("near_falloff_start", &whiteout::m3::Camera::nearFalloffStart, R"doc(Animated near falloff start (v4+))doc")
.def_readwrite("near_falloff_end", &whiteout::m3::Camera::nearFalloffEnd, R"doc(Animated near falloff end (v4+))doc")
.def_readwrite("dof_amount", &whiteout::m3::Camera::dofAmount, R"doc(Animated DOF strength (v2+))doc")
.def_readwrite("bokeh_f_stop", &whiteout::m3::Camera::bokehFStop, R"doc(Animated bokeh f-stop (v5+))doc")
.def_readwrite("bokeh_max_co_c_diameter", &whiteout::m3::Camera::bokehMaxCoCDiameter, R"doc(Animated bokeh max CoC diameter (v5+))doc")
;
py::class_<whiteout::m3::Model>(m, "Model", R"doc(MODL — Model root chunk (v23–v30, 784–868 bytes)
The root of all model data. Contains Ref<T> fields pointing to every sub-chunk in the file: skeleton, mesh, materials, particles, physics, etc. The preamble (bytes 0x000–0x0E3) is identical across all versions; version-dependent material and physics references follow at 0x0E4+.
Version history: - v23 (784 bytes): Base release layout - v24 (+ikCCD): 796 bytes - v25 (+volumeNoiseMaterials): 808 bytes - v26 (+stbMaterials): 820 bytes - v28 (+reflectionMaterials, +clothPhysics): 844 bytes - v29 (+lensFlareMaterials): 856 bytes - v30 (+dataDrivenMaterials): 868 bytes)doc")
.def(py::init<>())
.def_readwrite("name", &whiteout::m3::Model::name, R"doc(Model file path (Ref<CHAR>))doc")
.def_readwrite("flags", &whiteout::m3::Model::flags, R"doc(Not `None`: only 21 of the corpus's 56,146 models leave this at zero. These three are latches saying "this work is already done, do not redo it", and the converter does all three -- it sorts every `STC_`'s animIds, states `kAnimRefBound` on every bound AnimRef, and derives every `BONE.flags` from those (`m3_anim::SolveBoneAnimFlags`). The rest of the shipped bits are left clear so the editor recomputes them. A parsed or restored model overwrites this wholesale.)doc")
.def_readwrite("sequences", &whiteout::m3::Model::sequences, R"doc(Animation sequences (SEQS))doc")
.def_readwrite("sub_track_collections", &whiteout::m3::Model::subTrackCollections, R"doc(Sub-track containers (STC_) with keyframe refs)doc")
.def_readwrite("animation_groups", &whiteout::m3::Model::animationGroups, R"doc(Animation groups (STG_))doc")
.def_readwrite("bone_animation_sets", &whiteout::m3::Model::boneAnimationSets, R"doc(Bone animation sets (BSET, always null))doc")
.def_readwrite("animation_split_count", &whiteout::m3::Model::animationSplitCount, R"doc(Always 0)doc")
.def_readwrite("animation_states", &whiteout::m3::Model::animationStates, R"doc(Animation states (STS_))doc")
.def_readwrite("bones", &whiteout::m3::Model::bones, R"doc(Skeleton bones (BONE))doc")
.def_readwrite("skin_bone_count", &whiteout::m3::Model::skinBoneCount, R"doc(Number of bones affecting skin)doc")
.def_readwrite("divisions", &whiteout::m3::Model::divisions, R"doc(Mesh divisions (DIV_: faces, regions, batches))doc")
.def_readwrite("bone_lookup", &whiteout::m3::Model::boneLookup, R"doc(Bone index remap table (U16_))doc")
.def_readwrite("bounds", &whiteout::m3::Model::bounds, R"doc(Model bounding volume)doc")
.def_readwrite("collision_bounds", &whiteout::m3::Model::collisionBounds, R"doc(Collision bounding volume)doc")
.def_readwrite("collision_faces", &whiteout::m3::Model::collisionFaces, R"doc(Collision triangle indices (U16_))doc")
.def_readwrite("collision_verts", &whiteout::m3::Model::collisionVerts, R"doc(Collision vertex positions (VEC3))doc")
.def_readwrite("collision_normals", &whiteout::m3::Model::collisionNormals, R"doc(Collision face normals (VEC3))doc")
.def_readwrite("attachment_points", &whiteout::m3::Model::attachmentPoints, R"doc(Named bone locations (ATT_))doc")
.def_readwrite("attachment_point_addons", &whiteout::m3::Model::attachmentPointAddons, R"doc(Attachment point addon indices (U16_))doc")
.def_readwrite("lights", &whiteout::m3::Model::lights, R"doc(Lights (LITE))doc")
.def_readwrite("shadow_boxes", &whiteout::m3::Model::shadowBoxes, R"doc(Shadow boxes (SHBX))doc")
.def_readwrite("cameras", &whiteout::m3::Model::cameras, R"doc(Cameras (CAM_))doc")
.def_readwrite("cameras_addons", &whiteout::m3::Model::camerasAddons, R"doc(Camera addon indices (U16_))doc")
.def_readwrite("material_maps", &whiteout::m3::Model::materialMaps, R"doc(Material type+index maps (MATM))doc")
.def_readwrite("standard_materials", &whiteout::m3::Model::standardMaterials, R"doc(Standard materials (MAT_))doc")
.def_readwrite("displacement_materials", &whiteout::m3::Model::displacementMaterials, R"doc(Displacement materials (DIS_))doc")
.def_readwrite("composite_materials", &whiteout::m3::Model::compositeMaterials, R"doc(Composite materials (CMP_))doc")
.def_readwrite("terrain_materials", &whiteout::m3::Model::terrainMaterials, R"doc(Terrain materials (TER_))doc")
.def_readwrite("volume_materials", &whiteout::m3::Model::volumeMaterials, R"doc(Volume materials (VOL_))doc")
.def_readwrite("hair_materials", &whiteout::m3::Model::hairMaterials, R"doc(Hair materials (HAI_, defunct — always null))doc")
.def_readwrite("creep_materials", &whiteout::m3::Model::creepMaterials, R"doc(Creep materials (CREP))doc")
.def_readwrite("volume_noise_materials", &whiteout::m3::Model::volumeNoiseMaterials, R"doc(Volume noise materials (VON_, v25+))doc")
.def_readwrite("stb_materials", &whiteout::m3::Model::stbMaterials, R"doc(Splat terrain bake materials (STBM, v26+))doc")
.def_readwrite("reflection_materials", &whiteout::m3::Model::reflectionMaterials, R"doc(Reflection materials (REF_, v28+))doc")
.def_readwrite("lens_flare_materials", &whiteout::m3::Model::lensFlareMaterials, R"doc(Lens flare materials (LFLR, v29+))doc")
.def_readwrite("data_driven_materials", &whiteout::m3::Model::dataDrivenMaterials, R"doc(Data-driven materials (MADD, v30+))doc")
.def_readwrite("particle_emitters", &whiteout::m3::Model::particleEmitters, R"doc(Particle emitters (PAR_))doc")
.def_readwrite("particle_emitter_copies", &whiteout::m3::Model::particleEmitterCopies, R"doc(Particle emitter copies (PARC))doc")
.def_readwrite("ribbon_emitters", &whiteout::m3::Model::ribbonEmitters, R"doc(Ribbon emitters (RIB_))doc")
.def_readwrite("projections", &whiteout::m3::Model::projections, R"doc(Projectors / decals (PROJ))doc")