Compare commits
8 Commits
feature-im
...
feature-as
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0febd57503 | ||
|
|
51be4b8ee9 | ||
|
|
94b3554f2f | ||
|
|
ce6ae4a64c | ||
|
|
a593224b14 | ||
|
|
feb88794b2 | ||
|
|
499e6f19c6 | ||
|
|
1bdc158f08 |
@@ -1 +0,0 @@
|
|||||||
python-ase
|
|
||||||
196
src/__init__.py
196
src/__init__.py
@@ -1,8 +1,8 @@
|
|||||||
bl_info = {
|
bl_info = {
|
||||||
'name': 'ASCII Scene Export',
|
'name': 'ASCII Scene Export (ASE)',
|
||||||
'description': 'Export ASE (ASCII Scene Export) files',
|
'description': 'Export ASE (ASCII Scene Export) files',
|
||||||
'author': 'Colin Basnett (Darklight Games)',
|
'author': 'Colin Basnett (Darklight Games)',
|
||||||
'version': (1, 1, 2),
|
'version': (1, 2, 1),
|
||||||
'blender': (2, 90, 0),
|
'blender': (2, 90, 0),
|
||||||
'location': 'File > Import-Export',
|
'location': 'File > Import-Export',
|
||||||
'warning': 'This add-on is under development.',
|
'warning': 'This add-on is under development.',
|
||||||
@@ -12,204 +12,40 @@ bl_info = {
|
|||||||
'category': 'Import-Export'
|
'category': 'Import-Export'
|
||||||
}
|
}
|
||||||
|
|
||||||
import subprocess
|
if 'bpy' in locals():
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from collections import namedtuple
|
|
||||||
import bpy
|
|
||||||
|
|
||||||
|
|
||||||
def install_pip():
|
|
||||||
"""
|
|
||||||
Installs pip if not already present. Please note that ensurepip.bootstrap() also calls pip, which adds the
|
|
||||||
environment variable PIP_REQ_TRACKER. After ensurepip.bootstrap() finishes execution, the directory doesn't exist
|
|
||||||
anymore. However, when subprocess is used to call pip, in order to install a package, the environment variables
|
|
||||||
still contain PIP_REQ_TRACKER with the now nonexistent path. This is a problem since pip checks if PIP_REQ_TRACKER
|
|
||||||
is set and if it is, attempts to use it as temp directory. This would result in an error because the
|
|
||||||
directory can't be found. Therefore, PIP_REQ_TRACKER needs to be removed from environment variables.
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Check if pip is already installed
|
|
||||||
subprocess.run([sys.executable, '-m', 'pip', '--version'], check=True)
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
import ensurepip
|
|
||||||
|
|
||||||
ensurepip.bootstrap()
|
|
||||||
os.environ.pop('PIP_REQ_TRACKER', None)
|
|
||||||
|
|
||||||
|
|
||||||
def install_and_import_module(module_name, package_name=None, global_name=None):
|
|
||||||
"""
|
|
||||||
Installs the package through pip and attempts to import the installed module.
|
|
||||||
:param module_name: Module to import.
|
|
||||||
:param package_name: (Optional) Name of the package that needs to be installed. If None it is assumed to be equal
|
|
||||||
to the module_name.
|
|
||||||
:param global_name: (Optional) Name under which the module is imported. If None the module_name will be used.
|
|
||||||
This allows to import under a different name with the same effect as e.g. "import numpy as np" where "np" is
|
|
||||||
the global_name under which the module can be accessed.
|
|
||||||
:raises: subprocess.CalledProcessError and ImportError
|
|
||||||
"""
|
|
||||||
if package_name is None:
|
|
||||||
package_name = module_name
|
|
||||||
|
|
||||||
if global_name is None:
|
|
||||||
global_name = module_name
|
|
||||||
|
|
||||||
# Blender disables the loading of user site-packages by default. However, pip will still check them to determine
|
|
||||||
# if a dependency is already installed. This can cause problems if the packages is installed in the user
|
|
||||||
# site-packages and pip deems the requirement satisfied, but Blender cannot import the package from the user
|
|
||||||
# site-packages. Hence, the environment variable PYTHONNOUSERSITE is set to disallow pip from checking the user
|
|
||||||
# site-packages. If the package is not already installed for Blender's Python interpreter, it will then try to.
|
|
||||||
# The paths used by pip can be checked with `subprocess.run([bpy.app.binary_path_python, "-m", "site"], check=True)`
|
|
||||||
|
|
||||||
# Create a copy of the environment variables and modify them for the subprocess call
|
|
||||||
environ_copy = dict(os.environ)
|
|
||||||
environ_copy['PYTHONNOUSERSITE'] = '1'
|
|
||||||
|
|
||||||
subprocess.run([sys.executable, '-m', 'pip', 'install', package_name], check=True, env=environ_copy)
|
|
||||||
|
|
||||||
# The installation succeeded, attempt to import the module again
|
|
||||||
import_module(module_name, global_name)
|
|
||||||
|
|
||||||
|
|
||||||
class EXAMPLE_OT_install_dependencies(bpy.types.Operator):
|
|
||||||
bl_idname = 'io_scene_ase.install_dependencies'
|
|
||||||
bl_label = 'Install dependencies'
|
|
||||||
bl_description = ('Downloads and installs the required python packages for this add-on. '
|
|
||||||
'Internet connection is required. Blender may have to be started with '
|
|
||||||
'elevated permissions in order to install the package')
|
|
||||||
bl_options = {'REGISTER', 'INTERNAL'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(self, context):
|
|
||||||
# Deactivate when dependencies have been installed
|
|
||||||
return not dependencies_installed
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
try:
|
|
||||||
install_pip()
|
|
||||||
for dependency in dependencies:
|
|
||||||
install_and_import_module(module_name=dependency.module,
|
|
||||||
package_name=dependency.package,
|
|
||||||
global_name=dependency.name)
|
|
||||||
except (subprocess.CalledProcessError, ImportError) as err:
|
|
||||||
self.report({'ERROR'}, str(err))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
global dependencies_installed
|
|
||||||
dependencies_installed = True
|
|
||||||
|
|
||||||
# Register the panels, operators, etc. since dependencies are installed
|
|
||||||
for cls in classes:
|
|
||||||
bpy.utils.register_class(cls)
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class EXAMPLE_preferences(bpy.types.AddonPreferences):
|
|
||||||
bl_idname = __name__
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
layout.operator(EXAMPLE_OT_install_dependencies.bl_idname, icon='CONSOLE')
|
|
||||||
|
|
||||||
|
|
||||||
Dependency = namedtuple('Dependency', ['module', 'package', 'name'])
|
|
||||||
|
|
||||||
dependencies = (Dependency(module='asepy', package=None, name=None),)
|
|
||||||
|
|
||||||
dependencies_installed = False
|
|
||||||
|
|
||||||
|
|
||||||
def import_module(module_name, global_name=None, reload=True):
|
|
||||||
"""
|
|
||||||
Import a module.
|
|
||||||
:param module_name: Module to import.
|
|
||||||
:param global_name: (Optional) Name under which the module is imported. If None the module_name will be used.
|
|
||||||
This allows to import under a different name with the same effect as e.g. "import numpy as np" where "np" is
|
|
||||||
the global_name under which the module can be accessed.
|
|
||||||
:raises: ImportError and ModuleNotFoundError
|
|
||||||
"""
|
|
||||||
if global_name is None:
|
|
||||||
global_name = module_name
|
|
||||||
|
|
||||||
if global_name in globals():
|
|
||||||
importlib.reload(globals()[global_name])
|
|
||||||
else:
|
|
||||||
# Attempt to import the module and assign it to globals dictionary. This allow to access the module under
|
|
||||||
# the given name, just like the regular import would.
|
|
||||||
globals()[global_name] = importlib.import_module(module_name)
|
|
||||||
|
|
||||||
|
|
||||||
preference_classes = (EXAMPLE_OT_install_dependencies,
|
|
||||||
EXAMPLE_preferences)
|
|
||||||
|
|
||||||
if __name__ == 'io_scene_ase':
|
|
||||||
if 'bpy' in locals():
|
|
||||||
import importlib
|
import importlib
|
||||||
if 'ase' in locals(): importlib.reload(ase)
|
if 'ase' in locals(): importlib.reload(ase)
|
||||||
if 'builder' in locals(): importlib.reload(builder)
|
if 'builder' in locals(): importlib.reload(builder)
|
||||||
if 'writer' in locals(): importlib.reload(writer)
|
if 'writer' in locals(): importlib.reload(writer)
|
||||||
if 'exporter' in locals(): importlib.reload(exporter)
|
if 'exporter' in locals(): importlib.reload(exporter)
|
||||||
if 'reader' in locals(): importlib.reload(reader)
|
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import bpy.utils.previews
|
from bpy.props import PointerProperty
|
||||||
from . import ase
|
from . import ase
|
||||||
from . import builder
|
from . import builder
|
||||||
from . import writer
|
from . import writer
|
||||||
from . import exporter
|
from . import exporter
|
||||||
|
|
||||||
print('dependencies installed??')
|
classes = exporter.__classes__
|
||||||
print(dependencies_installed)
|
|
||||||
|
|
||||||
if dependencies_installed:
|
|
||||||
from . import reader
|
|
||||||
|
|
||||||
classes = (
|
|
||||||
exporter.ASE_OT_ExportOperator,
|
|
||||||
)
|
|
||||||
|
|
||||||
if dependencies_installed:
|
|
||||||
classes += reader.classes
|
|
||||||
|
|
||||||
|
|
||||||
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_ExportOperator.bl_idname, text='ASCII Scene Export (.ase)')
|
||||||
|
|
||||||
def menu_func_import(self, context):
|
|
||||||
self.layout.operator(reader.ASE_OT_ImportOperator.bl_idname, text='ASCII Scene Export (.ase)')
|
|
||||||
|
|
||||||
|
|
||||||
def register():
|
|
||||||
global dependencies_installed
|
|
||||||
|
|
||||||
for cls in preference_classes:
|
|
||||||
bpy.utils.register_class(cls)
|
|
||||||
|
|
||||||
try:
|
|
||||||
for dependency in dependencies:
|
|
||||||
import_module(module_name=dependency.module, global_name=dependency.name)
|
|
||||||
dependencies_installed = True
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
# Don't register other panels, operators etc.
|
|
||||||
return
|
|
||||||
|
|
||||||
|
def register():
|
||||||
for cls in classes:
|
for cls in classes:
|
||||||
bpy.utils.register_class(cls)
|
bpy.utils.register_class(cls)
|
||||||
|
|
||||||
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
|
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
|
||||||
|
|
||||||
if 'reader' in locals():
|
bpy.types.Scene.ase_export = PointerProperty(type=exporter.AseExportPropertyGroup)
|
||||||
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
|
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
|
del bpy.types.Scene.ase_export
|
||||||
|
|
||||||
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
|
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
|
||||||
if 'reader' in locals():
|
|
||||||
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
|
|
||||||
|
|
||||||
for cls in classes:
|
for cls in classes:
|
||||||
bpy.utils.unregister_class(cls)
|
bpy.utils.unregister_class(cls)
|
||||||
|
|||||||
41
src/ase.py
41
src/ase.py
@@ -1,4 +1,7 @@
|
|||||||
class ASEFace(object):
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
class AseFace(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.a = 0
|
self.a = 0
|
||||||
self.b = 0
|
self.b = 0
|
||||||
@@ -10,45 +13,37 @@ class ASEFace(object):
|
|||||||
self.material_index = 0
|
self.material_index = 0
|
||||||
|
|
||||||
|
|
||||||
class ASEVertexNormal(object):
|
class AseVertexNormal(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.vertex_index = 0
|
self.vertex_index = 0
|
||||||
self.normal = (0.0, 0.0, 0.0)
|
self.normal = (0.0, 0.0, 0.0)
|
||||||
|
|
||||||
|
|
||||||
class ASEFaceNormal(object):
|
class AseFaceNormal(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.normal = (0.0, 0.0, 1.0)
|
self.normal = (0.0, 0.0, 1.0)
|
||||||
self.vertex_normals = [ASEVertexNormal()] * 3
|
self.vertex_normals = [AseVertexNormal()] * 3
|
||||||
|
|
||||||
|
|
||||||
def is_collision_name(name):
|
class AseUVLayer(object):
|
||||||
return name.startswith('MCDCX_')
|
|
||||||
|
|
||||||
|
|
||||||
class ASEUVLayer(object):
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.texture_vertices = []
|
self.texture_vertices: List[Tuple[float, float, float]] = []
|
||||||
|
|
||||||
|
|
||||||
class ASEGeometryObject(object):
|
class AseGeometryObject(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.name = ''
|
self.name = ''
|
||||||
self.vertices = []
|
self.vertices: List[Tuple[float, float, float]] = []
|
||||||
self.uv_layers = []
|
self.uv_layers: List[AseUVLayer] = []
|
||||||
self.faces = []
|
self.faces: List[AseFace] = []
|
||||||
self.texture_vertex_faces = []
|
self.texture_vertex_faces = []
|
||||||
self.face_normals = []
|
self.face_normals: List[AseFaceNormal] = []
|
||||||
self.vertex_colors = []
|
self.vertex_colors: List[Tuple[float, float, float]] = []
|
||||||
self.vertex_offset = 0
|
self.vertex_offset = 0
|
||||||
self.texture_vertex_offset = 0
|
self.texture_vertex_offset = 0
|
||||||
|
|
||||||
@property
|
|
||||||
def is_collision(self):
|
|
||||||
return is_collision_name(self.name)
|
|
||||||
|
|
||||||
|
class Ase(object):
|
||||||
class ASE(object):
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.materials = []
|
self.materials: List[str] = []
|
||||||
self.geometry_objects = []
|
self.geometry_objects: List[AseGeometryObject] = []
|
||||||
|
|||||||
@@ -5,22 +5,29 @@ import math
|
|||||||
from mathutils import Matrix
|
from mathutils import Matrix
|
||||||
|
|
||||||
|
|
||||||
class ASEBuilderError(Exception):
|
def is_collision_name(name: str):
|
||||||
|
return name.startswith('MCDCX_')
|
||||||
|
|
||||||
|
|
||||||
|
def is_collision(geometry_object: AseGeometryObject):
|
||||||
|
return is_collision_name(geometry_object.name)
|
||||||
|
|
||||||
|
|
||||||
|
class AseBuilderError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ASEBuilderOptions(object):
|
class AseBuilderOptions(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.scale = 1.0
|
self.scale = 1.0
|
||||||
self.use_raw_mesh_data = False
|
self.use_raw_mesh_data = False
|
||||||
|
|
||||||
|
|
||||||
class ASEBuilder(object):
|
def build_ase(context: bpy.types.Context, options: AseBuilderOptions):
|
||||||
def build(self, context, options: ASEBuilderOptions):
|
ase = Ase()
|
||||||
ase = ASE()
|
|
||||||
|
|
||||||
main_geometry_object = None
|
main_geometry_object = None
|
||||||
for selected_object in context.selected_objects:
|
for selected_object in context.view_layer.objects.selected:
|
||||||
if selected_object is None or selected_object.type != 'MESH':
|
if selected_object is None or selected_object.type != 'MESH':
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -41,36 +48,36 @@ class ASEBuilder(object):
|
|||||||
if not is_collision_name(selected_object.name) and main_geometry_object is not None:
|
if not is_collision_name(selected_object.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 = selected_object.name
|
||||||
if not geometry_object.is_collision:
|
if not is_collision(geometry_object):
|
||||||
main_geometry_object = geometry_object
|
main_geometry_object = geometry_object
|
||||||
ase.geometry_objects.append(geometry_object)
|
ase.geometry_objects.append(geometry_object)
|
||||||
|
|
||||||
if geometry_object.is_collision:
|
if is_collision(geometry_object):
|
||||||
# Test that collision meshes are manifold and convex.
|
# Test that collision meshes are manifold and convex.
|
||||||
bm = bmesh.new()
|
bm = bmesh.new()
|
||||||
bm.from_mesh(mesh_object.data)
|
bm.from_mesh(mesh_data)
|
||||||
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 \'{selected_object.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 \'{selected_object.name}\' is not convex')
|
||||||
|
|
||||||
if not geometry_object.is_collision and len(selected_object.data.materials) == 0:
|
if not is_collision(geometry_object) and len(selected_object.data.materials) == 0:
|
||||||
raise ASEBuilderError(f'Mesh \'{selected_object.name}\' must have at least one material')
|
raise AseBuilderError(f'Mesh \'{selected_object.name}\' must have at least one material')
|
||||||
|
|
||||||
vertex_transform = Matrix.Scale(options.scale, 4) @ Matrix.Rotation(math.pi, 4, 'Z') @ mesh_object.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 in 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 is_collision(geometry_object):
|
||||||
for mesh_material_index, material in enumerate(selected_object.data.materials):
|
for mesh_material_index, material in enumerate(selected_object.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 \'{selected_object.name}\' cannot be empty')
|
||||||
try:
|
try:
|
||||||
# Reuse existing material entries for duplicates
|
# Reuse existing material entries for duplicates
|
||||||
material_index = ase.materials.index(material.name)
|
material_index = ase.materials.index(material.name)
|
||||||
@@ -84,12 +91,12 @@ class ASEBuilder(object):
|
|||||||
poly_groups, groups = mesh_data.calc_smooth_groups(use_bitflags=False)
|
poly_groups, groups = mesh_data.calc_smooth_groups(use_bitflags=False)
|
||||||
|
|
||||||
# Faces
|
# Faces
|
||||||
for face_index, loop_triangle in enumerate(mesh_data.loop_triangles):
|
for loop_triangle in 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 = 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.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
|
face.c = geometry_object.vertex_offset + mesh_data.loops[loop_triangle.loops[2]].vertex_index
|
||||||
if not geometry_object.is_collision:
|
if not is_collision(geometry_object):
|
||||||
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
|
||||||
# smoothing groups and effectively makes the whole model use sharp-edge rendering.
|
# smoothing groups and effectively makes the whole model use sharp-edge rendering.
|
||||||
@@ -101,14 +108,14 @@ class ASEBuilder(object):
|
|||||||
face.smoothing = (poly_groups[loop_triangle.polygon_index] - 1) % 32
|
face.smoothing = (poly_groups[loop_triangle.polygon_index] - 1) % 32
|
||||||
geometry_object.faces.append(face)
|
geometry_object.faces.append(face)
|
||||||
|
|
||||||
if not geometry_object.is_collision:
|
if not is_collision(geometry_object):
|
||||||
# Normals
|
# Normals
|
||||||
for face_index, loop_triangle in enumerate(mesh_data.loop_triangles):
|
for loop_triangle in mesh_data.loop_triangles:
|
||||||
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 range(3):
|
||||||
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]
|
||||||
face_normal.vertex_normals.append(vertex_normal)
|
face_normal.vertex_normals.append(vertex_normal)
|
||||||
@@ -117,7 +124,7 @@ class ASEBuilder(object):
|
|||||||
# Texture Coordinates
|
# Texture Coordinates
|
||||||
for i, uv_layer_data in enumerate([x.data for x in mesh_data.uv_layers]):
|
for i, uv_layer_data in enumerate([x.data for x in mesh_data.uv_layers]):
|
||||||
if i >= len(geometry_object.uv_layers):
|
if i >= len(geometry_object.uv_layers):
|
||||||
geometry_object.uv_layers.append(ASEUVLayer())
|
geometry_object.uv_layers.append(AseUVLayer())
|
||||||
uv_layer = geometry_object.uv_layers[i]
|
uv_layer = geometry_object.uv_layers[i]
|
||||||
for loop_index, loop in enumerate(mesh_data.loops):
|
for loop_index, loop in enumerate(mesh_data.loops):
|
||||||
u, v = uv_layer_data[loop_index].uv
|
u, v = uv_layer_data[loop_index].uv
|
||||||
@@ -138,13 +145,13 @@ class ASEBuilder(object):
|
|||||||
geometry_object.vertex_colors.append(tuple(color[0:3]))
|
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)
|
||||||
|
|
||||||
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')
|
||||||
|
|
||||||
if main_geometry_object is None:
|
if main_geometry_object is None:
|
||||||
raise ASEBuilderError('At least one non-collision mesh must be exported')
|
raise AseBuilderError('At least one non-collision mesh must be exported')
|
||||||
|
|
||||||
return ase
|
return ase
|
||||||
|
|||||||
109
src/exporter.py
109
src/exporter.py
@@ -1,10 +1,53 @@
|
|||||||
import bpy
|
|
||||||
import bpy_extras
|
import bpy_extras
|
||||||
from bpy.props import StringProperty, FloatProperty, EnumProperty, BoolProperty
|
import typing
|
||||||
|
from bpy.types import UIList, Context, UILayout, AnyType, Operator
|
||||||
|
from bpy.props import StringProperty, EnumProperty, BoolProperty, PointerProperty, CollectionProperty, IntProperty
|
||||||
from .builder import *
|
from .builder import *
|
||||||
from .writer import *
|
from .writer import *
|
||||||
|
|
||||||
|
|
||||||
|
class AseExportCollectionPropertyGroup(bpy.types.PropertyGroup):
|
||||||
|
is_selected: BoolProperty()
|
||||||
|
name: StringProperty()
|
||||||
|
collection: PointerProperty(type=bpy.types.Collection)
|
||||||
|
|
||||||
|
|
||||||
|
class AseExportPropertyGroup(bpy.types.PropertyGroup):
|
||||||
|
collection_list: CollectionProperty(type=AseExportCollectionPropertyGroup)
|
||||||
|
collection_list_index: IntProperty()
|
||||||
|
|
||||||
|
|
||||||
|
class ASE_UL_CollectionList(UIList):
|
||||||
|
def draw_item(self, context: Context, layout: UILayout, data: AnyType, item: AnyType, icon: int,
|
||||||
|
active_data: AnyType, active_property: str, index: int = 0, flt_flag: int = 0):
|
||||||
|
collection: bpy.types.Collection = getattr(item, 'collection')
|
||||||
|
row = layout.row()
|
||||||
|
row.prop(item, 'is_selected', text='')
|
||||||
|
row.label(text=collection.name, icon='OUTLINER_COLLECTION')
|
||||||
|
|
||||||
|
|
||||||
|
class AseExportCollectionsSelectAll(Operator):
|
||||||
|
bl_idname = 'ase_export.collections_select_all'
|
||||||
|
bl_label = 'All'
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> typing.Union[typing.Set[str], typing.Set[int]]:
|
||||||
|
pg = getattr(context.scene, 'ase_export')
|
||||||
|
for collection in pg.collection_list:
|
||||||
|
collection.is_selected = True
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class AseExportCollectionsDeselectAll(Operator):
|
||||||
|
bl_idname = 'ase_export.collections_deselect_all'
|
||||||
|
bl_label = 'None'
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> typing.Union[typing.Set[str], typing.Set[int]]:
|
||||||
|
pg = getattr(context.scene, 'ase_export')
|
||||||
|
for collection in pg.collection_list:
|
||||||
|
collection.is_selected = False
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
class ASE_OT_ExportOperator(bpy.types.Operator, bpy_extras.io_utils.ExportHelper):
|
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_idname = 'io_scene_ase.ase_export' # important since its how bpy.ops.import_test.some_data is constructed
|
||||||
bl_label = 'Export ASE'
|
bl_label = 'Export ASE'
|
||||||
@@ -17,32 +60,76 @@ class ASE_OT_ExportOperator(bpy.types.Operator, bpy_extras.io_utils.ExportHelper
|
|||||||
maxlen=255, # Max internal buffer length, longer would be hilighted.
|
maxlen=255, # Max internal buffer length, longer would be hilighted.
|
||||||
)
|
)
|
||||||
units: EnumProperty(
|
units: EnumProperty(
|
||||||
|
default='U',
|
||||||
items=(('M', 'Meters', ''),
|
items=(('M', 'Meters', ''),
|
||||||
('U', 'Unreal', '')),
|
('U', 'Unreal', '')),
|
||||||
name='Units',
|
name='Units'
|
||||||
default='U'
|
|
||||||
)
|
)
|
||||||
use_raw_mesh_data: BoolProperty(default=False, name='Raw Mesh Data')
|
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 = {
|
units_scale = {
|
||||||
'M': 60.352,
|
'M': 60.352,
|
||||||
'U': 1.0
|
'U': 1.0
|
||||||
}
|
}
|
||||||
|
should_use_sub_materials: BoolProperty(
|
||||||
|
default=True,
|
||||||
|
description='Material format', # fill this in with a more human-friendly name/description
|
||||||
|
name='Use Sub-materials'
|
||||||
|
)
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
|
pg = getattr(context.scene, 'ase_export')
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
rows = max(3, min(len(pg.collection_list), 10))
|
||||||
layout.prop(self, 'units', expand=False)
|
layout.prop(self, 'units', expand=False)
|
||||||
layout.prop(self, 'use_raw_mesh_data')
|
layout.prop(self, 'use_raw_mesh_data')
|
||||||
|
layout.prop(self, 'should_use_sub_materials')
|
||||||
|
|
||||||
|
# # SELECT ALL/NONE
|
||||||
|
# row = layout.row(align=True)
|
||||||
|
# row.label(text='Select')
|
||||||
|
# row.operator(AseExportCollectionsSelectAll.bl_idname, text='All', icon='CHECKBOX_HLT')
|
||||||
|
# row.operator(AseExportCollectionsDeselectAll.bl_idname, text='None', icon='CHECKBOX_DEHLT')
|
||||||
|
#
|
||||||
|
# layout.template_list('ASE_UL_CollectionList', '', pg, 'collection_list', pg, 'collection_list_index', rows=rows)
|
||||||
|
|
||||||
|
def invoke(self, context: bpy.types.Context, event):
|
||||||
|
# TODO: build a list of collections that have meshes in them
|
||||||
|
pg = getattr(context.scene, 'ase_export')
|
||||||
|
pg.collection_list.clear()
|
||||||
|
for collection in bpy.data.collections:
|
||||||
|
has_meshes = any(map(lambda x: x.type == 'MESH', collection.objects))
|
||||||
|
if has_meshes:
|
||||||
|
c = pg.collection_list.add()
|
||||||
|
c.collection = collection
|
||||||
|
|
||||||
|
context.window_manager.fileselect_add(self)
|
||||||
|
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
options = ASEBuilderOptions()
|
options = AseBuilderOptions()
|
||||||
options.scale = self.units_scale[self.units]
|
options.scale = self.units_scale[self.units]
|
||||||
options.use_raw_mesh_data = self.use_raw_mesh_data
|
options.use_raw_mesh_data = self.use_raw_mesh_data
|
||||||
try:
|
try:
|
||||||
ase = ASEBuilder().build(context, options)
|
ase = build_ase(context, options)
|
||||||
ASEWriter().write(self.filepath, ase)
|
writer_options = AseWriterOptions()
|
||||||
self.report({'INFO'}, 'ASE export successful')
|
writer_options.should_use_sub_materials = self.should_use_sub_materials
|
||||||
|
AseWriter().write(self.filepath, ase, writer_options)
|
||||||
|
self.report({'INFO'}, 'ASE exported successful')
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
except ASEBuilderError as e:
|
except AseBuilderError as e:
|
||||||
self.report({'ERROR'}, str(e))
|
self.report({'ERROR'}, str(e))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
|
||||||
|
__classes__ = (
|
||||||
|
AseExportCollectionPropertyGroup,
|
||||||
|
AseExportPropertyGroup,
|
||||||
|
AseExportCollectionsSelectAll,
|
||||||
|
AseExportCollectionsDeselectAll,
|
||||||
|
ASE_UL_CollectionList,
|
||||||
|
ASE_OT_ExportOperator
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import bpy
|
|
||||||
from bpy.props import StringProperty
|
|
||||||
import bpy_extras
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
from asepy import read_ase
|
|
||||||
|
|
||||||
|
|
||||||
class ASE_OT_ImportOperator(bpy.types.Operator, bpy_extras.io_utils.ImportHelper):
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
|
||||||
ASE_OT_ImportOperator
|
|
||||||
)
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import bpy
|
|
||||||
|
|
||||||
try:
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
bpy.ops.object.select_all(action='SELECT')
|
|
||||||
bpy.ops.object.delete()
|
|
||||||
|
|
||||||
bpy.ops.mesh.primitive_monkey_add()
|
|
||||||
bpy.ops.object.select_all(action='SELECT')
|
|
||||||
|
|
||||||
mesh_object = bpy.context.view_layer.objects.active
|
|
||||||
material = bpy.data.materials.new('asd')
|
|
||||||
mesh_object.data.materials.append(material)
|
|
||||||
|
|
||||||
r = bpy.ops.io_scene_ase.ase_export(filepath=r'.\\flat.ase')
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import unittest
|
|
||||||
import os
|
|
||||||
from subprocess import run, PIPE, STDOUT
|
|
||||||
from ..reader import read_ase
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
|
|
||||||
def run_blender_script(script_path: str, args=list()):
|
|
||||||
return run([os.environ['BLENDER_PATH'], '--background', '--python', script_path, '--'] + args)
|
|
||||||
|
|
||||||
|
|
||||||
class AseExportTests(unittest.TestCase):
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
def test_flat(self):
|
|
||||||
run_blender_script('src\\tests\\scripts\\export_flat_test.py')
|
|
||||||
read_ase('./flat.ase')
|
|
||||||
|
|
||||||
def test_smooth(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
print()
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,17 +1,22 @@
|
|||||||
from .ase import *
|
from .ase import Ase
|
||||||
|
|
||||||
|
|
||||||
class ASEFile(object):
|
class AseWriterOptions(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.should_use_sub_materials = True
|
||||||
|
|
||||||
|
|
||||||
|
class AseFile(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.commands = []
|
self.commands = []
|
||||||
|
|
||||||
def add_command(self, name):
|
def add_command(self, name):
|
||||||
command = ASECommand(name)
|
command = AseCommand(name)
|
||||||
self.commands.append(command)
|
self.commands.append(command)
|
||||||
return command
|
return command
|
||||||
|
|
||||||
|
|
||||||
class ASECommand(object):
|
class AseCommand(object):
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.data = []
|
self.data = []
|
||||||
@@ -39,17 +44,17 @@ class ASECommand(object):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def push_sub_command(self, name):
|
def push_sub_command(self, name):
|
||||||
command = ASECommand(name)
|
command = AseCommand(name)
|
||||||
self.sub_commands.append(command)
|
self.sub_commands.append(command)
|
||||||
return command
|
return command
|
||||||
|
|
||||||
def push_child(self, name):
|
def push_child(self, name):
|
||||||
child = ASECommand(name)
|
child = AseCommand(name)
|
||||||
self.children.append(child)
|
self.children.append(child)
|
||||||
return child
|
return child
|
||||||
|
|
||||||
|
|
||||||
class ASEWriter(object):
|
class AseWriter(object):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.fp = None
|
self.fp = None
|
||||||
@@ -97,19 +102,21 @@ class ASEWriter(object):
|
|||||||
else:
|
else:
|
||||||
self.fp.write('\n')
|
self.fp.write('\n')
|
||||||
|
|
||||||
def write_file(self, file: ASEFile):
|
def write_file(self, file: AseFile):
|
||||||
|
self.indent = 0
|
||||||
for command in file.commands:
|
for command in file.commands:
|
||||||
self.write_command(command)
|
self.write_command(command)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def build_ase_tree(ase) -> ASEFile:
|
def build_ase_file(ase: Ase, options: AseWriterOptions) -> AseFile:
|
||||||
root = ASEFile()
|
root = AseFile()
|
||||||
root.add_command('3DSMAX_ASCIIEXPORT').push_datum(200)
|
root.add_command('3DSMAX_ASCIIEXPORT').push_datum(200)
|
||||||
|
|
||||||
# Materials
|
# Materials
|
||||||
if len(ase.materials) > 0:
|
if len(ase.materials) > 0:
|
||||||
material_list = root.add_command('MATERIAL_LIST')
|
material_list = root.add_command('MATERIAL_LIST')
|
||||||
material_list.push_child('MATERIAL_COUNT').push_datum(len(ase.materials))
|
material_list.push_child('MATERIAL_COUNT').push_datum(len(ase.materials))
|
||||||
|
if options.should_use_sub_materials:
|
||||||
material_node = material_list.push_child('MATERIAL')
|
material_node = material_list.push_child('MATERIAL')
|
||||||
material_node.push_child('NUMSUBMTLS').push_datum(len(ase.materials))
|
material_node.push_child('NUMSUBMTLS').push_datum(len(ase.materials))
|
||||||
for material_index, material in enumerate(ase.materials):
|
for material_index, material in enumerate(ase.materials):
|
||||||
@@ -122,22 +129,50 @@ class ASEWriter(object):
|
|||||||
diffuse_node.push_child('UVW_V_OFFSET').push_datum(0.0)
|
diffuse_node.push_child('UVW_V_OFFSET').push_datum(0.0)
|
||||||
diffuse_node.push_child('UVW_U_TILING').push_datum(1.0)
|
diffuse_node.push_child('UVW_U_TILING').push_datum(1.0)
|
||||||
diffuse_node.push_child('UVW_V_TILING').push_datum(1.0)
|
diffuse_node.push_child('UVW_V_TILING').push_datum(1.0)
|
||||||
|
else:
|
||||||
|
for material_index, material in enumerate(ase.materials):
|
||||||
|
material_node = material_list.push_child('MATERIAL').push_datum(material_index)
|
||||||
|
material_node.push_child('MATERIAL_NAME').push_datum(material)
|
||||||
|
diffuse_node = material_node.push_child('MAP_DIFFUSE')
|
||||||
|
diffuse_node.push_child('MAP_NAME').push_datum('default')
|
||||||
|
diffuse_node.push_child('UVW_U_OFFSET').push_datum(0.0)
|
||||||
|
diffuse_node.push_child('UVW_V_OFFSET').push_datum(0.0)
|
||||||
|
diffuse_node.push_child('UVW_U_TILING').push_datum(1.0)
|
||||||
|
diffuse_node.push_child('UVW_V_TILING').push_datum(1.0)
|
||||||
|
|
||||||
for geometry_object in ase.geometry_objects:
|
for geometry_object in ase.geometry_objects:
|
||||||
geomobject_node = root.add_command('GEOMOBJECT')
|
geomobject_node = root.add_command('GEOMOBJECT')
|
||||||
geomobject_node.push_child('NODE_NAME').push_datum(geometry_object.name)
|
geomobject_node.push_child('NODE_NAME').push_datum(geometry_object.name)
|
||||||
|
|
||||||
|
# TODO: only do this in T3D compatibility mode (or just do it always because it makes no difference?)
|
||||||
|
transform_node = geomobject_node.push_child('NODE_TM')
|
||||||
|
transform_node.push_child('NODE_NAME').push_datum(geometry_object.name)
|
||||||
|
transform_node.push_child('INHERIT_POS').push_data([0, 0, 0])
|
||||||
|
transform_node.push_child('INHERIT_ROT').push_data([0, 0, 0])
|
||||||
|
transform_node.push_child('INHERIT_SCL').push_data([0, 0, 0])
|
||||||
|
transform_node.push_child('TM_ROW0').push_data([1.0, 0.0, 0.0])
|
||||||
|
transform_node.push_child('TM_ROW1').push_data([0.0, 1.0, 0.0])
|
||||||
|
transform_node.push_child('TM_ROW2').push_data([0.0, 0.0, 1.0])
|
||||||
|
transform_node.push_child('TM_ROW3').push_data([0.0, 0.0, 0.0])
|
||||||
|
transform_node.push_child('TM_POS').push_data([0.0, 0.0, 0.0])
|
||||||
|
transform_node.push_child('TM_ROTAXIS').push_data([0.0, 0.0, 0.0])
|
||||||
|
transform_node.push_child('TM_ROTANGLE').push_datum(0.0)
|
||||||
|
transform_node.push_child('TM_SCALE').push_datum(0.0)
|
||||||
|
transform_node.push_child('TM_SCALEAXIS').push_data([0.0, 0.0, 0.0])
|
||||||
|
transform_node.push_child('TM_SCALEAXISANG').push_datum(0.0)
|
||||||
|
|
||||||
mesh_node = geomobject_node.push_child('MESH')
|
mesh_node = geomobject_node.push_child('MESH')
|
||||||
|
|
||||||
# Vertices
|
|
||||||
mesh_node.push_child('MESH_NUMVERTEX').push_datum(len(geometry_object.vertices))
|
mesh_node.push_child('MESH_NUMVERTEX').push_datum(len(geometry_object.vertices))
|
||||||
|
mesh_node.push_child('MESH_NUMFACES').push_datum(len(geometry_object.faces))
|
||||||
|
|
||||||
|
# Vertices
|
||||||
vertex_list_node = mesh_node.push_child('MESH_VERTEX_LIST')
|
vertex_list_node = mesh_node.push_child('MESH_VERTEX_LIST')
|
||||||
for vertex_index, vertex in enumerate(geometry_object.vertices):
|
for vertex_index, vertex in enumerate(geometry_object.vertices):
|
||||||
mesh_vertex = vertex_list_node.push_child('MESH_VERTEX').push_datum(vertex_index)
|
mesh_vertex = vertex_list_node.push_child('MESH_VERTEX').push_datum(vertex_index)
|
||||||
mesh_vertex.push_data([x for x in vertex])
|
mesh_vertex.push_data([x for x in vertex])
|
||||||
|
|
||||||
# Faces
|
# Faces
|
||||||
mesh_node.push_child('MESH_NUMFACES').push_datum(len(geometry_object.faces))
|
|
||||||
faces_node = mesh_node.push_child('MESH_FACE_LIST')
|
faces_node = mesh_node.push_child('MESH_FACE_LIST')
|
||||||
for face_index, face in enumerate(geometry_object.faces):
|
for face_index, face in enumerate(geometry_object.faces):
|
||||||
face_node = faces_node.push_child('MESH_FACE')
|
face_node = faces_node.push_child('MESH_FACE')
|
||||||
@@ -182,18 +217,20 @@ class ASEWriter(object):
|
|||||||
cvert_list = mesh_node.push_child('MESH_CVERTLIST')
|
cvert_list = mesh_node.push_child('MESH_CVERTLIST')
|
||||||
for i, vertex_color in enumerate(geometry_object.vertex_colors):
|
for i, vertex_color in enumerate(geometry_object.vertex_colors):
|
||||||
cvert_list.push_child('MESH_VERTCOL').push_datum(i).push_data(vertex_color)
|
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))
|
mesh_node.push_child('MESH_NUMCVFACES').push_datum(len(geometry_object.texture_vertex_faces))
|
||||||
texture_faces_node = parent_node.push_child('MESH_CFACELIST')
|
texture_faces_node = mesh_node.push_child('MESH_CFACELIST')
|
||||||
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_CFACE')
|
texture_face_node = texture_faces_node.push_child('MESH_CFACE')
|
||||||
texture_face_node.push_data([texture_face_index] + list(texture_face))
|
texture_face_node.push_data([texture_face_index] + list(texture_face))
|
||||||
|
|
||||||
|
geomobject_node.push_child('PROP_MOTIONBLUR').push_datum(0)
|
||||||
|
geomobject_node.push_child('PROP_CASTSHADOW').push_datum(1)
|
||||||
|
geomobject_node.push_child('PROP_RECVSHADOW').push_datum(1)
|
||||||
geomobject_node.push_child('MATERIAL_REF').push_datum(0)
|
geomobject_node.push_child('MATERIAL_REF').push_datum(0)
|
||||||
|
|
||||||
return root
|
return root
|
||||||
|
|
||||||
def write(self, filepath, ase):
|
def write(self, filepath, ase: Ase, options: AseWriterOptions):
|
||||||
self.indent = 0
|
ase_file = self.build_ase_file(ase, options)
|
||||||
ase_file = self.build_ase_tree(ase)
|
|
||||||
with open(filepath, 'w') as self.fp:
|
with open(filepath, 'w') as self.fp:
|
||||||
self.write_file(ase_file)
|
self.write_file(ase_file)
|
||||||
|
|||||||
Reference in New Issue
Block a user