446 lines
17 KiB
Python
446 lines
17 KiB
Python
import bpy
|
|
from bpy.props import (StringProperty,
|
|
BoolProperty,
|
|
EnumProperty,
|
|
IntProperty,
|
|
FloatProperty,
|
|
CollectionProperty)
|
|
from bpy.types import Operator
|
|
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
|
from io_scene_gltf2 import ConvertGLTF2_Base
|
|
import importlib
|
|
|
|
# then import dependencies for our addon
|
|
if "Font" in locals():
|
|
importlib.reload(Font)
|
|
else:
|
|
from .common import Font
|
|
|
|
if "utils" in locals():
|
|
importlib.reload(utils)
|
|
else:
|
|
from .common import utils
|
|
|
|
|
|
# taken from blender_git/blender/scripts/addons/io_scene_gltf2/__init__.py
|
|
|
|
def get_font_faces_in_file(filepath):
|
|
from io_scene_gltf2.io.imp.gltf2_io_gltf import glTFImporter, ImportError
|
|
|
|
try:
|
|
import_settings = { 'import_user_extensions': [] }
|
|
gltf_importer = glTFImporter(filepath, import_settings)
|
|
gltf_importer.read()
|
|
gltf_importer.checks()
|
|
|
|
out = []
|
|
for node in gltf_importer.data.nodes:
|
|
if type(node.extras) != type(None) \
|
|
and "glyph" in node.extras \
|
|
and not ("type" in node.extras and node.extras["type"] == "metrics") \
|
|
and not (f"{utils.prefix()}_type" in node.extras and node.extras[f"{utils.prefix()}_type"] == "metrics"):
|
|
out.append(node.extras)
|
|
return out
|
|
|
|
except ImportError as e:
|
|
return None
|
|
|
|
# taken from blender_git/blender/scripts/addons/io_scene_gltf2/__init__.py
|
|
|
|
class GetFontFacesInFile(Operator, ImportHelper):
|
|
"""Load a glTF 2.0 font and check which faces are in there"""
|
|
bl_idname = f"abc3d.check_font_gltf"
|
|
bl_label = 'Check glTF 2.0 Font'
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
files: CollectionProperty(
|
|
name="File Path",
|
|
type=bpy.types.OperatorFileListElement,
|
|
)
|
|
|
|
# bpy.ops.abc3d.check_font_gltf(filepath="/home/jrkb/.config/blender/4.1/datafiles/abc3d/fonts/JRKB_LOL.glb")
|
|
found_fonts = []
|
|
|
|
def execute(self, context):
|
|
return self.check_gltf2(context)
|
|
|
|
def check_gltf2(self, context):
|
|
import os
|
|
import sys
|
|
|
|
if self.files:
|
|
# Multiple file check
|
|
ret = {'CANCELLED'}
|
|
dirname = os.path.dirname(self.filepath)
|
|
for file in self.files:
|
|
path = os.path.join(dirname, file.name)
|
|
if self.unit_check(path) == {'FINISHED'}:
|
|
ret = {'FINISHED'}
|
|
return ret
|
|
else:
|
|
# Single file check
|
|
return self.unit_check(self.filepath)
|
|
|
|
def unit_check(self, filename):
|
|
self.found_fonts.append(["LOL","WHATEVER"])
|
|
return {'FINISHED'}
|
|
|
|
class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper):
|
|
"""Load a glTF 2.0 font"""
|
|
bl_idname = f"abc3d.import_font_gltf"
|
|
bl_label = 'Import glTF 2.0 Font'
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
filter_glob: StringProperty(default="*.glb;*.gltf", options={'HIDDEN'})
|
|
|
|
files: CollectionProperty(
|
|
name="File Path",
|
|
type=bpy.types.OperatorFileListElement,
|
|
)
|
|
|
|
loglevel: IntProperty(
|
|
name='Log Level',
|
|
description="Log Level")
|
|
|
|
import_pack_images: BoolProperty(
|
|
name='Pack Images',
|
|
description='Pack all images into .blend file',
|
|
default=True
|
|
)
|
|
|
|
merge_vertices: BoolProperty(
|
|
name='Merge Vertices',
|
|
description=(
|
|
'The glTF format requires discontinuous normals, UVs, and '
|
|
'other vertex attributes to be stored as separate vertices, '
|
|
'as required for rendering on typical graphics hardware. '
|
|
'This option attempts to combine co-located vertices where possible. '
|
|
'Currently cannot combine verts with different normals'
|
|
),
|
|
default=False,
|
|
)
|
|
|
|
import_shading: EnumProperty(
|
|
name="Shading",
|
|
items=(("NORMALS", "Use Normal Data", ""),
|
|
("FLAT", "Flat Shading", ""),
|
|
("SMOOTH", "Smooth Shading", "")),
|
|
description="How normals are computed during import",
|
|
default="NORMALS")
|
|
|
|
bone_heuristic: EnumProperty(
|
|
name="Bone Dir",
|
|
items=(
|
|
("BLENDER", "Blender (best for import/export round trip)",
|
|
"Good for re-importing glTFs exported from Blender, "
|
|
"and re-exporting glTFs to glTFs after Blender editing. "
|
|
"Bone tips are placed on their local +Y axis (in glTF space)"),
|
|
("TEMPERANCE", "Temperance (average)",
|
|
"Decent all-around strategy. "
|
|
"A bone with one child has its tip placed on the local axis "
|
|
"closest to its child"),
|
|
("FORTUNE", "Fortune (may look better, less accurate)",
|
|
"Might look better than Temperance, but also might have errors. "
|
|
"A bone with one child has its tip placed at its child's root. "
|
|
"Non-uniform scalings may get messed up though, so beware"),
|
|
),
|
|
description="Heuristic for placing bones. Tries to make bones pretty",
|
|
default="BLENDER",
|
|
)
|
|
|
|
guess_original_bind_pose: BoolProperty(
|
|
name='Guess Original Bind Pose',
|
|
description=(
|
|
'Try to guess the original bind pose for skinned meshes from '
|
|
'the inverse bind matrices. '
|
|
'When off, use default/rest pose as bind pose'
|
|
),
|
|
default=True,
|
|
)
|
|
|
|
import_webp_texture: BoolProperty(
|
|
name='Import WebP textures',
|
|
description=(
|
|
"If a texture exists in WebP format, "
|
|
"loads the WebP texture instead of the fallback PNG/JPEG one"
|
|
),
|
|
default=False,
|
|
)
|
|
|
|
glyphs: StringProperty(
|
|
name='Import only these glyphs',
|
|
description=(
|
|
"Loading glyphs is expensive, if the meshes are huge"
|
|
"So we can filter all glyphs out that we do not want"
|
|
),
|
|
default="A",
|
|
)
|
|
|
|
marker_property: StringProperty(
|
|
name="Mark imported objects with this custom property.",
|
|
default="font_import",
|
|
)
|
|
|
|
font_name: StringProperty(
|
|
name="If defined, only import this font",
|
|
default="",
|
|
)
|
|
|
|
face_name: StringProperty(
|
|
name="If defined, only import this font face",
|
|
default="",
|
|
)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
layout.use_property_split = True
|
|
layout.use_property_decorate = False # No animation.
|
|
|
|
layout.prop(self, 'import_pack_images')
|
|
layout.prop(self, 'merge_vertices')
|
|
layout.prop(self, 'import_shading')
|
|
layout.prop(self, 'guess_original_bind_pose')
|
|
layout.prop(self, 'bone_heuristic')
|
|
layout.prop(self, 'export_import_convert_lighting_mode')
|
|
layout.prop(self, 'import_webp_texture')
|
|
|
|
def invoke(self, context, event):
|
|
import sys
|
|
preferences = bpy.context.preferences
|
|
for addon_name in preferences.addons.keys():
|
|
try:
|
|
if hasattr(sys.modules[addon_name], 'glTF2ImportUserExtension') or hasattr(sys.modules[addon_name], 'glTF2ImportUserExtensions'):
|
|
importer_extension_panel_unregister_functors.append(sys.modules[addon_name].register_panel())
|
|
except Exception:
|
|
pass
|
|
|
|
self.has_active_importer_extensions = len(importer_extension_panel_unregister_functors) > 0
|
|
return ImportHelper.invoke(self, context, event)
|
|
|
|
def execute(self, context):
|
|
return self.import_gltf2(context)
|
|
|
|
def import_gltf2(self, context):
|
|
import os
|
|
|
|
self.set_debug_log()
|
|
import_settings = self.as_keywords()
|
|
|
|
user_extensions = []
|
|
|
|
import sys
|
|
preferences = bpy.context.preferences
|
|
for addon_name in preferences.addons.keys():
|
|
try:
|
|
module = sys.modules[addon_name]
|
|
except Exception:
|
|
continue
|
|
if hasattr(module, 'glTF2ImportUserExtension'):
|
|
extension_ctor = module.glTF2ImportUserExtension
|
|
user_extensions.append(extension_ctor())
|
|
import_settings['import_user_extensions'] = user_extensions
|
|
|
|
if self.files:
|
|
# Multiple file import
|
|
ret = {'CANCELLED'}
|
|
dirname = os.path.dirname(self.filepath)
|
|
for file in self.files:
|
|
path = os.path.join(dirname, file.name)
|
|
if self.unit_import(path, import_settings) == {'FINISHED'}:
|
|
ret = {'FINISHED'}
|
|
return ret
|
|
else:
|
|
# Single file import
|
|
return self.unit_import(self.filepath, import_settings)
|
|
|
|
def unit_import(self, filename, import_settings):
|
|
import time
|
|
from io_scene_gltf2.io.imp.gltf2_io_gltf import glTFImporter, ImportError
|
|
from io_scene_gltf2.blender.imp.gltf2_blender_gltf import BlenderGlTF
|
|
from io_scene_gltf2.blender.imp.gltf2_blender_vnode import VNode, compute_vnodes
|
|
from io_scene_gltf2.blender.com.gltf2_blender_extras import set_extras
|
|
from io_scene_gltf2.blender.imp.gltf2_blender_node import BlenderNode
|
|
|
|
try:
|
|
gltf = glTFImporter(filename, import_settings)
|
|
gltf.read()
|
|
gltf.checks()
|
|
|
|
# start filtering glyphs like this:
|
|
# - collect indices of nodes that contain our glyphs
|
|
# - collect indices of their meshes
|
|
# - collect the node's parent tree
|
|
# - use these indices to create new lists of nodes and meshes
|
|
# - update the scene tree to contain only our nodes
|
|
# - replace the node and mesh list with ours
|
|
|
|
# indices of meshes to keep
|
|
mesh_indices = []
|
|
# indices of nodes to keep
|
|
node_indices = []
|
|
|
|
# convenience function to add a node to the indices
|
|
def add_node(node, recursive=True):
|
|
node_index = gltf.data.nodes.index(node)
|
|
if node_index not in node_indices:
|
|
node_indices.append(node_index)
|
|
if type(node.mesh) != type(None) and node.mesh >= 0:
|
|
mesh_index = node.mesh
|
|
if mesh_index not in mesh_indices:
|
|
mesh_indices.append(mesh_index)
|
|
if recursive and type(node.children) != type(None):
|
|
for c in node.children:
|
|
child = gltf.data.nodes[c]
|
|
add_node(child)
|
|
|
|
# convenience function to add a mesh to the indices
|
|
def add_parent_node(node, recursive=True):
|
|
index = gltf.data.nodes.index(node)
|
|
for parent in gltf.data.nodes:
|
|
if type(parent.children) != type(None) and index in parent.children:
|
|
add_node(parent, False)
|
|
if recursive:
|
|
add_parent_node(parent)
|
|
|
|
# populate our node_indices and mesh_indices
|
|
# by iterating through the nodes and check if they are
|
|
# indeed representing a glyph we want
|
|
for node in gltf.data.nodes:
|
|
# :-O woah
|
|
if type(node.extras) != type(None) \
|
|
and "glyph" in node.extras \
|
|
and (node.extras["glyph"] in self.glyphs \
|
|
or len(self.glyphs) == 0) \
|
|
and (self.font_name == "" or \
|
|
( "font_name" in node.extras \
|
|
and (node.extras["font_name"] in self.font_name \
|
|
or len(self.glyphs) == 0))) \
|
|
and (self.face_name == "" or \
|
|
( "face_name" in node.extras \
|
|
and (node.extras["face_name"] in self.face_name \
|
|
or len(self.glyphs) == 0))):
|
|
# if there is a match, add the node incl children ..
|
|
add_node(node)
|
|
# .. and their parents recursively
|
|
add_parent_node(node)
|
|
|
|
# in the end we need the objects, not the indices
|
|
# so let's prepare empy lists
|
|
meshes = []
|
|
nodes = []
|
|
|
|
# the indices will be off, as we have fewer elements
|
|
# so let's have a lookup table
|
|
mesh_index_table = {}
|
|
node_index_table = {}
|
|
|
|
# first, add all meshes and fill in lookup table
|
|
for mesh_index, mesh in enumerate(gltf.data.meshes):
|
|
if mesh_index in mesh_indices:
|
|
meshes.append(mesh)
|
|
mesh_index_table[mesh_index] = len(meshes) - 1
|
|
|
|
# second, add all nodes and fill in lookup table
|
|
# nodes also refer to their meshes
|
|
for node_index, node in enumerate(gltf.data.nodes):
|
|
if node_index in node_indices:
|
|
if type(node.mesh) != type(None):
|
|
node.mesh = mesh_index_table[node.mesh]
|
|
nodes.append(node)
|
|
node_index_table[node_index] = len(nodes) - 1
|
|
|
|
# the indices to children are messed up.
|
|
# some children are lost :(
|
|
# and some have different indices
|
|
for node in nodes:
|
|
if type(node.children) != type(None):
|
|
children = [] # brand new children
|
|
for i, c in enumerate(node.children):
|
|
# check if children are lost
|
|
if c in node_indices:
|
|
children.append(node_index_table[c])
|
|
# now replace old children with the new, however
|
|
# if we don't have children, we don't even need a list!
|
|
node.children = None if len(children) == 0 else children
|
|
|
|
# last step, kick nodes out of the scene tree if they're lost
|
|
for s in gltf.data.scenes:
|
|
scene_nodes = []
|
|
for n in s.nodes:
|
|
if n in node_indices:
|
|
scene_nodes.append(node_index_table[n])
|
|
s.nodes = scene_nodes
|
|
|
|
# very last step, replace nodes and meshes
|
|
gltf.data.nodes = nodes
|
|
gltf.data.meshes = meshes
|
|
|
|
# that's fucking it, we're done!
|
|
# hand over back to default blender behaviour :-)
|
|
# or.. not! blender will do some funny scene stuff
|
|
# which we don't want.
|
|
# so let's do it quick
|
|
|
|
print("Data are loaded, start creating Blender stuff")
|
|
|
|
start_time = time.time()
|
|
# first, convert gltf to blender
|
|
BlenderGlTF.set_convert_functions(gltf)
|
|
# compute things
|
|
BlenderGlTF.pre_compute(gltf)
|
|
compute_vnodes(gltf)
|
|
|
|
# apparently we need a scene, because
|
|
# when creating the objects, it will link the objects here
|
|
gltf.blender_scene = bpy.context.scene.name
|
|
|
|
def create_blender_object(gltf, vi, nodes):
|
|
vnode = gltf.vnodes[vi]
|
|
if vnode.type == VNode.Object:
|
|
if vnode.parent is not None:
|
|
if not hasattr(gltf.vnodes[vnode.parent],
|
|
"blender_object"):
|
|
create_blender_object(gltf,
|
|
vnode.parent,
|
|
nodes)
|
|
if not hasattr(vnode,
|
|
"blender_object"):
|
|
obj = BlenderNode.create_object(gltf, vi)
|
|
obj["font_import"] = True
|
|
n_vars = vars(nodes[vi])
|
|
if "extras" in n_vars:
|
|
set_extras(obj, n_vars["extras"])
|
|
if "glyph" in n_vars["extras"] and \
|
|
not ("type" in n_vars["extras"] and \
|
|
n_vars["extras"]["type"] == "metrics") and \
|
|
not (f"{utils.prefix()}_type" in n_vars["extras"] and \
|
|
n_vars["extras"][f"{utils.prefix()}_type"] == "metrics"):
|
|
obj["type"] = "glyph"
|
|
|
|
for vi, vnode in gltf.vnodes.items():
|
|
create_blender_object(gltf, vi, nodes)
|
|
|
|
elapsed_s = "{:.2f}s".format(time.time() - start_time)
|
|
print("font import gltf finished in " + elapsed_s)
|
|
|
|
gltf.log.removeHandler(gltf.log_handler)
|
|
|
|
return {'FINISHED'}
|
|
|
|
except ImportError as e:
|
|
self.report({'ERROR'}, e.args[0])
|
|
return {'CANCELLED'}
|
|
|
|
def set_debug_log(self):
|
|
import logging
|
|
if bpy.app.debug_value == 0:
|
|
self.loglevel = logging.CRITICAL
|
|
elif bpy.app.debug_value == 1:
|
|
self.loglevel = logging.ERROR
|
|
elif bpy.app.debug_value == 2:
|
|
self.loglevel = logging.WARNING
|
|
elif bpy.app.debug_value == 3:
|
|
self.loglevel = logging.INFO
|
|
else:
|
|
self.loglevel = logging.NOTSET
|