31 Commits

Author SHA1 Message Date
Colin Basnett
eb5a9ed564 Fixed and added some strings 2024-06-30 15:50:18 -07:00
Colin Basnett
09895e1fa1 Updated manifest 2024-06-30 11:21:20 -07:00
Colin Basnett
ac4d96b396 Added blender_manifest file for compatiblity with Blender Extensions platform 2024-06-17 12:13:45 -07:00
Colin Basnett
3fe4a6af53 A number of changes and additions to the exporter:
* The "Scale"/"Units" option has been removed from the exporter. If users want to work in proper units, they should override their Scene unit scaling.
* Asset instances can now be exported.
* Added operator to batch export top-level collections.
2024-05-28 21:57:15 -07:00
Colin Basnett
972ea5deda Added the ability to re-order materials when exporting multiple objects 2024-05-27 20:51:17 -07:00
Colin Basnett
2ff4a661f2 Fixed an issue with the order of texture vertex indices when one or more objects has negative scaling 2024-05-27 18:44:13 -07:00
Colin Basnett
956de550b2 Removed debug printing 2024-05-26 14:23:00 -07:00
Colin Basnett
bd90088aed Added logic to detect negative object scaling values and accomodate WYSIWYG normals on export
Also added "export collections" operator that exports each viible collection as an ASE
2024-05-26 13:47:26 -07:00
Colin Basnett
ecfd9897b1 Moved files to named subfolder for easier packaging 2024-02-28 18:50:12 -08:00
Colin Basnett
a3e350e96e Fix for rare error if vertex colors were somehow not active 2024-02-28 18:49:00 -08:00
Colin Basnett
7b417ae425 Incremented version to 2.0.0 and the minimum version to 4.0.0 2023-11-10 23:29:45 -08:00
Colin Basnett
c59f16ed5e Removed deprecated call to calc_normals_split 2023-11-10 23:28:30 -08:00
Colin Basnett
51be4b8ee9 Removed zip file committed in error 2022-08-15 21:53:24 -07:00
Colin Basnett
94b3554f2f Fixed multi-object export texture corruption error
This fixes the bug where exporting 3 or more objects would result in
one or more objects have corrupted texture coordinates
2022-08-15 21:39:13 -07:00
Colin Basnett
ce6ae4a64c Added description to raw mesh data option 2022-08-11 02:09:27 -07:00
Colin Basnett
a593224b14 Added (ASE) to the name of the addon
This should make it easier to find in the addon list when searching
2022-08-11 02:08:26 -07:00
Colin Basnett
feb88794b2 Incremented version to 1.2.0 2022-08-11 02:07:38 -07:00
Colin Basnett
499e6f19c6 The default units are now Unreal instead of Meters 2022-08-11 02:06:46 -07:00
Colin Basnett
1bdc158f08 Merge branch 'feature-wysiwyg' 2022-08-11 00:02:10 -07:00
Colin Basnett
e2e3905e2e Fixed a bug where collision meshes would not properly be exported in WYSIWYG mode 2022-05-20 16:40:55 -07:00
Colin Basnett
77f5aa21a5 Removed defunct comment 2022-05-16 17:29:10 -07:00
Colin Basnett
92f588b760 Initial commit for WYSIWYG export. Modifiers are automatically applied. 2022-05-11 17:12:29 -07:00
Colin Basnett
2b467b2da4 Minor formatting fix and comments 2022-05-01 02:05:48 -07:00
Colin Basnett
4b6231a094 Incremented version to 1.1.1 2022-02-09 14:30:15 -08:00
Colin Basnett
50a7d003cb Added manifold and convexity tests for collision objects. You will now be unable to export a concave or non-manifold collision shapes. 2022-02-08 21:42:26 -08:00
Colin Basnett
bd1a7257fd Added support for vertex colors. 2022-01-22 14:42:57 -08:00
Colin Basnett
7ebeb0d262 Added support for exporting multiple UV layers. 2021-12-27 02:07:50 -08:00
Colin Basnett
8edfac9bfb Fixed a bug where exporting more than two mesh objects would result in a corrupted ASE file. 2021-12-16 15:28:52 -08:00
Colin Basnett
1121b18fcb Incremented version. 2021-08-23 23:24:02 -07:00
Colin Basnett
dcd8c3ea65 Fix for bug #2 (missing unit settings on export in Blender 2.93) 2021-08-23 23:22:37 -07:00
Colin Basnett
283a44aec5 Removed zip file from the repository 🤦 2021-08-13 12:54:05 -07:00
11 changed files with 488 additions and 226 deletions

View File

@@ -3,3 +3,6 @@
This is a Blender addon allowing you to export static meshes to the now-defunct ASE (ASCII Scene Export) format still in use in legacy programs like Unreal Tournament 2004. This is a Blender addon allowing you to export static meshes to the now-defunct ASE (ASCII Scene Export) format still in use in legacy programs like Unreal Tournament 2004.
Check out [this video](https://www.youtube.com/watch?v=gpmBxCGHQjU) on how to install and use the addon. Check out [this video](https://www.youtube.com/watch?v=gpmBxCGHQjU) on how to install and use the addon.
Resources:
* https://wiki.beyondunreal.com/Legacy:ASE_File_Format

38
io_scene_ase/__init__.py Normal file
View File

@@ -0,0 +1,38 @@
if 'bpy' in locals():
import importlib
if 'ase' in locals(): importlib.reload(ase)
if 'builder' in locals(): importlib.reload(builder)
if 'writer' in locals(): importlib.reload(writer)
if 'exporter' in locals(): importlib.reload(exporter)
import bpy
import bpy.utils.previews
from . import ase
from . import builder
from . import writer
from . import exporter
classes = exporter.classes
def menu_func_export(self, context):
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():
for cls in classes:
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)
def unregister():
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
del bpy.types.Scene.ase_export
for cls in classes:
bpy.utils.unregister_class(cls)

View File

@@ -26,14 +26,20 @@ def is_collision_name(name):
return name.startswith('MCDCX_') return name.startswith('MCDCX_')
class ASEUVLayer(object):
def __init__(self):
self.texture_vertices = []
class ASEGeometryObject(object): class ASEGeometryObject(object):
def __init__(self): def __init__(self):
self.name = '' self.name = ''
self.vertices = [] self.vertices = []
self.texture_vertices = [] self.uv_layers = []
self.faces = [] self.faces = []
self.texture_vertex_faces = [] self.texture_vertex_faces = []
self.face_normals = [] self.face_normals = []
self.vertex_colors = []
self.vertex_offset = 0 self.vertex_offset = 0
self.texture_vertex_offset = 0 self.texture_vertex_offset = 0

View 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"

187
io_scene_ase/builder.py Normal file
View File

