10 Commits

Author SHA1 Message Date
SleepKiller
dac3ade7a4 initial vertex weights implementation 2020-10-16 13:40:27 +13:00
SleepKiller
b56fa79a19 update version number 2020-02-04 14:44:22 +13:00
SleepKiller
47fa855b78 fixed colour packing order 2020-02-04 14:28:34 +13:00
SleepKiller
2010dd21b2 correct -keepmaterial documentation
Thanks to Fox!
2020-01-31 04:32:11 +13:00
SleepKiller
6e322d78bf add 'Export Target' property 2020-01-06 17:08:36 +13:00
SleepKiller
8f24e4914e Update reference_manual.md 2019-11-24 22:23:46 +13:00
SleepKiller
23e479ae51 fix flat normals not being handled 2019-11-24 16:03:13 +13:00
SleepKiller
bc388640ed Update reference_manual.md 2019-11-22 13:52:04 +13:00
SleepKiller
0db2f30ef3 Update reference_manual.md 2019-11-22 13:49:39 +13:00
SleepKiller
e2c0c42d88 update readme 2019-11-22 02:42:49 +13:00
9 changed files with 210 additions and 47 deletions

View File

@@ -1,20 +1,32 @@
# SWBF-msh-Blender-Export
WIP .msh (SWBF toolchain version) exporter for Blender 2.8
Currently capable of exporting the active scene without collision primitives or skinning information.
Currently capable of exporting the active scene without skinning information.
### Installing
You install it like any other Blender addon, if you already know how to do that then great! Grab the [latest release](https://github.com/SleepKiller/SWBF-msh-Blender-Export/releases/latest) (or if you're the adventerous type [clone from source](https://github.com/SleepKiller/SWBF-msh-Blender-Export/archive/master.zip)) and you'll be good to go.
> TODO: Install instructions.
However if you don't know how to install then read on!
These instructions are going to be for Windows. If you're on a different platform I'm sure some quick web searching can help provide you with answers.
First download and extract the addon [latest release](https://github.com/SleepKiller/SWBF-msh-Blender-Export/releases/latest).
Then open up Explorer and paste `%USERPROFILE%\AppData\Roaming\Blender Foundation\Blender\2.80\` into it's address bar. Then go into the `scripts` folder in that directory and copy the `addons` folder from the extracted addon into the scripts folder.
Next open up Blender, go into Edit > Preferences > Addons. Select "Community" tab and then search for ".msh". "Import-Export: SWBF .msh export" should come up, check the box next to it. The preferences window should look like this once you're done.
![Installed addon.](docs/images/blender_addon_installed.png)
If you've done that then the addon is installed and you should now find "SWBF msh" listed under Blender's export options.
### Reference Manual
Included in the repository is a [Reference Manual](https://github.com/SleepKiller/SWBF-msh-Blender-Export/blob/master/docs/reference_manual.md#reference-manual) of sorts. There is no need to read through it before using the addon but anytime you have a question about how something works or why an export failed it should hopefully have the answers.
### Work to be done
- [x] Raise an error when a .msh segment has more than 32767 vertices.
- [x] Convert from Blender's coordinate space to .msh cooordinate space.
- [x] Add support for exporting materials. Blender's materials are all based around it's own renderers, so possibly going to need custom UI and properties in order to provide something useful for .msh files.
- [x] Add support for collision primitives. Blender doesn't seam to support having basic boxes, cylinders or spheres so it's likely some wacky rules and conventions will need to be used by the modeler. "Add a 1m mesh primitive, have "sphere/box/cylinder" in the name and control the size with the object's scale." Less intuitive than I'd like but it might be the best course of action.
- [ ] Investigate and add support for exporting bones and vertex weights.
- [ ] Investigate and add support for exporting animations.
- [ ] Investigate if anything special needs to be done for lod/lowres exporting.
- [ ] Investigate and add support for editing and exporting SWBF2 cloth.
- [ ] Implement .msh importing. Currently you can use the 1.2 release of [swbf-unmunge](releases/tag/v1.2.0) to save out munged models to glTF 2.0 files if you need to open a model in Blender.
### What from [glTF-Blender-IO](https://github.com/KhronosGroup/glTF-Blender-IO) was used?

View File

@@ -1,7 +1,7 @@
bl_info = {
'name': 'SWBF .msh export',
'author': 'SleepKiller',
"version": (0, 1, 0),
"version": (0, 2, 1),
'blender': (2, 80, 0),
'location': 'File > Import-Export',
'description': 'Export as SWBF .msh file',
@@ -82,6 +82,15 @@ class ExportMSH(Operator, ExportHelper):
default=False
)
export_target: EnumProperty(name="Export Target",
description="What to export.",
items=(
('SCENE', "Scene", "Export the current active scene."),
('SELECTED', "Selected", "Export the currently selected objects and their parents."),
('SELECTED_WITH_CHILDREN', "Selected with Children", "Export the currently selected objects with their children and parents.")
),
default='SCENE')
apply_modifiers: BoolProperty(
name="Apply Modifiers",
description="Whether to apply Modifiers during export or not.",
@@ -94,7 +103,8 @@ class ExportMSH(Operator, ExportHelper):
output_file=output_file,
scene=create_scene(
generate_triangle_strips=self.generate_triangle_strips,
apply_modifiers=self.apply_modifiers))
apply_modifiers=self.apply_modifiers,
export_target=self.export_target))
return {'FINISHED'}

View File

@@ -27,17 +27,24 @@ class ModelTransform:
translation: Vector = field(default_factory=Vector)
rotation: Quaternion = field(default_factory=Quaternion)
@dataclass
class VertexWeight:
""" Class representing a vertex weight in a .msh file. """
weight: float = 1.0
bone: int = 0
@dataclass
class GeometrySegment:
""" Class representing a 'SEGM' section in a .msh file. """
material_name: str = ""
material_name: str = field(default_factory=str)
positions: List[Vector] = field(default_factory=list)
normals: List[Vector] = field(default_factory=list)
colors: List[List[float]] = None
texcoords: List[Vector] = field(default_factory=list)
# TODO: Skin support.
weights: List[List[VertexWeight]] = None
polygons: List[List[int]] = field(default_factory=list)
triangles: List[List[int]] = field(default_factory=list)
@@ -63,5 +70,7 @@ class Model:
transform: ModelTransform = field(default_factory=ModelTransform)
bone_map: List[str] = None
geometry: List[GeometrySegment] = None
collisionprimitive: CollisionPrimitive = None

View File

@@ -3,6 +3,7 @@
import bpy
import math
from enum import Enum
from typing import List, Set, Dict, Tuple
from itertools import zip_longest
from .msh_model import *
@@ -13,7 +14,7 @@ SKIPPED_OBJECT_TYPES = {"LATTICE", "CAMERA", "LIGHT", "SPEAKER", "LIGHT_PROBE"}
MESH_OBJECT_TYPES = {"MESH", "CURVE", "SURFACE", "META", "FONT", "GPENCIL"}
MAX_MSH_VERTEX_COUNT = 32767
def gather_models(apply_modifiers: bool) -> List[Model]:
def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]:
""" Gathers the Blender objects from the current scene and returns them as a list of
Model objects. """
@@ -22,7 +23,7 @@ def gather_models(apply_modifiers: bool) -> List[Model]:
models_list: List[Model] = []
for uneval_obj in bpy.context.scene.objects:
for uneval_obj in select_objects(export_target):
if uneval_obj.type in SKIPPED_OBJECT_TYPES and uneval_obj.name not in parents:
continue
@@ -33,6 +34,9 @@ def gather_models(apply_modifiers: bool) -> List[Model]:
check_for_bad_lod_suffix(obj)
if obj.type == "ARMATURE":
models_list += expand_armature(obj)
local_translation, local_rotation, _ = obj.matrix_local.decompose()
model = Model()
@@ -47,7 +51,7 @@ def gather_models(apply_modifiers: bool) -> List[Model]:
if obj.type in MESH_OBJECT_TYPES:
mesh = obj.to_mesh()
model.geometry = create_mesh_geometry(mesh)
model.geometry = create_mesh_geometry(mesh, obj.vertex_groups)
obj.to_mesh_clear()
_, _, world_scale = obj.matrix_world.decompose()
@@ -63,6 +67,9 @@ def gather_models(apply_modifiers: bool) -> List[Model]:
if get_is_collision_primitive(obj):
model.collisionprimitive = get_collision_primitive(obj)
if obj.vertex_groups:
model.bone_map = [group.name for group in obj.vertex_groups]
models_list.append(model)
return models_list
@@ -79,7 +86,7 @@ def create_parents_set() -> Set[str]:
return parents
def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]:
def create_mesh_geometry(mesh: bpy.types.Mesh, has_weights: bool) -> List[GeometrySegment]:
""" Creates a list of GeometrySegment objects from a Blender mesh.
Does NOT create triangle strips in the GeometrySegment however. """
@@ -92,7 +99,7 @@ def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]:
material_count = max(len(mesh.materials), 1)
segments: List[GeometrySegment] = [GeometrySegment() for i in range(material_count)]
vertex_cache: List[Dict[Tuple[float], int]] = [dict() for i in range(material_count)]
vertex_cache = [dict() for i in range(material_count)]
vertex_remap: List[Dict[Tuple[int, int], int]] = [dict() for i in range(material_count)]
polygons: List[Set[int]] = [set() for i in range(material_count)]
@@ -100,10 +107,14 @@ def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]:
for segment in segments:
segment.colors = []
if has_weights:
for segment in segments:
segment.weights = []
for segment, material in zip(segments, mesh.materials):
segment.material_name = material.name
def add_vertex(material_index: int, vertex_index: int, loop_index: int) -> int:
def add_vertex(material_index: int, vertex_index: int, loop_index: int, use_smooth_normal: bool, face_normal: Vector) -> int:
nonlocal segments, vertex_remap
vertex_cache_miss_index = -1
@@ -111,19 +122,24 @@ def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]:
cache = vertex_cache[material_index]
remap = vertex_remap[material_index]
def get_cache_vertex(vertex_index: int, loop_index: int):
vertex_normal: Vector
if use_smooth_normal or mesh.use_auto_smooth:
if mesh.has_custom_normals:
vertex_normal = mesh.loops[loop_index].normal
else:
vertex_normal = mesh.vertices[vertex_index].normal
else:
vertex_normal = face_normal
def get_cache_vertex():
yield mesh.vertices[vertex_index].co.x
yield mesh.vertices[vertex_index].co.y
yield mesh.vertices[vertex_index].co.z
if mesh.has_custom_normals:
yield mesh.loops[loop_index].normal.x
yield mesh.loops[loop_index].normal.y
yield mesh.loops[loop_index].normal.z
else:
yield mesh.vertices[vertex_index].normal.x
yield mesh.vertices[vertex_index].normal.y
yield mesh.vertices[vertex_index].normal.z
yield vertex_normal.x
yield vertex_normal.y
yield vertex_normal.z
if mesh.uv_layers.active is not None:
yield mesh.uv_layers.active.data[loop_index].uv.x
@@ -133,7 +149,12 @@ def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]:
for v in mesh.vertex_colors.active.data[loop_index].color:
yield v
vertex_cache_entry = tuple(get_cache_vertex(vertex_index, loop_index))
if segment.weights is not None:
for v in mesh.vertices[vertex_index].groups:
yield v.group
yield v.weight
vertex_cache_entry = tuple(get_cache_vertex())
cached_vertex_index = cache.get(vertex_cache_entry, vertex_cache_miss_index)
if cached_vertex_index != vertex_cache_miss_index:
@@ -146,11 +167,7 @@ def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]:
remap[(vertex_index, loop_index)] = new_index
segment.positions.append(convert_vector_space(mesh.vertices[vertex_index].co))
if mesh.has_custom_normals:
segment.normals.append(convert_vector_space(mesh.loops[loop_index].normal))
else:
segment.normals.append(convert_vector_space(mesh.vertices[vertex_index].normal))
segment.normals.append(convert_vector_space(vertex_normal))
if mesh.uv_layers.active is None:
segment.texcoords.append(Vector((0.0, 0.0)))
@@ -160,14 +177,19 @@ def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]:
if segment.colors is not None:
segment.colors.append(list(mesh.vertex_colors.active.data[loop_index].color))
if segment.weights is not None:
groups = mesh.vertices[vertex_index].groups
segment.weights.append([VertexWeight(v.weight, v.group) for v in groups])
return new_index
for tri in mesh.loop_triangles:
polygons[tri.material_index].add(tri.polygon_index)
segments[tri.material_index].triangles.append([
add_vertex(tri.material_index, tri.vertices[0], tri.loops[0]),
add_vertex(tri.material_index, tri.vertices[1], tri.loops[1]),
add_vertex(tri.material_index, tri.vertices[2], tri.loops[2])])
add_vertex(tri.material_index, tri.vertices[0], tri.loops[0], tri.use_smooth, tri.normal),
add_vertex(tri.material_index, tri.vertices[1], tri.loops[1], tri.use_smooth, tri.normal),
add_vertex(tri.material_index, tri.vertices[2], tri.loops[2], tri.use_smooth, tri.normal)])
for segment, remap, polys in zip(segments, vertex_remap, polygons):
for poly_index in polys:
@@ -179,10 +201,12 @@ def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]:
def get_model_type(obj: bpy.types.Object) -> ModelType:
""" Get the ModelType for a Blender object. """
# TODO: Skinning support, etc
if obj.type in MESH_OBJECT_TYPES:
return ModelType.STATIC
if obj.vertex_groups:
return ModelType.SKIN
else:
return ModelType.STATIC
return ModelType.NULL
@@ -275,6 +299,74 @@ def check_for_bad_lod_suffix(obj: bpy.types.Object):
if name.endswith(f"_lod{i}"):
raise RuntimeError(failure_message)
def select_objects(export_target: str) -> List[bpy.types.Object]:
""" Returns a list of objects to export. """
if export_target == "SCENE" or not export_target in {"SELECTED", "SELECTED_WITH_CHILDREN"}:
return list(bpy.context.scene.objects)
objects = list(bpy.context.selected_objects)
added = {obj.name for obj in objects}
if export_target == "SELECTED_WITH_CHILDREN":
children = []
def add_children(parent):
nonlocal children
nonlocal added
for obj in bpy.context.scene.objects:
if obj.parent == parent and obj.name not in added:
children.append(obj)
added.add(obj.name)
add_children(obj)
for obj in objects:
add_children(obj)
objects = objects + children
parents = []
for obj in objects:
parent = obj.parent
while parent is not None:
if parent.name not in added:
parents.append(parent)
added.add(parent.name)
parent = parent.parent
return objects + parents
def expand_armature(obj: bpy.types.Object) -> List[Model]:
bones: List[Model] = []
for bone in obj.data.bones:
model = Model()
transform = bone.matrix_local
if bone.parent:
transform = bone.parent.matrix_local.inverted() @ transform
model.parent = bone.parent.name
else:
model.parent = obj.name
local_translation, local_rotation, _ = transform.decompose()
model.model_type = ModelType.BONE
model.name = bone.name
model.transform.rotation = convert_rotation_space(local_rotation)
model.transform.translation = convert_vector_space(local_translation)
bones.append(model)
return bones
def convert_vector_space(vec: Vector) -> Vector:
return Vector((-vec.x, vec.z, vec.y))

View File

@@ -44,7 +44,7 @@ class Scene:
materials: Dict[str, Material] = field(default_factory=dict)
models: List[Model] = field(default_factory=list)
def create_scene(generate_triangle_strips: bool, apply_modifiers: bool) -> Scene:
def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_target: str) -> Scene:
""" Create a msh Scene from the active Blender scene. """
scene = Scene()
@@ -53,7 +53,7 @@ def create_scene(generate_triangle_strips: bool, apply_modifiers: bool) -> Scene
scene.materials = gather_materials()
scene.models = gather_models(apply_modifiers=apply_modifiers)
scene.models = gather_models(apply_modifiers=apply_modifiers, export_target=export_target)
scene.models = sort_by_parent(scene.models)
if generate_triangle_strips:

View File

@@ -17,6 +17,7 @@ def save_scene(output_file, scene: Scene):
with msh2.create_child("SINF") as sinf:
_write_sinf(sinf, scene)
model_index: Dict[str, int] = {model.name:i for i, model in enumerate(scene.models)}
material_index: Dict[str, int] = {}
with msh2.create_child("MATL") as matl:
@@ -24,7 +25,7 @@ def save_scene(output_file, scene: Scene):
for index, model in enumerate(scene.models):
with msh2.create_child("MODL") as modl:
_write_modl(modl, model, index, material_index)
_write_modl(modl, model, index, material_index, model_index)
with hedr.create_child("CL1L"):
pass
@@ -97,7 +98,7 @@ def _write_matd(matd: Writer, material_name: str, material: Material):
with matd.create_child("TX3D") as tx3d:
tx3d.write_string(material.texture3)
def _write_modl(modl: Writer, model: Model, index: int, material_index: Dict[str, int]):
def _write_modl(modl: Writer, model: Model, index: int, material_index: Dict[str, int], model_index: Dict[str, int]):
with modl.create_child("MTYP") as mtyp:
mtyp.write_u32(model.model_type.value)
@@ -124,6 +125,10 @@ def _write_modl(modl: Writer, model: Model, index: int, material_index: Dict[str
with geom.create_child("SEGM") as segm:
_write_segm(segm, segment, material_index)
if model.bone_map:
with geom.create_child("ENVL") as envl:
_write_envl(envl, model, model_index)
if model.collisionprimitive is not None:
with modl.create_child("SWCI") as swci:
swci.write_u32(model.collisionprimitive.shape.value)
@@ -147,6 +152,10 @@ def _write_segm(segm: Writer, segment: GeometrySegment, material_index: Dict[str
for position in segment.positions:
posl.write_f32(position.x, position.y, position.z)
if segment.weights:
with segm.create_child("WGHT") as wght:
_write_wght(wght, segment.weights)
with segm.create_child("NRML") as nrml:
nrml.write_u32(len(segment.normals))
@@ -189,3 +198,23 @@ def _write_segm(segm: Writer, segment: GeometrySegment, material_index: Dict[str
for index in islice(strip, 2, len(strip)):
strp.write_u16(index)
def _write_wght(wght: Writer, weights: List[List[VertexWeight]]):
wght.write_u32(len(weights))
for weight_list in weights:
weight_list += [VertexWeight(0.0, 0)] * 4
weight_list = sorted(weight_list, key=lambda w: w.weight, reverse=True)
weight_list = weight_list[:4]
total_weight = max(sum(map(lambda w: w.weight, weight_list)), 1e-5)
for weight in weight_list:
wght.write_i32(weight.bone)
wght.write_f32(weight.weight / total_weight)
def _write_envl(envl: Writer, model: Model, model_index: Dict[str, int]):
envl.write_u32(len(model.bone_map))
for bone_name in model.bone_map:
envl.write_u32(model_index[bone_name])

View File

@@ -23,8 +23,8 @@ def min_vec(l: Vector, r: Vector) -> Vector:
def pack_color(color) -> int:
packed = 0
packed |= (int(color[0] * 255.0 + 0.5) << 8)
packed |= (int(color[1] * 255.0 + 0.5) << 16)
packed |= (int(color[0] * 255.0 + 0.5) << 16)
packed |= (int(color[1] * 255.0 + 0.5) << 8)
packed |= (int(color[2] * 255.0 + 0.5))
packed |= (int(color[3] * 255.0 + 0.5) << 24)

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -45,6 +45,16 @@ In order to improve runtime performance and reduce munged model size you are **s
For very complex scenes with meshes that have tens of thousands (or more) faces Blender may freeze up for a couple minutes while triangle strips are generated. Either minimize it and do something else on your PC while you wait and it'll eventually finish.
#### Export Target
Controls what to export from Blender.
| | |
| ---------------------- | ---------------------------------------------------------------------- |
| Scene | Export the current active scene. |
| Selected | Export the currently selected objects and their parents. |
| Selected with Children | Export the currently selected objects with their children and parents. |
#### Apply Modifiers
Whether to apply [Modifiers](https://docs.blender.org/manual/en/latest/modeling/modifiers/index.html) during export or not.
@@ -373,7 +383,7 @@ Can optionally have a Detail Map. Tiling for the detail map can specified with D
This rendertype also enables per-pixel lighting.
#### Materials.Rendertype.Normalmapped Envmapped (SWBF2)
#### Materials.Rendertype.Normalmapped Tiled Envmapped (SWBF2)
Enables the use of a Normal Map with the material. Tiling for the normal map can be controlled with Normal Map Tiling U and Normal Map Tiling V
Uses an Environment Map to show reflections on the model. Useful for anything you want to look reflective or
@@ -459,10 +469,13 @@ Sets the strength of the material's diffuse at the bottom of the "blink".
Speed of blinking, higher is faster.
### Materials.Texture Maps
All textures should be the names or paths to .tga files. SWBF's toolchain does not support .tga files with RLE (Run Length Encoding) compression or paletted .tga files. They should either be 8-bit greyscale, 24-bit RGB or 32-bit RGBA.
#### Materials.Texture Maps.Diffuse Map
The basic diffuse map for the material. The alpha channel is either the Transparency Map, Glow Map or Gloss Map, depending on the selected rendertype and flags.
Textures are not automatically copied over to the .msh file's folder on export at this time.
#### Materials.Texture Maps.Detail Map
Detail maps allow you to add in 'detail' to the Diffuse Map at runtime.
@@ -523,9 +536,7 @@ Keep all named objects in the .msh file as hardpoints.
#### -keepmaterial
- Usage Example: `-keepmaterial override_texture`
By default material names are not saved in .model files. And meshes referencing differently named materials but with identical names may be merged together to boost performance.
By specifying "-keepmaterial" for a material modelmunge is instructed to keep the name of a material around and to not merge meshes using the material with others that aren't.
Prevents the named object being marged with other objects by modelmunge and gives the object's .model material the same name as the object.
This is used with the "OverrideTexture", "OverrideTexture2" and "WheelTexture" .odf properties.