Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 48 additions & 7 deletions io_scene_md3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "Quake 3 Model (.md3)",
"author": "Vitaly Verhovodov",
"version": (0, 2, 1),
"blender": (2, 72, 0),
"blender": (2, 80, 0),
"location": "File > Import-Export > Quake 3 Model",
"description": "Quake 3 Model format (.md3)",
"warning": "",
Expand All @@ -11,6 +11,15 @@
"category": "Import-Export",
}

#Reload submodules when the main script is reloaded
if "bpy" in locals():
import importlib
if "export_md3" in locals():
print("Reloading: export_md3")
importlib.reload(export_md3)
if "import_md3" in locals():
print("Reloading: import_md3")
importlib.reload(import_md3)

import bpy
import struct
Expand Down Expand Up @@ -57,18 +66,50 @@ def menu_func_import(self, context):
def menu_func_export(self, context):
self.layout.operator(ExportMD3.bl_idname, text="Quake 3 Model (.md3)")

classes = (
ImportMD3,
ExportMD3
)

# Taken from https://github.com/CGCookie/blender-addon-updater
def make_annotations(cls):
"""Converts class fields to annotations if running with Blender 2.8"""
if bpy.app.version < (2, 80):
return cls
bl_props = {k: v for k, v in cls.__dict__.items() if isinstance(v, tuple)}
if bl_props:
if '__annotations__' not in cls.__dict__:
setattr(cls, '__annotations__', {})
annotations = cls.__dict__['__annotations__']
for k, v in bl_props.items():
annotations[k] = v
delattr(cls, k)
return cls

def get_import_menu():
if hasattr(bpy.types, "TOPBAR_MT_file_import"):
return bpy.types.TOPBAR_MT_file_import #2.8
return bpy.types.INFO_MT_file_import #2.7

def get_export_menu():
if hasattr(bpy.types, "TOPBAR_MT_file_export"):
return bpy.types.TOPBAR_MT_file_export #2.8
return bpy.types.INFO_MT_file_export #2.7

def register():
bpy.utils.register_module(__name__)
bpy.types.INFO_MT_file_import.append(menu_func_import)
bpy.types.INFO_MT_file_export.append(menu_func_export)
for cls in classes:
make_annotations(cls)
bpy.utils.register_class(cls)

get_import_menu().append(menu_func_import)
get_export_menu().append(menu_func_export)

def unregister():
bpy.utils.unregister_module(__name__)
bpy.types.INFO_MT_file_import.remove(menu_func_import)
bpy.types.INFO_MT_file_export.remove(menu_func_export)
for cls in classes:
bpy.utils.unregister_class(cls)

get_import_menu().remove(menu_func_import)
get_export_menu().remove(menu_func_export)

if __name__ == "__main__":
register()
93 changes: 54 additions & 39 deletions io_scene_md3/export_md3.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import bpy
import mathutils
import operator

from . import fmt_md3 as fmt
from .utils import OffsetBytesIO
Expand All @@ -21,35 +22,20 @@ def prepare_name(name):
return name[:-4] # cut off blender's .001 .002 etc
return name


def gather_shader_info(mesh):
'Returning uvmap name, texture name list'
uv_maps = {}
for material in mesh.materials:
for texture_slot in material.texture_slots:
if (
texture_slot is None
or not texture_slot.use
or not texture_slot.uv_layer
or texture_slot.texture_coords != 'UV' or not texture_slot.texture
or texture_slot.texture.type != 'IMAGE'
):
continue
uv_map_name = texture_slot.uv_layer
if uv_map_name not in uv_maps:
uv_maps[uv_map_name] = []
# one UV map can be used by many textures
uv_maps[uv_map_name].append(prepare_name(texture_slot.texture.name))
uv_maps = [(k, v) for k, v in uv_maps.items()]
if len(uv_maps) <= 0:
print('Warning: No UV maps found, zero filling will be used')
return None, []
elif len(uv_maps) == 1:
return uv_maps[0]
else:
print('Warning: Multiple UV maps found, only one will be chosen')
return uv_maps[0]
#This is a placeholder. No shader info is exported currently
return []

def get_uv_data(mesh):
'Return the active uv data'

#2.8 note: UV is no longer bound to the material, but directly to the object.
# UV is set regardless of texture being available
# Blender render is removed in 2.8; users must use cycles and assign textures using nodes.
# This method supports both blender render in 2.7, and cycles in both.
if mesh.uv_layers.active:
return mesh.uv_layers.active.data, mesh.uv_layers.active.name
return None, None

def gather_vertices(mesh, uvmap_data=None):
md3vert_to_loop_map = []
Expand Down Expand Up @@ -90,6 +76,12 @@ def find_interval(vs, t):
assert vs[a] <= t <= vs[b]
return a, b

def matmul(a, b):
if hasattr(bpy.app, "version") and bpy.app.version >= (2, 80):
return operator.matmul(a, b) #2.8: emulate a @ b
else:
return a * b #2.7