@@ -0,0 +1,187 @@
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 bmesh
import math
from mathutils import Matrix, Vector
SMOOTHING_GROUP_MAX = 32
class ASEBuilderError(Exception):
pass
class ASEBuilderOptions(object):
def __init__(self):
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):
def build(self, context: Context, options: ASEBuilderOptions, objects: Iterable[Object]):
ase = ASE()
main_geometry_object = None
mesh_objects = get_mesh_objects(objects)
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
if options.use_raw_mesh_data:
mesh_object = obj
mesh_data = mesh_object.data
else:
depsgraph = context.evaluated_depsgraph_get()
bm = bmesh.new()
bm.from_object(obj, depsgraph)
mesh_data = bpy.data.meshes.new('')
bm.to_mesh(mesh_data)
del bm
mesh_object = bpy.data.objects.new('', mesh_data)
mesh_object.matrix_world = matrix_world
if not is_collision_name(obj.name) and main_geometry_object is not None:
geometry_object = main_geometry_object
else:
geometry_object = ASEGeometryObject()
geometry_object.name = obj.name
if not geometry_object.is_collision:
main_geometry_object = geometry_object
ase.geometry_objects.append(geometry_object)
if geometry_object.is_collision:
# Test that collision meshes are manifold and convex.
bm = bmesh.new()
bm.from_mesh(mesh_object.data)
for edge in bm.edges:
if not edge.is_manifold:
del bm
raise ASEBuilderError(f'Collision mesh \'{obj.name}\' is not manifold')
if not edge.is_convex:
del bm
raise ASEBuilderError(f'Collision mesh \'{obj.name}\' is not convex')
if not geometry_object.is_collision and len(obj.data.materials) == 0:
raise ASEBuilderError(f'Mesh \'{obj.name}\' must have at least one material')
vertex_transform = Matrix.Rotation(math.pi, 4, 'Z') @ matrix_world
for vertex_index, vertex in enumerate(mesh_data.vertices):
geometry_object.vertices.append(vertex_transform @ vertex.co)
material_indices = []
if not geometry_object.is_collision:
for mesh_material_index, material in enumerate(obj.data.materials):
if material is None:
raise ASEBuilderError(f'Material slot {mesh_material_index + 1} for mesh \'{obj.name}\' cannot be empty')
material_indices.append(ase.materials.index(material))
mesh_data.calc_loop_triangles()
# Calculate smoothing groups.
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
for face_index, loop_triangle in enumerate(mesh_data.loop_triangles):
face = ASEFace()
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)
if not geometry_object.is_collision:
face.material_index = material_indices[loop_triangle.material_index]
# The UT2K4 importer only accepts 32 smoothing groups. Anything past this completely mangles the
# smoothing groups and effectively makes the whole model use sharp-edge rendering.
# The fix is to constrain the smoothing group between 0 and 31 by applying a modulo of 32 to the actual
# smoothing group index.
# 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
# will be treated as part of the same smoothing group.
face.smoothing = (poly_groups[loop_triangle.polygon_index] - 1) % SMOOTHING_GROUP_MAX
geometry_object.faces.append(face)
if not geometry_object.is_collision:
# Normals
for face_index, loop_triangle in enumerate(mesh_data.loop_triangles):
face_normal = ASEFaceNormal()
face_normal.normal = loop_triangle.normal
face_normal.vertex_normals = []
for i in loop_triangle_index_order:
vertex_normal = ASEVertexNormal()
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]
if should_invert_normals:
vertex_normal.normal = (-Vector(vertex_normal.normal)).to_tuple()
face_normal.vertex_normals.append(vertex_normal)
geometry_object.face_normals.append(face_normal)
# Texture Coordinates
for i, uv_layer_data in enumerate([x.data for x in mesh_data.uv_layers]):
if i >= len(geometry_object.uv_layers):
geometry_object.uv_layers.append(ASEUVLayer())
uv_layer = geometry_object.uv_layers[i]
for loop_index, loop in enumerate(mesh_data.loops):
u, v = uv_layer_data[loop_index].uv
uv_layer.texture_vertices.append((u, v, 0.0))
# Texture Faces
for loop_triangle in mesh_data.loop_triangles:
geometry_object.texture_vertex_faces.append(
tuple(map(lambda l: geometry_object.texture_vertex_offset + loop_triangle.loops[l], loop_triangle_index_order))
)
# Vertex Colors
if len(mesh_data.vertex_colors) > 0:
if mesh_data.vertex_colors.active is not None:
vertex_colors = mesh_data.vertex_colors.active.data
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
geometry_object.texture_vertex_offset += len(mesh_data.loops)
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:
raise ASEBuilderError('At least one mesh object must be selected')
if main_geometry_object is None:
raise ASEBuilderError('At least one non-collision mesh must be exported')
return ase

200
io_scene_ase/exporter.py Normal file
View 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,
)

View File

