Skip to content

Instantly share code, notes, and snippets.

@Jaezmien
Last active January 28, 2022 08:04
Show Gist options
  • Save Jaezmien/5deca214c1c54ad54fed480ccb5286bd to your computer and use it in GitHub Desktop.
Save Jaezmien/5deca214c1c54ad54fed480ccb5286bd to your computer and use it in GitHub Desktop.
Blender Addon to objects to Milkshape 3D ASCII format
# *heavily* based on blender's export_obj.py addon + obvr's obj2ms3dascii
# for use with Blender 2.7x and below
import bpy
import os
import shutil
import mathutils
from bpy_extras import io_utils
from bpy_extras.io_utils import ExportHelper, path_reference_mode, axis_conversion, orientation_helper_factory
from bpy.props import StringProperty, BoolProperty, FloatProperty
from bpy.types import Operator
# the bug
# from pprint import pprint
# only death can set me free
IS_BLENDER_2_7 = bpy.app.version <= (2, 79, 0)
if not IS_BLENDER_2_7:
from bpy_extras import node_shader_utils
bl_info = {
"name": "Milkshape 3D ASCII",
"description": "Exports selection as Milkshape 3D ASCII",
"author": "Jaezmien Naejara",
"version": (0, 0, 4),
"blender": (2, 80, 0),
"location": "File > Import-Export",
"category": "Import-Export"
}
def format_number(num):
return "{:.6f}".format(num)
def name_compat(name):
return None if (name is None) else name.replace(' ', '_')
def mesh_triangulate(me):
import bmesh
bm = bmesh.new()
bm.from_mesh(me)
bmesh.ops.triangulate(bm, faces=bm.faces)
bm.to_mesh(me)
bm.free()
def copy_material(filepath, image):
try:
shutil.copyfile( bpy.path.abspath(image.filepath) , os.path.join(os.path.dirname(filepath), bpy.path.basename(image.filepath)) )
except shutil.SameFileError:
print(filepath + " and " + image.filepath + " is the same file.")
def get_material_info(materialObj, filepath, extract_material_path, use_material_values):
source_dir = os.path.dirname(bpy.data.filepath)
dest_dir = os.path.dirname(filepath)
material = materialObj["material"]
info = {
"ambient": [0, 0, 0, 1], # Ka
"diffuse": [1, 1, 1, 1], # Kd
"specular": [1, 1, 1, 1], # Ks
"emissive": [0, 0, 0, 1], # Ke
"shininess": 0, # Ns
"transparency": 1, # d
"color_texture": "", # map_Kd
"alpha_texture": "" # map_d
}
if IS_BLENDER_2_7:
if material:
if use_material_values:
use_mirror = material.raytrace_mirror.use and material.raytrace_mirror.reflect_factor != 0.0
if use_mirror:
info["ambient"] = list((material.raytrace_mirror.reflect_factor * material.mirror_color)[:]) + [1]
else:
info["ambient"] = [ material.ambient, material.ambient, material.ambient, 1 ]
info["diffuse"] = list((material.diffuse_intensity * material.diffuse_color)[:]) + [1]
info["specular"] = list((material.specular_intensity * material.specular_color)[:]) + [1]
info["emissive"] = list((material.emit * material.diffuse_color)[:]) + [1]
info["shininess"] = ((0.4 - material.specular_slope) / 0.0004) if (material.specular_shader == "WARDISO") else ((material.specular_hardness - 1) / 0.51)
info["transparency"] = material.alpha
face_img = materialObj["faceImage"]
if face_img:
if getattr(face_img, "filepath", None):
if extract_material_path:
info["color_texture"] = bpy.path.basename(face_img.filepath)
copy_material(filepath, face_img)
else:
info["color_texture"] = io_utils.path_reference(face_img.filepath, source_dir, dest_dir, 'AUTO', "", set(), face_img.library)
else:
face_img = None
for mtex in reversed(material.texture_slots):
if mtex and mtex.texture and mtex.texture.type == 'IMAGE':
image = mtex.texture.image
if image:
key = ""
if (mtex.use_map_color_diffuse and (face_img is None) and (mtex.use_map_warp is False) and (mtex.texture_coords != 'REFLECTION')):
key = "color_texture"
if (mtex.use_map_alpha):
key = "alpha_texture"
if len(key.strip()) > 0:
if extract_material_path:
info[key] = bpy.path.basename(image.filepath)
copy_material(filepath, image)
else:
info[key] = repr(io_utils.path_reference(image.filepath, source_dir, dest_dir, 'AUTO', "", set(), image.library))[1:-1]
else:
mat_wrap = node_shader_utils.PrincipledBSDFWrapper(material) if material else None
if mat_wrap:
if use_material_values:
if mat_wrap.metallic != 0.0:
info["ambient"] = [mat_wrap.metallic, mat_wrap.metallic, mat_wrap.metallic, 1.0]
else:
info["ambient"] = [1.0, 1.0, 1.0, 1.0]
info["diffuse"] = list(mat_wrap.base_color[:3]) + [1]
info["specular"] = [mat_wrap.specular, mat_wrap.specular, mat_wrap.specular, 1.0] # Specular
emission_strength = mat_wrap.emission_strength
info["emissive"] = [emission_strength * c for c in mat_wrap.emission_color[:3]] + [1.0]
# TODO: This is different from 2.79's shininess value. Check why and try to convert to it
spec = (1.0 - mat_wrap.roughness) * 30
info["shininess"] = (spec * spec)
image_map = {
"color_texture": "base_color_texture",
"alpha_texture": "alpha_texture",
}
for key, mat_wrap_key in sorted(image_map.items()):
tex_wrap = getattr(mat_wrap, mat_wrap_key, None)
if not tex_wrap or not getattr(tex_wrap, "image", None):
continue
image = tex_wrap.image
if extract_material_path:
info[key] = bpy.path.basename(image.filepath)
copy_material( filepath, image )
else:
filepath = repr(io_utils.path_reference(image.filepath, source_dir, dest_dir, 'AUTO', "", set(), image.library))[1: -1]
info[key] = repr(filepath)[1: -1]
return info
def write(context, filepath, exp_class, selections, extract_material_path, use_material_values, global_matrix):
EXPORT_APPLY_MODIFIERS = True
EXPORT_APPLY_MODIFIERS_RENDER = False
#EXPORT_GLOBAL_MATRIX = mathutils.Matrix()
EXPORT_GLOBAL_MATRIX = global_matrix
def veckey3d(v):
return round(v.x, 4), round(v.y, 4), round(v.z, 4)
def veckey2d(v):
return round(v[0], 4), round(v[1], 4)
scene = context.scene
totverts = totuvco = totno = 1
face_vert_index = 1
with open(filepath, "w", encoding="utf8", newline="\n") as f:
output = []
output.append("""// MilkShape 3D ASCII
Frames: 30
Frame: 1""")
output.append("")
output.append("Meshes: %d" % len(selections))
materials = {}
for object in selections:
# Grab mesh
if IS_BLENDER_2_7:
try:
mesh = object.to_mesh(scene, EXPORT_APPLY_MODIFIERS, calc_tessface=False,
settings='RENDER' if EXPORT_APPLY_MODIFIERS_RENDER else 'PREVIEW')
except RuntimeError:
mesh = None
else:
depsgraph = context.evaluated_depsgraph_get()
ob_for_convert = object.evaluated_get(depsgraph) if EXPORT_APPLY_MODIFIERS else object.original
try:
mesh = ob_for_convert.to_mesh()
except RuntimeError:
mesh = None
if mesh is None:
continue
# Convert mesh to tris (for ITG)
mesh_triangulate(mesh)
mesh.transform(EXPORT_GLOBAL_MATRIX * object.matrix_world)
# If negative scaling, we have to invert the normals...
if object.matrix_world.determinant() < 0.0:
mesh.flip_normals()
# Grab uv texture/layer
if IS_BLENDER_2_7:
faceuv = len(mesh.uv_textures) > 0
if faceuv:
uv_texture = mesh.uv_textures.active.data[:]
uv_layer = mesh.uv_layers.active.data[:]
else:
faceuv = len(mesh.uv_layers) > 0
if faceuv:
uv_layer = mesh.uv_layers.active.data[:]
# Store mesh vertices
mesh_verts = mesh.vertices[:] # v
face_index_pairs = [(face, index) for index, face in enumerate(mesh.polygons)]
# Calculate normals
if face_index_pairs:
mesh.calc_normals_split()
if face_index_pairs:
if IS_BLENDER_2_7:
smooth_groups, smooth_groups_tot = mesh.calc_smooth_groups(False)
else:
smooth_groups, smooth_groups_tot = mesh.calc_smooth_groups(use_bitflags=False)
if smooth_groups_tot <= 1:
smooth_groups, smooth_groups_tot = (), 0
else:
smooth_groups, smooth_groups_tot = (), 0
# Sort by Material, then images
# so we dont over context switch in the obj file.
if IS_BLENDER_2_7:
if faceuv:
if smooth_groups:
sort_func = lambda a: (a[0].material_index,
hash(uv_texture[a[1]].image),
smooth_groups[a[1]] if a[0].use_smooth else False)
else:
sort_func = lambda a: (a[0].material_index,
hash(uv_texture[a[1]].image),
a[0].use_smooth)
elif len(materials) > 1:
if smooth_groups:
sort_func = lambda a: (a[0].material_index,
smooth_groups[a[1]] if a[0].use_smooth else False)
else:
sort_func = lambda a: (a[0].material_index,
a[0].use_smooth)
else:
# no materials
if smooth_groups:
sort_func = lambda a: smooth_groups[a[1] if a[0].use_smooth else False]
else:
sort_func = lambda a: a[0].use_smooth
else:
if len(materials) > 1:
if smooth_groups:
sort_func = lambda a: (a[0].material_index,
smooth_groups[a[1]] if a[0].use_smooth else False)
else:
sort_func = lambda a: (a[0].material_index,
a[0].use_smooth)
else:
# no materials
if smooth_groups:
sort_func = lambda a: smooth_groups[a[1] if a[0].use_smooth else False]
else:
sort_func = lambda a: a[0].use_smooth
face_index_pairs.sort(key=sort_func)
del sort_func
# get UVs
uv_list = list()
uv_list_unique = list()
loops = mesh.loops
uv_unique_count = 0
uv_face_mapping = [None] * len(face_index_pairs)
if faceuv:
uv = face_index = uv_index = uv_key = uv_val = uv_ls = None
uv_dict = {}
uv_get = uv_dict.get
for face, face_index in face_index_pairs:
uv_ls = uv_face_mapping[face_index] = []
for uv_index, l_index in enumerate(face.loop_indices):
uv = uv_layer[l_index].uv
uv_key = loops[l_index].vertex_index, veckey2d(uv)
uv_val = uv_get(uv_key)
if uv_val is None:
uv_val = uv_dict[uv_key] = uv_unique_count
uv_list_unique.append(uv[:])
uv_unique_count += 1
# wtf
uv_list.append(uv_val)
uv_ls.append(uv_val)
del face, uv_dict, uv, face_index, uv_index, uv_ls, uv_get, uv_key, uv_val
# grab normals
no_key = no_val = None
normals_to_idx = {}
no_get = normals_to_idx.get
loops_to_normals = [0] * len(loops)
no_unique_count = 0
normals_list = list()
for face, face_index in face_index_pairs:
for l_idx in face.loop_indices:
no_key = veckey3d(loops[l_idx].normal)
no_val = no_get(no_key)
if no_val is None:
no_val = normals_to_idx[no_key] = no_unique_count
normals_list.append(no_key)
no_unique_count += 1
loops_to_normals[l_idx] = no_val
del normals_to_idx, no_get, no_key, no_val
# Grab material for later
material_list = mesh.materials[:]
material_names = [m.name if m else None for m in material_list]
# avoid bad index errors
if not material_list:
material_list = [None]
material_names = [name_compat(None)]
for face, face_index in face_index_pairs:
face_material = min(face.material_index, len(material_list) - 1)
if IS_BLENDER_2_7 and faceuv:
tface = uv_texture[face_index]
f_image = tface.image
else:
f_image = None
material_name = material_names[face_material]
if (material_name not in materials):
materials[material_name] = {
"material": material_list[face_material],
"faceImage": f_image
}
# grab tris
face_list = list()
tris_list = list()
for face, face_index in face_index_pairs:
face_verts = [(vi, mesh_verts[v_idx], l_idx) for vi, (v_idx, l_idx) in enumerate(zip(face.vertices, face.loop_indices))]
face_map = list()
if faceuv:
for vi, v, li in face_verts:
key = [totverts + v.index, totuvco + uv_face_mapping[face_index][vi], totno + loops_to_normals[li]] # vert, uv, normal
face_map.append(key)
face_vert_index += 1
else:
for vi, v, li in face_verts:
key = [totverts + v.index, None, totno + loops_to_normals[li]] # uv is optional
face_map.append(key)
pass
face_list.append( face_map )
# totverts += len(mesh_verts)
# totuvco += uv_unique_count
# totno += no_unique_count
# Look at mesh faces, create tris + uv list for Milkshape 3D
new_uv_list = list()
new_vert_list = dict()
for face_maps in face_list:
for vertIndex, uvIndex, normIndex in face_maps:
faceIndex = str(vertIndex)
if uvIndex is not None: faceIndex += "/" + str(uvIndex)
if faceIndex not in new_vert_list:
if vertIndex > len(mesh_verts):
continue # adios
xyz = list(mesh_verts[vertIndex - 1].co[:])
uv = [0, 0]
if uvIndex is not None and uvIndex <= len(uv_list_unique):
uv = list( uv_list_unique[(uvIndex-1)] )
uv[1] = 1 - uv[1] # flip vertically, hopefully this doesn't crash and burn and die and
new_uv_list.append( " ".join( list(map(format_number, xyz + uv)) ) )
new_vert_list[faceIndex] = len(new_uv_list) - 1 # x//y -> vert index
# Create MS3DASCII tris list
for face_maps in face_list:
outp = []
for vertIndex, uvIndex, normIndex in face_maps:
if vertIndex > len(new_vert_list):
continue
faceIndex = str(vertIndex)
if uvIndex is not None: faceIndex += "/" + str(uvIndex)
outp.append( str(new_vert_list[faceIndex]) )
tris_list.append("0 " + " ".join(outp) + " " + " ".join( list(map(lambda x: str(x[2] - 1), face_maps)) ) + " 1")
# OUTPUT ###########################################
# Mesh name, Flags, Material Index
object_name1 = object.name
object_name2 = object.data.name
if object_name1 == object_name2:
object_namestring = name_compat(object_name1)
else:
object_namestring = '%s_%s' % (name_compat(object_name1), name_compat(object_name2))
material_name = -1
if object.active_material:
try:
material_name = list(materials.keys()).index(object.active_material.name)
except ValueError:
exp_class.report({"WARNING"}, "Could not index object.active_material on materials.keys")
print("Active material:")
print(object.active_materials)
print("materials.keys")
print(list(materials.keys()))
output.append(("\"%s\" 0 " + str(material_name)) % object_namestring)
# Vertices
output.append("%s" % str(len(new_uv_list)))
for vertex in new_uv_list:
output.append("0 " + vertex + " -1")
# Normals
output.append("%s" % str(len(normals_list)))
for norm in normals_list:
output.append(" ".join(map(format_number, norm)))
# Tris
output.append("%s" % str( len(tris_list) ))
output.append("\n".join(tris_list) )
# materials
output.append("")
active_mat_count = 0
for mat_name, mat_value in materials.items():
if mat_name == None:
continue
active_mat_count += 1
output.append("Materials: %d" % active_mat_count)
for mat_name, mat_value in materials.items():
if mat_name == None:
continue
output.append("\"%s\"" % name_compat(mat_name))
mat = get_material_info(mat_value, filepath, extract_material_path, use_material_values)
output.append(" ".join( map( format_number, mat["ambient"]) )) # ambient
output.append(" ".join( map( format_number, mat["diffuse"]) )) # diffuse
output.append(" ".join( map( format_number, mat["specular"]) )) # specular
output.append(" ".join( map( format_number, mat["emissive"]) )) # emissive
output.append(format_number(mat["shininess"])) # shininess
output.append(format_number(mat["transparency"])) # transparency
output.append("\"%s\"" % mat["color_texture"]) # color map
output.append("\"%s\"" % mat["alpha_texture"]) # alpha map
output.append("")
output.append("""Bones: 1
"Bone01"
""
0 0 0 0 0 0 0
1
1 0 0 0
1
1 0 0 0
GroupComments: 0
MaterialComments: 0
BoneComments: 0
ModelComment: 0""")
f.write( "\n".join(output) )
f.close()
return {'FINISHED'}
IOOBJOrientationHelper = orientation_helper_factory("IOOBJOrientationHelper", axis_forward='-Z', axis_up='Y')
class ExportMS3D(Operator, ExportHelper, IOOBJOrientationHelper):
"""Save selection to MS3D ASCII"""
bl_idname = "exportms3d.ascii"
bl_label = "Export as MS3D ASCII (.txt)"
filename_ext = ".txt"
# 2.7x: var = prop
# 2.8+: var: prop
filter_glob = StringProperty(
default="*.txt",
options={'HIDDEN'},
maxlen=255, # Max internal buffer length, longer would be clamped.
)
extract_material_path = BoolProperty(
name="Extract Material File",
description="Uses the material filename as the path instead of relative path. Also copies the file to the export directory.",
default=True,
)
default_material_values = BoolProperty(
name="Actual Material Values",
description="Uses the material's actual values instead of the default.",
default=False,
)
selection_only = BoolProperty(
name="Export Selection only",
description="Only exports the selected object.",
default=True,
)
global_scale = FloatProperty(
name="Scale",
min=0.01, max=1000.0,
default=1.0,
)
def execute(self, context):
# Grab selected objects
if self.selection_only:
selection = [obj for obj in context.selected_objects if obj.type == "MESH"]
else:
selection = [obj for obj in context.visible_objects if obj.type == "MESH"]
# Check if we have selected one object
if len(selection) < 1:
self.report({"WARNING"}, "Expected 1 selection, got none.")
return {"CANCELLED"}
global_matrix = (mathutils.Matrix.Scale(self.global_scale, 4) *
axis_conversion(to_forward=self.axis_forward,
to_up=self.axis_up,
).to_4x4())
return write(context, self.filepath, self, selection, self.extract_material_path, self.default_material_values, global_matrix)
# Only needed if you want to add into a dynamic menu
def menu_func_export(self, context):
self.layout.operator(ExportMS3D.bl_idname, text="Export as MS3D ASCII (.txt)")
def register():
bpy.utils.register_class(ExportMS3D)
if IS_BLENDER_2_7:
bpy.types.INFO_MT_file_export.append(menu_func_export)
else:
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
def unregister():
bpy.utils.unregister_class(ExportMS3D)
if IS_BLENDER_2_7:
bpy.types.INFO_MT_file_export.remove(menu_func_export)
else:
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
if __name__ == "__main__":
register()
# bpy.ops.exportms3d.ascii('INVOKE_DEFAULT')
# *heavily* based on blender's export_obj.py addon + obvr's obj2ms3dascii
# for use with Blender 2.8 and above
import bpy
import os
import shutil
import mathutils
from bpy_extras import io_utils
from bpy_extras.io_utils import ExportHelper, path_reference_mode, axis_conversion, orientation_helper
from bpy.props import StringProperty, BoolProperty, FloatProperty
from bpy.types import Operator
# the bug
# from pprint import pprint
# only death can set me free
IS_BLENDER_2_7 = bpy.app.version <= (2, 79, 0)
if not IS_BLENDER_2_7:
from bpy_extras import node_shader_utils
bl_info = {
"name": "Milkshape 3D ASCII",
"description": "Exports selection as Milkshape 3D ASCII",
"author": "Jaezmien Naejara",
"version": (0, 0, 4),
"blender": (2, 80, 0),
"location": "File > Import-Export",
"category": "Import-Export"
}
def format_number(num):
return "{:.6f}".format(num)
def name_compat(name):
return None if (name is None) else name.replace(' ', '_')
def mesh_triangulate(me):
import bmesh
bm = bmesh.new()
bm.from_mesh(me)
bmesh.ops.triangulate(bm, faces=bm.faces)
bm.to_mesh(me)
bm.free()
def copy_material(filepath, image):
try:
shutil.copyfile( bpy.path.abspath(image.filepath) , os.path.join(os.path.dirname(filepath), bpy.path.basename(image.filepath)) )
except shutil.SameFileError:
print(filepath + " and " + image.filepath + " is the same file.")
def get_material_info(materialObj, filepath, extract_material_path, use_material_values):
source_dir = os.path.dirname(bpy.data.filepath)
dest_dir = os.path.dirname(filepath)
material = materialObj["material"]
info = {
"ambient": [0, 0, 0, 1], # Ka
"diffuse": [1, 1, 1, 1], # Kd
"specular": [1, 1, 1, 1], # Ks
"emissive": [0, 0, 0, 1], # Ke
"shininess": 0, # Ns
"transparency": 1, # d
"color_texture": "", # map_Kd
"alpha_texture": "" # map_d
}
if IS_BLENDER_2_7:
if material:
if use_material_values:
use_mirror = material.raytrace_mirror.use and material.raytrace_mirror.reflect_factor != 0.0
if use_mirror:
info["ambient"] = list((material.raytrace_mirror.reflect_factor * material.mirror_color)[:]) + [1]
else:
info["ambient"] = [ material.ambient, material.ambient, material.ambient, 1 ]
info["diffuse"] = list((material.diffuse_intensity * material.diffuse_color)[:]) + [1]
info["specular"] = list((material.specular_intensity * material.specular_color)[:]) + [1]
info["emissive"] = list((material.emit * material.diffuse_color)[:]) + [1]
info["shininess"] = ((0.4 - material.specular_slope) / 0.0004) if (material.specular_shader == "WARDISO") else ((material.specular_hardness - 1) / 0.51)
info["transparency"] = material.alpha
face_img = materialObj["faceImage"]
if face_img:
if getattr(face_img, "filepath", None):
if extract_material_path:
info["color_texture"] = bpy.path.basename(face_img.filepath)
copy_material(filepath, face_img)
else:
info["color_texture"] = io_utils.path_reference(face_img.filepath, source_dir, dest_dir, 'AUTO', "", set(), face_img.library)
else:
face_img = None
for mtex in reversed(material.texture_slots):
if mtex and mtex.texture and mtex.texture.type == 'IMAGE':
image = mtex.texture.image
if image:
key = ""
if (mtex.use_map_color_diffuse and (face_img is None) and (mtex.use_map_warp is False) and (mtex.texture_coords != 'REFLECTION')):
key = "color_texture"
if (mtex.use_map_alpha):
key = "alpha_texture"
if len(key.strip()) > 0:
if extract_material_path:
info[key] = bpy.path.basename(image.filepath)
copy_material(filepath, image)
else:
info[key] = repr(io_utils.path_reference(image.filepath, source_dir, dest_dir, 'AUTO', "", set(), image.library))[1:-1]
else:
mat_wrap = node_shader_utils.PrincipledBSDFWrapper(material) if material else None
if mat_wrap:
if use_material_values:
if mat_wrap.metallic != 0.0:
info["ambient"] = [mat_wrap.metallic, mat_wrap.metallic, mat_wrap.metallic, 1.0]
else:
info["ambient"] = [1.0, 1.0, 1.0, 1.0]
info["diffuse"] = list(mat_wrap.base_color[:3]) + [1]
info["specular"] = [mat_wrap.specular, mat_wrap.specular, mat_wrap.specular, 1.0] # Specular
emission_strength = mat_wrap.emission_strength
info["emissive"] = [emission_strength * c for c in mat_wrap.emission_color[:3]] + [1.0]
# TODO: This is different from 2.79's shininess value. Check why and try to convert to it
spec = (1.0 - mat_wrap.roughness) * 30
info["shininess"] = (spec * spec)
image_map = {
"color_texture": "base_color_texture",
"alpha_texture": "alpha_texture",
}
for key, mat_wrap_key in sorted(image_map.items()):
tex_wrap = getattr(mat_wrap, mat_wrap_key, None)
if not tex_wrap or not getattr(tex_wrap, "image", None):
continue
image = tex_wrap.image
if extract_material_path:
info[key] = bpy.path.basename(image.filepath)
copy_material( filepath, image )
else:
filepath = repr(io_utils.path_reference(image.filepath, source_dir, dest_dir, 'AUTO', "", set(), image.library))[1: -1]
info[key] = repr(filepath)[1: -1]
return info
def write(context, filepath, exp_class, selections, extract_material_path, use_material_values, global_matrix):
EXPORT_APPLY_MODIFIERS = True
EXPORT_APPLY_MODIFIERS_RENDER = False
#EXPORT_GLOBAL_MATRIX = mathutils.Matrix()
EXPORT_GLOBAL_MATRIX = global_matrix
def veckey3d(v):
return round(v.x, 4), round(v.y, 4), round(v.z, 4)
def veckey2d(v):
return round(v[0], 4), round(v[1], 4)
scene = context.scene
totverts = totuvco = totno = 1
face_vert_index = 1
with open(filepath, "w", encoding="utf8", newline="\n") as f:
output = []
output.append("""// MilkShape 3D ASCII
Frames: 30
Frame: 1""")
output.append("")
output.append("Meshes: %d" % len(selections))
materials = {}
for object in selections:
# Grab mesh
if IS_BLENDER_2_7:
try:
mesh = object.to_mesh(scene, EXPORT_APPLY_MODIFIERS, calc_tessface=False,
settings='RENDER' if EXPORT_APPLY_MODIFIERS_RENDER else 'PREVIEW')
except RuntimeError:
mesh = None
else:
depsgraph = context.evaluated_depsgraph_get()
ob_for_convert = object.evaluated_get(depsgraph) if EXPORT_APPLY_MODIFIERS else object.original
try:
mesh = ob_for_convert.to_mesh()
except RuntimeError:
mesh = None
if mesh is None:
continue
# Convert mesh to tris (for ITG)
mesh_triangulate(mesh)
mesh.transform(EXPORT_GLOBAL_MATRIX @ object.matrix_world)
# If negative scaling, we have to invert the normals...
if object.matrix_world.determinant() < 0.0:
mesh.flip_normals()
# Grab uv texture/layer
if IS_BLENDER_2_7:
faceuv = len(mesh.uv_textures) > 0
if faceuv:
uv_texture = mesh.uv_textures.active.data[:]
uv_layer = mesh.uv_layers.active.data[:]
else:
faceuv = len(mesh.uv_layers) > 0
if faceuv:
uv_layer = mesh.uv_layers.active.data[:]
# Store mesh vertices
mesh_verts = mesh.vertices[:] # v
face_index_pairs = [(face, index) for index, face in enumerate(mesh.polygons)]
# Calculate normals
if face_index_pairs:
mesh.calc_normals_split()
if face_index_pairs:
if IS_BLENDER_2_7:
smooth_groups, smooth_groups_tot = mesh.calc_smooth_groups(False)
else:
smooth_groups, smooth_groups_tot = mesh.calc_smooth_groups(use_bitflags=False)
if smooth_groups_tot <= 1:
smooth_groups, smooth_groups_tot = (), 0
else:
smooth_groups, smooth_groups_tot = (), 0
# Sort by Material, then images
# so we dont over context switch in the obj file.
if IS_BLENDER_2_7:
if faceuv:
if smooth_groups:
sort_func = lambda a: (a[0].material_index,
hash(uv_texture[a[1]].image),
smooth_groups[a[1]] if a[0].use_smooth else False)
else:
sort_func = lambda a: (a[0].material_index,
hash(uv_texture[a[1]].image),
a[0].use_smooth)
elif len(materials) > 1:
if smooth_groups:
sort_func = lambda a: (a[0].material_index,
smooth_groups[a[1]] if a[0].use_smooth else False)
else:
sort_func = lambda a: (a[0].material_index,
a[0].use_smooth)
else:
# no materials
if smooth_groups:
sort_func = lambda a: smooth_groups[a[1] if a[0].use_smooth else False]
else:
sort_func = lambda a: a[0].use_smooth
else:
if len(materials) > 1:
if smooth_groups:
sort_func = lambda a: (a[0].material_index,
smooth_groups[a[1]] if a[0].use_smooth else False)
else:
sort_func = lambda a: (a[0].material_index,
a[0].use_smooth)
else:
# no materials
if smooth_groups:
sort_func = lambda a: smooth_groups[a[1] if a[0].use_smooth else False]
else:
sort_func = lambda a: a[0].use_smooth
face_index_pairs.sort(key=sort_func)
del sort_func
# get UVs
uv_list = list()
uv_list_unique = list()
loops = mesh.loops
uv_unique_count = 0
uv_face_mapping = [None] * len(face_index_pairs)
if faceuv:
uv = face_index = uv_index = uv_key = uv_val = uv_ls = None
uv_dict = {}
uv_get = uv_dict.get
for face, face_index in face_index_pairs:
uv_ls = uv_face_mapping[face_index] = []
for uv_index, l_index in enumerate(face.loop_indices):
uv = uv_layer[l_index].uv
uv_key = loops[l_index].vertex_index, veckey2d(uv)
uv_val = uv_get(uv_key)
if uv_val is None:
uv_val = uv_dict[uv_key] = uv_unique_count
uv_list_unique.append(uv[:])
uv_unique_count += 1
# wtf
uv_list.append(uv_val)
uv_ls.append(uv_val)
del face, uv_dict, uv, face_index, uv_index, uv_ls, uv_get, uv_key, uv_val
# grab normals
no_key = no_val = None
normals_to_idx = {}
no_get = normals_to_idx.get
loops_to_normals = [0] * len(loops)
no_unique_count = 0
normals_list = list()
for face, face_index in face_index_pairs:
for l_idx in face.loop_indices:
no_key = veckey3d(loops[l_idx].normal)
no_val = no_get(no_key)
if no_val is None:
no_val = normals_to_idx[no_key] = no_unique_count
normals_list.append(no_key)
no_unique_count += 1
loops_to_normals[l_idx] = no_val
del normals_to_idx, no_get, no_key, no_val
# Grab material for later
material_list = mesh.materials[:]
material_names = [m.name if m else None for m in material_list]
# avoid bad index errors
if not material_list:
material_list = [None]
material_names = [name_compat(None)]
for face, face_index in face_index_pairs:
face_material = min(face.material_index, len(material_list) - 1)
if IS_BLENDER_2_7 and faceuv:
tface = uv_texture[face_index]
f_image = tface.image
else:
f_image = None
material_name = material_names[face_material]
if (material_name not in materials):
materials[material_name] = {
"material": material_list[face_material],
"faceImage": f_image
}
# grab tris
face_list = list()
tris_list = list()
for face, face_index in face_index_pairs:
face_verts = [(vi, mesh_verts[v_idx], l_idx) for vi, (v_idx, l_idx) in enumerate(zip(face.vertices, face.loop_indices))]
face_map = list()
if faceuv:
for vi, v, li in face_verts:
key = [totverts + v.index, totuvco + uv_face_mapping[face_index][vi], totno + loops_to_normals[li]] # vert, uv, normal
face_map.append(key)
face_vert_index += 1
else:
for vi, v, li in face_verts:
key = [totverts + v.index, None, totno + loops_to_normals[li]] # uv is optional
face_map.append(key)
pass
face_list.append( face_map )
# totverts += len(mesh_verts)
# totuvco += uv_unique_count
# totno += no_unique_count
# Look at mesh faces, create tris + uv list for Milkshape 3D
new_uv_list = list()
new_vert_list = dict()
for face_maps in face_list:
for vertIndex, uvIndex, normIndex in face_maps:
faceIndex = str(vertIndex)
if uvIndex is not None: faceIndex += "/" + str(uvIndex)
if faceIndex not in new_vert_list:
if vertIndex > len(mesh_verts):
continue # adios
xyz = list(mesh_verts[vertIndex - 1].co[:])
uv = [0, 0]
if uvIndex is not None and uvIndex <= len(uv_list_unique):
uv = list( uv_list_unique[(uvIndex-1)] )
uv[1] = 1 - uv[1] # flip vertically, hopefully this doesn't crash and burn and die and
new_uv_list.append( " ".join( list(map(format_number, xyz + uv)) ) )
new_vert_list[faceIndex] = len(new_uv_list) - 1 # x//y -> vert index
# Create MS3DASCII tris list
for face_maps in face_list:
outp = []
for vertIndex, uvIndex, normIndex in face_maps:
if vertIndex > len(new_vert_list):
continue
faceIndex = str(vertIndex)
if uvIndex is not None: faceIndex += "/" + str(uvIndex)
outp.append( str(new_vert_list[faceIndex]) )
tris_list.append("0 " + " ".join(outp) + " " + " ".join( list(map(lambda x: str(x[2] - 1), face_maps)) ) + " 1")
# OUTPUT ###########################################
# Mesh name, Flags, Material Index
object_name1 = object.name
object_name2 = object.data.name
if object_name1 == object_name2:
object_namestring = name_compat(object_name1)
else:
object_namestring = '%s_%s' % (name_compat(object_name1), name_compat(object_name2))
material_name = -1
if object.active_material:
try:
material_name = list(materials.keys()).index(object.active_material.name)
except ValueError:
exp_class.report({"WARNING"}, "Could not index object.active_material on materials.keys")
print("Active material:")
print(object.active_materials)
print("materials.keys")
print(list(materials.keys()))
output.append(("\"%s\" 0 " + str(material_name)) % object_namestring)
# Vertices
output.append("%s" % str(len(new_uv_list)))
for vertex in new_uv_list:
output.append("0 " + vertex + " -1")
# Normals
output.append("%s" % str(len(normals_list)))
for norm in normals_list:
output.append(" ".join(map(format_number, norm)))
# Tris
output.append("%s" % str( len(tris_list) ))
output.append("\n".join(tris_list) )
# materials
output.append("")
active_mat_count = 0
for mat_name, mat_value in materials.items():
if mat_name == None:
continue
active_mat_count += 1
output.append("Materials: %d" % active_mat_count)
for mat_name, mat_value in materials.items():
if mat_name == None:
continue
output.append("\"%s\"" % name_compat(mat_name))
mat = get_material_info(mat_value, filepath, extract_material_path, use_material_values)
output.append(" ".join( map( format_number, mat["ambient"]) )) # ambient
output.append(" ".join( map( format_number, mat["diffuse"]) )) # diffuse
output.append(" ".join( map( format_number, mat["specular"]) )) # specular
output.append(" ".join( map( format_number, mat["emissive"]) )) # emissive
output.append(format_number(mat["shininess"])) # shininess
output.append(format_number(mat["transparency"])) # transparency
output.append("\"%s\"" % mat["color_texture"]) # color map
output.append("\"%s\"" % mat["alpha_texture"]) # alpha map
output.append("")
output.append("""Bones: 1
"Bone01"
""
0 0 0 0 0 0 0
1
1 0 0 0
1
1 0 0 0
GroupComments: 0
MaterialComments: 0
BoneComments: 0
ModelComment: 0""")
f.write( "\n".join(output) )
f.close()
return {'FINISHED'}
@orientation_helper(axis_forward='-Z', axis_up='Y')
class ExportMS3D(Operator, ExportHelper):
"""Save selection to MS3D ASCII"""
bl_idname = "exportms3d.ascii"
bl_label = "Export as MS3D ASCII (.txt)"
filename_ext = ".txt"
# 2.7x: var = prop
# 2.8+: var: prop
filter_glob: StringProperty(
default="*.txt",
options={'HIDDEN'},
maxlen=255, # Max internal buffer length, longer would be clamped.
)
extract_material_path: BoolProperty(
name="Extract Material File",
description="Uses the material filename as the path instead of relative path. Also copies the file to the export directory.",
default=True,
)
default_material_values: BoolProperty(
name="Actual Material Values",
description="Uses the material's actual values instead of the default.",
default=False,
)
selection_only: BoolProperty(
name="Export Selection only",
description="Only exports the selected object.",
default=True,
)
global_scale: FloatProperty(
name="Scale",
min=0.01, max=1000.0,
default=1.0,
)
def execute(self, context):
# Grab selected objects
if self.selection_only:
selection = [obj for obj in context.selected_objects if obj.type == "MESH"]
else:
selection = [obj for obj in context.visible_objects if obj.type == "MESH"]
# Check if we have selected one object
if len(selection) < 1:
print(selection)
self.report({"WARNING"}, "Expected 1 selection, got none.")
return {"CANCELLED"}
global_matrix = (mathutils.Matrix.Scale(self.global_scale, 4) @
axis_conversion(to_forward=self.axis_forward,
to_up=self.axis_up,
).to_4x4())
return write(context, self.filepath, self, selection, self.extract_material_path, self.default_material_values, global_matrix)
# Only needed if you want to add into a dynamic menu
def menu_func_export(self, context):
self.layout.operator(ExportMS3D.bl_idname, text="Export as MS3D ASCII (.txt)")
def register():
bpy.utils.register_class(ExportMS3D)
if IS_BLENDER_2_7:
bpy.types.INFO_MT_file_export.append(menu_func_export)
else:
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
def unregister():
bpy.utils.unregister_class(ExportMS3D)
if IS_BLENDER_2_7:
bpy.types.INFO_MT_file_export.remove(menu_func_export)
else:
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
if __name__ == "__main__":
register()
# bpy.ops.exportms3d.ascii('INVOKE_DEFAULT')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment