|
| 1 | +import bpy |
| 2 | +import bmesh |
| 3 | +import numpy |
| 4 | +from os.path import splitext, basename |
| 5 | +from bpy_extras.io_utils import ImportHelper |
| 6 | +from bpy.props import StringProperty, BoolProperty, EnumProperty |
| 7 | +from bpy.types import Operator |
| 8 | +from .kaitai_lithtech_dat_struct import LithtechDat |
| 9 | +from mathutils import Vector |
| 10 | + |
| 11 | + |
| 12 | +def createRenderNode(parent, rn_name, rn): |
| 13 | + """Create a collection for the render node and all objects within |
| 14 | + Positional arguments: |
| 15 | + parent: parent collection the node gets appended to |
| 16 | + rn_name: name for the render node collection |
| 17 | + rn: the data for the render node from type LithtechDat.RenderNode |
| 18 | + """ |
| 19 | + # renderNode |
| 20 | + renderNode_collection = bpy.data.collections.new(rn_name) |
| 21 | + # |
| 22 | + # render nodes can no vertices |
| 23 | + if 0 < rn.num_vertices: |
| 24 | + # |
| 25 | + vertices = [[*map(lambda x: x*0.01, [v.v_pos.x, v.v_pos.z, v.v_pos.y])] |
| 26 | + for v in rn.vertices] |
| 27 | + triangles = [tri.t for tri in rn.triangles] |
| 28 | + tri_counts = [s.triangle_count for s in rn.sections] |
| 29 | + # |
| 30 | + for i, s in enumerate(rn.sections): |
| 31 | + t_from = sum(tri_counts[:i]) |
| 32 | + t_till = t_from + tri_counts[i] |
| 33 | + # |
| 34 | + # this steps undercuts the polyIndex |
| 35 | + section_tris = [t.t for t in rn.triangles[t_from:t_till]] |
| 36 | + # dont forget about polyIndices |
| 37 | + section_tris_poly_indices = [ |
| 38 | + t.poly_index for t in rn.triangles[t_from:t_till]] |
| 39 | + # preparation for next step |
| 40 | + section_tris = numpy.asarray(section_tris).reshape(-1) |
| 41 | + section_vert_indices = numpy.unique(section_tris) |
| 42 | + # find for each triangle the new correct indices to the separated vertices |
| 43 | + section_tris = numpy.asarray( |
| 44 | + [(numpy.where(section_vert_indices == t))[0][0] for t in section_tris]) |
| 45 | + # reshape back to triangle |
| 46 | + section_tris = section_tris.reshape( |
| 47 | + len(section_tris) // 3, 3).tolist() |
| 48 | + # |
| 49 | + # now we separate the verticies from the render node to the section |
| 50 | + # one bmesh for each section |
| 51 | + bm = bmesh.new() |
| 52 | + for i in section_vert_indices: |
| 53 | + bm.verts.new([ |
| 54 | + rn.vertices[i].v_pos.x * 0.01, |
| 55 | + rn.vertices[i].v_pos.z * 0.01, |
| 56 | + rn.vertices[i].v_pos.y * 0.01 |
| 57 | + ]) |
| 58 | + continue |
| 59 | + bm.verts.ensure_lookup_table() |
| 60 | + # for debugging (does the same thing) |
| 61 | + # section_vertices = [[rn.vertices[i].v_pos.x * 0.01,rn.vertices[i].v_pos.z * 0.01,rn.vertices[i].v_pos.y * 0.01] for j in section_vert_indices] |
| 62 | + # create the faces from triangles |
| 63 | + for t, poly_index in zip(section_tris, section_tris_poly_indices): |
| 64 | + verts = [bm.verts[i] for i in t] |
| 65 | + # face can already exist |
| 66 | + try: |
| 67 | + face = bm.faces.new(verts) |
| 68 | + bm.faces.ensure_lookup_table() |
| 69 | + face.material_index = poly_index |
| 70 | + except: |
| 71 | + pass |
| 72 | + continue |
| 73 | + # |
| 74 | + # still missing the texture from the section data can be empty if StrWithLen2 has length 0 |
| 75 | + # texture_name0 = s.texture_name[0].data |
| 76 | + # texture_name1 = s.texture_name[1].data |
| 77 | + # |
| 78 | + m = bpy.data.meshes.new(f"{rn_name}_Section_{i:04}") |
| 79 | + bm.to_mesh(m) |
| 80 | + o = bpy.data.objects.new(f"{rn_name}_Object_{i:04}", m) |
| 81 | + renderNode_collection.objects.link(o) |
| 82 | + continue |
| 83 | + pass |
| 84 | + # no vertices available only create empty object |
| 85 | + else: |
| 86 | + o = bpy.data.objects.new(f"{rn_name}_Object", None) |
| 87 | + o.empty_display_type = 'CUBE' |
| 88 | + o.location = Vector((rn.v_center.x, rn.v_center.z, rn.v_center.y)) |
| 89 | + o.empty_display_size = max( |
| 90 | + [rn.v_half_dims.x, rn.v_half_dims.z, rn.v_half_dims.y]) |
| 91 | + renderNode_collection.objects.link(o) |
| 92 | + pass |
| 93 | + parent.children.link(renderNode_collection) |
| 94 | + return |
| 95 | + |
| 96 | +# requires createRenderNode |
| 97 | + |
| 98 | + |
| 99 | +def createWMRenderNode(parent, wmrn): |
| 100 | + """create a World Model Render Node |
| 101 | + Positional Arguments: |
| 102 | + parent: collection the render node is going to be append to |
| 103 | + wmrn: dat_importer.lithtech_dat.LithtechDat.RenderNode node to be created |
| 104 | + """ |
| 105 | + node_collection = bpy.data.collections.new(f"WMRN_{wmrn.name.data}") |
| 106 | + for i, render_node in enumerate(wmrn.render_nodes): |
| 107 | + createRenderNode( |
| 108 | + node_collection, f"{wmrn.name.data}_RN_{i}", render_node) |
| 109 | + continue |
| 110 | + parent.children.link(node_collection) |
| 111 | + return |
| 112 | + |
| 113 | +# requires createRenderNode |
| 114 | + |
| 115 | + |
| 116 | +def createRenderNodes(parent, world): |
| 117 | + """create Render Nodes |
| 118 | + Positional Arguments: |
| 119 | + parent: collection the render node group is append to |
| 120 | + world: dat_importer.lithtech_dat.LithtechDat imported map file |
| 121 | + """ |
| 122 | + render_nodes = bpy.data.collections.new("RenderNodes") |
| 123 | + for i, render_node in enumerate(world.render_data.render_nodes): |
| 124 | + createRenderNode(render_nodes, f"RenderNode_{i:03}", render_node) |
| 125 | + continue |
| 126 | + parent.children.link(render_nodes) |
| 127 | + return |
| 128 | + |
| 129 | +# requires createRenderNode |
| 130 | +# requires createWMRenderNode |
| 131 | + |
| 132 | + |
| 133 | +def createWMRenderNodes(parent, world): |
| 134 | + """Create the World Model Render Nodes |
| 135 | + Positional Arguments: |
| 136 | + parent: the parent collection the world model render node group is append to |
| 137 | + world: dat_importer.lithtech_dat.LithtechDat imported map file |
| 138 | + """ |
| 139 | + wm_render_nodes = bpy.data.collections.new("WMRenderNodes") |
| 140 | + for node in world.render_data.world_model_render_nodes: |
| 141 | + createWMRenderNode(wm_render_nodes, node) |
| 142 | + continue |
| 143 | + parent.children.link(wm_render_nodes) |
| 144 | + return |
| 145 | + |
| 146 | + |
| 147 | +def createPhysicsData(parent, world): |
| 148 | + """Create the physics/ collision data |
| 149 | + Positional Arguments: |
| 150 | + parent: collection the data physics collection is append to |
| 151 | + world: dat_importer.lithtech_dat.LithtechDat imported map file |
| 152 | + """ |
| 153 | + physics = vertigo.collision_data.polygons |
| 154 | + poly_vertices = [] |
| 155 | + poly_triangles = [] |
| 156 | + index = 0 |
| 157 | + scale = 0.01 |
| 158 | + for poly in physics: |
| 159 | + poly_triangles.append([]) |
| 160 | + for vec in poly.vertices_pos: |
| 161 | + poly_vertices.append([vec.x * scale, vec.z * scale, vec.y * scale]) |
| 162 | + poly_triangles[-1].append(index) |
| 163 | + index += 1 |
| 164 | + continue |
| 165 | + continue |
| 166 | + m = bpy.data.meshes.new("CollisionData") |
| 167 | + m.from_pydata(poly_vertices, [], poly_triangles) |
| 168 | + o = bpy.data.objects.new("CollisionData", m) |
| 169 | + parent.objects.link(o) |
| 170 | + return |
| 171 | + |
| 172 | + |
| 173 | +def read_some_data(C, filepath, use_some_setting): |
| 174 | + print("running read_some_data...") |
| 175 | + dat = LithtechDat.from_file(filepath) |
| 176 | + name = basename(splitext(filepath)[0]) |
| 177 | + map = bpy.data.collections.new(name) |
| 178 | + createRenderNodes(map, dat) |
| 179 | + createWMRenderNodes(map, dat) |
| 180 | + C.collection.children.link(map) |
| 181 | + |
| 182 | + return {'FINISHED'} |
| 183 | + |
| 184 | + |
| 185 | +class ImportLithtechDat(Operator, ImportHelper): |
| 186 | + """Import a Lithtech DAT map file |
| 187 | + This importer is developed and tested for Combat Arms maps. |
| 188 | + It should work for all Lithtech engine map files, but no guarantees! |
| 189 | + If you encounter any issue feel free to open a issue on GitHub. |
| 190 | + """ |
| 191 | + # # important since its how bpy.ops.import_scene.lithtech_dat is constructed |
| 192 | + bl_idname = "import_scene.lithtech_dat" |
| 193 | + bl_label = "Import Lithtech DAT map (.dat)" |
| 194 | + |
| 195 | + # ImportHelper mixin class uses this |
| 196 | + filename_ext = ".dat" |
| 197 | + |
| 198 | + filter_glob: StringProperty( |
| 199 | + default="*.dat", |
| 200 | + options={'HIDDEN'}, |
| 201 | + maxlen=255, # Max internal buffer length, longer would be clamped. |
| 202 | + ) |
| 203 | + |
| 204 | + # List of operator properties, the attributes will be assigned |
| 205 | + # to the class instance from the operator settings before calling. |
| 206 | + use_setting: BoolProperty( |
| 207 | + name="Example Boolean", |
| 208 | + description="Example Tooltip", |
| 209 | + default=True, |
| 210 | + ) |
| 211 | + |
| 212 | + type: EnumProperty( |
| 213 | + name="Example Enum", |
| 214 | + description="Choose between two items", |
| 215 | + items=( |
| 216 | + ('OPT_A', "First Option", "Description one"), |
| 217 | + ('OPT_B', "Second Option", "Description two"), |
| 218 | + ), |
| 219 | + default='OPT_A', |
| 220 | + ) |
| 221 | + |
| 222 | + def execute(self, context): |
| 223 | + return read_some_data(context, self.filepath, self.use_setting) |
0 commit comments