├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── berrybush
├── __init__.py
├── blender
│ ├── __init__.py
│ ├── animpreview.py
│ ├── backup.py
│ ├── bone.py
│ ├── brresexport.py
│ ├── brresimport.py
│ ├── common.py
│ ├── icons
│ │ ├── rgb.png
│ │ ├── val_0_8.png
│ │ ├── val_1_8.png
│ │ ├── val_2_8.png
│ │ ├── val_3_8.png
│ │ ├── val_4_8.png
│ │ ├── val_5_8.png
│ │ ├── val_6_8.png
│ │ ├── val_7_8.png
│ │ └── val_8_8.png
│ ├── limiter.py
│ ├── material.py
│ ├── mesh.py
│ ├── preferences.py
│ ├── proputils.py
│ ├── render.py
│ ├── scene.py
│ ├── shaders.py
│ ├── shaders
│ │ ├── fragment.glsl
│ │ ├── postprocess
│ │ │ ├── fragment.glsl
│ │ │ └── vertex.glsl
│ │ └── vertex.glsl
│ ├── shaderstruct.py
│ ├── tev.py
│ ├── texture.py
│ ├── updater.py
│ └── verify.py
└── wii
│ ├── __init__.py
│ ├── alias.py
│ ├── animation.py
│ ├── binaryutils.py
│ ├── bitstruct.py
│ ├── brres.py
│ ├── brresdict.py
│ ├── chr0.py
│ ├── clr0.py
│ ├── common.py
│ ├── dxt1lookups.py
│ ├── gx.py
│ ├── hermite.py
│ ├── mdl0.py
│ ├── pat0.py
│ ├── plt0.py
│ ├── serialization.py
│ ├── srt0.py
│ ├── subfile.py
│ ├── tex0.py
│ ├── transform.py
│ └── vis0.py
├── docs
├── animation-merge-example.png
├── animation-nla-access.png
├── backup-options.png
├── blending-discarding.png
├── color-alpha.png
├── color-regs.png
├── color-swap-brga-input.png
├── color-swap-brga-output.png
├── color-swap-brga.png
├── color-swap-rgba.png
├── color-swap-table.png
├── depth.png
├── export.png
├── format-example.png
├── getting-started
│ ├── 1
│ │ ├── blank-scene.png
│ │ ├── brawlbox-export.png
│ │ ├── brawlbox-replace.png
│ │ ├── cactus-pyramid.png
│ │ ├── developer-extras.png
│ │ ├── dolphin-first-export.png
│ │ ├── dolphin-setting.png
│ │ ├── dump-filtered.png
│ │ ├── dump.png
│ │ ├── front-view-lm-grayscale.png
│ │ ├── front-view.png
│ │ ├── full-glory.png
│ │ ├── image-editor-lm.png
│ │ ├── image-editor.png
│ │ ├── image-format-i8.png
│ │ ├── imported.png
│ │ ├── info-log.png
│ │ ├── lm-colored.png
│ │ ├── material-settings.png
│ │ ├── obj-yikes.png
│ │ ├── pad-to.png
│ │ ├── pyramid-flipped-with-texture.png
│ │ ├── pyramid-flipped.png
│ │ ├── pyramid-green-ingame.png
│ │ ├── pyramid-green.png
│ │ ├── pyramid-red-ingame.png
│ │ ├── pyramid-red.png
│ │ ├── tev-config-view.png
│ │ ├── tev-stage-1.png
│ │ ├── tev-stage-2-solo.png
│ │ ├── tev-stage-2.png
│ │ ├── tev-stage-color.png
│ │ ├── tev-stages.png
│ │ ├── texture-image.png
│ │ ├── texture-rotated.png
│ │ ├── texture-settings.png
│ │ ├── texture-transform.png
│ │ ├── textures.png
│ │ ├── unpack-icon.png
│ │ ├── unpack-options.png
│ │ ├── unpack-reload.png
│ │ ├── verifier-blue.png
│ │ ├── verifier-options.png
│ │ ├── verifier-yellow.png
│ │ ├── verify-search.png
│ │ ├── warning-suppressed.png
│ │ └── warning-unsuppressed.png
│ └── 2
│ │ ├── active-action.png
│ │ ├── add-vcol.png
│ │ ├── animation-error.png
│ │ ├── animation-preview.png
│ │ ├── animation-workspace.png
│ │ ├── blending-discarding.png
│ │ ├── blending-enabled.png
│ │ ├── bone-parenting.png
│ │ ├── brawlbox-animation.png
│ │ ├── color-attribute-set.png
│ │ ├── cube-black.png
│ │ ├── cube-ingame-lit-lava.png
│ │ ├── cube-ingame-lit.png
│ │ ├── cube-ingame-xlu.png
│ │ ├── cube-ingame.png
│ │ ├── cube-red.png
│ │ ├── cube-textured-cmpr.png
│ │ ├── cube-textured-nearest.png
│ │ ├── cube-textured.png
│ │ ├── cube-transformed.png
│ │ ├── cube-white.png
│ │ ├── culling-back.png
│ │ ├── depth-disabled.png
│ │ ├── export-settings.png
│ │ ├── graph-editor-access.png
│ │ ├── graph-editor.png
│ │ ├── half-alpha.png
│ │ ├── light-channel-added.png
│ │ ├── light-channel-associated.png
│ │ ├── light-channel-no-associated.png
│ │ ├── lightmap-proper-mapping.png
│ │ ├── lightmap-textures-proper.png
│ │ ├── lightmap-textures.png
│ │ ├── nla-open.png
│ │ ├── no-tev-config.png
│ │ ├── pose-animation-created.png
│ │ ├── pose-animation.png
│ │ ├── pow-imported.png
│ │ ├── push-down.png
│ │ ├── pushed-down.png
│ │ ├── stage-1.png
│ │ ├── stage-2.png
│ │ ├── stage-3.png
│ │ ├── stage-color-constant.png
│ │ ├── stage-color.png
│ │ ├── stages.png
│ │ ├── tev-eq.png
│ │ ├── tev-output.png
│ │ ├── tev-texture-rgb.png
│ │ ├── texture-added.png
│ │ ├── tints-applied.png
│ │ ├── translucent-render-group-enabled.png
│ │ ├── uv-grid.png
│ │ ├── uv-slots.png
│ │ ├── uv2-broken.png
│ │ ├── uv2-fixed.png
│ │ ├── vertex-paint.png
│ │ └── vertex-painted.png
├── hotkey-example.png
├── image-active.png
├── image-animation-slots.png
├── image-mipmaps.png
├── import.png
├── indirect-example.png
├── indirect-selections.png
├── indirect-texturing.png
├── indirect-water.png
├── install-button.png
├── install-enable.png
├── install-file-select.png
├── install-preferences.png
├── install-release.png
├── install-save-prefs.png
├── lighting-channel.png
├── logo-blender.png
├── logo-ingame.png
├── material.png
├── mesh-settings.png
├── scene-settings.png
├── selections.png
├── texture-settings.png
└── vis-driver-example.png
└── logo.png
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | log.txt
2 | private
3 | __pycache__
4 | *tests.py
5 | venv
6 | .vscode
7 | *.zip
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Blender addon for BRRES models, particularly with New Super Mario Bros. Wii modding in mind.
4 |
5 | Main features include:
6 | - BRRES importer & exporter with support for most features of the format
7 | - Render engine and new material editing interface
8 | - Verifier to catch possible mistakes in model data
9 |
10 | BerryBush is targeted at Blender 3.3 LTS, but works with Blender 3.4 as well. Wider support is planned in the future. A more detailed summary of BerryBush's current scope and limitations can be found [here](https://github.com/hayden0729/berrybush/wiki/Scope-and-Limitations).
11 |
12 | # Installation
13 |
14 | Download the [latest release](https://github.com/hayden0729/berrybush/releases/latest). In Blender, open **Edit > Preferences... > Add-ons > Install...**, and then choose the ZIP file you downloaded. A more detailed step-by-step guide can be found [here](https://github.com/hayden0729/berrybush/wiki/Installation). After installation, check out the rest of [the wiki](https://github.com/hayden0729/berrybush/wiki) to get started.
15 |
16 | The same process can be used to update BerryBush to a new version. You can also [uninstall the addon](https://github.com/hayden0729/berrybush/wiki/Installation#Uninstalling-BerryBush) from the same menu.
17 |
18 | # Support
19 |
20 | Detailed guides and documentation are available in [the wiki](https://github.com/hayden0729/berrybush/wiki). Additionally, if you're ever unsure what something does within Blender, you can hover over it to view its description.
21 |
22 | Feature requests, bug reports, documentation corrections, or other feedback are all welcome on [the issues page](https://github.com/hayden0729/berrybush/issues). Note that this project is mainly intended for New Super Mario Bros. Wii modding, so although it may happen to work for other games, NSMBW-related features are the primary concern.
23 |
24 | Still have questions? You can also always feel free to contact me on Discord (@hayden0729) in [Evolution](https://discord.gg/aZAaXVJ) or [Horizon](https://discord.gg/XnQJnwa).
25 |
26 | # Resources/Credits
27 |
28 | - [AVSYS Wiki](https://avsys.xyz/)
29 | - [BrawlCrate](https://github.com/soopercool101/BrawlCrate)
30 | - [Custom Mario Kart Wiiki](https://wiki.tockdom.com/)
31 | - [Dolphin Emulator](https://github.com/dolphin-emu/dolphin)
32 | - [libogc](https://github.com/devkitPro/libogc)
33 | - [Yet Another Gamecube Documentation](http://hitmen.c02.at/files/yagcd/yagcd/)
34 |
--------------------------------------------------------------------------------
/berrybush/__init__.py:
--------------------------------------------------------------------------------
1 | # BerryBush - BRRES support for Blender focused on New Super Mario Bros. Wii
2 | # Copyright (C) 2023 hayden0729
3 | #
4 | # This program is free software; you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation; either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 |
18 | # standard imports
19 | import pathlib
20 | # 3rd party imports
21 | import bpy
22 | import bpy.utils.previews
23 | # internal imports
24 | from .blender import (
25 | animpreview,
26 | backup,
27 | bone,
28 | brresexport,
29 | brresimport,
30 | material,
31 | mesh,
32 | preferences,
33 | proputils,
34 | render,
35 | scene,
36 | tev,
37 | texture,
38 | updater,
39 | verify
40 | )
41 |
42 |
43 | ICONS: bpy.utils.previews.ImagePreviewCollection
44 | """BerryBush's custom icons. Access by importing within a function to avoid circular imports."""
45 |
46 |
47 | bl_info = {
48 | "name" : "BRRES format (BerryBush)",
49 | "author": "hayden0729",
50 | "version": (1, 4, 6),
51 | "blender" : (3, 3, 0),
52 | "location": "File > Import-Export",
53 | "description": "NSMBW focused BRRES support",
54 | "doc_url": "https://github.com/hayden0729/berrybush/wiki",
55 | "category" : "Import-Export"
56 | }
57 |
58 |
59 | classes = (
60 | # data
61 | tev.TevStageSelSettings,
62 | tev.TevStageIndSettings,
63 | tev.TevStageColorParams,
64 | tev.TevStageAlphaParams,
65 | tev.TevStageSettings,
66 | tev.ColorSwapSettings,
67 | tev.TevSettings,
68 | texture.TextureTransform,
69 | texture.TexImg,
70 | texture.TexSettings,
71 | texture.MipmapSlot,
72 | texture.ImgSettings,
73 | material.IndTexIndividualSettings,
74 | material.IndTransform,
75 | material.IndTexSettings,
76 | material.ColorRegSettings,
77 | material.LightChannelColorAlphaSettings,
78 | material.LightChannelSettings,
79 | material.AlphaSettings,
80 | material.DepthSettings,
81 | material.MiscMatSettings,
82 | material.MatSettings,
83 | mesh.MeshAttrSettings,
84 | mesh.MeshSettings,
85 | bone.BoneSettings,
86 | brresimport.ImportSettings,
87 | brresexport.ExportSettings,
88 | scene.SceneSettings,
89 | # ui
90 | material.MatPanel,
91 | tev.TevPanel,
92 | tev.TevColorSwapPanel,
93 | tev.TevIndSelPanel,
94 | tev.TevStagePanel,
95 | tev.TevStageSelPanel,
96 | tev.TevStageIndPanel,
97 | tev.TevStageColorPanel,
98 | tev.TevStageAlphaPanel,
99 | texture.TexPanel,
100 | texture.TexTransformPanel,
101 | texture.TexSettingsPanel,
102 | texture.ImgPanel,
103 | texture.MipmapPanel,
104 | material.IndTexPanel,
105 | material.IndTransformPanel,
106 | material.ColorRegPanel,
107 | material.LightChannelPanel,
108 | material.AlphaSettingsPanel,
109 | material.DepthSettingsPanel,
110 | material.MatMiscPanel,
111 | mesh.MeshPanel,
112 | bone.BonePanel,
113 | brresimport.GeneralPanel,
114 | brresimport.ArmPanel,
115 | brresimport.AnimPanel,
116 | brresexport.GeneralPanel,
117 | brresexport.ArmGeoPanel,
118 | brresexport.ImagePanel,
119 | brresexport.AnimPanel,
120 | render.FilmPanel,
121 | # operators
122 | backup.ClearBackups,
123 | proputils.CustomIDCollOpAdd,
124 | proputils.CustomIDCollOpRemove,
125 | proputils.CustomIDCollOpClone,
126 | proputils.CustomIDCollOpClearSelection,
127 | proputils.CustomIDCollOpChoose,
128 | proputils.CustomIDCollOpMoveUp,
129 | proputils.CustomIDCollOpMoveDown,
130 | brresimport.ImportBRRES,
131 | brresexport.ExportBRRES,
132 | verify.VerifyBRRES,
133 | animpreview.PreviewAnimation,
134 | updater.ShowLatestVersion,
135 | updater.UpdateBRRES,
136 | updater.UpdateVertColors1_1_0,
137 | # preferences
138 | preferences.BerryBushPreferences,
139 | # render engine
140 | render.BerryBushRenderEngine
141 | )
142 |
143 |
144 | unusedPropertyGroupRemovalHandler = proputils.getUnusedPropertyGroupRemovalHandler(
145 | tev.TevSettings
146 | )
147 |
148 |
149 | def register():
150 | if bpy.app.version >= (3, 5, 0):
151 | raise RuntimeError("BerryBush does not support this version of Blender "
152 | "(supported versions: 3.3, 3.4)")
153 | global ICONS # pylint: disable=global-statement
154 | ICONS = bpy.utils.previews.new()
155 | for f in (pathlib.Path(__file__).parent / "blender" / "icons").iterdir():
156 | if f.is_file():
157 | ICONS.load(f.stem.upper(), str(f.resolve()), 'IMAGE')
158 | for cls in classes:
159 | bpy.utils.register_class(cls)
160 | if issubclass(cls, proputils.DynamicPropertyGroup):
161 | cls.registerDynamicClasses()
162 | bpy.types.Scene.brres = bpy.props.PointerProperty(type=scene.SceneSettings)
163 | bpy.types.Bone.brres = bpy.props.PointerProperty(type=bone.BoneSettings)
164 | bpy.types.Mesh.brres = bpy.props.PointerProperty(type=mesh.MeshSettings)
165 | bpy.types.Material.brres = bpy.props.PointerProperty(type=material.MatSettings)
166 | bpy.types.Image.brres = bpy.props.PointerProperty(type=texture.ImgSettings)
167 | bpy.types.TOPBAR_MT_file_import.append(brresimport.drawOp)
168 | bpy.types.TOPBAR_MT_file_export.append(brresexport.drawOp)
169 | bpy.types.VIEW3D_MT_object.append(verify.drawOp)
170 | bpy.types.NLA_MT_view.append(animpreview.drawOp)
171 | bpy.app.handlers.load_post.append(updater.update)
172 | bpy.app.handlers.load_post.append(unusedPropertyGroupRemovalHandler)
173 | bpy.app.handlers.load_post.append(updater.checkLatestVer)
174 | bpy.app.handlers.save_pre.append(updater.saveVer)
175 | render.BerryBushRenderEngine.registerOnPanels()
176 |
177 |
178 | def unregister():
179 | render.BerryBushRenderEngine.unregisterOnPanels()
180 | bpy.app.handlers.save_pre.remove(updater.saveVer)
181 | bpy.app.handlers.load_post.remove(unusedPropertyGroupRemovalHandler)
182 | bpy.app.handlers.load_post.remove(updater.update)
183 | bpy.types.NLA_MT_view.remove(animpreview.drawOp)
184 | bpy.types.VIEW3D_MT_object.remove(verify.drawOp)
185 | bpy.types.TOPBAR_MT_file_export.remove(brresexport.drawOp)
186 | bpy.types.TOPBAR_MT_file_import.remove(brresimport.drawOp)
187 | del bpy.types.Image.brres
188 | del bpy.types.Material.brres
189 | del bpy.types.Mesh.brres
190 | del bpy.types.Bone.brres
191 | del bpy.types.Scene.brres
192 | for cls in reversed(classes):
193 | bpy.utils.unregister_class(cls)
194 | if issubclass(cls, proputils.DynamicPropertyGroup):
195 | cls.unregisterDynamicClasses()
196 | bpy.utils.previews.remove(ICONS)
197 |
198 |
199 | if __name__ == "__main__":
200 | register()
201 |
--------------------------------------------------------------------------------
/berrybush/blender/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/berrybush/blender/__init__.py
--------------------------------------------------------------------------------
/berrybush/blender/animpreview.py:
--------------------------------------------------------------------------------
1 | # standard imports
2 | from itertools import chain
3 | # 3rd party imports
4 | import bpy
5 |
6 |
7 | class PreviewAnimation(bpy.types.Operator):
8 | """Preview an animation by soloing NLA tracks by name"""
9 |
10 | bl_idname = "brres.preview_anim"
11 | bl_label = "Preview Animation"
12 |
13 | name: bpy.props.StringProperty(
14 | name="Name",
15 | description="All NLA tracks with this name will be soloed, and any others will be un-soloed", # pylint: disable=line-too-long
16 | )
17 |
18 | def execute(self, context: bpy.types.Context):
19 | frame_start = frame_end = 0
20 | for obj in chain(bpy.data.objects.values(), bpy.data.materials.values(), bpy.data.armatures.values()):
21 | animData: bpy.types.AnimData | None = obj.animation_data
22 | if animData is not None:
23 | for track in animData.nla_tracks:
24 | track: bpy.types.NlaTrack
25 | solo = track.name == self.name
26 | if track.is_solo != solo:
27 | track.is_solo = solo
28 | if track.strips:
29 | frame_start = min(frame_end, track.strips[0].frame_start)
30 | frame_end = max(frame_end, track.strips[-1].frame_end)
31 | context.scene.frame_start = int(frame_start)
32 | context.scene.frame_end = int(frame_end)
33 | return {'FINISHED'}
34 |
35 | def invoke(self, context: bpy.types.Context, event: bpy.types.Event):
36 | return context.window_manager.invoke_props_dialog(self)
37 |
38 | def draw(self, context: bpy.types.Context):
39 | layout = self.layout
40 | layout.use_property_split = True
41 | layout.prop(self, "name")
42 |
43 |
44 | def drawOp(self, context: bpy.types.Context):
45 | layout: bpy.types.UILayout = self.layout
46 | layout.separator()
47 | layout.operator(PreviewAnimation.bl_idname, text="BerryBush: Preview Animation")
48 |
--------------------------------------------------------------------------------
/berrybush/blender/backup.py:
--------------------------------------------------------------------------------
1 | # standard imports
2 | from datetime import datetime
3 | from pathlib import Path
4 | import shutil
5 | # 3rd party imports
6 | import bpy
7 | # internal imports
8 | from .preferences import getPrefs
9 |
10 |
11 | class ClearBackups(bpy.types.Operator):
12 | """Clear the BerryBush backups directory."""
13 |
14 | bl_idname = "brres.clear_backups"
15 | bl_label = "Clear Backups"
16 |
17 | def execute(self, context: bpy.types.Context):
18 | backupDir = getPrefs(context).backupDir
19 | backupPath = Path(backupDir)
20 | for file in backupPath.iterdir():
21 | if isBackup(file):
22 | file.unlink()
23 | return {'FINISHED'}
24 |
25 |
26 | def tryBackup(filepath: str, context: bpy.types.Context):
27 | """Back up a file to the BerryBush backups directory if backups are enabled and the file exists.
28 |
29 | If a backup is created and the max capacity is enabled and exceeded,
30 | the oldest file(s) in the directory are deleted until at capacity.
31 | """
32 | prefs = getPrefs(context)
33 | path = Path(filepath)
34 | if not prefs.doBackups or not path.exists():
35 | return
36 | # path is ensured to be absolute for the sake of clear error messages
37 | backupFolderPath = Path(prefs.backupDir).absolute()
38 | try:
39 | backupFolderPath.mkdir(exist_ok=True)
40 | except OSError as e:
41 | raise OSError("Failed to open the BerryBush backup folder. "
42 | "You may have to change its location in the addon preferences, "
43 | "or make sure the path to it exists. See above for more info.") from e
44 | backupLabel = datetime.now().strftime("(backup %Y-%m-%d %H-%M-%S)")
45 | backupPath = Path(backupFolderPath, f"{path.stem} {backupLabel}{path.suffix}")
46 | try:
47 | shutil.copy(filepath, str(backupPath))
48 | except OSError as e:
49 | raise OSError("Failed to create a BRRES backup. See above for more info.") from e
50 | if prefs.doMaxBackups:
51 | backupFiles = [f for f in backupFolderPath.iterdir() if isBackup(f)]
52 | backupFiles.sort(key=lambda file: file.stat().st_mtime, reverse=True)
53 | while len(backupFiles) > prefs.maxBackups:
54 | try:
55 | backupFiles.pop().unlink()
56 | except OSError as e:
57 | raise OSError("Failed to delete an old BRRES backup. "
58 | "See above for more info.") from e
59 |
60 |
61 | def isBackup(path: Path):
62 | """Whether a path should be considered a BerryBush backup."""
63 | return path.is_file() and path.suffix == ".brres" and "backup" in path.stem
64 |
--------------------------------------------------------------------------------
/berrybush/blender/bone.py:
--------------------------------------------------------------------------------
1 | # 3rd party imports
2 | import bpy
3 | # internal imports
4 | from .common import PropertyPanel, UNDOCUMENTED
5 |
6 |
7 | class BoneSettings(bpy.types.PropertyGroup):
8 |
9 | bbMode: bpy.props.EnumProperty(
10 | name="Billboard Mode",
11 | description="Mode of moving this bone based on the camera",
12 | items=(
13 | ('OFF', "None", ""),
14 | ('STANDARD', "Standard", "No rotation restrictions, affected by parent rotation, Z axis parallel to camera's Z axis"), # pylint: disable=line-too-long
15 | ('STANDARD_PERSP', "Standard (Perspective)", "No rotation restrictions, affected by parent rotation, Z axis pointing at camera"), # pylint: disable=line-too-long
16 | ('ROTATION', "Rotation", "Y axis parallel to camera's Y axis, unaffected by parent rotation, Z axis parallel to camera's Z axis"), # pylint: disable=line-too-long
17 | ('ROTATION_PERSP', "Rotation (Perspective)", "Y axis parallel to camera's Y axis, unaffected by parent rotation, Z axis pointing at camera"), # pylint: disable=line-too-long
18 | ('Y_ROTATION', "Y Rotation", "Only Y rotation allowed, affected by parent rotation, Z axis parallel to camera's Z axis"), # pylint: disable=line-too-long
19 | ('Y_ROTATION_PERSP', "Y Rotation (Perspective)", "Only Y rotation allowed, affected by parent rotation, Z axis pointing at camera"), # pylint: disable=line-too-long
20 | ),
21 | default='OFF',
22 | options=set()
23 | )
24 |
25 | bbParent: bpy.props.StringProperty(
26 | name="Billboard Parent",
27 | description=UNDOCUMENTED,
28 | options=set()
29 | )
30 |
31 |
32 | class BonePanel(PropertyPanel):
33 | bl_idname = "BRRES_PT_bone_attrs"
34 | bl_label = "BRRES Settings"
35 | bl_context = "bone"
36 |
37 | @classmethod
38 | def poll(cls, context):
39 | return (context.bone or context.edit_bone)
40 |
41 | def draw(self, context):
42 | layout = self.layout
43 | layout.use_property_split = True
44 | layout.use_property_decorate = False
45 | bone = context.bone if context.bone else context.armature.bones[context.edit_bone.name]
46 | boneSettings = bone.brres
47 | col = layout.column()
48 | col.prop(boneSettings, "bbMode")
49 | col.prop_search(boneSettings, "bbParent", context.armature, "bones")
50 |
--------------------------------------------------------------------------------
/berrybush/blender/icons/rgb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/berrybush/blender/icons/rgb.png
--------------------------------------------------------------------------------
/berrybush/blender/icons/val_0_8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/berrybush/blender/icons/val_0_8.png
--------------------------------------------------------------------------------
/berrybush/blender/icons/val_1_8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/berrybush/blender/icons/val_1_8.png
--------------------------------------------------------------------------------
/berrybush/blender/icons/val_2_8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/berrybush/blender/icons/val_2_8.png
--------------------------------------------------------------------------------
/berrybush/blender/icons/val_3_8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/berrybush/blender/icons/val_3_8.png
--------------------------------------------------------------------------------
/berrybush/blender/icons/val_4_8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/berrybush/blender/icons/val_4_8.png
--------------------------------------------------------------------------------
/berrybush/blender/icons/val_5_8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/berrybush/blender/icons/val_5_8.png
--------------------------------------------------------------------------------
/berrybush/blender/icons/val_6_8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/berrybush/blender/icons/val_6_8.png
--------------------------------------------------------------------------------
/berrybush/blender/icons/val_7_8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/berrybush/blender/icons/val_7_8.png
--------------------------------------------------------------------------------
/berrybush/blender/icons/val_8_8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/berrybush/blender/icons/val_8_8.png
--------------------------------------------------------------------------------
/berrybush/blender/limiter.py:
--------------------------------------------------------------------------------
1 | from abc import abstractmethod
2 | import bpy
3 |
4 |
5 | class ObjectLimiter():
6 |
7 | def __init__(self, context: bpy.types.Context):
8 | self._context = context
9 | self._objects: dict[str, bool] = {}
10 |
11 | def includes(self, obj: bpy.types.Object):
12 | if obj.name in self._objects:
13 | return self._objects[obj.name]
14 | result: bool
15 | if obj.type == 'ARMATURE' and any(self.includes(c) for c in obj.children_recursive):
16 | result = True
17 | else:
18 | result = self._shouldInclude(obj)
19 | self._objects[obj.name] = result
20 | return result
21 |
22 | @abstractmethod
23 | def _shouldInclude(self, obj: bpy.types.Object):
24 | """Whether obj should be included by this limiter (after caching/parenting processing)"""
25 |
26 |
27 | class AllObjectLimiter(ObjectLimiter):
28 |
29 | def _shouldInclude(self, obj: bpy.types.Object):
30 | return True
31 |
32 |
33 | class SelectedObjectLimiter(ObjectLimiter):
34 |
35 | def _shouldInclude(self, obj: bpy.types.Object):
36 | return obj.select_get()
37 |
38 |
39 | class VisibleObjectLimiter(ObjectLimiter):
40 |
41 | def __init__(self, context: bpy.types.Context):
42 | super().__init__(context)
43 | self._getObjStatuses(context.view_layer.layer_collection)
44 |
45 | def _getObjStatuses(self, layerCollection: bpy.types.LayerCollection):
46 | """Evaluate, for each object contained recursively within a layer collection,
47 | whether that object should be included by this limiter."""
48 | if layerCollection.visible_get():
49 | # layer collection is visible, so test hiding for direct children
50 | # (sub-collections, objects)
51 | for childLayerCollection in layerCollection.children:
52 | self._getObjStatuses(childLayerCollection)
53 | for obj in layerCollection.collection.objects:
54 | self._objects[obj.name] = not obj.hide_get()
55 | else:
56 | # layer collection is invisible, so contents are invisible
57 | for obj in layerCollection.collection.all_objects:
58 | # ensure object isn't already tracked, in case it's visible in another collection
59 | if obj.name not in self._objects:
60 | self._objects[obj.name] = False
61 |
62 | def _shouldInclude(self, obj: bpy.types.Object):
63 | # if we get here, it means the object wasn't found in the initial view layer sweep
64 | # so just default to hide_get()
65 | return not obj.hide_get()
66 |
67 |
68 | class ObjectLimiterFactory():
69 |
70 | _LIMITER_TYPES = {
71 | "ALL": AllObjectLimiter,
72 | "SELECTED": SelectedObjectLimiter,
73 | "VISIBLE": VisibleObjectLimiter
74 | }
75 |
76 | @classmethod
77 | def create(cls, context: bpy.types.Context, typeStr: str):
78 | return cls._LIMITER_TYPES[typeStr](context)
79 |
--------------------------------------------------------------------------------
/berrybush/blender/mesh.py:
--------------------------------------------------------------------------------
1 | # 3rd party imports
2 | import bpy
3 | # internal imports
4 | from .common import PropertyPanel, drawCheckedProp, drawProp
5 | from .proputils import CloneablePropertyGroup
6 | from ..wii import gx
7 | from ..wii.alias import alias
8 |
9 |
10 | class MeshAttrSettings(CloneablePropertyGroup):
11 |
12 | @classmethod
13 | def getCloneSources(cls, context: bpy.types.Context):
14 | return {m.name: m.brres.meshAttrs for m in bpy.data.meshes}
15 |
16 | _desc = "Attribute layer or UV map to use for this BRRES attribute"
17 |
18 | clr1: bpy.props.StringProperty(name="Slot 1", description=_desc, options=set())
19 | clr2: bpy.props.StringProperty(name="Slot 2", description=_desc, options=set())
20 | uv1: bpy.props.StringProperty(name="Slot 1", description=_desc, options=set(), default="UVMap")
21 | uv2: bpy.props.StringProperty(name="Slot 2", description=_desc, options=set())
22 | uv3: bpy.props.StringProperty(name="Slot 3", description=_desc, options=set())
23 | uv4: bpy.props.StringProperty(name="Slot 4", description=_desc, options=set())
24 | uv5: bpy.props.StringProperty(name="Slot 5", description=_desc, options=set())
25 | uv6: bpy.props.StringProperty(name="Slot 6", description=_desc, options=set())
26 | uv7: bpy.props.StringProperty(name="Slot 7", description=_desc, options=set())
27 | uv8: bpy.props.StringProperty(name="Slot 8", description=_desc, options=set())
28 |
29 | clrs = alias("clr1", "clr2")
30 | uvs = alias("uv1", "uv2", "uv3", "uv4", "uv5", "uv6", "uv7", "uv8")
31 |
32 |
33 | class MeshSettings(CloneablePropertyGroup):
34 |
35 | @classmethod
36 | def getCloneSources(cls, context: bpy.types.Context):
37 | return {m.name: m.brres for m in bpy.data.meshes}
38 |
39 | meshAttrs: bpy.props.PointerProperty(type=MeshAttrSettings)
40 |
41 | enableDrawPrio: bpy.props.BoolProperty(
42 | name="Enable Draw Priority",
43 | description="Give this object a special priority during rendering (if disabled, object has minimum priority)", # pylint: disable=line-too-long
44 | default=False,
45 | options=set()
46 | )
47 |
48 | drawPrio: bpy.props.IntProperty(
49 | name="Draw Priority",
50 | description="Priority for rendering this object (higher -> draw later than lower-priority others in the same render group)", # pylint: disable=line-too-long
51 | min=1,
52 | max=255,
53 | default=1,
54 | options=set()
55 | )
56 |
57 |
58 | class MeshPanel(PropertyPanel):
59 | bl_idname = "BRRES_PT_mesh_attrs"
60 | bl_label = "BRRES Settings"
61 | bl_context = "data"
62 | bl_options = set()
63 |
64 | @classmethod
65 | def poll(cls, context):
66 | return context.mesh is not None
67 |
68 | def draw(self, context):
69 | layout = self.layout
70 | meshSettings = context.active_object.data.brres
71 | meshAttrs = meshSettings.meshAttrs
72 | meshSettings.drawCloneUI(layout)
73 | labels = ("Color Attributes", "UV Maps")
74 | icons = ('GROUP_VCOL', 'GROUP_UVS')
75 | propNames = ("clr", "uv")
76 | propCounts = (gx.MAX_CLR_ATTRS, gx.MAX_UV_ATTRS)
77 | for label, icon, propName, propCount in zip(labels, icons, propNames, propCounts):
78 | layout.label(text=label, icon=icon)
79 | for i in range(1, propCount + 1):
80 | drawProp(layout, meshAttrs, f"{propName}{i}", factor=.2)
81 | layout.label(text="Other Settings", icon='SETTINGS')
82 | drawCheckedProp(layout, meshSettings, "enableDrawPrio", meshSettings, "drawPrio", .34)
83 |
--------------------------------------------------------------------------------
/berrybush/blender/preferences.py:
--------------------------------------------------------------------------------
1 | # 3rd party imports
2 | import bpy
3 | import bpy.utils.previews
4 | # internal imports
5 | from .common import drawColumnSeparator, drawCheckedProp
6 |
7 |
8 | ADDON_IDNAME = __name__.split(".", maxsplit=1)[0]
9 |
10 |
11 | class BerryBushPreferences(bpy.types.AddonPreferences):
12 | bl_idname = ADDON_IDNAME
13 |
14 | doBackups: bpy.props.BoolProperty(
15 | name="Back Up Overwritten Files",
16 | description="Create backups when BRRES files are overwritten during export",
17 | default=True
18 | )
19 |
20 | backupDir: bpy.props.StringProperty(
21 | name="Backup Directory",
22 | description="Directory for backing up overwritten BRRES files",
23 | subtype='FILE_PATH',
24 | default=bpy.utils.user_resource('SCRIPTS', path="berrybush_backups")
25 | )
26 |
27 | doMaxBackups: bpy.props.BoolProperty(
28 | name="Backup Limit",
29 | description="Whether to automatically delete old backups once the backup directory reaches a certain capacity", # pylint:disable=line-too-long
30 | default=True
31 | )
32 |
33 | maxBackups: bpy.props.IntProperty(
34 | name="Backup Limit",
35 | description="Maximum number of backups that can be stored before old ones are automatically deleted", # pylint:disable=line-too-long
36 | min=1,
37 | default=100
38 | )
39 |
40 | doUpdateChecks: bpy.props.BoolProperty(
41 | name="Check For Updates",
42 | default=True
43 | )
44 |
45 | skipThisVersion: bpy.props.BoolProperty(
46 | name="Skip This Version"
47 | )
48 |
49 | latestKnownVersion: bpy.props.IntVectorProperty(size=3)
50 |
51 | def draw(self, context):
52 | # backup stuff
53 | self.layout.prop(self, "doBackups")
54 | col = self.layout.column()
55 | col.enabled = self.doBackups
56 | col.prop(self, "backupDir")
57 | drawColumnSeparator(col)
58 | drawCheckedProp(col, self, "doMaxBackups", self, "maxBackups")
59 | self.layout.operator("brres.clear_backups")
60 | # update stuff
61 | row = self.layout.row().split(factor=.5)
62 | row.prop(self, "doUpdateChecks")
63 | skipRow = row.row()
64 | skipRow.enabled = self.doUpdateChecks
65 | skipRow.prop(self, "skipThisVersion")
66 |
67 |
68 | def getPrefs(context: bpy.types.Context) -> BerryBushPreferences:
69 | """Get the BerryBush preferences from a Blender context."""
70 | return context.preferences.addons[ADDON_IDNAME].preferences
71 |
--------------------------------------------------------------------------------
/berrybush/blender/scene.py:
--------------------------------------------------------------------------------
1 | # 3rd party imports
2 | import bpy
3 | # internal imports
4 | from .brresimport import ImportSettings
5 | from .brresexport import ExportSettings
6 | from .tev import TevSettings
7 |
8 |
9 | class SceneSettings(bpy.types.PropertyGroup):
10 |
11 | tevConfigs: TevSettings.CustomIDCollectionProperty()
12 |
13 | importSettings: bpy.props.PointerProperty(type=ImportSettings)
14 | exportSettings: bpy.props.PointerProperty(type=ExportSettings)
15 |
16 | renderAssumeOpaqueMats: bpy.props.BoolProperty(
17 | name="Assume Opaque Materials",
18 | description="For materials with blending disabled, write an alpha of 1 (opaque). Only affects transparent renders (not viewport)", # pylint: disable=line-too-long
19 | default=True,
20 | options=set()
21 | )
22 |
23 | renderNoTransparentOverwrite: bpy.props.BoolProperty(
24 | name="Prevent Transparent Overwrites",
25 | description="Always use blending factors '1' and '1 - Source' for alpha source & destination factors, respectively, independent from color blending. Prevents objects from being made more transparent by stuff in front of them. Only affects transparent renders (not viewport)", # pylint: disable=line-too-long
26 | default=True,
27 | options=set()
28 | )
29 |
30 | # version default of (0, 0, 0) just behaves as "most recent" & gets updated on blendfile save
31 | # this is done because we want the default to be the most recent version, but can't do that
32 | # directly because:
33 | # 1) we can't import it from bl_info here because that would make a circular dependency
34 | # 2) we can't store the version here and then import it when creating bl_info because bl_info
35 | # is parsed manually by blender or something and seemingly can't use variables for its fields
36 | version: bpy.props.IntVectorProperty(default=(0, 0, 0))
37 |
--------------------------------------------------------------------------------
/berrybush/blender/shaders.py:
--------------------------------------------------------------------------------
1 | # standard imports
2 | import pathlib
3 | # 3rd party imports
4 | import bpy
5 | import bgl # this is deprecated, but has a lot of functionality that gpu still lacks
6 | import gpu
7 | import numpy as np
8 | # internal imports
9 | from .common import enumVal
10 | from .shaderstruct import (
11 | ShaderBool, ShaderInt, ShaderFloat, ShaderVec, ShaderArr, ShaderMat, ShaderStruct
12 | )
13 | from .material import ColorRegSettings, IndTransform, LightChannelSettings
14 | from .texture import TexSettings, TextureTransform
15 | from .tev import TevStageSettings
16 | from ..wii import gx, transform as tf
17 |
18 |
19 | # CURRENT SHADER APPROACH (ubershader vs dynamic):
20 | # one vertex shader & fragment shader compiled for everything, taking material info through a ubo
21 | # this has good info & links about this issue:
22 | # https://community.khronos.org/t/ubershader-and-branching-cost/108571
23 |
24 |
25 | TEX_WRAPS = {
26 | 'CLAMP': bgl.GL_CLAMP_TO_EDGE,
27 | 'REPEAT': bgl.GL_REPEAT,
28 | 'MIRROR': bgl.GL_MIRRORED_REPEAT
29 | }
30 |
31 |
32 | TEX_FILTERS = {
33 | 'NEAREST': bgl.GL_NEAREST,
34 | 'LINEAR': bgl.GL_LINEAR,
35 | 'NEAREST_MIPMAP_NEAREST': bgl.GL_NEAREST_MIPMAP_NEAREST,
36 | 'LINEAR_MIPMAP_NEAREST': bgl.GL_LINEAR_MIPMAP_NEAREST,
37 | 'NEAREST_MIPMAP_LINEAR': bgl.GL_NEAREST_MIPMAP_LINEAR,
38 | 'LINEAR_MIPMAP_LINEAR': bgl.GL_LINEAR_MIPMAP_LINEAR,
39 | }
40 |
41 |
42 | class ShaderTevStageSels(ShaderStruct):
43 | tex = ShaderInt
44 | texSwap = ShaderInt
45 | ras = ShaderInt
46 | rasSwap = ShaderInt
47 |
48 |
49 | class ShaderTevStageIndSettings(ShaderStruct):
50 | texIdx = ShaderInt
51 | fmt = ShaderInt
52 | bias = ShaderVec(ShaderInt, 3)
53 | bumpAlphaComp = ShaderInt
54 | mtxType = ShaderInt
55 | mtxIdx = ShaderInt
56 | wrap = ShaderVec(ShaderInt, 2)
57 | utcLOD = ShaderBool
58 | addPrev = ShaderBool
59 |
60 |
61 | class ShaderTevStageCalcParams(ShaderStruct):
62 | constSel = ShaderInt
63 | args = ShaderVec(ShaderInt, 4)
64 | compMode = ShaderBool
65 | op = ShaderInt
66 | scale = ShaderFloat
67 | bias = ShaderFloat
68 | compChan = ShaderInt
69 | clamp = ShaderBool
70 | outputIdx = ShaderInt
71 |
72 |
73 | class ShaderTevStage(ShaderStruct):
74 | sels = ShaderTevStageSels
75 | ind = ShaderTevStageIndSettings
76 | colorParams = ShaderTevStageCalcParams
77 | alphaParams = ShaderTevStageCalcParams
78 |
79 | @classmethod
80 | def fromStageSettings(cls, stage: TevStageSettings):
81 | rStage = ShaderTevStage()
82 | # selections
83 | sels = stage.sels
84 | rSels = rStage.sels
85 | rSels.tex = sels.texSlot - 1
86 | rSels.texSwap = sels.texSwapSlot - 1
87 | rSels.ras = enumVal(sels, "rasterSel", callback=type(sels).rasterSelItems)
88 | rSels.rasSwap = sels.rasSwapSlot - 1
89 | # indirect settings
90 | ind = stage.indSettings
91 | rInd = rStage.ind
92 | rInd.texIdx = ind.slot - 1
93 | rInd.fmt = int(ind.fmt[-1])
94 | rInd.bias = tuple((-128 if rInd.fmt == 8 else 1) if b else 0 for b in ind.enableBias)
95 | rInd.bumpAlphaComp = enumVal(ind, "bumpAlphaComp")
96 | rInd.mtxType = enumVal(ind, "mtxType")
97 | rInd.mtxIdx = ind.mtxSlot - 1 if ind.enable else -1
98 | rInd.wrap = tuple(-1 if w == 'OFF' else int(w[3:]) for w in (ind.wrapU, ind.wrapV))
99 | rInd.utcLOD = ind.utcLOD
100 | rInd.addPrev = ind.addPrev
101 | # color & alpha params
102 | rStage.colorParams.constSel = gx.TEVConstSel[stage.sels.constColor].value
103 | rStage.alphaParams.constSel = gx.TEVConstSel[stage.sels.constAlpha].value
104 | rCalcParams = (rStage.colorParams, rStage.alphaParams)
105 | calcParams = (stage.colorParams, stage.alphaParams)
106 | for rParams, params in zip(rCalcParams, calcParams):
107 | argItemsCallback = type(params).argItems
108 | rParams.args = tuple(enumVal(params, arg, callback=argItemsCallback) for arg in "abcd")
109 | rParams.compMode = params.compMode
110 | if rParams.compMode:
111 | rParams.op = enumVal(params, "compOp")
112 | rParams.compChan = enumVal(params, "compChan")
113 | else:
114 | rParams.op = enumVal(params, "op")
115 | rParams.scale = 2 ** (enumVal(params, "scale") - 1)
116 | rParams.bias = (0, .5, -.5)[enumVal(params, "bias")]
117 | rParams.outputIdx = enumVal(params, "output")
118 | rParams.clamp = params.clamp
119 | return rStage
120 |
121 |
122 | class ShaderTexture(ShaderStruct):
123 | mtx = ShaderMat(ShaderFloat, 2, 3)
124 | dims = ShaderVec(ShaderInt, 2)
125 | mapMode = ShaderInt
126 | hasImg = ShaderBool
127 |
128 | def __init__(self):
129 | super().__init__()
130 | self.wrap: tuple[int, int] = ()
131 | self.filter: tuple[int, int] = ()
132 | self.lodBias: float = 0
133 | self.imgName: str = ""
134 | self.imgSlot: int = 0
135 | self.transform = tf.Transformation(2)
136 | self.mtx = ((1, 0, 0), (0, 1, 0))
137 | self._s = (1, 1)
138 | self._r = 0
139 | self._t = (0, 0)
140 |
141 | def setMtx(self, texTf: TextureTransform, tfGen: tf.MtxGenerator):
142 | """Set this texture's transformation matrix."""
143 | # TODO: implement similar caching for indirect matrices
144 | s = texTf.scale.to_tuple()
145 | r = texTf.rotation
146 | t = texTf.translation.to_tuple()
147 | if s != self._s or r != self._r or t != self._t:
148 | stf = self.transform
149 | stf.set(s, np.rad2deg((r, )), t)
150 | self.mtx = tuple(tuple(v) for v in tfGen.genMtx(stf)[:2])
151 | self._s = s
152 | self._r = r
153 | self._t = t
154 |
155 | @classmethod
156 | def fromTexSettings(cls, tex: TexSettings, tfGen: tf.MtxGenerator):
157 | rTex = cls()
158 | # image
159 | img: bpy.types.Image = tex.activeImg
160 | rTex.hasImg = img is not None
161 | rTex.imgName = img.name if rTex.hasImg else ""
162 | rTex.imgSlot = tex.activeImgSlot
163 | # transform
164 | rTex.setMtx(tex.transform, tfGen)
165 | # settings
166 | rTex.dims = tuple(img.size) if rTex.hasImg else (0, 0)
167 | rTex.mapMode = enumVal(tex, "mapMode", callback=type(tex).coordSrcItems)
168 | rTex.wrap = (TEX_WRAPS[tex.wrapModeU], TEX_WRAPS[tex.wrapModeV])
169 | mmLevels = len(img.brres.mipmaps) if rTex.hasImg else 0
170 | minFilter = f'{tex.minFilter}_MIPMAP_{tex.mipFilter}' if mmLevels > 0 else tex.minFilter
171 | rTex.filter = (TEX_FILTERS[minFilter], TEX_FILTERS[tex.magFilter])
172 | rTex.lodBias = tex.lodBias
173 | return rTex
174 |
175 |
176 | class ShaderIndTex(ShaderStruct):
177 | texIdx = ShaderInt
178 | mode = ShaderInt
179 | lightIdx = ShaderInt
180 | coordScale = ShaderVec(ShaderInt, 2)
181 |
182 |
183 | class ShaderLightChanSettings(ShaderStruct):
184 | difFromReg = ShaderBool
185 | ambFromReg = ShaderBool
186 | difMode = ShaderInt
187 | atnMode = ShaderInt
188 | enabledLights = ShaderArr(ShaderBool, 8)
189 |
190 |
191 | class ShaderLightChan(ShaderStruct):
192 | difReg = ShaderVec(ShaderFloat, 4)
193 | ambReg = ShaderVec(ShaderFloat, 4)
194 | colorSettings = ShaderLightChanSettings
195 | alphaSettings = ShaderLightChanSettings
196 |
197 | def setRegs(self, lc: LightChannelSettings):
198 | """Update this light channel's diffuse/ambient registers from BRRES settings."""
199 | self.difReg = tuple(lc.difColor)
200 | self.ambReg = tuple(lc.ambColor)
201 |
202 | @classmethod
203 | def fromLightChanSettings(cls, lc: LightChannelSettings):
204 | rlc = ShaderLightChan()
205 | rlc.setRegs(lc)
206 | rlcCA = (rlc.colorSettings, rlc.alphaSettings)
207 | lcCA = (lc.colorSettings, lc.alphaSettings)
208 | for rOps, ops in zip(rlcCA, lcCA): # (options for color/alpha)
209 | rOps.difFromReg = ops.difFromReg
210 | rOps.ambFromReg = ops.ambFromReg
211 | rOps.difMode = enumVal(ops, "diffuseMode") if ops.enableDiffuse else -1
212 | rOps.atnMode = enumVal(ops, "attenuationMode") if ops.enableAttenuation else -1
213 | rOps.enabledLights = tuple(ops.enabledLights)
214 | return rlc
215 |
216 |
217 | class ShaderMaterial(ShaderStruct):
218 | colorSwaps = ShaderArr(ShaderVec(ShaderInt, 4), gx.MAX_COLOR_SWAPS)
219 | stages = ShaderArr(ShaderTevStage, gx.MAX_TEV_STAGES)
220 | textures = ShaderArr(ShaderTexture, gx.MAX_TEXTURES)
221 | inds = ShaderArr(ShaderIndTex, gx.MAX_INDIRECTS)
222 | indMtcs = ShaderArr(ShaderMat(ShaderFloat, 2, 3), gx.MAX_INDIRECT_MTCS)
223 | constColors = ShaderArr(ShaderVec(ShaderFloat, 4), gx.MAX_TEV_CONST_COLORS)
224 | outputColors = ShaderArr(ShaderVec(ShaderFloat, 4), gx.MAX_TEV_STAND_COLORS + 1)
225 | lightChans = ShaderArr(ShaderLightChan, gx.MAX_CLR_ATTRS)
226 | enableBlend = ShaderBool
227 | alphaTestVals = ShaderVec(ShaderFloat, 2)
228 | alphaTestComps = ShaderVec(ShaderInt, 2)
229 | alphaTestLogic = ShaderInt
230 | alphaTestEnable = ShaderBool
231 | constAlpha = ShaderFloat
232 | numStages = ShaderInt
233 | numTextures = ShaderInt
234 | numIndMtcs = ShaderInt
235 |
236 | blendSubtract: bool = False
237 | blendSrcFac: int = 0
238 | blendDstFac: int = 0
239 | enableBlendLogic: bool = False
240 | blendLogicOp: int = 0
241 | enableDither: bool = False
242 | blendUpdateColorBuffer: bool = False
243 | blendUpdateAlphaBuffer: bool = False
244 | enableConstAlpha: bool = False
245 | enableCulling: bool = False
246 | cullMode: int = 0
247 | isXlu: bool = False
248 |
249 | enableDepthTest: bool = False
250 | depthFunc: int = 0
251 | enableDepthUpdate: bool = False
252 |
253 | name: str = ""
254 |
255 | def setColorRegs(self, regs: ColorRegSettings):
256 | """Set this material's color registers from BRRES settings."""
257 | self.constColors = tuple(tuple(c) for c in regs.constant)
258 | self.outputColors = tuple(tuple(c) for c in regs.standard)
259 |
260 | def setIndMtcs(self, tfs: list[IndTransform], tfGen: tf.MtxGenerator):
261 | """Set this material's indirect matrices from BRRES settings."""
262 | self.indMtcs = ()
263 | for itf in tfs:
264 | itf = itf.transform
265 | s, r, t = itf.scale, [np.rad2deg(itf.rotation)], itf.translation
266 | mtx = tf.IndMtxGen2D.genMtx(tf.Transformation(2, s, r, t))[:2]
267 | self.indMtcs += (tuple(tuple(v) for v in mtx), )
268 | self.numIndMtcs = len(tfs)
269 |
270 |
271 | class ShaderMesh(ShaderStruct):
272 | colors = ShaderArr(ShaderInt, gx.MAX_CLR_ATTRS)
273 | uvs = ShaderArr(ShaderInt, gx.MAX_UV_ATTRS)
274 |
275 |
276 | RENDER_STRUCTS = (
277 | ShaderTevStageSels,
278 | ShaderTevStageIndSettings,
279 | ShaderTevStageCalcParams,
280 | ShaderTevStage,
281 | ShaderTexture,
282 | ShaderIndTex,
283 | ShaderLightChanSettings,
284 | ShaderLightChan,
285 | ShaderMaterial,
286 | ShaderMesh
287 | )
288 |
289 | def compileBrresShader() -> gpu.types.GPUShader:
290 | """Compile & return the main BRRES shader."""
291 | shaderInfo = gpu.types.GPUShaderCreateInfo()
292 | # uniforms
293 | shaderInfo.typedef_source("".join(s.getSource() for s in RENDER_STRUCTS))
294 | shaderInfo.uniform_buf(1, ShaderMaterial.getName(), "material")
295 | shaderInfo.uniform_buf(2, ShaderMesh.getName(), "mesh")
296 | shaderInfo.push_constant('MAT4', "modelViewProjectionMtx")
297 | shaderInfo.push_constant('MAT3', "normalMtx")
298 | shaderInfo.push_constant('BOOL', "forceOpaque")
299 | shaderInfo.push_constant('BOOL', "isConstAlphaWrite")
300 | for i in range(gx.MAX_TEXTURES):
301 | shaderInfo.sampler(i, 'FLOAT_2D', f"image{i}")
302 | # vertex inputs
303 | shaderInfo.vertex_in(0, 'VEC3', "position")
304 | shaderInfo.vertex_in(1, 'VEC3', "normal")
305 | for clr in range(gx.MAX_CLR_ATTRS):
306 | shaderInfo.vertex_in(2 + clr, 'VEC4', f"color{clr}")
307 | for uv in range(gx.MAX_UV_ATTRS):
308 | shaderInfo.vertex_in(4 + uv, 'VEC2', f"uv{uv}")
309 | # interfaces (vertex outputs/fragment inputs)
310 | interfaceInfo = gpu.types.GPUStageInterfaceInfo("shader_interface")
311 | interfaceInfo.smooth('VEC4', "clipSpace")
312 | interfaceInfo.smooth('VEC3', "fragPosition")
313 | interfaceInfo.smooth('VEC3', "fragNormal")
314 | for clr in range(gx.MAX_CLR_ATTRS):
315 | interfaceInfo.smooth('VEC4', f"fragColor{clr}")
316 | for uv in range(gx.MAX_UV_ATTRS):
317 | interfaceInfo.smooth('VEC2', f"fragUV{uv}")
318 | shaderInfo.vertex_out(interfaceInfo)
319 | # fragment outputs
320 | shaderInfo.fragment_out(0, 'VEC4', "fragOutput")
321 | # compile
322 | shaderPath = (pathlib.Path(__file__).parent / "shaders").resolve()
323 | with open(shaderPath / "vertex.glsl", "r", encoding="utf-8") as f:
324 | shaderInfo.vertex_source(f.read())
325 | with open(shaderPath / "fragment.glsl", "r", encoding="utf-8") as f:
326 | shaderInfo.fragment_source(f.read())
327 | return gpu.shader.create_from_info(shaderInfo)
328 |
329 |
330 | BRRES_SHADER = compileBrresShader()
331 |
332 |
333 | def compilePostprocessShader() -> gpu.types.GPUShader:
334 | """Compile & return the post-processing shader."""
335 | shaderInfo = gpu.types.GPUShaderCreateInfo()
336 | shaderInfo.sampler(0, 'FLOAT_2D', "tex")
337 | shaderInfo.push_constant('BOOL', "doAlpha")
338 | shaderInfo.vertex_in(0, 'VEC2', "position")
339 | interfaceInfo = gpu.types.GPUStageInterfaceInfo("shader_interface")
340 | interfaceInfo.smooth('VEC2', "fragPosition")
341 | shaderInfo.vertex_out(interfaceInfo)
342 | shaderInfo.fragment_out(0, 'VEC4', "fragOutput")
343 | shaderPath = (pathlib.Path(__file__).parent / "shaders" / "postprocess").resolve()
344 | with open(shaderPath / "vertex.glsl", "r", encoding="utf-8") as f:
345 | shaderInfo.vertex_source(f.read())
346 | with open(shaderPath / "fragment.glsl", "r", encoding="utf-8") as f:
347 | shaderInfo.fragment_source(f.read())
348 | return gpu.shader.create_from_info(shaderInfo)
349 |
350 |
351 | POSTPROCESS_SHADER = compilePostprocessShader()
352 |
--------------------------------------------------------------------------------
/berrybush/blender/shaders/postprocess/fragment.glsl:
--------------------------------------------------------------------------------
1 | void main() {
2 | fragOutput = texture(tex, fragPosition / 2 + .5);
3 | if (doAlpha) {
4 | // convert to straight alpha before gamma correction
5 | // (since transparent rendering is done on top of a black background,
6 | // it's essentially premultiplied up to this point)
7 | fragOutput.rgb /= fragOutput.a;
8 | }
9 | else {
10 | fragOutput.a = 1.0;
11 | }
12 | // convert to linear color space, expected by blender
13 | fragOutput = pow(fragOutput, vec4(2.2, 2.2, 2.2, 1.0));
14 | if (doAlpha) {
15 | // convert back to premultiplied alpha, expected by blender
16 | fragOutput.rgb *= fragOutput.a;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/berrybush/blender/shaders/postprocess/vertex.glsl:
--------------------------------------------------------------------------------
1 | void main() {
2 | gl_Position = vec4(position, 1, 1);
3 | fragPosition = position;
4 | }
5 |
--------------------------------------------------------------------------------
/berrybush/blender/shaders/vertex.glsl:
--------------------------------------------------------------------------------
1 | void main() {
2 | clipSpace = modelViewProjectionMtx * vec4(position, 1.0);
3 | gl_Position = clipSpace;
4 | fragPosition = position;
5 | fragNormal = normal;
6 | fragColor0 = color0;
7 | fragColor1 = color1;
8 | fragUV0 = uv0;
9 | fragUV1 = uv1;
10 | fragUV2 = uv2;
11 | fragUV3 = uv3;
12 | fragUV4 = uv4;
13 | fragUV5 = uv5;
14 | fragUV6 = uv6;
15 | fragUV7 = uv7;
16 | }
17 |
--------------------------------------------------------------------------------
/berrybush/blender/shaderstruct.py:
--------------------------------------------------------------------------------
1 | # standard imports
2 | from abc import abstractmethod
3 | from dataclasses import dataclass
4 | from functools import cache
5 | import struct
6 | from typing import Generic, Iterator, TypeVar, TYPE_CHECKING
7 | # internal imports
8 | from ..wii.binaryutils import pad
9 | # special typing imports
10 | if TYPE_CHECKING:
11 | from typing_extensions import Self
12 | else:
13 | Self = object
14 |
15 |
16 | T = TypeVar("T", bound="ShaderType")
17 | B = TypeVar("B", bound="BasicShaderType")
18 |
19 |
20 | class ShaderType(Generic[T]):
21 | """GLSL type definition.
22 |
23 | Use in custom structs by type-hinting members with type instances that define their parameters
24 | (e.g., array length).
25 | """
26 |
27 | @classmethod
28 | @abstractmethod
29 | def getDefault(cls):
30 | """Get the default value for this type."""
31 |
32 | @abstractmethod
33 | def getName(self) -> str:
34 | """Get the name of this type as it would appear in shader code."""
35 |
36 | @abstractmethod
37 | def getSize(self) -> int:
38 | """Return the size of this type, in bytes."""
39 |
40 | @abstractmethod
41 | def getAlignment(self) -> int:
42 | """Return the alignment of this type, in bytes."""
43 |
44 | @abstractmethod
45 | def packVal(self, val) -> bytes:
46 | """Pack a value for this type to bytes."""
47 |
48 |
49 | class SimpleShaderType(ShaderType):
50 | """Shader type definition that doesn't require instances for hinting, just the type itself."""
51 | # pylint:disable=arguments-differ
52 |
53 | @classmethod
54 | @abstractmethod
55 | def getName(cls) -> str:
56 | pass
57 |
58 | @classmethod
59 | @abstractmethod
60 | def getSize(cls) -> int:
61 | pass
62 |
63 | @classmethod
64 | @abstractmethod
65 | def getAlignment(cls) -> int:
66 | pass
67 |
68 | @classmethod
69 | @abstractmethod
70 | def packVal(cls, val) -> bytes:
71 | pass
72 |
73 |
74 | class BasicShaderType(SimpleShaderType):
75 | """Fundamental GLSL data type (int, float, etc)"""
76 |
77 | @classmethod
78 | def getDefault(cls):
79 | return 0
80 |
81 | @classmethod
82 | def getSize(cls):
83 | return 4
84 |
85 | @classmethod
86 | def getAlignment(cls):
87 | return 4
88 |
89 | @classmethod
90 | @abstractmethod
91 | def getPrefix(cls) -> str:
92 | """Get the prefix for another type (vector or matrix) containing this type."""
93 |
94 |
95 | class ShaderBool(BasicShaderType):
96 |
97 | @classmethod
98 | def getDefault(cls):
99 | return False
100 |
101 | @classmethod
102 | def getName(cls):
103 | return "bool"
104 |
105 | @classmethod
106 | def packVal(cls, val):
107 | return struct.pack("I", val)
108 |
109 | @classmethod
110 | def getPrefix(cls):
111 | return "b"
112 |
113 |
114 | class ShaderInt(BasicShaderType):
115 |
116 | @classmethod
117 | def getName(cls):
118 | return "int"
119 |
120 | @classmethod
121 | def packVal(cls, val):
122 | return struct.pack("i", val)
123 |
124 | @classmethod
125 | def getPrefix(cls):
126 | return "i"
127 |
128 |
129 | class ShaderUInt(BasicShaderType):
130 |
131 | @classmethod
132 | def getName(cls):
133 | return "uint"
134 |
135 | @classmethod
136 | def packVal(cls, val):
137 | return struct.pack("I", val)
138 |
139 | @classmethod
140 | def getPrefix(cls):
141 | return "u"
142 |
143 |
144 | class ShaderFloatType(BasicShaderType): # pylint: disable=abstract-method
145 | pass
146 |
147 |
148 | class ShaderFloat(ShaderFloatType):
149 |
150 | @classmethod
151 | def getName(cls):
152 | return "float"
153 |
154 | @classmethod
155 | def packVal(cls, val):
156 | return struct.pack("f", val)
157 |
158 | @classmethod
159 | def getPrefix(cls):
160 | return ""
161 |
162 |
163 | class ShaderDouble(ShaderFloatType):
164 |
165 | @classmethod
166 | def getName(cls):
167 | return "double"
168 |
169 | @classmethod
170 | def getSize(cls):
171 | return 8
172 |
173 | @classmethod
174 | def getAlignment(cls):
175 | return 8
176 |
177 | @classmethod
178 | def packVal(cls, val):
179 | return struct.pack("d", val)
180 |
181 | @classmethod
182 | def getPrefix(cls):
183 | return "d"
184 |
185 |
186 | @dataclass(frozen=True)
187 | class ShaderVec(ShaderType[B]):
188 |
189 | dtype: type[B]
190 | length: int
191 |
192 | @classmethod
193 | def getDefault(cls):
194 | return ()
195 |
196 | @cache
197 | def getName(self):
198 | return f"{self.dtype.getPrefix()}vec{self.length}"
199 |
200 | @cache
201 | def getSize(self):
202 | return self.dtype.getSize() * self.length
203 |
204 | @cache
205 | def getAlignment(self):
206 | return self.dtype.getSize() * (self.length + self.length % 2)
207 |
208 | @cache
209 | def packVal(self, val):
210 | packed = b"".join(self.dtype.packVal(v) for v in val)
211 | return packed + b"\x00" * (self.getSize() - len(packed))
212 |
213 |
214 | @dataclass(frozen=True)
215 | class ShaderArr(ShaderType[T]):
216 |
217 | dtype: type[T] | T
218 | length: int
219 |
220 | def __iter__(self) -> Iterator[T]:
221 | pass
222 |
223 | @classmethod
224 | def getDefault(cls):
225 | return ()
226 |
227 | @cache
228 | def _isScalarOrVectorArr(self):
229 | return (
230 | (isinstance(self.dtype, ShaderVec)) or
231 | (isinstance(self.dtype, type) and issubclass(self.dtype, BasicShaderType))
232 | )
233 |
234 | @cache
235 | def getName(self):
236 | return f"{self.dtype.getName()}[{self.length}]"
237 |
238 | @cache
239 | def getSize(self):
240 | if self._isScalarOrVectorArr():
241 | return self.getAlignment() * self.length
242 | return self.dtype.getSize() * self.length
243 |
244 | @cache
245 | def getAlignment(self):
246 | if self._isScalarOrVectorArr():
247 | return pad(self.dtype.getSize(), 16)
248 | return self.dtype.getAlignment()
249 |
250 | def packVal(self, val):
251 | align = self.getAlignment()
252 | padding = b"\x00" * (align - self.dtype.getSize())
253 | packed = b"".join(self.dtype.packVal(v) + padding for v in val)
254 | return packed + b"\x00" * (self.getSize() - len(packed))
255 |
256 |
257 | class ShaderMat(ShaderType):
258 |
259 | def __init__(self, dtype: type[ShaderFloatType], cols: int, rows: int = None):
260 | self.dtype = dtype
261 | self.cols = cols
262 | self.rows = rows if rows else cols
263 |
264 | @classmethod
265 | def getDefault(cls):
266 | return ()
267 |
268 | @cache
269 | def getName(self):
270 | dims = f"{self.cols}x{self.rows}" if self.cols != self.rows else self.cols
271 | return f"{self.dtype.getPrefix()}mat{dims}"
272 |
273 | @cache
274 | def getAlignment(self):
275 | return ShaderArr(ShaderVec(self.dtype, self.rows), self.cols).getAlignment()
276 |
277 | @cache
278 | def getSize(self):
279 | return ShaderArr(ShaderVec(self.dtype, self.rows), self.cols).getSize()
280 |
281 | @cache
282 | def packVal(self, val):
283 | return ShaderArr(ShaderVec(self.dtype, self.rows), self.cols).packVal(val)
284 |
285 |
286 | def _renderField(name: str):
287 | return property(
288 | fget=lambda self: self._getField(name),
289 | fset=lambda self, v: self._setField(name, v)
290 | )
291 |
292 |
293 | class ShaderStructMeta(type):
294 |
295 | def __new__(mcs, clsname, bases, attrs):
296 | hasComplexFields = False
297 | attrs["_renderFieldTypes"] = fields = {}
298 | for fieldName, f in attrs.items():
299 | if isinstance(f, ShaderType) or (isinstance(f, type) and issubclass(f, ShaderType)):
300 | attrs[fieldName] = _renderField(fieldName)
301 | fields[fieldName] = f
302 | if isinstance(f, type) and issubclass(f, ShaderStruct):
303 | hasComplexFields = True
304 | elif isinstance(f, ShaderArr):
305 | if isinstance(f.dtype, type) and issubclass(f.dtype, ShaderStruct):
306 | hasComplexFields = True
307 | attrs["_hasComplexFields"] = hasComplexFields # true for nested structs (see pack())
308 | return super().__new__(mcs, clsname, bases, attrs)
309 |
310 |
311 | class ShaderStruct(SimpleShaderType, metaclass=ShaderStructMeta):
312 | """Custom struct type for GLSL. Subclass and add fields through ShaderType instances.
313 |
314 | Load this struct into a Blender shader info object using the info's typedef_source() method with
315 | this class's getSource() result. Then, you can create uniform variables of this type using the
316 | info's uniform_buf() method and load instances of it by creating UBOs with the instances' pack()
317 | results as data.
318 | """
319 |
320 | _renderFieldTypes: dict[str, ShaderType]
321 | _hasComplexFields: bool
322 |
323 | def __init__(self):
324 | self._renderFieldVals = {n: t.getDefault() for n, t in self._renderFieldTypes.items()}
325 | self._packed: bytes | None = None
326 |
327 | def _getField(self, name: str):
328 | return self._renderFieldVals[name]
329 |
330 | def _setField(self, name: str, v):
331 | if self._renderFieldVals[name] != v:
332 | self._renderFieldVals[name] = v
333 | # invalidate packed cache (literally the entire point of this function & _getField())
334 | self._packed = None
335 |
336 | @classmethod
337 | @cache
338 | def getSource(cls):
339 | """Return the GLSL definition for this struct."""
340 | fields = "".join(f"{t.getName()} {name};" for name, t in cls._renderFieldTypes.items())
341 | return f"struct {cls.getName()} {{{fields}}};"
342 |
343 | @classmethod
344 | def getDefault(cls):
345 | return cls()
346 |
347 | @classmethod
348 | @cache
349 | def getName(cls):
350 | return cls.__name__
351 |
352 | @classmethod
353 | @cache
354 | def getSize(cls) -> int:
355 | size = 0
356 | for name, t in cls._renderFieldTypes.items():
357 | size = pad(size, t.getAlignment()) + t.getSize()
358 | return pad(size, cls.getAlignment())
359 |
360 | @classmethod
361 | @cache
362 | def getAlignment(cls):
363 | return pad(max(t.getAlignment() for name, t in cls._renderFieldTypes.items()), 16)
364 |
365 | @classmethod
366 | def packVal(cls, val: Self) -> bytes:
367 | if val is None:
368 | return b"\x00" * cls.getSize()
369 | if val._packed is not None:
370 | return val._packed
371 | packed = b""
372 | for name, t in cls._renderFieldTypes.items():
373 | packed = pad(packed, t.getAlignment()) + t.packVal(getattr(val, name))
374 | packed = pad(packed, cls.getAlignment())
375 | # for efficiency, packed values are usually cached & cache is invalidated when fields change
376 | # however, if this struct contains other structs, we can't rely on that, since these other
377 | # structs are themselves mutable! so if that's the case, just don't cache ever
378 | if not val._hasComplexFields:
379 | val._packed = packed
380 | return packed
381 |
382 | def pack(self):
383 | """Pack this struct to bytes."""
384 | return self.packVal(self)
385 |
--------------------------------------------------------------------------------
/berrybush/blender/updater.py:
--------------------------------------------------------------------------------
1 | # standard imports
2 | import json
3 | import urllib
4 | import urllib.request
5 | # 3rd party imports
6 | import bpy
7 | # internal imports
8 | from .common import paragraphLabel, getLayerData, setLayerData
9 | from .preferences import BerryBushPreferences, getPrefs
10 |
11 |
12 | class LatestVersionChecker():
13 |
14 | def __init__(self):
15 | self._hasChecked = False
16 |
17 | def _retrieveLatestRelease(self) -> tuple[tuple[int, int, int], str]:
18 | """Get the tag & release URL for the latest available version of BerryBush from GitHub."""
19 | url = "https://api.github.com/repos/hayden0729/berrybush/releases/latest"
20 | with urllib.request.urlopen(url) as response:
21 | data = json.load(response)
22 | return (tuple(int(v) for v in data["tag_name"].split(".")), data["html_url"])
23 |
24 | def check(self, currentVer: tuple[int, int, int], prefs: BerryBushPreferences):
25 | """Display a popup if a newer version of BerryBush than the installed one is available.
26 |
27 | (If this has already been done, do nothing)
28 | """
29 | if prefs.doUpdateChecks and not self._hasChecked:
30 | self._hasChecked = True
31 | try:
32 | latestVer, url = self._retrieveLatestRelease()
33 | except:
34 | print("Failed to retrieve the latest BerryBush version from GitHub")
35 | return
36 | if not (prefs.skipThisVersion and latestVer == tuple(prefs.latestKnownVersion)):
37 | # if the latest version isn't set to be skipped, always reset relevant prefs
38 | prefs.latestKnownVersion = latestVer
39 | prefs.skipThisVersion = False
40 | # finally, do the actual check
41 | if currentVer < latestVer:
42 | bpy.ops.brres.show_latest_version('INVOKE_DEFAULT',
43 | current=currentVer, latest=latestVer, url=url)
44 |
45 |
46 | LATEST_VERSION_CHECKER = LatestVersionChecker()
47 |
48 |
49 | @bpy.app.handlers.persistent
50 | def checkLatestVer(_):
51 | LATEST_VERSION_CHECKER.check(addonVer(), getPrefs(bpy.context))
52 |
53 |
54 | class ShowLatestVersion(bpy.types.Operator):
55 | """Link to the newest version of BerryBush."""
56 |
57 | bl_idname = "brres.show_latest_version"
58 | bl_label = "BerryBush Update Available!"
59 |
60 | current: bpy.props.IntVectorProperty(size=3)
61 | latest: bpy.props.IntVectorProperty(size=3)
62 | url: bpy.props.StringProperty()
63 |
64 | def execute(self, context: bpy.types.Context):
65 | bpy.ops.wm.url_open(url=self.url)
66 | return {'FINISHED'}
67 |
68 | def invoke(self, context: bpy.types.Context, event: bpy.types.Event):
69 | return context.window_manager.invoke_props_dialog(self)
70 |
71 | def draw(self, context: bpy.types.Context):
72 | preferences = getPrefs(context)
73 | currentStr = verStr(self.current)
74 | latestStr = verStr(self.latest)
75 | label = f"Click OK to view the latest release ({currentStr} → {latestStr})"
76 | self.layout.label(text=label)
77 | row = self.layout.row().split(factor=.5)
78 | row.prop(preferences, "doUpdateChecks", invert_checkbox=True, text="Disable Update Checks")
79 | skipRow = row.row()
80 | skipRow.enabled = preferences.doUpdateChecks
81 | skipRow.prop(preferences, "skipThisVersion")
82 |
83 |
84 | @bpy.app.handlers.persistent
85 | def update(_):
86 | """Update the active Blend file if it uses an outdated BerryBush version."""
87 | bpy.ops.brres.update()
88 |
89 |
90 | @bpy.app.handlers.persistent
91 | def saveVer(_):
92 | """Before saving, update the BerryBush version for every scene in the Blend file."""
93 | currentVer = addonVer()
94 | for scene in bpy.data.scenes:
95 | scene.brres.version = currentVer
96 |
97 |
98 | def addonVer() -> tuple[int, int, int]:
99 | """Current installed version of BerryBush, respresented as a tuple."""
100 | # this is all needed for the version setup descriped in scene.py
101 | from .. import bl_info # pylint: disable=import-outside-toplevel
102 | return bl_info["version"]
103 |
104 |
105 | def verStr(ver: tuple[int, int, int]):
106 | """Get a string representation for an addon version."""
107 | return ".".join(str(i) for i in ver)
108 |
109 |
110 | class UpdateBRRES(bpy.types.Operator):
111 | """Update the active Blend file if it uses an outdated BerryBush version."""
112 |
113 | bl_idname = "brres.update"
114 | bl_label = "Update BRRES Settings"
115 |
116 | def execute(self, context: bpy.types.Context):
117 | # get latest version & blendfile version from scene settings
118 | sceneSettings = bpy.data.scenes[0].brres
119 | sceneVer = tuple(sceneSettings.version)
120 | currentVer = addonVer()
121 | # special 1.0.0 detection (it used a different version system)
122 | if "version_" in sceneSettings:
123 | sceneVer = (1, 0, 0)
124 | for scene in bpy.data.scenes:
125 | del scene.brres["version_"]
126 | # perform updates
127 | if sceneVer != currentVer and sceneVer != (0, 0, 0):
128 | self.report({'INFO'}, f"Loading file saved using BerryBush version {verStr(sceneVer)}"
129 | f" (current version: {verStr(currentVer)})")
130 | if sceneVer < (1, 1, 0):
131 | # open vertex color gamma updater
132 | bpy.ops.brres.update_vert_colors_1_1_0('INVOKE_DEFAULT')
133 | if sceneVer < (1, 2, 0):
134 | # update texture transform names for fcurves
135 | for mat in bpy.data.materials:
136 | for tex in mat.brres.textures:
137 | tex.transform.name = tex.name
138 | for indTf in mat.brres.indSettings.transforms:
139 | indTf.transform.name = indTf.name
140 | if sceneVer < (1, 4, 0):
141 | # update texture transform names for fcurves
142 | for scene in bpy.data.scenes:
143 | for tevConfig in scene.brres.tevConfigs:
144 | if not tevConfig.users:
145 | tevConfig.fakeUser = True
146 | for scene in bpy.data.scenes:
147 | scene.brres.version = currentVer
148 | return {'FINISHED'}
149 |
150 |
151 | class UpdateVertColors1_1_0(bpy.types.Operator):
152 | """Update all vertex colors in the scene from BerryBush 1.0.0 conventions to 1.1.0."""
153 |
154 | bl_idname = "brres.update_vert_colors_1_1_0"
155 | bl_label = "Update Vertex Colors for BerryBush 1.1.0"
156 | bl_options = {'UNDO'}
157 |
158 | def execute(self, context: bpy.types.Context):
159 | layerData = {}
160 | for mesh in bpy.data.meshes:
161 | clrs = mesh.brres.meshAttrs.clrs
162 | meshLayerData = getLayerData(mesh, clrs, unique=False, doProcessing=False)
163 | for layer, data, indices in meshLayerData:
164 | if layer is not None and layer not in layerData:
165 | data[:, :3] **= 2.2
166 | layerData[layer] = data
167 | setLayerData(layerData)
168 | return {'FINISHED'}
169 |
170 | def invoke(self, context: bpy.types.Context, event: bpy.types.Event):
171 | return context.window_manager.invoke_props_dialog(self)
172 |
173 | def draw(self, context: bpy.types.Context):
174 | paragraphLabel(self.layout, "This file was saved using an older version of BerryBush, which had different conventions for importing, exporting, and rendering vertex colors. Click OK to update all BRRES vertex color layers to the new conventions (or click elsewhere to keep them unchanged, which will alter how they're rendered and exported).") # pylint: disable=line-too-long
175 |
--------------------------------------------------------------------------------
/berrybush/blender/verify.py:
--------------------------------------------------------------------------------
1 | # 3rd party imports
2 | import bpy
3 | import numpy as np
4 | # internal imports
5 | from .common import usedMatSlots
6 | from .limiter import ObjectLimiter, ObjectLimiterFactory
7 | from ..wii import gx
8 |
9 |
10 | def verifyBRRES(op: "VerifyBRRES", context: bpy.types.Context, limiter: ObjectLimiter):
11 | """Verify the scene's BRRES settings, reporting warnings.
12 |
13 | The number of warnings reported and number of warnings suppressed are returned."""
14 | numProblems = 0
15 | numSuppressed = 0
16 | usedMats = set()
17 | images: set[bpy.types.Image] = set()
18 | # invalid attribute references
19 | for rigObj in bpy.data.objects:
20 | if rigObj.type == 'ARMATURE' and limiter.includes(rigObj):
21 | for obj in bpy.data.objects:
22 | if limiter.includes(obj) and obj.parent is rigObj:
23 | try:
24 | mesh = obj.to_mesh()
25 | except RuntimeError:
26 | continue
27 | for attr in (*mesh.brres.meshAttrs.clrs, *mesh.brres.meshAttrs.uvs):
28 | if attr == "" or attr in mesh.uv_layers or attr in mesh.attributes:
29 | continue
30 | numProblems += 1
31 | e = (f"Mesh '{mesh.name}' references an attribute '{attr}' "
32 | f"in its BRRES settings, but has no such attribute")
33 | op.report({'INFO'}, e)
34 | # grab used materials for use in material verification
35 | objUsedMats = {matSlot.material for matSlot in usedMatSlots(obj, mesh)}
36 | if None in objUsedMats:
37 | numProblems += 1
38 | e = (f"Mesh '{mesh.name}' has geometry without an assigned material "
39 | "(this will not be exported)")
40 | op.report({'INFO'}, e)
41 | usedMats |= objUsedMats
42 | # material problems
43 | usedTevs = set()
44 | usedMats.discard(None)
45 | for mat in usedMats:
46 | # texture problems
47 | for tex in mat.brres.textures:
48 | for slot, texImg in enumerate(tex.imgs, 1):
49 | img = texImg.img
50 | images.add(img)
51 | # no image
52 | if img is None:
53 | numProblems += 1
54 | e = (f"Animation Slot {slot} of texture '{tex.name}' of material '{mat.name}' "
55 | f"is empty")
56 | op.report({'INFO'}, e)
57 | continue
58 | # non-power of 2 dims
59 | wrapModes = (tex.wrapModeU, tex.wrapModeV)
60 | for size, wrapMode in zip(img.size, wrapModes):
61 | if wrapMode != 'CLAMP' and bin(size).count("1") > 1:
62 | numProblems += 1
63 | e = (f"The image '{img.name}' used in Animation Slot {slot} of texture "
64 | f"'{tex.name}' of material '{mat.name}' has a non-power of 2 size on "
65 | f"an axis with non-clamp wrapping "
66 | f"(dimensions: {tuple(img.size)})")
67 | op.report({'INFO'}, e)
68 | break
69 | # materials w/o tev configs
70 | try:
71 | tev = context.scene.brres.tevConfigs[mat.brres.tevID]
72 | except KeyError:
73 | numProblems += 1
74 | op.report({'INFO'}, f"Material '{mat.name}' lacks a TEV configuration")
75 | continue
76 | # tev problems
77 | usedTevs.add(tev)
78 | numTextures = len(mat.brres.textures)
79 | numIndTransforms = len(mat.brres.indSettings.transforms)
80 | numLightChans = len(mat.brres.lightChans)
81 | for stage in tev.stages:
82 | # indirect problems
83 | if stage.indSettings.enable:
84 | # invalid texture references
85 | indTexSlot = tev.indTexSlots[stage.indSettings.slot - 1]
86 | if indTexSlot > numTextures:
87 | numProblems += 1
88 | e = (f"Stage '{stage.name}' of TEV config '{tev.name}' references texture slot "
89 | f"{indTexSlot} for its indirect texture, but material '{mat.name}', which "
90 | f"uses this config, lacks enough textures for this")
91 | op.report({'INFO'}, e)
92 | # invalid transform references
93 | mtxSlot = stage.indSettings.mtxSlot
94 | if mtxSlot > numIndTransforms:
95 | numProblems += 1
96 | e = (f"Stage '{stage.name}' of TEV config '{tev.name}' references indirect "
97 | f"texture transform slot {mtxSlot}, but material '{mat.name}', which uses "
98 | f"this config, lacks enough indirect texture transforms for this")
99 | op.report({'INFO'}, e)
100 | # invalid references in main tev args
101 | texSlot = stage.sels.texSlot
102 | rasSel = stage.sels.rasterSel
103 | args = (*stage.colorParams.args, *stage.alphaParams.args)
104 | # invalid texture references
105 | if texSlot > numTextures and ('TEX_COLOR' in args or 'TEX_ALPHA' in args):
106 | numProblems += 1
107 | e = (f"Stage '{stage.name}' of TEV config '{tev.name}' references texture slot "
108 | f"{texSlot}, but material '{mat.name}', which uses this config, lacks enough "
109 | f"textures for this")
110 | op.report({'INFO'}, e)
111 | # invalid light channel references
112 | if rasSel.startswith('COLOR'):
113 | rasSlot = int(rasSel[-1]) + 1
114 | if rasSlot > numLightChans and ('RASTER_COLOR' in args or 'RASTER_ALPHA' in args):
115 | numProblems += 1
116 | e = (f"Stage '{stage.name}' of TEV config '{tev.name}' references lighting "
117 | f"channel slot {rasSlot}, but material '{mat.name}', which uses this "
118 | f"config, lacks enough lighting channels for this")
119 | op.report({'INFO'}, e)
120 | for tev in usedTevs:
121 | # no stages
122 | if len([stage for stage in tev.stages if not stage.hide]) == 0:
123 | numProblems += 1
124 | op.report({'INFO'}, f"TEV config '{tev.name}' doesn't have any enabled stages")
125 | # referencing standard color slot 1 before it's set
126 | chanInfo = (
127 | ("RGB", "colorParams", 'STANDARD_0_COLOR'),
128 | ("Alpha", "alphaParams", 'STANDARD_0_ALPHA')
129 | )
130 | reg0SetStages = {}
131 | for chanName, paramsName, argName in chanInfo: # do this first for color, then for alpha
132 | # first, get first stage where standard slot 1 is set
133 | firstStageSet = 0
134 | for stage in tev.stages:
135 | params = getattr(stage, paramsName)
136 | if params.output == '0':
137 | break
138 | firstStageSet += 1
139 | reg0SetStages[chanName] = firstStageSet
140 | # then, find stages before that where standard slot 1 is used
141 | for stage in tev.stages[:firstStageSet + 1]: # stage where it's set gets included
142 | params = getattr(stage, paramsName)
143 | if argName in stage.colorParams.args or argName in stage.alphaParams.args:
144 | numProblems += 1
145 | e = (f"Stage '{stage.name}' of TEV config '{tev.name}' references Standard "
146 | f"Color Slot 1 {chanName} before a value is written to that register (its "
147 | f"initial state is undefined)")
148 | op.report({'INFO'}, e)
149 | # image problems
150 | for img in images:
151 | # excessive size
152 | dims = np.array(img.size, dtype=int)
153 | if np.any(dims > gx.MAX_TEXTURE_SIZE):
154 | if not op.includeSuppressed and img.brres.warnSupSize:
155 | numSuppressed += 1
156 | else:
157 | numProblems += 1
158 | e = (f"Image '{img.name}' has dimensions that aren't"
159 | f"both <= {gx.MAX_TEXTURE_SIZE}: {tuple(dims)}")
160 | op.report({'INFO'}, e)
161 | # improper mipmap dims
162 | for mm in img.brres.mipmaps:
163 | dims //= 2
164 | mmImg: bpy.types.Image = mm.img
165 | if mmImg is not None:
166 | mmDims = np.array(mmImg.size, dtype=int)
167 | if np.any(mmDims != dims):
168 | numProblems += 1
169 | e = (f"Image '{img.name}' has a mipmap '{mmImg.name}' with improper dimensions "
170 | f"(should be {tuple(dims)}, but are {tuple(mmDims)})")
171 | op.report({'INFO'}, e)
172 | return (numProblems, numSuppressed)
173 |
174 |
175 | def WarningSuppressionProperty():
176 | """Property for suppressing BRRES verifier warnings."""
177 | return bpy.props.BoolProperty(
178 | name="BRRES Warning",
179 | description="Click to suppress so that this will be ignored by the verifier",
180 | default=False,
181 | options=set()
182 | )
183 |
184 |
185 | def drawWarningUI(layout: bpy.types.UILayout, text: str,
186 | suppressionData = None, suppressionProp = ""):
187 | """Draw a BRRES verifier warning, optionally with a suppression button."""
188 | warnRow = layout.row()
189 | warnRow.alignment = 'CENTER'
190 | labelRow = warnRow.row()
191 | labelRow.alignment = 'CENTER'
192 | labelRow.label(text="", icon='ERROR')
193 | labelRow.label(text=text)
194 | if suppressionData:
195 | suppressionEnabled = getattr(suppressionData, suppressionProp)
196 | labelRow.enabled = not suppressionEnabled
197 | supIcon = 'HIDE_ON' if suppressionEnabled else 'HIDE_OFF'
198 | warnRow.prop(suppressionData, suppressionProp, text="", icon=supIcon, emboss=False)
199 |
200 |
201 | class VerifyBRRES(bpy.types.Operator):
202 | """Verify the scene's BRRES settings"""
203 |
204 | bl_idname = "brres.verify"
205 | bl_label = "Verify BRRES Settings"
206 |
207 | limitTo: bpy.props.EnumProperty(
208 | name="Limit To",
209 | description="Data to verify (only includes assets that would be used by the corresponding export setting)", # pylint: disable=line-too-long
210 | items=(
211 | ('ALL', "All", ""),
212 | ('SELECTED', "Selected", ""),
213 | ('VISIBLE', "Visible", ""),
214 | ),
215 | default='ALL'
216 | )
217 |
218 | includeSuppressed: bpy.props.BoolProperty(
219 | name="Bypass Warning Suppression",
220 | description="Report all detected problems, including those flagged to be ignored",
221 | default=False
222 | )
223 |
224 | def execute(self, context: bpy.types.Context):
225 | self.report({'INFO'}, "Searching for BRRES warnings to report...")
226 | limiter = ObjectLimiterFactory.create(context, self.limitTo)
227 | warns, suppressed = verifyBRRES(self, context, limiter)
228 | if warns:
229 | plural = "s" if warns > 1 else ""
230 | sup = f", plus {suppressed} suppressed" if suppressed else ""
231 | e = f"{warns} BRRES warning{plural} reported{sup}. Check the Info Log for details"
232 | self.report({'WARNING'}, e)
233 | elif suppressed:
234 | plural = "s" if suppressed > 1 else ""
235 | self.report({'INFO'}, f"{suppressed} BRRES warning{plural} reported & suppressed")
236 | else:
237 | self.report({'INFO'}, "No BRRES warnings reported")
238 | return {'FINISHED'}
239 |
240 | def invoke(self, context: bpy.types.Context, event: bpy.types.Event):
241 | return context.window_manager.invoke_props_dialog(self)
242 |
243 | def draw(self, context: bpy.types.Context):
244 | layout = self.layout
245 | layout.use_property_split = True
246 | self.layout.prop(self, "limitTo")
247 | self.layout.prop(self, "includeSuppressed")
248 |
249 |
250 | def drawOp(self, context: bpy.types.Context):
251 | layout: bpy.types.UILayout = self.layout
252 | layout.separator()
253 | layout.operator(VerifyBRRES.bl_idname, text="Verify BRRES Settings", icon='CHECKMARK')
254 |
--------------------------------------------------------------------------------
/berrybush/wii/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/berrybush/wii/__init__.py
--------------------------------------------------------------------------------
/berrybush/wii/alias.py:
--------------------------------------------------------------------------------
1 | from collections.abc import Sequence
2 |
3 |
4 | def alias(*attrs: str, forceList: bool = False):
5 | """Property that works identically to some attribute, just under a different name.
6 |
7 | You can provide multiple attribute names to make this property
8 | act like a list containing all of them.
9 | If you want the list behavior with only one attribute, you can use the
10 | "forceList" keyword argument.
11 | """
12 | # for multi attrs, use attr list & special methods to make stuff as easy & intuitive as possible
13 | if len(attrs) > 1 or forceList:
14 | return property(
15 | fget=lambda self: AttrList(self, *attrs),
16 | fset=lambda self, v: replaceVals(self, attrs, v),
17 | fdel=lambda self: deleteVals(self, attrs))
18 | # if there's only one attr, just make the property work exactly like the attr
19 | return property(
20 | fget=lambda self: getattr(self, *attrs),
21 | fset=lambda self, v: setattr(self, *attrs, v),
22 | fdel=lambda self: delattr(self, *attrs)
23 | )
24 |
25 |
26 | def replaceVals(obj, attrs: tuple[str, ...], newVals):
27 | """Replace the values of some attributes on an object with a set of new ones."""
28 | for attr, val in zip(attrs, newVals):
29 | setattr(obj, attr, val)
30 |
31 |
32 | def deleteVals(obj, attrs: tuple[str, ...]):
33 | """Delete a set of attributes on an object."""
34 | for attr in attrs:
35 | delattr(obj, attr)
36 |
37 |
38 | class AttrList(Sequence):
39 | """List of some attributes for some parent object.
40 |
41 | Using list-like access, the object's values for these attributes can be both retrieved and set.
42 |
43 | The attributes are referenced by name in the constructor, and then afterwards, they can be
44 | found at indices based on the order in which they were passed.
45 | This is used for multi-attribute aliases.
46 | """
47 |
48 | def __init__(self, parent, *attrs: str):
49 | self._parent = parent
50 | self._attrs = list(attrs)
51 |
52 | def __getitem__(self, i):
53 | if isinstance(i, slice):
54 | return self.__class__(self._parent, *self._attrs[i])
55 | return getattr(self._parent, self._attrs[i])
56 |
57 | def __len__(self):
58 | return len(self._attrs)
59 |
60 | def __setitem__(self, key, val):
61 | if isinstance(key, slice):
62 | for attr, v in zip(self._attrs[key], val):
63 | setattr(self._parent, attr, v)
64 | else:
65 | setattr(self._parent, self._attrs[key], val)
66 |
67 | def __repr__(self):
68 | return "AttrList" + repr(tuple(self))
69 |
--------------------------------------------------------------------------------
/berrybush/wii/binaryutils.py:
--------------------------------------------------------------------------------
1 | # standard imports
2 | from functools import cache
3 | from numbers import Number
4 |
5 |
6 | @cache
7 | def maxDifBit(i1: int, i2: int):
8 | """Return the index (from the right) of the most significant bit that's different for two ints.
9 |
10 | If they are equal, return -1.
11 | If they have different lengths, return the length of the longest one.
12 | """
13 | idx = -1
14 | while i1 != i2:
15 | i1 >>= 1
16 | i2 >>= 1
17 | idx += 1
18 | return idx
19 |
20 |
21 | @cache
22 | def strToInt(string: str):
23 | """Convert an ASCII string to an int representation."""
24 | return int.from_bytes(string.encode("ascii"), "big")
25 |
26 |
27 | def maxBitVal(numBits: int):
28 | """Return the maximum value possible for a value stored in some number of bits."""
29 | return (1 << numBits) - 1
30 |
31 |
32 | @cache
33 | def normBitVal(val: float, numBits: int):
34 | """Normalize some value from 0-1 based on the max allowed by the # bits in which it's stored."""
35 | return val / maxBitVal(numBits)
36 |
37 |
38 | @cache
39 | def denormBitVal(val: float, numBits: int):
40 | """Inverse of normBitVal; multiply val by max allowed by # bits in which it's stored.
41 |
42 | Note that the output is rounded to the nearest integer, so some decimal precision may be lost.
43 | """
44 | return round(val * maxBitVal(numBits))
45 |
46 |
47 | @cache
48 | def pad(obj, n: int, startOffset = 0, extra: bool = False):
49 | """Pad an object (number or sequence of bytes) until it or its length is a multiple of n.
50 |
51 | Optionally, you can pass in a start offset that's temporarily added for the calculation.
52 | You can also set whether n should be added an additional time if it's exactly a multiple of n.
53 | """
54 | objLen = len(obj) if not isinstance(obj, Number) else obj
55 | padLen = n - ((startOffset + objLen) % n)
56 | if not extra:
57 | padLen %= n
58 | padding = bytearray(padLen) if not isinstance(obj, Number) else padLen
59 | return obj + padding
60 |
61 |
62 | def bitsToBytes(b: int):
63 | """Get the number of bytes that a number of bits represents."""
64 | return -(b // -8) # ceiling division
65 |
66 |
67 | def calcOffset(a: int, b: int):
68 | """Calculate an offset from address A to B. If either is 0, return 0."""
69 | if (a == 0 or b == 0):
70 | return 0
71 | return b - a
72 |
--------------------------------------------------------------------------------
/berrybush/wii/bitstruct.py:
--------------------------------------------------------------------------------
1 | # standard imports
2 | from typing import TypeVar
3 | # internal imports
4 | from .binaryutils import maxBitVal, bitsToBytes
5 |
6 |
7 | T = TypeVar("T")
8 |
9 |
10 | class _Bits:
11 | """Defines the format of one BitStruct field. See BitStruct for details."""
12 |
13 | def __init__(self, size: int, dtype: type, default = 0):
14 | self.size = size
15 | self.dtype = dtype
16 | self.mask = maxBitVal(self.size)
17 | self.maxVal = self.mask
18 | self.minVal = 0
19 | self.default = default
20 |
21 | def outOfRange(self, v: int):
22 | """Raise an error for an out-of-range value."""
23 | raise ValueError(f"Invalid value for BitStruct field (value is {v}, "
24 | f"but format only allows {self.minVal} to {self.maxVal})")
25 |
26 | def checkRange(self, v: int):
27 | """Verify that this format can represent some value."""
28 | return self.minVal <= v and v <= self.maxVal
29 |
30 | def unpack(self, v: int):
31 | """Unpack bits for this format."""
32 | return self.dtype(v)
33 |
34 | def pack(self, v):
35 | """Pack a value for this format to bits."""
36 | v = int(v)
37 | if not self.checkRange(v):
38 | self.outOfRange(v)
39 | return v
40 |
41 |
42 | class SignedBitsMixin():
43 |
44 | def __init__(self, size: int, dtype: type, default = 0):
45 | super().__init__(size, dtype, default)
46 | self.maxVal = maxBitVal(self.size - 1)
47 | self.minVal = -self.maxVal - 1
48 |
49 | def unpack(self, v: int):
50 | """Unpack bits for this format."""
51 | if v & (1 << (self.size - 1)):
52 | # read negative number from binary
53 | v = -(self.mask & ~v + 1)
54 | return super().unpack(v)
55 |
56 | def pack(self, v):
57 | """Pack a value for this format to bits."""
58 | v = super().pack(v)
59 | if v < 0:
60 | # get two's complement & add sign bit
61 | # (while keeping value positive in python so that it can be packed properly)
62 | v = self.mask & ~abs(v) + 1
63 | return v
64 |
65 |
66 | class NormalizedBitsMixin():
67 |
68 | def __init__(self, size: int, dtype: type, default = 0):
69 | super().__init__(size, dtype, default)
70 |
71 | def outOfRange(self, v: int):
72 | raise ValueError(f"Invalid value for BitStruct field (value is {v / self.maxVal}, "
73 | f"but format only allows {self.minVal / self.maxVal} to {1})")
74 |
75 | def unpack(self, v: int):
76 | """Unpack bits for this format."""
77 | return super().unpack(v) / self.maxVal
78 |
79 | def pack(self, v):
80 | """Pack a value for this format to bits."""
81 | return super().pack(v * self.maxVal)
82 |
83 |
84 | class _SignedBits(SignedBitsMixin, _Bits):
85 | """Bits that support signed values (which lowers the maximum possible value!)."""
86 |
87 | class _NormalizedBits(NormalizedBitsMixin, _Bits):
88 | """Bits that should be interpreted as normalized to the range 0 to 1."""
89 |
90 | class _NormalizedSignedBits(NormalizedBitsMixin, SignedBitsMixin, _Bits):
91 | """Bits that should be interpreted as normalized to the range -1 to 1."""
92 |
93 |
94 | # fake constructors for Bits classes so we can have type hinting for dtype
95 |
96 | def Bits(size: int, dtype: type[T], default = 0) -> T:
97 | return _Bits(size, dtype, default)
98 |
99 | def SignedBits(size: int, dtype: type[T], default = 0) -> T:
100 | return _SignedBits(size, dtype, default)
101 |
102 | def NormalizedBits(size: int, dtype: type[T], default = 0) -> T:
103 | return _NormalizedBits(size, dtype, default)
104 |
105 | def NormalizedSignedBits(size: int, dtype: type[T], default = 0) -> T:
106 | return _NormalizedSignedBits(size, dtype, default)
107 |
108 |
109 | def _bitProperty(fmt: _Bits, bitIdx: int):
110 | return property(
111 | fget=lambda self: self._getField(fmt, bitIdx),
112 | fset=lambda self, v: self._setField(fmt, bitIdx, v)
113 | )
114 |
115 |
116 | class BitStructMeta(type):
117 |
118 | def __new__(mcs, clsname, bases, attrs):
119 | bitIdx = 0
120 | attrs["_bitFmts"] = bitFmts = {}
121 | for attrName, attr in attrs.items():
122 | if isinstance(attr, _Bits):
123 | attrs[attrName] = _bitProperty(attr, bitIdx)
124 | bitFmts[attr] = bitIdx
125 | bitIdx += attr.size
126 | attrs["size"] = bitsToBytes(bitIdx)
127 | return super().__new__(mcs, clsname, bases, attrs)
128 |
129 |
130 | class BitStruct(object, metaclass=BitStructMeta):
131 | """Used for creating binary data structures that hold values of potentially varying sizes.
132 |
133 | Subclass and add fields through the Bits class. Any non-Bits members are ignored on pack/unpack.
134 |
135 | The bit format includes 3 components: size, type, and default.
136 | Size is the only required one, and defines its size in bits.
137 |
138 | "type" is the type of data. It should be convertible to & from an int.
139 |
140 | "default" is the field's default value for every class instance.
141 |
142 | Simple example::
143 |
144 | class TestStruct(BitStruct):
145 | a = Bits(1, bool)
146 | b = int = 3
147 | c = Bits(3, int)
148 |
149 | In this case, a has a size of 1 bit and c has 3. b gets ignored.
150 | Note that from top to bottom, members are sorted from least significant bits to most.
151 | The bits here would be arranged like "ccca" (0101) when packed, since b would get ignored.
152 | """
153 |
154 | size: int
155 | """Size of this BitStruct in bytes."""
156 |
157 | def __init__(self, val: int = None):
158 | if val is None:
159 | self._val = 0
160 | for fmt, bitIdx in self._bitFmts.items():
161 | self._setField(fmt, bitIdx, fmt.default)
162 | else:
163 | self._val = val
164 |
165 | def __eq__(self, other: "BitStruct"):
166 | return isinstance(other, BitStruct) and self._val == other._val
167 |
168 | def __int__(self):
169 | return self._val
170 |
171 | def copy(self):
172 | """Quickly return a copy of this BitStruct with the same values."""
173 | return type(self)(self._val)
174 |
175 | @classmethod
176 | def unpack(cls, b: bytes):
177 | """Unpack a BitStruct from a sequence of bytes."""
178 | return cls(int.from_bytes(b, "big"))
179 |
180 | def pack(self):
181 | """Pack this BitStruct to bytes (big endian, as few as possible based on format)."""
182 | return self._val.to_bytes(self.size, "big")
183 |
184 | def _getField(self, fmt: _Bits, bitIdx: int):
185 | return fmt.unpack(self._val >> bitIdx & fmt.mask)
186 |
187 | def _setField(self, fmt: _Bits, bitIdx: int, v: int):
188 | self._val = (self._val & ~(fmt.mask << bitIdx)) | (fmt.pack(v) << bitIdx)
189 |
--------------------------------------------------------------------------------
/berrybush/wii/brres.py:
--------------------------------------------------------------------------------
1 | # standard imports
2 | from functools import cache
3 | from struct import Struct
4 | from typing import TypeVar
5 | # internal imports
6 | from .binaryutils import pad
7 | from .brresdict import DictReader, DictWriter
8 | from .serialization import Serializer, Reader, Writer, StrPoolReadMixin, StrPoolWriteMixin
9 | from .subfile import Subfile, SubfileReader, SubfileWriter
10 | from .common import getKey
11 | from .mdl0 import MDL0, MDL0Reader, MDL0Writer
12 | from .tex0 import TEX0, TEX0Reader, TEX0Writer
13 | from .plt0 import PLT0, PLT0Reader, PLT0Writer
14 | from .chr0 import CHR0, CHR0Reader, CHR0Writer
15 | from .clr0 import CLR0, CLR0Reader, CLR0Writer
16 | from .pat0 import PAT0, PAT0Reader, PAT0Writer
17 | from .srt0 import SRT0, SRT0Reader, SRT0Writer
18 | from .vis0 import VIS0, VIS0Reader, VIS0Writer
19 |
20 |
21 | SUBFILE_T = TypeVar("SUBFILE_T", bound=Subfile)
22 | SUBFILE_TYPES = (MDL0, TEX0, PLT0, CHR0, CLR0, PAT0, SRT0, VIS0)
23 |
24 |
25 | class BRRES():
26 | """Binary Revolution RESource, used to store 3D Wii assets as part of the NW4R library."""
27 |
28 | def __init__(self):
29 | self.files: dict[type[Subfile], list[Subfile]] = {}
30 |
31 | def folder(self, fileType: type[SUBFILE_T]) -> list[SUBFILE_T]:
32 | """Get the folder of this BRRES for some file type, creating a new one if none exists."""
33 | try:
34 | return self.files[fileType]
35 | except KeyError:
36 | self.files[fileType] = []
37 | return self.files[fileType]
38 |
39 | def allFiles(self) -> tuple[Subfile]:
40 | """One flat tuple containing all the files of this BRRES."""
41 | return tuple(f for fldr in self.files.values() for f in fldr)
42 |
43 | def search(self, fileType: type[SUBFILE_T], fileName: str) -> SUBFILE_T:
44 | """Search this BRRES for a subfile with the specified type & name.
45 |
46 | Raise a ValueError if the subfile is not found.
47 | """
48 | try:
49 | return next(f for f in self.files[fileType] if f.name == fileName)
50 | except (KeyError, StopIteration) as e:
51 | raise ValueError(f"{fileType} file '{fileName}' not found") from e
52 |
53 | def sort(self):
54 | """Sort the folders and files of this BRRES filesystem."""
55 | st = SUBFILE_TYPES
56 | self.files = {
57 | t: sorted(self.files[t], key=lambda f: f.name.casefold()) for t in st if t in self.files
58 | }
59 |
60 | @classmethod
61 | def unpack(cls, data: bytes) -> "BRRES":
62 | """Unpack a BRRES file from bytes."""
63 | return BRRESReader().unpack(data).getInstance()
64 |
65 | def pack(self):
66 | """Pack this BRRES file to bytes."""
67 | return BRRESWriter().fromInstance(self).pack()
68 |
69 |
70 | class BRRESSerializer(Serializer[None, BRRES]):
71 |
72 | MAGIC = b"bres"
73 |
74 | _HEAD_STRCT = Struct(">4s 2s 2x IHH")
75 | _ROOT_STRCT = Struct(">4s I")
76 |
77 | def __init__(self):
78 | super().__init__()
79 | self._stringPool = {}
80 |
81 |
82 | class BRRESReader(BRRESSerializer, Reader, StrPoolReadMixin):
83 |
84 | _stringPool: dict[int, str]
85 |
86 | _FILE_READERS: dict[str, type[SubfileReader]] = {r.FOLDER_NAME: r for r in (
87 | MDL0Reader, TEX0Reader, PLT0Reader, CHR0Reader,
88 | CLR0Reader, PAT0Reader, SRT0Reader, VIS0Reader
89 | )}
90 |
91 | def __init__(self):
92 | super().__init__()
93 | self.files: dict[type[Subfile], dict[str, SubfileReader]] = {}
94 |
95 | def fileName(self, fileReader: SubfileReader):
96 | """Name of a subfile in this BRRES."""
97 | return getKey(self.files[fileReader.DATA_TYPE], fileReader)
98 |
99 | def unpack(self, data: bytes):
100 | super().unpack(data)
101 | self._data = BRRES()
102 | unpackedHeader = self._HEAD_STRCT.unpack_from(data, self.offset)
103 | byteOrder = unpackedHeader[1]
104 | if byteOrder == b"\xff\xfe":
105 | raise NotImplementedError("Little-endian BRRES files are currently unsupported")
106 | rootOffset = unpackedHeader[3] + self.offset
107 | dictOffset = rootOffset + self._ROOT_STRCT.size
108 | files = DictReader(self, dictOffset).unpack(data).readEntries(data, DictReader)
109 | for folderName, folder in files.items():
110 | try:
111 | reader = self._FILE_READERS[folderName]
112 | self.files[reader.DATA_TYPE] = folder.readEntries(data, reader)
113 | except KeyError:
114 | print(f"Unsupported BRRES folder '{folderName}' detected & ignored")
115 | return self
116 |
117 | def _updateInstance(self):
118 | super()._updateInstance()
119 | self._data.files = {t: [f.getInstance() for f in d.values()] for t, d in self.files.items()}
120 |
121 | def readString(self, data: bytes, offset: int):
122 | if offset not in self._stringPool:
123 | length = int.from_bytes(data[offset - 4 : offset], "big")
124 | self._stringPool[offset] = data[offset : offset + length].decode("ascii")
125 | return self._stringPool[offset]
126 |
127 |
128 | class BRRESWriter(BRRESSerializer, Writer, StrPoolWriteMixin):
129 |
130 | _stringPool: dict[str, int]
131 |
132 | _FILE_WRITERS: dict[type[Subfile], type[SubfileWriter]] = {w.DATA_TYPE: w for w in (
133 | MDL0Writer, TEX0Writer, PLT0Writer, CHR0Writer,
134 | CLR0Writer, PAT0Writer, SRT0Writer, VIS0Writer
135 | )}
136 |
137 | def __init__(self):
138 | super().__init__()
139 | self.files: DictWriter[BRRESWriter] = DictWriter(self)
140 |
141 | @classmethod
142 | def _rootPad(cls, obj):
143 | """Padding applied to BRRES roots before the file data."""
144 | return pad(obj, 32, 16)
145 |
146 | @classmethod
147 | def _finalPad(cls, obj):
148 | """Padding applied to whole BRRES files once everything's been packed."""
149 | return pad(obj, 128)
150 |
151 | def stringOffset(self, string: str):
152 | return self._stringPool.get(string, 0)
153 |
154 | @classmethod
155 | @cache
156 | def _packStr(cls, string: str):
157 | """Pack a string to bytes in the BRRES string format (u32 length followed by string)."""
158 | return len(string).to_bytes(4, "big") + pad(string.encode("ascii"), 4, extra=True)
159 |
160 | @classmethod
161 | @cache
162 | def _strSize(cls, string: str):
163 | """Get the size in bytes of a string in its BRRES representation."""
164 | return 4 + pad(len(string), 4, extra=True) # add 4 bc strings are preceded by u32 length
165 |
166 | def fromInstance(self, data: BRRES):
167 | super().fromInstance(data)
168 | offset = self.offset
169 | # generate file/folder writers
170 | files = {}
171 | folderOffset = offset + self._HEAD_STRCT.size
172 | rootFldrSize = DictWriter.sizeFromLen(len(data.files))
173 | folderSize = sum(DictWriter.sizeFromLen(len(fldr)) for fldr in data.files.values())
174 | fileOffset = folderOffset + self._rootPad(self._ROOT_STRCT.size + rootFldrSize + folderSize)
175 | rootFldrOffset = folderOffset + self._ROOT_STRCT.size
176 | folderOffset = rootFldrOffset + rootFldrSize
177 | for folderType, folder in data.files.items():
178 | if folderType is TEX0:
179 | # the tex0 folder must be padded to 16, and always is in retail files
180 | # (however, this IS a special case - other folders may start at in-between offsets)
181 | fileOffset = pad(fileOffset, 16)
182 | writer = self._FILE_WRITERS[folderType]
183 | writers = {} # file writers for this folder
184 | for file in folder:
185 | fileWriter = writer(self, fileOffset).fromInstance(file)
186 | writers[file.name] = fileWriter
187 | fileOffset += fileWriter.size()
188 | folderWriter = DictWriter(self, folderOffset).fromInstance(writers)
189 | files[writer.FOLDER_NAME] = folderWriter
190 | folderOffset += folderWriter.size()
191 | self.files = DictWriter(self, rootFldrOffset).fromInstance(files)
192 | # generate string pool
193 | offset = fileOffset + 4 # offset of the first string in the pool
194 | for string in sorted(self.getStrings(), key=lambda s: s.encode("ascii")):
195 | if string not in self._stringPool:
196 | self._stringPool[string] = offset
197 | offset += self._strSize(string)
198 | self._size = self._finalPad(offset - self.offset)
199 | return self
200 |
201 | def _calcSize(self):
202 | return super()._calcSize()
203 |
204 | def getStrings(self):
205 | return self.files.getStrings()
206 |
207 | def pack(self) -> bytes:
208 | # pack root w/ main dict holding all resources
209 | nest = self.files.nest
210 | packedRes = b"".join(f.pack() for f in nest)
211 | rootSize = self._ROOT_STRCT.size + len(packedRes)
212 | packedRoot = self._rootPad(self._ROOT_STRCT.pack(b"root", rootSize) + packedRes)
213 | # pack files
214 | packedFiles = b""
215 | numFiles = 1 # starts at 1 bc the root is counted as a "file" for this count
216 | for folder in nest[1:]:
217 | files = folder.getInstance().values()
218 | if files and isinstance(next(iter(files)), TEX0Writer):
219 | packedFiles = pad(packedFiles, 16) # special tex0 pad detailed in fromInstance()
220 | packedFiles += b"".join(file.pack() for file in files)
221 | numFiles += len(files)
222 | # pack strings
223 | packedStrs = b"".join(self._packStr(string) for string in self._stringPool)
224 | # pack header
225 | rootOffset = self._HEAD_STRCT.size
226 | bom = b"\xfe\xff"
227 | packedHead = self._HEAD_STRCT.pack(self.MAGIC, bom, self._size, rootOffset, numFiles)
228 | return super().pack() + self._finalPad(packedHead + packedRoot + packedFiles + packedStrs)
229 |
--------------------------------------------------------------------------------
/berrybush/wii/brresdict.py:
--------------------------------------------------------------------------------
1 | # standard imports
2 | from struct import Struct
3 | from typing import TypeVar
4 | # internal imports
5 | from .binaryutils import strToInt, maxDifBit, calcOffset, maxBitVal
6 | from .serialization import (
7 | S_PARENT_T, Readable, Writable, Serializer, Reader, Writer, StrPoolReadMixin, StrPoolWriteMixin
8 | )
9 |
10 |
11 | _R_STR_T = TypeVar("_R_STR_T", "Readable", "StrPoolReadMixin")
12 | _W_STR_T = TypeVar("_W_STR_T", "Writable", "StrPoolWriteMixin")
13 | _S_ENTRY_T = TypeVar("_S_ENTRY_T")
14 | _R_ENTRY_T = TypeVar("_R_ENTRY_T", bound="Readable")
15 |
16 |
17 | class DictSerializer(Serializer[S_PARENT_T, dict[str, _S_ENTRY_T]]):
18 | """Serializer for an ordered map with string keys stored in a BRRES file."""
19 | DATA_TYPE = dict
20 | _HEAD_STRCT = Struct(">II")
21 |
22 |
23 | class DictReader(DictSerializer[_R_STR_T, int], Reader, StrPoolReadMixin):
24 | """Reader for an ordered map with string keys stored in a BRRES file."""
25 |
26 | def __init__(self, parent: S_PARENT_T = None, offset = 0):
27 | super().__init__(parent, offset)
28 | self._duplicateName = ""
29 | """If any entry names are duplicated, one is stored here to be used for an error message."""
30 |
31 | def unpack(self, data: bytes):
32 | super().unpack(data)
33 | offset = self._offset
34 | self._data = {}
35 | self._duplicateName = ""
36 | unpackedHeader = self._HEAD_STRCT.unpack_from(data, offset)
37 | numEntries = unpackedHeader[1]
38 | entrySize = EntryStruct.size()
39 | firstEntryOffset = offset + self._HEAD_STRCT.size + entrySize
40 | lastEntryOffset = firstEntryOffset + entrySize * numEntries
41 | # parse entry names & add offsets to dict, keeping track of duplicate names if any
42 | for entryOffset in range(firstEntryOffset, lastEntryOffset, entrySize):
43 | entry = EntryStruct().unpack(data[entryOffset:])
44 | nameOffset = offset + entry.nameOffset
45 | dataOffset = offset + entry.dataOffset
46 | name = self.readString(data, nameOffset)
47 | if name in self._data:
48 | self._duplicateName = name
49 | self._data[name] = dataOffset
50 | return self
51 |
52 | def readEntries(self, data: bytes, entryType: type[_R_ENTRY_T]):
53 | """Unpack this dict's entries and return a dict containing them.
54 |
55 | The entry type is required to determine how to unpack the entries. Their parents will be
56 | set to the parent of this dict. If any of this dict's entries point to the same offset,
57 | they'll be given the same reader object.
58 | """
59 | if self._duplicateName:
60 | raise ValueError(f"Cannot unpack BRRES collection of type '{entryType.__name__}' "
61 | f"containing multiple entries with the same name "
62 | f"('{self._duplicateName}')")
63 | unpackedByOffset: dict[int, _R_ENTRY_T] = {}
64 | unpackedByKey: dict[str, _R_ENTRY_T] = {}
65 | for key, offset in self._data.items():
66 | if offset not in unpackedByOffset:
67 | unpackedByOffset[offset] = entryType(self.parentSer, offset).unpack(data)
68 | unpackedByKey[key] = unpackedByOffset[offset]
69 | return unpackedByKey
70 |
71 |
72 | class DictWriter(DictSerializer[_W_STR_T, Writer], Writer, StrPoolWriteMixin):
73 | """Writer for an ordered map with string keys stored in a BRRES file."""
74 |
75 | @classmethod
76 | def sizeFromLen(cls, length: int):
77 | """Calculate the size in bytes of a dict with [length] entries."""
78 | return cls._HEAD_STRCT.size + EntryStruct.size() * (length + 1)
79 |
80 | def _calcSize(self):
81 | return self.sizeFromLen(len(self._data))
82 |
83 | def getStrings(self):
84 | entries = self._data.values()
85 | strs = set(self._data.keys())
86 | return strs.union(*(e.getStrings() for e in entries if isinstance(e, StrPoolWriteMixin)))
87 |
88 | @property
89 | def nest(self):
90 | """Tuple containing this writer as well as any nested dict writer entries."""
91 | nest = tuple(ne for e in self._data.values() if isinstance(e, DictWriter) for ne in e.nest)
92 | return (self, ) + nest
93 |
94 | def pack(self):
95 | # 1 entry is added to the beginning to act as a root for the binary search tree
96 | processedEntries = [EntryStruct()]
97 | for entryName, entryData in self._data.items():
98 | processedEntry = EntryStruct.generate(self._data, processedEntries)
99 | processedEntry.dataOffset = calcOffset(self.offset, entryData.offset)
100 | processedEntry.nameOffset = calcOffset(self.offset, self.stringOffset(entryName))
101 | processedEntries.append(processedEntry)
102 | packedEntries = b"".join(entry.pack() for entry in processedEntries)
103 | packedHeader = self._HEAD_STRCT.pack(self._size, len(processedEntries) - 1)
104 | return super().pack() + packedHeader + packedEntries
105 |
106 |
107 | class EntryStruct(Readable, Writable):
108 | """Entry in a BRRES dict.
109 |
110 | This is structured the way that BRRES dict entries are structured in packed form, and it is only
111 | to be used in BRRES dicts internally for unpacking/packing.
112 | """
113 |
114 | _STRCT = Struct(">HxxHHII")
115 |
116 | def __init__(self, idx = 0):
117 | super().__init__()
118 | self.id = maxBitVal(16) # id for comparisons in binary tree traversal (defaults to max)
119 | self.idxL = idx # index of left child in parent dict (if no children, is this entry's idx)
120 | self.idxR = idx # index of right child in parent dict (if no children, is this entry's idx)
121 | self.nameOffset = 0 # offset to entry's name in brres, relative to start of parent dict
122 | self.dataOffset = 0 # offset to entry's data in brres, relative to start of parent dict
123 |
124 | def unpack(self, data: bytes):
125 | unpacked = self._STRCT.unpack_from(data)
126 | self.id, self.idxL, self.idxR, self.nameOffset, self.dataOffset = unpacked
127 | return self
128 |
129 | @classmethod
130 | def size(cls): # classmethod override is fine imo - pylint: disable=arguments-differ
131 | return cls._STRCT.size
132 |
133 | def pack(self):
134 | return self._STRCT.pack(self.id, self.idxL, self.idxR, self.nameOffset, self.dataOffset)
135 |
136 | def getIDBit(self, entryName: str):
137 | """For some name, get its bit to which this entry's ID points."""
138 | charIdx = self.id >> 3
139 | bitIdx = self.id & 0b111
140 | return charIdx < len(entryName) and strToInt(entryName[charIdx]) >> bitIdx & 1
141 |
142 | @classmethod
143 | def calcID(cls, n1: str, n2: str):
144 | """Calculate an entry ID for the search tree based on a comparison of two entries' names.
145 |
146 | The entry corresponding to the first name provided should receive the ID.
147 |
148 | This 16-bit ID is made up of two components:
149 | Most significant 13 bits: The index of the last character that differs between the two names
150 | (or the last possible index in n1, if it's longer than n2)
151 | Least significant 3 bits: The index of the most significant bit that differs between the
152 | characters at that index in each name
153 | """
154 | charIdx = len(n1) - 1
155 | if len(n1) <= len(n2):
156 | charIdx = [i for i, (c1, c2) in enumerate(zip(n1, n2)) if c1 != c2][-1]
157 | bitIdx = maxDifBit(strToInt(n1[charIdx]), strToInt(n2[charIdx]) if charIdx < len(n2) else 0)
158 | return (charIdx << 3) | (bitIdx)
159 |
160 | @classmethod
161 | def generate(cls, d: dict[str], entries: list["EntryStruct"]):
162 | """Generate a struct for a BRRES dict entry and calculate its ID & left/right indices.
163 |
164 | A dict and a list of other structs (each corresponding to one of the dict's entries up to
165 | this one) are required.
166 |
167 | Structs in the aforementioned list may have their left & right indices changed as this
168 | entry is inserted into the binary search tree.
169 |
170 | Further detail: BRRES dict entries are stored in a binary search tree, described by the id
171 | and left & right indices of each entry. This method exists for that tree's creation. To
172 | be honest, I don't entirely understand how it works, so a lot of the black magic here is
173 | based on the sources of BrawlBox and SZSTools. I need to revisit this in the future.
174 | """
175 | # list of entry names
176 | names = [""] + list(d.keys())
177 | # entry: entry being inserted into tree (ultimately returned)
178 | # cur: current entry in tree traversal, being compared to entry
179 | # prev: previous cur before this one
180 | entryIdx = len(entries)
181 | entry = cls(entryIdx)
182 | prevIdx = 0
183 | curIdx = entries[prevIdx].idxL
184 | entry.id = cls.calcID(names[entryIdx], names[prevIdx])
185 | # previous direction travelled in traversal
186 | isRight = False
187 | # go until entry is right of current or current is right of previous
188 | while entry.id <= entries[curIdx].id and entries[curIdx].id < entries[prevIdx].id:
189 | # if entry and current have the same id, calculate a new one for entry
190 | if entry.id == entries[curIdx].id:
191 | entry.id = cls.calcID(names[entryIdx], names[curIdx])
192 | if entry.getIDBit(names[curIdx]):
193 | entry.idxL = entryIdx
194 | entry.idxR = curIdx
195 | else:
196 | entry.idxL = curIdx
197 | entry.idxR = entryIdx
198 | # go to the next node
199 | prevIdx = curIdx
200 | isRight = entries[curIdx].getIDBit(names[entryIdx])
201 | if isRight: # is new cur node right of prev node?
202 | curIdx = entries[curIdx].idxR
203 | else:
204 | curIdx = entries[curIdx].idxL
205 | # done with tree traversal, now update indices in entry and its parent
206 | if len(names[curIdx]) == len(names[entryIdx]) and entry.getIDBit(names[curIdx]):
207 | entry.idxR = curIdx
208 | else:
209 | entry.idxL = curIdx
210 | if isRight:
211 | entries[prevIdx].idxR = entryIdx
212 | else:
213 | entries[prevIdx].idxL = entryIdx
214 | return entry
215 |
--------------------------------------------------------------------------------
/berrybush/wii/chr0.py:
--------------------------------------------------------------------------------
1 | # standard imports
2 | from typing import TypeVar
3 | from struct import Struct
4 | # 3rd party imports
5 | import numpy as np
6 | # internal imports
7 | from .animation import (
8 | Animation, AnimSerializer, AnimSubfile, I4, I6, I12, D1, D2, D4,
9 | readFrameRefs, packFrameRefs, groupAnimWriters, serializeAnims
10 | )
11 | from .alias import alias
12 | from .binaryutils import pad
13 | from .bitstruct import BitStruct, Bits
14 | from .brresdict import DictReader, DictWriter
15 | from .serialization import Serializer, Reader, Writer, StrPoolReadMixin, StrPoolWriteMixin
16 | from .subfile import BRRES_SER_T, SubfileSerializer, SubfileReader, SubfileWriter
17 | from . import transform as tf
18 |
19 |
20 | CHR0_SER_T = TypeVar("CHR0_SER_T", bound="CHR0Serializer")
21 |
22 |
23 | class AnimCode(BitStruct):
24 | _pad = Bits(1, bool, True)
25 | identitySRT = Bits(1, bool)
26 | identityRT = Bits(1, bool)
27 | identityS = Bits(1, bool)
28 | isoS = Bits(1, bool) # isometric: same value for each component
29 | isoR = Bits(1, bool)
30 | isoT = Bits(1, bool)
31 | mdlS = Bits(1, bool)
32 | mdlR = Bits(1, bool)
33 | mdlT = Bits(1, bool)
34 | segScaleComp = Bits(1, bool)
35 | segScaleCompParent = Bits(1, bool)
36 | hierarchicalScale = Bits(1, bool)
37 | _fixSX = Bits(1, bool)
38 | _fixSY = Bits(1, bool)
39 | _fixSZ = Bits(1, bool)
40 | _fixRX = Bits(1, bool)
41 | _fixRY = Bits(1, bool)
42 | _fixRZ = Bits(1, bool)
43 | _fixTX = Bits(1, bool)
44 | _fixTY = Bits(1, bool)
45 | _fixTZ = Bits(1, bool)
46 | hasS = Bits(1, bool)
47 | hasR = Bits(1, bool)
48 | hasT = Bits(1, bool)
49 | fmtS = Bits(2, int)
50 | fmtR = Bits(3, int)
51 | fmtT = Bits(2, int)
52 |
53 | fixS = alias("_fixSX", "_fixSY", "_fixSZ")
54 | fixR = alias("_fixRX", "_fixRY", "_fixRZ")
55 | fixT = alias("_fixTX", "_fixTY", "_fixTZ")
56 |
57 |
58 | class JointAnim():
59 | """Contains animation data for a joint.
60 |
61 | This data is separated into 3 lists: one for scale, one for rotation, and one for translation.
62 | If any of these lists are empty, the model's values are used for that transformation. Otherwise,
63 | the lists must be filled with lists of keyframes - one for each transformation component.
64 | """
65 |
66 | def __init__(self, jointName: str = None):
67 | self.jointName = jointName
68 | self.scale = [Animation(np.array(((0, tf.IDENTITY_S, 0), ))) for _ in range(3)]
69 | self.rot = [Animation(np.array(((0, tf.IDENTITY_R, 0), ))) for _ in range(3)]
70 | self.trans = [Animation(np.array(((0, tf.IDENTITY_T, 0), ))) for _ in range(3)]
71 | self.segScaleComp = False
72 | self.segScaleCompParent = False # at least one child of this joint has ssc enabled
73 | self.hierarchicalScale = False
74 | self.animFmts: list[type[AnimSerializer]] = [type(None)] * 3
75 | """Formats for scale, rotation, and translation respectively used for packing.
76 |
77 | Eventually, you'll be able to set these to NoneType for automatic detection, but for now
78 | they must be manually set.
79 | """
80 |
81 |
82 | class JointAnimSerializer(Serializer[CHR0_SER_T, JointAnim]):
83 |
84 | _HEAD_STRCT = Struct(">i 4s")
85 | _KEYFRAME_FMTS: tuple[type[AnimSerializer], ...] = (type(None), I4, I6, I12, D1, D2, D4)
86 |
87 |
88 | class JointAnimReader(JointAnimSerializer["CHR0Reader"], Reader, StrPoolReadMixin):
89 |
90 | def unpack(self, data: bytes):
91 | super().unpack(data)
92 | self._data = anim = JointAnim()
93 | unpackedHeader = self._HEAD_STRCT.unpack_from(data, self.offset)
94 | anim.jointName = self.readString(data, self.offset + unpackedHeader[0])
95 | c = AnimCode.unpack(unpackedHeader[1])
96 | anim.segScaleComp = c.segScaleComp
97 | anim.segScaleCompParent = c.segScaleCompParent
98 | anim.hierarchicalScale = c.hierarchicalScale
99 | o = self._HEAD_STRCT.size
100 | baseOffset = self.offset
101 | anim.animFmts = [self._KEYFRAME_FMTS[fmt] for fmt in (c.fmtS, c.fmtR, c.fmtT)]
102 | fmtS, fmtR, fmtT = anim.animFmts
103 | l = self.parentSer.length
104 | o += readFrameRefs(data, baseOffset, o, c.isoS, c.mdlS, c.fixS, c.hasS, fmtS, anim.scale, l)
105 | o += readFrameRefs(data, baseOffset, o, c.isoR, c.mdlR, c.fixR, c.hasR, fmtR, anim.rot, l)
106 | o += readFrameRefs(data, baseOffset, o, c.isoT, c.mdlT, c.fixT, c.hasT, fmtT, anim.trans, l)
107 | return self
108 |
109 |
110 | class JointAnimWriter(JointAnimSerializer["CHR0Writer"], Writer, StrPoolWriteMixin):
111 |
112 | def __init__(self, parent: "CHR0Writer", offset = 0):
113 | super().__init__(parent, offset)
114 | self._s: list[AnimSerializer | float] = []
115 | self._r: list[AnimSerializer | float] = []
116 | self._t: list[AnimSerializer | float] = []
117 | self._animCode = AnimCode()
118 |
119 | @property
120 | def _animData(self):
121 | return self._s + self._r + self._t
122 |
123 | @property
124 | def animData(self):
125 | return (d for d in self._animData if isinstance(d, AnimSerializer))
126 |
127 | def _packAnims(self, data: list[Animation], identityVal: int, fmt: type[AnimSerializer]):
128 | """Process animation data for one transformation.
129 |
130 | Return a tuple that contains a bunch of info about this data (e.g., whether it's isometric,
131 | whether it's fixed, etc).
132 | """
133 | if fmt is type(None):
134 | fmt = I12 # autodetection not implemented yet - just default to i12
135 | iso = useModel = False
136 | fixed = [False] * len(data)
137 | animData: list[AnimSerializer | float] = []
138 | nonFixed: list[Animation] = []
139 | nonFixedSers: list[AnimSerializer] = []
140 | if len(data) == 0:
141 | useModel = True
142 | else: # there is custom animation data, don't just use model
143 | iso = all(c == data[0] for c in data[1:]) # iso: all components have the same data
144 | if iso:
145 | data = data[:1]
146 | # get fixed data & prepare non-fixed data for format filtering
147 | for i, anim in enumerate(data):
148 | if len(anim.keyframes) == 1:
149 | fixed[i] = True
150 | animData.append(anim.keyframes[0, 1])
151 | else:
152 | animData.append(None)
153 | nonFixed.append(anim)
154 | # serializeAnims() needs to be rewritten for speed and stuff
155 | # (so no format autodetection for now - setting to nonetype just defaults to i12 above)
156 | # nonFixedSers = serializeAnims(nonFixed, list(self._KEYFRAME_FMTS[1:]))
157 | nonFixedSers = [fmt().fromInstance(anim) for anim in nonFixed]
158 | if iso:
159 | fixed[:] = fixed[:1] * len(fixed)
160 | identity = iso and fixed[0] and data[0].keyframes[0, 1] == identityVal
161 | if identity:
162 | animData[:] = []
163 | exists = not identity and not useModel
164 | fmtIdx = self._KEYFRAME_FMTS.index(type(nonFixedSers[0])) if nonFixed else 0
165 | # put non-fixed data back in the main list
166 | nonFixedIdx = 0
167 | for i, anim in enumerate(animData):
168 | if anim is None:
169 | animData[i] = nonFixedSers[nonFixedIdx]
170 | nonFixedIdx += 1
171 | return (identity, iso, useModel, fixed, exists, fmtIdx, animData)
172 |
173 | def fromInstance(self, data: JointAnim):
174 | super().fromInstance(data)
175 | c = self._animCode
176 | c.segScaleComp = self._data.segScaleComp
177 | c.segScaleCompParent = self._data.segScaleCompParent
178 | c.hierarchicalScale = self._data.hierarchicalScale
179 | fmtS, fmtR, fmtT = data.animFmts
180 | idS, c.isoS, c.mdlS, c.fixS, c.hasS, c.fmtS, self._s = self._packAnims(data.scale, 1, fmtS)
181 | idR, c.isoR, c.mdlR, c.fixR, c.hasR, c.fmtR, self._r = self._packAnims(data.rot, 0, fmtR)
182 | idT, c.isoT, c.mdlT, c.fixT, c.hasT, c.fmtT, self._t = self._packAnims(data.trans, 0, fmtT)
183 | c.identityS = idS
184 | c.identityRT = idR and idT
185 | c.identitySRT = idS and idR and idT
186 | self._size = self._HEAD_STRCT.size + 4 * len(self._animData)
187 | return self
188 |
189 | def _calcSize(self):
190 | return super()._calcSize()
191 |
192 | def pack(self):
193 | """Pack this writer's main data, describing its format w/ pointers to frame data."""
194 | nameOffset = self.stringOffset(self._data.jointName) - self.offset
195 | packedHeader = self._HEAD_STRCT.pack(nameOffset, self._animCode.pack())
196 | return packedHeader + packFrameRefs(self._animData, self.offset)
197 |
198 |
199 | class CHR0(AnimSubfile):
200 | """BRRES subfile for MDL0 joint movement animations."""
201 |
202 | _VALID_VERSIONS = (5, )
203 |
204 | def __init__(self, name: str = None, version = -1):
205 | super().__init__(name, version)
206 | self.jointAnims: list[JointAnim] = []
207 | self.mtxGen: type[tf.MtxGenerator] = tf.StdMtxGen3D
208 |
209 |
210 | class CHR0Serializer(SubfileSerializer[BRRES_SER_T, CHR0]):
211 |
212 | DATA_TYPE = CHR0
213 | FOLDER_NAME = "AnmChr(NW4R)"
214 | MAGIC = b"CHR0"
215 |
216 | _HEAD_STRCT = Struct(">iiiiHH 3x ? i")
217 | _MTX_GEN_TYPES = (tf.StdMtxGen3D, tf.XSIMtxGen3D, tf.MayaMtxGen3D)
218 |
219 |
220 | class CHR0Reader(CHR0Serializer, SubfileReader):
221 |
222 | def unpack(self, data: bytes):
223 | super().unpack(data)
224 | self._data = CHR0()
225 | unpackedHeader = self._HEAD_STRCT.unpack_from(data, self.offset + self._CMN_STRCT.size)
226 | dataOffset = unpackedHeader[0]
227 | self._data.length = unpackedHeader[4]
228 | self._data.enableLoop = unpackedHeader[6]
229 | self._data.mtxGen = self._MTX_GEN_TYPES[unpackedHeader[7]]
230 | if dataOffset > 0:
231 | d = DictReader(self, self.offset + dataOffset).unpack(data)
232 | animData = d.readEntries(data, JointAnimReader)
233 | self._data.jointAnims = [jointData.getInstance() for jointData in animData.values()]
234 | return self
235 |
236 | @property
237 | def length(self):
238 | return self._data.length
239 |
240 |
241 | class CHR0Writer(CHR0Serializer, SubfileWriter):
242 |
243 | def __init__(self, parent, offset = 0):
244 | super().__init__(parent, offset)
245 | dictOffset = offset + self._CMN_STRCT.size + self._HEAD_STRCT.size
246 | self._jointAnims: DictWriter[JointAnimWriter] = DictWriter(self, dictOffset)
247 | self._animData: list[list[AnimSerializer]] = []
248 |
249 | def getStrings(self):
250 | return self._jointAnims.getStrings()
251 |
252 | def fromInstance(self, data: CHR0):
253 | super().fromInstance(data)
254 | animWriters: dict[str, JointAnimWriter] = {}
255 | dataOffset = self._jointAnims.offset + DictWriter.sizeFromLen(len(data.jointAnims))
256 | for a in data.jointAnims:
257 | animWriters[a.jointName] = writer = JointAnimWriter(self, dataOffset).fromInstance(a)
258 | dataOffset += writer.size()
259 | self._animData = groupAnimWriters([list(a.animData) for a in animWriters.values()], False)
260 | padAmount = 0
261 | for anims in self._animData:
262 | for anim in anims:
263 | anim.offset = dataOffset
264 | animSize = anims[0].size()
265 | paddedSize = pad(animSize, 4)
266 | padAmount = paddedSize - animSize
267 | dataOffset += paddedSize
268 | self._jointAnims.fromInstance(animWriters)
269 | self._size = dataOffset - padAmount - self.offset # don't include pad from last anim entry
270 | return self
271 |
272 | def _calcSize(self):
273 | return super()._calcSize()
274 |
275 | def pack(self):
276 | packedHeader = self._HEAD_STRCT.pack(
277 | self._CMN_STRCT.size + self._HEAD_STRCT.size, 0,
278 | self.stringOffset(self._data.name) - self.offset, 0,
279 | self._data.length, len(self._data.jointAnims), self._data.enableLoop,
280 | self._MTX_GEN_TYPES.index(self._data.mtxGen)
281 | )
282 | jointAnimWriters: list[JointAnimWriter] = self._jointAnims.getInstance().values()
283 | packedData = b"".join(w.pack() for w in jointAnimWriters)
284 | packedData += b"".join(pad(w[0].pack(), 4) for w in self._animData)
285 | return super().pack() + packedHeader + self._jointAnims.pack() + packedData
286 |
--------------------------------------------------------------------------------
/berrybush/wii/clr0.py:
--------------------------------------------------------------------------------
1 | # standard imports
2 | from typing import TypeVar
3 | from struct import Struct
4 | # 3rd party imports
5 | import numpy as np
6 | # internal imports
7 | from .alias import alias
8 | from .animation import AnimSubfile, groupAnimWriters
9 | from .binaryutils import maxBitVal
10 | from .bitstruct import BitStruct, Bits
11 | from .brresdict import DictReader, DictWriter
12 | from .serialization import Serializer, Reader, Writer, StrPoolReadMixin, StrPoolWriteMixin
13 | from .subfile import BRRES_SER_T, SubfileSerializer, SubfileReader, SubfileWriter
14 | from . import gx
15 |
16 |
17 | CLR0_SER_T = TypeVar("CLR0_SER_T", bound="CLR0Serializer")
18 |
19 |
20 | class AnimCode(BitStruct):
21 | _hasDif0 = Bits(1, bool)
22 | _fixDif0 = Bits(1, bool)
23 | _hasDif1 = Bits(1, bool)
24 | _fixDif1 = Bits(1, bool)
25 | _hasAmb0 = Bits(1, bool)
26 | _fixAmb0 = Bits(1, bool)
27 | _hasAmb1 = Bits(1, bool)
28 | _fixAmb1 = Bits(1, bool)
29 | _hasStand0 = Bits(1, bool)
30 | _fixStand0 = Bits(1, bool)
31 | _hasStand1 = Bits(1, bool)
32 | _fixStand1 = Bits(1, bool)
33 | _hasStand2 = Bits(1, bool)
34 | _fixStand2 = Bits(1, bool)
35 | _hasConst0 = Bits(1, bool)
36 | _fixConst0 = Bits(1, bool)
37 | _hasConst1 = Bits(1, bool)
38 | _fixConst1 = Bits(1, bool)
39 | _hasConst2 = Bits(1, bool)
40 | _fixConst2 = Bits(1, bool)
41 | _hasConst3 = Bits(1, bool)
42 | _fixConst3 = Bits(1, bool)
43 | _pad = Bits(10, int)
44 |
45 | hasClrs = alias(
46 | "_hasDif0", "_hasDif1", "_hasAmb0", "_hasAmb1", "_hasStand0", "_hasStand1", "_hasStand2",
47 | "_hasConst0", "_hasConst1", "_hasConst2", "_hasConst3"
48 | )
49 | fixClrs = alias(
50 | "_fixDif0", "_fixDif1", "_fixAmb0", "_fixAmb1", "_fixStand0", "_fixStand1", "_fixStand2",
51 | "_fixConst0", "_fixConst1", "_fixConst2", "_fixConst3"
52 | )
53 |
54 |
55 | class RegAnim():
56 | """Contains animation data for a color register."""
57 |
58 | def __init__(self, colors: np.ndarray, mask: np.ndarray):
59 | self.colors = colors
60 | self.mask = mask
61 | """Mask that lets you ignore parts of the color animation data, which is stored as RGBA8."""
62 |
63 | def __len__(self):
64 | return len(self.colors)
65 |
66 | def __eq__(self, other):
67 | return (
68 | isinstance(other, RegAnim)
69 | and np.all(self.colors == other.colors)
70 | and np.all(self.mask == other.mask)
71 | )
72 |
73 | @property
74 | def normalized(self):
75 | """Colors for this animation, normalized from 0-1.
76 |
77 | Colors can be retrieved and set through this property, or directly through "colors".
78 | """
79 | return self.colors / maxBitVal(8)
80 |
81 | @normalized.setter
82 | def normalized(self, colors: np.ndarray):
83 | self.colors = (colors * maxBitVal(8) + .5).astype(np.uint8)
84 |
85 |
86 | class RegAnimWriter(Writer["MatAnimWriter", RegAnim]):
87 |
88 | def __init__(self, parent: "MatAnimWriter", offset = 0):
89 | super().__init__(parent, offset)
90 | self._colors: np.ndarray = None
91 |
92 | @property
93 | def offset(self):
94 | return self._offset
95 |
96 | @offset.setter
97 | def offset(self, o: int):
98 | self._offset = o
99 |
100 | def fromInstance(self, data: RegAnim):
101 | super().fromInstance(data)
102 | # packed frames: all frames for this animation, cropped or padded to parent length + 1
103 | # (there's always an extra frame for the endpoint, even though it's unused ingame as
104 | # only the half-open [start, end) frame interval matters)
105 | numPackedFrames = self.parentSer.parentSer.getInstance().length + 1
106 | colors = self._data.colors[:numPackedFrames] # crop
107 | self._colors = np.pad(colors, ((0, numPackedFrames - len(colors)), (0, 0)), "edge") # pad
108 | self._size = self._colors.size
109 | return self
110 |
111 | def _calcSize(self):
112 | return super()._calcSize()
113 |
114 | def pack(self):
115 | return self._colors.tobytes()
116 |
117 |
118 | class MatAnim():
119 | """Contains animation data for a material's color registers."""
120 |
121 | def __init__(self, matName: str = None):
122 | self.matName = matName
123 | self.difRegs: dict[int, RegAnim] = {}
124 | self.ambRegs: dict[int, RegAnim] = {}
125 | self.standRegs: dict[int, RegAnim] = {}
126 | self.constRegs: dict[int, RegAnim] = {}
127 |
128 | @property
129 | def allRegs(self):
130 | """Animations for all registers that can be animated by this animation.
131 |
132 | They appear in the order (difRegs, ambRegs, standRegs, constRegs), in one flattened
133 | tuple. Non-animated registers are represented with None.
134 | """
135 | difRegs = tuple(self.difRegs.get(i) for i in range(gx.MAX_CLR_ATTRS))
136 | ambRegs = tuple(self.ambRegs.get(i) for i in range(gx.MAX_CLR_ATTRS))
137 | standRegs = tuple(self.standRegs.get(i) for i in range(gx.MAX_TEV_STAND_COLORS))
138 | constRegs = tuple(self.constRegs.get(i) for i in range(gx.MAX_TEV_CONST_COLORS))
139 | return difRegs + ambRegs + standRegs + constRegs
140 |
141 | def setRegAnim(self, i: int, anim: RegAnim | None):
142 | """Set a register animation via an index into allRegs."""
143 | regDicts = (self.difRegs, self.ambRegs, self.standRegs, self.constRegs)
144 | mxs = (gx.MAX_CLR_ATTRS, gx.MAX_CLR_ATTRS, gx.MAX_TEV_STAND_COLORS, gx.MAX_TEV_CONST_COLORS)
145 | for regDict, maxSlots in zip(regDicts, mxs):
146 | if i < maxSlots:
147 | regDict[i] = anim
148 | return
149 | else:
150 | i -= maxSlots
151 | raise IndexError("Register index out of range")
152 |
153 |
154 | class MatAnimSerializer(Serializer[CLR0_SER_T, MatAnim]):
155 |
156 | _HEAD_STRCT = Struct(">i 4s")
157 | _REG_ENTRY_STRCT = Struct(">4s 4s")
158 |
159 |
160 | class MatAnimReader(MatAnimSerializer["CLR0Reader"], Reader, StrPoolReadMixin):
161 |
162 | def unpack(self, data: bytes):
163 | super().unpack(data)
164 | self._data = anim = MatAnim()
165 | unpackedHeader = self._HEAD_STRCT.unpack_from(data, self.offset)
166 | anim.matName = self.readString(data, self.offset + unpackedHeader[0])
167 | c = AnimCode.unpack(unpackedHeader[1])
168 | dataOffset = self.offset + self._HEAD_STRCT.size
169 | for i, (has, fixed) in enumerate(zip(c.hasClrs, c.fixClrs)):
170 | if has:
171 | entryData = self._REG_ENTRY_STRCT.unpack_from(data, dataOffset)
172 | mask = np.frombuffer(entryData[0], dtype=np.uint8)
173 | colorData = entryData[1] # either fixed value or anim offset
174 | if not fixed:
175 | # note: offset is relative to exactly where the offset is stored
176 | # (4 bytes after data offset)
177 | animOffset = dataOffset + 4 + int.from_bytes(colorData, "big")
178 | colorData = data[animOffset : animOffset + (self.parentSer.length) * 4]
179 | colors = np.frombuffer(colorData, dtype=np.uint8).reshape(-1, 4)
180 | anim.setRegAnim(i, RegAnim(colors, mask))
181 | dataOffset += self._REG_ENTRY_STRCT.size
182 | return self
183 |
184 |
185 | class MatAnimWriter(MatAnimSerializer["CLR0Writer"], Writer, StrPoolWriteMixin):
186 |
187 | def __init__(self, parent: "CLR0Writer", offset = 0):
188 | super().__init__(parent, offset)
189 | self._regs: list[RegAnimWriter] = []
190 | self._animCode = AnimCode()
191 |
192 | @property
193 | def regs(self):
194 | """Color data for this animation's non-fixed registers."""
195 | return [r for r in self._regs if len(r.getInstance().colors) > 1]
196 |
197 | def fromInstance(self, data: MatAnim):
198 | super().fromInstance(data)
199 | c = self._animCode
200 | c.hasClrs = (a is not None for a in data.allRegs)
201 | c.fixClrs = (a is not None and len(a.colors) == 1 for a in data.allRegs)
202 | self._regs = [RegAnimWriter(self).fromInstance(a) for a in data.allRegs if a is not None]
203 | self._size = self._HEAD_STRCT.size + self._REG_ENTRY_STRCT.size * len(self._regs)
204 | return self
205 |
206 | def _calcSize(self):
207 | return super()._calcSize()
208 |
209 | def pack(self):
210 | """Pack this writer's main data, describing its format w/ pointers to frame data."""
211 | nameOffset = self.stringOffset(self._data.matName) - self.offset
212 | packedHeader = self._HEAD_STRCT.pack(nameOffset, self._animCode.pack())
213 | packedData = b""
214 | offset = self.offset + self._HEAD_STRCT.size + 4
215 | for w in self._regs:
216 | anim = w.getInstance()
217 | data = int(w.offset - offset).to_bytes(4, "big") if len(anim) > 1 else w.pack()
218 | packedData += self._REG_ENTRY_STRCT.pack(anim.mask.tobytes(), data)
219 | offset += self._REG_ENTRY_STRCT.size
220 | return packedHeader + packedData
221 |
222 |
223 | class CLR0(AnimSubfile):
224 | """BRRES subfile for MDL0 material color register animations."""
225 |
226 | _VALID_VERSIONS = (4, )
227 |
228 | def __init__(self, name: str = None, version = -1):
229 | super().__init__(name, version)
230 | self.matAnims: list[MatAnim] = []
231 |
232 |
233 | class CLR0Serializer(SubfileSerializer[BRRES_SER_T, CLR0]):
234 |
235 | DATA_TYPE = CLR0
236 | FOLDER_NAME = "AnmClr(NW4R)"
237 | MAGIC = b"CLR0"
238 |
239 | _HEAD_STRCT = Struct(">iiiiHH 3x ?")
240 |
241 |
242 | class CLR0Reader(CLR0Serializer, SubfileReader):
243 |
244 | def __init__(self, parent, offset = 0):
245 | super().__init__(parent, offset)
246 | self.length: int = 0
247 |
248 | def unpack(self, data: bytes):
249 | super().unpack(data)
250 | self._data = CLR0()
251 | unpackedHeader = self._HEAD_STRCT.unpack_from(data, self.offset + self._CMN_STRCT.size)
252 | dataOffset = unpackedHeader[0]
253 | self._data.length = self.length = unpackedHeader[4]
254 | self._data.enableLoop = unpackedHeader[6]
255 | if dataOffset > 0:
256 | d = DictReader(self, self.offset + dataOffset).unpack(data)
257 | animData = d.readEntries(data, MatAnimReader)
258 | self._data.matAnims = [matData.getInstance() for matData in animData.values()]
259 | return self
260 |
261 |
262 | class CLR0Writer(CLR0Serializer, SubfileWriter):
263 |
264 | def __init__(self, parent, offset = 0):
265 | super().__init__(parent, offset)
266 | dictOffset = offset + self._CMN_STRCT.size + self._HEAD_STRCT.size
267 | self._matAnims: DictWriter[MatAnimWriter] = DictWriter(self, dictOffset)
268 | self._clrAnims: list[list[RegAnimWriter]] = []
269 |
270 | def getStrings(self):
271 | return self._matAnims.getStrings()
272 |
273 | def fromInstance(self, data: CLR0):
274 | super().fromInstance(data)
275 | animWriters: dict[str, MatAnimWriter] = {}
276 | dataOffset = self._matAnims.offset + DictWriter.sizeFromLen(len(data.matAnims))
277 | for a in data.matAnims:
278 | animWriters[a.matName] = writer = MatAnimWriter(self, dataOffset).fromInstance(a)
279 | dataOffset += writer.size()
280 | self._clrAnims = groupAnimWriters([a.regs for a in animWriters.values()])
281 | for anims in self._clrAnims:
282 | for anim in anims:
283 | anim.offset = dataOffset
284 | dataOffset += anims[0].size()
285 | self._matAnims.fromInstance(animWriters)
286 | self._size = dataOffset - self.offset
287 | return self
288 |
289 | def _calcSize(self):
290 | return super()._calcSize()
291 |
292 | def pack(self):
293 | packedHeader = self._HEAD_STRCT.pack(
294 | self._CMN_STRCT.size + self._HEAD_STRCT.size, 0,
295 | self.stringOffset(self._data.name) - self.offset, 0,
296 | self._data.length, len(self._data.matAnims), self._data.enableLoop,
297 | )
298 | matAnimWriters: list[MatAnimWriter] = self._matAnims.getInstance().values()
299 | packedData = b"".join(w.pack() for w in matAnimWriters)
300 | packedData += b"".join(w[0].pack() for w in self._clrAnims)
301 | return super().pack() + packedHeader + self._matAnims.pack() + packedData
302 |
--------------------------------------------------------------------------------
/berrybush/wii/common.py:
--------------------------------------------------------------------------------
1 | # standard imports
2 | from enum import Enum
3 | from itertools import chain
4 | from typing import Iterable, TypeVar, TYPE_CHECKING
5 | # special typing imports
6 | if TYPE_CHECKING:
7 | from typing_extensions import Self
8 | else:
9 | Self = object
10 |
11 |
12 | T = TypeVar("T")
13 | U = TypeVar("U")
14 |
15 |
16 | class Tree():
17 | """Simple base class for data stored in a tree structure."""
18 |
19 | def __init__(self, parent: Self = None):
20 | self._parent: Self = None
21 | self._children: list[Self] = []
22 | self.parent = parent
23 |
24 | def addChild(self, child: Self):
25 | """Add a child to this tree item. If the child is already present, do nothing."""
26 | if child not in self._children:
27 | child.parent = None # remove child from current parent
28 | child._parent = self
29 | self._children.append(child) # set parent and add here
30 |
31 | def removeChild(self, child: Self):
32 | """Remove a child from this tree item."""
33 | if child._parent is self:
34 | child._parent = None
35 | self._children.remove(child)
36 | else:
37 | raise ValueError("Child does not belong to this parent")
38 |
39 | @property
40 | def parent(self):
41 | """Parent of this tree item."""
42 | return self._parent
43 |
44 | @parent.setter
45 | def parent(self, parent: Self):
46 | if parent is not self._parent:
47 | if self._parent is not None:
48 | self._parent.removeChild(self) # remove from current parent
49 | if parent is not None:
50 | parent.addChild(self) # add to new parent
51 |
52 | @property
53 | def children(self):
54 | """Children of this tree item."""
55 | return tuple(self._children)
56 |
57 | def deepChildren(self, includeSelf: bool = True) -> chain[Self]:
58 | """Generator for children, grandchildren, etc of this tree item retrieved recursively."""
59 | childrenGen = chain.from_iterable(c.deepChildren() for c in self._children)
60 | if not includeSelf:
61 | return childrenGen
62 | return chain((self, ), childrenGen)
63 |
64 | def ancestors(self, includeSelf: bool = True) -> tuple[Self, ...]:
65 | """Tuple containing the ancestors of this tree item retrieved recursively."""
66 | parent = self.parent
67 | return () + (parent.ancestors() if parent else ()) + ((self, ) if includeSelf else ())
68 |
69 |
70 | class EnumWithAttr(Enum):
71 | """Use this for an enum with a custom attribute ("_attr_") in addition to its value.
72 |
73 | Based on stuff from https://stackoverflow.com/questions/12680080/python-enums-with-attributes
74 | """
75 | def __new__(cls, val, attr):
76 | entry = object.__new__(cls)
77 | entry._value_ = val
78 | entry._attr_ = attr
79 | return entry
80 |
81 |
82 | def getKey(d: dict, val):
83 | """Get first key for some value in a dict.
84 |
85 | Raises ValueError if the value is not present.
86 | """
87 | try:
88 | return next(k for k, v in d.items() if v == val)
89 | except StopIteration as e:
90 | raise ValueError(f"'{val}' is not in dict") from e
91 |
92 |
93 | def keyVals(d: dict[T, U], vals: tuple) -> list[U]:
94 | """Get a list containing the values for a sequence of keys in a dict.
95 |
96 | Keys not in the dict are ignored.
97 | """
98 | return [d[v] for v in vals if v in d]
99 |
100 |
101 | def keyValsDef(d: dict[T, U], vals: tuple, default) -> list[U]:
102 | """Get a list containing the values for a sequence of keys in a dict.
103 |
104 | If any keys aren't in the dict, a default value is used.
105 | """
106 | return [d[v] if v in d else default for v in vals]
107 |
108 |
109 | def unique(l: Iterable[T]) -> list[T]:
110 | """Return a list containing the unique values from an iterable with order preserved."""
111 | try: # make a dict from it, fast and clean
112 | return list(dict.fromkeys(l))
113 | except TypeError: # type's unhashable so we've gotta do some more work
114 | used = []
115 | return [v for v in l if v not in used and (used.append(v) or True)]
116 |
117 |
118 | def fillList(l: list[T], n: int, v) -> list[T]:
119 | """Return a copy of a list filled until with some value until its length's a multiple of n.
120 |
121 | If the list is empty, fill it to the value. (0 doesn't count as a multiple)
122 | """
123 | length = len(l)
124 | padAmount = (n - (length % n)) % n if length > 0 else n
125 | return list(l) + [v] * padAmount
126 |
--------------------------------------------------------------------------------
/berrybush/wii/hermite.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 |
4 | def h0eval(t: np.ndarray):
5 | """Evaluate the first Hermite basis function at time(s) t."""
6 | return 2 * (t ** 3) - 3 * (t ** 2) + 1
7 |
8 |
9 | def h1eval(t: np.ndarray):
10 | """Evaluate the second Hermite basis function at time(s) t."""
11 | return -2 * (t ** 3) + 3 * (t ** 2)
12 |
13 |
14 | def h2eval(t: np.ndarray):
15 | """Evaluate the third Hermite basis function at time(s) t."""
16 | return (t ** 3) - 2 * (t ** 2) + t
17 |
18 |
19 | def h3eval(t: np.ndarray):
20 | """Evaluate the fourth Hermite basis function at time(s) t."""
21 | return (t ** 3) - (t ** 2)
22 |
23 |
24 | def hp1eval(t: np.ndarray):
25 | """Evaluate the derivative of the first Hermite basis function at time(s) t."""
26 | return 6 * (t ** 2) - 6 * t
27 |
28 |
29 | def hp2eval(t: np.ndarray):
30 | """Evaluate the derivative of the second Hermite basis function at time(s) t."""
31 | return -6 * (t ** 2) + 6 * t
32 |
33 |
34 | def hp3eval(t: np.ndarray):
35 | """Evaluate the derivative of the third Hermite basis function at time(s) t."""
36 | return 3 * (t ** 2) - 4 * t + 1
37 |
38 |
39 | def hp4eval(t: np.ndarray):
40 | """Evaluate the derivative of the fourth Hermite basis function at time(s) t."""
41 | return 3 * (t ** 2) - 2 * t
42 |
43 |
44 | def generateBasisLookup(n: int):
45 | """Generate a lookup table of length n for the Hermite basis functions & their derivatives."""
46 | t = np.linspace(0, 1, n, dtype=np.float64)
47 | return np.array((
48 | (h0eval(t), h1eval(t), h2eval(t), h3eval(t)),
49 | (hp1eval(t), hp2eval(t), hp3eval(t), hp4eval(t))
50 | ))
51 |
52 |
53 | # note: 64 bytes per entry, so 2^16 entries -> 4 MB
54 | # plus one extra entry, since it has to include both endpoints
55 | # (dividing based on a power of 2 is 100% arbitrary,
56 | # but probably better than a power of 2 minus one)
57 | BASIS_LOOKUP = generateBasisLookup(2 ** 16 + 1)
58 |
59 |
60 | def interpolateCurve(curve: np.ndarray, x: np.ndarray) -> np.ndarray:
61 | """Hermite curve interpolation. Return the X/Y/tangent values at some X on the curve.
62 |
63 | Multiple positions, and optionally multiple curves (one per position), may be provided
64 | for batch calculations.
65 |
66 | A single curve is made up of two points, each with an X, Y, and tangent value.
67 | (For instance, `[[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]]` defines 2 curves)
68 | """
69 | x1, y1, t1 = curve[..., 0, 0], curve[..., 0, 1], curve[..., 0, 2]
70 | x2, y2, t2 = curve[..., 1, 0], curve[..., 1, 1], curve[..., 1, 2]
71 | span = x2 - x1
72 | # handle span of 0 to prevent division by 0 problems
73 | # whether it's a scalar or array,
74 | # do stuff that will arbitrarily result in taking the left control point
75 | if np.isscalar(span):
76 | if span == 0:
77 | return np.repeat(curve[0, np.newaxis], len(x), axis=0)
78 | else:
79 | span[span == 0] = 1
80 | # https://www.cubic.org/docs/hermite.htm
81 | t = (x - x1) / span
82 | i = ((BASIS_LOOKUP.shape[-1] - 1) * t + .5).astype(np.int64)
83 | h, hp = BASIS_LOOKUP[:, :, i]
84 | # note: this hermite implementation is non-parameterized, so tangents are scalars,
85 | # but they're usually vectors (that's the case in the link above)
86 | # the parameterized version would have tangents [span, span * t1] and [span, span * t2]
87 | # since the formula we're using (linked) is for the parameterized verson,
88 | # we have to multiply t1 & t2 by span
89 | # this multiplication is also referenced here:
90 | # https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Interpolation_on_an_arbitrary_interval
91 | result = np.empty((*x.shape, 3))
92 | result[..., 0] = x
93 | result[..., 1] = y1 * h[0] + y2 * h[1] + (t1 * h[2] + t2 * h[3]) * span
94 | result[..., 2] = (y1 * hp[0] + y2 * hp[1]) / span + t1 * hp[2] + t2 * hp[3]
95 | return result
96 |
97 |
98 | def interpolateSpline(spline: np.ndarray, positions: np.ndarray) -> np.ndarray:
99 | """Get the X, Y, and tangent values for a Hermite spline at the given X values.
100 |
101 | (A spline is an array of control points, each with an X, Y, and tangent value)
102 | """
103 | interpolated = np.empty((*positions.shape, 3))
104 | interpolated[:, 0] = positions
105 | # first, determine which positions are in bounds
106 | outOfBoundsL = positions < spline[0, 0]
107 | outOfBoundsR = positions > spline[-1, 0]
108 | outOfBounds = np.logical_or(outOfBoundsL, outOfBoundsR)
109 | inBounds = np.logical_not(outOfBounds)
110 | # handle out-of-bounds positions
111 | interpolated[outOfBoundsL, 1] = spline[0, 1]
112 | interpolated[outOfBoundsR, 1] = spline[-1, 1]
113 | interpolated[outOfBounds, 2] = 0
114 | # handle in-bounds positions
115 | i = np.searchsorted(spline[:, 0], positions[inBounds]) # each position's next ctrl point index
116 | curves = np.empty((len(i), 2, 3)) # each position's corresponding curve
117 | curves[:, 0] = spline[i - 1]
118 | curves[:, 1] = spline[i]
119 | interpolated[inBounds] = interpolateCurve(curves, positions[inBounds])
120 | return interpolated
121 |
122 |
123 | def simplifySpline(spline: np.ndarray, maxError: float, precision: float = 1) -> np.ndarray:
124 | """Simplify a Hermite spline, enforcing a maximum error tested throughout the curve's domain.
125 |
126 | The error is tested at a set of X values no more than `precision` units apart from one another
127 | that includes both endpoints.
128 |
129 | (e.g., if the spline starts & ends on integer X values, and `precision = 1`, the error will be
130 | tested at every integer X value in the domain)
131 | """
132 | numPositions = int(np.ceil((spline[-1, 0] - spline[0, 0]) / precision)) * precision + 1
133 | positions = np.linspace(spline[0, 0], spline[-1, 0], numPositions)
134 | # maxError *= np.ptp(spline[..., 1]) # uncomment for a "relative" error rather than absolute
135 | simplified = simplifySplineRough(interpolateSpline(spline, positions), maxError)
136 | # since we do simplification based on the interpolated, it's possible (though rare) that
137 | # the "simplified" version will actually be larger than the original
138 | # in that case, just return the original
139 | return min(spline, simplified, key=len)
140 |
141 |
142 | def simplifySplineRough(points: np.ndarray, maxError: float) -> np.ndarray:
143 | """Simplify a Hermite spline, enforcing a maximum error at the original control points."""
144 | # define the new curve using the first & last points of the current spline, then test errors
145 | newCurve = np.empty((2, 3)) # this is faster than np.stack((points[0], points[-1]))
146 | newCurve[0] = points[0]
147 | newCurve[1] = points[-1]
148 | newPoints = interpolateCurve(newCurve, points[:, 0])
149 | errors = np.abs(newPoints[:, 1] - points[:, 1])
150 | maxErrorIndex = errors.argmax()
151 | # at the point of maximum error, if max error is exceeded, use that point as a pivot and
152 | # repeat the process on both sides recursively - eventually you get a simplified spline
153 | if errors[maxErrorIndex] > maxError:
154 | left = simplifySplineRough(points[:maxErrorIndex + 1], maxError)
155 | right = simplifySplineRough(points[maxErrorIndex:], maxError)
156 | return np.concatenate((left, right[1:]))
157 | else:
158 | return newCurve
159 | # note that this is loosely based on philip j schneider's bezier fitting algorithm,
160 | # but it's dramatically simpler because we use scalar tangents rather than vectors
161 | # (which has consequences such as making t and y both functions of x and makes everything nice)
162 | # (this is also mentioned in hermite() above)
163 |
164 |
165 | # print(simplifySpline(np.array([[0, 0, 1], [2, 2, 1], [4, 4, 1]]), 0.01)) # demo
166 |
--------------------------------------------------------------------------------
/berrybush/wii/plt0.py:
--------------------------------------------------------------------------------
1 | # standard imports
2 | from struct import Struct
3 | # 3rd party imports
4 | import numpy as np
5 | # internal imports
6 | from .subfile import BRRES_SER_T, Subfile, SubfileSerializer, SubfileReader, SubfileWriter
7 | from .tex0 import ListableImageFormat, IA8, RGB565, RGB5A3, TEX0
8 |
9 |
10 | class PLT0(Subfile):
11 | """BRRES subfile for texture palettes."""
12 |
13 | _VALID_VERSIONS = (1, 3)
14 |
15 | def __init__(self, name: str = None, version = -1):
16 | super().__init__(name, version)
17 | self.fmt: ListableImageFormat = None
18 | self.colors = np.ndarray((0, 0, 4))
19 |
20 | def isCompatible(self, tex: TEX0):
21 | """Whether this palette can be used with a palette image."""
22 | return tex.isPaletteIndices and np.max(tex.images) < len(self.colors)
23 |
24 | def __len__(self):
25 | return len(self.colors)
26 |
27 |
28 | class PLT0Serializer(SubfileSerializer[BRRES_SER_T, PLT0]):
29 |
30 | DATA_TYPE = PLT0
31 | FOLDER_NAME = "Palettes(NW4R)"
32 | MAGIC = b"PLT0"
33 |
34 | _IMG_FORMATS: tuple[type[ListableImageFormat]] = (IA8, RGB565, RGB5A3)
35 | _HEAD_STRCT = Struct(">iiIHxxii 24x")
36 |
37 |
38 | class PLT0Reader(PLT0Serializer, SubfileReader):
39 |
40 | def unpack(self, data: bytes):
41 | super().unpack(data)
42 | unpackedHeader = self._HEAD_STRCT.unpack_from(data, self.offset + self._CMN_STRCT.size)
43 | plt = self._data
44 | plt.fmt: type[ListableImageFormat] = self._IMG_FORMATS[unpackedHeader[2]]
45 | if plt.fmt is None:
46 | raise NotImplementedError("Unsupported image format detected")
47 | numEntries = unpackedHeader[3]
48 | dataOffset = unpackedHeader[0]
49 | if dataOffset > 0:
50 | plt.colors = plt.fmt.importList(data[self.offset + dataOffset:], numEntries)
51 | return self
52 |
53 |
54 | class PLT0Writer(PLT0Serializer, SubfileWriter):
55 |
56 | def _calcSize(self):
57 | listSize = self._data.fmt.listSize(len(self._data.colors))
58 | return self._CMN_STRCT.size + self._HEAD_STRCT.size + listSize
59 |
60 | def pack(self):
61 | plt = self._data
62 | packedHeader = self._HEAD_STRCT.pack(self._CMN_STRCT.size + self._HEAD_STRCT.size,
63 | self.stringOffset(self._data.name) - self.offset,
64 | self._IMG_FORMATS.index(plt.fmt), len(plt.colors),
65 | 0, 0)
66 | return super().pack() + packedHeader + plt.fmt.exportList(plt.colors)
67 |
--------------------------------------------------------------------------------
/berrybush/wii/serialization.py:
--------------------------------------------------------------------------------
1 | # standard imports
2 | from abc import abstractmethod, ABC
3 | from typing import Generic, TypeVar
4 |
5 |
6 | S_PARENT_T = TypeVar("S_PARENT_T", bound="Serializable")
7 | S_PARENT_T = TypeVar("S_PARENT_T", bound="Readable")
8 | S_PARENT_T = TypeVar("S_PARENT_T", bound="Writable")
9 | S_DATA_T = TypeVar("S_DATA_T")
10 |
11 |
12 | class Serializable(ABC, Generic[S_PARENT_T]):
13 | """Object that supports some degree of conversion to/from bytes.
14 |
15 | Has a serializable parent that makes it possible to deal with references pointing beyond this
16 | object's scope.
17 | """
18 |
19 | def __init__(self, parent: S_PARENT_T = None):
20 | self.parentSer = parent
21 |
22 |
23 | class Readable(Serializable[S_PARENT_T]):
24 | """Object that can be unpacked from bytes."""
25 |
26 | @abstractmethod
27 | def unpack(self, data: bytes):
28 | """Unpack data from bytes and store it in this object, which is returned."""
29 | return self
30 |
31 |
32 | class Writable(Serializable[S_PARENT_T]):
33 | """Object that can be packed to bytes."""
34 |
35 | @abstractmethod
36 | def size(self) -> int:
37 | """Size of this serializable object, in bytes."""
38 |
39 | @abstractmethod
40 | def pack(self) -> bytes:
41 | """Pack the data stored in this object to bytes."""
42 |
43 |
44 | class AddressedSerializable(Serializable[S_PARENT_T]):
45 | """Serializable object with an offset indicating its location."""
46 |
47 | def __init__(self, parent: S_PARENT_T = None, offset = 0):
48 | super().__init__(parent)
49 | self._offset = offset
50 |
51 | @property
52 | def offset(self):
53 | """Absolute address of this serializer."""
54 | return self._offset
55 |
56 | class Serializer(AddressedSerializable[S_PARENT_T], Generic[S_PARENT_T, S_DATA_T]):
57 | """Serializable helper to convert some BRRES type to/from bytes."""
58 |
59 | DATA_TYPE: type[S_DATA_T]
60 |
61 | def __init__(self, parent: S_PARENT_T = None, offset = 0):
62 | super().__init__(parent, offset)
63 | self._data: S_DATA_T = None
64 |
65 | def getInstance(self) -> S_DATA_T:
66 | """Get this serializer's instance of the data type it manages."""
67 | return self._data
68 |
69 |
70 | # general guidelines for implementing the serializer methods in subclasses:
71 | # (note that these are general, and exceptions can & should be made for middlemen like dicts)
72 | # - unpack: keep absolute offsets except strings; avoid parent access & never call getInstance
73 | # - _updateInstance: update instance based on stored offsets & parent data (not parent getInstance)
74 | # - fromInstance: create info from instance w/ direct data references; don't access parent
75 | # - pack: get abs offsets & pack based on parent (data or getInstance, but use data when possible)
76 |
77 |
78 | class Reader(Readable[S_PARENT_T], Serializer[S_PARENT_T, S_DATA_T]):
79 | """Helper to read a BRRES type from bytes."""
80 |
81 | def __init__(self, parent: S_PARENT_T = None, offset = 0):
82 | super().__init__(parent, offset)
83 | self._dataCached = False
84 |
85 | @abstractmethod
86 | def unpack(self, data: bytes):
87 | """Unpack data from bytes and store it in this reader, which is returned."""
88 | super().unpack(data)
89 | self._data = None
90 | self._dataCached = False
91 | return self
92 |
93 | def _updateInstance(self):
94 | """Update this reader's data instance based on its current state."""
95 |
96 | def getInstance(self):
97 | """Get this reader's instance of the data type it manages.
98 |
99 | This instance is created on unpack, and gets updated based on the reader's state the first
100 | time getInstance() is called. All following getInstance() calls return the same object
101 | without any modifications, unless unpack() is called again (which creates a new instance
102 | and restarts the cycle).
103 | """
104 | if not self._dataCached:
105 | self._dataCached = True
106 | self._updateInstance()
107 | return super().getInstance()
108 |
109 |
110 | class Writer(Writable[S_PARENT_T], Serializer[S_PARENT_T, S_DATA_T]):
111 | """Helper to write a BRRES type to bytes."""
112 |
113 | def __init__(self, parent: S_PARENT_T = None, offset = 0):
114 | super().__init__(parent, offset)
115 | self._size = 0
116 |
117 | def fromInstance(self, data: S_DATA_T):
118 | """Update this writer based on an instance of the data it serializes & return the writer.
119 |
120 | Note that after calling this, the provided instance is guaranteed to be the one returned by
121 | getInstance().
122 | """
123 | self._data = data
124 | self._size = self._calcSize()
125 | return self
126 |
127 | @abstractmethod
128 | def _calcSize(self) -> int:
129 | """Calculate the size, in bytes, of the data stored in this writer.
130 |
131 | This size is ultimately stored in self._size during fromInstance(), and the size() method
132 | simply returns self._size. You shouldn't ever have to call this method yourself.
133 |
134 | If you override fromInstance() and add your own size calculation there, you can just call
135 | super()._calcSize() here.
136 | """
137 | return 0
138 |
139 | def size(self):
140 | return self._size
141 |
142 | @abstractmethod
143 | def pack(self) -> bytes:
144 | if self._size <= 0:
145 | raise RuntimeError("Cannot pack writable object with size <= 0")
146 | return b""
147 |
148 |
149 | class StrPoolReadMixin():
150 | """Mixin for a readable object that uses a BRRES string pool."""
151 |
152 | def readString(self, data: bytes, offset: int) -> str:
153 | """Read & return a string from the master BRRES string pool."""
154 | return self.parentSer.readString(data, offset)
155 |
156 |
157 | class StrPoolWriteMixin():
158 | """Mixin for a writable object that uses a BRRES string pool."""
159 |
160 | def getStrings(self) -> set[str]:
161 | """Return a set of strings this data uses for the master BRRES string pool."""
162 | return set()
163 |
164 | def stringOffset(self, string: str):
165 | """Absolute offset of a string in the master BRRES string pool. 0 if not found."""
166 | return self.parentSer.stringOffset(string)
167 |
--------------------------------------------------------------------------------
/berrybush/wii/srt0.py:
--------------------------------------------------------------------------------
1 | # standard imports
2 | from typing import TypeVar
3 | from struct import Struct
4 | # 3rd party imports
5 | import numpy as np
6 | # internal imports
7 | from .animation import (
8 | Animation, AnimSubfile, I12, readFrameRefs, packFrameRefs, groupAnimWriters
9 | )
10 | from .alias import alias
11 | from .bitstruct import BitStruct, Bits
12 | from .brresdict import DictReader, DictWriter
13 | from .serialization import Serializer, Reader, Writer, StrPoolReadMixin, StrPoolWriteMixin
14 | from .subfile import BRRES_SER_T, SubfileSerializer, SubfileReader, SubfileWriter
15 | from . import gx, transform as tf
16 |
17 |
18 | SRT0_SER_T = TypeVar("SRT0_SER_T", bound="SRT0Serializer")
19 | MAT_ANIM_SER_T = TypeVar("MAT_ANIM_SER_T", bound="MatAnimSerializer")
20 |
21 |
22 | class AnimCode(BitStruct):
23 | _pad0 = Bits(1, bool, True)
24 | identityS = Bits(1, bool)
25 | identityR = Bits(1, bool)
26 | identityT = Bits(1, bool)
27 | isoS = Bits(1, bool)
28 | _fixSX = Bits(1, bool)
29 | _fixSY = Bits(1, bool)
30 | _fixR = Bits(1, bool)
31 | _fixTX = Bits(1, bool)
32 | _fixTY = Bits(1, bool)
33 | _pad1 = Bits(22, int)
34 |
35 | fixS = alias("_fixSX", "_fixSY")
36 | fixR = alias("_fixR", forceList=True)
37 | fixT = alias("_fixTX", "_fixTY")
38 |
39 |
40 | class TexAnim():
41 | """Contains animation data for a texture.
42 |
43 | This data is separated into 3 lists: one for scale, one for rotation, and one for translation.
44 | If any of these lists are empty, the model's values are used for that transformation.
45 | """
46 |
47 | def __init__(self):
48 | self.scale = [Animation(np.array(((0, tf.IDENTITY_S, 0), ))) for _ in range(2)]
49 | self.rot = [Animation(np.array(((0, tf.IDENTITY_R, 0), )))]
50 | self.trans = [Animation(np.array(((0, tf.IDENTITY_T, 0), ))) for _ in range(2)]
51 |
52 |
53 | class TexAnimSerializer(Serializer[MAT_ANIM_SER_T, TexAnim]):
54 |
55 | _HEAD_STRCT = Struct(">4s")
56 |
57 |
58 | class TexAnimReader(TexAnimSerializer["MatAnimReader"], Reader, StrPoolReadMixin):
59 |
60 | def unpack(self, data: bytes):
61 | super().unpack(data)
62 | self._data = anim = TexAnim()
63 | unpackedHeader = self._HEAD_STRCT.unpack_from(data, self.offset)
64 | c = AnimCode.unpack(unpackedHeader[0])
65 | o = self.offset + self._HEAD_STRCT.size
66 | hasS, hasR, hasT = not c.identityS, not c.identityR, not c.identityT
67 | o += readFrameRefs(data, None, o, c.isoS, False, c.fixS, hasS, I12, anim.scale)
68 | o += readFrameRefs(data, None, o, False, False, c.fixR, hasR, I12, anim.rot)
69 | o += readFrameRefs(data, None, o, False, False, c.fixT, hasT, I12, anim.trans)
70 | return self
71 |
72 |
73 | class TexAnimWriter(TexAnimSerializer["MatAnimWriter"], Writer, StrPoolWriteMixin):
74 |
75 | def __init__(self, parent: "MatAnimWriter", offset = 0):
76 | super().__init__(parent, offset)
77 | self._s: list[I12 | float] = []
78 | self._r: list[I12 | float] = []
79 | self._t: list[I12 | float] = []
80 | self._animCode = AnimCode()
81 |
82 | @property
83 | def animData(self):
84 | return self._s + self._r + self._t
85 |
86 | @property
87 | def numKeyframes(self):
88 | return max(len(d.getInstance().keyframes) for d in self.animData if isinstance(d, I12))
89 |
90 | def _packAnims(self, data: list[Animation], isScale = False):
91 | """Process animation data for one transformation.
92 |
93 | Return a tuple that contains a bunch of info about this data (e.g., whether it's isometric,
94 | whether it's fixed, etc).
95 | """
96 | fixed = [False] * len(data)
97 | animData: list[I12 | float] = []
98 | # iso can only actually be written for scale, but knowing if data is iso is still useful
99 | iso = all(c == data[0] for c in data)
100 | writeIso = iso and isScale
101 | for i, anim in enumerate(data if not writeIso else data[:1]):
102 | if len(anim.keyframes) == 1:
103 | fixed[i] = True
104 | animData.append(anim.keyframes[0, 1])
105 | else:
106 | animData.append(I12().fromInstance(anim))
107 | if iso:
108 | fixed[:] = fixed[:1] * len(fixed)
109 | identityVal = 1 if isScale else 0
110 | identity = iso and fixed[0] and data[0].keyframes[0, 1] == identityVal
111 | if identity:
112 | animData[:] = []
113 | return (identity, writeIso, fixed, animData)
114 |
115 | def fromInstance(self, data: TexAnim):
116 | super().fromInstance(data)
117 | c = self._animCode
118 | c.identityS, c.isoS, c.fixS, self._s = self._packAnims(data.scale, True)
119 | c.identityR, _, c.fixR, self._r = self._packAnims(data.rot)
120 | c.identityT, _, c.fixT, self._t = self._packAnims(data.trans)
121 | self._size = self._HEAD_STRCT.size + 4 * len(self.animData)
122 | return self
123 |
124 | def _calcSize(self):
125 | return super()._calcSize()
126 |
127 | def pack(self):
128 | """Pack this writer's main data, describing its format w/ pointers to frame data."""
129 | packedHeader = self._animCode.pack()
130 | frameRefOffset = self.offset + self._HEAD_STRCT.size
131 | return packedHeader + packFrameRefs(self.animData, frameRefOffset, individualRelative=True)
132 |
133 |
134 | class MatAnim():
135 | """Contains animation data for a material's texture transforms.
136 |
137 | This data is separated into 2 lists: one for the regular textures, and one for the indirect
138 | texture transforms. Each list has an entry for each texture/transform. If a texture/transform
139 | has no animation, the entry is None.
140 | """
141 |
142 | def __init__(self, matName: str = None):
143 | self.matName = matName
144 | self.texAnims: dict[int, TexAnim] = {}
145 | self.indAnims: dict[int, TexAnim] = {}
146 |
147 |
148 | class MatAnimSerializer(Serializer[SRT0_SER_T, MatAnim]):
149 |
150 | _HEAD_STRCT = Struct(">iII")
151 |
152 |
153 | class MatAnimReader(MatAnimSerializer["SRT0Reader"], Reader, StrPoolReadMixin):
154 |
155 | def unpack(self, data: bytes):
156 | super().unpack(data)
157 | self._data = anim = MatAnim()
158 | unpackedHeader = self._HEAD_STRCT.unpack_from(data, self.offset)
159 | anim.matName = self.readString(data, self.offset + unpackedHeader[0])
160 | o = self.offset + self._HEAD_STRCT.size
161 | # read texture data & indirect transform data
162 | anims = (self._data.texAnims, self._data.indAnims)
163 | maxs = (gx.MAX_TEXTURES, gx.MAX_INDIRECT_MTCS)
164 | for animDict, maxSlots, usedFlag in zip(anims, maxs, unpackedHeader[1:3]):
165 | for i in range(maxSlots):
166 | if usedFlag & (1 << i):
167 | animOffset = self.offset + int.from_bytes(data[o : o + 4], "big")
168 | o += 4
169 | animDict[i] = TexAnimReader(self, animOffset).unpack(data).getInstance()
170 | return self
171 |
172 |
173 | class MatAnimWriter(MatAnimSerializer["SRT0Writer"], Writer, StrPoolWriteMixin):
174 |
175 | def __init__(self, parent: "SRT0Writer", offset = 0):
176 | super().__init__(parent, offset)
177 | self._anims: list[TexAnimWriter] = []
178 |
179 | @property
180 | def animData(self):
181 | """All animation writers used by the texture anim writers of this material anim writer."""
182 | return (d for w in self._anims for d in w.animData if isinstance(d, I12))
183 |
184 | def fromInstance(self, data: MatAnim):
185 | super().fromInstance(data)
186 | texAnims = sorted(data.texAnims.items(), key=lambda item: item[0])
187 | indAnims = sorted(data.indAnims.items(), key=lambda item: item[0])
188 | anims = texAnims + indAnims
189 | animWriterOffset = self.offset + self._HEAD_STRCT.size + 4 * len(anims)
190 | for idx, anim in anims:
191 | writer = TexAnimWriter(self, animWriterOffset).fromInstance(anim)
192 | self._anims.append(writer)
193 | animWriterOffset += writer.size()
194 | self._size = animWriterOffset - self.offset
195 | return self
196 |
197 | def _calcSize(self):
198 | return super()._calcSize()
199 |
200 | @classmethod
201 | def _getUsedFlags(cls, anims: dict[int, TexAnim], maxSlots: int):
202 | """Get flags for the present slots in a dict of texture animations."""
203 | flags = 0
204 | for i in range(maxSlots):
205 | if i in anims:
206 | flags |= (1 << i)
207 | return flags
208 |
209 | def pack(self):
210 | nameOffset = self.stringOffset(self._data.matName) - self.offset
211 | usedTexFlags = self._getUsedFlags(self._data.texAnims, gx.MAX_TEXTURES)
212 | usedIndFlags = self._getUsedFlags(self._data.indAnims, gx.MAX_INDIRECT_MTCS)
213 | packedHeader = self._HEAD_STRCT.pack(nameOffset, usedTexFlags, usedIndFlags)
214 | anmOffsets = b"".join(int(a.offset - self.offset).to_bytes(4, "big") for a in self._anims)
215 | return packedHeader + anmOffsets + b"".join(a.pack() for a in self._anims)
216 |
217 |
218 | class SRT0(AnimSubfile):
219 | """BRRES subfile for MDL0 texture movement animations."""
220 |
221 | _VALID_VERSIONS = (5, )
222 |
223 | def __init__(self, name: str = None, version = -1):
224 | super().__init__(name, version)
225 | self.matAnims: list[MatAnim] = []
226 | self.mtxGen: type[tf.MtxGenerator] = tf.MayaMtxGen2D
227 |
228 |
229 | class SRT0Serializer(SubfileSerializer[BRRES_SER_T, SRT0]):
230 |
231 | DATA_TYPE = SRT0
232 | FOLDER_NAME = "AnmTexSrt(NW4R)"
233 | MAGIC = b"SRT0"
234 |
235 | _HEAD_STRCT = Struct(">iiiiHHi 3x ?")
236 | _MTX_GEN_TYPES = (tf.MayaMtxGen2D, tf.XSIMtxGen2D, tf.MaxMtxGen2D)
237 |
238 |
239 | class SRT0Reader(SRT0Serializer, SubfileReader):
240 |
241 | def unpack(self, data: bytes):
242 | super().unpack(data)
243 | self._data = SRT0()
244 | unpackedHeader = self._HEAD_STRCT.unpack_from(data, self.offset + self._CMN_STRCT.size)
245 | dataOffset = unpackedHeader[0]
246 | self._data.length = unpackedHeader[4]
247 | self._data.mtxGen = self._MTX_GEN_TYPES[unpackedHeader[6]]
248 | self._data.enableLoop = unpackedHeader[7]
249 | if dataOffset > 0:
250 | d = DictReader(self, self.offset + dataOffset).unpack(data)
251 | animData = d.readEntries(data, MatAnimReader)
252 | self._data.matAnims = [matData.getInstance() for matData in animData.values()]
253 | return self
254 |
255 |
256 | class SRT0Writer(SRT0Serializer, SubfileWriter):
257 |
258 | def __init__(self, parent, offset = 0):
259 | super().__init__(parent, offset)
260 | dictOffset = offset + self._CMN_STRCT.size + self._HEAD_STRCT.size
261 | self._matAnims: DictWriter[MatAnimWriter] = DictWriter(self, dictOffset)
262 | self._animData: list[list[I12]] = []
263 |
264 | def getStrings(self):
265 | return self._matAnims.getStrings()
266 |
267 | def fromInstance(self, data: SRT0):
268 | super().fromInstance(data)
269 | animWriters: dict[str, MatAnimWriter] = {}
270 | dataOffset = self._matAnims.offset + DictWriter.sizeFromLen(len(data.matAnims))
271 | for a in data.matAnims:
272 | animWriters[a.matName] = writer = MatAnimWriter(self, dataOffset).fromInstance(a)
273 | dataOffset += writer.size()
274 | self._matAnims.fromInstance(animWriters)
275 | self._animData = groupAnimWriters([list(a.animData) for a in animWriters.values()])
276 | for anims in self._animData:
277 | for anim in anims:
278 | anim.offset = dataOffset
279 | dataOffset += anims[0].size()
280 | self._size = dataOffset - self.offset
281 | return self
282 |
283 | def _calcSize(self):
284 | return super()._calcSize()
285 |
286 | def pack(self):
287 | packedHeader = self._HEAD_STRCT.pack(
288 | self._CMN_STRCT.size + self._HEAD_STRCT.size, 0,
289 | self.stringOffset(self._data.name) - self.offset, 0,
290 | self._data.length, len(self._data.matAnims),
291 | self._MTX_GEN_TYPES.index(self._data.mtxGen), self._data.enableLoop
292 | )
293 | matAnimWriters: list[MatAnimWriter] = self._matAnims.getInstance().values()
294 | packedData = b"".join(w.pack() for w in matAnimWriters)
295 | packedData += b"".join(w[0].pack() for w in self._animData)
296 | return super().pack() + packedHeader + self._matAnims.pack() + packedData
297 |
--------------------------------------------------------------------------------
/berrybush/wii/subfile.py:
--------------------------------------------------------------------------------
1 | # standard imports
2 | from abc import abstractmethod
3 | from struct import Struct
4 | from typing import TypeVar, TYPE_CHECKING
5 | # internal imports
6 | from .binaryutils import pad
7 | from .serialization import Serializer, Reader, Writer, StrPoolReadMixin, StrPoolWriteMixin
8 | # special typing imports
9 | if TYPE_CHECKING:
10 | from brres import BRRESSerializer, BRRESReader, BRRESWriter
11 | else:
12 | BRRESSerializer = BRRESReader = BRRESWriter = object
13 |
14 |
15 | BRRES_SER_T = TypeVar("BRRES_SER_T", bound=BRRESSerializer)
16 | FILE_T = TypeVar("FILE_T", bound="Subfile")
17 |
18 |
19 | class SubfileVersionError(Exception):
20 | """Attempted to set an invalid subfile version"""
21 |
22 | def __init__(self, subfileType: type["Subfile"], ver: int):
23 | super().__init__(f"Version {ver} is invalid for {subfileType.__name__} subfiles")
24 |
25 |
26 | class Subfile():
27 | """Component of a BRRES file for storing an individual resource (model, texture, etc)."""
28 |
29 | _VALID_VERSIONS: tuple[int, ...] # versions supported so far
30 |
31 | def __init__(self, name: str = None, version = -1):
32 | self.name = name
33 | self.version = version
34 |
35 | @property
36 | def version(self) -> int:
37 | """Subfile format version.
38 |
39 | Must be valid for this subfile type. Setting to -1 makes it the most recent option."""
40 | return self._version
41 |
42 | @version.setter
43 | def version(self, version):
44 | if version == -1: # -1 indicates to just use most recent version
45 | version = self._VALID_VERSIONS[-1]
46 | if version not in self._VALID_VERSIONS:
47 | raise SubfileVersionError(type(self), version)
48 | self._version = version
49 |
50 |
51 | class SubfileSerializer(Serializer[BRRES_SER_T, FILE_T]):
52 | """Serializer for a BRRES subfile."""
53 | FOLDER_NAME: str # name of brres folder used for storing this subfile type
54 | MAGIC: bytes # 4 bytes used for file type identification
55 | _CMN_STRCT = Struct(">4s IIi") # common header at start of every subfile
56 |
57 |
58 | class SubfileReader(SubfileSerializer[BRRESReader, FILE_T], Reader, StrPoolReadMixin):
59 | """Reader for a BRRES subfile."""
60 |
61 | @abstractmethod
62 | def unpack(self, data: bytes):
63 | super().unpack(data)
64 | unpackedHeader = self._CMN_STRCT.unpack_from(data, self._offset)
65 | self._data = self.DATA_TYPE(version=unpackedHeader[2])
66 | return self
67 |
68 | def _updateInstance(self):
69 | super()._updateInstance()
70 | self._data.name = self.parentSer.fileName(self)
71 |
72 |
73 | class SubfileWriter(SubfileSerializer[BRRESWriter, FILE_T], Writer, StrPoolWriteMixin):
74 | """Writer for a BRRES subfile."""
75 |
76 | def size(self):
77 | return pad(super().size(), 4)
78 |
79 | @abstractmethod
80 | def pack(self):
81 | head = self._CMN_STRCT.pack(self.MAGIC, self._size, self._data.version, -self._offset)
82 | return super().pack() + head
83 |
--------------------------------------------------------------------------------
/berrybush/wii/vis0.py:
--------------------------------------------------------------------------------
1 | # standard imports
2 | from typing import TypeVar
3 | from struct import Struct
4 | # 3rd party imports
5 | import numpy as np
6 | # internal imports
7 | from .animation import AnimSubfile
8 | from .binaryutils import pad
9 | from .bitstruct import BitStruct, Bits
10 | from .brresdict import DictReader, DictWriter
11 | from .serialization import Serializer, Reader, Writer, StrPoolReadMixin, StrPoolWriteMixin
12 | from .subfile import BRRES_SER_T, SubfileSerializer, SubfileReader, SubfileWriter
13 |
14 |
15 | VIS0_SER_T = TypeVar("VIS0_SER_T", bound="VIS0Serializer")
16 |
17 |
18 | class AnimCode(BitStruct):
19 | fixedVal = Bits(1, bool)
20 | fixed = Bits(1, bool)
21 |
22 |
23 | class JointAnim():
24 | """Contains visibility animation data for a joint."""
25 |
26 | def __init__(self, jointName: str = None):
27 | self.jointName = jointName
28 | self.frames = np.ndarray((0), bool)
29 |
30 |
31 | class JointAnimSerializer(Serializer[VIS0_SER_T, JointAnim]):
32 |
33 | _HEAD_STRCT = Struct(">iI")
34 |
35 |
36 | class JointAnimReader(JointAnimSerializer["VIS0Reader"], Reader, StrPoolReadMixin):
37 |
38 | def unpack(self, data: bytes):
39 | super().unpack(data)
40 | self._data = anim = JointAnim()
41 | unpackedHeader = self._HEAD_STRCT.unpack_from(data, self.offset)
42 | anim.jointName = self.readString(data, self.offset + unpackedHeader[0])
43 | animCode = AnimCode(unpackedHeader[1])
44 | if animCode.fixed:
45 | anim.frames = np.array([animCode.fixedVal])
46 | else:
47 | # data is stored as a simple sequence of bits, padded to a multiple of 4 bytes
48 | numFrames = self.parentSer.length
49 | dataOffset = self.offset + self._HEAD_STRCT.size
50 | dataSize = pad(numFrames, 32) // 8
51 | unpackedData = np.frombuffer(data[dataOffset : dataOffset + dataSize], dtype=np.uint8)
52 | anim.frames = np.unpackbits(unpackedData)[:numFrames].astype(bool)
53 | return self
54 |
55 |
56 | class JointAnimWriter(JointAnimSerializer["VIS0Writer"], Writer, StrPoolWriteMixin):
57 |
58 | def __init__(self, parent: "VIS0Writer", offset = 0):
59 | super().__init__(parent, offset)
60 | self._animCode = AnimCode()
61 |
62 | def fromInstance(self, data: JointAnim):
63 | super().fromInstance(data)
64 | self._animCode = AnimCode()
65 | if np.all(data.frames == data.frames[0]):
66 | self._animCode.fixed = True
67 | self._animCode.fixedVal = data.frames[0]
68 | self._size = self._HEAD_STRCT.size
69 | else:
70 | # calculate & store data size (here instead of in _calcSize() for convenience;
71 | # only have to check if fixed once)
72 | numFrames = self.parentSer.getInstance().length
73 | dataSize = pad(numFrames, 32) // 8
74 | self._size = self._HEAD_STRCT.size + dataSize
75 | return self
76 |
77 | def _calcSize(self):
78 | return super()._calcSize()
79 |
80 | def pack(self):
81 | packedData = b""
82 | if not self._animCode.fixed:
83 | numFrames = self.parentSer.getInstance().length
84 | dataSize = pad(numFrames, 32) // 8
85 | frames = self._data.frames[:numFrames]
86 | frames = np.pad(frames, (0, numFrames - len(frames)), "edge")
87 | packedData = pad(np.packbits(frames).tobytes(), dataSize)
88 | nameOffset = self.stringOffset(self._data.jointName) - self.offset
89 | return self._HEAD_STRCT.pack(nameOffset, int(self._animCode)) + packedData
90 |
91 |
92 | class VIS0(AnimSubfile):
93 | """BRRES subfile for MDL0 joint visibility animations."""
94 |
95 | _VALID_VERSIONS = (4, )
96 |
97 | def __init__(self, name: str = None, version = -1):
98 | super().__init__(name, version)
99 | self.jointAnims: list[JointAnim] = []
100 |
101 |
102 | class VIS0Serializer(SubfileSerializer[BRRES_SER_T, VIS0]):
103 |
104 | DATA_TYPE = VIS0
105 | FOLDER_NAME = "AnmVis(NW4R)"
106 | MAGIC = b"VIS0"
107 |
108 | _HEAD_STRCT = Struct(">iiiiHH 3x ?")
109 |
110 |
111 | class VIS0Reader(VIS0Serializer, SubfileReader):
112 |
113 | def __init__(self, parent, offset = 0):
114 | super().__init__(parent, offset)
115 | self.length: int = 0
116 |
117 | def unpack(self, data: bytes):
118 | super().unpack(data)
119 | self._data = VIS0()
120 | unpackedHeader = self._HEAD_STRCT.unpack_from(data, self.offset + self._CMN_STRCT.size)
121 | dataOffset = unpackedHeader[0]
122 | self._data.length = self.length = unpackedHeader[4]
123 | self._data.enableLoop = unpackedHeader[6]
124 | if dataOffset > 0:
125 | d = DictReader(self, self.offset + dataOffset).unpack(data)
126 | animData = d.readEntries(data, JointAnimReader)
127 | self._data.jointAnims = [jointData.getInstance() for jointData in animData.values()]
128 | return self
129 |
130 |
131 | class VIS0Writer(VIS0Serializer, SubfileWriter):
132 |
133 | def __init__(self, parent, offset = 0):
134 | super().__init__(parent, offset)
135 | dictOffset = offset + self._CMN_STRCT.size + self._HEAD_STRCT.size
136 | self._jointAnims: DictWriter[JointAnimWriter] = DictWriter(self, dictOffset)
137 |
138 | def getStrings(self):
139 | return self._jointAnims.getStrings()
140 |
141 | def fromInstance(self, data: VIS0):
142 | super().fromInstance(data)
143 | animWriters: dict[str, JointAnimWriter] = {}
144 | dataOffset = self._jointAnims.offset + DictWriter.sizeFromLen(len(data.jointAnims))
145 | for a in data.jointAnims:
146 | animWriters[a.jointName] = writer = JointAnimWriter(self, dataOffset).fromInstance(a)
147 | dataOffset += writer.size()
148 | self._jointAnims.fromInstance(animWriters)
149 | self._size = dataOffset - self.offset
150 | return self
151 |
152 | def _calcSize(self):
153 | return super()._calcSize()
154 |
155 | def pack(self):
156 | packedHeader = self._HEAD_STRCT.pack(
157 | self._CMN_STRCT.size + self._HEAD_STRCT.size, 0,
158 | self.stringOffset(self._data.name) - self.offset, 0,
159 | self._data.length, len(self._data.jointAnims), self._data.enableLoop,
160 | )
161 | jointAnimWriters: list[JointAnimWriter] = self._jointAnims.getInstance().values()
162 | packedData = b"".join(w.pack() for w in jointAnimWriters)
163 | return super().pack() + packedHeader + self._jointAnims.pack() + packedData
164 |
--------------------------------------------------------------------------------
/docs/animation-merge-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/animation-merge-example.png
--------------------------------------------------------------------------------
/docs/animation-nla-access.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/animation-nla-access.png
--------------------------------------------------------------------------------
/docs/backup-options.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/backup-options.png
--------------------------------------------------------------------------------
/docs/blending-discarding.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/blending-discarding.png
--------------------------------------------------------------------------------
/docs/color-alpha.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/color-alpha.png
--------------------------------------------------------------------------------
/docs/color-regs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/color-regs.png
--------------------------------------------------------------------------------
/docs/color-swap-brga-input.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/color-swap-brga-input.png
--------------------------------------------------------------------------------
/docs/color-swap-brga-output.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/color-swap-brga-output.png
--------------------------------------------------------------------------------
/docs/color-swap-brga.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/color-swap-brga.png
--------------------------------------------------------------------------------
/docs/color-swap-rgba.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/color-swap-rgba.png
--------------------------------------------------------------------------------
/docs/color-swap-table.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/color-swap-table.png
--------------------------------------------------------------------------------
/docs/depth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/depth.png
--------------------------------------------------------------------------------
/docs/export.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/export.png
--------------------------------------------------------------------------------
/docs/format-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/format-example.png
--------------------------------------------------------------------------------
/docs/getting-started/1/blank-scene.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/blank-scene.png
--------------------------------------------------------------------------------
/docs/getting-started/1/brawlbox-export.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/brawlbox-export.png
--------------------------------------------------------------------------------
/docs/getting-started/1/brawlbox-replace.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/brawlbox-replace.png
--------------------------------------------------------------------------------
/docs/getting-started/1/cactus-pyramid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/cactus-pyramid.png
--------------------------------------------------------------------------------
/docs/getting-started/1/developer-extras.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/developer-extras.png
--------------------------------------------------------------------------------
/docs/getting-started/1/dolphin-first-export.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/dolphin-first-export.png
--------------------------------------------------------------------------------
/docs/getting-started/1/dolphin-setting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/dolphin-setting.png
--------------------------------------------------------------------------------
/docs/getting-started/1/dump-filtered.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/dump-filtered.png
--------------------------------------------------------------------------------
/docs/getting-started/1/dump.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/dump.png
--------------------------------------------------------------------------------
/docs/getting-started/1/front-view-lm-grayscale.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/front-view-lm-grayscale.png
--------------------------------------------------------------------------------
/docs/getting-started/1/front-view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/front-view.png
--------------------------------------------------------------------------------
/docs/getting-started/1/full-glory.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/full-glory.png
--------------------------------------------------------------------------------
/docs/getting-started/1/image-editor-lm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/image-editor-lm.png
--------------------------------------------------------------------------------
/docs/getting-started/1/image-editor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/image-editor.png
--------------------------------------------------------------------------------
/docs/getting-started/1/image-format-i8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/image-format-i8.png
--------------------------------------------------------------------------------
/docs/getting-started/1/imported.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/imported.png
--------------------------------------------------------------------------------
/docs/getting-started/1/info-log.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/info-log.png
--------------------------------------------------------------------------------
/docs/getting-started/1/lm-colored.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/lm-colored.png
--------------------------------------------------------------------------------
/docs/getting-started/1/material-settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/material-settings.png
--------------------------------------------------------------------------------
/docs/getting-started/1/obj-yikes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/obj-yikes.png
--------------------------------------------------------------------------------
/docs/getting-started/1/pad-to.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/pad-to.png
--------------------------------------------------------------------------------
/docs/getting-started/1/pyramid-flipped-with-texture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/pyramid-flipped-with-texture.png
--------------------------------------------------------------------------------
/docs/getting-started/1/pyramid-flipped.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/pyramid-flipped.png
--------------------------------------------------------------------------------
/docs/getting-started/1/pyramid-green-ingame.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/pyramid-green-ingame.png
--------------------------------------------------------------------------------
/docs/getting-started/1/pyramid-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/pyramid-green.png
--------------------------------------------------------------------------------
/docs/getting-started/1/pyramid-red-ingame.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/pyramid-red-ingame.png
--------------------------------------------------------------------------------
/docs/getting-started/1/pyramid-red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/pyramid-red.png
--------------------------------------------------------------------------------
/docs/getting-started/1/tev-config-view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/tev-config-view.png
--------------------------------------------------------------------------------
/docs/getting-started/1/tev-stage-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/tev-stage-1.png
--------------------------------------------------------------------------------
/docs/getting-started/1/tev-stage-2-solo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/tev-stage-2-solo.png
--------------------------------------------------------------------------------
/docs/getting-started/1/tev-stage-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/tev-stage-2.png
--------------------------------------------------------------------------------
/docs/getting-started/1/tev-stage-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/tev-stage-color.png
--------------------------------------------------------------------------------
/docs/getting-started/1/tev-stages.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/tev-stages.png
--------------------------------------------------------------------------------
/docs/getting-started/1/texture-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/texture-image.png
--------------------------------------------------------------------------------
/docs/getting-started/1/texture-rotated.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/texture-rotated.png
--------------------------------------------------------------------------------
/docs/getting-started/1/texture-settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/texture-settings.png
--------------------------------------------------------------------------------
/docs/getting-started/1/texture-transform.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/texture-transform.png
--------------------------------------------------------------------------------
/docs/getting-started/1/textures.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/textures.png
--------------------------------------------------------------------------------
/docs/getting-started/1/unpack-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/unpack-icon.png
--------------------------------------------------------------------------------
/docs/getting-started/1/unpack-options.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/unpack-options.png
--------------------------------------------------------------------------------
/docs/getting-started/1/unpack-reload.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/unpack-reload.png
--------------------------------------------------------------------------------
/docs/getting-started/1/verifier-blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/verifier-blue.png
--------------------------------------------------------------------------------
/docs/getting-started/1/verifier-options.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/verifier-options.png
--------------------------------------------------------------------------------
/docs/getting-started/1/verifier-yellow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/verifier-yellow.png
--------------------------------------------------------------------------------
/docs/getting-started/1/verify-search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/verify-search.png
--------------------------------------------------------------------------------
/docs/getting-started/1/warning-suppressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/warning-suppressed.png
--------------------------------------------------------------------------------
/docs/getting-started/1/warning-unsuppressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/1/warning-unsuppressed.png
--------------------------------------------------------------------------------
/docs/getting-started/2/active-action.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/active-action.png
--------------------------------------------------------------------------------
/docs/getting-started/2/add-vcol.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/add-vcol.png
--------------------------------------------------------------------------------
/docs/getting-started/2/animation-error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/animation-error.png
--------------------------------------------------------------------------------
/docs/getting-started/2/animation-preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/animation-preview.png
--------------------------------------------------------------------------------
/docs/getting-started/2/animation-workspace.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/animation-workspace.png
--------------------------------------------------------------------------------
/docs/getting-started/2/blending-discarding.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/blending-discarding.png
--------------------------------------------------------------------------------
/docs/getting-started/2/blending-enabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/blending-enabled.png
--------------------------------------------------------------------------------
/docs/getting-started/2/bone-parenting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/bone-parenting.png
--------------------------------------------------------------------------------
/docs/getting-started/2/brawlbox-animation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/brawlbox-animation.png
--------------------------------------------------------------------------------
/docs/getting-started/2/color-attribute-set.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/color-attribute-set.png
--------------------------------------------------------------------------------
/docs/getting-started/2/cube-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/cube-black.png
--------------------------------------------------------------------------------
/docs/getting-started/2/cube-ingame-lit-lava.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/cube-ingame-lit-lava.png
--------------------------------------------------------------------------------
/docs/getting-started/2/cube-ingame-lit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/cube-ingame-lit.png
--------------------------------------------------------------------------------
/docs/getting-started/2/cube-ingame-xlu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/cube-ingame-xlu.png
--------------------------------------------------------------------------------
/docs/getting-started/2/cube-ingame.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/cube-ingame.png
--------------------------------------------------------------------------------
/docs/getting-started/2/cube-red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/cube-red.png
--------------------------------------------------------------------------------
/docs/getting-started/2/cube-textured-cmpr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/cube-textured-cmpr.png
--------------------------------------------------------------------------------
/docs/getting-started/2/cube-textured-nearest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/cube-textured-nearest.png
--------------------------------------------------------------------------------
/docs/getting-started/2/cube-textured.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/cube-textured.png
--------------------------------------------------------------------------------
/docs/getting-started/2/cube-transformed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/cube-transformed.png
--------------------------------------------------------------------------------
/docs/getting-started/2/cube-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/cube-white.png
--------------------------------------------------------------------------------
/docs/getting-started/2/culling-back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/culling-back.png
--------------------------------------------------------------------------------
/docs/getting-started/2/depth-disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/depth-disabled.png
--------------------------------------------------------------------------------
/docs/getting-started/2/export-settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/export-settings.png
--------------------------------------------------------------------------------
/docs/getting-started/2/graph-editor-access.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/graph-editor-access.png
--------------------------------------------------------------------------------
/docs/getting-started/2/graph-editor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/graph-editor.png
--------------------------------------------------------------------------------
/docs/getting-started/2/half-alpha.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/half-alpha.png
--------------------------------------------------------------------------------
/docs/getting-started/2/light-channel-added.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/light-channel-added.png
--------------------------------------------------------------------------------
/docs/getting-started/2/light-channel-associated.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/light-channel-associated.png
--------------------------------------------------------------------------------
/docs/getting-started/2/light-channel-no-associated.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/light-channel-no-associated.png
--------------------------------------------------------------------------------
/docs/getting-started/2/lightmap-proper-mapping.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/lightmap-proper-mapping.png
--------------------------------------------------------------------------------
/docs/getting-started/2/lightmap-textures-proper.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/lightmap-textures-proper.png
--------------------------------------------------------------------------------
/docs/getting-started/2/lightmap-textures.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/lightmap-textures.png
--------------------------------------------------------------------------------
/docs/getting-started/2/nla-open.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/nla-open.png
--------------------------------------------------------------------------------
/docs/getting-started/2/no-tev-config.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/no-tev-config.png
--------------------------------------------------------------------------------
/docs/getting-started/2/pose-animation-created.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/pose-animation-created.png
--------------------------------------------------------------------------------
/docs/getting-started/2/pose-animation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/pose-animation.png
--------------------------------------------------------------------------------
/docs/getting-started/2/pow-imported.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/pow-imported.png
--------------------------------------------------------------------------------
/docs/getting-started/2/push-down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/push-down.png
--------------------------------------------------------------------------------
/docs/getting-started/2/pushed-down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/pushed-down.png
--------------------------------------------------------------------------------
/docs/getting-started/2/stage-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/stage-1.png
--------------------------------------------------------------------------------
/docs/getting-started/2/stage-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/stage-2.png
--------------------------------------------------------------------------------
/docs/getting-started/2/stage-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/stage-3.png
--------------------------------------------------------------------------------
/docs/getting-started/2/stage-color-constant.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/stage-color-constant.png
--------------------------------------------------------------------------------
/docs/getting-started/2/stage-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/stage-color.png
--------------------------------------------------------------------------------
/docs/getting-started/2/stages.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/stages.png
--------------------------------------------------------------------------------
/docs/getting-started/2/tev-eq.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/tev-eq.png
--------------------------------------------------------------------------------
/docs/getting-started/2/tev-output.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/tev-output.png
--------------------------------------------------------------------------------
/docs/getting-started/2/tev-texture-rgb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/tev-texture-rgb.png
--------------------------------------------------------------------------------
/docs/getting-started/2/texture-added.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/texture-added.png
--------------------------------------------------------------------------------
/docs/getting-started/2/tints-applied.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/tints-applied.png
--------------------------------------------------------------------------------
/docs/getting-started/2/translucent-render-group-enabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/translucent-render-group-enabled.png
--------------------------------------------------------------------------------
/docs/getting-started/2/uv-grid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/uv-grid.png
--------------------------------------------------------------------------------
/docs/getting-started/2/uv-slots.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/uv-slots.png
--------------------------------------------------------------------------------
/docs/getting-started/2/uv2-broken.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/uv2-broken.png
--------------------------------------------------------------------------------
/docs/getting-started/2/uv2-fixed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/uv2-fixed.png
--------------------------------------------------------------------------------
/docs/getting-started/2/vertex-paint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/vertex-paint.png
--------------------------------------------------------------------------------
/docs/getting-started/2/vertex-painted.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/getting-started/2/vertex-painted.png
--------------------------------------------------------------------------------
/docs/hotkey-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/hotkey-example.png
--------------------------------------------------------------------------------
/docs/image-active.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/image-active.png
--------------------------------------------------------------------------------
/docs/image-animation-slots.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/image-animation-slots.png
--------------------------------------------------------------------------------
/docs/image-mipmaps.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/image-mipmaps.png
--------------------------------------------------------------------------------
/docs/import.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/import.png
--------------------------------------------------------------------------------
/docs/indirect-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/indirect-example.png
--------------------------------------------------------------------------------
/docs/indirect-selections.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/indirect-selections.png
--------------------------------------------------------------------------------
/docs/indirect-texturing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/indirect-texturing.png
--------------------------------------------------------------------------------
/docs/indirect-water.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/indirect-water.png
--------------------------------------------------------------------------------
/docs/install-button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/install-button.png
--------------------------------------------------------------------------------
/docs/install-enable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/install-enable.png
--------------------------------------------------------------------------------
/docs/install-file-select.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/install-file-select.png
--------------------------------------------------------------------------------
/docs/install-preferences.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/install-preferences.png
--------------------------------------------------------------------------------
/docs/install-release.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/install-release.png
--------------------------------------------------------------------------------
/docs/install-save-prefs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/install-save-prefs.png
--------------------------------------------------------------------------------
/docs/lighting-channel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/lighting-channel.png
--------------------------------------------------------------------------------
/docs/logo-blender.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/logo-blender.png
--------------------------------------------------------------------------------
/docs/logo-ingame.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/logo-ingame.png
--------------------------------------------------------------------------------
/docs/material.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/material.png
--------------------------------------------------------------------------------
/docs/mesh-settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/mesh-settings.png
--------------------------------------------------------------------------------
/docs/scene-settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/scene-settings.png
--------------------------------------------------------------------------------
/docs/selections.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/selections.png
--------------------------------------------------------------------------------
/docs/texture-settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/texture-settings.png
--------------------------------------------------------------------------------
/docs/vis-driver-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/docs/vis-driver-example.png
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hayden0729/berrybush/248875c0b96d1eba6533e25a9b6d67527f995cea/logo.png
--------------------------------------------------------------------------------