feat(tools): added & slightly improved Blender gltf-auto-export tool

This commit is contained in:
kaosat.dev 2023-07-26 23:34:08 +02:00
parent 5de91bf720
commit 6eace81fde
2 changed files with 790 additions and 0 deletions

View File

@ -0,0 +1,46 @@
#TODO: this is not actually in use yet, just use the blender_auto_export_gltf.py file
bl_info = {
"name": "Test glTF/glb auto-export",
"author": "kaosigh",
"version": (0, 1),
"blender": (3, 4, 0),
"location": "File > Import-Export",
"description": "glTF/glb auto-export",
"warning": "",
"wiki_url": "",
"tracker_url": "",
"category": "Import-Export"
}
import bpy
from .blender_auto_export_gltf import TEST_AUTO_OT_gltf
from .blender_auto_export_gltf import deps_update_handler
from .blender_auto_export_gltf import save_handler
from .blender_auto_export_gltf import get_changedScene
from .blender_auto_export_gltf import set_ChangedScene
# Only needed if you want to add into a dynamic menu
def menu_func_import(self, context):
self.layout.operator(TEST_AUTO_OT_gltf.bl_idname, text="glTF auto Export (.glb/gltf)")
def register():
bpy.utils.register_class(TEST_AUTO_OT_gltf)
bpy.types.TOPBAR_MT_file_export.append(menu_func_import)
bpy.app.handlers.depsgraph_update_post.append(deps_update_handler)
bpy.app.handlers.save_post.append(save_handler)
#bpy.types.TOPBAR_MT_file_export.append(menu_func_import)
bpy.types.Scene.changedScene = bpy.props.StringProperty(get=get_changedScene, set=set_ChangedScene)
def unregister():
bpy.utils.unregister_class(TEST_AUTO_OT_gltf)
bpy.types.TOPBAR_MT_file_export.remove(menu_func_import)
bpy.app.handlers.depsgraph_update_post.remove(deps_update_handler)
bpy.app.handlers.save_post.remove(save_handler)
#bpy.types.TOPBAR_MT_file_export.remove(menu_func_import)
del bpy.types.Scene.changedScene

View File