@@ -1,6 +1,3 @@
from .ase import *
class ASEFile(object): class ASEFile(object):
def __init__(self): def __init__(self):
self.commands = [] self.commands = []
@@ -146,18 +143,20 @@ class ASEWriter(object):
face_node.push_sub_command('MESH_MTLID').push_datum(face.material_index) face_node.push_sub_command('MESH_MTLID').push_datum(face.material_index)
# Texture Coordinates # Texture Coordinates
if len(geometry_object.texture_vertices) > 0: for i, uv_layer in enumerate(geometry_object.uv_layers):
mesh_node.push_child('MESH_NUMTVERTEX').push_datum(len(geometry_object.texture_vertices)) parent_node = mesh_node if i == 0 else mesh_node.push_child('MESH_MAPPINGCHANNEL')
tvertlist_node = mesh_node.push_child('MESH_TVERTLIST') if i > 0:
for tvert_index, tvert in enumerate(geometry_object.texture_vertices): parent_node.push_datum(i + 1)
parent_node.push_child('MESH_NUMTVERTEX').push_datum(len(uv_layer.texture_vertices))
tvertlist_node = parent_node.push_child('MESH_TVERTLIST')
for tvert_index, tvert in enumerate(uv_layer.texture_vertices):
tvert_node = tvertlist_node.push_child('MESH_TVERT') tvert_node = tvertlist_node.push_child('MESH_TVERT')
tvert_node.push_datum(tvert_index) tvert_node.push_datum(tvert_index)
tvert_node.push_data(list(tvert)) tvert_node.push_data(list(tvert))
# Texture Faces # Texture Faces
if len(geometry_object.texture_vertex_faces) > 0: if len(geometry_object.texture_vertex_faces) > 0:
mesh_node.push_child('MESH_NUMTVFACES').push_datum(len(geometry_object.texture_vertex_faces)) parent_node.push_child('MESH_NUMTVFACES').push_datum(len(geometry_object.texture_vertex_faces))
texture_faces_node = mesh_node.push_child('MESH_TFACELIST') texture_faces_node = parent_node.push_child('MESH_TFACELIST')
for texture_face_index, texture_face in enumerate(geometry_object.texture_vertex_faces): for texture_face_index, texture_face in enumerate(geometry_object.texture_vertex_faces):
texture_face_node = texture_faces_node.push_child('MESH_TFACE') texture_face_node = texture_faces_node.push_child('MESH_TFACE')
texture_face_node.push_data([texture_face_index] + list(texture_face)) texture_face_node.push_data([texture_face_index] + list(texture_face))
@@ -174,6 +173,18 @@ class ASEWriter(object):
vertex_normal_node.push_datum(vertex_normal.vertex_index) vertex_normal_node.push_datum(vertex_normal.vertex_index)
vertex_normal_node.push_data(list(vertex_normal.normal)) vertex_normal_node.push_data(list(vertex_normal.normal))
# Vertex Colors
if len(geometry_object.vertex_colors) > 0:
mesh_node.push_child('MESH_NUMCVERTEX').push_datum(len(geometry_object.vertex_colors))
cvert_list = mesh_node.push_child('MESH_CVERTLIST')
for i, vertex_color in enumerate(geometry_object.vertex_colors):
cvert_list.push_child('MESH_VERTCOL').push_datum(i).push_data(vertex_color)
parent_node.push_child('MESH_NUMCVFACES').push_datum(len(geometry_object.texture_vertex_faces))
texture_faces_node = parent_node.push_child('MESH_CFACELIST')
for texture_face_index, texture_face in enumerate(geometry_object.texture_vertex_faces):
texture_face_node = texture_faces_node.push_child('MESH_CFACE')
texture_face_node.push_data([texture_face_index] + list(texture_face))
geomobject_node.push_child('MATERIAL_REF').push_datum(0) geomobject_node.push_child('MATERIAL_REF').push_datum(0)
return root return root

View File

