Skip to content

Commit

Permalink
Updated exported and importer to export/import High, Medium and Low L…
Browse files Browse the repository at this point in the history
…OD models
  • Loading branch information
Jonathan-Greve committed Dec 23, 2023
1 parent c031bb9 commit c4dac5e
Show file tree
Hide file tree
Showing 2 changed files with 150 additions and 40 deletions.
50 changes: 47 additions & 3 deletions SourceFiles/model_exporter.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> indices;

// Indices for medium and low quality LODs.
std::vector<int> indices_med;
std::vector<int> 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<int> texture_indices;
Expand Down Expand Up @@ -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},
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
140 changes: 103 additions & 37 deletions blender_addons/model_import_addon/import_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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', [])
Expand All @@ -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:
Expand All @@ -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'}

Expand All @@ -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')
Expand Down

0 comments on commit c4dac5e

Please sign in to comment.