├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── flver.py ├── importer.py ├── preview.png └── reader.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Eliza Velasquez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Fromsoft FLVER Plugin for Blender 2 | ================================= 3 | 4 | This is a plugin for importing Fromsoft FLVER files from games such as *Dark 5 | Souls*, *Bloodborne*, and *Sekiro* into Blender. It supports importing 6 | geometry, (untextured) materials, and armature data. It has been tested on a 7 | very limited selection of actual files (specifically from *Dark Souls* and 8 | *Dark Souls: Remastered*), so some tweaking may be required. The code for 9 | interpreting the FLVER files was heavily based on 10 | [SoulsFormats](https://github.com/JKAnderson/SoulsFormats). 11 | 12 | ![](preview.png?raw=true) 13 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "Import Fromsoft FLVER models", 3 | "description": 4 | "Import models from various Fromsoft games such as Dark Souls", 5 | "author": "Eliza Velasquez", 6 | "version": (0, 1, 0), 7 | "blender": (2, 80, 0), 8 | "category": "Import-Export", 9 | "location": "File > Import", 10 | "warning": "", 11 | "support": "COMMUNITY", 12 | "wiki_url": "", # TODO: wiki url 13 | "tracker_url": "", # TODO: tracker url 14 | } 15 | 16 | _submodules = { 17 | "importer", 18 | "flver", 19 | "reader", 20 | } 21 | 22 | # Reload submodules on addon reload 23 | if "bpy" in locals(): 24 | import importlib 25 | for submodule in _submodules: 26 | if submodule in locals(): 27 | importlib.reload(locals()[submodule]) 28 | 29 | import bpy 30 | from . import importer 31 | from bpy_extras.io_utils import ImportHelper 32 | from bpy.props import StringProperty, BoolProperty 33 | 34 | 35 | class FlverImporter(bpy.types.Operator, ImportHelper): 36 | bl_idname = "import_scene.flver" 37 | bl_label = "Fromsoft (.flver)" 38 | 39 | filter_glob = StringProperty(default="*.flver", options={"HIDDEN"}) 40 | 41 | transpose_y_and_z = BoolProperty( 42 | name="Transpose Y and Z axes", 43 | description=("This will correct the orientation of the model. " + 44 | "Rarely necessary to disable."), 45 | default=True) 46 | 47 | import_skeleton = BoolProperty( 48 | name="Import skeleton", 49 | description=("Disable to prevent the creation of an Armature " + 50 | "and corresponding vertex groups."), 51 | default=True) 52 | 53 | connect_bones = BoolProperty( 54 | name="Connect bones", 55 | description=( 56 | "Disable to import disjointed bones rotated about their " + 57 | "original Euler angles. This may be potentially desireable " 58 | "for authoring derivative FLVER files."), 59 | default=True) 60 | 61 | def execute(self, context): 62 | importer.run(context=context, 63 | path=self.filepath, 64 | transpose_y_and_z=self.transpose_y_and_z, 65 | import_skeleton=self.import_skeleton, 66 | connect_bones=self.connect_bones) 67 | return {"FINISHED"} 68 | 69 | 70 | def menu_import(self, context): 71 | self.layout.operator(FlverImporter.bl_idname) 72 | 73 | 74 | def register(): 75 | bpy.utils.register_class(FlverImporter) 76 | bpy.types.TOPBAR_MT_file_import.append(menu_import) 77 | 78 | 79 | def unregister(): 80 | bpy.types.TOPBAR_MT_file_import.remove(menu_import) 81 | bpy.utils.unregister_class(FlverImporter) 82 | -------------------------------------------------------------------------------- /flver.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | import struct 3 | 4 | 5 | class Endianness(Enum): 6 | BIG = b"B\0" 7 | LITTLE = b"L\0" 8 | 9 | 10 | class TextEncoding(Enum): 11 | SHIFT_JIS = 0 12 | UTF_16 = 1 13 | 14 | 15 | class Header: 16 | def __init__(self, endianness, version, bounding_box_min, bounding_box_max, 17 | default_vertex_index_size, text_encoding, unk4A, unk4C, unk5C, 18 | unk5D, unk68): 19 | self.endianness = endianness 20 | self.version = version 21 | self.bounding_box_min = bounding_box_min 22 | self.bounding_box_max = bounding_box_max 23 | self.default_vertex_index_size = default_vertex_index_size 24 | self.text_encoding = text_encoding 25 | self.unk4A = unk4A 26 | self.unk4C = unk4C 27 | self.unk5C = unk5C 28 | self.unk5D = unk5D 29 | self.unk68 = unk68 30 | 31 | 32 | class Dummy: 33 | def __init__(self, position, color, forward, reference_id, 34 | parent_bone_index, upward, attach_bone_index, flag1, 35 | use_upward_vector, unk30, unk34): 36 | self.position = position 37 | self.color = color 38 | self.forward = forward 39 | self.reference_id = reference_id 40 | self.parent_bone_index = parent_bone_index 41 | self.upward = upward 42 | self.attach_bone_index = attach_bone_index 43 | self.flag1 = flag1 44 | self.use_upward_vector = use_upward_vector 45 | self.unk30 = unk30 46 | self.unk34 = unk34 47 | 48 | 49 | class Material: 50 | def __init__(self, name, mtd_path, texture_count, texture_index, flags, 51 | unk18): 52 | self.name = name 53 | self.mtd_path = mtd_path 54 | self.texture_count = texture_count 55 | self.texture_index = texture_index 56 | self.flags = flags 57 | self.unk18 = unk18 58 | 59 | 60 | class Bone: 61 | def __init__(self, translation, name, rotation, parent_index, child_index, 62 | scale, next_sibling_index, previous_sibling_index, 63 | bounding_box_min, unk3C, bounding_box_max): 64 | self.translation = translation 65 | self.name = name 66 | self.rotation = rotation 67 | self.parent_index = parent_index 68 | self.child_index = child_index 69 | self.scale = scale 70 | self.next_sibling_index = next_sibling_index 71 | self.bounding_box_min = bounding_box_min 72 | self.unk3C = unk3C 73 | self.bounding_box_max = bounding_box_max 74 | 75 | 76 | class Mesh: 77 | # According to upstream: when 1, mesh is in bind pose; when 0, it isn't. 78 | # Most likely has further implications. 79 | class DynamicMode(Enum): 80 | NON_DYNAMIC = 0 81 | DYNAMIC = 1 82 | 83 | def __init__(self, dynamic_mode, material_index, default_bone_index, 84 | bone_indices, index_buffer_indices, vertex_buffer_indices): 85 | self.dynamic_mode = dynamic_mode 86 | self.material_index = material_index 87 | self.default_bone_index = default_bone_index 88 | self.bone_indices = bone_indices 89 | self.index_buffer_indices = index_buffer_indices 90 | self.vertex_buffer_indices = vertex_buffer_indices 91 | 92 | 93 | class IndexBuffer: 94 | class DetailFlags(Enum): 95 | LOD_LEVEL1 = 0x01000000 96 | LOD_LEVEL2 = 0x02000000 97 | MOTION_BLUR = 0x80000000 98 | 99 | class PrimitiveMode(Enum): 100 | TRIANGLES = 0 101 | TRIANGLE_STRIP = 1 102 | 103 | class BackfaceVisibility(Enum): 104 | SHOW = 0 105 | CULL = 1 106 | 107 | def __init__(self, detail_flags, primitive_mode, backface_visibility, 108 | unk06, indices): 109 | self.detail_flags = detail_flags 110 | self.primitive_mode = primitive_mode 111 | self.backface_visibility = backface_visibility 112 | self.unk06 = unk06 113 | self.indices = indices 114 | 115 | def _inflate(self, faces): 116 | if self.primitive_mode == self.PrimitiveMode.TRIANGLES: 117 | for i in range(0, len(self.indices), 3): 118 | faces.append(tuple(self.indices[i:i + 3])) 119 | else: 120 | direction = -1 121 | f1 = self.indices[0] 122 | f2 = self.indices[1] 123 | for i in range(2, len(self.indices)): 124 | f3 = self.indices[i] 125 | direction *= -1 126 | if f1 != f2 and f2 != f3 and f3 != f1: 127 | if direction > 0: 128 | faces.append((f1, f2, f3)) 129 | else: 130 | faces.append((f1, f3, f2)) 131 | f1 = f2 132 | f2 = f3 133 | 134 | 135 | class VertexBuffer: 136 | def __init__(self, buffer_index, struct_index, struct_size, vertex_count, 137 | buffer_data): 138 | self.buffer_index = buffer_index 139 | self.struct_index = struct_index 140 | self.struct_size = struct_size 141 | self.vertex_count = vertex_count 142 | self.buffer_data = buffer_data 143 | 144 | def _inflate(self, vertices, struct, version): 145 | struct_size = sum(member.size() for member in struct) 146 | assert self.struct_size == struct_size 147 | assert len(self.buffer_data) % self.struct_size == 0 148 | 149 | # For now, only select from a limited set of attributes: POSITION, 150 | # BONE_WEIGHTS, BONE_INDICES, and UV. 151 | struct_members = [ 152 | member for member in struct if member.attribute_type in { 153 | VertexBufferStructMember.AttributeType.POSITION, 154 | VertexBufferStructMember.AttributeType.BONE_WEIGHTS, 155 | VertexBufferStructMember.AttributeType.BONE_INDICES, 156 | VertexBufferStructMember.AttributeType.UV, 157 | } 158 | ] 159 | 160 | attribute_map = { 161 | VertexBufferStructMember.AttributeType.POSITION: 162 | vertices.positions, 163 | VertexBufferStructMember.AttributeType.BONE_WEIGHTS: 164 | vertices.bone_weights, 165 | VertexBufferStructMember.AttributeType.BONE_INDICES: 166 | vertices.bone_indices, 167 | VertexBufferStructMember.AttributeType.UV: vertices.uv, 168 | } 169 | for member in struct_members: 170 | for i in range(self.vertex_count): 171 | data = member._unpack(self.buffer_data, i * self.struct_size, 172 | version) 173 | attribute_map[member.attribute_type].append(data) 174 | 175 | 176 | class VertexBufferStructMember: 177 | class DataType(Enum): 178 | # Two single-precision floats. 179 | FLOAT2 = 0x01 180 | # Three single-precision floats. 181 | FLOAT3 = 0x02 182 | # Four single-precision floats. 183 | FLOAT4 = 0x03 184 | # Unknown. 185 | BYTE4A = 0x10 186 | # Four bytes 187 | BONE_INDICES = 0x11 188 | # Two shorts? 189 | SHORT2_TO_FLOAT2 = 0x12 190 | # Four bytes. 191 | BYTE4C = 0x13 192 | # Two shorts. 193 | UV = 0x15 194 | # Two shorts and two shorts. 195 | UV_PAIR = 0x16 196 | # Four shorts, maybe unsigned? 197 | SHORT_BONE_INDICES = 0x18 198 | # Four shorts. 199 | BONE_WEIGHTS = 0x1A 200 | # Unknown. 201 | SHORT4_TO_FLOAT4B = 0x2E 202 | # Unknown. 203 | BYTE4E = 0x2F 204 | 205 | class AttributeType(Enum): 206 | # Location of the vertex. 207 | POSITION = 0 208 | # Weight of the vertex's attachment to bones. 209 | BONE_WEIGHTS = 1 210 | # Bones the vertex is weighted to, indexing the parent mesh's bone 211 | # indices. 212 | BONE_INDICES = 2 213 | # Orientation of the vertex. 214 | NORMAL = 3 215 | # Texture coordinates of the vertex. 216 | UV = 5 217 | # Vector pointing perpendicular to the normal. 218 | TANGENT = 6 219 | # Vector pointing perpendicular to the normal and tangent. 220 | BITANGENT = 7 221 | # Data used for blending, alpha, etc. 222 | VERTEX_COLOR = 10 223 | 224 | def __init__(self, unk00, struct_offset, data_type, attribute_type, index): 225 | self.unk00 = unk00 226 | self.struct_offset = struct_offset 227 | self.data_type = data_type 228 | self.attribute_type = attribute_type 229 | self.index = index 230 | 231 | def size(self): 232 | if self.data_type in { 233 | self.DataType.BYTE4A, 234 | self.DataType.BONE_INDICES, 235 | self.DataType.SHORT2_TO_FLOAT2, 236 | self.DataType.BYTE4C, 237 | self.DataType.UV, 238 | self.DataType.BYTE4E, 239 | }: 240 | return 4 241 | if self.data_type in { 242 | self.DataType.FLOAT2, 243 | self.DataType.UV_PAIR, 244 | self.DataType.BONE_INDICES, 245 | self.DataType.BONE_WEIGHTS, 246 | self.DataType.SHORT4_TO_FLOAT4B, 247 | }: 248 | return 8 249 | if self.data_type == self.DataType.FLOAT3: 250 | return 12 251 | if self.data_type == self.DataType.FLOAT4: 252 | return 16 253 | raise Exception("unknown size for data type") 254 | 255 | def _unpack(self, buf, offset, version): 256 | if version >= 0x2000F: 257 | uv_divisor = 2048.0 258 | else: 259 | uv_divisor = 1024.0 260 | offset += self.struct_offset 261 | if self.data_type == self.DataType.FLOAT2: 262 | return tuple(struct.unpack_from("ff", buf, offset)) 263 | if self.data_type == self.DataType.FLOAT3: 264 | return tuple(struct.unpack_from("fff", buf, offset)) 265 | if self.data_type == self.DataType.FLOAT4: 266 | return tuple(struct.unpack_from("ffff", buf, offset)) 267 | if self.data_type == self.DataType.UV: 268 | uv = struct.unpack_from("hh", buf, offset) 269 | return tuple(component / uv_divisor for component in uv) 270 | if self.data_type == self.DataType.BONE_INDICES: 271 | return tuple(struct.unpack_from("BBBB", buf, offset)) 272 | if self.data_type == self.DataType.BONE_WEIGHTS: 273 | weights = struct.unpack_from("HHHH", buf, offset) 274 | return tuple(weight / 32767.0 for weight in weights) 275 | raise Exception( 276 | f"vertex data type not yet implemented: {self.data_type.name} for " 277 | + f"attribute {self.attribute_type.name} " + 278 | str(list(hex(c) for c in buf[offset:offset + self.size()]))) 279 | 280 | 281 | class Texture: 282 | def __init__(self, path, type_name, scale, unk10, unk11, unk14, unk18, 283 | unk1C): 284 | self.path = path 285 | self.type_name = type_name 286 | self.scale = scale 287 | self.unk10 = unk10 288 | self.unk11 = unk11 289 | self.unk14 = unk14 290 | self.unk18 = unk18 291 | self.unk1C = unk1C 292 | 293 | 294 | class InflatedMesh: 295 | class Vertices: 296 | def __init__(self): 297 | self.positions = [] 298 | self.bone_weights = [] 299 | self.bone_indices = [] 300 | self.uv = [] 301 | 302 | def __init__(self): 303 | self.faces = [] 304 | self.vertices = self.Vertices() 305 | 306 | 307 | class Flver: 308 | def __init__(self, header, dummies, materials, bones, meshes, 309 | index_buffers, vertex_buffers, vertex_buffer_structs, 310 | textures): 311 | self.header = header 312 | self.dummies = dummies 313 | self.materials = materials 314 | self.bones = bones 315 | self.meshes = meshes 316 | self.index_buffers = index_buffers 317 | self.vertex_buffers = vertex_buffers 318 | self.vertex_buffer_structs = vertex_buffer_structs 319 | self.textures = textures 320 | 321 | # For every mesh, combine all index buffers into a single index buffer and 322 | # all vertex buffer attributes into individual corresponding attribute 323 | # lists. 324 | def inflate(self): 325 | return [self._inflate_mesh(mesh) for mesh in self.meshes] 326 | 327 | def _inflate_mesh(self, mesh): 328 | result = InflatedMesh() 329 | 330 | # Triangulate faces 331 | index_buffers = [ 332 | self.index_buffers[index] for index in mesh.index_buffer_indices 333 | if len(self.index_buffers[index].detail_flags) == 0 334 | ] 335 | if len(index_buffers) == 0: 336 | return None 337 | assert len(index_buffers) == 1 338 | if index_buffers[ 339 | 0].backface_visibility == IndexBuffer.BackfaceVisibility.SHOW: 340 | print("visible backfaces: " + 341 | self.materials[mesh.material_index].name) 342 | index_buffers[0]._inflate(result.faces) 343 | 344 | # Parse vertex buffer attributes 345 | vertex_buffers = [ 346 | self.vertex_buffers[index] for index in mesh.vertex_buffer_indices 347 | ] 348 | assert len(vertex_buffers) > 0 349 | for vertex_buffer in vertex_buffers: 350 | struct = self.vertex_buffer_structs[vertex_buffer.struct_index] 351 | vertex_buffer._inflate(vertices=result.vertices, 352 | struct=struct, 353 | version=self.header.version) 354 | 355 | return result 356 | -------------------------------------------------------------------------------- /importer.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import bmesh 4 | import bpy 5 | from bpy.app.translations import pgettext 6 | from mathutils import Matrix, Vector 7 | from mathutils.noise import random 8 | 9 | from . import reader 10 | 11 | 12 | def create_armature(context, armature_name, collection, flver_data, 13 | connect_bones, axes): 14 | armature = bpy.data.objects.new(armature_name, 15 | bpy.data.armatures.new(armature_name)) 16 | collection.objects.link(armature) 17 | armature.show_in_front = True 18 | if connect_bones: 19 | armature.data.display_type = "STICK" 20 | else: 21 | armature.data.display_type = "WIRE" 22 | bpy.context.view_layer.objects.active = armature 23 | 24 | bpy.ops.object.mode_set(mode="EDIT", toggle=False) 25 | 26 | root_bones = [] 27 | for flver_bone in flver_data.bones: 28 | bone = armature.data.edit_bones.new(flver_bone.name) 29 | if flver_bone.parent_index < 0: 30 | root_bones.append(bone) 31 | 32 | def transform_bone_and_siblings(bone_index, parent_matrix): 33 | while bone_index != -1: 34 | flver_bone = flver_data.bones[bone_index] 35 | bone = armature.data.edit_bones[bone_index] 36 | if flver_bone.parent_index >= 0: 37 | bone.parent = armature.data.edit_bones[flver_bone.parent_index] 38 | 39 | translation_vector = Vector( 40 | (flver_bone.translation[0], flver_bone.translation[1], 41 | flver_bone.translation[2])) 42 | rotation_matrix = ( 43 | Matrix.Rotation(flver_bone.rotation[1], 4, 'Y') 44 | @ Matrix.Rotation(flver_bone.rotation[2], 4, 'Z') 45 | @ Matrix.Rotation(flver_bone.rotation[0], 4, 'X')) 46 | 47 | head = parent_matrix @ translation_vector 48 | tail = head + rotation_matrix @ Vector((0, 0.05, 0)) 49 | 50 | bone.head = (head[axes[0]], head[axes[1]], head[axes[2]]) 51 | bone.tail = (tail[axes[0]], tail[axes[1]], tail[axes[2]]) 52 | 53 | # Transform children and advance to next sibling 54 | transform_bone_and_siblings( 55 | flver_bone.child_index, parent_matrix 56 | @ Matrix.Translation(translation_vector) @ rotation_matrix) 57 | bone_index = flver_bone.next_sibling_index 58 | 59 | transform_bone_and_siblings(0, Matrix()) 60 | 61 | def connect_bone(bone): 62 | children = bone.children 63 | if len(children) == 0: 64 | parent = bone.parent 65 | if parent is not None: 66 | direction = parent.tail - parent.head 67 | direction.normalize() 68 | length = (bone.tail - bone.head).magnitude 69 | bone.tail = bone.head + direction * length 70 | return 71 | if len(children) > 1: 72 | for child in children: 73 | connect_bone(child) 74 | return 75 | child = children[0] 76 | bone.tail = child.head 77 | child.use_connect = True 78 | connect_bone(child) 79 | 80 | if connect_bones: 81 | for bone in root_bones: 82 | connect_bone(bone) 83 | 84 | bpy.ops.object.mode_set(mode="OBJECT") 85 | return armature 86 | 87 | 88 | def run(context, path, transpose_y_and_z, import_skeleton, connect_bones): 89 | if bpy.context.mode != "OBJECT": 90 | bpy.ops.object.mode_set(mode="OBJECT") 91 | 92 | flver_data = reader.read_flver(path) 93 | inflated_meshes = flver_data.inflate() 94 | 95 | collection = \ 96 | bpy.context.view_layer.active_layer_collection.collection 97 | 98 | model_name = os.path.splitext(os.path.basename(path))[0] 99 | 100 | if transpose_y_and_z: 101 | axes = (0, 2, 1) 102 | else: 103 | axes = (0, 1, 2) 104 | 105 | # Create armature 106 | if import_skeleton: 107 | armature = create_armature(context=context, 108 | armature_name=model_name, 109 | collection=collection, 110 | flver_data=flver_data, 111 | connect_bones=connect_bones, 112 | axes=axes) 113 | 114 | # Create materials 115 | materials = [] 116 | for flver_material in flver_data.materials: 117 | material = bpy.data.materials.new(flver_material.name) 118 | material.diffuse_color = (random(), random(), random(), 1.0) 119 | materials.append(material) 120 | 121 | for index, (flver_mesh, inflated_mesh) in enumerate( 122 | zip(flver_data.meshes, inflated_meshes)): 123 | if inflated_mesh is None: 124 | continue 125 | 126 | # Construct mesh 127 | material_name = flver_data.materials[flver_mesh.material_index].name 128 | verts = [ 129 | Vector((v[axes[0]], v[axes[1]], v[axes[2]])) 130 | for v in inflated_mesh.vertices.positions 131 | ] 132 | mesh_name = f"{model_name}.{index}.{material_name}" 133 | mesh = bpy.data.meshes.new(name=mesh_name) 134 | mesh.from_pydata(verts, [], inflated_mesh.faces) 135 | 136 | # Create object 137 | obj = bpy.data.objects.new(mesh_name, mesh) 138 | collection.objects.link(obj) 139 | 140 | # Assign armature 141 | if import_skeleton: 142 | obj.modifiers.new(type="ARMATURE", 143 | name=pgettext("Armature")).object = armature 144 | obj.parent = armature 145 | 146 | # Assign material 147 | obj.data.materials.append(materials[flver_mesh.material_index]) 148 | 149 | # Create vertex groups for bones 150 | for bone_index in flver_mesh.bone_indices: 151 | obj.vertex_groups.new(name=flver_data.bones[bone_index].name) 152 | 153 | bm = bmesh.new() 154 | bm.from_mesh(mesh) 155 | 156 | uv_layer = bm.loops.layers.uv.new() 157 | for face in bm.faces: 158 | for loop in face.loops: 159 | u, v = inflated_mesh.vertices.uv[loop.vert.index] 160 | loop[uv_layer].uv = (u, 1.0 - v) 161 | if import_skeleton: 162 | weight_layer = bm.verts.layers.deform.new() 163 | for vert in bm.verts: 164 | weights = inflated_mesh.vertices.bone_weights[vert.index] 165 | indices = inflated_mesh.vertices.bone_indices[vert.index] 166 | for index, weight in zip(indices, weights): 167 | if weight == 0.0: 168 | continue 169 | vert[weight_layer][index] = weight 170 | 171 | bm.to_mesh(mesh) 172 | bm.free() 173 | mesh.update() 174 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizagamedev/blender-flver/25cc152de19acb4028035d3ed389706df25e094a/preview.png -------------------------------------------------------------------------------- /reader.py: -------------------------------------------------------------------------------- 1 | import struct 2 | from collections import deque 3 | from . import flver 4 | 5 | 6 | class StructReader: 7 | def __init__(self, fp): 8 | self.fp = fp 9 | self.endianness = None 10 | self.text_encoding = None 11 | 12 | def tell(self): 13 | return self.fp.tell() 14 | 15 | def seek(self, offset): 16 | self.fp.seek(offset, 0) 17 | 18 | def read(self, count, offset=None): 19 | if offset is not None: 20 | position = self.fp.tell() 21 | self.fp.seek(offset, 0) 22 | result = self.fp.read(count) 23 | if offset is not None: 24 | self.fp.seek(position, 0) 25 | return result 26 | 27 | def read_struct(self, fmt, offset=None): 28 | if offset is not None: 29 | position = self.fp.tell() 30 | self.fp.seek(offset, 0) 31 | 32 | # Prefix endianness marker for struct 33 | prefix = "" 34 | if self.endianness == flver.Endianness.BIG: 35 | prefix = ">" 36 | elif self.endianness == flver.Endianness.LITTLE: 37 | prefix = "<" 38 | 39 | result = struct.unpack(prefix + fmt, 40 | self.fp.read(struct.calcsize(fmt))) 41 | if offset is not None: 42 | self.fp.seek(position, 0) 43 | return result 44 | 45 | def read_string(self, offset=None): 46 | if self.text_encoding == flver.TextEncoding.UTF_16: 47 | terminator = b"\0\0" 48 | encoding = "utf_16_le" 49 | elif self.text_encoding == flver.TextEncoding.SHIFT_JIS: 50 | terminator = b"\0" 51 | encoding = "shift_jis" 52 | 53 | if offset is not None: 54 | position = self.fp.tell() 55 | self.fp.seek(offset, 0) 56 | raw_string = bytearray() 57 | while True: 58 | char = self.fp.read(len(terminator)) 59 | if char == terminator: 60 | break 61 | raw_string.extend(char) 62 | result = raw_string.decode(encoding=encoding) 63 | if offset is not None: 64 | self.fp.seek(position, 0) 65 | return result 66 | 67 | 68 | def read_dummy(reader, header): 69 | data = deque(reader.read_struct("fffBBBBfffHhfffh??IIII")) 70 | 71 | position = (data.popleft(), data.popleft(), data.popleft()) 72 | 73 | # Upstream is uncertain about RGB ordering 74 | if header.version == 0x20010: 75 | b = data.popleft() # B 76 | g = data.popleft() # B 77 | r = data.popleft() # B 78 | a = data.popleft() # B 79 | else: 80 | a = data.popleft() # B 81 | r = data.popleft() # B 82 | g = data.popleft() # B 83 | b = data.popleft() # B 84 | color = (r, g, b, a) 85 | 86 | forward = (data.popleft(), data.popleft(), data.popleft()) # fff 87 | reference_id = data.popleft() # H 88 | parent_bone_index = data.popleft() # h 89 | upward = (data.popleft(), data.popleft(), data.popleft()) # fff 90 | attach_bone_index = data.popleft() # h 91 | flag1 = data.popleft() # ? 92 | use_upward_vector = data.popleft() # ? 93 | unk30 = data.popleft() # I 94 | unk34 = data.popleft() # I 95 | assert data.popleft() == 0 # I 96 | assert data.popleft() == 0 # I 97 | 98 | return flver.Dummy( 99 | position=position, 100 | color=color, 101 | forward=forward, 102 | reference_id=reference_id, 103 | parent_bone_index=parent_bone_index, 104 | upward=upward, 105 | attach_bone_index=attach_bone_index, 106 | flag1=flag1, 107 | use_upward_vector=use_upward_vector, 108 | unk30=unk30, 109 | unk34=unk34, 110 | ) 111 | 112 | 113 | def read_material(reader): 114 | data = deque(reader.read_struct("IIIIIIII")) 115 | 116 | name = reader.read_string(data.popleft()) # I 117 | mtd_path = reader.read_string(data.popleft()) # I 118 | texture_count = data.popleft() # I 119 | texture_index = data.popleft() # I 120 | flags = data.popleft() # I 121 | data.popleft() # TODO: gx offset (I) 122 | unk18 = data.popleft() # I 123 | assert data.popleft() == 0 # I 124 | 125 | return flver.Material( 126 | name=name, 127 | mtd_path=mtd_path, 128 | texture_count=texture_count, 129 | texture_index=texture_index, 130 | flags=flags, 131 | unk18=unk18, 132 | ) 133 | 134 | 135 | def read_bone(reader): 136 | data = deque(reader.read_struct("fffIfffhhfffhhfffIfff")) 137 | 138 | translation = (data.popleft(), data.popleft(), data.popleft()) # fff 139 | name = reader.read_string(data.popleft()) # I 140 | rotation = (data.popleft(), data.popleft(), data.popleft()) # fff 141 | parent_index = data.popleft() # h 142 | child_index = data.popleft() # h 143 | scale = (data.popleft(), data.popleft(), data.popleft()) # fff 144 | next_sibling_index = data.popleft() # h 145 | previous_sibling_index = data.popleft() # h 146 | bounding_box_min = (data.popleft(), data.popleft(), data.popleft()) # fff 147 | unk3C = data.popleft() # I 148 | bounding_box_max = (data.popleft(), data.popleft(), data.popleft()) # fff 149 | assert reader.read(0x34) == b"\0" * 0x34 150 | 151 | return flver.Bone( 152 | translation=translation, 153 | name=name, 154 | rotation=rotation, 155 | parent_index=parent_index, 156 | child_index=child_index, 157 | scale=scale, 158 | next_sibling_index=next_sibling_index, 159 | previous_sibling_index=previous_sibling_index, 160 | bounding_box_min=bounding_box_min, 161 | unk3C=unk3C, 162 | bounding_box_max=bounding_box_max, 163 | ) 164 | 165 | 166 | def read_mesh(reader): 167 | data = deque(reader.read_struct("BBBBIIIIIIIIIII")) 168 | 169 | dynamic_mode = flver.Mesh.DynamicMode(data.popleft()) # B 170 | assert data.popleft() == 0 # B 171 | assert data.popleft() == 0 # B 172 | assert data.popleft() == 0 # B 173 | material_index = data.popleft() # I 174 | assert data.popleft() == 0 # I 175 | assert data.popleft() == 0 # I 176 | default_bone_index = data.popleft() # I 177 | bone_count = data.popleft() # I 178 | data.popleft() # TODO: bounding box offset (I) 179 | bone_offset = data.popleft() # I 180 | index_buffer_count = data.popleft() # I 181 | index_buffer_offset = data.popleft() # I 182 | vertex_buffer_count = data.popleft() # I 183 | assert vertex_buffer_count in {1, 2, 3} 184 | vertex_buffer_offset = data.popleft() # I 185 | 186 | bone_indices = reader.read_struct("I" * bone_count, bone_offset) 187 | index_buffer_indices = reader.read_struct("I" * index_buffer_count, 188 | index_buffer_offset) 189 | vertex_buffer_indices = reader.read_struct("I" * vertex_buffer_count, 190 | vertex_buffer_offset) 191 | 192 | return flver.Mesh( 193 | dynamic_mode=dynamic_mode, 194 | material_index=material_index, 195 | default_bone_index=default_bone_index, 196 | bone_indices=bone_indices, 197 | index_buffer_indices=index_buffer_indices, 198 | vertex_buffer_indices=vertex_buffer_indices, 199 | ) 200 | 201 | 202 | def read_index_buffer(reader, header, data_offset): 203 | data = deque(reader.read_struct("IBBHII")) 204 | 205 | detail_flags = set() 206 | detail_binary_flags = data.popleft() # I 207 | for flag in flver.IndexBuffer.DetailFlags: 208 | if (detail_binary_flags & flag.value) != 0: 209 | detail_flags.add(flag) 210 | 211 | primitive_mode = flver.IndexBuffer.PrimitiveMode(data.popleft()) # B 212 | backface_visibility = flver.IndexBuffer.BackfaceVisibility( 213 | data.popleft()) # B 214 | unk06 = data.popleft() 215 | index_count = data.popleft() # I 216 | indices_offset = data.popleft() # I 217 | 218 | index_size = 0 219 | if header.version > 0x20005: 220 | additional_data = deque(reader.read_struct("IIII")) 221 | assert additional_data.popleft() >= 0 # indices length (I) 222 | assert additional_data.popleft() == 0 # I 223 | index_size = additional_data.popleft() # I 224 | assert index_size in {0, 16, 32} 225 | assert additional_data.popleft() == 0 # I 226 | if index_size == 0: 227 | index_size = header.default_vertex_index_size 228 | 229 | if index_size == 16: 230 | indices = reader.read_struct("H" * index_count, 231 | data_offset + indices_offset) 232 | elif index_size == 32: 233 | indices = reader.read_struct("I" * index_count, 234 | data_offset + indices_offset) 235 | 236 | return flver.IndexBuffer( 237 | detail_flags=detail_flags, 238 | primitive_mode=primitive_mode, 239 | backface_visibility=backface_visibility, 240 | unk06=unk06, 241 | indices=indices, 242 | ) 243 | 244 | 245 | def read_vertex_buffer(reader, data_offset): 246 | data = deque(reader.read_struct("IIIIIIII")) 247 | 248 | buffer_index = data.popleft() # I 249 | struct_index = data.popleft() # I 250 | struct_size = data.popleft() # I 251 | vertex_count = data.popleft() # I 252 | assert data.popleft() == 0 # I 253 | assert data.popleft() == 0 # I 254 | buffer_length = data.popleft() # I 255 | buffer_offset = data.popleft() # I 256 | 257 | # Read buffer data 258 | buffer_data = reader.read(buffer_length, data_offset + buffer_offset) 259 | 260 | return flver.VertexBuffer( 261 | buffer_index=buffer_index, 262 | struct_index=struct_index, 263 | struct_size=struct_size, 264 | vertex_count=vertex_count, 265 | buffer_data=buffer_data, 266 | ) 267 | 268 | 269 | def read_vertex_buffer_struct_member(reader, struct_offset): 270 | data = deque(reader.read_struct("IIIII")) 271 | 272 | unk00 = data.popleft() # I 273 | assert data.popleft() == struct_offset 274 | data_type = flver.VertexBufferStructMember.DataType(data.popleft()) # I 275 | attribute_type = flver.VertexBufferStructMember.AttributeType( 276 | data.popleft()) # I 277 | index = data.popleft() # I 278 | 279 | return flver.VertexBufferStructMember( 280 | unk00=unk00, 281 | struct_offset=struct_offset, 282 | data_type=data_type, 283 | attribute_type=attribute_type, 284 | index=index, 285 | ) 286 | 287 | 288 | def read_vertex_buffer_structs(reader): 289 | data = deque(reader.read_struct("IIII")) 290 | 291 | member_count = data.popleft() # I 292 | assert data.popleft() == 0 # I 293 | assert data.popleft() == 0 # I 294 | member_offset = data.popleft() # I 295 | 296 | position = reader.tell() 297 | reader.seek(member_offset) 298 | 299 | struct_offset = 0 300 | result = [] 301 | for _ in range(member_count): 302 | member = read_vertex_buffer_struct_member(reader, struct_offset) 303 | struct_offset += member.size() 304 | result.append(member) 305 | 306 | reader.seek(position) 307 | return result 308 | 309 | 310 | def read_texture(reader): 311 | data = deque(reader.read_struct("IIffB?BBfff")) 312 | 313 | path = reader.read_string(data.popleft()) # I 314 | type_name = reader.read_string(data.popleft()) # I 315 | scale = (data.popleft(), data.popleft()) # ff 316 | unk10 = data.popleft() # B 317 | assert unk10 in {0, 1, 2} 318 | unk11 = data.popleft() # ? 319 | assert data.popleft() == 0 # B 320 | assert data.popleft() == 0 # B 321 | unk14 = data.popleft() # f 322 | unk18 = data.popleft() # f 323 | unk1C = data.popleft() # f 324 | 325 | return flver.Texture( 326 | path=path, 327 | type_name=type_name, 328 | scale=scale, 329 | unk10=unk10, 330 | unk11=unk11, 331 | unk14=unk14, 332 | unk18=unk18, 333 | unk1C=unk1C, 334 | ) 335 | 336 | 337 | def read_flver(file_name): 338 | with open(file_name, 'rb') as fp: 339 | reader = StructReader(fp) 340 | 341 | # Read until endianness 342 | data = deque(reader.read_struct("6s2s")) 343 | assert data.popleft() == b"FLVER\0" 344 | endianness = flver.Endianness(data.popleft()) 345 | reader.endianness = endianness 346 | 347 | data = deque( 348 | reader.read_struct("IIIIIIIIffffffIIBB?BIIIIBBBBIIIIIIII")) 349 | # Gundam Unicorn: 0x20005, 0x2000E 350 | # DS1: 2000C, 2000D 351 | # DS2 NT: 2000F, 20010 352 | # DS2: 20010, 20009 (armor 9320) 353 | # SFS: 20010 354 | # BB: 20013, 20014 355 | # DS3: 20013, 20014 356 | # SDT: 2001A, 20016 (test chr) 357 | version = data.popleft() # I 358 | assert version in { 359 | 0x20005, 0x20009, 0x2000C, 0x2000D, 0x2000E, 0x2000F, 0x20010, 360 | 0x20013, 0x20014, 0x20016, 0x2001A 361 | } 362 | 363 | data_offset = data.popleft() # I 364 | assert data.popleft() >= 0 # data length (I) 365 | dummy_count = data.popleft() # I 366 | material_count = data.popleft() # I 367 | bone_count = data.popleft() # I 368 | mesh_count = data.popleft() # I 369 | vertex_buffer_count = data.popleft() # I 370 | 371 | # fff 372 | bounding_box_min = (data.popleft(), data.popleft(), data.popleft()) 373 | # fff 374 | bounding_box_max = (data.popleft(), data.popleft(), data.popleft()) 375 | 376 | assert data.popleft() >= 0 # Face count of main mesh (I) 377 | assert data.popleft() >= 0 # Total face count of all meshes (I) 378 | 379 | default_vertex_index_size = data.popleft() # B 380 | assert default_vertex_index_size in {0, 16, 32} 381 | text_encoding = flver.TextEncoding(data.popleft()) # B 382 | reader.text_encoding = text_encoding 383 | unk4A = data.popleft() # ? 384 | assert data.popleft() == 0 # B 385 | 386 | unk4C = data.popleft() # I 387 | index_buffer_count = data.popleft() # I 388 | vertex_buffer_struct_count = data.popleft() # I 389 | texture_count = data.popleft() # I 390 | 391 | unk5C = data.popleft() # B 392 | unk5D = data.popleft() # B 393 | assert data.popleft() == 0 # B 394 | assert data.popleft() == 0 # B 395 | 396 | assert data.popleft() == 0 # I 397 | assert data.popleft() == 0 # I 398 | unk68 = data.popleft() # I 399 | assert unk68 in {0, 1, 2, 3, 4} 400 | assert data.popleft() == 0 401 | assert data.popleft() == 0 402 | assert data.popleft() == 0 403 | assert data.popleft() == 0 404 | assert data.popleft() == 0 405 | 406 | header = flver.Header( 407 | endianness=endianness, 408 | version=version, 409 | bounding_box_min=bounding_box_min, 410 | bounding_box_max=bounding_box_max, 411 | default_vertex_index_size=default_vertex_index_size, 412 | text_encoding=text_encoding, 413 | unk4A=unk4A, 414 | unk4C=unk4C, 415 | unk5C=unk5C, 416 | unk5D=unk5D, 417 | unk68=unk68, 418 | ) 419 | 420 | dummies = [] 421 | for _ in range(dummy_count): 422 | dummies.append(read_dummy(reader, header)) 423 | materials = [] 424 | for _ in range(material_count): 425 | materials.append(read_material(reader)) 426 | bones = [] 427 | for _ in range(bone_count): 428 | bones.append(read_bone(reader)) 429 | meshes = [] 430 | for _ in range(mesh_count): 431 | meshes.append(read_mesh(reader)) 432 | index_buffers = [] 433 | for _ in range(index_buffer_count): 434 | index_buffers.append(read_index_buffer(reader, header, 435 | data_offset)) 436 | vertex_buffers = [] 437 | for _ in range(vertex_buffer_count): 438 | vertex_buffers.append(read_vertex_buffer(reader, data_offset)) 439 | vertex_buffer_structs = [] 440 | for _ in range(vertex_buffer_struct_count): 441 | vertex_buffer_structs.append(read_vertex_buffer_structs(reader)) 442 | textures = [] 443 | for _ in range(texture_count): 444 | textures.append(read_texture(reader)) 445 | # Ignore unknown Sekiro struct for now 446 | 447 | return flver.Flver( 448 | header=header, 449 | dummies=dummies, 450 | materials=materials, 451 | bones=bones, 452 | meshes=meshes, 453 | index_buffers=index_buffers, 454 | vertex_buffers=vertex_buffers, 455 | vertex_buffer_structs=vertex_buffer_structs, 456 | textures=textures, 457 | ) 458 | 459 | 460 | if __name__ == "__main__": 461 | model = read_flver("gwyndolin_remaster.flver") 462 | meshes = model.inflate_meshes() 463 | print(meshes[4].vertex_attributes[flver.AttributeType.BONE_WEIGHTS]) 464 | --------------------------------------------------------------------------------