diff --git a/SourceFiles/model_exporter.h b/SourceFiles/model_exporter.h index 80a4e97..000553c 100644 --- a/SourceFiles/model_exporter.h +++ b/SourceFiles/model_exporter.h @@ -67,8 +67,16 @@ struct gwmb_submodel { // Faces are in counter-clockwise order. // Each consecutive 3 indices represents a face so: indices.size() % 3 == 0 and indices.size() > 0. + // These indices are used for the "High" (best quality) LOD. std::vector indices; + // Indices for medium and low quality LODs. + std::vector indices_med; + std::vector indices_low; + + // Flags telling us if the model has medium or low LOD indices. + bool has_med_lod; + bool has_low_lod; // The index of the texture to use for each UV map. The vector has length: num_texcoords std::vector texture_indices; @@ -198,6 +206,10 @@ namespace nlohmann { j = json{ {"vertices", s.vertices}, {"indices", s.indices}, + {"indices_med", s.indices_med}, + {"indices_low", s.indices_low}, + {"has_med_lod", s.has_med_lod}, + {"has_low_lod", s.has_low_lod}, {"texture_indices", s.texture_indices}, {"texture_uv_map_index", s.texture_uv_map_index}, {"texture_blend_flags", s.texture_blend_flags}, @@ -208,6 +220,10 @@ namespace nlohmann { static void from_json(const json& j, gwmb_submodel& s) { j.at("vertices").get_to(s.vertices); j.at("indices").get_to(s.indices); + j.at("indices_med").get_to(s.indices_med); + j.at("indices_low").get_to(s.indices_low); + j.at("has_med_lod").get_to(s.has_med_lod); + j.at("has_low_lod").get_to(s.has_low_lod); j.at("texture_indices").get_to(s.texture_indices); j.at("texture_uv_map_index").get_to(s.texture_uv_map_index); j.at("texture_blend_flags").get_to(s.texture_blend_flags); @@ -364,13 +380,41 @@ class model_exporter { gwmb_submodel_i.vertices[j] = new_gwmb_vertex; } - // Add indices to gwmb_submodel - gwmb_submodel_i.indices.resize(submodel.indices.size()); - for (int j = 0; j < submodel.indices.size(); j++) { + // Add High LOD indices to gwmb_submodel + gwmb_submodel_i.indices.resize(submodel.num_indices0); + for (int j = 0; j < submodel.num_indices0; j++) { const auto index = submodel.indices[j]; gwmb_submodel_i.indices[j] = index; } + if (submodel.num_indices0 != submodel.num_indices1) { + gwmb_submodel_i.has_med_lod = true; + + gwmb_submodel_i.indices_med.resize(submodel.num_indices1); + int index_offset = submodel.num_indices0; + for (int j = 0; j < submodel.num_indices1; j++) { + const auto index = submodel.indices[index_offset+j]; + gwmb_submodel_i.indices_med[j] = index; + } + } + else { + gwmb_submodel_i.has_med_lod = false; + } + + if (submodel.num_indices0 != submodel.num_indices2 && submodel.num_indices1 != submodel.num_indices2) { + gwmb_submodel_i.has_low_lod = true; + + int index_offset = gwmb_submodel_i.has_med_lod ? submodel.num_indices0 + submodel.num_indices1 : submodel.num_indices0; + gwmb_submodel_i.indices_low.resize(submodel.num_indices2); + for (int j = 0; j < submodel.num_indices2; j++) { + const auto index = submodel.indices[index_offset + j]; + gwmb_submodel_i.indices_low[j] = index; + } + } + else { + gwmb_submodel_i.has_low_lod = false; + } + AMAT_file amat_file; if (model_file.AMAT_filenames_chunk.texture_filenames.size() > 0) { int sub_model_index = geometry_chunk.models[i].unknown; diff --git a/blender_addons/model_import_addon/import_model.py b/blender_addons/model_import_addon/import_model.py index e6e5ba1..146ae11 100644 --- a/blender_addons/model_import_addon/import_model.py +++ b/blender_addons/model_import_addon/import_model.py @@ -3,6 +3,24 @@ import os import numpy as np + +def hide_collection_in_view_layer(collection, view_layer): + # Recursively search for the matching LayerCollection + def recurse(layer_collections, target_collection): + for layer_collection in layer_collections.children: + if layer_collection.collection == target_collection: + return layer_collection + found = recurse(layer_collection, target_collection) + if found: + return found + return None + + layer_collection = recurse(view_layer.layer_collection, collection) + if layer_collection: + layer_collection.hide_viewport = True + + + def set_metric_space(): bpy.context.scene.unit_settings.system = 'METRIC' bpy.context.scene.unit_settings.length_unit = 'METERS' @@ -437,34 +455,53 @@ def create_mesh_from_json(context, directory, filename): # Ensure a collection for the model hash exists under the GWMB_Models collection model_collection = ensure_collection(context, model_hash, parent_collection=gwmb_collection) + high_collection = ensure_collection(context, "LOD_HIGH", parent_collection=model_collection) # Set the model's hash collection as the active collection layer_collection = bpy.context.view_layer.layer_collection.children[gwmb_collection.name].children[ model_collection.name] + bpy.context.view_layer.active_layer_collection = layer_collection for idx, submodel in enumerate(data.get('submodels', [])): pixel_shader_type = submodel['pixel_shader_type'] vertices_data = submodel.get('vertices', []) - indices = submodel.get('indices', []) - - # Swap axis to match Blenders coordinate system. - # Also scale the vertices to be inches rather than meters. 1 Inch = 0.0254m. (Guild Wars uses GW Inches) vertices = [swap_axes(v['pos']) for v in vertices_data] + + has_med_lod = submodel['has_med_lod'] + has_low_lod = submodel['has_low_lod'] + + if (has_low_lod): + low_collection = ensure_collection(context, "LOD_LOW", parent_collection=model_collection) + indices_low = submodel.get('indices_low', []) + faces_low = [tuple(reversed(indices_low[i:i + 3])) for i in range(0, len(indices_low), 3)] + + if (has_med_lod): + med_collection = ensure_collection(context, "LOD_MEDIUM", parent_collection=model_collection) + indices_med = submodel.get('indices_med', []) + faces_med = [tuple(reversed(indices_med[i:i + 3])) for i in range(0, len(indices_med), 3)] + + indices_high = submodel.get('indices', []) + faces_high = [tuple(reversed(indices_high[i:i + 3])) for i in range(0, len(indices_high), 3)] texture_blend_flags = submodel.get('texture_blend_flags', []) # Normals normals = [swap_axes(v['normal']) if v['has_normal'] else (0, 0, 1) for v in vertices_data] - # Faces - faces = [tuple(reversed(indices[i:i + 3])) for i in range(0, len(indices), 3)] - - mesh = bpy.data.meshes.new(name="{}_submodel_{}".format(model_hash, idx)) - mesh.from_pydata(vertices, [], faces) - - # Set Normals - mesh.normals_split_custom_set_from_vertices(normals) + mesh_high = bpy.data.meshes.new(name="{}_submodel_{}_highLOD".format(model_hash, idx)) + mesh_high.from_pydata(vertices, [], faces_high) + mesh_high.normals_split_custom_set_from_vertices(normals) + + if (has_med_lod): + mesh_med = bpy.data.meshes.new(name="{}_submodel_{}_mediumLOD".format(model_hash, idx)) + mesh_med.from_pydata(vertices, [], faces_med) + mesh_med.normals_split_custom_set_from_vertices(normals) + + if (has_low_lod): + mesh_low = bpy.data.meshes.new(name="{}_submodel_{}_lowLOD".format(model_hash, idx)) + mesh_low.from_pydata(vertices, [], faces_low) + mesh_low.normals_split_custom_set_from_vertices(normals) # Create material for the submodel texture_indices = submodel.get('texture_indices', []) @@ -475,21 +512,36 @@ def create_mesh_from_json(context, directory, filename): # We also get the correct texture types for the selected textures above texture_types = [all_texture_types[i] for i in texture_indices] - uv_map_names = [] + UVs = [] for uv_index, tex_index in enumerate(uv_map_indices): - uv_layer_name = f"UV_{uv_index}" - uv_map_names.append(uv_layer_name) - uv_layer = mesh.uv_layers.new(name=uv_layer_name) uvs = [] for vertex in vertices_data: # In DirectX 11 (DX11), used by the Guild Wars Map Browser, the UV coordinate system originates at the top left with (0,0), meaning the V coordinate increases downwards. # In Blender, however, the UV coordinate system originates at the bottom left with (0,0), so the V coordinate increases upwards. # Therefore, to correctly map DX11 UVs to Blender's UV system, we subtract the V value from 1, effectively flipping the texture on the vertical axis. - uvs.append( - (vertex['texture_uv_coords'][tex_index]['x'], 1 - vertex['texture_uv_coords'][tex_index]['y'])) - - for i, loop in enumerate(mesh.loops): - uv_layer.data[i].uv = uvs[loop.vertex_index] + uvs.append((vertex['texture_uv_coords'][tex_index]['x'], 1 - vertex['texture_uv_coords'][tex_index]['y'])) + UVs.append(uvs) + + uv_map_names = [] + for uv_index in range(len(uv_map_indices)): + uv_layer_name = f"UV_{uv_index}" + uv_map_names.append(uv_layer_name) + uvs = UVs[uv_index] + + uv_layer_high = mesh_high.uv_layers.new(name=uv_layer_name) + + for i, loop in enumerate(mesh_high.loops): + uv_layer_high.data[i].uv = uvs[loop.vertex_index] + + if (has_med_lod): + uv_layer_med = mesh_med.uv_layers.new(name=uv_layer_name) + for i, loop in enumerate(mesh_med.loops): + uv_layer_med.data[i].uv = uvs[loop.vertex_index] + + if (has_low_lod): + uv_layer_low = mesh_low.uv_layers.new(name=uv_layer_name) + for i, loop in enumerate(mesh_low.loops): + uv_layer_low.data[i].uv = uvs[loop.vertex_index] material = None if pixel_shader_type == 6: @@ -503,18 +555,38 @@ def create_mesh_from_json(context, directory, filename): else: raise "Unknown pixel_shader_type" - mesh.update() - - obj_name = "{}_{}".format(model_hash, idx) - obj = bpy.data.objects.new(obj_name, mesh) - obj.data.materials.append(material) + mesh_high.update() + obj_high_name = "{}_{}_highLOD".format(model_hash, idx) + obj_high = bpy.data.objects.new(obj_high_name, mesh_high) + obj_high.data.materials.append(material) # Link the object to the model's collection directly - model_collection.objects.link(obj) - - # Make sure the object is also in the scene collection for visibility - context.view_layer.objects.active = obj - obj.select_set(True) + high_collection.objects.link(obj_high) + + view_layer = bpy.context.view_layer + if (has_med_lod): + mesh_med.update() + + obj_med_name = "{}_{}_medLOD".format(model_hash, idx) + obj_med = bpy.data.objects.new(obj_med_name, mesh_med) + obj_med.data.materials.append(material) + + # Link the object to the model's collection directly + med_collection.objects.link(obj_med) + + hide_collection_in_view_layer(med_collection, view_layer) + + if (has_med_lod): + mesh_med.update() + + obj_low_name = "{}_{}_lowLOD".format(model_hash, idx) + obj_low = bpy.data.objects.new(obj_low_name, mesh_low) + obj_low.data.materials.append(material) + + # Link the object to the model's collection directly + low_collection.objects.link(obj_low) + + hide_collection_in_view_layer(low_collection, view_layer) return {'FINISHED'} @@ -525,12 +597,6 @@ class IMPORT_OT_JSONMesh(bpy.types.Operator): bl_description = "Import a Guild Wars Map Browser model file (.json)" bl_options = {'REGISTER', 'UNDO'} -# directory: bpy.props.StringProperty( -# subtype='DIR_PATH', -# default="", -# description="Directory used for importing the GWMB model" -# ) - # Use a CollectionProperty to store multiple file paths files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement) directory: bpy.props.StringProperty(subtype='DIR_PATH')