@ -0,0 +1,744 @@
bl_info = {
"name": "blender_auto_export_gltf",
"author": "kaosigh",
"version": (0, 1),
"blender": (3, 6, 0),
"location": "File > Import-Export",
"description": "glTF/glb auto-export",
"warning": "",
"wiki_url": "",
"tracker_url": "",
"category": "Import-Export"
}
import os
import bpy
from bpy.types import Operator, AddonPreferences
from bpy.app.handlers import persistent
from bpy_extras.io_utils import ExportHelper
from bpy.props import (BoolProperty,
IntProperty,
StringProperty,
EnumProperty,
CollectionProperty
)
#see here for original gltf exporter infos https://github.com/KhronosGroup/glTF-Blender-IO/blob/main/addons/io_scene_gltf2/__init__.py
@persistent
def deps_update_handler(scene):
print("-------------")
print("depsgraph_update_post", scene.name)
changed = scene.name or ""
print("changed", changed)
bpy.context.scene.changedScene = changed
#set_ChangedScene(changed)
#auto_export()
#https://docs.blender.org/api/current/bpy.ops.export_scene.html#bpy.ops.export_scene.gltf
def export_gltf (path, export_settings):
settings = {**export_settings, "filepath": path}
bpy.ops.export_scene.gltf(**settings)
def get_collection_hierarchy(root_col, levels=1):
"""Read hierarchy of the collections in the scene"""
level_lookup = {}
def recurse(root_col, parent, depth):
if depth > levels:
return
if isinstance(parent, bpy.types.Collection):
level_lookup.setdefault(parent, []).append(root_col)
for child in root_col.children:
recurse(child, root_col, depth + 1)
recurse(root_col, root_col.children, 0)
return level_lookup
"""
This exports the library's collections into seperate gltf files
"""
def export_library_split(scene, folder_path, gltf_export_preferences):
# backup current active scene
old_current_scene = bpy.context.scene
# set active scene to be the given scene
bpy.context.window.scene = scene
export_settings = { **gltf_export_preferences, 'use_active_scene': True}
root_collection = scene.collection
collections_lookup = get_collection_hierarchy(root_collection, 3)
children_to_parent_collections = {i : k for k, v in collections_lookup.items() for i in v}
scene_objects = [o for o in root_collection.objects]
candidates = [x for v in collections_lookup.values() for x in v]
"""
print("prt_col", children_to_parent_collections)
print("scene objects", scene_objects)
print("candidate", candidates)
"""
if not candidates:
# self.report({'INFO'}, "Nothing to export")
# reset current scene from backup
bpy.context.window.scene = old_current_scene
return #{'CANCELLED'}
# Unlink all Collections and objects
for canditate in candidates:
children_to_parent_collections.get(canditate).children.unlink(canditate)
for object in scene_objects:
scene_objects.objects.unlink(object)
# (Re-)link collections of choice to root level and export
for canditate in candidates:
root_collection.children.link(canditate)
collection_name = canditate.name
gltf_output_path = os.path.join(folder_path, collection_name)
export_gltf(gltf_output_path, export_settings)
print("exporting", collection_name, "to", gltf_output_path)
root_collection.children.unlink(canditate)
# Reset all back
for object in scene_objects:
scene_objects.objects.link(object)
for canditate in candidates:
children_to_parent_collections.get(canditate).children.link(canditate)
# reset current scene from backup
bpy.context.window.scene = old_current_scene
def debug_test(scene):
root_collection = scene.collection
collections_lookup = get_collection_hierarchy(root_collection, 1)
children_to_parent_collections = {i : k for k, v in collections_lookup.items() for i in v}
scene_objects = [o for o in root_collection.objects]
candidates = [x for v in collections_lookup.values() for x in v]
print("prt_col", children_to_parent_collections)
print("scene objects", scene_objects)
print("candidates", candidates)
"""
export the library into only a few gltf files ie
scene_collection
asset_pack1
asset_a
asset_b
asset_c
asset_pack2
asset_d
asset_e
would export 2 gltf files
asset_pack1.glb
with three scenes:
asset_a
asset_b
asset_c
asset_pack2.glb
with two scenes:
asset_d
asset_f
"""
def export_library_merged(scene, folder_path, gltf_export_preferences):
# backup current active scene
old_current_scene = bpy.context.scene
# set active scene to be the given scene
bpy.context.window.scene = scene
export_settings = {
**gltf_export_preferences,
'use_active_scene': False,
'use_visible': False,
}
root_collection = scene.collection
collections_lookup = get_collection_hierarchy(root_collection, 3)
children_to_parent_collections = {i : k for k, v in collections_lookup.items() for i in v}
scene_objects = [o for o in root_collection.objects]
candidates = [x for v in collections_lookup.values() for x in v]
"""
print("prt_col", children_to_parent_collections)
print("scene objects", scene_objects)
print("candidate", candidates)
"""
virtual_scenes = []
if not candidates:
# self.report({'INFO'}, "Nothing to export")
return #{'CANCELLED'}
for canditate in candidates:
#print("candidate collection", canditate)
virtual_scene = bpy.data.scenes.new(name=canditate.name)
virtual_scenes.append(virtual_scene)
virtual_scene.collection.children.link(canditate)
try:
gltf_output_path = os.path.join(folder_path, "library")
export_gltf(gltf_output_path, export_settings)
except Exception:
print("failed to export to gltf")
for virtual_scene in virtual_scenes:
bpy.data.scenes.remove(virtual_scene)
# TODO: we want to exclude the library and the game scene ???
"""
#backup test
backup = bpy.data.scenes["toto"]
## add back the scene
#bpy.data.scenes["toto"] = backup
collection = ""
try:
collection = bpy.data.collections["virtual"]
bpy.data.collections.remove(collection)
collection = bpy.data.collections.new("virtual")
except Exception:
collection = bpy.data.collections.new("virtual")
raise
#bpy.data.scenes["toto"].collection.children.unlink(bpy.data.scenes["toto"].collection)
#print("copy", collection)
# nuke the scene
toto = bpy.data.scenes["toto"]
print(" toto.collection", toto.collection.children)
for child in toto.collection.children:
print("child ", child)
for child in toto.collection.objects:
print("child ", child)
collection.children.link(toto.collection)
for child in collection.children:
print("child 2 ", child)
for child in toto.collection.objects:
print("child ", child)
toto.collection.objects.unlink(child)
#toto.collection.children.unlink(toto.collection)
bpy.data.scenes.remove(toto)
# now recreate it
toto = bpy.data.scenes.new(name="toto")
toto.collection.children.link(collection)
for child in collection.objects:
print("adding back child ", child)
toto.collection.objects.link(child)
"""
# reset current scene from backup
bpy.context.window.scene = old_current_scene
def export_main(scene, folder_path, gltf_export_preferences, output_name):
print("exporting to", folder_path, output_name)
# backup current active scene
old_current_scene = bpy.context.scene
# set active scene to be the given scene
bpy.context.window.scene = scene
gltf_output_path = os.path.join(folder_path, output_name)
export_settings = { **gltf_export_preferences, 'use_active_scene': True}
export_gltf(gltf_output_path, export_settings)
# reset current scene from backup
bpy.context.window.scene = old_current_scene
def auto_export():
file_path = bpy.data.filepath
# Get the folder
folder_path = os.path.dirname(file_path)
addon_prefs = bpy.context.preferences.addons[__name__].preferences
library_scene = bpy.data.scenes["library"]
"""
print("folder", folder_path)
scn_col = bpy.context.scene.collection
print("scene", scn_col)
print("scenes", library_scene, game_scene)
library_root_collection = library_scene.collection
library_base_collections_lookup = get_collection_hierarchy(library_root_collection, 3)
print("lib root collection", library_root_collection)
print("all collections", library_base_collections_lookup)
"""
"""
lkp_col = get_collection_hierarchy(scn_col, levels=3)
prt_col = {i : k for k, v in lkp_col.items() for i in v}
scn_obj = [o for o in scn_col.objects]
candidates = [x for v in lkp_col.values() for x in v]
print("scn_col", scn_col)
print("lkp_col", lkp_col)
print("prt_col", prt_col)
print("scene objects", scn_obj)
print("candidate", candidates)
"""
print("-------------")
gltf_export_preferences = dict(
export_format= 'GLB', #'GLB', 'GLTF_SEPARATE', 'GLTF_EMBEDDED'
check_existing=False,
use_selection=False,
use_visible=True, # Export visible and hidden objects. See Object/Batch Export to skip.
use_renderable=False,
use_active_collection= False,
use_active_collection_with_nested=False,
use_active_scene = False,
export_texcoords=True,
export_normals=True,
# here add draco settings
export_draco_mesh_compression_enable = False,
export_tangents=False,
#export_materials
export_colors=True,
export_attributes=True,
#use_mesh_edges
#use_mesh_vertices
export_cameras=True,
export_extras=True, # For custom exported properties.
export_lights=True,
export_yup=False,
export_skins=True,
export_morph=False,
export_apply=False,
export_animations=False
)
for key in addon_prefs.__annotations__.keys():
if key is not "export_game" and key is not "export_game_scene_name" and key is not "export_game_output_name": #FIXME: ugh, cleanup
gltf_export_preferences[key] = getattr(addon_prefs,key)
print("overriding setting", key, "value", getattr(addon_prefs,key))
# testing (we want an in-memory scene, not one that is visible in the ui)
#invisible_scene = bpy.types.Scene("foo")
# export the library
#export_library_split(library_scene, folder_path)
#export_library_merged(library_scene, folder_path, gltf_export_preferences)
# export the main game world
# export_main(game_scene, folder_path, gltf_export_preferences)
export_game = getattr(addon_prefs,"export_game")
export_game_scene_name = getattr(addon_prefs,"export_game_scene_name")
export_game_output_name = getattr(addon_prefs,"export_game_output_name")
print("exporting ??", export_game, export_game_scene_name, export_game_output_name)
print("last changed", bpy.context.scene.changedScene)
# optimised variation
last_changed = bpy.context.scene.changedScene #get_changedScene()
if last_changed == export_game_scene_name:
# export the main game world
if export_game:
game_scene = bpy.data.scenes[export_game_scene_name]
print("game world changed, exporting game gltf only")
export_main(game_scene, folder_path, gltf_export_preferences, export_game_output_name)
if last_changed == "library": # if the library has changed, so will likely the game world that uses the library assets
print("library changed, exporting both game & library gltf")
# export the library
# export_library_merged(library_scene, folder_path, gltf_export_preferences)
# export the main game world
if export_game:
game_scene = bpy.data.scenes[export_game_scene_name]
export_main(game_scene, folder_path, gltf_export_preferences, export_game_output_name)
return {'FINISHED'}
@persistent
def save_handler(dummy):
print("-------------")
print("saved", bpy.data.filepath)
auto_export()
def get_changedScene(self):
return self["changedScene"]
def set_ChangedScene(self, value):
self["changedScene"] = value
class AutoExportGltfAddonPreferences(AddonPreferences):
# this must match the add-on name, use '__package__'
# when defining this in a submodule of a python package.
bl_idname = __name__
export_format: EnumProperty(
name='Format',
items=(('GLB', 'glTF Binary (.glb)',
'Exports a single file, with all data packed in binary form. '
'Most efficient and portable, but more difficult to edit later'),
('GLTF_EMBEDDED', 'glTF Embedded (.gltf)',
'Exports a single file, with all data packed in JSON. '
'Less efficient than binary, but easier to edit later'),
('GLTF_SEPARATE', 'glTF Separate (.gltf + .bin + textures)',
'Exports multiple files, with separate JSON, binary and texture data. '
'Easiest to edit later')),
description=(
'Output format and embedding options. Binary is most efficient, '
'but JSON (embedded or separate) may be easier to edit later'
),
default='GLB'
)
export_game: BoolProperty(
name='Export world/level',
description='Export world/level into a seperate gltf file',
default=False
)
export_game_scene_name: StringProperty(
name='Scene to auto export',
description='The name of the main scene/level/world to auto export',
default='world'
)
export_game_output_name: StringProperty(
name='Glb output name',
description='The glb output name for the main scene to auto export',
default='world'
)
export_copyright: StringProperty(
name='Copyright',
description='Legal rights and conditions for the model',
default=''
)
export_image_format: EnumProperty(
name='Images',
items=(('AUTO', 'Automatic',
'Save PNGs as PNGs and JPEGs as JPEGs. '
'If neither one, use PNG'),
('JPEG', 'JPEG Format (.jpg)',
'Save images as JPEGs. (Images that need alpha are saved as PNGs though.) '
'Be aware of a possible loss in quality'),
('NONE', 'None',
'Don\'t export images'),
),
description=(
'Output format for images. PNG is lossless and generally preferred, but JPEG might be preferable for web '
'applications due to the smaller file size. Alternatively they can be omitted if they are not needed'
),
default='AUTO'
)
export_texture_dir: StringProperty(
name='Textures',
description='Folder to place texture files in. Relative to the .gltf file',
default='',
)
"""
export_jpeg_quality: IntProperty(
name='JPEG quality',
description='Quality of JPEG export',
default=75,
min=0,
max=100
)
"""
export_keep_originals: BoolProperty(
name='Keep original',
description=('Keep original textures files if possible. '
'WARNING: if you use more than one texture, '
'where pbr standard requires only one, only one texture will be used. '
'This can lead to unexpected results'
),
default=False,
)
export_texcoords: BoolProperty(
name='UVs',
description='Export UVs (texture coordinates) with meshes',
default=True
)
export_normals: BoolProperty(
name='Normals',
description='Export vertex normals with meshes',
default=True
)
export_draco_mesh_compression_enable: BoolProperty(
name='Draco mesh compression',
description='Compress mesh using Draco',
default=False
)
export_draco_mesh_compression_level: IntProperty(
name='Compression level',
description='Compression level (0 = most speed, 6 = most compression, higher values currently not supported)',
default=6,
min=0,
max=10
)
export_draco_position_quantization: IntProperty(
name='Position quantization bits',
description='Quantization bits for position values (0 = no quantization)',
default=14,
min=0,
max=30
)
export_draco_normal_quantization: IntProperty(
name='Normal quantization bits',
description='Quantization bits for normal values (0 = no quantization)',
default=10,
min=0,
max=30
)
export_draco_texcoord_quantization: IntProperty(
name='Texcoord quantization bits',
description='Quantization bits for texture coordinate values (0 = no quantization)',
default=12,
min=0,
max=30
)
export_draco_color_quantization: IntProperty(
name='Color quantization bits',
description='Quantization bits for color values (0 = no quantization)',
default=10,
min=0,
max=30
)
export_draco_generic_quantization: IntProperty(
name='Generic quantization bits',
description='Quantization bits for generic coordinate values like weights or joints (0 = no quantization)',
default=12,
min=0,
max=30
)
export_tangents: BoolProperty(
name='Tangents',
description='Export vertex tangents with meshes',
default=False
)
export_materials: EnumProperty(
name='Materials',
items=(('EXPORT', 'Export',
'Export all materials used by included objects'),
('PLACEHOLDER', 'Placeholder',
'Do not export materials, but write multiple primitive groups per mesh, keeping material slot information'),
('NONE', 'No export',
'Do not export materials, and combine mesh primitive groups, losing material slot information')),
description='Export materials',
default='EXPORT'
)
export_original_specular: BoolProperty(
name='Export original PBR Specular',
description=(
'Export original glTF PBR Specular, instead of Blender Principled Shader Specular'
),
default=False,
)
export_colors: BoolProperty(
name='Vertex Colors',
description='Export vertex colors with meshes',
default=True
)
export_attributes: BoolProperty(
name='Attributes',
description='Export Attributes (when starting with underscore)',
default=False
)
use_mesh_edges: BoolProperty(
name='Loose Edges',
description=(
'Export loose edges as lines, using the material from the first material slot'
),
default=False,
)
use_mesh_vertices: BoolProperty(
name='Loose Points',
description=(
'Export loose points as glTF points, using the material from the first material slot'
),
default=False,
)
export_cameras: BoolProperty(
name='Cameras',
description='Export cameras',
default=True
)
use_selection: BoolProperty(
name='Selected Objects',
description='Export selected objects only',
default=False
)
use_visible: BoolProperty(
name='Visible Objects',
description='Export visible objects only',
default=True
)
use_renderable: BoolProperty(
name='Renderable Objects',
description='Export renderable objects only',
default=False
)
export_apply: BoolProperty(
name='Export Apply Modifiers',
description='Apply modifiers (excluding Armatures) to mesh objects -'
'WARNING: prevents exporting shape keys',
default=True
)
export_yup: BoolProperty(
name='+Y Up',
description='Export using glTF convention, +Y up',
default=False
)
use_visible: BoolProperty(
name='Visible Objects',
description='Export visible objects only',
default=False
)
use_renderable: BoolProperty(
name='Renderable Objects',
description='Export renderable objects only',
default=False
)
export_extras: BoolProperty(
name='Custom Properties',
description='Export custom properties as glTF extras',
default=True
)
export_animations: BoolProperty(
name='Animations',
description='Exports active actions and NLA tracks as glTF animations',
default=False
)
class TEST_AUTO_OT_gltf(Operator, ExportHelper):
"""test"""
bl_idname = "export_scenes.auto_gltf"
bl_label = "Apply settings"
bl_options = {'PRESET', 'UNDO'}
# ExportHelper mixin class uses this
filename_ext = ''
filter_glob: StringProperty(
default='*.glb;*.gltf',
options={'HIDDEN'}
)
# List of operator properties, the attributes will be assigned
# to the class instance from the operator setting before calling.
def draw(self, context):
layout = self.layout
preferences = context.preferences
addon_prefs = preferences.addons[__name__].preferences
# print("KEYS", dir(addon_prefs))
#print("BLAS", addon_prefs.__annotations__)
#print(addon_prefs.__dict__)
for key in addon_prefs.__annotations__.keys():
layout.prop(addon_prefs, key)
#print("key", key)
#def __init__(self):
# print("initializing my magic foo")
def execute(self, context):
preferences = context.preferences
#print("preferences", preferences.addons, __name__)
addon_prefs = preferences.addons[__name__].preferences
#print("addon prefs", addon_prefs)
#info = ("Path: %s, Number: %d, Boolean %r" %
# (addon_prefs.filepath, addon_prefs.number, addon_prefs.boolean))
#self.report({'INFO'}, info)
#print(info)
print("CHGANGED color", self)
return {'FINISHED'}
# Only needed if you want to add into a dynamic menu
def menu_func_import(self, context):
self.layout.operator(TEST_AUTO_OT_gltf.bl_idname, text="glTF auto Export (.glb/gltf)")
classes = [TEST_AUTO_OT_gltf, AutoExportGltfAddonPreferences]
def register():
for cls in classes:
bpy.utils.register_class(cls)
#bpy.types.Scene.my_tool = bpy.props.PointerProperty(type=My_Settings)
#CollectionProperty
bpy.types.TOPBAR_MT_file_export.append(menu_func_import)
bpy.app.handlers.depsgraph_update_post.append(deps_update_handler)
bpy.app.handlers.save_post.append(save_handler)
#bpy.types.TOPBAR_MT_file_export.append(menu_func_import)
bpy.types.Scene.changedScene = bpy.props.StringProperty(get=get_changedScene, set=set_ChangedScene)
def unregister():
for cls in classes:
bpy.utils.unregister_class(cls)
bpy.types.TOPBAR_MT_file_export.remove(menu_func_import)
bpy.app.handlers.depsgraph_update_post.remove(deps_update_handler)
bpy.app.handlers.save_post.remove(save_handler)
#bpy.types.TOPBAR_MT_file_export.remove(menu_func_import)
del bpy.types.Scene.changedScene
#del bpy.types.Scene.my_tool
if __name__ == "__main__":
register()