class MD3Exporter:
def __init__(self, context):
Expand All @@ -99,6 +91,19 @@ def __init__(self, context):
def scene(self):
return self.context.scene

def get_object_mesh(self, obj):
if hasattr(obj, "evaluated_get"):
dg = self.context.evaluated_depsgraph_get()
return obj.evaluated_get(dg).to_mesh() #2.8
else:
return obj.to_mesh(self.scene, True, 'PREVIEW') #2.7

def set_active_object(self, obj):
if hasattr(self.context, "view_layer"):
self.context.view_layer.objects.active = obj #2.8
else:
self.context.scene.objects.active = obj #2.7

def pack_tag(self, name):
tag = self.scene.objects[name]
m = tag.matrix_basis.transposed()
Expand Down Expand Up @@ -140,7 +145,7 @@ def get_evaluated_vertex_co(self, frame, i):
a, b, t = self.mesh_sk_abs
co = interp(kbs[a].data[i].co, kbs[b].data[i].co, t)

co = self.mesh_matrix * co
co = matmul(self.mesh_matrix, co)
self.mesh_vco[frame].append(co)
return co

Expand All @@ -152,11 +157,11 @@ def pack_surface_vert(self, frame, i):
normal=tuple(self.mesh.loops[loop_id].normal))

def pack_surface_ST(self, i):
if self.mesh_uvmap_name is None:
if self.mesh_uvmap_data is None:
s, t = 0.0, 0.0
else:
loop_idx = self.mesh_md3vert_to_loop[i]
s, t = self.mesh.uv_layers[self.mesh_uvmap_name].data[loop_idx].uv
s, t = self.mesh_uvmap_data[loop_idx].uv
return fmt.TexCoord.pack(s, t)

def switch_frame(self, i):
Expand All @@ -165,9 +170,13 @@ def switch_frame(self, i):
def surface_start_frame(self, i):
self.switch_frame(i)

obj = self.scene.objects.active
if hasattr(self.context, "view_layer"):
obj = self.context.view_layer.objects.active #2.8
else:
obj = self.context.scene.objects.active #2.7

self.mesh_matrix = obj.matrix_world
self.mesh = obj.to_mesh(self.scene, True, 'PREVIEW')
self.mesh = self.get_object_mesh(obj)
self.mesh.calc_normals_split()

self.mesh_sk_rel = None
Expand All @@ -190,15 +199,16 @@ def surface_start_frame(self, i):

def pack_surface(self, surf_name):
obj = self.scene.objects[surf_name]
self.scene.objects.active = obj
self.set_active_object(obj)
bpy.ops.object.modifier_add(type='TRIANGULATE') # no 4-gons or n-gons
self.mesh = obj.to_mesh(self.scene, True, 'PREVIEW')
self.mesh = self.get_object_mesh(obj)
self.mesh.calc_normals_split()

self.mesh_uvmap_name, self.mesh_shader_list = gather_shader_info(self.mesh)
self.mesh_uvmap_data, uvmap_name = get_uv_data(self.mesh)
self.mesh_shader_list = gather_shader_info(self.mesh)
self.mesh_md3vert_to_loop, self.mesh_loop_to_md3vert = gather_vertices(
self.mesh,
None if self.mesh_uvmap_name is None else self.mesh.uv_layers[self.mesh_uvmap_name].data)
self.mesh_uvmap_data)

nShaders = len(self.mesh_shader_list)
nVerts = len(self.mesh_md3vert_to_loop)
Expand Down Expand Up @@ -227,8 +237,9 @@ def pack_surface(self, surf_name):
# release here, to_mesh used for every frame
bpy.ops.object.modifier_remove(modifier=obj.modifiers[-1].name)

print('Surface {}: nVerts={}{} nTris={}{} nShaders={}{}'.format(
print('Surface {}: map={} nVerts={}{} nTris={}{} nShaders={}{}'.format(
surf_name,
uvmap_name,
nVerts, ' (Too many!)' if nVerts > 4096 else '',
nTris, ' (Too many!)' if nTris > 8192 else '',
nShaders, ' (Too many!)' if nShaders > 256 else '',
Expand Down Expand Up @@ -283,8 +294,12 @@ def __call__(self, filename):
self.surfNames = []
self.tagNames = []
for o in self.scene.objects:
if o.hide: # skip hidden objects
# skip hidden objects
if hasattr(o, "hide_viewport") and o.hide_viewport: #2.8
continue
elif hasattr(o, "hide") and o.hide: #2.7
continue

if o.type == 'MESH':
self.surfNames.append(o.name)
elif o.type == 'EMPTY' and o.empty_draw_type == 'ARROWS':
Expand Down Expand Up @@ -320,4 +335,4 @@ def __call__(self, filename):
**f.getoffsets()
))
file.write(f.getvalue())
print('nFrames={} nSurfaces={}'.format(self.nFrames, len(surfaces_bin)))
print('MD3: nFrames={} nSurfaces={}'.format(self.nFrames, len(surfaces_bin)))