@@ -1,51 +0,0 @@
bl_info = {
'name': 'ASCII Scene Export',
'description': 'Export ASE (ASCII Scene Export) files',
'author': 'Colin Basnett (Darklight Games)',
'version': (1, 0, 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():
import importlib
if 'ase' in locals(): importlib.reload(ase)
if 'builder' in locals(): importlib.reload(builder)
if 'writer' in locals(): importlib.reload(writer)
if 'exporter' in locals(): importlib.reload(exporter)
import bpy
import bpy.utils.previews
from bpy.props import IntProperty, CollectionProperty, StringProperty
import os
from . import ase
from . import builder
from . import writer
from . import exporter
classes = (
exporter.ASE_OT_ExportOperator,
)
def menu_func_export(self, context):
self.layout.operator(exporter.ASE_OT_ExportOperator.bl_idname, text='ASCII Scene Export (.ase)')
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
def unregister():
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
for cls in classes:
bpy.utils.unregister_class(cls)

View File

@@ -1,112 +0,0 @@
from .ase import *
import bpy
import bmesh
import math
from mathutils import Matrix
class ASEBuilderError(Exception):
pass
class ASEBuilderOptions(object):
def __init__(self):
self.scale = 1.0
class ASEBuilder(object):
def build(self, context, options: ASEBuilderOptions):
ase = ASE()
main_geometry_object = None
for obj in context.selected_objects:
if obj is None or obj.type != 'MESH':
continue
mesh_data = obj.data
if not is_collision_name(obj.name) and main_geometry_object is not None:
geometry_object = main_geometry_object
else:
geometry_object = ASEGeometryObject()
geometry_object.name = obj.name
if not geometry_object.is_collision:
main_geometry_object = geometry_object
ase.geometry_objects.append(geometry_object)
if not geometry_object.is_collision and len(mesh_data.materials) == 0:
raise ASEBuilderError(f'Mesh \'{obj.name}\' must have at least one material')
geometry_object.vertex_offset += len(geometry_object.vertices)
vertex_transform = Matrix.Scale(options.scale, 4) @ Matrix.Rotation(math.pi, 4, 'Z') @ obj.matrix_world
for vertex_index, vertex in enumerate(mesh_data.vertices):
geometry_object.vertices.append(vertex_transform @ vertex.co)
material_indices = []
if not geometry_object.is_collision:
for mesh_material_index, material in enumerate(mesh_data.materials):
if material is None:
raise ASEBuilderError(f'Material slot {mesh_material_index + 1} for mesh \'{obj.name}\' cannot be empty')
try:
# 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_normals_split()
poly_groups, groups = mesh_data.calc_smooth_groups(use_bitflags=False)
# Faces
for face_index, loop_triangle in enumerate(mesh_data.loop_triangles):
face = ASEFace()
face.a = geometry_object.vertex_offset + mesh_data.loops[loop_triangle.loops[0]].vertex_index
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:
face.material_index = material_indices[loop_triangle.material_index]
# The UT2K4 importer only accepts 32 smoothing groups. Anything past this completely mangles the
# smoothing groups and effectively makes the whole model use sharp-edge rendering.
face.smoothing = (poly_groups[loop_triangle.polygon_index] - 1) % 32
geometry_object.faces.append(face)
# Normals
if not geometry_object.is_collision:
for face_index, loop_triangle in enumerate(mesh_data.loop_triangles):
face_normal = ASEFaceNormal()
face_normal.normal = loop_triangle.normal
face_normal.vertex_normals = []
for i in range(3):
vertex_normal = ASEVertexNormal()
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]
face_normal.vertex_normals.append(vertex_normal)
geometry_object.face_normals.append(face_normal)
uv_layer = mesh_data.uv_layers.active.data
# Texture Coordinates
geometry_object.texture_vertex_offset += len(geometry_object.texture_vertices)
if not geometry_object.is_collision:
for loop_index, loop in enumerate(mesh_data.loops):
u, v = uv_layer[loop_index].uv
geometry_object.texture_vertices.append((u, v, 0.0))
# Texture Faces
if not geometry_object.is_collision:
for loop_triangle in mesh_data.loop_triangles:
geometry_object.texture_vertex_faces.append((
geometry_object.texture_vertex_offset + loop_triangle.loops[0],
geometry_object.texture_vertex_offset + loop_triangle.loops[1],
geometry_object.texture_vertex_offset + loop_triangle.loops[2]
))
if len(ase.geometry_objects) == 0:
raise ASEBuilderError('At least one mesh object must be selected')
if main_geometry_object is None:
raise ASEBuilderError('At least one non-collision mesh must be exported')
return ase

View File

@@ -1,47 +0,0 @@
import bpy
import bpy_extras
from bpy.props import StringProperty, FloatProperty, EnumProperty
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(
items=(('M', 'Meters', ''),
('U', 'Unreal', '')),
name='Units'
)
units_scale = {
'M': 60.352,
'U': 1.0
}
def draw(self, context):
layout = self.layout
layout.prop(self, 'units', expand=False)
def execute(self, context):
options = ASEBuilderOptions()
options.scale = self.units_scale[self.units]
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'}

Binary file not shown.