├── .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 --------------------------------------------------------------------------------