Advertisement
cfox04

ybnimport refactor 1

May 11th, 2022
915
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 18.34 KB | None | 0 0
  1. from traceback import format_exc
  2. import bpy
  3.  
  4. from typing import Union
  5.  
  6. from ..tools.utils import get_distance_of_vectors
  7. from .collision_materials import create_collision_material_from_index
  8. from ..tools.meshhelper import create_disc, create_vertexcolor_layer, create_box, get_direction_of_vectors
  9. from ..resources.bound import *
  10. from ..sollumz_properties import *
  11. from .properties import BoundFlags, CollisionMatFlags
  12. from numpy import absolute
  13. import os
  14.  
  15.  
  16. def apply_bound_properties(bpy_object: bpy.types.Object, bound_xml: Bound):
  17.     """Sets the bound_properties data block of bound_bpy to the corresponding properties of bound_xml."""
  18.     bpy_object.bound_properties.procedural_id = bound_xml.procedural_id
  19.     bpy_object.bound_properties.room_id = bound_xml.room_id
  20.     bpy_object.bound_properties.ped_density = bound_xml.ped_density
  21.     bpy_object.bound_properties.poly_flags = bound_xml.poly_flags
  22.     bpy_object.bound_properties.inertia = bound_xml.inertia
  23.     bpy_object.bound_properties.unk_flags = bound_xml.unk_flags
  24.     bpy_object.bound_properties.margin = bound_xml.margin
  25.     bpy_object.bound_properties.volume = bound_xml.volume
  26.  
  27.  
  28. def apply_bound_child_properties(bpy_object: bpy.types.Object, bound_xml: Bound):
  29.     """Applys transforms and flags from bounds that are children of composites to the specified bpy_object."""
  30.     bpy_object.matrix_world = bound_xml.composite_transform.transposed()
  31.     assign_composite_flags(bpy_object.composite_flags1,
  32.                            bound_xml.composite_flags1)
  33.     assign_composite_flags(bpy_object.composite_flags2,
  34.                            bound_xml.composite_flags2)
  35.  
  36.  
  37. # TODO: Test
  38. def assign_composite_flags(bpy_flags: BoundFlags, xml_flags: list[str]):
  39.     """Assigns the list of xml_flags to the provided BoundFlags data block."""
  40.     for flag in xml_flags:
  41.         flag_lower = flag.lower()
  42.         if hasattr(bpy_flags, flag_lower):
  43.             setattr(bpy_flags, flag_lower, True)
  44.  
  45.  
  46. # TODO: Make import and operators create bound shapes the same way using the same function
  47. def bound_to_bpy_object(bound_xml: Bound, name: str) -> bpy.types.Object:
  48.     """Create a bpy mesh object from generic cwxml bound object."""
  49.     # Create blender objects
  50.     mesh = bpy.meshes.new(name)
  51.     bpy_object = bpy.data.objects.new(name, mesh)
  52.  
  53.     # Create materials
  54.     mat_index = bpy_object.material_index
  55.     try:
  56.         # TODO: Remove try except, just prevent error from happening in the first place
  57.         # TODO: What about the material properties?
  58.         mat = create_collision_material_from_index(mat_index)
  59.         mesh.materials.append(mat)
  60.     except IndexError:
  61.         print(
  62.             f"Warning: Failed to set materials for {name}. Material index {mat_index} does not exist in collisionmats list.")
  63.  
  64.     apply_bound_properties(bpy_object, bound_xml)
  65.  
  66.     bpy.context.collection.objects.link(bpy_object)
  67.  
  68.     return bpy_object
  69.  
  70.  
  71. def bound_to_bpy_empty(bound_xml: Bound, name: str) -> bpy.types.Object:
  72.     """Create a bpy empty from generic cwxml Bound object with bound properties applied."""
  73.     bpy_empty = bpy.data.objects.new(name, None)
  74.     bpy_empty.empty_display_size = 0
  75.     apply_bound_properties(bpy_empty, bound_xml)
  76.  
  77.     bpy.context.collection.objects.link(bpy_empty)
  78.  
  79.     return bpy_empty
  80.  
  81.  
  82. def bound_box_to_bpy(box_xml: BoundBox) -> bpy.types.Object:
  83.     """Create a bpy mesh object from a cwxml BoundBox object."""
  84.     sollum_type = SollumType.BOUND_BOX
  85.  
  86.     box_bpy = bound_to_bpy_object(
  87.         box_xml, name=SOLLUMZ_UI_NAMES[sollum_type])
  88.     box_bpy.sollum_type
  89.  
  90.     apply_bound_child_properties(box_bpy, box_xml)
  91.  
  92.     # TODO: Make sure absolute() works as expected
  93.     box_bpy.bound_dimensions = absolute(
  94.         box_xml.box_max - box_xml.box_min)
  95.     box_bpy.data.transform(Matrix.Translation(box_xml.box_center))
  96.  
  97.     return box_bpy
  98.  
  99.  
  100. def bound_sphere_to_bpy(sphere_xml: BoundSphere) -> bpy.types.Object:
  101.     """Create a bpy mesh object from a cwxml BoundSphere object."""
  102.     sollum_type = SollumType.BOUND_SPHERE
  103.  
  104.     sphere_bpy = bound_to_bpy_object(
  105.         sphere_xml, name=SOLLUMZ_UI_NAMES[sollum_type])
  106.     sphere_bpy.sollum_type = sollum_type
  107.  
  108.     apply_bound_child_properties(sphere_bpy, sphere_xml)
  109.  
  110.     sphere_bpy.bound_radius = sphere_xml.sphere_radius
  111.  
  112.     return sphere_bpy
  113.  
  114.  
  115. def bound_capsule_to_bpy(capsule_xml: BoundCapsule) -> bpy.types.Object:
  116.     """Create a bpy mesh object from a cwxml BoundCapsule object."""
  117.     sollum_type = SollumType.BOUND_CAPSULE
  118.  
  119.     capsule_bpy = bound_to_bpy_object(
  120.         capsule_xml, name=SOLLUMZ_UI_NAMES[sollum_type])
  121.     capsule_bpy.sollum_type = sollum_type
  122.  
  123.     apply_bound_child_properties(capsule_bpy, capsule_xml)
  124.  
  125.     capsule_bpy.bound_length = capsule_xml.box_max.z - capsule_xml.box_min.z
  126.     capsule_bpy.bound_radius = capsule_xml.sphere_radius
  127.  
  128.     return capsule_bpy
  129.  
  130.  
  131. def bound_cylinder_to_bpy(cylinder_xml: BoundCylinder) -> bpy.types.Object:
  132.     """Create a bpy mesh object from a cwxml BoundCylinder object."""
  133.     sollum_type = SollumType.BOUND_CYLINDER
  134.  
  135.     cylinder_bpy = bound_to_bpy_object(
  136.         cylinder_xml, name=SOLLUMZ_UI_NAMES[sollum_type])
  137.     cylinder_bpy.sollum_type = sollum_type
  138.  
  139.     apply_bound_child_properties(cylinder_bpy, cylinder_xml)
  140.  
  141.     extents = cylinder_xml.box_max - cylinder_xml.box_min
  142.     cylinder_bpy.bound_length = extents.y
  143.     cylinder_bpy.bound_radius = extents.x * 0.5
  144.  
  145.     return cylinder_bpy
  146.  
  147.  
  148. def bound_disc_to_bpy(disc_xml: BoundDisc) -> bpy.types.Object:
  149.     """Create a bpy mesh object from a cwxml BoundDisc object."""
  150.     sollum_type = SollumType.BOUND_DISC
  151.  
  152.     disc_bpy = bound_to_bpy_object(
  153.         disc_xml, name=SOLLUMZ_UI_NAMES[sollum_type])
  154.     disc_bpy.sollum_type = sollum_type
  155.  
  156.     apply_bound_child_properties(disc_bpy, disc_xml)
  157.  
  158.     disc_bpy.bound_radius = disc_xml.sphere_radius
  159.     # TODO: Why is create_disc used?
  160.     create_disc(disc_bpy.data, disc_xml.sphere_radius, disc_bpy.margin * 2)
  161.  
  162.     return disc_bpy
  163.  
  164.  
  165. def material_xml_to_bpy_material(material_xml: MaterialItem) -> bpy.types.Material:
  166.     """Create a bpy material from a cwxml material"""
  167.     material_bpy = create_collision_material_from_index(material_xml.type)
  168.     # Assign collision properties
  169.     material_bpy.collision_properties.procedural_id = material_xml.procedural_id
  170.     material_bpy.collision_properties.room_id = material_xml.room_id
  171.     material_bpy.collision_properties.ped_density = material_xml.ped_density
  172.     material_bpy.collision_properties.material_color_index = material_xml.material_color_index
  173.  
  174.     # Assign flags
  175.     # TODO: Use same method as assign_composite_flags
  176.     for flag_name in CollisionMatFlags.__annotations__.keys():
  177.         if f"FLAG_{flag_name.upper()}" in material_xml.flags:
  178.             setattr(material_bpy.collision_flags, flag_name, True)
  179.  
  180.     return material_bpy
  181.  
  182.  
  183. def create_bound_polygon_mesh(vertices: list[Vector], triangles: list[Triangle], materials: list[bpy.types.Material], damaged: bool = False) -> bpy.types.Object:
  184.     """Create a bpy mesh object that contains all triangle polygons from a cwxml BoundGeometry."""
  185.     # TODO: Rename enum SollumType.BOUND_POLY_TRIANGLE2 to SollumType.BOUND_POLY_TRIANGLE_DAMAGED
  186.     sollum_type = SollumType.BOUND_POLY_TRIANGLE if not damaged else SollumType.BOUND_POLY_TRIANGLE2
  187.  
  188.     name = SOLLUMZ_UI_NAMES[sollum_type]
  189.     mesh = bpy.data.meshes.new(name)
  190.     bpy_object = bpy.data.objects.new(name, mesh)
  191.     bpy_object.sollum_type = sollum_type
  192.  
  193.     # Total vertices of mesh in order of faces
  194.     mesh_vertices = []
  195.     # Vertex indices of triangles [v1, v2, v3]
  196.     faces = []
  197.     # Index of material for each triangle
  198.     material_indices = []
  199.  
  200.     for triangle in triangles:
  201.         face_indices = []
  202.         triangle_vertices = [vertices[triangle.v1],
  203.                              vertices[triangle.v2], vertices[triangle.v3]]
  204.  
  205.         for vertex in triangle_vertices:
  206.             if not vertex in mesh_vertices:
  207.                 mesh_vertices.append(vertex)
  208.             face_indices.append(mesh_vertices.index(vertex))
  209.  
  210.         faces.append(face_indices)
  211.  
  212.         material_indices.append(triangle.material_index)
  213.  
  214.     try:
  215.         mesh.from_pydata(vertices=mesh_vertices, edges=[], faces=faces)
  216.     except:
  217.         # TODO: Message dialog
  218.         print(
  219.             f"Error encountered while creating a {name} from XML vertex data:\n{format_exc()}")
  220.         return bpy_object
  221.  
  222.     # TODO: Are extra materials added?
  223.     for material in materials:
  224.         mesh.materials.append(material)
  225.  
  226.     for index, triangle in mesh.polygons.items():
  227.         if 0 <= index < len(material_indices):
  228.             triangle.material_index = material_indices[index]
  229.  
  230.     bpy.context.collection.objects.link(bpy_object)
  231.  
  232.     return bpy_object
  233.  
  234.  
  235. def polygon_bound_to_bpy(polygon_xml: Polygon, sollum_type: SollumType, materials: list[bpy.types.Material]):
  236.     """Create a bpy mesh object from a generic cwxml bound polygon."""
  237.     name = SOLLUMZ_UI_NAMES[sollum_type]
  238.     mesh = bpy.data.meshes.new(name)
  239.     if 0 <= polygon_xml.material_index < len(materials):
  240.         mesh.materials.append(materials[polygon_xml.material_index])
  241.  
  242.     polygon_bpy = bpy.data.objects.new(name, mesh)
  243.     polygon_bpy.sollum_type = sollum_type
  244.  
  245.     bpy.context.collection.objects.link(polygon_bpy)
  246.  
  247.     return polygon_bpy
  248.  
  249.  
  250. def polygon_box_to_bpy(box_xml: Box, vertices: list[Vector], materials: list[bpy.types.Material]) -> bpy.types.Object:
  251.     """Create a bpy mesh object from a cwxml bound polygon box."""
  252.     box_bpy = polygon_bound_to_bpy(
  253.         box_xml, SollumType.BOUND_POLY_BOX, materials)
  254.  
  255.     # Four opposing corners of the box
  256.     v1 = vertices[box_xml.v1]
  257.     v2 = vertices[box_xml.v2]
  258.     v3 = vertices[box_xml.v3]
  259.     v4 = vertices[box_xml.v4]
  260.     center = (v1 + v2 + v3 + v4) * 0.25
  261.  
  262.     # Get edges from the 4 opposing corners of the box
  263.     a1 = ((v3 + v4) - (v1 + v2)) * 0.5
  264.     v2 = v1 + a1
  265.     v3 = v3 - a1
  266.     v4 = v4 - a1
  267.  
  268.     minedge = Vector((0.0001, 0.0001, 0.0001))
  269.     edge1 = max(v2 - v1, minedge)
  270.     edge2 = max(v3 - v1, minedge)
  271.     edge3 = max((v4 - v1), minedge)
  272.  
  273.     # Order edges
  274.     s1 = False
  275.     s2 = False
  276.     s3 = False
  277.     if edge2.length > edge1.length:
  278.         t1 = edge1
  279.         edge1 = edge2
  280.         edge2 = t1
  281.         s1 = True
  282.     if edge3.length > edge1.length:
  283.         t1 = edge1
  284.         edge1 = edge3
  285.         edge3 = t1
  286.         s2 = True
  287.     if edge3.length > edge2.length:
  288.         t1 = edge2
  289.         edge2 = edge3
  290.         edge3 = t1
  291.         s3 = True
  292.  
  293.     # Ensure all edge vectors are perpendicular to each other
  294.     b1 = edge1.normalized()
  295.     b2 = edge2.normalized()
  296.     b3 = b1.cross(b2).normalized()
  297.     b2 = b1.cross(b3).normalized()
  298.     edge2 = b2 * edge2.dot(b2)
  299.     edge3 = b3 * edge3.dot(b3)
  300.  
  301.     # Unswap edges
  302.     if s3 == True:
  303.         t1 = edge2
  304.         edge2 = edge3
  305.         edge3 = t1
  306.     if s2 == True:
  307.         t1 = edge1
  308.         edge1 = edge3
  309.         edge3 = t1
  310.     if s1 == True:
  311.         t1 = edge1
  312.         edge1 = edge2
  313.         edge2 = t1
  314.  
  315.     # Construct transformation matrix using edge vectors
  316.     mat = Matrix()
  317.     mat[0] = edge1.x, edge2.x, edge3.x, center.x
  318.     mat[1] = edge1.y, edge2.y, edge3.y, center.y
  319.     mat[2] = edge1.z, edge2.z, edge3.z, center.z
  320.  
  321.     # Create unit cube (1x1x1)
  322.     create_box(box_bpy.data, size=1)
  323.     box_bpy.matrix_basis = mat
  324.  
  325.     return box_bpy
  326.  
  327.  
  328. def polygon_sphere_to_bpy(sphere_xml: Sphere, vertices: list[Vector], materials: list[bpy.types.Material]) -> bpy.types.Object:
  329.     """Create a bpy mesh object from a cwxml polygon sphere."""
  330.     sphere_bpy = polygon_bound_to_bpy(
  331.         sphere_xml, SollumType.BOUND_POLY_SPHERE, materials)
  332.  
  333.     sphere_bpy.bound_radius = sphere_xml.radius
  334.     sphere_bpy.location = vertices[sphere_xml.v]
  335.  
  336.     return sphere_bpy
  337.  
  338.  
  339. def polygon_capsule_to_bpy(capsule_xml: Capsule, vertices: list[Vector], materials: list[bpy.types.Material]) -> bpy.types.Object:
  340.     """Create a bpy mesh object from a cwxml polygon capsule."""
  341.     capsule_bpy = polygon_bound_to_bpy(
  342.         capsule_xml, SollumType.BOUND_POLY_CAPSULE, materials)
  343.     v1 = vertices[capsule_xml.v1]
  344.     v2 = vertices[capsule_xml.v2]
  345.     rot = get_direction_of_vectors(v1, v2)
  346.     capsule_bpy.bound_radius = capsule_xml.radius * 2
  347.     capsule_bpy.bound_length = (
  348.         (v1 - v2).length + (capsule_xml.radius * 2)) / 2
  349.  
  350.     capsule_bpy.location = (v1 + v2) / 2
  351.     capsule_bpy.rotation_euler = rot
  352.  
  353.     return capsule_bpy
  354.  
  355.  
  356. def polygon_cylinder_to_bpy(cylinder_xml: Cylinder, vertices: list[Vector], materials: list[bpy.types.Material]) -> bpy.types.Object:
  357.     """Create a bpy mesh object from a cwxml polygon cylinder."""
  358.     cylinder_bpy = polygon_bound_to_bpy(
  359.         cylinder_xml, SollumType.BOUND_POLY_CYLINDER, materials)
  360.     v1 = vertices[cylinder_xml.v1]
  361.     v2 = vertices[cylinder_xml.v2]
  362.     rot = get_direction_of_vectors(v1, v2)
  363.     cylinder_bpy.bound_radius = cylinder_xml.radius
  364.     cylinder_bpy.bound_length = get_distance_of_vectors(v1, v2)
  365.     cylinder_bpy.matrix_world = Matrix()
  366.  
  367.     cylinder_bpy.location = (v1 + v2) / 2
  368.     cylinder_bpy.rotation_euler = rot
  369.  
  370.     return cylinder_bpy
  371.  
  372.  
  373. # TODO: Needs testing after refactor
  374. def bound_geometry_to_bpy(geometry_xml: Union[BoundGeometryBVH, BoundGeometry], bvh=True) -> bpy.types.Object:
  375.     """Create a bpy empty containing the Bound Polygons as bpy mesh objects from a cwxml BoundGeometry/BoundGeometryBVH object."""
  376.     geometry_bpy = bound_to_bpy_empty(
  377.         geometry_xml, SOLLUMZ_UI_NAMES[SollumType.BOUND_GEOMETRYBVH if bvh else SollumType.BOUND_GEOMETRY])
  378.     apply_bound_child_properties(geometry_bpy, geometry_xml)
  379.  
  380.     # Create materials
  381.     materials = []
  382.     for material_xml in geometry_xml.materials:
  383.         materials.append(material_xml_to_bpy_material(material_xml))
  384.  
  385.     triangle_polygons = [
  386.         polygon for polygon in geometry_xml.polygons if type(polygon) == Triangle]
  387.  
  388.     # Create a single mesh object from all the bound polygon triangles
  389.     bound_polygon_mesh = create_bound_polygon_mesh(
  390.         vertices=geometry_xml.vertices, triangles=triangle_polygons, materials=materials)
  391.     bound_polygon_mesh.parent = geometry_bpy
  392.  
  393.     # BoundGeometry collisions can have a second vertex list that represents the vertices in a "damaged" state
  394.     bound_polygon_mesh_damaged = None
  395.     if not bvh:
  396.         bound_polygon_mesh_damaged = create_bound_polygon_mesh(
  397.             vertices=geometry_xml.vertices_2, triangles=triangle_polygons, materials=materials)
  398.         bound_polygon_mesh_damaged.parent = geometry_bpy
  399.  
  400.     # Apply vertex colors to the mesh
  401.     if len(geometry_xml.vertex_colors) > 0:
  402.         create_vertexcolor_layer(mesh=bound_polygon_mesh.data, num=0,
  403.                                  name="Color Layer", colors=geometry_xml.vertex_colors)
  404.  
  405.     for polygon in geometry_xml.polygons:
  406.         polygon_bpy = None
  407.         # TODO: Rename polygon classes and find way to avoid if statements
  408.         if type(polygon) == Box:
  409.             polygon_bpy = polygon_box_to_bpy(
  410.                 polygon, geometry_xml.vertices, bound_polygon_mesh.data.materials)
  411.         elif type(polygon) == Sphere:
  412.             polygon_bpy = polygon_sphere_to_bpy(
  413.                 polygon, geometry_xml.vertices, bound_polygon_mesh.data.materials)
  414.         elif type(polygon) == Cylinder:
  415.             polygon_bpy = polygon_cylinder_to_bpy(
  416.                 polygon, geometry_xml.vertices, bound_polygon_mesh.data.materials)
  417.         elif type(polygon) == Capsule:
  418.             polygon_bpy = polygon_capsule_to_bpy(
  419.                 polygon, geometry_xml.vertices, bound_polygon_mesh.data.materials)
  420.         else:
  421.             # TODO: Message dialog
  422.             print(
  423.                 f"Failed to create objects for '{polygon_bpy}': {type(polygon).__name__} is an invalid bound type!")
  424.             continue
  425.  
  426.         polygon_bpy.parent = geometry_bpy
  427.  
  428.     # TODO: Investigate
  429.     # GeometryBVHs that have an UnkType of 2 have the triangle vertices offsetted by GeometryCenter.
  430.     # Otherwise, the entire BVH is offsetted.
  431.     if geometry_xml.unk_type == 2:
  432.         bound_polygon_mesh.location = geometry_xml.geometry_center
  433.         if bound_polygon_mesh_damaged is not None:
  434.             bound_polygon_mesh_damaged.location += geometry_xml.geometry_center
  435.     else:
  436.         geometry_bpy.location += geometry_xml.geometry_center
  437.  
  438.     return geometry_bpy
  439.  
  440.  
  441. def composite_to_bpy(composite_xml: BoundComposite, name: str) -> bpy.types.Object:
  442.     """Create a bpy empty object from a cwxml BoundComposite object."""
  443.     # Create composite bpy empty
  444.     composite_bpy = bound_to_bpy_empty(composite_xml, name)
  445.  
  446.     # Create composite children
  447.     for i, child in enumerate(composite_xml.children):
  448.         child_bpy = None
  449.  
  450.         # TODO: Figure out how to do this without all the if statements
  451.         if isinstance(child, BoundBox):
  452.             child_bpy = bound_box_to_bpy(child)
  453.         elif isinstance(child, BoundSphere):
  454.             child_bpy = bound_sphere_to_bpy(child)
  455.         elif isinstance(child, BoundCylinder):
  456.             child_bpy = bound_cylinder_to_bpy(child)
  457.         elif isinstance(child, BoundCapsule):
  458.             child_bpy = bound_capsule_to_bpy(child)
  459.         elif isinstance(child, BoundDisc):
  460.             child_bpy = bound_disc_to_bpy(child)
  461.         elif isinstance(child, BoundCloth):
  462.             # NOTE: Not yet implemented
  463.             child_bpy = bound_to_bpy_empty(child)
  464.         elif isinstance(child, BoundGeometryBVH):
  465.             child_bpy = bound_geometry_to_bpy(child)
  466.         elif isinstance(child, BoundGeometry):
  467.             child_bpy = bound_geometry_to_bpy(child)
  468.         else:
  469.             print(
  470.                 f"Failed to create objects for '{child}': {type(child).__name__} is an invalid bound type!")
  471.             continue
  472.  
  473.         # TODO: Alternative method? If not, document this
  474.         child_bpy.creation_index = i
  475.         child_bpy.parent = composite_bpy
  476.  
  477.     return composite_bpy
  478.  
  479.  
  480. def import_ybn(filepath: str) -> bpy.types.Object:
  481.     """Import collisions into the scene from a .ybn.xml file."""
  482.     ybn_xml = YBN.from_xml_file(filepath)
  483.  
  484.     if ybn_xml.composite is None:
  485.         # TODO: Show message dialog
  486.         print("No composite present in file")
  487.         return
  488.  
  489.     return composite_to_bpy(composite_xml=ybn_xml.composite, name=os.path.basename(
  490.         filepath.replace(YBN.file_extension, '')))
  491.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement