restructured import & export operators into separate files + added .sar & GAArea import/export

This commit is contained in:
ArthurHeitmann
2022-06-28 11:04:35 +02:00
parent 5f35978032
commit c783a40b23
21 changed files with 2441 additions and 275 deletions
+26 -271
View File
@@ -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()
+737
View File
@@ -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
]
}
+271
View File
@@ -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")
+16
View File
@@ -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'}
+59
View File
@@ -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!")
+15
View File
@@ -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'}
+222
View File
@@ -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!")
+20
View File
@@ -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'}
+67
View File
@@ -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
+42
View File
@@ -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'}
+383
View File
@@ -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!")
+32
View File
@@ -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'}
+16
View File
@@ -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)
+140
View File
@@ -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)
+18
View File
@@ -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'}
+16
View File
@@ -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
View File
@@ -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)
+282
View File
@@ -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
+50
View File
@@ -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'}
+20
View File
@@ -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)