Last active
January 28, 2022 08:04
-
-
Save Jaezmien/5deca214c1c54ad54fed480ccb5286bd to your computer and use it in GitHub Desktop.
Blender Addon to objects to Milkshape 3D ASCII format
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# *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') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# *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