Compare commits
13 Commits
1.2.1
...
blender-4.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb5a9ed564 | ||
|
|
09895e1fa1 | ||
|
|
ac4d96b396 | ||
|
|
3fe4a6af53 | ||
|
|
972ea5deda | ||
|
|
2ff4a661f2 | ||
|
|
956de550b2 | ||
|
|
bd90088aed | ||
|
|
ecfd9897b1 | ||
|
|
a3e350e96e | ||
|
|
7b417ae425 | ||
|
|
c59f16ed5e | ||
|
|
51be4b8ee9 |
Binary file not shown.
@@ -1,17 +1,3 @@
|
|||||||
bl_info = {
|
|
||||||
'name': 'ASCII Scene Export (ASE)',
|
|
||||||
'description': 'Export ASE (ASCII Scene Export) files',
|
|
||||||
'author': 'Colin Basnett (Darklight Games)',
|
|
||||||
'version': (1, 2, 1),
|
|
||||||
'blender': (2, 90, 0),
|
|
||||||
'location': 'File > Import-Export',
|
|
||||||
'warning': 'This add-on is under development.',
|
|
||||||
'wiki_url': 'https://github.com/DarklightGames/io_scene_ase/wiki',
|
|
||||||
'tracker_url': 'https://github.com/DarklightGames/io_scene_ase/issues',
|
|
||||||
'support': 'COMMUNITY',
|
|
||||||
'category': 'Import-Export'
|
|
||||||
}
|
|
||||||
|
|
||||||
if 'bpy' in locals():
|
if 'bpy' in locals():
|
||||||
import importlib
|
import importlib
|
||||||
if 'ase' in locals(): importlib.reload(ase)
|
if 'ase' in locals(): importlib.reload(ase)
|
||||||
@@ -26,24 +12,27 @@ from . import builder
|
|||||||
from . import writer
|
from . import writer
|
||||||
from . import exporter
|
from . import exporter
|
||||||
|
|
||||||
classes = (
|
classes = exporter.classes
|
||||||
exporter.ASE_OT_ExportOperator,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def menu_func_export(self, context):
|
def menu_func_export(self, context):
|
||||||
self.layout.operator(exporter.ASE_OT_ExportOperator.bl_idname, text='ASCII Scene Export (.ase)')
|
self.layout.operator(exporter.ASE_OT_export.bl_idname, text='ASCII Scene Export (.ase)')
|
||||||
|
self.layout.operator(exporter.ASE_OT_export_collections.bl_idname, text='ASCII Scene Export Collections (.ase)')
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
for cls in classes:
|
for cls in classes:
|
||||||
bpy.utils.register_class(cls)
|
bpy.utils.register_class(cls)
|
||||||
|
|
||||||
|
bpy.types.Scene.ase_export = bpy.props.PointerProperty(type=exporter.ASE_PG_export)
|
||||||
|
|
||||||
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
|
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
|
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
|
||||||
|
|
||||||
|
del bpy.types.Scene.ase_export
|
||||||
|
|
||||||
for cls in classes:
|
for cls in classes:
|
||||||
bpy.utils.unregister_class(cls)
|
bpy.utils.unregister_class(cls)
|
||||||
27
io_scene_ase/blender_manifest.toml
Normal file
27
io_scene_ase/blender_manifest.toml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
schema_version = "1.0.0"
|
||||||
|
id = "io_scene_ase"
|
||||||
|
version = "2.1.0"
|
||||||
|
name = "ASCII Scene Export (.ase)"
|
||||||
|
tagline = "Export .ase files used in Unreal Engine 1 & 2"
|
||||||
|
maintainer = "Colin Basnett <cmbasnett@gmail.com>"
|
||||||
|
type = "add-on"
|
||||||
|
website = "https://github.com/DarklightGames/io_scene_ase/"
|
||||||
|
tags = ["Game Engine", "Import-Export"]
|
||||||
|
blender_version_min = "4.2.0"
|
||||||
|
# Optional: maximum supported Blender version
|
||||||
|
# blender_version_max = "5.1.0"
|
||||||
|
license = [
|
||||||
|
"SPDX:GPL-3.0-or-later",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build]
|
||||||
|
paths_exclude_pattern = [
|
||||||
|
"/.git/",
|
||||||
|
"__pycache__/",
|
||||||
|
"/venv/",
|
||||||
|
"/.github/",
|
||||||
|
".gitignore",
|
||||||
|
]
|
||||||
|
|
||||||
|
[permissions]
|
||||||
|
files = "Export ASE files to disk"
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
from .ase import *
|
from typing import Iterable, Optional, List, Tuple
|
||||||
|
|
||||||
|
from bpy.types import Object, Context, Material
|
||||||
|
|
||||||
|
from .ase import ASE, ASEGeometryObject, ASEFace, ASEFaceNormal, ASEVertexNormal, ASEUVLayer, is_collision_name
|
||||||
import bpy
|
import bpy
|
||||||
import bmesh
|
import bmesh
|
||||||
import math
|
import math
|
||||||
from mathutils import Matrix
|
from mathutils import Matrix, Vector
|
||||||
|
|
||||||
|
SMOOTHING_GROUP_MAX = 32
|
||||||
|
|
||||||
class ASEBuilderError(Exception):
|
class ASEBuilderError(Exception):
|
||||||
pass
|
pass
|
||||||
@@ -11,38 +16,63 @@ class ASEBuilderError(Exception):
|
|||||||
|
|
||||||
class ASEBuilderOptions(object):
|
class ASEBuilderOptions(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.scale = 1.0
|
|
||||||
self.use_raw_mesh_data = False
|
self.use_raw_mesh_data = False
|
||||||
|
self.materials: Optional[List[Material]] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_object_matrix(obj: Object, asset_instance: Optional[Object] = None) -> Matrix:
|
||||||
|
if asset_instance is not None:
|
||||||
|
return asset_instance.matrix_world @ Matrix().Translation(asset_instance.instance_collection.instance_offset) @ obj.matrix_local
|
||||||
|
return obj.matrix_world
|
||||||
|
|
||||||
|
|
||||||
|
def get_mesh_objects(objects: Iterable[Object]) -> List[Tuple[Object, Optional[Object]]]:
|
||||||
|
mesh_objects = []
|
||||||
|
for obj in objects:
|
||||||
|
if obj.type == 'MESH':
|
||||||
|
mesh_objects.append((obj, None))
|
||||||
|
elif obj.instance_collection:
|
||||||
|
for instance_object in obj.instance_collection.all_objects:
|
||||||
|
if instance_object.type == 'MESH':
|
||||||
|
mesh_objects.append((instance_object, obj))
|
||||||
|
return mesh_objects
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ASEBuilder(object):
|
class ASEBuilder(object):
|
||||||
def build(self, context, options: ASEBuilderOptions):
|
def build(self, context: Context, options: ASEBuilderOptions, objects: Iterable[Object]):
|
||||||
ase = ASE()
|
ase = ASE()
|
||||||
|
|
||||||
main_geometry_object = None
|
main_geometry_object = None
|
||||||
for selected_object in context.selected_objects:
|
mesh_objects = get_mesh_objects(objects)
|
||||||
if selected_object is None or selected_object.type != 'MESH':
|
|
||||||
continue
|
context.window_manager.progress_begin(0, len(mesh_objects))
|
||||||
|
|
||||||
|
ase.materials = options.materials
|
||||||
|
|
||||||
|
for object_index, (obj, asset_instance) in enumerate(mesh_objects):
|
||||||
|
|
||||||
|
matrix_world = get_object_matrix(obj, asset_instance)
|
||||||
|
|
||||||
# Evaluate the mesh after modifiers are applied
|
# Evaluate the mesh after modifiers are applied
|
||||||
if options.use_raw_mesh_data:
|
if options.use_raw_mesh_data:
|
||||||
mesh_object = selected_object
|
mesh_object = obj
|
||||||
mesh_data = mesh_object.data
|
mesh_data = mesh_object.data
|
||||||
else:
|
else:
|
||||||
depsgraph = context.evaluated_depsgraph_get()
|
depsgraph = context.evaluated_depsgraph_get()
|
||||||
bm = bmesh.new()
|
bm = bmesh.new()
|
||||||
bm.from_object(selected_object, depsgraph)
|
bm.from_object(obj, depsgraph)
|
||||||
mesh_data = bpy.data.meshes.new('')
|
mesh_data = bpy.data.meshes.new('')
|
||||||
bm.to_mesh(mesh_data)
|
bm.to_mesh(mesh_data)
|
||||||
del bm
|
del bm
|
||||||
mesh_object = bpy.data.objects.new('', mesh_data)
|
mesh_object = bpy.data.objects.new('', mesh_data)
|
||||||
mesh_object.matrix_world = selected_object.matrix_world
|
mesh_object.matrix_world = matrix_world
|
||||||
|
|
||||||
if not is_collision_name(selected_object.name) and main_geometry_object is not None:
|
if not is_collision_name(obj.name) and main_geometry_object is not None:
|
||||||
geometry_object = main_geometry_object
|
geometry_object = main_geometry_object
|
||||||
else:
|
else:
|
||||||
geometry_object = ASEGeometryObject()
|
geometry_object = ASEGeometryObject()
|
||||||
geometry_object.name = selected_object.name
|
geometry_object.name = obj.name
|
||||||
if not geometry_object.is_collision:
|
if not geometry_object.is_collision:
|
||||||
main_geometry_object = geometry_object
|
main_geometry_object = geometry_object
|
||||||
ase.geometry_objects.append(geometry_object)
|
ase.geometry_objects.append(geometry_object)
|
||||||
@@ -54,41 +84,43 @@ class ASEBuilder(object):
|
|||||||
for edge in bm.edges:
|
for edge in bm.edges:
|
||||||
if not edge.is_manifold:
|
if not edge.is_manifold:
|
||||||
del bm
|
del bm
|
||||||
raise ASEBuilderError(f'Collision mesh \'{selected_object.name}\' is not manifold')
|
raise ASEBuilderError(f'Collision mesh \'{obj.name}\' is not manifold')
|
||||||
if not edge.is_convex:
|
if not edge.is_convex:
|
||||||
del bm
|
del bm
|
||||||
raise ASEBuilderError(f'Collision mesh \'{selected_object.name}\' is not convex')
|
raise ASEBuilderError(f'Collision mesh \'{obj.name}\' is not convex')
|
||||||
|
|
||||||
if not geometry_object.is_collision and len(selected_object.data.materials) == 0:
|
if not geometry_object.is_collision and len(obj.data.materials) == 0:
|
||||||
raise ASEBuilderError(f'Mesh \'{selected_object.name}\' must have at least one material')
|
raise ASEBuilderError(f'Mesh \'{obj.name}\' must have at least one material')
|
||||||
|
|
||||||
|
vertex_transform = Matrix.Rotation(math.pi, 4, 'Z') @ matrix_world
|
||||||
|
|
||||||
vertex_transform = Matrix.Scale(options.scale, 4) @ Matrix.Rotation(math.pi, 4, 'Z') @ mesh_object.matrix_world
|
|
||||||
for vertex_index, vertex in enumerate(mesh_data.vertices):
|
for vertex_index, vertex in enumerate(mesh_data.vertices):
|
||||||
geometry_object.vertices.append(vertex_transform @ vertex.co)
|
geometry_object.vertices.append(vertex_transform @ vertex.co)
|
||||||
|
|
||||||
material_indices = []
|
material_indices = []
|
||||||
if not geometry_object.is_collision:
|
if not geometry_object.is_collision:
|
||||||
for mesh_material_index, material in enumerate(selected_object.data.materials):
|
for mesh_material_index, material in enumerate(obj.data.materials):
|
||||||
if material is None:
|
if material is None:
|
||||||
raise ASEBuilderError(f'Material slot {mesh_material_index + 1} for mesh \'{selected_object.name}\' cannot be empty')
|
raise ASEBuilderError(f'Material slot {mesh_material_index + 1} for mesh \'{obj.name}\' cannot be empty')
|
||||||
try:
|
material_indices.append(ase.materials.index(material))
|
||||||
# Reuse existing material entries for duplicates
|
|
||||||
material_index = ase.materials.index(material.name)
|
|
||||||
except ValueError:
|
|
||||||
material_index = len(ase.materials)
|
|
||||||
ase.materials.append(material.name)
|
|
||||||
material_indices.append(material_index)
|
|
||||||
|
|
||||||
mesh_data.calc_loop_triangles()
|
mesh_data.calc_loop_triangles()
|
||||||
mesh_data.calc_normals_split()
|
|
||||||
|
# Calculate smoothing groups.
|
||||||
poly_groups, groups = mesh_data.calc_smooth_groups(use_bitflags=False)
|
poly_groups, groups = mesh_data.calc_smooth_groups(use_bitflags=False)
|
||||||
|
|
||||||
|
# Figure out how many scaling axes are negative.
|
||||||
|
# This is important for calculating the normals of the mesh.
|
||||||
|
_, _, scale = vertex_transform.decompose()
|
||||||
|
negative_scaling_axes = sum([1 for x in scale if x < 0])
|
||||||
|
should_invert_normals = negative_scaling_axes % 2 == 1
|
||||||
|
|
||||||
|
loop_triangle_index_order = (2, 1, 0) if should_invert_normals else (0, 1, 2)
|
||||||
|
|
||||||
# Faces
|
# Faces
|
||||||
for face_index, loop_triangle in enumerate(mesh_data.loop_triangles):
|
for face_index, loop_triangle in enumerate(mesh_data.loop_triangles):
|
||||||
face = ASEFace()
|
face = ASEFace()
|
||||||
face.a = geometry_object.vertex_offset + mesh_data.loops[loop_triangle.loops[0]].vertex_index
|
face.a, face.b, face.c = map(lambda j: geometry_object.vertex_offset + mesh_data.loops[loop_triangle.loops[j]].vertex_index, loop_triangle_index_order)
|
||||||
face.b = geometry_object.vertex_offset + mesh_data.loops[loop_triangle.loops[1]].vertex_index
|
|
||||||
face.c = geometry_object.vertex_offset + mesh_data.loops[loop_triangle.loops[2]].vertex_index
|
|
||||||
if not geometry_object.is_collision:
|
if not geometry_object.is_collision:
|
||||||
face.material_index = material_indices[loop_triangle.material_index]
|
face.material_index = material_indices[loop_triangle.material_index]
|
||||||
# The UT2K4 importer only accepts 32 smoothing groups. Anything past this completely mangles the
|
# The UT2K4 importer only accepts 32 smoothing groups. Anything past this completely mangles the
|
||||||
@@ -98,7 +130,7 @@ class ASEBuilder(object):
|
|||||||
# This may result in bad calculated normals on export in rare cases. For example, if a face with a
|
# This may result in bad calculated normals on export in rare cases. For example, if a face with a
|
||||||
# smoothing group of 3 is adjacent to a face with a smoothing group of 35 (35 % 32 == 3), those faces
|
# smoothing group of 3 is adjacent to a face with a smoothing group of 35 (35 % 32 == 3), those faces
|
||||||
# will be treated as part of the same smoothing group.
|
# will be treated as part of the same smoothing group.
|
||||||
face.smoothing = (poly_groups[loop_triangle.polygon_index] - 1) % 32
|
face.smoothing = (poly_groups[loop_triangle.polygon_index] - 1) % SMOOTHING_GROUP_MAX
|
||||||
geometry_object.faces.append(face)
|
geometry_object.faces.append(face)
|
||||||
|
|
||||||
if not geometry_object.is_collision:
|
if not geometry_object.is_collision:
|
||||||
@@ -107,10 +139,12 @@ class ASEBuilder(object):
|
|||||||
face_normal = ASEFaceNormal()
|
face_normal = ASEFaceNormal()
|
||||||
face_normal.normal = loop_triangle.normal
|
face_normal.normal = loop_triangle.normal
|
||||||
face_normal.vertex_normals = []
|
face_normal.vertex_normals = []
|
||||||
for i in range(3):
|
for i in loop_triangle_index_order:
|
||||||
vertex_normal = ASEVertexNormal()
|
vertex_normal = ASEVertexNormal()
|
||||||
vertex_normal.vertex_index = geometry_object.vertex_offset + mesh_data.loops[loop_triangle.loops[i]].vertex_index
|
vertex_normal.vertex_index = geometry_object.vertex_offset + mesh_data.loops[loop_triangle.loops[i]].vertex_index
|
||||||
vertex_normal.normal = loop_triangle.split_normals[i]
|
vertex_normal.normal = loop_triangle.split_normals[i]
|
||||||
|
if should_invert_normals:
|
||||||
|
vertex_normal.normal = (-Vector(vertex_normal.normal)).to_tuple()
|
||||||
face_normal.vertex_normals.append(vertex_normal)
|
face_normal.vertex_normals.append(vertex_normal)
|
||||||
geometry_object.face_normals.append(face_normal)
|
geometry_object.face_normals.append(face_normal)
|
||||||
|
|
||||||
@@ -125,22 +159,25 @@ class ASEBuilder(object):
|
|||||||
|
|
||||||
# Texture Faces
|
# Texture Faces
|
||||||
for loop_triangle in mesh_data.loop_triangles:
|
for loop_triangle in mesh_data.loop_triangles:
|
||||||
geometry_object.texture_vertex_faces.append((
|
geometry_object.texture_vertex_faces.append(
|
||||||
geometry_object.texture_vertex_offset + loop_triangle.loops[0],
|
tuple(map(lambda l: geometry_object.texture_vertex_offset + loop_triangle.loops[l], loop_triangle_index_order))
|
||||||
geometry_object.texture_vertex_offset + loop_triangle.loops[1],
|
)
|
||||||
geometry_object.texture_vertex_offset + loop_triangle.loops[2]
|
|
||||||
))
|
|
||||||
|
|
||||||
# Vertex Colors
|
# Vertex Colors
|
||||||
if len(mesh_data.vertex_colors) > 0:
|
if len(mesh_data.vertex_colors) > 0:
|
||||||
vertex_colors = mesh_data.vertex_colors.active.data
|
if mesh_data.vertex_colors.active is not None:
|
||||||
for color in map(lambda x: x.color, vertex_colors):
|
vertex_colors = mesh_data.vertex_colors.active.data
|
||||||
geometry_object.vertex_colors.append(tuple(color[0:3]))
|
for color in map(lambda x: x.color, vertex_colors):
|
||||||
|
geometry_object.vertex_colors.append(tuple(color[0:3]))
|
||||||
|
|
||||||
# Update data offsets for next iteration
|
# Update data offsets for next iteration
|
||||||
geometry_object.texture_vertex_offset += len(mesh_data.loops)
|
geometry_object.texture_vertex_offset += len(mesh_data.loops)
|
||||||
geometry_object.vertex_offset = len(geometry_object.vertices)
|
geometry_object.vertex_offset = len(geometry_object.vertices)
|
||||||
|
|
||||||
|
context.window_manager.progress_update(object_index)
|
||||||
|
|
||||||
|
context.window_manager.progress_end()
|
||||||
|
|
||||||
if len(ase.geometry_objects) == 0:
|
if len(ase.geometry_objects) == 0:
|
||||||
raise ASEBuilderError('At least one mesh object must be selected')
|
raise ASEBuilderError('At least one mesh object must be selected')
|
||||||
|
|
||||||
200
io_scene_ase/exporter.py
Normal file
200
io_scene_ase/exporter.py
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
import os.path
|
||||||
|
from typing import Iterable, List, Set, Union
|
||||||
|
|
||||||
|
from bpy_extras.io_utils import ExportHelper
|
||||||
|
from bpy.props import StringProperty, BoolProperty, CollectionProperty, PointerProperty, IntProperty
|
||||||
|
from bpy.types import Operator, Material, PropertyGroup, UIList, Object
|
||||||
|
from .builder import ASEBuilder, ASEBuilderOptions, ASEBuilderError, get_mesh_objects
|
||||||
|
from .writer import ASEWriter
|
||||||
|
|
||||||
|
|
||||||
|
class ASE_PG_material(PropertyGroup):
|
||||||
|
material: PointerProperty(type=Material)
|
||||||
|
|
||||||
|
|
||||||
|
class ASE_PG_export(PropertyGroup):
|
||||||
|
material_list: CollectionProperty(name='Materials', type=ASE_PG_material)
|
||||||
|
material_list_index: IntProperty(name='Index', default=0)
|
||||||
|
|
||||||
|
|
||||||
|
def get_unique_materials(mesh_objects: Iterable[Object]) -> List[Material]:
|
||||||
|
materials = set()
|
||||||
|
for mesh_object in mesh_objects:
|
||||||
|
for i, material_slot in enumerate(mesh_object.material_slots):
|
||||||
|
material = material_slot.material
|
||||||
|
if material is None:
|
||||||
|
raise RuntimeError('Material slot cannot be empty (index ' + str(i) + ')')
|
||||||
|
materials.add(material)
|
||||||
|
return list(materials)
|
||||||
|
|
||||||
|
|
||||||
|
def populate_material_list(mesh_objects: Iterable[Object], material_list):
|
||||||
|
materials = get_unique_materials(mesh_objects)
|
||||||
|
material_list.clear()
|
||||||
|
for index, material in enumerate(materials):
|
||||||
|
m = material_list.add()
|
||||||
|
m.material = material
|
||||||
|
m.index = index
|
||||||
|
|
||||||
|
|
||||||
|
class ASE_OT_material_list_move_up(Operator):
|
||||||
|
bl_idname = 'ase_export.material_list_item_move_up'
|
||||||
|
bl_label = 'Move Up'
|
||||||
|
bl_options = {'INTERNAL'}
|
||||||
|
bl_description = 'Move the selected material up one slot'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
pg = getattr(context.scene, 'ase_export')
|
||||||
|
return pg.material_list_index > 0
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
pg = getattr(context.scene, 'ase_export')
|
||||||
|
pg.material_list.move(pg.material_list_index, pg.material_list_index - 1)
|
||||||
|
pg.material_list_index -= 1
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class ASE_OT_material_list_move_down(Operator):
|
||||||
|
bl_idname = 'ase_export.material_list_item_move_down'
|
||||||
|
bl_label = 'Move Down'
|
||||||
|
bl_options = {'INTERNAL'}
|
||||||
|
bl_description = 'Move the selected material down one slot'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
pg = getattr(context.scene, 'ase_export')
|
||||||
|
return pg.material_list_index < len(pg.material_list) - 1
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
pg = getattr(context.scene, 'ase_export')
|
||||||
|
pg.material_list.move(pg.material_list_index, pg.material_list_index + 1)
|
||||||
|
pg.material_list_index += 1
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class ASE_UL_materials(UIList):
|
||||||
|
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
||||||
|
row = layout.row()
|
||||||
|
row.prop(item.material, 'name', text='', emboss=False, icon_value=layout.icon(item.material))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ASE_OT_export(Operator, ExportHelper):
|
||||||
|
bl_idname = 'io_scene_ase.ase_export'
|
||||||
|
bl_label = 'Export ASE'
|
||||||
|
bl_space_type = 'PROPERTIES'
|
||||||
|
bl_region_type = 'WINDOW'
|
||||||
|
bl_description = 'Export selected objects to ASE'
|
||||||
|
filename_ext = '.ase'
|
||||||
|
filter_glob: StringProperty(default="*.ase", options={'HIDDEN'}, maxlen=255)
|
||||||
|
use_raw_mesh_data: BoolProperty(default=False, name='Raw Mesh Data', description='No modifiers will be evaluated as part of the exported mesh')
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
|
||||||
|
materials_header, materials_panel = layout.panel('Materials', default_closed=False)
|
||||||
|
materials_header.label(text='Materials')
|
||||||
|
|
||||||
|
if materials_panel:
|
||||||
|
row = materials_panel.row()
|
||||||
|
row.template_list('ASE_UL_materials', '', context.scene.ase_export, 'material_list', context.scene.ase_export, 'material_list_index')
|
||||||
|
col = row.column(align=True)
|
||||||
|
col.operator(ASE_OT_material_list_move_up.bl_idname, icon='TRIA_UP', text='')
|
||||||
|
col.operator(ASE_OT_material_list_move_down.bl_idname, icon='TRIA_DOWN', text='')
|
||||||
|
|
||||||
|
advanced_header, advanced_panel = layout.panel('Advanced', default_closed=True)
|
||||||
|
advanced_header.label(text='Advanced')
|
||||||
|
|
||||||
|
if advanced_panel:
|
||||||
|
advanced_panel.prop(self, 'use_raw_mesh_data')
|
||||||
|
|
||||||
|
def invoke(self, context: 'Context', event: 'Event' ) -> Union[Set[str], Set[int]]:
|
||||||
|
mesh_objects = [x[0] for x in get_mesh_objects(context.selected_objects)]
|
||||||
|
|
||||||
|
pg = getattr(context.scene, 'ase_export')
|
||||||
|
populate_material_list(mesh_objects, pg.material_list)
|
||||||
|
|
||||||
|
context.window_manager.fileselect_add(self)
|
||||||
|
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
options = ASEBuilderOptions()
|
||||||
|
options.use_raw_mesh_data = self.use_raw_mesh_data
|
||||||
|
pg = getattr(context.scene, 'ase_export')
|
||||||
|
options.materials = [x.material for x in pg.material_list]
|
||||||
|
try:
|
||||||
|
ase = ASEBuilder().build(context, options, context.selected_objects)
|
||||||
|
ASEWriter().write(self.filepath, ase)
|
||||||
|
self.report({'INFO'}, 'ASE exported successfully')
|
||||||
|
return {'FINISHED'}
|
||||||
|
except ASEBuilderError as e:
|
||||||
|
self.report({'ERROR'}, str(e))
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
|
||||||
|
class ASE_OT_export_collections(Operator, ExportHelper):
|
||||||
|
bl_idname = 'io_scene_ase.ase_export_collections'
|
||||||
|
bl_label = 'Export Collections to ASE'
|
||||||
|
bl_space_type = 'PROPERTIES'
|
||||||
|
bl_region_type = 'WINDOW'
|
||||||
|
bl_description = 'Batch export collections to ASE. The name of the collection will be used as the filename'
|
||||||
|
filename_ext = '.ase'
|
||||||
|
filter_glob: StringProperty(
|
||||||
|
default="*.ase",
|
||||||
|
options={'HIDDEN'},
|
||||||
|
maxlen=255, # Max internal buffer length, longer would be hilighted.
|
||||||
|
)
|
||||||
|
use_raw_mesh_data: BoolProperty(
|
||||||
|
default=False,
|
||||||
|
description='No modifiers will be evaluated as part of the exported mesh',
|
||||||
|
name='Raw Mesh Data')
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.prop(self, 'use_raw_mesh_data')
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
options = ASEBuilderOptions()
|
||||||
|
options.use_raw_mesh_data = self.use_raw_mesh_data
|
||||||
|
|
||||||
|
# Iterate over all the visible collections in the scene.
|
||||||
|
layer_collections = context.view_layer.layer_collection.children
|
||||||
|
collections = [x.collection for x in layer_collections if not x.hide_viewport and not x.exclude]
|
||||||
|
|
||||||
|
context.window_manager.progress_begin(0, len(layer_collections))
|
||||||
|
|
||||||
|
for i, collection in enumerate(collections):
|
||||||
|
# Iterate over all the objects in the collection.
|
||||||
|
mesh_objects = get_mesh_objects(collection.all_objects)
|
||||||
|
# Get all the materials used by the objects in the collection.
|
||||||
|
options.materials = get_unique_materials([x[0] for x in mesh_objects])
|
||||||
|
|
||||||
|
try:
|
||||||
|
ase = ASEBuilder().build(context, options, collection.all_objects)
|
||||||
|
dirname = os.path.dirname(self.filepath)
|
||||||
|
filepath = os.path.join(dirname, collection.name + '.ase')
|
||||||
|
ASEWriter().write(filepath, ase)
|
||||||
|
except ASEBuilderError as e:
|
||||||
|
self.report({'ERROR'}, str(e))
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
context.window_manager.progress_update(i)
|
||||||
|
|
||||||
|
context.window_manager.progress_end()
|
||||||
|
|
||||||
|
self.report({'INFO'}, f'{len(collections)} collections exported successfully')
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
classes = (
|
||||||
|
ASE_PG_material,
|
||||||
|
ASE_UL_materials,
|
||||||
|
ASE_PG_export,
|
||||||
|
ASE_OT_export,
|
||||||
|
ASE_OT_export_collections,
|
||||||
|
ASE_OT_material_list_move_down,
|
||||||
|
ASE_OT_material_list_move_up,
|
||||||
|
)
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
from .ase import *
|
|
||||||
|
|
||||||
|
|
||||||
class ASEFile(object):
|
class ASEFile(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.commands = []
|
self.commands = []
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import bpy
|
|
||||||
import bpy_extras
|
|
||||||
from bpy.props import StringProperty, FloatProperty, EnumProperty, BoolProperty
|
|
||||||
from .builder import *
|
|
||||||
from .writer import *
|
|
||||||
|
|
||||||
|
|
||||||
class ASE_OT_ExportOperator(bpy.types.Operator, bpy_extras.io_utils.ExportHelper):
|
|
||||||
bl_idname = 'io_scene_ase.ase_export' # important since its how bpy.ops.import_test.some_data is constructed
|
|
||||||
bl_label = 'Export ASE'
|
|
||||||
bl_space_type = 'PROPERTIES'
|
|
||||||
bl_region_type = 'WINDOW'
|
|
||||||
filename_ext = '.ase'
|
|
||||||
filter_glob: StringProperty(
|
|
||||||
default="*.ase",
|
|
||||||
options={'HIDDEN'},
|
|
||||||
maxlen=255, # Max internal buffer length, longer would be hilighted.
|
|
||||||
)
|
|
||||||
units: EnumProperty(
|
|
||||||
default='U',
|
|
||||||
items=(('M', 'Meters', ''),
|
|
||||||
('U', 'Unreal', '')),
|
|
||||||
name='Units'
|
|
||||||
)
|
|
||||||
use_raw_mesh_data: BoolProperty(
|
|
||||||
default=False,
|
|
||||||
description='No modifiers will be evaluated as part of the exported mesh',
|
|
||||||
name='Raw Mesh Data')
|
|
||||||
units_scale = {
|
|
||||||
'M': 60.352,
|
|
||||||
'U': 1.0
|
|
||||||
}
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
layout.prop(self, 'units', expand=False)
|
|
||||||
layout.prop(self, 'use_raw_mesh_data')
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
options = ASEBuilderOptions()
|
|
||||||
options.scale = self.units_scale[self.units]
|
|
||||||
options.use_raw_mesh_data = self.use_raw_mesh_data
|
|
||||||
try:
|
|
||||||
ase = ASEBuilder().build(context, options)
|
|
||||||
ASEWriter().write(self.filepath, ase)
|
|
||||||
self.report({'INFO'}, 'ASE exported successful')
|
|
||||||
return {'FINISHED'}
|
|
||||||
except ASEBuilderError as e:
|
|
||||||
self.report({'ERROR'}, str(e))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
Reference in New Issue
Block a user