mirror of
https://github.com/YorhaX2P/NieR2Blender2NieR-PS4.git
synced 2026-08-26 19:43:33 +00:00
restructured import & export operators into separate files + added .sar & GAArea import/export
This commit is contained in:
+26
-271
@@ -1,6 +1,3 @@
|
||||
from .utils.utilOperators import RecalculateObjectIndices, RemoveUnusedVertexGroups, MergeVertexGroupCopies, \
|
||||
DeleteLooseGeometrySelected, DeleteLooseGeometryAll, RipMeshByUVIslands
|
||||
|
||||
bl_info = {
|
||||
"name": "Nier2Blender2NieR (NieR:Automata Data Exporter)",
|
||||
"author": "Woeful_Wolf & RaiderB",
|
||||
@@ -9,101 +6,27 @@ bl_info = {
|
||||
"description": "Import/Export NieR:Automata WMB/WTP/WTA/DTT/DAT/COL files.",
|
||||
"category": "Import-Export"}
|
||||
|
||||
import traceback
|
||||
|
||||
from bpy.props import StringProperty
|
||||
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||
|
||||
from . import preferences
|
||||
from .col.exporter import col_ui_manager, col_exporter
|
||||
from .dat_dtt.exporter import dat_dtt_ui_manager
|
||||
from .utils.util import *
|
||||
from .utils.utilOperators import RecalculateObjectIndices, RemoveUnusedVertexGroups, MergeVertexGroupCopies, \
|
||||
DeleteLooseGeometrySelected, DeleteLooseGeometryAll, RipMeshByUVIslands
|
||||
from .wta_wtp.exporter import wta_wtp_ui_manager
|
||||
from .bxm.exporter.gaAreaExportOperator import ExportNierGaArea
|
||||
from .bxm.exporter.sarExportOperator import ExportNierSar
|
||||
from .bxm.importer.gaAreaImportOperator import ImportNierGaArea
|
||||
from .bxm.importer.sarImportOperator import ImportNierSar
|
||||
from .col.exporter.colExportOperator import ExportNierCol
|
||||
from .col.importer.colImportOperator import ImportNierCol
|
||||
from .dat_dtt.importer.datImportOperator import ImportNierDtt, ImportNierDat
|
||||
from .lay.exporter.layExportOperator import ExportNierLay
|
||||
from .lay.importer.layImportOperator import ImportNierLay
|
||||
from .wmb.exporter.wmbExportOperator import ExportNierWmb
|
||||
from .wmb.importer.wmbImportOperator import ImportNierWmb
|
||||
|
||||
|
||||
class ExportNierLay(bpy.types.Operator, ExportHelper):
|
||||
'''Export a NieR:Automata LAY File'''
|
||||
bl_idname = "export.lay_data"
|
||||
bl_label = "Export LAY File"
|
||||
bl_options = {'PRESET'}
|
||||
filename_ext = ".lay"
|
||||
filter_glob: StringProperty(default="*.lay", options={'HIDDEN'})
|
||||
|
||||
def execute(self, context):
|
||||
from .lay.exporter import lay_exporter
|
||||
|
||||
lay_exporter.main(self.filepath)
|
||||
return {'FINISHED'}
|
||||
|
||||
class ExportNierCol(bpy.types.Operator, ExportHelper):
|
||||
'''Export a NieR:Automata COL File'''
|
||||
bl_idname = "export.col_data"
|
||||
bl_label = "Export COL File"
|
||||
bl_options = {'PRESET'}
|
||||
filename_ext = ".col"
|
||||
filter_glob: StringProperty(default="*.col", options={'HIDDEN'})
|
||||
|
||||
generateColTree: bpy.props.BoolProperty(name="Generate Collision Tree", description="This automatically generates colTreeNodes based on your geometry and assigns the right meshes to the right colTreeNodes. Only disable it if you are manually adjusting them", default=True)
|
||||
centre_origins: bpy.props.BoolProperty(name="Centre Origins", description="This automatically centres the origins of all your objects. (Recommended)", default=True)
|
||||
triangulate_meshes: bpy.props.BoolProperty(name="Triangulate Meshes", description="This automatically adds and applies the Triangulate Modifier on all your objects. (Slow)", default=True)
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
if self.centre_origins:
|
||||
print("Centering origins...")
|
||||
centre_origins("COL")
|
||||
|
||||
if self.triangulate_meshes:
|
||||
print("Triangulating meshes...")
|
||||
triangulate_meshes("COL")
|
||||
|
||||
col_exporter.main(self.filepath, self.generateColTree)
|
||||
return {'FINISHED'}
|
||||
|
||||
class ExportNierWmb(bpy.types.Operator, ExportHelper):
|
||||
'''Export a NieR:Automata WMB File'''
|
||||
bl_idname = "export.wmb_data"
|
||||
bl_label = "Export WMB File"
|
||||
bl_options = {'PRESET'}
|
||||
filename_ext = ".wmb"
|
||||
filter_glob: StringProperty(default="*.wmb", options={'HIDDEN'})
|
||||
|
||||
centre_origins: bpy.props.BoolProperty(name="Centre Origins", description="This automatically centres the origins of all your objects. (Recommended)", default=True)
|
||||
triangulate_meshes: bpy.props.BoolProperty(name="Triangulate Meshes", description="This automatically adds and applies the Triangulate Modifier on all your objects. Only disable if you know your meshes are triangulated and you wish to reduce export times", default=True)
|
||||
delete_loose_geometry: bpy.props.BoolProperty(name="Delete Loose Geometry", description="This automatically runs the 'Delete Loose Geometry (All)' operator before exporting. It deletes all loose vertices or edges that could result in unwanted results in-game", default=True)
|
||||
|
||||
def execute(self, context):
|
||||
from .wmb.exporter import wmb_exporter
|
||||
|
||||
bpy.data.collections['WMB'].all_objects[0].select_set(True)
|
||||
|
||||
if self.centre_origins:
|
||||
print("Centering origins...")
|
||||
wmb_exporter.centre_origins()
|
||||
|
||||
"""
|
||||
if self.purge_materials:
|
||||
print("Purging materials...")
|
||||
wmb_exporter.purge_unused_materials()
|
||||
"""
|
||||
|
||||
if self.triangulate_meshes:
|
||||
print("Triangulating meshes...")
|
||||
wmb_exporter.triangulate_meshes()
|
||||
|
||||
if self.delete_loose_geometry:
|
||||
print("Deleting loose geometry...")
|
||||
bpy.ops.b2n.deleteloosegeometryall()
|
||||
|
||||
try:
|
||||
print("Starting export...")
|
||||
wmb_exporter.main(self.filepath)
|
||||
return wmb_exporter.restore_blend()
|
||||
except:
|
||||
print(traceback.format_exc())
|
||||
self.report({'ERROR'}, "An unexpected error has occurred during export. Please check the console for more info.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
class NierObjectMenu(bpy.types.Menu):
|
||||
bl_idname = 'OBJECT_MT_n2b2n'
|
||||
bl_label = 'NieR Tools'
|
||||
@@ -116,181 +39,6 @@ class NierObjectMenu(bpy.types.Menu):
|
||||
self.layout.operator(RipMeshByUVIslands.bl_idname)
|
||||
self.layout.operator(CreateLayBoundingBox.bl_idname, icon="CUBE")
|
||||
|
||||
|
||||
class ImportNierWmb(bpy.types.Operator, ImportHelper):
|
||||
'''Load a Nier:Automata WMB File.'''
|
||||
bl_idname = "import_scene.wmb_data"
|
||||
bl_label = "Import WMB Data"
|
||||
bl_options = {'PRESET'}
|
||||
filename_ext = ".wmb"
|
||||
filter_glob: StringProperty(default="*.wmb", options={'HIDDEN'})
|
||||
|
||||
reset_blend: bpy.props.BoolProperty(name="Reset Blender Scene on Import", default=True)
|
||||
|
||||
def execute(self, context):
|
||||
from .wmb.importer import wmb_importer
|
||||
if self.reset_blend:
|
||||
wmb_importer.reset_blend()
|
||||
return wmb_importer.main(False, self.filepath)
|
||||
|
||||
def importDat(only_extract, filepath):
|
||||
head = os.path.split(filepath)[0]
|
||||
tail = os.path.split(filepath)[1]
|
||||
tailless_tail = tail[:-4]
|
||||
dat_filepath = head + '\\' + tailless_tail + '.dat'
|
||||
extract_dir = head + '\\nier2blender_extracted'
|
||||
from .dat_dtt.importer import dat_unpacker
|
||||
if os.path.isfile(dat_filepath):
|
||||
dat_unpacker.main(dat_filepath, extract_dir + '\\' + tailless_tail + '.dat', dat_filepath) # dat
|
||||
else:
|
||||
print('DAT not found. Only extracting DTT. (No materials, collisions or layouts will automatically be imported)')
|
||||
|
||||
last_filename = dat_unpacker.main(filepath, extract_dir + '\\' + tailless_tail + '.dtt', filepath) # dtt
|
||||
|
||||
wmb_filepath = extract_dir + '\\' + tailless_tail + '.dtt\\' + last_filename[:-4] + '.wmb'
|
||||
if not os.path.exists(wmb_filepath):
|
||||
wmb_filepath = extract_dir + '\\' + tailless_tail + '.dat\\' + last_filename[:-4] + '.wmb' # if not in dtt, then must be in dat
|
||||
|
||||
# WMB
|
||||
from .wmb.importer import wmb_importer
|
||||
wmb_importer.main(only_extract, wmb_filepath)
|
||||
|
||||
if only_extract:
|
||||
return {'FINISHED'}
|
||||
|
||||
bpy.context.scene.DatDir = extract_dir + '\\' + tailless_tail + '.dat'
|
||||
bpy.context.scene.DttDir = extract_dir + '\\' + tailless_tail + '.dtt'
|
||||
bpy.context.scene.ExportFileName = tailless_tail
|
||||
|
||||
# COL
|
||||
col_filepath = extract_dir + '\\' + tailless_tail + '.dat\\' + tailless_tail + '.col'
|
||||
if os.path.isfile(col_filepath):
|
||||
from .col.importer import col_importer
|
||||
col_importer.main(col_filepath)
|
||||
|
||||
# LAY
|
||||
lay_filepath = extract_dir + '\\' + tailless_tail + '.dat\\' + 'Layout.lay'
|
||||
if os.path.isfile(lay_filepath):
|
||||
from .lay.importer import lay_importer
|
||||
lay_importer.main(lay_filepath, __package__)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
class ImportNierDtt(bpy.types.Operator, ImportHelper):
|
||||
'''Load a Nier:Automata DTT (and DAT) File.'''
|
||||
bl_idname = "import_scene.dtt_data"
|
||||
bl_label = "Import DTT (and DAT) Data"
|
||||
bl_options = {'PRESET'}
|
||||
filename_ext = ".dtt"
|
||||
filter_glob: StringProperty(default="*.dtt", options={'HIDDEN'})
|
||||
|
||||
reset_blend: bpy.props.BoolProperty(name="Reset Blender Scene on Import", default=True)
|
||||
bulk_import: bpy.props.BoolProperty(name="Bulk Import All DTT/DATs In Folder (Experimental)", default=False)
|
||||
only_extract: bpy.props.BoolProperty(name="Only Extract DTT/DAT Contents. (Experimental)", default=False)
|
||||
|
||||
def execute(self, context):
|
||||
from .wmb.importer import wmb_importer
|
||||
if self.reset_blend and not self.only_extract:
|
||||
wmb_importer.reset_blend()
|
||||
if self.bulk_import:
|
||||
folder = os.path.split(self.filepath)[0]
|
||||
for filename in os.listdir(folder):
|
||||
if filename[-4:] == '.dtt':
|
||||
try:
|
||||
filepath = folder + '\\' + filename
|
||||
importDat(self.only_extract, filepath)
|
||||
except:
|
||||
print('ERROR: FAILED TO IMPORT', filename)
|
||||
return {'FINISHED'}
|
||||
|
||||
else:
|
||||
return importDat(self.only_extract, self.filepath)
|
||||
|
||||
class ImportNierDat(bpy.types.Operator, ImportHelper):
|
||||
'''Load a Nier:Automata DAT File.'''
|
||||
bl_idname = "import_scene.dat_data"
|
||||
bl_label = "Import DAT Data"
|
||||
bl_options = {'PRESET'}
|
||||
filename_ext = ".dat"
|
||||
filter_glob: StringProperty(default="*.dat", options={'HIDDEN'})
|
||||
|
||||
reset_blend: bpy.props.BoolProperty(name="Reset Blender Scene on Import", default=True)
|
||||
bulk_import: bpy.props.BoolProperty(name="Bulk Import All DTT/DATs In Folder (Experimental)", default=False)
|
||||
only_extract: bpy.props.BoolProperty(name="Only Extract DTT/DAT Contents. (Experimental)", default=False)
|
||||
|
||||
def doImport(self, onlyExtract, filepath):
|
||||
head = os.path.split(filepath)[0]
|
||||
tail = os.path.split(filepath)[1]
|
||||
tailless_tail = tail[:-4]
|
||||
dat_filepath = head + '\\' + tailless_tail + '.dat'
|
||||
extract_dir = head + '\\nier2blender_extracted'
|
||||
from .dat_dtt.importer import dat_unpacker
|
||||
if os.path.isfile(dat_filepath):
|
||||
dat_unpacker.main(dat_filepath, extract_dir + '\\' + tailless_tail + '.dat', dat_filepath) # dat
|
||||
|
||||
if onlyExtract:
|
||||
return {'FINISHED'}
|
||||
|
||||
bpy.context.scene.DatDir = extract_dir + '\\' + tailless_tail + '.dat'
|
||||
bpy.context.scene.DttDir = extract_dir + '\\' + tailless_tail + '.dtt'
|
||||
bpy.context.scene.ExportFileName = tailless_tail
|
||||
|
||||
# COL
|
||||
col_filepath = extract_dir + '\\' + tailless_tail + '.dat\\' + tailless_tail + '.col'
|
||||
if os.path.isfile(col_filepath):
|
||||
from .col.importer import col_importer
|
||||
col_importer.main(col_filepath)
|
||||
|
||||
# LAY
|
||||
lay_filepath = extract_dir + '\\' + tailless_tail + '.dat\\' + 'Layout.lay'
|
||||
if os.path.isfile(lay_filepath):
|
||||
from .lay.importer import lay_importer
|
||||
lay_importer.main(lay_filepath, __package__)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def execute(self, context):
|
||||
from .wmb.importer import wmb_importer
|
||||
if self.reset_blend and not self.only_extract:
|
||||
wmb_importer.reset_blend()
|
||||
if self.bulk_import:
|
||||
folder = os.path.split(self.filepath)[0]
|
||||
for filename in os.listdir(folder):
|
||||
if filename[-4:] == '.dat':
|
||||
try:
|
||||
filepath = folder + '\\' + filename
|
||||
return self.doImport(self.only_extract, filepath)
|
||||
except:
|
||||
print('ERROR: FAILED TO IMPORT', filename)
|
||||
return {'FINISHED'}
|
||||
|
||||
else:
|
||||
return self.doImport(self.only_extract, self.filepath)
|
||||
|
||||
class ImportNierCol(bpy.types.Operator, ImportHelper):
|
||||
'''Load a Nier:Automata Col (Collision) File.'''
|
||||
bl_idname = "import_scene.col_data"
|
||||
bl_label = "Import Col Data"
|
||||
bl_options = {'PRESET'}
|
||||
filename_ext = ".col"
|
||||
filter_glob: StringProperty(default="*.col", options={'HIDDEN'})
|
||||
|
||||
def execute(self, context):
|
||||
from .col.importer import col_importer
|
||||
return col_importer.main(self.filepath)
|
||||
|
||||
class ImportNierLay(bpy.types.Operator, ImportHelper):
|
||||
'''Load a Nier:Automata Lay (Layout) File.'''
|
||||
bl_idname = "import_scene.lay_data"
|
||||
bl_label = "Import Lay Data"
|
||||
bl_options = {'PRESET'}
|
||||
filename_ext = ".lay"
|
||||
filter_glob: StringProperty(default="*.lay", options={'HIDDEN'})
|
||||
|
||||
def execute(self, context):
|
||||
from .lay.importer import lay_importer
|
||||
return lay_importer.main(self.filepath, __package__)
|
||||
|
||||
class CreateLayBoundingBox(bpy.types.Operator):
|
||||
"""Create Layout Object Bounding Box"""
|
||||
bl_idname = "n2b.create_lay_bb"
|
||||
@@ -315,6 +63,8 @@ def menu_func_import(self, context):
|
||||
self.layout.operator(ImportNierDat.bl_idname, text="DAT File for Nier:Automata (col+lay) (.dat)", icon_value=yorha_icon.icon_id)
|
||||
self.layout.operator(ImportNierCol.bl_idname, text="Collision File for Nier:Automata (.col)", icon_value=yorha_icon.icon_id)
|
||||
self.layout.operator(ImportNierLay.bl_idname, text="Layout File for Nier:Automata (.lay)", icon_value=yorha_icon.icon_id)
|
||||
self.layout.operator(ImportNierSar.bl_idname, text="Audio Environment File (.sar)", icon_value=yorha_icon.icon_id)
|
||||
self.layout.operator(ImportNierGaArea.bl_idname, text="Visual Environment File (GAArea.bxm)", icon_value=yorha_icon.icon_id)
|
||||
|
||||
def menu_func_export(self, context):
|
||||
pcoll = preview_collections["main"]
|
||||
@@ -323,6 +73,8 @@ def menu_func_export(self, context):
|
||||
self.layout.operator(ExportNierWmb.bl_idname, text="WMB File for NieR:Automata (.wmb)", icon_value=emil_icon.icon_id)
|
||||
self.layout.operator(ExportNierCol.bl_idname, text="Collision File for NieR:Automata (.col)", icon_value=emil_icon.icon_id)
|
||||
self.layout.operator(ExportNierLay.bl_idname, text="Layout File for NieR:Automata (.lay)", icon_value=emil_icon.icon_id)
|
||||
self.layout.operator(ExportNierSar.bl_idname, text="Audio Environment File (.sar)", icon_value=emil_icon.icon_id)
|
||||
self.layout.operator(ExportNierGaArea.bl_idname, text="Visual Environment File (GAArea.bxm)", icon_value=emil_icon.icon_id)
|
||||
|
||||
def menu_func_utils(self, context):
|
||||
pcoll = preview_collections["main"]
|
||||
@@ -335,10 +87,14 @@ classes = (
|
||||
ImportNierDat,
|
||||
ImportNierCol,
|
||||
ImportNierLay,
|
||||
ImportNierSar,
|
||||
ImportNierGaArea,
|
||||
CreateLayBoundingBox,
|
||||
ExportNierWmb,
|
||||
ExportNierCol,
|
||||
ExportNierSar,
|
||||
ExportNierLay,
|
||||
ExportNierGaArea,
|
||||
NierObjectMenu,
|
||||
RecalculateObjectIndices,
|
||||
RemoveUnusedVertexGroups,
|
||||
@@ -392,11 +148,7 @@ def unregister():
|
||||
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
|
||||
bpy.types.VIEW3D_MT_object.remove(menu_func_utils)
|
||||
|
||||
if __name__ == '__main__':
|
||||
register()
|
||||
|
||||
|
||||
## Extras
|
||||
## Collision Extras
|
||||
def setColourByCollisionType(obj):
|
||||
opacity = 1.0
|
||||
collisionType = int(obj.collisionType)
|
||||
@@ -445,4 +197,7 @@ surfaceTypes = [
|
||||
("22", "Concrete 4", ""),
|
||||
("23", "Car", ""),
|
||||
("24", "Flowers", "")
|
||||
]
|
||||
]
|
||||
|
||||
if __name__ == '__main__':
|
||||
register()
|
||||
@@ -0,0 +1,737 @@
|
||||
approxMapOffsets = {
|
||||
"g10218": [
|
||||
-1500.216552734375,
|
||||
692.2970581054688,
|
||||
0.0
|
||||
],
|
||||
"g10318": [
|
||||
-1350.2166748046875,
|
||||
605.6943969726562,
|
||||
0.0
|
||||
],
|
||||
"g10319": [
|
||||
-1350.2166748046875,
|
||||
432.48931884765625,
|
||||
0.0
|
||||
],
|
||||
"g10415": [
|
||||
-1200.216552734375,
|
||||
1038.70703125,
|
||||
0.0
|
||||
],
|
||||
"g10416": [
|
||||
-1200.216552734375,
|
||||
865.5020141601562,
|
||||
0.0
|
||||
],
|
||||
"g10417": [
|
||||
-1200.216552734375,
|
||||
692.2969360351562,
|
||||
0.0
|
||||
],
|
||||
"g10418": [
|
||||
-1200.216552734375,
|
||||
519.0918579101562,
|
||||
0.0
|
||||
],
|
||||
"g10419": [
|
||||
-1200.2166748046875,
|
||||
345.88677978515625,
|
||||
0.0
|
||||
],
|
||||
"g10514": [
|
||||
-1050.216552734375,
|
||||
1125.3095703125,
|
||||
0.0
|
||||
],
|
||||
"g10515": [
|
||||
-1050.216552734375,
|
||||
952.1045532226562,
|
||||
0.0
|
||||
],
|
||||
"g10516": [
|
||||
-1050.216552734375,
|
||||
778.8994750976562,
|
||||
0.0
|
||||
],
|
||||
"g10517": [
|
||||
-1050.216552734375,
|
||||
605.6943969726562,
|
||||
0.0
|
||||
],
|
||||
"g10518": [
|
||||
-1050.216552734375,
|
||||
432.48944091796875,
|
||||
0.0
|
||||
],
|
||||
"g10519": [
|
||||
-1050.2166748046875,
|
||||
259.28436279296875,
|
||||
0.0
|
||||
],
|
||||
"g10520": [
|
||||
-1050.2166748046875,
|
||||
86.0792236328125,
|
||||
0.0
|
||||
],
|
||||
"g10614": [
|
||||
-900.2164306640625,
|
||||
1038.70703125,
|
||||
0.0
|
||||
],
|
||||
"g10615": [
|
||||
-900.2164916992188,
|
||||
865.5020141601562,
|
||||
0.0
|
||||
],
|
||||
"g10616": [
|
||||
-900.2164916992188,
|
||||
692.2969360351562,
|
||||
0.0
|
||||
],
|
||||
"g10617": [
|
||||
-900.216552734375,
|
||||
519.0919799804688,
|
||||
0.0
|
||||
],
|
||||
"g10618": [
|
||||
-900.2166137695312,
|
||||
345.88690185546875,
|
||||
0.0
|
||||
],
|
||||
"g10619": [
|
||||
-900.2166748046875,
|
||||
172.68182373046875,
|
||||
0.0
|
||||
],
|
||||
"g10713": [
|
||||
-750.2163696289062,
|
||||
1125.3095703125,
|
||||
0.0
|
||||
],
|
||||
"g10714": [
|
||||
-750.2164306640625,
|
||||
952.1045532226562,
|
||||
0.0
|
||||
],
|
||||
"g10715": [
|
||||
-750.2164916992188,
|
||||
778.8994750976562,
|
||||
0.0
|
||||
],
|
||||
"g10716": [
|
||||
-750.216552734375,
|
||||
605.6943969726562,
|
||||
0.0
|
||||
],
|
||||
"g10717": [
|
||||
-750.216552734375,
|
||||
432.48931884765625,
|
||||
0.0
|
||||
],
|
||||
"g10718": [
|
||||
-750.2166748046875,
|
||||
259.28424072265625,
|
||||
0.0
|
||||
],
|
||||
"g10719": [
|
||||
-750.2167358398438,
|
||||
86.0791015625,
|
||||
0.0
|
||||
],
|
||||
"g10722": [
|
||||
-750.4308471679688,
|
||||
-428.4694519042969,
|
||||
0.0
|
||||
],
|
||||
"g10812": [
|
||||
-600.2164306640625,
|
||||
1211.912109375,
|
||||
0.0
|
||||
],
|
||||
"g10813": [
|
||||
-600.21630859375,
|
||||
1038.707275390625,
|
||||
0.0
|
||||
],
|
||||
"g10814": [
|
||||
-600.2164306640625,
|
||||
865.5020141601562,
|
||||
0.0
|
||||
],
|
||||
"g10815": [
|
||||
-600.2168579101562,
|
||||
692.2975463867188,
|
||||
0.0
|
||||
],
|
||||
"g10816": [
|
||||
-600.0177612304688,
|
||||
519.5239868164062,
|
||||
0.0
|
||||
],
|
||||
"g10817": [
|
||||
-600.2166137695312,
|
||||
345.88677978515625,
|
||||
0.0
|
||||
],
|
||||
"g10818": [
|
||||
-600.2166748046875,
|
||||
172.68170166015625,
|
||||
0.0
|
||||
],
|
||||
"g10820": [
|
||||
-600.4114379882812,
|
||||
-168.7242431640625,
|
||||
0.0
|
||||
],
|
||||
"g10821": [
|
||||
-600.4321899414062,
|
||||
-341.87237548828125,
|
||||
0.0
|
||||
],
|
||||
"g10822": [
|
||||
-600.0316162109375,
|
||||
-514.9127197265625,
|
||||
0.0
|
||||
],
|
||||
"g10912": [
|
||||
-463.0992126464844,
|
||||
1253.018310546875,
|
||||
0.0
|
||||
],
|
||||
"g10914": [
|
||||
-450.103271484375,
|
||||
781.9497680664062,
|
||||
0.0
|
||||
],
|
||||
"g10915": [
|
||||
-450.2168884277344,
|
||||
605.6950073242188,
|
||||
0.0
|
||||
],
|
||||
"g10916": [
|
||||
-450.2155456542969,
|
||||
432.49017333984375,
|
||||
0.0
|
||||
],
|
||||
"g10917": [
|
||||
-450.2691650390625,
|
||||
259.18878173828125,
|
||||
0.0
|
||||
],
|
||||
"g10918": [
|
||||
-450.1297302246094,
|
||||
86.0806884765625,
|
||||
0.0
|
||||
],
|
||||
"g10919": [
|
||||
-450.1297607421875,
|
||||
-87.1243896484375,
|
||||
0.0
|
||||
],
|
||||
"g10920": [
|
||||
-450.4114685058594,
|
||||
-255.326904296875,
|
||||
0.0
|
||||
],
|
||||
"g10921": [
|
||||
-450.3250427246094,
|
||||
-428.4050598144531,
|
||||
0.0
|
||||
],
|
||||
"g10922": [
|
||||
-450.3506164550781,
|
||||
-601.2713623046875,
|
||||
0.0
|
||||
],
|
||||
"g10923": [
|
||||
-450.3504943847656,
|
||||
-774.326171875,
|
||||
0.0
|
||||
],
|
||||
"g10924": [
|
||||
-450.3506164550781,
|
||||
-748.4771118164062,
|
||||
0.0
|
||||
],
|
||||
"g11013": [
|
||||
-301.1095275878906,
|
||||
864.2005004882812,
|
||||
0.0
|
||||
],
|
||||
"g11014": [
|
||||
-301.10955810546875,
|
||||
690.9953002929688,
|
||||
0.0
|
||||
],
|
||||
"g11015": [
|
||||
-288.1788635253906,
|
||||
525.6282348632812,
|
||||
0.0
|
||||
],
|
||||
"g11016": [
|
||||
-300.34954833984375,
|
||||
345.68548583984375,
|
||||
0.0
|
||||
],
|
||||
"g11017": [
|
||||
-300.1296691894531,
|
||||
172.68365478515625,
|
||||
0.0
|
||||
],
|
||||
"g11018": [
|
||||
-300.1579284667969,
|
||||
-0.65093994140625,
|
||||
0.0
|
||||
],
|
||||
"g11021": [
|
||||
-300.3565979003906,
|
||||
-520.3690185546875,
|
||||
0.0
|
||||
],
|
||||
"g11113": [
|
||||
-151.10986328125,
|
||||
777.5980834960938,
|
||||
0.0
|
||||
],
|
||||
"g11114": [
|
||||
-151.134765625,
|
||||
604.4125366210938,
|
||||
0.0
|
||||
],
|
||||
"g11115": [
|
||||
-150.3494873046875,
|
||||
432.28802490234375,
|
||||
0.0
|
||||
],
|
||||
"g11117": [
|
||||
-150.12872314453125,
|
||||
86.0755615234375,
|
||||
0.0
|
||||
],
|
||||
"g11118": [
|
||||
-150.0880126953125,
|
||||
-87.24432373046875,
|
||||
0.0
|
||||
],
|
||||
"g11119": [
|
||||
-150.08807373046875,
|
||||
-260.44927978515625,
|
||||
0.0
|
||||
],
|
||||
"g11120": [
|
||||
-150.0880126953125,
|
||||
-433.65423583984375,
|
||||
0.0
|
||||
],
|
||||
"g11121": [
|
||||
-150.0880126953125,
|
||||
-606.8593139648438,
|
||||
0.0
|
||||
],
|
||||
"g11211": [
|
||||
-1.10986328125,
|
||||
1037.405517578125,
|
||||
0.0
|
||||
],
|
||||
"g11212": [
|
||||
-1.1085205078125,
|
||||
864.1998901367188,
|
||||
0.0
|
||||
],
|
||||
"g11213": [
|
||||
-1.109619140625,
|
||||
690.9955444335938,
|
||||
0.0
|
||||
],
|
||||
"g11214": [
|
||||
-1.109619140625,
|
||||
517.7904663085938,
|
||||
0.0
|
||||
],
|
||||
"g11215": [
|
||||
-1.109619140625,
|
||||
344.58538818359375,
|
||||
0.0
|
||||
],
|
||||
"g11217": [
|
||||
-0.0880126953125,
|
||||
-0.64178466796875,
|
||||
0.0
|
||||
],
|
||||
"g11218": [
|
||||
-0.08807373046875,
|
||||
-173.84686279296875,
|
||||
0.0
|
||||
],
|
||||
"g11219": [
|
||||
-0.088134765625,
|
||||
-347.05194091796875,
|
||||
0.0
|
||||
],
|
||||
"g11220": [
|
||||
-0.088134765625,
|
||||
-520.2568359375,
|
||||
0.0
|
||||
],
|
||||
"g11221": [
|
||||
-0.08819580078125,
|
||||
-693.4619140625,
|
||||
0.0
|
||||
],
|
||||
"g11311": [
|
||||
148.890625,
|
||||
950.8032836914062,
|
||||
0.0
|
||||
],
|
||||
"g11312": [
|
||||
148.8905029296875,
|
||||
777.5982055664062,
|
||||
0.0
|
||||
],
|
||||
"g11313": [
|
||||
148.8905029296875,
|
||||
604.3931274414062,
|
||||
0.0
|
||||
],
|
||||
"g11314": [
|
||||
149.07034301757812,
|
||||
433.2586975097656,
|
||||
1.862645149230957e-09
|
||||
],
|
||||
"g11315": [
|
||||
149.07029724121094,
|
||||
260.05364990234375,
|
||||
1.862645149230957e-09
|
||||
],
|
||||
"g11316": [
|
||||
149.0074005126953,
|
||||
87.91712951660156,
|
||||
0.0
|
||||
],
|
||||
"g11317": [
|
||||
149.9119873046875,
|
||||
-87.24432373046875,
|
||||
0.0
|
||||
],
|
||||
"g11318": [
|
||||
149.911865234375,
|
||||
-260.44940185546875,
|
||||
0.0
|
||||
],
|
||||
"g11319": [
|
||||
149.91180419921875,
|
||||
-433.65447998046875,
|
||||
0.0
|
||||
],
|
||||
"g11320": [
|
||||
149.91156005859375,
|
||||
-606.8595581054688,
|
||||
0.0
|
||||
],
|
||||
"g11413": [
|
||||
299.0704040527344,
|
||||
519.8612670898438,
|
||||
0.0
|
||||
],
|
||||
"g11414": [
|
||||
299.0703430175781,
|
||||
346.65618896484375,
|
||||
1.862645149230957e-09
|
||||
],
|
||||
"g11415": [
|
||||
295.99090576171875,
|
||||
171.0111846923828,
|
||||
1.862645149230957e-09
|
||||
],
|
||||
"g11416": [
|
||||
299.912109375,
|
||||
-0.6416015625,
|
||||
0.0
|
||||
],
|
||||
"g11417": [
|
||||
299.9119873046875,
|
||||
-173.84674072265625,
|
||||
0.0
|
||||
],
|
||||
"g11418": [
|
||||
299.911865234375,
|
||||
-347.05194091796875,
|
||||
0.0
|
||||
],
|
||||
"g11419": [
|
||||
299.9117431640625,
|
||||
-520.2570190429688,
|
||||
0.0
|
||||
],
|
||||
"g11420": [
|
||||
299.91552734375,
|
||||
-693.4678344726562,
|
||||
0.0
|
||||
],
|
||||
"g11421": [
|
||||
299.8963623046875,
|
||||
-866.6572875976562,
|
||||
0.0
|
||||
],
|
||||
"g11512": [
|
||||
449.0704345703125,
|
||||
606.4638061523438,
|
||||
-4.0046870708465576e-07
|
||||
],
|
||||
"g11513": [
|
||||
449.07037353515625,
|
||||
433.25872802734375,
|
||||
1.862645149230957e-09
|
||||
],
|
||||
"g11514": [
|
||||
449.0703125,
|
||||
260.05364990234375,
|
||||
1.862645149230957e-09
|
||||
],
|
||||
"g11515": [
|
||||
451.3209228515625,
|
||||
86.68668365478516,
|
||||
4.6566128730773926e-09
|
||||
],
|
||||
"g11516": [
|
||||
450.8204345703125,
|
||||
-86.2853012084961,
|
||||
0.0
|
||||
],
|
||||
"g11517": [
|
||||
449.78082275390625,
|
||||
-259.92742919921875,
|
||||
0.0
|
||||
],
|
||||
"g11519": [
|
||||
449.90869140625,
|
||||
-606.8829345703125,
|
||||
0.0
|
||||
],
|
||||
"g11520": [
|
||||
449.8963623046875,
|
||||
-780.0547485351562,
|
||||
0.0
|
||||
],
|
||||
"g11521": [
|
||||
449.8963623046875,
|
||||
-953.2598266601562,
|
||||
0.0
|
||||
],
|
||||
"g11612": [
|
||||
599.0704345703125,
|
||||
519.8540649414062,
|
||||
-4.284083843231201e-08
|
||||
],
|
||||
"g11613": [
|
||||
599.0685424804688,
|
||||
346.6540222167969,
|
||||
-4.284083843231201e-08
|
||||
],
|
||||
"g11614": [
|
||||
605.47705078125,
|
||||
180.02053833007812,
|
||||
6.146728992462158e-08
|
||||
],
|
||||
"g11615": [
|
||||
600.9014282226562,
|
||||
-0.23164868354797363,
|
||||
4.6938657760620117e-07
|
||||
],
|
||||
"g11616": [
|
||||
600.16015625,
|
||||
-172.87147521972656,
|
||||
0.0
|
||||
],
|
||||
"g11617": [
|
||||
600.22900390625,
|
||||
-346.29901123046875,
|
||||
0.0
|
||||
],
|
||||
"g11619": [
|
||||
599.8966064453125,
|
||||
-693.4522094726562,
|
||||
0.0
|
||||
],
|
||||
"g11620": [
|
||||
599.8968505859375,
|
||||
-866.6596069335938,
|
||||
0.0
|
||||
],
|
||||
"g11711": [
|
||||
749.0704345703125,
|
||||
606.4566040039062,
|
||||
-4.284083843231201e-08
|
||||
],
|
||||
"g11712": [
|
||||
755.470947265625,
|
||||
438.18072509765625,
|
||||
6.146728992462158e-08
|
||||
],
|
||||
"g11713": [
|
||||
755.070068359375,
|
||||
265.11236572265625,
|
||||
6.146728992462158e-08
|
||||
],
|
||||
"g11714": [
|
||||
755.233154296875,
|
||||
93.22415161132812,
|
||||
6.146728992462158e-08
|
||||
],
|
||||
"g11716": [
|
||||
750.253173828125,
|
||||
-259.66363525390625,
|
||||
0.0
|
||||
],
|
||||
"g11717": [
|
||||
750.29931640625,
|
||||
-432.89617919921875,
|
||||
0.0
|
||||
],
|
||||
"g11811": [
|
||||
899.5013427734375,
|
||||
529.2744140625,
|
||||
7.636845111846924e-08
|
||||
],
|
||||
"g11812": [
|
||||
899.5413208007812,
|
||||
355.6947937011719,
|
||||
6.146728992462158e-08
|
||||
],
|
||||
"g11813": [
|
||||
905.233154296875,
|
||||
179.82669067382812,
|
||||
6.146728992462158e-08
|
||||
],
|
||||
"g11816": [
|
||||
900.1815185546875,
|
||||
-346.137451171875,
|
||||
0.0
|
||||
],
|
||||
"g11911": [
|
||||
1049.720703125,
|
||||
442.1005859375,
|
||||
1.6763806343078613e-08
|
||||
],
|
||||
"g12010": [
|
||||
1201.000732421875,
|
||||
525.80029296875,
|
||||
-4.6566128730773926e-08
|
||||
],
|
||||
"g31118": [
|
||||
-150.0880126953125,
|
||||
-87.24432373046875,
|
||||
0.0
|
||||
],
|
||||
"g31119": [
|
||||
-150.08807373046875,
|
||||
-260.44927978515625,
|
||||
0.0
|
||||
],
|
||||
"g31120": [
|
||||
-150.0880126953125,
|
||||
-433.65423583984375,
|
||||
0.0
|
||||
],
|
||||
"g31121": [
|
||||
-150.0880126953125,
|
||||
-606.8593139648438,
|
||||
0.0
|
||||
],
|
||||
"g31217": [
|
||||
-0.0880126953125,
|
||||
-0.64178466796875,
|
||||
0.0
|
||||
],
|
||||
"g31218": [
|
||||
-0.08807373046875,
|
||||
-173.84686279296875,
|
||||
0.0
|
||||
],
|
||||
"g31219": [
|
||||
-0.088134765625,
|
||||
-347.05194091796875,
|
||||
0.0
|
||||
],
|
||||
"g31220": [
|
||||
-0.088134765625,
|
||||
-520.2568359375,
|
||||
0.0
|
||||
],
|
||||
"g31221": [
|
||||
-0.08781299740076065,
|
||||
-693.4619750976562,
|
||||
0.0
|
||||
],
|
||||
"g31317": [
|
||||
149.9119873046875,
|
||||
-87.24432373046875,
|
||||
0.0
|
||||
],
|
||||
"g31318": [
|
||||
149.911865234375,
|
||||
-260.44940185546875,
|
||||
0.0
|
||||
],
|
||||
"g31319": [
|
||||
149.91180419921875,
|
||||
-433.65447998046875,
|
||||
0.0
|
||||
],
|
||||
"g31320": [
|
||||
149.91156005859375,
|
||||
-606.8595581054688,
|
||||
0.0
|
||||
],
|
||||
"g31416": [
|
||||
299.912109375,
|
||||
-0.6416015625,
|
||||
0.0
|
||||
],
|
||||
"g31417": [
|
||||
299.9119873046875,
|
||||
-173.84674072265625,
|
||||
0.0
|
||||
],
|
||||
"g31418": [
|
||||
299.911865234375,
|
||||
-347.05194091796875,
|
||||
0.0
|
||||
],
|
||||
"g31419": [
|
||||
299.9117431640625,
|
||||
-520.2570190429688,
|
||||
0.0
|
||||
],
|
||||
"g31518": [
|
||||
449.9117431640625,
|
||||
-433.6544494628906,
|
||||
0.0
|
||||
],
|
||||
"g50100": [
|
||||
0.4574565887451172,
|
||||
-0.25896692276000977,
|
||||
0.0
|
||||
],
|
||||
"g50200": [
|
||||
-0.03338015079498291,
|
||||
-0.046170711517333984,
|
||||
0.0
|
||||
],
|
||||
"g50300": [
|
||||
0.03852522373199463,
|
||||
-0.06927397847175598,
|
||||
0.016518428921699524
|
||||
],
|
||||
"g52001": [
|
||||
-0.2926056385040283,
|
||||
-0.7865628004074097,
|
||||
0.0
|
||||
],
|
||||
"g53001": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
from __future__ import annotations
|
||||
from io import BufferedReader, BufferedWriter
|
||||
from ...utils.ioUtils import *
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import List, Dict
|
||||
|
||||
class BxmHeader:
|
||||
type: str
|
||||
flags: int
|
||||
nodeCount: int
|
||||
dataCount: int
|
||||
dataSize: int
|
||||
|
||||
def fromFile(self, file: BufferedReader):
|
||||
self.type = file.read(4).decode("ascii")
|
||||
self.flags = readBe_int32(file)
|
||||
self.nodeCount = readBe_int16(file)
|
||||
self.dataCount = readBe_int16(file)
|
||||
self.dataSize = readBe_int32(file)
|
||||
|
||||
def writeToFile(self, file: BufferedWriter):
|
||||
for char in self.type:
|
||||
writeBe_char(file, char)
|
||||
writeBe_int32(file, self.flags)
|
||||
writeBe_int16(file, self.nodeCount)
|
||||
writeBe_int16(file, self.dataCount)
|
||||
writeBe_int32(file, self.dataSize)
|
||||
|
||||
class NodeInfo:
|
||||
childCount: int
|
||||
firstChildIndex: int
|
||||
attributeCount: int
|
||||
dataIndex: int
|
||||
|
||||
def fromFile(self, file: BufferedReader):
|
||||
self.childCount = readBe_int16(file)
|
||||
self.firstChildIndex = readBe_int16(file)
|
||||
self.attributeCount = readBe_int16(file)
|
||||
self.dataIndex = readBe_int16(file)
|
||||
|
||||
def writeToFile(self, file: BufferedWriter):
|
||||
writeBe_int16(file, self.childCount)
|
||||
writeBe_int16(file, self.firstChildIndex)
|
||||
writeBe_int16(file, self.attributeCount)
|
||||
writeBe_int16(file, self.dataIndex)
|
||||
|
||||
class DataOffsets:
|
||||
nameOffset: int
|
||||
valueOffset: int
|
||||
|
||||
def fromFile(self, file: BufferedReader):
|
||||
self.nameOffset = readBe_int16(file)
|
||||
self.valueOffset = readBe_int16(file)
|
||||
|
||||
def writeToFile(self, file: BufferedWriter):
|
||||
writeBe_int16(file, self.nameOffset)
|
||||
writeBe_int16(file, self.valueOffset)
|
||||
|
||||
class XmlNode:
|
||||
name: str
|
||||
value: str
|
||||
attributes: Dict
|
||||
children: List
|
||||
parent: XmlNode
|
||||
|
||||
_index: int
|
||||
_firstChildIndex: int
|
||||
_childCount: int
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.name = ""
|
||||
self.value = ""
|
||||
self.attributes = {}
|
||||
self.children = []
|
||||
self._index = -1
|
||||
self._firstChildIndex = -1
|
||||
self._childCount = -1
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.children or self.value:
|
||||
tagOpen = f"<{self.name}"
|
||||
tagOpenClose = ">"
|
||||
tagClose = f"</{self.name}>"
|
||||
else:
|
||||
tagOpen = f"<{self.name}"
|
||||
tagOpenClose = "/>"
|
||||
tagClose = ""
|
||||
attributes = ""
|
||||
if self.attributes:
|
||||
attributes = " " + " ".join([f"{key}=\"{value}\"" for key, value in self.attributes.items()])
|
||||
children = ""
|
||||
if self.children:
|
||||
children = "".join([str(child) for child in self.children])
|
||||
return f"{tagOpen}{attributes}{tagOpenClose}{self.value}{children}{tagClose}"
|
||||
|
||||
def toXml(self) -> ET.Element:
|
||||
node = ET.Element(self.name)
|
||||
if self.value:
|
||||
node.text = self.value
|
||||
for key, value in self.attributes.items():
|
||||
node.set(key, value)
|
||||
for child in self.children:
|
||||
node.append(child.toXml())
|
||||
|
||||
return node
|
||||
|
||||
def bxmToXmlFromFile(file: BufferedReader) -> ET.Element:
|
||||
header = BxmHeader()
|
||||
header.fromFile(file)
|
||||
|
||||
nodesInfos: List[NodeInfo] = []
|
||||
for i in range(header.nodeCount):
|
||||
node = NodeInfo()
|
||||
node.fromFile(file)
|
||||
nodesInfos.append(node)
|
||||
|
||||
dataOffsets: List[DataOffsets] = []
|
||||
for i in range(header.dataCount):
|
||||
dataOffset = DataOffsets()
|
||||
dataOffset.fromFile(file)
|
||||
dataOffsets.append(dataOffset)
|
||||
|
||||
stringsOffsets = 0x10 + 8*header.nodeCount + 4*header.dataCount
|
||||
|
||||
nodes: List[XmlNode] = []
|
||||
for i, nodeInfo in enumerate(nodesInfos):
|
||||
node = XmlNode()
|
||||
node._index = i
|
||||
node._firstChildIndex = nodeInfo.firstChildIndex
|
||||
node._childCount = nodeInfo.childCount
|
||||
|
||||
nodeNameOffset = dataOffsets[nodeInfo.dataIndex].nameOffset
|
||||
if nodeNameOffset != -1:
|
||||
file.seek(stringsOffsets + nodeNameOffset)
|
||||
node.name = read_string(file)
|
||||
nodeValueOffset = dataOffsets[nodeInfo.dataIndex].valueOffset
|
||||
if nodeValueOffset != -1:
|
||||
file.seek(stringsOffsets + nodeValueOffset)
|
||||
node.value = read_string(file)
|
||||
|
||||
node.attributes = {}
|
||||
for i in range(nodeInfo.attributeCount):
|
||||
attributeName = ""
|
||||
attributeValue = ""
|
||||
attributeNameOffset = dataOffsets[nodeInfo.dataIndex + 1 + i].nameOffset
|
||||
if attributeNameOffset != -1:
|
||||
file.seek(stringsOffsets + attributeNameOffset)
|
||||
attributeName = read_string(file)
|
||||
attributeValueOffset = dataOffsets[nodeInfo.dataIndex + 1 + i].valueOffset
|
||||
if attributeValueOffset != -1:
|
||||
file.seek(stringsOffsets + attributeValueOffset)
|
||||
attributeValue = read_string(file)
|
||||
node.attributes[attributeName] = attributeValue
|
||||
|
||||
nodes.append(node)
|
||||
|
||||
def getNodeNextSiblings(node: XmlNode) -> List[XmlNode]:
|
||||
return nodes[node._index + 1 : node._firstChildIndex]
|
||||
|
||||
def getNodeChildren(node: XmlNode) -> List[XmlNode]:
|
||||
if node._childCount == 0:
|
||||
return []
|
||||
firstChild = nodes[node._firstChildIndex]
|
||||
otherChildren = getNodeNextSiblings(firstChild)
|
||||
return [firstChild] + otherChildren
|
||||
|
||||
for node in nodes:
|
||||
node.children = getNodeChildren(node)
|
||||
for child in node.children:
|
||||
child.parent = node
|
||||
|
||||
xmlRootNode = nodes[0].toXml()
|
||||
return xmlRootNode
|
||||
|
||||
def bxmToXml(file: str) -> ET.Element:
|
||||
with open(file, "rb") as f:
|
||||
return bxmToXmlFromFile(f)
|
||||
|
||||
def xmlToBxm(root: ET.Element, outFileName: str) -> None:
|
||||
# flatten tree
|
||||
nodes: List[ET.Element] = []
|
||||
def getNodes(node: ET.Element):
|
||||
nodes.append(node)
|
||||
for child in node:
|
||||
getNodes(child)
|
||||
getNodes(root)
|
||||
|
||||
# gather all unique strings in tag names, tag value, attribute names and attribute values
|
||||
uniqueStrings: List[str] = []
|
||||
def tryAddString(string: str):
|
||||
if string and string not in uniqueStrings:
|
||||
uniqueStrings.append(string)
|
||||
for node in nodes:
|
||||
tryAddString(node.tag)
|
||||
for key, value in node.attrib.items():
|
||||
tryAddString(key)
|
||||
tryAddString(value)
|
||||
tryAddString(node.text and node.text.strip())
|
||||
|
||||
# calculate string offsets
|
||||
stringToOffset: Dict[str, int] = {}
|
||||
curOffset = 0
|
||||
for string in uniqueStrings:
|
||||
stringToOffset[string] = curOffset
|
||||
curOffset += len(string) + 1
|
||||
|
||||
# calculate data offsets (for strings)
|
||||
dataOffsets: List[DataOffsets] = []
|
||||
nodeToDataIndex: Dict[ET.Element, int] = {}
|
||||
for node in nodes:
|
||||
dataOffset = DataOffsets()
|
||||
dataOffset.nameOffset = stringToOffset.get(node.tag, -1)
|
||||
dataOffset.valueOffset = stringToOffset.get(node.text, -1)
|
||||
nodeToDataIndex[node] = len(dataOffsets)
|
||||
dataOffsets.append(dataOffset)
|
||||
for key, value in node.attrib.items():
|
||||
dataOffset = DataOffsets()
|
||||
dataOffset.nameOffset = stringToOffset.get(key, -1)
|
||||
dataOffset.valueOffset = stringToOffset.get(value, -1)
|
||||
dataOffsets.append(dataOffset)
|
||||
|
||||
# make node infos
|
||||
nodeInfos: List[NodeInfo] = []
|
||||
nodeInfoToXmlNode: Dict[NodeInfo, ET.Element] = {}
|
||||
nodeCombos: List[(NodeInfo, ET.Element)] = []
|
||||
parentMap = { child: parent for parent in nodes for child in parent }
|
||||
def nodeToNodeInfo(node: ET.Element) -> NodeInfo:
|
||||
nodeInfo = NodeInfo()
|
||||
nodeInfo.childCount = len(node)
|
||||
nodeInfo.attributeCount = len(node.attrib)
|
||||
nodeInfo.dataIndex = nodeToDataIndex[node]
|
||||
nodeInfoToXmlNode[nodeInfo] = node
|
||||
nodeCombos.append((nodeInfo, node))
|
||||
return nodeInfo
|
||||
|
||||
def addNodeChildrenToInfos(node: ET.Element):
|
||||
for child in node:
|
||||
nodeInfos.append(nodeToNodeInfo(child))
|
||||
for child in node:
|
||||
addNodeChildrenToInfos(child)
|
||||
nodeInfos.append(nodeToNodeInfo(root))
|
||||
addNodeChildrenToInfos(root)
|
||||
for nodeInfo in nodeInfos:
|
||||
nextIndex = -1
|
||||
if nodeInfo.childCount > 0:
|
||||
firstChild = nodeInfoToXmlNode[nodeInfo].find("*")
|
||||
nextIndex = next(i for i, (childInfo, child) in enumerate(nodeCombos) if child == firstChild)
|
||||
else:
|
||||
xmlNode = nodeInfoToXmlNode[nodeInfo]
|
||||
parent = parentMap[xmlNode]
|
||||
lastChild = parent[-1]
|
||||
lastChildIndex = next(i for i, (childInfo, child) in enumerate(nodeCombos) if child == lastChild)
|
||||
nextIndex = lastChildIndex + 1
|
||||
nodeInfo.firstChildIndex = nextIndex
|
||||
|
||||
# write file
|
||||
header = BxmHeader()
|
||||
header.type = "XML\x00"
|
||||
header.flags = 0
|
||||
header.nodeCount = len(nodeInfos)
|
||||
header.dataCount = len(dataOffsets)
|
||||
header.dataSize = sum(len(string) + 1 for string in uniqueStrings)
|
||||
|
||||
with open(outFileName, "wb") as f:
|
||||
header.writeToFile(f)
|
||||
for nodeInfo in nodeInfos:
|
||||
nodeInfo.writeToFile(f)
|
||||
for dataOffset in dataOffsets:
|
||||
dataOffset.writeToFile(f)
|
||||
for string in uniqueStrings:
|
||||
f.write(string.encode("utf-8") + b"\x00")
|
||||
@@ -0,0 +1,16 @@
|
||||
import bpy
|
||||
from bpy_extras.io_utils import ExportHelper
|
||||
|
||||
|
||||
class ExportNierGaArea(bpy.types.Operator, ExportHelper):
|
||||
'''Export a Nier:Automata Ga Area File.'''
|
||||
bl_idname = "export_scene.ga_area"
|
||||
bl_label = "Export GAArea.bxm"
|
||||
bl_options = {'PRESET', 'UNDO'}
|
||||
filename_ext = ".bxm"
|
||||
filter_glob: bpy.props.StringProperty(default="*.bxm", options={'HIDDEN'})
|
||||
|
||||
def execute(self, context):
|
||||
from . import gaAreaExporter
|
||||
gaAreaExporter.exportGaArea(self.filepath)
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,59 @@
|
||||
import bpy
|
||||
|
||||
from ..common.bxm import xmlToBxm
|
||||
from ...utils.xmlIntegrationUtils import vecToXmlVec2, setXmlAttribAsElement, vecToXmlVec3, floatToStr
|
||||
import xml.etree.ElementTree as ET
|
||||
from ...utils.util import getChildrenInOrder
|
||||
|
||||
# <PrimitiveInfo>
|
||||
# <Trans>-37.882129 -116.717265 691.357685</Trans>
|
||||
# <Top>13.976493</Top>
|
||||
# <Bottom>-247.411024</Bottom>
|
||||
# <Point0>-53.809411 722.293589</Point0>
|
||||
# <Point1>-53.809411 660.421781</Point1>
|
||||
# <Point2>-21.954847 660.421781</Point2>
|
||||
# <Point3>-21.954847 722.293589</Point3>
|
||||
# </PrimitiveInfo>
|
||||
def cubeToXml(cube: bpy.types.Object, primitiveInfo: ET.Element) -> None:
|
||||
loc = cube.location
|
||||
vertices = [v.co + loc for v in cube.data.vertices]
|
||||
points = [vecToXmlVec2(v) for v in vertices]
|
||||
bottom = vertices[0][2]
|
||||
height = cube.modifiers["Solidify"].thickness
|
||||
top = bottom + height
|
||||
|
||||
setXmlAttribAsElement(primitiveInfo, "Trans", vecToXmlVec3(loc))
|
||||
setXmlAttribAsElement(primitiveInfo, "Top", floatToStr(top))
|
||||
setXmlAttribAsElement(primitiveInfo, "Bottom", floatToStr(bottom))
|
||||
|
||||
for i, point in enumerate(points):
|
||||
setXmlAttribAsElement(primitiveInfo, f"Point{i}", point)
|
||||
|
||||
|
||||
def exportGaArea(file: str):
|
||||
print("Exporting sar")
|
||||
|
||||
if "GA_ALL" not in bpy.data.objects:
|
||||
raise "No GA_ALL in scene"
|
||||
|
||||
xmlRoot = ET.Element("GA_ALL")
|
||||
gaRoot = bpy.data.objects["GA_ALL"]
|
||||
|
||||
for ga in getChildrenInOrder(gaRoot):
|
||||
gaXml = ET.SubElement(xmlRoot, "GA")
|
||||
primitiveType = ga["xml-PrimitiveType"]
|
||||
graphicAdjustInfo = ET.SubElement(gaXml, "GraphicAdjustInfo")
|
||||
for key, value in ga.items():
|
||||
if not key.startswith("xml-GAI-"):
|
||||
continue
|
||||
setXmlAttribAsElement(graphicAdjustInfo, key[8:], value)
|
||||
setXmlAttribAsElement(gaXml, "PrimitiveType", hex(primitiveType))
|
||||
|
||||
if primitiveType == 2:
|
||||
cubeToXml(ga, ET.SubElement(gaXml, "PrimitiveInfo"))
|
||||
else:
|
||||
raise "Unknown primitive type"
|
||||
|
||||
xmlToBxm(xmlRoot, file)
|
||||
|
||||
print("Done!")
|
||||
@@ -0,0 +1,15 @@
|
||||
import bpy
|
||||
from bpy_extras.io_utils import ExportHelper
|
||||
|
||||
class ExportNierSar(bpy.types.Operator, ExportHelper):
|
||||
'''Export a Nier:Automata Sar (Skeleton) File.'''
|
||||
bl_idname = "export_scene.sar"
|
||||
bl_label = "Export Sar Data"
|
||||
bl_options = {'PRESET', 'UNDO'}
|
||||
filename_ext = ".sar"
|
||||
filter_glob: bpy.props.StringProperty(default="*.sar", options={'HIDDEN'})
|
||||
|
||||
def execute(self, context):
|
||||
from . import sarExporter
|
||||
sarExporter.exportSar(self.filepath)
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,222 @@
|
||||
import bpy
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import List
|
||||
from ..common.bxm import xmlToBxm
|
||||
from ...utils.xmlIntegrationUtils import objPosToXmlVec4, floatToStr, vecToXmlVec4, transferXmlPropsToXml
|
||||
from ...utils.util import getChildrenInOrder
|
||||
|
||||
|
||||
def getChildByStr(obj: bpy.types.Object, name: str) -> bpy.types.Object:
|
||||
for child in obj.children:
|
||||
if name in child.name:
|
||||
return child
|
||||
return None
|
||||
|
||||
# <Shape ShapeType="0" WorkType="0" Pos="933.734 34.0273 -351.43 -1" EdgeRadius="50" CoreRadius="0" />
|
||||
def handleSphere(shapeObj: bpy.types.Object, shapeElement: ET.Element) -> None:
|
||||
coreSphereObj = getChildByStr(shapeObj, "Core-Sphere")
|
||||
edgeSphereObj = getChildByStr(shapeObj, "Edge-Sphere")
|
||||
shapeElement.attrib["Pos"] = objPosToXmlVec4(coreSphereObj)
|
||||
shapeElement.attrib["CoreRadius"] = floatToStr(coreSphereObj.scale[0])
|
||||
shapeElement.attrib["EdgeRadius"] = floatToStr(edgeSphereObj.scale[0])
|
||||
|
||||
# <Shape ShapeType="1" WorkType="0" EdgeRadius="30" CoreRadius="0" IsLoop="0">
|
||||
# <Point Pos="209.823 13.4794 -289.055 1" />
|
||||
# <Point Pos="210.713 10.9276 -293.882 2" />
|
||||
# <Point Pos="227.58 6.93472 -304.964 8" />
|
||||
# </Shape>
|
||||
def handleCurve(shapeObj: bpy.types.Object, shapeElement: ET.Element) -> None:
|
||||
coreCurveObj = getChildByStr(shapeObj, "Core-Curve")
|
||||
edgeCurveObj = getChildByStr(shapeObj, "Edge-Curve")
|
||||
shapeElement.attrib["EdgeRadius"] = floatToStr(edgeCurveObj.data.bevel_depth)
|
||||
shapeElement.attrib["CoreRadius"] = floatToStr(coreCurveObj.data.bevel_depth)
|
||||
|
||||
allPosW: List[float] = coreCurveObj["allPosW"]
|
||||
allPointLocations = []
|
||||
for i, point in enumerate(coreCurveObj.data.splines[0].points):
|
||||
w = allPosW[i] if i < len(allPosW) else allPosW[-1]
|
||||
allPointLocations.append(point.co[:3] + (w,))
|
||||
|
||||
if shapeElement.attrib["IsLoop"] == "1":
|
||||
del allPointLocations[-1]
|
||||
|
||||
for point in allPointLocations:
|
||||
pointElement = ET.SubElement(shapeElement, "Point")
|
||||
pointElement.attrib["Pos"] = vecToXmlVec4(point)
|
||||
|
||||
# <Shape ShapeType="2" WorkType="0" Origin="-455.743 15.9309 -420.493 3" Rot="-0.174905 -2.27877 0 1" EdgeRadius="50" CoreRadius="1" IsLoop="0">
|
||||
# <Point Pos="0 0 0 1" Height="19.0856" />
|
||||
# <Point Pos="4.99757 0.407875 -2.30815 0" Height="19.092" />
|
||||
# <Point Pos="11.0426 0.175317 -0.992114 0" Height="20.123" />
|
||||
# </Shape>
|
||||
def handleShapeTallCurve(shapeObj: bpy.types.Object, shapeElement: ET.Element) -> None:
|
||||
shapeElement.attrib["Origin"] = objPosToXmlVec4(shapeObj)
|
||||
shapeElement.attrib["Rot"] = vecToXmlVec4(shapeObj.rotation_euler[:] + (1,))
|
||||
|
||||
meshObj = getChildByStr(shapeObj, "Core-Tall-Curve")
|
||||
radius = meshObj.modifiers["Solidify"].thickness
|
||||
shapeElement.attrib["CoreRadius"] = floatToStr(radius / 2)
|
||||
|
||||
# pos height data is in mesh faces
|
||||
allPosW: List[float] = shapeObj["allPosW"]
|
||||
vertices = meshObj.data.vertices
|
||||
if shapeElement.attrib["IsLoop"] == "1":
|
||||
del allPosW[-1]
|
||||
del allPosW[-1]
|
||||
|
||||
isReversed = False
|
||||
for i in range(int(len(vertices) / 2)):
|
||||
if isReversed:
|
||||
pos = meshObj.data.vertices[i * 2 + 1].co
|
||||
height = meshObj.data.vertices[i * 2].co[2] - pos[2]
|
||||
else:
|
||||
pos = meshObj.data.vertices[i * 2].co
|
||||
height = meshObj.data.vertices[i * 2 + 1].co[2] - pos[2]
|
||||
w = allPosW[i] if i < len(allPosW) else allPosW[-1]
|
||||
pos = pos[:3] + (w,)
|
||||
pointElement = ET.SubElement(shapeElement, "Point")
|
||||
pointElement.attrib["Pos"] = vecToXmlVec4(pos)
|
||||
pointElement.attrib["Height"] = floatToStr(height)
|
||||
isReversed = not isReversed
|
||||
|
||||
|
||||
# <Shape ShapeType="10" WorkType="0" Origin="945.463 10.5621 -365.603 4.50049" Rot="0 0.529685 0 1" Size="59.6161 21.8511 35.5307 1" DepthTop="0" DepthBottom="0" DepthSide="0 0 0 0" />
|
||||
def handleCube(shapeObj: bpy.types.Object, shapeElement: ET.Element) -> None:
|
||||
cube = getChildByStr(shapeObj, "Cube")
|
||||
shapeElement.attrib["Origin"] = objPosToXmlVec4(cube)
|
||||
shapeElement.attrib["Rot"] = vecToXmlVec4(cube.rotation_euler[:] + (1,))
|
||||
shapeElement.attrib["Size"] = vecToXmlVec4(cube.scale[:] + (1,))
|
||||
|
||||
# <Shape ShapeType="100" WorkType="0" Origin="-8.4962 -104.472 -517.295 2.25244e+011" Rot="0 -1.19805 0 1" Size="10.8398 8 0.1 1" />
|
||||
def handleSphereStretched(shapeObj: bpy.types.Object, shapeElement: ET.Element) -> None:
|
||||
sphere = getChildByStr(shapeObj, "Sphere")
|
||||
loc = list(sphere.location)
|
||||
scale = sphere.scale[:] + (1,)
|
||||
loc[0] += scale[1] / 2
|
||||
loc[1] += scale[0] / 2
|
||||
loc[2] -= scale[2] / 2
|
||||
loc += [sphere["posW"]]
|
||||
shapeElement.attrib["Origin"] = vecToXmlVec4(loc)
|
||||
shapeElement.attrib["Rot"] = vecToXmlVec4(sphere.rotation_euler[:] + (1,))
|
||||
shapeElement.attrib["Size"] = vecToXmlVec4(scale)
|
||||
|
||||
# <Shape ShapeType="15" WorkType="0" Origin="500.583 -44.6082 -166.047 -3.59599e+013" Rot="0 0 0 1" EdgeRadius="100" CoreRadius="10" Height="100" DepthTop="0" DepthBottom="0" />
|
||||
def handleCylinder(shapeObj: bpy.types.Object, shapeElement: ET.Element) -> None:
|
||||
coreCylinder = getChildByStr(shapeObj, "Core-Cylinder")
|
||||
edgeCylinder = getChildByStr(shapeObj, "Edge-Cylinder")
|
||||
height = coreCylinder.data.extrude
|
||||
loc = list(coreCylinder.location)[:3]
|
||||
loc[2] -= height / 2
|
||||
loc += [coreCylinder["posW"]]
|
||||
coreRadius = coreCylinder.scale[0]
|
||||
edgeRadius = edgeCylinder.scale[0]
|
||||
shapeElement.attrib["Origin"] = vecToXmlVec4(loc)
|
||||
shapeElement.attrib["Rot"] = vecToXmlVec4(coreCylinder.rotation_euler[:] + (1,))
|
||||
shapeElement.attrib["EdgeRadius"] = floatToStr(edgeRadius)
|
||||
shapeElement.attrib["CoreRadius"] = floatToStr(coreRadius)
|
||||
shapeElement.attrib["Height"] = floatToStr(height)
|
||||
|
||||
# <Shape ShapeType="11" WorkType="0" Origin="538.154 -28.5174 -488.861 2.5" Rot="0.0946439 0.103248 0 1" Height="14.411" DepthTop="10" DepthBottom="0">
|
||||
# <Point Pos="0 0 0 1" Depth="0" />
|
||||
# <Point Pos="-3.13521 0 5.45735 2" Depth="0" />
|
||||
# <Point Pos="15.3979 0 12.4781 1" Depth="0" />
|
||||
# <Point Pos="16.505 0 3.42961 2" Depth="0" />
|
||||
# </Shape>
|
||||
def handlePolygonExtruded(shapeObj: bpy.types.Object, shapeElement: ET.Element) -> None:
|
||||
polyObj = getChildByStr(shapeObj, "PolygonExtruded")
|
||||
height = polyObj.modifiers["Solidify"].thickness
|
||||
|
||||
shapeElement.attrib["Origin"] = objPosToXmlVec4(shapeObj)
|
||||
shapeElement.attrib["Rot"] = vecToXmlVec4(shapeObj.rotation_euler[:] + (1,))
|
||||
shapeElement.attrib["Height"] = floatToStr(height)
|
||||
|
||||
# points in vertices
|
||||
vertices = polyObj.data.vertices
|
||||
allPosW: List[float] = polyObj["allPosW"]
|
||||
allDepth: List[float] = polyObj["allDepth"]
|
||||
for i in range(len(vertices)):
|
||||
pointElement = ET.SubElement(shapeElement, "Point")
|
||||
w = allPosW[i] if i < len(allPosW) else allPosW[-1]
|
||||
depth = allDepth[i] if i < len(allDepth) else allDepth[-1]
|
||||
pointElement.attrib["Pos"] = vecToXmlVec4(vertices[i].co[:3] + (w,))
|
||||
pointElement.attrib["Depth"] = floatToStr(depth)
|
||||
|
||||
# <Shape ShapeType="200" WorkType="0" Origin="664.479 -59.7746 -333.469 2.5" Rot="0 1.61465 0 1">
|
||||
# <!-- List of sphares that create of volume when connected (loft) -->
|
||||
# <Point RightPos="0 0 0 1" LeftPos="11.4518 0 0.160929 1" Height="8" Param="0" />
|
||||
# <Point RightPos="-0.117254 0 5.11685 2" LeftPos="11.3345 0 5.27777 2" Height="8" Param="60" />
|
||||
# <Point RightPos="-0.0804682 0 8.7414 3" LeftPos="11.3736 0 8.84134 3" Height="8" Param="60" />
|
||||
# <Point RightPos="-0.0512033 0 10.8302 4" LeftPos="11.4029 0 10.9302 4" Height="8" Param="100" />
|
||||
# <Point RightPos="2.08775 0 14.9255 1" LeftPos="9.49098 0 14.7633 1" Height="8" Param="100" />
|
||||
# </Shape>
|
||||
def handleLoftedVolume(shapeObj: bpy.types.Object, shapeElement: ET.Element) -> None:
|
||||
shapeElement.attrib["Origin"] = objPosToXmlVec4(shapeObj)
|
||||
shapeElement.attrib["Rot"] = vecToXmlVec4(shapeObj.rotation_euler[:] + (1,))
|
||||
|
||||
for point in getChildrenInOrder(shapeObj):
|
||||
pointXml = ET.SubElement(shapeElement, "Point")
|
||||
|
||||
allPosW = point["allPosW"]
|
||||
leftPos = point.data.vertices[0].co[:] + (allPosW[0],)
|
||||
rightPos = point.data.vertices[1].co[:] + (allPosW[1],)
|
||||
geometryNodeTree = point.modifiers[0].node_group
|
||||
heightIdentifier = geometryNodeTree.inputs[1].identifier
|
||||
height = point.modifiers[0][heightIdentifier]
|
||||
|
||||
pointXml.attrib["RightPos"] = vecToXmlVec4(rightPos)
|
||||
pointXml.attrib["LeftPos"] = vecToXmlVec4(leftPos)
|
||||
pointXml.attrib["Height"] = floatToStr(height)
|
||||
pointXml.attrib["Param"] = point["xml-Param"]
|
||||
|
||||
|
||||
def handleShapeObj(shapeObj: bpy.types.Object, shapeElement: ET.Element):
|
||||
type = shapeObj["xml-ShapeType"]
|
||||
if type == "0": # no points (sphere?)
|
||||
handleSphere(shapeObj, shapeElement)
|
||||
elif type == "1": # list of points with Pos
|
||||
handleCurve(shapeObj, shapeElement)
|
||||
elif type == "2": # list of points with Pos and Depth
|
||||
handleShapeTallCurve(shapeObj, shapeElement)
|
||||
elif type == "10": # no points (cube?)
|
||||
handleCube(shapeObj, shapeElement)
|
||||
elif type == "11": # polygon with height
|
||||
handlePolygonExtruded(shapeObj, shapeElement)
|
||||
elif type == "15": # cylinder
|
||||
handleCylinder(shapeObj, shapeElement)
|
||||
elif type == "100": # no points (sphere?) (stretched)
|
||||
handleSphereStretched(shapeObj, shapeElement)
|
||||
elif type == "200": # 2 points with rightPos, leftPos, height, param
|
||||
handleLoftedVolume(shapeObj, shapeElement)
|
||||
else:
|
||||
print(f"Unknown shape type {type}")
|
||||
unknownTypeXml = ET.parse(shapeObj["unknownShape"])
|
||||
for child in unknownTypeXml.getroot():
|
||||
shapeElement.append(child)
|
||||
|
||||
def exportSar(file: str):
|
||||
print("Exporting sar")
|
||||
|
||||
if "Field-Root" not in bpy.data.objects:
|
||||
raise "No Field-Root in scene"
|
||||
|
||||
xmlRoot = ET.Element("Field")
|
||||
root = bpy.data.objects["Field-Root"]
|
||||
transferXmlPropsToXml(root, xmlRoot)
|
||||
|
||||
layers = getChildrenInOrder(root)
|
||||
for layer in layers:
|
||||
layerXml = ET.SubElement(xmlRoot, "Layer")
|
||||
transferXmlPropsToXml(layer, layerXml)
|
||||
|
||||
for shapeGroup in getChildrenInOrder(layer):
|
||||
shapeGroupXml = ET.SubElement(layerXml, "ShapeGroup")
|
||||
transferXmlPropsToXml(shapeGroup, shapeGroupXml)
|
||||
|
||||
for shape in getChildrenInOrder(shapeGroup):
|
||||
shapeXml = ET.SubElement(shapeGroupXml, "Shape")
|
||||
transferXmlPropsToXml(shape, shapeXml)
|
||||
handleShapeObj(shape, shapeXml)
|
||||
|
||||
xmlToBxm(xmlRoot, file)
|
||||
|
||||
print("Done!")
|
||||
@@ -0,0 +1,20 @@
|
||||
import bpy
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
|
||||
|
||||
class ImportNierGaArea(bpy.types.Operator, ImportHelper):
|
||||
'''Load a Nier:Automata Ga Area File.'''
|
||||
bl_idname = "import_scene.ga_area"
|
||||
bl_label = "Import GAArea.bxm"
|
||||
bl_options = {'PRESET', 'UNDO'}
|
||||
|
||||
filename_ext = ".bxm"
|
||||
filter_glob: bpy.props.StringProperty(default="*.bxm", options={'HIDDEN'})
|
||||
|
||||
def doImport(self, filepath):
|
||||
from . import gaAreaImporter
|
||||
gaAreaImporter.importGaArea(filepath)
|
||||
|
||||
def execute(self, context):
|
||||
self.doImport(self.filepath)
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,67 @@
|
||||
from typing import List
|
||||
import bpy
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from ..common.bxm import bxmToXml
|
||||
from ...utils.xmlIntegrationUtils import strToFloat, xmlVecToVec3, xmlVecToVec2, makeMeshObj, randomRgb, tryAddEmpty, \
|
||||
setCurrentCollection, tryAddCollection
|
||||
|
||||
# <PrimitiveInfo>
|
||||
# <Trans>-37.882129 -116.717265 691.357685</Trans>
|
||||
# <Top>13.976493</Top>
|
||||
# <Bottom>-247.411024</Bottom>
|
||||
# <Point0>-53.809411 722.293589</Point0>
|
||||
# <Point1>-53.809411 660.421781</Point1>
|
||||
# <Point2>-21.954847 660.421781</Point2>
|
||||
# <Point3>-21.954847 722.293589</Point3>
|
||||
# </PrimitiveInfo>
|
||||
def addGaCube(parent: bpy.types.Object, xml: ET.Element, color: List[float]) -> bpy.types.Object:
|
||||
bottom = strToFloat(xml.find("Bottom").text)
|
||||
height = strToFloat(xml.find("Top").text) - bottom
|
||||
loc = xmlVecToVec3(xml.find("Trans").text)
|
||||
vertices = [
|
||||
xmlVecToVec2(point.text) + [bottom]
|
||||
for point in xml if point.tag.startswith("Point")
|
||||
]
|
||||
for vert in vertices:
|
||||
for i in range(3):
|
||||
vert[i] -= loc[i]
|
||||
faces = [list(range(len(vertices)))]
|
||||
obj = makeMeshObj("obj", vertices, [], faces, parent, color)
|
||||
obj.location = loc
|
||||
obj.show_wire = True
|
||||
|
||||
solidifyMod = obj.modifiers.new("Solidify", "SOLIDIFY")
|
||||
solidifyMod.thickness = height
|
||||
solidifyMod.offset = -1
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
def importGaArea(file: str) -> None:
|
||||
print(f"Importing {file}")
|
||||
xml: ET.Element = bxmToXml(file)
|
||||
# write to file
|
||||
# with open(file + ".xml", "wb") as f:
|
||||
# f.write(ET.tostring(xml))
|
||||
assert xml.tag == "GA_ALL"
|
||||
|
||||
setCurrentCollection(tryAddCollection(f"GaArea", bpy.context.scene.collection))
|
||||
|
||||
gaRoot = tryAddEmpty("GA_ALL")
|
||||
gaRoot.hide_set(True)
|
||||
for i, ga in enumerate(xml.findall("GA")):
|
||||
primitiveType = int(ga.find("PrimitiveType").text, 16)
|
||||
|
||||
randomColor = randomRgb(str(i)) + [0.5]
|
||||
if primitiveType == 2:
|
||||
gaObj = addGaCube(gaRoot, ga.find("PrimitiveInfo"), randomColor)
|
||||
else:
|
||||
raise Exception(f"Unknown primitive type {primitiveType}")
|
||||
|
||||
filterName = ga.find("GraphicAdjustInfo").find("FilterName").text
|
||||
gaObj.name = f"{i}-GA_{filterName}"
|
||||
gaObj["xml-PrimitiveType"] = primitiveType
|
||||
for prop in ga.find("GraphicAdjustInfo"):
|
||||
gaObj[f"xml-GAI-{prop.tag}"] = prop.text
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import os
|
||||
|
||||
import bpy
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
class ImportNierSar(bpy.types.Operator, ImportHelper):
|
||||
'''Load a Nier:Automata Sar (Skeleton) File.'''
|
||||
bl_idname = "import_scene.sar"
|
||||
bl_label = "Import Sar Data"
|
||||
bl_options = {'PRESET', 'UNDO'}
|
||||
filename_ext = ".sar"
|
||||
filter_glob: bpy.props.StringProperty(default="*.sar", options={'HIDDEN'})
|
||||
|
||||
tryApplyingOffsets: bpy.props.BoolProperty(name="Try Applying Offsets", default=False)
|
||||
|
||||
onlyToXml: bpy.props.BoolProperty(name="Only Convert To XML", default=False)
|
||||
recursivelyImport: bpy.props.BoolProperty(name="Import all recursively", default=False)
|
||||
|
||||
def doImport(self, filepath):
|
||||
from . import sarImporter
|
||||
from ..common import bxm
|
||||
|
||||
if self.onlyToXml:
|
||||
xml = bxm.bxmToXml(filepath)
|
||||
with open(filepath + ".xml", "wb") as f:
|
||||
f.write(ET.tostring(xml))
|
||||
else:
|
||||
sarImporter.importSar(filepath, self.tryApplyingOffsets)
|
||||
|
||||
def execute(self, context):
|
||||
if self.recursivelyImport:
|
||||
directory = os.path.split(self.filepath)[0]
|
||||
for root, dirs, files in os.walk(directory):
|
||||
for file in files:
|
||||
if file.endswith(".sar"):
|
||||
self.doImport(root + '\\' + file)
|
||||
print("Imported all file!")
|
||||
else:
|
||||
self.doImport(self.filepath)
|
||||
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,383 @@
|
||||
import os
|
||||
import bpy
|
||||
from ..common.bxm import *
|
||||
from ...utils.xmlIntegrationUtils import xmlVecToVec3, makeSphereObj, strToFloat, setObjPosFromXmlPos, makeCurve, \
|
||||
xmlVecToVec4, makeMeshObj, makeCube, makeCircle, setXmlAttributesOnObj, tryAddEmpty, randomRgb, tryAddCollection, \
|
||||
setCurrentCollection
|
||||
from ..common.approxMapOffsets import approxMapOffsets
|
||||
|
||||
class HandleShapeParams:
|
||||
shape: ET.Element
|
||||
parentObj: bpy.types.Object
|
||||
parentId: str
|
||||
color: List[float]
|
||||
|
||||
def __init__(self, shape: ET.Element, parentObj: bpy.types.Object, parentId: str, color: List[float]):
|
||||
self.shape = shape
|
||||
self.parentObj = parentObj
|
||||
self.parentId = parentId
|
||||
self.color = color
|
||||
|
||||
# <Shape ShapeType="0" WorkType="0" Pos="933.734 34.0273 -351.43 -1" EdgeRadius="50" CoreRadius="0" />
|
||||
def handleSphere(params: HandleShapeParams) -> None:
|
||||
shape = params.shape
|
||||
parentObj = params.parentObj
|
||||
parentId = params.parentId
|
||||
color = params.color
|
||||
|
||||
parentObj.hide_set(True)
|
||||
coreSphere = makeSphereObj(
|
||||
f"Core-Sphere-{parentId}",
|
||||
strToFloat(shape.attrib["CoreRadius"]),
|
||||
parentObj,
|
||||
color
|
||||
)
|
||||
setObjPosFromXmlPos(coreSphere, shape.attrib["Pos"])
|
||||
edgeSphere = makeSphereObj(
|
||||
f"Edge-Sphere-{parentId}",
|
||||
strToFloat(shape.attrib["EdgeRadius"]),
|
||||
parentObj,
|
||||
color[:3] + [0.1]
|
||||
)
|
||||
setObjPosFromXmlPos(edgeSphere, shape.attrib["Pos"])
|
||||
|
||||
# <Shape ShapeType="1" WorkType="0" EdgeRadius="30" CoreRadius="0" IsLoop="0">
|
||||
# <Point Pos="209.823 13.4794 -289.055 1" />
|
||||
# <Point Pos="210.713 10.9276 -293.882 2" />
|
||||
# <Point Pos="227.58 6.93472 -304.964 8" />
|
||||
# </Shape>
|
||||
def handleCurve(params: HandleShapeParams) -> None:
|
||||
shape = params.shape
|
||||
parentObj = params.parentObj
|
||||
parentId = params.parentId
|
||||
color = params.color
|
||||
|
||||
parentObj.hide_set(True)
|
||||
|
||||
makeCurve(
|
||||
f"Core-Curve-{parentId}",
|
||||
[xmlVecToVec4(point.attrib["Pos"]) for point in shape],
|
||||
strToFloat(shape.attrib["CoreRadius"]),
|
||||
shape.attrib["IsLoop"] == "1",
|
||||
parentObj,
|
||||
color
|
||||
)
|
||||
makeCurve(
|
||||
f"Edge-Curve-{parentId}",
|
||||
[xmlVecToVec4(point.attrib["Pos"]) for point in shape],
|
||||
strToFloat(shape.attrib["EdgeRadius"]),
|
||||
shape.attrib["IsLoop"] == "1",
|
||||
parentObj,
|
||||
color[:3] + [0.1]
|
||||
)
|
||||
|
||||
# <Shape ShapeType="2" WorkType="0" Origin="-455.743 15.9309 -420.493 3" Rot="-0.174905 -2.27877 0 1" EdgeRadius="50" CoreRadius="1" IsLoop="0">
|
||||
# <Point Pos="0 0 0 1" Height="19.0856" />
|
||||
# <Point Pos="4.99757 0.407875 -2.30815 0" Height="19.092" />
|
||||
# <Point Pos="11.0426 0.175317 -0.992114 0" Height="20.123" />
|
||||
# </Shape>
|
||||
def handleShapeTallCurve(params: HandleShapeParams) -> None:
|
||||
shape = params.shape
|
||||
parentObj = params.parentObj
|
||||
parentId = params.parentId
|
||||
color = params.color
|
||||
|
||||
setObjPosFromXmlPos(parentObj, shape.attrib["Origin"])
|
||||
parentObj.rotation_euler = xmlVecToVec3(shape.attrib["Rot"])
|
||||
|
||||
points = shape.findall("Point")
|
||||
locations = [xmlVecToVec4(point.attrib["Pos"]) for point in points]
|
||||
parentObj["allPosW"] = [loc[3] for loc in locations]
|
||||
locations = [loc[:3] for loc in locations]
|
||||
|
||||
# initialize first edge vertices
|
||||
topLoc = locations[0][:]
|
||||
topLoc[2] += strToFloat(points[0].attrib["Height"])
|
||||
vertices = [locations[0], topLoc]
|
||||
reverseOrder = True
|
||||
# add edge vertices
|
||||
for i, point in enumerate(points):
|
||||
if i == 0:
|
||||
continue
|
||||
topLoc = locations[i][:]
|
||||
topLoc[2] += strToFloat(point.attrib["Height"])
|
||||
if reverseOrder:
|
||||
vertices.extend([topLoc, locations[i]])
|
||||
else:
|
||||
vertices.extend([locations[i], topLoc])
|
||||
reverseOrder = not reverseOrder
|
||||
# make faces
|
||||
faces: List[List[float]] = []
|
||||
reverseOrder = False
|
||||
for i in range(int(len(vertices) / 2 - 1)):
|
||||
if reverseOrder:
|
||||
faces.append([i * 2 + 3, i * 2 + 2, i * 2 + 1, i * 2])
|
||||
else:
|
||||
faces.append([i * 2, i * 2 + 1, i * 2 + 2, i * 2 + 3])
|
||||
reverseOrder = not reverseOrder
|
||||
|
||||
curveLike = makeMeshObj(
|
||||
f"Core-Tall-Curve-{parentId}",
|
||||
vertices,
|
||||
[],
|
||||
faces,
|
||||
parentObj,
|
||||
color
|
||||
)
|
||||
|
||||
# add solidify and bevel modifiers to display radius
|
||||
curveLike.modifiers.new("Solidify", type="SOLIDIFY")
|
||||
curveLike.modifiers["Solidify"].thickness = strToFloat(shape.attrib["CoreRadius"]) * 2
|
||||
curveLike.modifiers["Solidify"].offset = 0
|
||||
|
||||
curveLike.modifiers.new("Bevel", type="BEVEL")
|
||||
curveLike.modifiers["Bevel"].width = strToFloat(shape.attrib["CoreRadius"])
|
||||
curveLike.modifiers["Bevel"].segments = 4
|
||||
|
||||
|
||||
# <Shape ShapeType="10" WorkType="0" Origin="945.463 10.5621 -365.603 4.50049" Rot="0 0.529685 0 1" Size="59.6161 21.8511 35.5307 1" DepthTop="0" DepthBottom="0" DepthSide="0 0 0 0" />
|
||||
def handleCube(params: HandleShapeParams) -> None:
|
||||
shape = params.shape
|
||||
parentObj = params.parentObj
|
||||
parentId = params.parentId
|
||||
color = params.color
|
||||
|
||||
parentObj.hide_set(True)
|
||||
cubeObj = makeCube(
|
||||
f"Cube-{parentId}",
|
||||
parentObj,
|
||||
color
|
||||
)
|
||||
cubeObj.show_wire = True
|
||||
setObjPosFromXmlPos(cubeObj, shape.attrib["Origin"])
|
||||
cubeObj.rotation_euler = xmlVecToVec3(shape.attrib["Rot"])
|
||||
cubeObj.scale = xmlVecToVec3(shape.attrib["Size"])
|
||||
|
||||
# <Shape ShapeType="100" WorkType="0" Origin="-8.4962 -104.472 -517.295 2.25244e+011" Rot="0 -1.19805 0 1" Size="10.8398 8 0.1 1" />
|
||||
def handleSphereStretched(params: HandleShapeParams) -> None:
|
||||
shape = params.shape
|
||||
parentObj = params.parentObj
|
||||
parentId = params.parentId
|
||||
color = params.color
|
||||
|
||||
parentObj.hide_set(True)
|
||||
sphereObj = makeSphereObj(
|
||||
f"Sphere-{parentId}",
|
||||
0.5,
|
||||
parentObj,
|
||||
color
|
||||
)
|
||||
setObjPosFromXmlPos(sphereObj, shape.attrib["Origin"])
|
||||
sphereObj.rotation_euler = xmlVecToVec3(shape.attrib["Rot"])
|
||||
scale = xmlVecToVec3(shape.attrib["Size"])
|
||||
sphereObj.scale = scale
|
||||
sphereObj.location[0] -= scale[1] / 2
|
||||
sphereObj.location[1] -= scale[0] / 2
|
||||
sphereObj.location[2] += scale[2] / 2
|
||||
|
||||
# <Shape ShapeType="15" WorkType="0" Origin="500.583 -44.6082 -166.047 -3.59599e+013" Rot="0 0 0 1" EdgeRadius="100" CoreRadius="10" Height="100" DepthTop="0" DepthBottom="0" />
|
||||
def handleCylinder(params: HandleShapeParams) -> None:
|
||||
shape = params.shape
|
||||
parentObj = params.parentObj
|
||||
parentId = params.parentId
|
||||
color = params.color
|
||||
|
||||
parentObj.hide_set(True)
|
||||
coreCylinder = makeCircle(
|
||||
f"Core-Cylinder-{parentId}",
|
||||
1,
|
||||
parentObj,
|
||||
color
|
||||
)
|
||||
setObjPosFromXmlPos(coreCylinder, shape.attrib["Origin"])
|
||||
coreCylinder.rotation_euler = xmlVecToVec3(shape.attrib["Rot"])
|
||||
radius = strToFloat(shape.attrib["CoreRadius"])
|
||||
height = strToFloat(shape.attrib["Height"])
|
||||
coreCylinder.scale = [radius, radius, 1]
|
||||
curveData: bpy.types.Curve = coreCylinder.data
|
||||
curveData.extrude = height
|
||||
coreCylinder.location[2] += height / 2
|
||||
edgeCylinder = makeCircle(
|
||||
f"Edge-Cylinder-{parentId}",
|
||||
1,
|
||||
parentObj,
|
||||
color[:3] + [0.1]
|
||||
)
|
||||
setObjPosFromXmlPos(edgeCylinder, shape.attrib["Origin"])
|
||||
edgeCylinder.rotation_euler = xmlVecToVec3(shape.attrib["Rot"])
|
||||
radius = strToFloat(shape.attrib["EdgeRadius"])
|
||||
edgeCylinder.scale = [radius, radius, 1]
|
||||
curveData: bpy.types.Curve = edgeCylinder.data
|
||||
curveData.extrude = height
|
||||
edgeCylinder.location[2] += height / 2
|
||||
|
||||
# <Shape ShapeType="11" WorkType="0" Origin="538.154 -28.5174 -488.861 2.5" Rot="0.0946439 0.103248 0 1" Height="14.411" DepthTop="10" DepthBottom="0">
|
||||
# <Point Pos="0 0 0 1" Depth="0" />
|
||||
# <Point Pos="-3.13521 0 5.45735 2" Depth="0" />
|
||||
# <Point Pos="15.3979 0 12.4781 1" Depth="0" />
|
||||
# <Point Pos="16.505 0 3.42961 2" Depth="0" />
|
||||
# </Shape>
|
||||
def handlePolygonExtruded(params: HandleShapeParams) -> None:
|
||||
shape = params.shape
|
||||
parentObj = params.parentObj
|
||||
parentId = params.parentId
|
||||
color = params.color
|
||||
setObjPosFromXmlPos(parentObj, shape.attrib["Origin"])
|
||||
parentObj.rotation_euler = xmlVecToVec3(shape.attrib["Rot"])
|
||||
|
||||
vertices = [
|
||||
xmlVecToVec4(point.attrib["Pos"])
|
||||
for point in shape.findall("Point")
|
||||
]
|
||||
allPosW = [loc[3] for loc in vertices]
|
||||
vertices = [loc[:3] for loc in vertices]
|
||||
faces = [list(range(len(vertices)))]
|
||||
polyObj = makeMeshObj(f"PolygonExtruded-{parentId}", vertices, [], faces, parentObj, color)
|
||||
polyObj.show_wire = True
|
||||
polyObj["allPosW"] = allPosW
|
||||
polyObj["allDepth"] = [strToFloat(point.attrib["Depth"]) for point in shape.findall("Point")]
|
||||
|
||||
height = strToFloat(shape.attrib["Height"])
|
||||
solidifyMod = polyObj.modifiers.new("Solidify", "SOLIDIFY")
|
||||
solidifyMod.thickness = height
|
||||
solidifyMod.offset = 1
|
||||
|
||||
# <Shape ShapeType="200" WorkType="0" Origin="664.479 -59.7746 -333.469 2.5" Rot="0 1.61465 0 1">
|
||||
# <!-- List of sphares that create of volume when connected (loft) -->
|
||||
# <Point RightPos="0 0 0 1" LeftPos="11.4518 0 0.160929 1" Height="8" Param="0" />
|
||||
# <Point RightPos="-0.117254 0 5.11685 2" LeftPos="11.3345 0 5.27777 2" Height="8" Param="60" />
|
||||
# <Point RightPos="-0.0804682 0 8.7414 3" LeftPos="11.3736 0 8.84134 3" Height="8" Param="60" />
|
||||
# <Point RightPos="-0.0512033 0 10.8302 4" LeftPos="11.4029 0 10.9302 4" Height="8" Param="100" />
|
||||
# <Point RightPos="2.08775 0 14.9255 1" LeftPos="9.49098 0 14.7633 1" Height="8" Param="100" />
|
||||
# </Shape>
|
||||
def handleLoftedVolume(params: HandleShapeParams) -> None:
|
||||
shape = params.shape
|
||||
parentObj = params.parentObj
|
||||
parentId = params.parentId
|
||||
color = params.color
|
||||
setObjPosFromXmlPos(parentObj, shape.attrib["Origin"])
|
||||
parentObj.rotation_euler = xmlVecToVec3(shape.attrib["Rot"])
|
||||
|
||||
for i, point in enumerate(shape.findall("Point")):
|
||||
# for each point create mesh with left & right pos vertices
|
||||
leftPos = xmlVecToVec4(point.attrib["LeftPos"])
|
||||
rightPos = xmlVecToVec4(point.attrib["RightPos"])
|
||||
pointObj = makeMeshObj(
|
||||
f"{i}-Point-{parentId}",
|
||||
[leftPos[:3], rightPos[:3]],
|
||||
[[0, 1]],
|
||||
[],
|
||||
parentObj,
|
||||
color
|
||||
)
|
||||
pointObj["allPosW"] = [leftPos[3], rightPos[3]]
|
||||
pointObj["xml-Param"] = point.attrib["Param"]
|
||||
pointObj.show_wire = True
|
||||
|
||||
# with geometry nodes extrude to height
|
||||
geometryNodesMod: bpy.types.NodesModifier = pointObj.modifiers.new("GeometryNodes", "NODES")
|
||||
nodeTree = geometryNodesMod.node_group
|
||||
|
||||
inputNode = nodeTree.nodes["Group Input"]
|
||||
outputNode = nodeTree.nodes["Group Output"]
|
||||
extrudeNode = nodeTree.nodes.new("GeometryNodeExtrudeMesh")
|
||||
combineXyzNode = nodeTree.nodes.new("ShaderNodeCombineXYZ")
|
||||
|
||||
extrudeNode.mode = "EDGES"
|
||||
inputNode.outputs.new("VALUE", "Height")
|
||||
combineXyzNode.location = (-170, -140)
|
||||
|
||||
nodeTree.links.new(inputNode.outputs["Geometry"], extrudeNode.inputs["Mesh"])
|
||||
nodeTree.links.new(extrudeNode.outputs["Mesh"], outputNode.inputs["Geometry"])
|
||||
nodeTree.links.new(inputNode.outputs["Height"], combineXyzNode.inputs["Z"])
|
||||
nodeTree.links.new(combineXyzNode.outputs["Vector"], extrudeNode.inputs["Offset"])
|
||||
|
||||
heightIdentifier = nodeTree.inputs[1].identifier
|
||||
geometryNodesMod[heightIdentifier] = strToFloat(point.attrib["Height"])
|
||||
|
||||
encounteredShapes = set()
|
||||
def handleShape(params: HandleShapeParams) -> None:
|
||||
global encounteredShapes
|
||||
type = params.shape.attrib["ShapeType"]
|
||||
encounteredShapes.add(type)
|
||||
|
||||
if type == "0": # no points (sphere?)
|
||||
handleSphere(params)
|
||||
elif type == "1": # List of points with Pos
|
||||
handleCurve(params)
|
||||
elif type == "2": # List of points with Pos & height
|
||||
handleShapeTallCurve(params)
|
||||
elif type == "10": # no points (cube?)
|
||||
handleCube(params)
|
||||
elif type == "11": # polygon with height
|
||||
handlePolygonExtruded(params)
|
||||
elif type == "15": # Cylinder
|
||||
handleCylinder(params)
|
||||
elif type == "100": # no points (sphere?) (stretched)
|
||||
handleSphereStretched(params)
|
||||
elif type == "200": # points with rightPos, leftPos, height (volume in between faces?)
|
||||
handleLoftedVolume(params)
|
||||
else:
|
||||
print(f"Unknown shape type: {type}")
|
||||
params.parentObj["unknownShape"] = ET.tostring(params.shape)
|
||||
|
||||
def importSar(file: str, tryApplyingOffset: bool) -> None:
|
||||
global currentCollection
|
||||
global encounteredShapes
|
||||
encounteredShapes = set()
|
||||
|
||||
print(f"Importing {file}")
|
||||
xml: ET.Element = bxmToXml(file)
|
||||
# write to file
|
||||
with open(file + ".xml", "wb") as f:
|
||||
f.write(ET.tostring(xml))
|
||||
assert(xml.tag == "Field")
|
||||
|
||||
baseName = os.path.basename(file)
|
||||
tryAddCollection("SAR", bpy.context.scene.collection)
|
||||
setCurrentCollection(tryAddCollection(f"SAR_{baseName}", bpy.data.collections["SAR"]))
|
||||
|
||||
tileName = baseName[:6]
|
||||
rootOffsets: bpy.types.Object = None
|
||||
globalOffsets: List[float] = None
|
||||
if tileName in approxMapOffsets:
|
||||
globalOffsets = approxMapOffsets[tileName][:]
|
||||
globalOffsets[0] *= -1
|
||||
globalOffsets[1] *= -1
|
||||
if globalOffsets and tryApplyingOffset:
|
||||
rootOffsets = tryAddEmpty(f"{tileName}-offset")
|
||||
rootOffsets.location = globalOffsets
|
||||
rootOffsets.hide_set(True)
|
||||
|
||||
fieldRoot = tryAddEmpty("Field-Root", rootOffsets)
|
||||
fieldRoot.hide_set(True)
|
||||
setXmlAttributesOnObj(fieldRoot, xml)
|
||||
|
||||
for lI, layer in enumerate(xml.findall("Layer")):
|
||||
layObj = tryAddEmpty(f"{lI}-Layer-{layer.attrib['Name']}", fieldRoot)
|
||||
layObj.hide_set(True)
|
||||
setXmlAttributesOnObj(layObj, layer)
|
||||
layerColor = randomRgb(layer.attrib["Name"]) + [0.5]
|
||||
|
||||
for sgI, shapeGroup in enumerate(layer.findall("ShapeGroup")):
|
||||
shapeGroupObj = tryAddEmpty(f"{sgI}-ShapeGroup-/{lI}", layObj)
|
||||
shapeGroupObj.hide_set(True)
|
||||
setXmlAttributesOnObj(shapeGroupObj, shapeGroup)
|
||||
|
||||
for sI, shape in enumerate(shapeGroup.findall("Shape")):
|
||||
shapeObj = tryAddEmpty(f"{sI}-Shape-Type={shape.attrib['ShapeType']}-/{lI}/{sgI}", shapeGroupObj)
|
||||
setXmlAttributesOnObj(shapeObj, shape)
|
||||
handleShape(HandleShapeParams(shape, shapeObj, f"{lI}-{sgI}-{sI}", layerColor))
|
||||
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
bpy.context.view_layer.objects.active = None
|
||||
for window in bpy.context.window_manager.windows:
|
||||
for area in window.screen.areas:
|
||||
if area.type == 'VIEW_3D':
|
||||
for space in area.spaces:
|
||||
if space.type == 'VIEW_3D':
|
||||
space.shading.type = "SOLID"
|
||||
space.shading.color_type = "OBJECT"
|
||||
|
||||
print("Encountered Shapes:", encounteredShapes)
|
||||
print("Done!")
|
||||
@@ -0,0 +1,32 @@
|
||||
import bpy
|
||||
from bpy.props import StringProperty
|
||||
from bpy_extras.io_utils import ExportHelper
|
||||
|
||||
from . import col_exporter
|
||||
from ...utils.util import centre_origins, triangulate_meshes
|
||||
|
||||
|
||||
class ExportNierCol(bpy.types.Operator, ExportHelper):
|
||||
'''Export a NieR:Automata COL File'''
|
||||
bl_idname = "export.col_data"
|
||||
bl_label = "Export COL File"
|
||||
bl_options = {'PRESET'}
|
||||
filename_ext = ".col"
|
||||
filter_glob: StringProperty(default="*.col", options={'HIDDEN'})
|
||||
|
||||
generateColTree: bpy.props.BoolProperty(name="Generate Collision Tree", description="This automatically generates colTreeNodes based on your geometry and assigns the right meshes to the right colTreeNodes. Only disable it if you are manually adjusting them", default=True)
|
||||
centre_origins: bpy.props.BoolProperty(name="Centre Origins", description="This automatically centres the origins of all your objects. (Recommended)", default=True)
|
||||
triangulate_meshes: bpy.props.BoolProperty(name="Triangulate Meshes", description="This automatically adds and applies the Triangulate Modifier on all your objects. (Slow)", default=True)
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
if self.centre_origins:
|
||||
print("Centering origins...")
|
||||
centre_origins("COL")
|
||||
|
||||
if self.triangulate_meshes:
|
||||
print("Triangulating meshes...")
|
||||
triangulate_meshes("COL")
|
||||
|
||||
col_exporter.main(self.filepath, self.generateColTree)
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,16 @@
|
||||
import bpy
|
||||
from bpy.props import StringProperty
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
|
||||
|
||||
class ImportNierCol(bpy.types.Operator, ImportHelper):
|
||||
'''Load a Nier:Automata Col (Collision) File.'''
|
||||
bl_idname = "import_scene.col_data"
|
||||
bl_label = "Import Col Data"
|
||||
bl_options = {'PRESET'}
|
||||
filename_ext = ".col"
|
||||
filter_glob: StringProperty(default="*.col", options={'HIDDEN'})
|
||||
|
||||
def execute(self, context):
|
||||
from . import col_importer
|
||||
return col_importer.main(self.filepath)
|
||||
@@ -0,0 +1,140 @@
|
||||
import os
|
||||
|
||||
import bpy
|
||||
from bpy.props import StringProperty
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
|
||||
|
||||
def importDat(only_extract, filepath):
|
||||
head = os.path.split(filepath)[0]
|
||||
tail = os.path.split(filepath)[1]
|
||||
tailless_tail = tail[:-4]
|
||||
dat_filepath = head + '\\' + tailless_tail + '.dat'
|
||||
extract_dir = head + '\\nier2blender_extracted'
|
||||
from . import dat_unpacker
|
||||
if os.path.isfile(dat_filepath):
|
||||
dat_unpacker.main(dat_filepath, extract_dir + '\\' + tailless_tail + '.dat', dat_filepath) # dat
|
||||
else:
|
||||
print('DAT not found. Only extracting DTT. (No materials, collisions or layouts will automatically be imported)')
|
||||
|
||||
last_filename = dat_unpacker.main(filepath, extract_dir + '\\' + tailless_tail + '.dtt', filepath) # dtt
|
||||
|
||||
wmb_filepath = extract_dir + '\\' + tailless_tail + '.dtt\\' + last_filename[:-4] + '.wmb'
|
||||
if not os.path.exists(wmb_filepath):
|
||||
wmb_filepath = extract_dir + '\\' + tailless_tail + '.dat\\' + last_filename[:-4] + '.wmb' # if not in dtt, then must be in dat
|
||||
|
||||
# WMB
|
||||
from ...wmb.importer import wmb_importer
|
||||
wmb_importer.main(only_extract, wmb_filepath)
|
||||
|
||||
if only_extract:
|
||||
return {'FINISHED'}
|
||||
|
||||
bpy.context.scene.DatDir = extract_dir + '\\' + tailless_tail + '.dat'
|
||||
bpy.context.scene.DttDir = extract_dir + '\\' + tailless_tail + '.dtt'
|
||||
bpy.context.scene.ExportFileName = tailless_tail
|
||||
|
||||
# COL
|
||||
col_filepath = extract_dir + '\\' + tailless_tail + '.dat\\' + tailless_tail + '.col'
|
||||
if os.path.isfile(col_filepath):
|
||||
from ...col.importer import col_importer
|
||||
col_importer.main(col_filepath)
|
||||
|
||||
# LAY
|
||||
lay_filepath = extract_dir + '\\' + tailless_tail + '.dat\\' + 'Layout.lay'
|
||||
if os.path.isfile(lay_filepath):
|
||||
from ...lay.importer import lay_importer
|
||||
lay_importer.main(lay_filepath, __package__)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
class ImportNierDtt(bpy.types.Operator, ImportHelper):
|
||||
'''Load a Nier:Automata DTT (and DAT) File.'''
|
||||
bl_idname = "import_scene.dtt_data"
|
||||
bl_label = "Import DTT (and DAT) Data"
|
||||
bl_options = {'PRESET'}
|
||||
filename_ext = ".dtt"
|
||||
filter_glob: StringProperty(default="*.dtt", options={'HIDDEN'})
|
||||
|
||||
reset_blend: bpy.props.BoolProperty(name="Reset Blender Scene on Import", default=True)
|
||||
bulk_import: bpy.props.BoolProperty(name="Bulk Import All DTT/DATs In Folder (Experimental)", default=False)
|
||||
only_extract: bpy.props.BoolProperty(name="Only Extract DTT/DAT Contents. (Experimental)", default=False)
|
||||
|
||||
def execute(self, context):
|
||||
from ...wmb.importer import wmb_importer
|
||||
if self.reset_blend and not self.only_extract:
|
||||
wmb_importer.reset_blend()
|
||||
if self.bulk_import:
|
||||
folder = os.path.split(self.filepath)[0]
|
||||
for filename in os.listdir(folder):
|
||||
if filename[-4:] == '.dtt':
|
||||
try:
|
||||
filepath = folder + '\\' + filename
|
||||
importDat(self.only_extract, filepath)
|
||||
except:
|
||||
print('ERROR: FAILED TO IMPORT', filename)
|
||||
return {'FINISHED'}
|
||||
|
||||
else:
|
||||
return importDat(self.only_extract, self.filepath)
|
||||
|
||||
class ImportNierDat(bpy.types.Operator, ImportHelper):
|
||||
'''Load a Nier:Automata DAT File.'''
|
||||
bl_idname = "import_scene.dat_data"
|
||||
bl_label = "Import DAT Data"
|
||||
bl_options = {'PRESET'}
|
||||
filename_ext = ".dat"
|
||||
filter_glob: StringProperty(default="*.dat", options={'HIDDEN'})
|
||||
|
||||
reset_blend: bpy.props.BoolProperty(name="Reset Blender Scene on Import", default=True)
|
||||
bulk_import: bpy.props.BoolProperty(name="Bulk Import All DTT/DATs In Folder (Experimental)", default=False)
|
||||
only_extract: bpy.props.BoolProperty(name="Only Extract DTT/DAT Contents. (Experimental)", default=False)
|
||||
|
||||
def doImport(self, onlyExtract, filepath):
|
||||
head = os.path.split(filepath)[0]
|
||||
tail = os.path.split(filepath)[1]
|
||||
tailless_tail = tail[:-4]
|
||||
dat_filepath = head + '\\' + tailless_tail + '.dat'
|
||||
extract_dir = head + '\\nier2blender_extracted'
|
||||
from . import dat_unpacker
|
||||
if os.path.isfile(dat_filepath):
|
||||
dat_unpacker.main(dat_filepath, extract_dir + '\\' + tailless_tail + '.dat', dat_filepath) # dat
|
||||
|
||||
if onlyExtract:
|
||||
return {'FINISHED'}
|
||||
|
||||
bpy.context.scene.DatDir = extract_dir + '\\' + tailless_tail + '.dat'
|
||||
bpy.context.scene.DttDir = extract_dir + '\\' + tailless_tail + '.dtt'
|
||||
bpy.context.scene.ExportFileName = tailless_tail
|
||||
|
||||
# COL
|
||||
col_filepath = extract_dir + '\\' + tailless_tail + '.dat\\' + tailless_tail + '.col'
|
||||
if os.path.isfile(col_filepath):
|
||||
from ...col.importer import col_importer
|
||||
col_importer.main(col_filepath)
|
||||
|
||||
# LAY
|
||||
lay_filepath = extract_dir + '\\' + tailless_tail + '.dat\\' + 'Layout.lay'
|
||||
if os.path.isfile(lay_filepath):
|
||||
from ...lay.importer import lay_importer
|
||||
lay_importer.main(lay_filepath, __package__)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def execute(self, context):
|
||||
from ...wmb.importer import wmb_importer
|
||||
if self.reset_blend and not self.only_extract:
|
||||
wmb_importer.reset_blend()
|
||||
if self.bulk_import:
|
||||
folder = os.path.split(self.filepath)[0]
|
||||
for filename in os.listdir(folder):
|
||||
if filename[-4:] == '.dat':
|
||||
try:
|
||||
filepath = folder + '\\' + filename
|
||||
return self.doImport(self.only_extract, filepath)
|
||||
except:
|
||||
print('ERROR: FAILED TO IMPORT', filename)
|
||||
return {'FINISHED'}
|
||||
|
||||
else:
|
||||
return self.doImport(self.only_extract, self.filepath)
|
||||
@@ -0,0 +1,18 @@
|
||||
import bpy
|
||||
from bpy.props import StringProperty
|
||||
from bpy_extras.io_utils import ExportHelper
|
||||
|
||||
|
||||
class ExportNierLay(bpy.types.Operator, ExportHelper):
|
||||
'''Export a NieR:Automata LAY File'''
|
||||
bl_idname = "export.lay_data"
|
||||
bl_label = "Export LAY File"
|
||||
bl_options = {'PRESET'}
|
||||
filename_ext = ".lay"
|
||||
filter_glob: StringProperty(default="*.lay", options={'HIDDEN'})
|
||||
|
||||
def execute(self, context):
|
||||
from . import lay_exporter
|
||||
|
||||
lay_exporter.main(self.filepath)
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,16 @@
|
||||
import bpy
|
||||
from bpy.props import StringProperty
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
|
||||
|
||||
class ImportNierLay(bpy.types.Operator, ImportHelper):
|
||||
'''Load a Nier:Automata Lay (Layout) File.'''
|
||||
bl_idname = "import_scene.lay_data"
|
||||
bl_label = "Import Lay Data"
|
||||
bl_options = {'PRESET'}
|
||||
filename_ext = ".lay"
|
||||
filter_glob: StringProperty(default="*.lay", options={'HIDDEN'})
|
||||
|
||||
def execute(self, context):
|
||||
from . import lay_importer
|
||||
return lay_importer.main(self.filepath, __package__)
|
||||
+9
-4
@@ -1,4 +1,6 @@
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
import bmesh
|
||||
import bpy
|
||||
import numpy as np
|
||||
@@ -66,13 +68,16 @@ def objectsInCollectionInOrder(collectionName):
|
||||
def allObjectsInCollectionInOrder(collectionName):
|
||||
return sorted(bpy.data.collections[collectionName].all_objects, key=getObjKey) if collectionName in bpy.data.collections else []
|
||||
|
||||
def getChildrenInOrder(obj: bpy.types.Object) -> List[bpy.types.Object]:
|
||||
return sorted(obj.children, key=getObjKey)
|
||||
|
||||
def create_dir(dirpath):
|
||||
if not os.path.exists(dirpath):
|
||||
os.makedirs(dirpath)
|
||||
if not os.path.exists(dirpath):
|
||||
os.makedirs(dirpath)
|
||||
|
||||
def print_class(obj):
|
||||
print ('\n'.join(sorted(['%s:\t%s ' % item for item in obj.__dict__.items() if item[0].find('Offset') < 0 or item[0].find('unknown') < 0 ])))
|
||||
print('\n')
|
||||
print ('\n'.join(sorted(['%s:\t%s ' % item for item in obj.__dict__.items() if item[0].find('Offset') < 0 or item[0].find('unknown') < 0 ])))
|
||||
print('\n')
|
||||
|
||||
def getObjectVolume(obj):
|
||||
return np.prod(obj.dimensions)
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
import math
|
||||
import random
|
||||
import re
|
||||
from typing import Tuple, List, Dict
|
||||
import bpy
|
||||
from mathutils import Color
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
currentCollection: bpy.types.Collection = None
|
||||
def setCurrentCollection(col: bpy.types.Collection):
|
||||
global currentCollection
|
||||
currentCollection = col
|
||||
|
||||
|
||||
def tryAddCollection(collName: str, parent: bpy.types.Collection) -> bpy.types.Collection:
|
||||
if collName in bpy.data.collections:
|
||||
return bpy.data.collections[collName]
|
||||
else:
|
||||
newColl = bpy.data.collections.new(collName)
|
||||
parent.children.link(newColl)
|
||||
return newColl
|
||||
|
||||
|
||||
def tryAddEmpty(name: str, parentObj: bpy.types.Object = None) -> bpy.types.Object:
|
||||
if name in bpy.data.objects and (
|
||||
bpy.data.objects[name].parent == parentObj and bpy.data.objects[name].users_collection[
|
||||
0] == currentCollection):
|
||||
for child in list(bpy.data.objects[name].children):
|
||||
bpy.data.objects.remove(child, do_unlink=True)
|
||||
bpy.data.objects.remove(bpy.data.objects[name], do_unlink=True)
|
||||
newObj = bpy.data.objects.new(name, None)
|
||||
currentCollection.objects.link(newObj)
|
||||
if parentObj is not None:
|
||||
newObj.parent = parentObj
|
||||
return newObj
|
||||
|
||||
|
||||
def prepareObject(obj: bpy.types.Object, name: str, parent: bpy.types.Object, color: List[float] = None) -> None:
|
||||
obj.name = name
|
||||
obj.parent = parent
|
||||
if color:
|
||||
obj.color = color
|
||||
for coll in obj.users_collection:
|
||||
coll.objects.unlink(obj)
|
||||
currentCollection.objects.link(obj)
|
||||
|
||||
|
||||
def makeMeshObj(name: str, vertices: List[List[float]], edges: List[List[float]], faces: List[List[float]],
|
||||
parent: bpy.types.Object, color: List[float] = None) -> bpy.types.Object:
|
||||
cube = bpy.data.meshes.new(name)
|
||||
cubeObj = bpy.data.objects.new(name, cube)
|
||||
prepareObject(cubeObj, name, parent, color)
|
||||
cubeObj.show_wire = True
|
||||
cube.from_pydata(vertices, edges, faces)
|
||||
|
||||
# entering & exiting edit mode fixes some crashes
|
||||
bpy.context.view_layer.objects.active = cubeObj
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.context.view_layer.objects.active = None
|
||||
|
||||
return cubeObj
|
||||
|
||||
|
||||
def makeCube(name, parent: bpy.types.Object, color: List[float], originAtCorner=True) -> bpy.types.Object:
|
||||
vertices = [
|
||||
[0, 0, 0],
|
||||
[1, 0, 0],
|
||||
[1, 1, 0],
|
||||
[0, 1, 0],
|
||||
[0, 0, 1],
|
||||
[1, 0, 1],
|
||||
[1, 1, 1],
|
||||
[0, 1, 1]
|
||||
]
|
||||
if not originAtCorner:
|
||||
for i in range(len(vertices)):
|
||||
for j in range(3):
|
||||
vertices[i][j] -= 0.5
|
||||
faces = [
|
||||
[0, 1, 2, 3],
|
||||
[4, 5, 6, 7],
|
||||
[0, 1, 5, 4],
|
||||
[2, 3, 7, 6],
|
||||
[0, 3, 7, 4],
|
||||
[1, 2, 6, 5]
|
||||
]
|
||||
cubeObj = makeMeshObj(name, vertices, [], faces, parent, color)
|
||||
|
||||
return cubeObj
|
||||
|
||||
|
||||
def makeSphereObj(name: str, radius: float, parent: bpy.types.Object, color: List[float]) -> bpy.types.Object:
|
||||
bpy.ops.mesh.primitive_uv_sphere_add(radius=1)
|
||||
sphereObj = bpy.context.active_object
|
||||
prepareObject(sphereObj, name, parent, color)
|
||||
sphereObj.scale = [radius, radius, radius]
|
||||
|
||||
return sphereObj
|
||||
|
||||
|
||||
def makeCurve(name: str, points: List[List[float]], radius: float, isLoop: bool, parent: bpy.types.Object,
|
||||
color: List[float]) -> bpy.types.Object:
|
||||
curve: bpy.types.Curve = bpy.data.curves.new(name, "CURVE")
|
||||
curveObj = bpy.data.objects.new(name, curve)
|
||||
prepareObject(curveObj, name, parent, color)
|
||||
|
||||
curve.dimensions = "3D"
|
||||
curve.splines.new(type="POLY")
|
||||
|
||||
locations = points
|
||||
if len(locations[0]) == 4:
|
||||
wLocs = [loc[3] for loc in locations]
|
||||
curveObj["allPosW"] = wLocs
|
||||
if isLoop:
|
||||
locations.append(locations[0])
|
||||
|
||||
curve.splines.active.points.add(len(locations) - 1)
|
||||
for i, loc in enumerate(locations):
|
||||
curvePoint = curve.splines[0].points[i]
|
||||
if len(loc) == 4:
|
||||
loc[3] = 1
|
||||
else:
|
||||
loc.append(1)
|
||||
curvePoint.co = loc
|
||||
curve.splines[0].use_endpoint_u = True
|
||||
curve.splines[0].use_endpoint_v = False
|
||||
|
||||
curve.bevel_mode = "ROUND"
|
||||
curve.bevel_depth = radius
|
||||
curve.bevel_resolution = 8
|
||||
|
||||
return curveObj
|
||||
|
||||
|
||||
def makeBezier(
|
||||
name: str,
|
||||
points: List[List[float]],
|
||||
leftHandles: List[List[float]], rightHandles: List[List[float]],
|
||||
loops: bool,
|
||||
parent: bpy.types.Object,
|
||||
radius: float,
|
||||
color: List[float]
|
||||
) -> bpy.types.Object:
|
||||
curve: bpy.types.Curve = bpy.data.curves.new(name, "CURVE")
|
||||
curveObj = bpy.data.objects.new(name, curve)
|
||||
prepareObject(curveObj, name, parent, color)
|
||||
|
||||
curve.dimensions = "3D"
|
||||
curve.splines.new(type="BEZIER")
|
||||
|
||||
locations = points
|
||||
|
||||
curve.splines.active.bezier_points.add(len(locations) - 1)
|
||||
for i in range(len(locations)):
|
||||
curvePoint = curve.splines[0].bezier_points[i]
|
||||
curvePoint.co = locations[i]
|
||||
curvePoint.handle_left = leftHandles[i] if leftHandles else locations[i]
|
||||
curvePoint.handle_right = rightHandles[i] if rightHandles else locations[i]
|
||||
|
||||
curve.splines[0].use_cyclic_u = loops
|
||||
|
||||
if radius > 0:
|
||||
curve.bevel_mode = "ROUND"
|
||||
curve.bevel_depth = radius
|
||||
curve.bevel_resolution = 8
|
||||
|
||||
return curveObj
|
||||
|
||||
|
||||
def makeCircle(name: str, radius: float, parent: bpy.types.Object, color: List[float]) -> bpy.types.Object:
|
||||
bpy.ops.curve.primitive_bezier_circle_add(radius=radius)
|
||||
circleObj = bpy.context.active_object
|
||||
prepareObject(circleObj, name, parent, color)
|
||||
return circleObj
|
||||
|
||||
|
||||
# importing - misc
|
||||
|
||||
seedOffsets: Dict[str, int] = {}
|
||||
def randomRgb(seed: str = "") -> List[float]:
|
||||
if seed not in seedOffsets:
|
||||
seedOffsets[seed] = 0
|
||||
random.seed(f"{seed}{seedOffsets[seed]}")
|
||||
seedOffsets[seed] += 1
|
||||
color = Color()
|
||||
color.hsv = [random.random(), 1, 1]
|
||||
return [color.r, color.g, color.b]
|
||||
|
||||
|
||||
def setXmlAttributesOnObj(obj: bpy.types.Object, xml: ET.Element):
|
||||
for attr in xml.attrib:
|
||||
obj[f"xml-{attr}"] = xml.attrib[attr]
|
||||
|
||||
|
||||
def strToFloat(str: str) -> float:
|
||||
if "#IND" in str:
|
||||
return float("nan")
|
||||
if str == "1.#INF":
|
||||
return float("inf")
|
||||
if str == "-1.#INF":
|
||||
return float("-inf")
|
||||
return float(str)
|
||||
|
||||
|
||||
def xmlVecToVec2(vecStr: str) -> List[float]:
|
||||
vals = [strToFloat(s) for s in vecStr.split(" ")]
|
||||
return [vals[0], -vals[1]]
|
||||
|
||||
|
||||
def xmlVecToVec3(vecStr: str) -> List[float]:
|
||||
vals = [strToFloat(s) for s in vecStr.split(" ")]
|
||||
return [vals[0], -vals[2], vals[1]]
|
||||
|
||||
|
||||
def xmlVecToVec4(vecStr: str) -> List[float]:
|
||||
vals = [strToFloat(s) for s in vecStr.split(" ")]
|
||||
return [vals[0], -vals[2], vals[1], vals[3]]
|
||||
|
||||
|
||||
def setObjPosFromXmlPos(obj: bpy.types.Object, posStr: str) -> None:
|
||||
vals = xmlVecToVec4(posStr)
|
||||
obj.location = vals[:3]
|
||||
obj["posW"] = vals[3]
|
||||
|
||||
|
||||
# exporting
|
||||
|
||||
def transferXmlPropsToXml(obj: bpy.types.Object, element: ET.Element):
|
||||
for prop in obj.keys():
|
||||
if prop.startswith("xml-"):
|
||||
element.attrib[prop[4:]] = obj[prop]
|
||||
|
||||
|
||||
biggestFloatInt = 2 ** 24
|
||||
|
||||
|
||||
def floatFmt(f: float):
|
||||
expFallback = str(f)
|
||||
if abs(f) > biggestFloatInt and "e" not in expFallback:
|
||||
expFallback = f"{f:e}"
|
||||
if "e" in expFallback:
|
||||
eSplitPos = expFallback.find("e") + 2
|
||||
p1 = expFallback[:eSplitPos]
|
||||
p2 = expFallback[eSplitPos:]
|
||||
p2 = (3 - len(p2)) * "0" + p2
|
||||
return p1 + p2
|
||||
fStr = f"{f:.4f}"
|
||||
fStr = re.sub(r"\.?0+$", "", fStr)
|
||||
return fStr
|
||||
|
||||
|
||||
def floatToStr(num: float) -> str:
|
||||
if math.isnan(num):
|
||||
return "-1.#IND"
|
||||
if num == float("inf"):
|
||||
return "1.#INF"
|
||||
if num == float("-inf"):
|
||||
return "-1.#INF"
|
||||
return floatFmt(num)
|
||||
|
||||
|
||||
def vecToXmlVec2(vec: Tuple[float, float, float]) -> str:
|
||||
return f"{floatToStr(vec[0])} {floatToStr(-vec[1])}"
|
||||
|
||||
|
||||
def vecToXmlVec3(vec: Tuple[float, float, float]) -> str:
|
||||
return f"{floatToStr(vec[0])} {floatToStr(vec[2])} {floatToStr(-vec[1])}"
|
||||
|
||||
|
||||
def vecToXmlVec4(vec: Tuple[float, float, float, float]) -> str:
|
||||
return f"{floatToStr(vec[0])} {floatToStr(vec[2])} {floatToStr(-vec[1])} {floatToStr(vec[3])}"
|
||||
|
||||
|
||||
def objPosToXmlVec4(obj: bpy.types.Object) -> str:
|
||||
return f"{floatToStr(obj.location[0])} {floatToStr(obj.location[2])} {floatToStr(-obj.location[1])} {floatToStr(obj['posW'])}"
|
||||
|
||||
|
||||
def setXmlAttribAsElement(element: ET.Element, attr: str, value: str):
|
||||
subElem = ET.SubElement(element, attr)
|
||||
subElem.text = value
|
||||
@@ -0,0 +1,50 @@
|
||||
import traceback
|
||||
|
||||
import bpy
|
||||
from bpy.props import StringProperty
|
||||
from bpy_extras.io_utils import ExportHelper
|
||||
|
||||
|
||||
class ExportNierWmb(bpy.types.Operator, ExportHelper):
|
||||
'''Export a NieR:Automata WMB File'''
|
||||
bl_idname = "export.wmb_data"
|
||||
bl_label = "Export WMB File"
|
||||
bl_options = {'PRESET'}
|
||||
filename_ext = ".wmb"
|
||||
filter_glob: StringProperty(default="*.wmb", options={'HIDDEN'})
|
||||
|
||||
centre_origins: bpy.props.BoolProperty(name="Centre Origins", description="This automatically centres the origins of all your objects. (Recommended)", default=True)
|
||||
triangulate_meshes: bpy.props.BoolProperty(name="Triangulate Meshes", description="This automatically adds and applies the Triangulate Modifier on all your objects. Only disable if you know your meshes are triangulated and you wish to reduce export times", default=True)
|
||||
delete_loose_geometry: bpy.props.BoolProperty(name="Delete Loose Geometry", description="This automatically runs the 'Delete Loose Geometry (All)' operator before exporting. It deletes all loose vertices or edges that could result in unwanted results in-game", default=True)
|
||||
|
||||
def execute(self, context):
|
||||
from . import wmb_exporter
|
||||
|
||||
bpy.data.collections['WMB'].all_objects[0].select_set(True)
|
||||
|
||||
if self.centre_origins:
|
||||
print("Centering origins...")
|
||||
wmb_exporter.centre_origins()
|
||||
|
||||
"""
|
||||
if self.purge_materials:
|
||||
print("Purging materials...")
|
||||
wmb_exporter.purge_unused_materials()
|
||||
"""
|
||||
|
||||
if self.triangulate_meshes:
|
||||
print("Triangulating meshes...")
|
||||
wmb_exporter.triangulate_meshes()
|
||||
|
||||
if self.delete_loose_geometry:
|
||||
print("Deleting loose geometry...")
|
||||
bpy.ops.b2n.deleteloosegeometryall()
|
||||
|
||||
try:
|
||||
print("Starting export...")
|
||||
wmb_exporter.main(self.filepath)
|
||||
return wmb_exporter.restore_blend()
|
||||
except:
|
||||
print(traceback.format_exc())
|
||||
self.report({'ERROR'}, "An unexpected error has occurred during export. Please check the console for more info.")
|
||||
return {'CANCELLED'}
|
||||
@@ -0,0 +1,20 @@
|
||||
import bpy
|
||||
from bpy.props import StringProperty
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
|
||||
|
||||
class ImportNierWmb(bpy.types.Operator, ImportHelper):
|
||||
'''Load a Nier:Automata WMB File.'''
|
||||
bl_idname = "import_scene.wmb_data"
|
||||
bl_label = "Import WMB Data"
|
||||
bl_options = {'PRESET'}
|
||||
filename_ext = ".wmb"
|
||||
filter_glob: StringProperty(default="*.wmb", options={'HIDDEN'})
|
||||
|
||||
reset_blend: bpy.props.BoolProperty(name="Reset Blender Scene on Import", default=True)
|
||||
|
||||
def execute(self, context):
|
||||
from . import wmb_importer
|
||||
if self.reset_blend:
|
||||
wmb_importer.reset_blend()
|
||||
return wmb_importer.main(False, self.filepath)
|
||||
Reference in New Issue
Block a user