diff --git a/.gitignore b/.gitignore index 9c8e47b..4230186 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ __pycache__/ -*.code-workspace \ No newline at end of file +*.code-workspace +*.pyd \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..094705e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "bsp_builder/pybind11"] + path = bsp_builder/pybind11 + url = ../../pybind/pybind11 + branch = stable diff --git a/README.md b/README.md index 3db4e26..1e200f6 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ After installation, setup your `BF2 mod directory` (`Edit -> Preferences -> Add- - 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, different bone set needs to be selected). #### Static modeling: -Export options will only be avaliable when you have an object active in the viewport. This is written 100% in Python which means the export is painfully slow, be patient! Collmesh export with ~4k tris can take up to 20 minutes, trying to export more complex stuff will be a suicide. +Export options will only be avaliable when you have an object active in the viewport. ##### StaticMesh exporting: - The active object needs to be the root of the hierarchy, each child of the root object must be a GEOM object (suffixed with `_geom`), each child of the GEOM object must be a LOD object (suffixed with `_lod`) containing mesh data. @@ -42,6 +42,7 @@ root - Inside the `Shader Editor`, assign texture files to the desired texture map types. There should be be 7 `Image Texture` nodes, each corresponding to Base, Detail, Dirt, Crack, Detail Normal, Crack Normal. The last one (SpecularLUT) can be ignored. Detail, Dirt, Crack and their normal maps are optional (can be removed or left unset). There should also be 5 `UV Map` nodes (linked to their corresponding image texture nodes), assign UV layers to them as described below. - Each LOD's mesh must have assigned a minimum of 1 and a maximum of 5 UV layers and each UV layer must be called `UV`, where each one corresponds to the following texture maps: UV0 = Base, UV1 = Detail, UV2 = Dirt, UV3 (or UV2 if Dirt layer is not present) = Crack and the last one (always UV4) is the Lightmap UV, when Lightmap UV is not present it will be generated. - Setting material's Blend Mode to `Alpha Blend` or `Alpha Clip` will export the material with BF2's `Alpha Blend` (not fully supported yet) or `Alpha Test` transparency modes respectively. +- Export is currently written 100% in Python which means the it is painfully slow, be patient! ##### CollisionMesh exporting: - The active object needs to be the root of the hierarchy, each child of the root object must be a GEOM object (suffixed with `_geom`), each child of the geom object must be a SUBGEOM object (suffixed with `_subgeom`) , each child of the SUBGEOM object must be a LOD object (suffixed with `_lod`) containing mesh data. diff --git a/__init__.py b/__init__.py index 75fd276..aa47180 100644 --- a/__init__.py +++ b/__init__.py @@ -24,7 +24,7 @@ "author" : "Marek Zajac", "description" : "", "blender" : (3, 4, 0), - "version" : (0, 1, 2), + "version" : (0, 2, 0), "location" : "", "warning" : "", "category" : "Import-Export" diff --git a/bsp_builder/.gitignore b/bsp_builder/.gitignore new file mode 100644 index 0000000..635dc55 --- /dev/null +++ b/bsp_builder/.gitignore @@ -0,0 +1,8 @@ +build/ +dist/ +_build/ +_generate/ +*.so +*.py[cod] +*.egg-info +*env* diff --git a/bsp_builder/CMakeLists.txt b/bsp_builder/CMakeLists.txt new file mode 100644 index 0000000..25e2c1f --- /dev/null +++ b/bsp_builder/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.4...3.18) +project(bsp_builder) + +set(PYBIND11_PYTHON_VERSION "3.10") + +add_subdirectory(pybind11) +pybind11_add_module(bsp_builder src/main.cpp) diff --git a/bsp_builder/pybind11 b/bsp_builder/pybind11 new file mode 160000 index 0000000..8b03ffa --- /dev/null +++ b/bsp_builder/pybind11 @@ -0,0 +1 @@ +Subproject commit 8b03ffa7c06cd9c8a38297b1c8923695d1ff1b07 diff --git a/bsp_builder/src/main.cpp b/bsp_builder/src/main.cpp new file mode 100644 index 0000000..f57d5b1 --- /dev/null +++ b/bsp_builder/src/main.cpp @@ -0,0 +1,396 @@ +#include +#include +#include +#include +#include + +typedef std::tuple Face; +typedef std::tuple Vertex; + +struct Vec3 { + float x, y, z; + + Vec3(float x, float y, float z) : x(x), y(y), z(z) {}; + Vec3() : x(0.0f), y(0.0f), z(0.0f) {}; + Vec3(Vertex v) : x(std::get<0>(v)), + y(std::get<1>(v)), + z(std::get<2>(v)) {}; + + float length() const { + return ::std::sqrt(x * x + y * y + z * z); + } + + Vec3& scale(float s) { + x = x * s; + y = y * s; + z = z * s; + return *this; + } + + Vec3& normalize() { + float len = length(); + if (len == 0.0f) return *this; + return scale(1.0f / len); + } + + Vec3& add(const Vec3& v) { + x += v.x; + y += v.y; + z += v.z; + return *this; + } + + Vec3& sub(const Vec3& v) { + x -= v.x; + y -= v.y; + z -= v.z; + return *this; + } + + Vec3& sub_vectors(const Vec3& v1, const Vec3& v2) { + x = v1.x - v2.x; + y = v1.y - v2.y; + z = v1.z - v2.z; + return *this; + } + + Vec3 cross_product(const Vec3& v) const { + Vec3 result; + result.x = y * v.z - z * v.y; + result.y = z * v.x - x * v.z; + result.z = x * v.y - y * v.x; + return result; + } + + float dot_product(const Vec3& v) const { + return v.x * x + v.y * y + v.z * z; + } + + bool equals(const Vec3& v) const { + return x == v.x && y == v.y && z == v.z; + } +}; + +class Poly; + +class Plane { +public: + Plane(float val, int axis) : val(val), axis(axis) { + switch (axis) { + case 0: + normal.x = 1.0f; + point.x = val; + break; + case 1: + normal.y = 1.0f; + point.y = val; + break; + case 2: + normal.z = 1.0f; + point.z = val; + break; + default: + assert(0); + } + + d = -normal.dot_product(point); + }; + + const float val; + const int axis; +private: + + Vec3 normal; + Vec3 point; + float d; + + friend class Poly; +}; + +enum class PolyType { + FRONT, + BACK, + COPLANAR, + STRADDLE +}; + +class Poly { +public: + Poly(const Face& face, const std::vector& verts) + { + points[0] = verts[std::get<0>(face)]; + points[1] = verts[std::get<1>(face)]; + points[2] = verts[std::get<2>(face)]; + + indexes[0] = std::get<0>(face); + indexes[1] = std::get<1>(face); + indexes[2] = std::get<2>(face); + + for (Vec3& point : points) { + center.add(point); + } + center.scale(1.0f / points.size()); + + Vec3 a, b; + a.sub_vectors(points[0], points[1]); + b.sub_vectors(points[2], points[1]); + normal = a.cross_product(b); + normal.normalize(); + d = -normal.dot_product(center); + } + + PolyType clasify(Plane& plane) const { + if (intersects(plane)) { + return PolyType::STRADDLE; + } + else { + Vec3 delta; + delta.sub_vectors(center, plane.point); + float dotp = delta.dot_product(plane.normal); + if (dotp == 0.0f) + return PolyType::COPLANAR; + else if (dotp < 0.0f) + return PolyType::FRONT; + else + return PolyType::BACK; + } + } + + std::array indexes; +private: + bool intersects(Plane& plane) const { + bool last_side_parallel = false; + if (!normal.equals(plane.normal)) { + Vec3 EdgeDelta; + float numer, denom, t; + + for (unsigned short vertex = 0; vertex < points.size(); vertex++) { + unsigned short prevVertex = vertex ? vertex - 1 : points.size() - 1; + + EdgeDelta.sub_vectors(points[vertex], points[prevVertex]); + denom = EdgeDelta.dot_product(plane.normal); + + if (denom) { + numer = points[prevVertex].dot_product(plane.normal) + plane.d; + t = -numer / denom; + + if (!(last_side_parallel && t == 0.0f)) { + if (t > 0.0f && t < 0.999999f) { + return true; + } + } + } + last_side_parallel = (denom == 0.0f); + } + } + return false; + } + +private: + std::array points; + Vec3 center; + Vec3 normal; + float d; +}; + +struct Node { + + Node(const Plane& split_plane) : split_plane(split_plane) {} + + std::vector front_faces; + std::vector back_faces; + Node* front_node = nullptr; + Node* back_node = nullptr; + const Plane split_plane; +}; + +typedef std::array AxisPlanes; + +class BspBuilder { +public: + BspBuilder(const std::vector& verts, const std::vector& faces) + : vert_count(verts.size()) + { + polys.reserve(faces.size()); + std::vector poly_indexes; + poly_indexes.reserve(faces.size()); + for (int i = 0; i < faces.size(); ++i) { + const Face& face = faces[i]; + poly_indexes.push_back(i); + polys.push_back(Poly(face, verts)); + } + + planes.reserve(vert_count * 3); + for (int i = 0; i < vert_count; ++i) { + Vec3 vertx = verts[i]; + AxisPlanes planes_per_axis = { + Plane(vertx.x, 0), + Plane(vertx.y, 1), + Plane(vertx.z, 2) + }; + planes.push_back(planes_per_axis); + } + + root = build_bsp_tree(poly_indexes); + } + Node* root = nullptr; + + ~BspBuilder() { + destroy_bsp_tree(root); + } + +private: + std::vector polys; // lookup for face_idx -> poly + std::vector planes; // lookup for vert_index -> 3 planes created by this vert + size_t vert_count; + + Node* build_bsp_tree(std::vector& poly_indexes) { + + Plane* split_plane = find_best_split_plane(poly_indexes); + if (split_plane == nullptr) { + return nullptr; + } + + std::vector front; + std::vector back; + + for (size_t poly_index : poly_indexes) { + const Poly& poly = polys[poly_index]; + switch (poly.clasify(*split_plane)) { + case PolyType::STRADDLE: + case PolyType::COPLANAR: + front.push_back(poly_index); + back.push_back(poly_index); + break; + case PolyType::FRONT: + front.push_back(poly_index); + break; + case PolyType::BACK: + back.push_back(poly_index); + break; + default: + assert(0); + } + } + + Node* node = new Node(*split_plane); + node->front_node = build_bsp_tree(front); + if (node->front_node == nullptr) + node->front_faces = std::move(front); + + node->back_node = build_bsp_tree(back); + if (node->back_node == nullptr) + node->back_faces = std::move(back); + + return node; + } + + Plane* find_best_split_plane(std::vector& poly_indexes) { + static constexpr float COPLANAR_WEIGHT = 0.5f; // puts more emphasis on keeping to minimum coplanar polygons + static constexpr float INTERSECT_WIEGHT = 1.0f; // puts more emphasis on keeping to minimum intersecting polygons + static constexpr float SPLIT_WEIGHT = 1.0f; // puts more emphasis on equal split on front / back polygons + static constexpr float MIN_SPLIT_METRIC = 0.5f; // minimum acceptable metric, when to stop splitting + + float best_metric = std::numeric_limits::infinity(); + Plane* best_split_plane = nullptr; + + std::unique_ptr p_plane_flags = std::make_unique(vert_count * 3); + bool* plane_flags = p_plane_flags.get(); + std::memset(plane_flags, 0, vert_count * 3 * sizeof(bool)); + + size_t poly_count = poly_indexes.size(); + + for (size_t test_poly_index : poly_indexes) { + const Poly& test_poly = polys[test_poly_index]; + for (int i = 0; i < 3; ++i) { + // create a plane from each poly vert and axis + int vert = test_poly.indexes[i]; + + if (plane_flags[3 * vert + i]) { + continue; // plane already checked + } + + plane_flags[3 * vert + i] = true; + Plane& split_plane = planes[vert][i]; + + size_t coplanar_count = 0; + size_t intersect_count = 0; + size_t front_count = 0; + size_t back_count = 0; + + for (size_t poly_index : poly_indexes) { + const Poly& poly = polys[poly_index]; + switch (poly.clasify(split_plane)) { + case PolyType::STRADDLE: + intersect_count++; + break; + case PolyType::COPLANAR: + coplanar_count++; + break; + case PolyType::FRONT: + front_count++; + break; + case PolyType::BACK: + back_count++; + break; + default: + assert(0); + } + } + + if (front_count == 0 || back_count == 0) // can't split into two sets + continue; + + float split_ratio = (float)front_count / (float)(front_count + back_count); + float intersect_ratio = (float)intersect_count / (float)poly_count; + float coplanar_ratio = (float)coplanar_count / (float)poly_count; + + float metric = std::abs(0.5f - split_ratio) * SPLIT_WEIGHT + + intersect_ratio * INTERSECT_WIEGHT + + coplanar_ratio * COPLANAR_WEIGHT; + + if (metric > MIN_SPLIT_METRIC) + continue; + + if (metric < best_metric) { + best_metric = metric; + best_split_plane = &split_plane; + } + } + } + + return best_split_plane; + } + + void destroy_bsp_tree(Node* node) { + if (node->front_node) { + destroy_bsp_tree(node->front_node); + } + if (node->back_node) { + destroy_bsp_tree(node->back_node); + } + delete node; + } +}; + +namespace py = pybind11; + +PYBIND11_MODULE(bsp_builder, m) { + + py::class_(m, "BspBuilder") + .def(py::init&, const std::vector&>()) + .def_readonly("root", &BspBuilder::root); + + py::class_(m, "Plane") + .def(py::init()) + .def_readonly("val", &Plane::val) + .def_readonly("axis", &Plane::axis); + + py::class_(m, "Node") + .def(py::init()) + .def_readonly("split_plane", &Node::split_plane) + .def_readonly("front_faces", &Node::front_faces) + .def_readonly("back_faces", &Node::back_faces) + .def_readonly("front_node", &Node::front_node) + .def_readonly("back_node", &Node::back_node); +} diff --git a/core/bf2/bf2_collmesh.py b/core/bf2/bf2_collmesh.py index cdb76e7..ab969fb 100644 --- a/core/bf2/bf2_collmesh.py +++ b/core/bf2/bf2_collmesh.py @@ -1,9 +1,15 @@ from typing import Dict, List, Tuple, Optional from .fileutils import FileUtils -from .bsp_builder import BspBuilder + from .bf2_common import Vec3, calc_bounds, load_n_elems +try: + from ...bsp_builder import BspBuilder +except ImportError as e: + print("Cannot import BSP builder, falling back to slow af python implementation", e) + from .py_bsp_builder import BspBuilder + import os class BF2CollMeshException(Exception): @@ -178,26 +184,32 @@ def _get_nodes_list(node, node_list): for face in face_refs: f.write_word(faces.index(face)) - @staticmethod def build(verts, faces): - builder = BspBuilder(verts, faces) + _verts = [(v.x, v.y, v.z) for v in verts] + _faces = [f.verts for f in faces] + builder = BspBuilder(_verts, _faces) def _copy(builder_node, parent=None): split_plane = builder_node.split_plane node = BSP.Node(split_plane.val, split_plane.axis) node.parent = parent - children = builder_node.get_children() - faces = builder_node.get_faces() + children = (builder_node.front_node, builder_node.back_node) + face_idxs = (builder_node.front_faces, builder_node.back_faces) for i in range(2): if children[i] is not None: node.children[i] = _copy(children[i], node) else: node.children[i] = None - node.faces[i] = faces[i] + node.faces[i] = [faces[face_idx] for face_idx in face_idxs[i]] return node + if builder.root is None: + # might happen when collmesh is very simple + print("Cannot build BSP tree, no good enough split plane found") + return None + root = _copy(builder.root) mins, maxs = calc_bounds(verts) return BSP(mins, maxs, root) @@ -273,11 +285,12 @@ def save(self, f : FileUtils, update_bounds=True): self.min.save(f) self.max.save(f) - f.write_byte(0x31) + self.bsp = BSP.build(self.verts, self.faces) if self.bsp is None: - self.bsp = BSP.build(self.verts, self.faces) - - self.bsp.save(f, self.faces) + f.write_byte(0x30) + else: + f.write_byte(0x31) + self.bsp.save(f, self.faces) class SubGeom: diff --git a/core/bf2/bsp_builder.py b/core/bf2/py_bsp_builder.py similarity index 65% rename from core/bf2/bsp_builder.py rename to core/bf2/py_bsp_builder.py index 0d4f82c..6d12469 100644 --- a/core/bf2/bsp_builder.py +++ b/core/bf2/py_bsp_builder.py @@ -1,16 +1,21 @@ -from typing import List, Optional +from typing import List, Tuple, Optional from .bf2_common import Vec3 class PolyType: FRONT = 0, BACK = 1, COPLANAR = 2, - STRADDLE = 3, + STRADDLE = 3 class Poly: - def __init__(self, points, face_ref): - self.face_ref = face_ref - self.points : List[Vec3] = points + def __init__(self, face, verts, face_idx): + self.face_idx = face_idx + self.indexes = face + self.points : Tuple[Vec3] = ( + verts[face[0]].copy(), + verts[face[1]].copy(), + verts[face[2]].copy() + ) self.center = Vec3() for point in self.points: @@ -69,44 +74,33 @@ def __init__(self, val, axis): class Node: - def __init__(self): - self.front_polys : List[Poly] = None - self.back_polys : List[Poly] = None + def __init__(self, split_plane): + self.front_faces : List[Poly] = None + self.back_faces : List[Poly] = None self.front_node : Optional[Node] = None self.back_node : Optional[Node] = None - self.split_plane : Plane = None - - def get_children(self): - return (self.front_node, self.back_node) - - def get_faces(self): - front = [poly.face_ref for poly in self.front_polys] if self.front_polys else None - back = [poly.face_ref for poly in self.back_polys] if self.back_polys else None - return (front, back) + self.split_plane : Plane = split_plane class BspBuilder: - def __init__(self, verts, faces): - self.verts = verts + def __init__(self, verts : Tuple[float], faces : Tuple[int]): + self.verts = [Vec3(*v) for v in verts] self.faces = faces polys = list() - for face in faces: - points = list() - points.append(verts[face.verts[0]].copy()) - points.append(verts[face.verts[1]].copy()) - points.append(verts[face.verts[2]].copy()) - polys.append(Poly(points, face)) + for face_idx, face in enumerate(faces): + polys.append(Poly(face, self.verts, face_idx)) self.root = self._build_bsp_tree(polys) def _get_all_planes(self, polys : List[Poly]): - checked_verts = set() + planes_to_check = set() # set of (vert, axis) for poly in polys: - for i, vert in enumerate(poly.face_ref.verts): - if vert in checked_verts: - continue - checked_verts.add(vert) - yield Plane(self.verts[vert][i], i) + 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) def _find_best_split_plane(self, polys : List[Poly]): COPLANAR_WEIGHT = 0.5 # puts more emphasis on keeping to minimum coplanar polygons @@ -118,30 +112,30 @@ def _find_best_split_plane(self, polys : List[Poly]): best_split_plane = None for split_plane in self._get_all_planes(polys): - coplanar : List[Poly] = list() - intersect : List[Poly] = list() - front : List[Poly] = list() - back : List[Poly] = list() + coplanar_count = 0 + intersect_count = 0 + front_count = 0 + back_count = 0 for poly in polys: c = poly.classify(split_plane) if c == PolyType.STRADDLE: - intersect.append(poly) + intersect_count += 1 elif c == PolyType.COPLANAR: - coplanar.append(poly) + coplanar_count += 1 elif c == PolyType.FRONT: - front.append(poly) + front_count += 1 elif c == PolyType.BACK: - back.append(poly) + back_count += 1 else: raise RuntimeError() - if not front or not back: # can't split into two sets + if front_count == 0 or back_count == 0: # can't split into two sets continue - split_ratio = len(front) / (len(front) + len(back)) - intersect_ratio = len(intersect) / len(polys) - coplanar_ratio = len(coplanar) / len(polys) + split_ratio = front_count / (front_count + back_count) + intersect_ratio = intersect_count / len(polys) + coplanar_ratio = coplanar_count / len(polys) metric = (abs(0.5 - split_ratio) * SPLIT_WEIGHT + intersect_ratio * INTERSECT_WIEGHT + @@ -156,7 +150,7 @@ def _find_best_split_plane(self, polys : List[Poly]): return best_split_plane - def _build_bsp_tree(self, polys : List[Poly], level=0): + def _build_bsp_tree(self, polys : List[Poly]): split_plane = self._find_best_split_plane(polys) if split_plane is None: @@ -177,14 +171,13 @@ def _build_bsp_tree(self, polys : List[Poly], level=0): else: raise RuntimeError() - root = Node() - root.split_plane = split_plane - root.front_node = self._build_bsp_tree(front, level+1) - if root.front_node is None: - root.front_polys = front + node = Node(split_plane) + node.front_node = self._build_bsp_tree(front) + if node.front_node is None: + node.front_faces = [f.face_idx for f in front] - root.back_node = self._build_bsp_tree(back, level+1) - if root.back_node is None: - root.back_polys = back + node.back_node = self._build_bsp_tree(back) + if node.back_node is None: + node.back_faces = [f.face_idx for f in back] - return root + return node