Skip to content

Commit

Permalink
0.8.3 update:
Browse files Browse the repository at this point in the history
- Fixed bad normals on skinned BundledMesh geometry parts
- changed error to a warning when exceeding bone/geom part BF2 limit
- some exception handling improvements
- some optimizations to python based BSP export (should be about 3x faster)
  • Loading branch information
marekzajac97 committed Aug 28, 2024
1 parent 5f26334 commit 65ce1aa
Show file tree
Hide file tree
Showing 19 changed files with 219 additions and 106 deletions.
4 changes: 2 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ After installation, set up your `BF2 mod directory` (`Edit -> Preferences -> Add
- TIP: If you plan to modify the imported animation, you can delete redundant keyframes using [Decimate](https://docs.blender.org/manual/en/latest/editors/graph_editor/fcurves/editing.html#decimate) option
- When exporting, you can select/deselect bones for export in the export menu (matters for 3P animations, depending on whether you're making soldier or weapon animations, a different bone set needs to be selected).

# Import/export of ObjectTemplate vs (Bundled/Skinned/Static/Collision)Mesh
There are two ways of importing BF2 meshes. One is to use `Import -> BF2` menu to directly import a specific mesh file type e.g. `StaticMesh`. This however will only import the "raw" mesh data according to its internal file structure lacking information about objectTemplate's metadata such as geometry part names, their position/rotation, hierarchy, collision material names etc. This is fine for simple meshes or when you intend just to make small tweaks to the mesh, but generally if you don't have a good reason to use those options, **don't** use them. A preferable way to import a mesh is the `Import -> BF2 -> ObjecTemplate (.con)` option, which parses the objectTemplate definition and imports the visible geometry of the object (optionally also its collision mesh), split all mesh parts into sub-meshes, reposition them, recreate their hierarchy as well as rename collision mesh materials. For re-exporting, always use the corresponding option from the `Export -> BF2` menu.
# ObjectTemplate vs Mesh import/export
There are two ways of importing BF2 meshes. One is to use `Import -> BF2` menu to directly import a specific mesh file type (`StaticMesh`, `SkinnedMesh`, `BundledMesh` or `CollisionMesh`). This however will only import the _raw_ mesh data according to its internal file structure lacking information present in the ObjectTemplate (`.con`) definition such as mapping of the geometry part to BF2 object type and name, geometry part transformations, hierarchy or collmesh material names. This data is not essential for simple meshes or when you intend just to make small tweaks to the mesh, but generally if you don't have a good reason to use those options, **don't** use them. The second (and preferable) way to import a mesh is the `Import -> BF2 -> ObjecTemplate (.con)` option, which parses the ObjectTemplate definition and imports its visible geometry and (optionally) collision mesh, split all mesh parts into sub-meshes, reposition them, recreate their hierarchy as well as rename collision mesh materials. For re-exporting, always use the corresponding option from the `Export -> BF2` menu.

# ObjectTemplate exporting
- `Export -> BF2 -> ObjecTemplate (.con)` shall be used for exporting objects created from scratch (like 3ds Max exporter, it spits out a `.con` file + visible mesh + collision mesh into `Meshes` sub-directory). Export option will only be available when you have an object active in the viewport. Before reading any further, I highly advise you to import any existing BF2 mesh first (`Import -> BF2 -> ObjecTemplate (.con)`), and look at how everything is set up to make below steps easier to follow.
Expand Down
2 changes: 1 addition & 1 deletion io_scene_bf2/blender_manifest.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
schema_version = "1.0.0"
id = "io_scene_bf2"
version = "0.8.2"
version = "0.8.3"
name = "Battlefield 2"
tagline = "Import and export asset files for DICE's Refractor 2 engine"
maintainer = "Marek Zajac <[email protected]>"
Expand Down
30 changes: 15 additions & 15 deletions io_scene_bf2/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@

from .mesh import import_mesh
# from .mesh import import_mesh

from .mesh import import_bundledmesh
from .mesh import export_bundledmesh
# from .mesh import import_bundledmesh
# from .mesh import export_bundledmesh

from .mesh import import_staticmesh
from .mesh import export_staticmesh
# from .mesh import import_staticmesh
# from .mesh import export_staticmesh

from .mesh import import_skinnedmesh
from .mesh import export_skinnedmesh
# from .mesh import import_skinnedmesh
# from .mesh import export_skinnedmesh

from .animation import import_animation
from .animation import export_animation
# from .animation import import_animation
# from .animation import export_animation

from .collision_mesh import import_collisionmesh
from .collision_mesh import export_collisionmesh
# from .collision_mesh import import_collisionmesh
# from .collision_mesh import export_collisionmesh

from .skeleton import import_skeleton
from .skeleton import export_skeleton
# from .skeleton import import_skeleton
# from .skeleton import export_skeleton

from .object_template import import_object
from .object_template import export_object
# from .object_template import import_object
# from .object_template import export_object
15 changes: 11 additions & 4 deletions io_scene_bf2/core/animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
import math

from mathutils import Matrix # type: ignore
from .bf2.bf2_animation import BF2Animation, BF2KeyFrame
from .bf2.bf2_animation import BF2Animation, BF2KeyFrame, BF2AnimationException
from .utils import to_matrix, conv_bf2_to_blender, conv_blender_to_bf2
from .skeleton import (ske_get_bone_rot,
find_animated_weapon_object, ske_weapon_part_ids)
from .exceptions import ImportException, ExportException

def get_bones_for_export(rig):
ske_bones = rig['bf2_bones']
Expand Down Expand Up @@ -82,13 +83,19 @@ def export_animation(context, rig, baf_file, bones_to_export=None, fstart=None,

# revert to frame before export
scene.frame_set(saved_frame)

baf.export(baf_file)

try:
baf.export(baf_file)
except BF2AnimationException as e:
raise ExportException(str(e)) from e


def import_animation(context, rig, baf_file, insert_at_frame=0):
scene = context.scene
baf = BF2Animation(baf_file)
try:
baf = BF2Animation(baf_file)
except BF2AnimationException as e:
raise ImportException(str(e)) from e

armature = rig.data
context.view_layer.objects.active = rig
Expand Down
6 changes: 6 additions & 0 deletions io_scene_bf2/core/bf2/bf2_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ def calc_bounds(verts):
return (_min, _max)

class Quat:
__slots__ = ('x', 'y', 'z', 'w')

def __init__(self, x=0.0, y=0.0, z=0.0, w=1.0):
self.x = x
self.y = y
Expand Down Expand Up @@ -101,6 +103,8 @@ def __eq__(self, other):
return False

class Vec3:
__slots__ = ('x', 'y', 'z')

def __init__(self, x=0.0, y=0.0, z=0.0):
self.x = x
self.y = y
Expand Down Expand Up @@ -228,6 +232,8 @@ def __eq__(self, other):
return False

class Mat4:
__slots__ = ('m')

def __init__(self, m=None):
if m is not None:
self.m = list()
Expand Down
1 change: 1 addition & 0 deletions io_scene_bf2/core/bf2/bf2_mesh/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .bf2_visiblemesh import BF2MeshException
from .bf2_bundledmesh import BF2BundledMesh
from .bf2_skinnedmesh import BF2SkinnedMesh
from .bf2_staticmesh import BF2StaticMesh
Expand Down
2 changes: 1 addition & 1 deletion io_scene_bf2/core/bf2/bf2_skeleton.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def __init__(self, ske_file='', name=''):

if not self.roots:
raise BF2SkeletonException("Invalid .ske file, missing root node")

def export(self, export_path):
with open(export_path, "wb") as f:
ske_data = FileUtils(f)
Expand Down
52 changes: 44 additions & 8 deletions io_scene_bf2/core/bf2/bsp_builder.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
from typing import List, Tuple, Optional
from .bf2_common import Vec3

PRE_CACHE_SPLIT_PLANES = True

class PolyType:
FRONT = 0,
BACK = 1,
COPLANAR = 2,
STRADDLE = 3

class Poly:
__slots__ = ('face_idx', 'indexes', 'points',
'center', 'normal', 'd')

def __init__(self, face, verts, face_idx):
self.face_idx = face_idx
self.indexes = face
Expand Down Expand Up @@ -36,7 +41,7 @@ def _intersects(self, plane):
for vertex in range(len(self.points)):
prev_vert = vertex - 1 if vertex != 0 else len(self.points) - 1

edge_delta = self.points[vertex].copy().sub( self.points[prev_vert])
edge_delta = self.points[vertex].copy().sub(self.points[prev_vert])
denom = edge_delta.dot_product(plane.normal)
if denom:
numer = self.points[prev_vert].dot_product(plane.normal) + plane.d
Expand All @@ -48,6 +53,7 @@ def _intersects(self, plane):
last_side_parallel = denom == 0.0
return False


def classify(self, plane):
if self._intersects(plane):
return PolyType.STRADDLE
Expand All @@ -62,6 +68,9 @@ def classify(self, plane):
return PolyType.BACK

class Plane:
__slots__ = ('val', 'axis', 'normal',
'point', 'd', 'face_cache')

def __init__(self, val, axis):
self.val = val
self.axis = axis
Expand All @@ -71,9 +80,24 @@ def __init__(self, val, axis):
self.point = Vec3()
self.point[axis] = val
self.d = -self.normal.dot_product(self.point)
self.face_cache = None

def cache_faces(self, polys):
self.face_cache = list()
for poly in polys:
c = poly.classify(self)
self.face_cache.append(c)

def classify(self, poly):
if self.face_cache is not None:
return self.face_cache[poly.face_idx]
return poly.classify(self)


class Node:
__slots__ = ('front_faces', 'back_faces', 'front_node',
'back_node', 'split_plane')

def __init__(self, split_plane):
self.front_faces : List[Poly] = None
self.back_faces : List[Poly] = None
Expand All @@ -82,6 +106,9 @@ def __init__(self, split_plane):
self.split_plane : Plane = split_plane

class BspBuilder:
__slots__ = ('verts', 'faces', 'complanar_weigth',
'intersect_weight', 'split_weight',
'min_split_metric', 'split_planes', 'root')

def __init__(self, verts : Tuple[float], faces : Tuple[int],
complanar_weigth = 0.5, intersect_weight = 1.0,
Expand All @@ -97,30 +124,39 @@ def __init__(self, verts : Tuple[float], faces : Tuple[int],
polys = list()
for face_idx, face in enumerate(faces):
polys.append(Poly(face, self.verts, face_idx))

self.split_planes = dict()
for vert, i in self._get_all_split_plane_ids(polys):
split_plane = Plane(self.verts[vert][i], i)
if PRE_CACHE_SPLIT_PLANES:
split_plane.cache_faces(polys)
self.split_planes[(vert, i)] = split_plane

self.root = self._build_bsp_tree(polys)

def _get_all_planes(self, polys : List[Poly]):
def _get_all_split_plane_ids(self, polys : List[Poly]):
planes_to_check = set() # set of (vert, axis)
for poly in polys:
for i, vert in enumerate(poly.indexes):
planes_to_check.add((vert, i))

for plane in planes_to_check:
vert, i = plane
yield Plane(self.verts[vert][i], i)
yield plane

def _find_best_split_plane(self, polys : List[Poly]):
def _find_best_split_plane(self, polys : List[Poly]) -> Plane:
best_metric = float("inf")
best_split_plane = None

for split_plane in self._get_all_planes(polys):
for split_plane_id in self._get_all_split_plane_ids(polys):
split_plane = self.split_planes[split_plane_id]

coplanar_count = 0
intersect_count = 0
front_count = 0
back_count = 0

for poly in polys:
c = poly.classify(split_plane)
c = split_plane.classify(poly)
if c == PolyType.STRADDLE:
intersect_count += 1
elif c == PolyType.COPLANAR:
Expand Down Expand Up @@ -162,7 +198,7 @@ def _build_bsp_tree(self, polys : List[Poly]):
back : List[Poly] = list()

for poly in polys:
c = poly.classify(split_plane)
c = split_plane.classify(poly)
if c == PolyType.STRADDLE or c == PolyType.COPLANAR:
front.append(poly)
back.append(poly)
Expand Down
5 changes: 4 additions & 1 deletion io_scene_bf2/core/collision_mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,10 @@ def export_collisionmesh(root_obj, mesh_file, geom_parts=None):
col = _export_collistionmesh_col(col_idx, col_obj, material_to_index)
geom.cols.append(col)

collmesh.export(mesh_file)
try:
collmesh.export(mesh_file)
except BF2CollMeshException as e:
raise ExportException(str(e)) from e

return collmesh, material_to_index

Expand Down
49 changes: 28 additions & 21 deletions io_scene_bf2/core/mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
from itertools import chain
from mathutils import Vector # type: ignore

from .bf2.bf2_mesh import BF2Mesh, BF2BundledMesh, BF2SkinnedMesh, BF2StaticMesh
from .bf2.bf2_mesh import BF2MeshException, BF2Mesh, BF2BundledMesh, BF2SkinnedMesh, BF2StaticMesh
from .bf2.bf2_common import Mat4
from .bf2.bf2_mesh.bf2_visiblemesh import Material, MaterialWithTransparency, Vertex, VertexAttribute
from .bf2.bf2_mesh.bf2_visiblemesh import Material, MaterialWithTransparency, Vertex
from .bf2.fileutils import FileUtils

from .exceptions import ImportException, ExportException
Expand Down Expand Up @@ -105,21 +105,27 @@ def __init__(self, context, mesh_file, mesh_type='', reload=False,
reporter=DEFAULT_REPORTER):
self.context = context
self.is_vegitation = 'vegitation' in mesh_file.lower() # yeah this is legit how BF2 detects it lmao
if mesh_type:
self.bf2_mesh = _MESH_TYPES[mesh_type.upper()](mesh_file)

if mesh_type:
self._loader = lambda: _MESH_TYPES[mesh_type.upper()](mesh_file)
else:
self.bf2_mesh = BF2Mesh.load(mesh_file)
self._loader = lambda: BF2Mesh.load(mesh_file)

self.bf2_mesh = None
self.reload = reload
self.texture_path = texture_path
self.geom_to_ske = geom_to_ske
self.reporter = reporter
self.mesh_materials = []
self.mesh_name = self.bf2_mesh.name
self.merge_materials = merge_materials

def import_mesh(self, name='', geom=None, lod=None):
if name:
self.mesh_name = name
try:
self.bf2_mesh = self._loader()
except BF2MeshException as e:
raise ImportException(str(e)) from e

self.mesh_name = name or self.bf2_mesh.name
self._cleanup_old_materials()
if geom is None and lod is None:
if self.reload: delete_object_if_exists(self.mesh_name)
Expand Down Expand Up @@ -511,9 +517,11 @@ def export_mesh(self):
for lod_obj in geom_obj:
bf2_lod = bf2_geom.new_lod()
self._export_mesh_lod(bf2_lod, lod_obj)

self.bf2_mesh.export(self.mesh_file)
return self.bf2_mesh
try:
self.bf2_mesh.export(self.mesh_file)
except BF2MeshException as e:
raise ExportException(str(e)) from e
return self.bf2_mesh

def _setup_vertex_attributes(self):
self.bf2_mesh.add_vert_attr('FLOAT3', 'POSITION')
Expand Down Expand Up @@ -605,9 +613,6 @@ def _export_mesh_lod(self, bf2_lod, lod_obj):
animuv_matrix_index = None
animuv_rot_center = None

# XXX: I have no idea what map is this supposed to be calculated on
# I assume it must match with tangents which were used to generate the normal map
# but we don't know this! so its probably needed to be added as an export setting?
if not self.tangent_uv_map:
raise ExportException("No UV selected for tangent space generation!")
mesh.calc_tangents(uvmap=self.tangent_uv_map)
Expand Down Expand Up @@ -648,8 +653,9 @@ def _export_mesh_lod(self, bf2_lod, lod_obj):
# that's why we gonna write all groups defined
bf2_lod.parts_num = len(vertex_group_to_part_id)
if bf2_lod.parts_num > MAX_GEOM_LIMIT:
raise ExportException(f"{lod_obj.name}: BF2 only supports a maximum of "
self.reporter.warning(f"{lod_obj.name}: BF2 only supports a maximum of "
f"{MAX_GEOM_LIMIT} geometry parts but got {bf2_lod.parts_num}")

elif mesh_type == BF2SkinnedMesh:
bone_to_matrix = dict()
bone_to_id = dict()
Expand Down Expand Up @@ -790,7 +796,7 @@ def _export_mesh_lod(self, bf2_lod, lod_obj):
vert.position = _swap_zy(blend_vertex.co)
vert.normal = _swap_zy(loop.normal)
vert.tangent = _swap_zy(loop.tangent)

# blendindices
blendindices = [0, 0, 0, 0]
# - (BundledMesh) first one is geom part index, second one unused
Expand Down Expand Up @@ -822,8 +828,6 @@ def _export_mesh_lod(self, bf2_lod, lod_obj):
except ValueError:
blendindices[_bone_idx] = len(bone_list)
bone_list.append(_bone_name)
if len(bone_list) > MAX_BONE_LIMIT:
raise ExportException(f"{lod_obj.name} (mat: {blend_material.name}): BF2 only supports a maximum of {MAX_BONE_LIMIT} bones per material")
bf2_bone = bf2_rig.new_bone()
if _bone_name not in bone_to_id:
raise ExportException(f"{lod_obj.name} (mat: {blend_material.name}): bone '{_bone_name}' is not present in BF2 skeleton")
Expand Down Expand Up @@ -906,9 +910,12 @@ def _export_mesh_lod(self, bf2_lod, lod_obj):
stats += f'\n\tfaces: {len(bf2_mat.faces)}'
print(stats)

if mesh_type == BF2SkinnedMesh and not bone_list:
self.reporter.warning(f"{lod_obj.name}: (mat: {blend_material.name}): has no weights assigned")

if mesh_type == BF2SkinnedMesh:
if not bone_list:
self.reporter.warning(f"{lod_obj.name}: Material '{blend_material.name}' has no weights assigned")
if len(bone_list) > MAX_BONE_LIMIT:
self.reporter.warning(f"{lod_obj.name}: BF2 only supports a maximum of {MAX_BONE_LIMIT} bones per material,"
f" but material '{blend_material.name}' has got {len(bone_list)} bones (vertex groups) assigned!")
mesh.free_tangents()

def _can_merge_vert(self, this, other, uv_count):
Expand Down
Loading

0 comments on commit 65ce1aa

Please sign in to comment.