FBX IO: Export normals with IndexToDirect reference mode #105020

Merged
Thomas Barlow merged 3 commits from Mysteryem/blender-addons:fbx_normals_IndexToDirect into main 2023-12-02 02:08:21 +01:00
11 changed files with 395 additions and 75 deletions
Showing only changes of commit bc5132040f - Show all commits

View File

@ -139,7 +139,7 @@ class MAX3DS_PT_import_include(bpy.types.Panel):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
layout.use_property_split = True layout.use_property_split = True
layout.use_property_decorate = True layout.use_property_decorate = False
sfile = context.space_data sfile = context.space_data
operator = sfile.active_operator operator = sfile.active_operator

View File

@ -5,7 +5,7 @@
bl_info = { bl_info = {
"name": "FBX format", "name": "FBX format",
"author": "Campbell Barton, Bastien Montagne, Jens Restemeier, @Mysteryem", "author": "Campbell Barton, Bastien Montagne, Jens Restemeier, @Mysteryem",
"version": (5, 10, 1), "version": (5, 10, 3),
"blender": (4, 1, 0), "blender": (4, 1, 0),
"location": "File > Import-Export", "location": "File > Import-Export",
"description": "FBX IO meshes, UVs, vertex colors, materials, textures, cameras, lamps and actions", "description": "FBX IO meshes, UVs, vertex colors, materials, textures, cameras, lamps and actions",

View File

@ -23,6 +23,7 @@ from . import encode_bin, data_types
# "Constants" # "Constants"
FBX_VERSION = 7400 FBX_VERSION = 7400
# 1004 adds use of "OtherFlags"->"TCDefinition" to control the FBX_KTIME opt-in in FBX version 7700.
FBX_HEADER_VERSION = 1003 FBX_HEADER_VERSION = 1003
FBX_SCENEINFO_VERSION = 100 FBX_SCENEINFO_VERSION = 100
FBX_TEMPLATES_VERSION = 100 FBX_TEMPLATES_VERSION = 100
@ -54,7 +55,19 @@ FBX_ANIM_KEY_VERSION = 4008
FBX_NAME_CLASS_SEP = b"\x00\x01" FBX_NAME_CLASS_SEP = b"\x00\x01"
FBX_ANIM_PROPSGROUP_NAME = "d" FBX_ANIM_PROPSGROUP_NAME = "d"
FBX_KTIME = 46186158000 # This is the number of "ktimes" in one second (yep, precision over the nanosecond...) FBX_KTIME_V7 = 46186158000 # This is the number of "ktimes" in one second (yep, precision over the nanosecond...)
# FBX 2019.5 (FBX version 7700) changed the number of "ktimes" per second, however, the new value is opt-in until FBX
# version 8000 where it will probably become opt-out.
FBX_KTIME_V8 = 141120000
# To explicitly use the V7 value in FBX versions 7700-7XXX: fbx_root->"FBXHeaderExtension"->"OtherFlags"->"TCDefinition"
# is set to 127.
# To opt in to the V8 value in FBX version 7700-7XXX: "TCDefinition" is set to 0.
FBX_TIMECODE_DEFINITION_TO_KTIME_PER_SECOND = {
0: FBX_KTIME_V8,
127: FBX_KTIME_V7,
}
# The "ktimes" per second for Blender exported FBX is constant because the exported `FBX_VERSION` is constant.
FBX_KTIME = FBX_KTIME_V8 if FBX_VERSION >= 8000 else FBX_KTIME_V7
MAT_CONVERT_LIGHT = Matrix.Rotation(math.pi / 2.0, 4, 'X') # Blender is -Z, FBX is -Y. MAT_CONVERT_LIGHT = Matrix.Rotation(math.pi / 2.0, 4, 'X') # Blender is -Z, FBX is -Y.
@ -216,7 +229,7 @@ UNITS = {
"degree": 360.0, "degree": 360.0,
"radian": math.pi * 2.0, "radian": math.pi * 2.0,
"second": 1.0, # Ref unit! "second": 1.0, # Ref unit!
"ktime": FBX_KTIME, "ktime": FBX_KTIME, # For export use only because the imported "ktimes" per second may vary.
} }

View File

@ -48,6 +48,9 @@ from .fbx_utils import (
MESH_ATTRIBUTE_SHARP_FACE, MESH_ATTRIBUTE_SHARP_FACE,
MESH_ATTRIBUTE_SHARP_EDGE, MESH_ATTRIBUTE_SHARP_EDGE,
expand_shape_key_range, expand_shape_key_range,
FBX_KTIME_V7,
FBX_KTIME_V8,
FBX_TIMECODE_DEFINITION_TO_KTIME_PER_SECOND,
) )
LINEAR_INTERPOLATION_VALUE = bpy.types.Keyframe.bl_rna.properties['interpolation'].enum_items['LINEAR'].value LINEAR_INTERPOLATION_VALUE = bpy.types.Keyframe.bl_rna.properties['interpolation'].enum_items['LINEAR'].value
@ -803,9 +806,8 @@ def blen_read_invalid_animation_curve(key_times, key_values):
return key_times, key_values return key_times, key_values
def _convert_fbx_time_to_blender_time(key_times, blen_start_offset, fbx_start_offset, fps): def _convert_fbx_time_to_blender_time(key_times, blen_start_offset, fbx_start_offset, fps, fbx_ktime):
from .fbx_utils import FBX_KTIME timefac = fps / fbx_ktime
timefac = fps / FBX_KTIME
# Convert from FBX timing to Blender timing. # Convert from FBX timing to Blender timing.
# Cannot subtract in-place because key_times could be read directly from FBX and could be used by multiple Actions. # Cannot subtract in-place because key_times could be read directly from FBX and could be used by multiple Actions.
@ -838,19 +840,21 @@ def blen_read_animation_curve(fbx_curve):
return blen_read_invalid_animation_curve(key_times, key_values) return blen_read_invalid_animation_curve(key_times, key_values)
def blen_store_keyframes(fbx_key_times, blen_fcurve, key_values, blen_start_offset, fps, fbx_start_offset=0): def blen_store_keyframes(fbx_key_times, blen_fcurve, key_values, blen_start_offset, fps, fbx_ktime, fbx_start_offset=0):
"""Set all keyframe times and values for a newly created FCurve. """Set all keyframe times and values for a newly created FCurve.
Linear interpolation is currently assumed. Linear interpolation is currently assumed.
This is a convenience function for calling blen_store_keyframes_multi with only a single fcurve and values array.""" This is a convenience function for calling blen_store_keyframes_multi with only a single fcurve and values array."""
blen_store_keyframes_multi(fbx_key_times, [(blen_fcurve, key_values)], blen_start_offset, fps, fbx_start_offset) blen_store_keyframes_multi(fbx_key_times, [(blen_fcurve, key_values)], blen_start_offset, fps, fbx_ktime,
fbx_start_offset)
def blen_store_keyframes_multi(fbx_key_times, fcurve_and_key_values_pairs, blen_start_offset, fps, fbx_start_offset=0): def blen_store_keyframes_multi(fbx_key_times, fcurve_and_key_values_pairs, blen_start_offset, fps, fbx_ktime,
fbx_start_offset=0):
"""Set all keyframe times and values for multiple pairs of newly created FCurves and keyframe values arrays, where """Set all keyframe times and values for multiple pairs of newly created FCurves and keyframe values arrays, where
each pair has the same keyframe times. each pair has the same keyframe times.
Linear interpolation is currently assumed.""" Linear interpolation is currently assumed."""
bl_key_times = _convert_fbx_time_to_blender_time(fbx_key_times, blen_start_offset, fbx_start_offset, fps) bl_key_times = _convert_fbx_time_to_blender_time(fbx_key_times, blen_start_offset, fbx_start_offset, fps, fbx_ktime)
num_keys = len(bl_key_times) num_keys = len(bl_key_times)
# Compatible with C float type # Compatible with C float type
@ -883,7 +887,8 @@ def blen_store_keyframes_multi(fbx_key_times, fcurve_and_key_values_pairs, blen_
blen_fcurve.update() blen_fcurve.update()
def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, global_scale, shape_key_deforms): def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, global_scale, shape_key_deforms,
fbx_ktime):
""" """
'Bake' loc/rot/scale into the action, 'Bake' loc/rot/scale into the action,
taking any pre_ and post_ matrix into account to transform from fbx into blender space. taking any pre_ and post_ matrix into account to transform from fbx into blender space.
@ -947,10 +952,9 @@ def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, glo
assert(channel in {0, 1, 2}) assert(channel in {0, 1, 2})
blen_curve = blen_curves[channel] blen_curve = blen_curves[channel]
fbx_key_times, values = blen_read_animation_curve(curve) fbx_key_times, values = blen_read_animation_curve(curve)
blen_store_keyframes(fbx_key_times, blen_curve, values, anim_offset, fps) blen_store_keyframes(fbx_key_times, blen_curve, values, anim_offset, fps, fbx_ktime)
elif isinstance(item, ShapeKey): elif isinstance(item, ShapeKey):
deform_values = shape_key_deforms.setdefault(item, [])
for fbxprop, channel_to_curve in fbx_curves.items(): for fbxprop, channel_to_curve in fbx_curves.items():
assert(fbxprop == b'DeformPercent') assert(fbxprop == b'DeformPercent')
for channel, curve in channel_to_curve.items(): for channel, curve in channel_to_curve.items():
@ -960,12 +964,14 @@ def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, glo
fbx_key_times, values = blen_read_animation_curve(curve) fbx_key_times, values = blen_read_animation_curve(curve)
# A fully activated shape key in FBX DeformPercent is 100.0 whereas it is 1.0 in Blender. # A fully activated shape key in FBX DeformPercent is 100.0 whereas it is 1.0 in Blender.
values = values / 100.0 values = values / 100.0
blen_store_keyframes(fbx_key_times, blen_curve, values, anim_offset, fps) blen_store_keyframes(fbx_key_times, blen_curve, values, anim_offset, fps, fbx_ktime)
# Store the minimum and maximum shape key values, so that the shape key's slider range can be expanded # Store the minimum and maximum shape key values, so that the shape key's slider range can be expanded
# if necessary after reading all animations. # if necessary after reading all animations.
deform_values.append(values.min()) if values.size:
deform_values.append(values.max()) deform_values = shape_key_deforms.setdefault(item, [])
deform_values.append(values.min())
deform_values.append(values.max())
elif isinstance(item, Camera): elif isinstance(item, Camera):
for fbxprop, channel_to_curve in fbx_curves.items(): for fbxprop, channel_to_curve in fbx_curves.items():
@ -981,7 +987,7 @@ def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, glo
# Remap the imported values from FBX to Blender. # Remap the imported values from FBX to Blender.
values = values / 1000.0 values = values / 1000.0
values *= global_scale values *= global_scale
blen_store_keyframes(fbx_key_times, blen_curve, values, anim_offset, fps) blen_store_keyframes(fbx_key_times, blen_curve, values, anim_offset, fps, fbx_ktime)
else: # Object or PoseBone: else: # Object or PoseBone:
transform_data = item.fbx_transform_data transform_data = item.fbx_transform_data
@ -1042,10 +1048,10 @@ def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, glo
# Each channel has the same keyframe times, so the combined times can be passed once along with all the curves # Each channel has the same keyframe times, so the combined times can be passed once along with all the curves
# and values arrays. # and values arrays.
blen_store_keyframes_multi(combined_fbx_times, zip(blen_curves, channel_values), anim_offset, fps) blen_store_keyframes_multi(combined_fbx_times, zip(blen_curves, channel_values), anim_offset, fps, fbx_ktime)
def blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene, anim_offset, global_scale): def blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene, anim_offset, global_scale, fbx_ktime):
""" """
Recreate an action per stack/layer/object combinations. Recreate an action per stack/layer/object combinations.
Only the first found action is linked to objects, more complex setups are not handled, Only the first found action is linked to objects, more complex setups are not handled,
@ -1092,7 +1098,7 @@ def blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene, anim_o
id_data.animation_data.action = action id_data.animation_data.action = action
# And actually populate the action! # And actually populate the action!
blen_read_animations_action_item(action, item, cnodes, scene.render.fps, anim_offset, global_scale, blen_read_animations_action_item(action, item, cnodes, scene.render.fps, anim_offset, global_scale,
shape_key_values) shape_key_values, fbx_ktime)
# If the minimum/maximum animated value is outside the slider range of the shape key, attempt to expand the slider # If the minimum/maximum animated value is outside the slider range of the shape key, attempt to expand the slider
# range until the animated range fits and has extra room to be decreased or increased further. # range until the animated range fits and has extra room to be decreased or increased further.
@ -3629,6 +3635,21 @@ def load(operator, context, filepath="",
# Animation! # Animation!
def _(): def _():
# Find the number of "ktimes" per second for this file.
# Start with the default for this FBX version.
fbx_ktime = FBX_KTIME_V8 if version >= 8000 else FBX_KTIME_V7
# Try to find the value of the nested elem_root->'FBXHeaderExtension'->'OtherFlags'->'TCDefinition' element
# and look up the "ktimes" per second for its value.
if header := elem_find_first(elem_root, b'FBXHeaderExtension'):
# The header version that added TCDefinition support is 1004.
if elem_prop_first(elem_find_first(header, b'FBXHeaderVersion'), default=0) >= 1004:
if other_flags := elem_find_first(header, b'OtherFlags'):
if timecode_definition := elem_find_first(other_flags, b'TCDefinition'):
timecode_definition_value = elem_prop_first(timecode_definition)
# If its value is unknown or missing, default to FBX_KTIME_V8.
fbx_ktime = FBX_TIMECODE_DEFINITION_TO_KTIME_PER_SECOND.get(timecode_definition_value,
FBX_KTIME_V8)
fbx_tmpl_astack = fbx_template_get((b'AnimationStack', b'FbxAnimStack')) fbx_tmpl_astack = fbx_template_get((b'AnimationStack', b'FbxAnimStack'))
fbx_tmpl_alayer = fbx_template_get((b'AnimationLayer', b'FbxAnimLayer')) fbx_tmpl_alayer = fbx_template_get((b'AnimationLayer', b'FbxAnimLayer'))
stacks = {} stacks = {}
@ -3742,7 +3763,8 @@ def load(operator, context, filepath="",
curvenodes[acn_uuid][ac_uuid] = (fbx_acitem, channel) curvenodes[acn_uuid][ac_uuid] = (fbx_acitem, channel)
# And now that we have sorted all this, apply animations! # And now that we have sorted all this, apply animations!
blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene, settings.anim_offset, global_scale) blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene, settings.anim_offset, global_scale,
fbx_ktime)
_(); del _ _(); del _

View File

@ -5,7 +5,7 @@
bl_info = { bl_info = {
'name': 'glTF 2.0 format', 'name': 'glTF 2.0 format',
'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', 'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors',
"version": (4, 1, 34), "version": (4, 1, 36),
'blender': (4, 1, 0), 'blender': (4, 1, 0),
'location': 'File > Import-Export', 'location': 'File > Import-Export',
'description': 'Import-Export as glTF 2.0', 'description': 'Import-Export as glTF 2.0',
@ -48,6 +48,7 @@ from bpy.props import (StringProperty,
BoolProperty, BoolProperty,
EnumProperty, EnumProperty,
IntProperty, IntProperty,
FloatProperty,
CollectionProperty) CollectionProperty)
from bpy.types import Operator from bpy.types import Operator
from bpy_extras.io_utils import ImportHelper, ExportHelper from bpy_extras.io_utils import ImportHelper, ExportHelper
@ -155,6 +156,102 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
default='' default=''
) )
# gltfpack properties
export_use_gltfpack: BoolProperty(
name='Use Gltfpack',
description='Use gltfpack to simplify the mesh and/or compress its textures',
default=False,
)
export_gltfpack_tc: BoolProperty(
name='KTX2 Compression',
description='Convert all textures to KTX2 with BasisU supercompression',
default=True,
)
export_gltfpack_tq: IntProperty(
name='Texture Encoding Quality',
description='Texture encoding quality',
default=8,
min=1,
max=10,
)
export_gltfpack_si: FloatProperty(
name='Mesh Simplification Ratio',
description='Simplify meshes targeting triangle count ratio',
default=1.0,
min=0.0,
max=1.0,
)
export_gltfpack_sa: BoolProperty(
name='Aggressive Mesh Simplification',
description='Aggressively simplify to the target ratio disregarding quality',
default=False,
)
export_gltfpack_slb: BoolProperty(
name='Lock Mesh Border Vertices',
description='Lock border vertices during simplification to avoid gaps on connected meshes',
default=False,
)
export_gltfpack_vp: IntProperty(
name='Position Quantization',
description='Use N-bit quantization for positions',
default=14,
min=1,
max=16,
)
export_gltfpack_vt: IntProperty(
name='Texture Coordinate Quantization',
description='Use N-bit quantization for texture coordinates',
default=12,
min=1,
max=16,
)
export_gltfpack_vn: IntProperty(
name='Normal/Tangent Quantization',
description='Use N-bit quantization for normals and tangents',
default=8,
min=1,
max=16,
)
export_gltfpack_vc: IntProperty(
name='Vertex Color Quantization',
description='Use N-bit quantization for colors',
default=8,
min=1,
max=16,
)
export_gltfpack_vpi: EnumProperty(
name='Vertex Position Attributes',
description='Type to use for vertex position attributes',
items=(('Integer', 'Integer', 'Use integer attributes for positions'),
('Normalized', 'Normalized', 'Use normalized attributes for positions'),
('Floating-point', 'Floating-point', 'Use floating-point attributes for positions')),
default='Integer',
)
export_gltfpack_noq: BoolProperty(
name='Disable Quantization',
description='Disable quantization; produces much larger glTF files with no extensions',
default=True,
)
# TODO: some stuff in Textures
# TODO: Animations
# TODO: Scene
# TODO: some stuff in Miscellaneous
export_format: EnumProperty( export_format: EnumProperty(
name='Format', name='Format',
items=(('GLB', 'glTF Binary (.glb)', items=(('GLB', 'glTF Binary (.glb)',
@ -965,6 +1062,25 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
export_settings['gltf_hierarchy_full_collections'] = self.export_hierarchy_full_collections export_settings['gltf_hierarchy_full_collections'] = self.export_hierarchy_full_collections
# gltfpack stuff
export_settings['gltf_use_gltfpack'] = self.export_use_gltfpack
if self.export_use_gltfpack:
export_settings['gltf_gltfpack_tc'] = self.export_gltfpack_tc
export_settings['gltf_gltfpack_tq'] = self.export_gltfpack_tq
export_settings['gltf_gltfpack_si'] = self.export_gltfpack_si
export_settings['gltf_gltfpack_sa'] = self.export_gltfpack_sa
export_settings['gltf_gltfpack_slb'] = self.export_gltfpack_slb
export_settings['gltf_gltfpack_vp'] = self.export_gltfpack_vp
export_settings['gltf_gltfpack_vt'] = self.export_gltfpack_vt
export_settings['gltf_gltfpack_vn'] = self.export_gltfpack_vn
export_settings['gltf_gltfpack_vc'] = self.export_gltfpack_vc
export_settings['gltf_gltfpack_vpi'] = self.export_gltfpack_vpi
export_settings['gltf_gltfpack_noq'] = self.export_gltfpack_noq
export_settings['gltf_binary'] = bytearray() export_settings['gltf_binary'] = bytearray()
export_settings['gltf_binaryfilename'] = ( export_settings['gltf_binaryfilename'] = (
path_to_uri(os.path.splitext(os.path.basename(self.filepath))[0] + '.bin') path_to_uri(os.path.splitext(os.path.basename(self.filepath))[0] + '.bin')
@ -1034,6 +1150,56 @@ class GLTF_PT_export_main(bpy.types.Panel):
layout.prop(operator, 'will_save_settings') layout.prop(operator, 'will_save_settings')
class GLTF_PT_export_gltfpack(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'
bl_label = "gltfpack"
bl_parent_id = "FILE_PT_operator"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
gltfpack_path = context.preferences.addons['io_scene_gltf2'].preferences.gltfpack_path_ui.strip()
if (gltfpack_path == ''): # gltfpack not setup in plugin preferences -> dont show any gltfpack relevant options in export dialog
return False;
sfile = context.space_data
operator = sfile.active_operator
return operator.bl_idname == "EXPORT_SCENE_OT_gltf"
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
sfile = context.space_data
operator = sfile.active_operator
col = layout.column(heading = "gltfpack", align = True)
col.prop(operator, 'export_use_gltfpack')
col = layout.column(heading = "Textures", align = True)
col.prop(operator, 'export_gltfpack_tc')
col.prop(operator, 'export_gltfpack_tq')
col = layout.column(heading = "Simplification", align = True)
col.prop(operator, 'export_gltfpack_si')
col.prop(operator, 'export_gltfpack_sa')
col.prop(operator, 'export_gltfpack_slb')
col = layout.column(heading = "Vertices", align = True)
col.prop(operator, 'export_gltfpack_vp')
col.prop(operator, 'export_gltfpack_vt')
col.prop(operator, 'export_gltfpack_vn')
col.prop(operator, 'export_gltfpack_vc')
col = layout.column(heading = "Vertex positions", align = True)
col.prop(operator, 'export_gltfpack_vpi')
#col = layout.column(heading = "Animations", align = True)
#col = layout.column(heading = "Scene", align = True)
col = layout.column(heading = "Miscellaneous", align = True)
col.prop(operator, 'export_gltfpack_noq')
class GLTF_PT_export_include(bpy.types.Panel): class GLTF_PT_export_include(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER' bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS' bl_region_type = 'TOOL_PROPS'
@ -1923,15 +2089,15 @@ class GLTF_AddonPreferences(bpy.types.AddonPreferences):
bl_idname = __package__ bl_idname = __package__
settings_node_ui : bpy.props.BoolProperty( settings_node_ui : bpy.props.BoolProperty(
default= False, default= False,
description="Displays glTF Material Output node in Shader Editor (Menu Add > Output)" description="Displays glTF Material Output node in Shader Editor (Menu Add > Output)"
) )
KHR_materials_variants_ui : bpy.props.BoolProperty( KHR_materials_variants_ui : bpy.props.BoolProperty(
default= False, default= False,
description="Displays glTF UI to manage material variants", description="Displays glTF UI to manage material variants",
update=gltf_variant_ui_update update=gltf_variant_ui_update
) )
animation_ui: bpy.props.BoolProperty( animation_ui: bpy.props.BoolProperty(
default=False, default=False,
@ -1939,12 +2105,21 @@ class GLTF_AddonPreferences(bpy.types.AddonPreferences):
update=gltf_animation_ui_update update=gltf_animation_ui_update
) )
gltfpack_path_ui: bpy.props.StringProperty(
default="",
name="glTFpack file path",
description="Path to gltfpack binary",
subtype='FILE_PATH'
)
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
row = layout.row() row = layout.row()
row.prop(self, "settings_node_ui", text="Shader Editor Add-ons") row.prop(self, "settings_node_ui", text="Shader Editor Add-ons")
row.prop(self, "KHR_materials_variants_ui", text="Material Variants") row.prop(self, "KHR_materials_variants_ui", text="Material Variants")
row.prop(self, "animation_ui", text="Animation UI") row.prop(self, "animation_ui", text="Animation UI")
row = layout.row()
row.prop(self, "gltfpack_path_ui", text="Path to gltfpack")
def menu_func_import(self, context): def menu_func_import(self, context):
self.layout.operator(ImportGLTF2.bl_idname, text='glTF 2.0 (.glb/.gltf)') self.layout.operator(ImportGLTF2.bl_idname, text='glTF 2.0 (.glb/.gltf)')
@ -1973,6 +2148,7 @@ classes = (
GLTF_PT_export_animation_shapekeys, GLTF_PT_export_animation_shapekeys,
GLTF_PT_export_animation_sampling, GLTF_PT_export_animation_sampling,
GLTF_PT_export_animation_optimize, GLTF_PT_export_animation_optimize,
GLTF_PT_export_gltfpack,
GLTF_PT_export_user_extensions, GLTF_PT_export_user_extensions,
ImportGLTF2, ImportGLTF2,
GLTF_PT_import_user_extensions, GLTF_PT_import_user_extensions,
@ -1983,6 +2159,7 @@ classes = (
def register(): def register():
from .blender.com import gltf2_blender_ui as blender_ui from .blender.com import gltf2_blender_ui as blender_ui
for c in classes: for c in classes:
bpy.utils.register_class(c) bpy.utils.register_class(c)
# bpy.utils.register_module(__name__) # bpy.utils.register_module(__name__)

View File

@ -2,6 +2,8 @@
# #
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
import os
import subprocess
import time import time
import bpy import bpy
@ -109,6 +111,70 @@ def __create_buffer(exporter, export_settings):
return buffer return buffer
def __postprocess_with_gltfpack(export_settings):
gltfpack_binary_file_path = bpy.context.preferences.addons['io_scene_gltf2'].preferences.gltfpack_path_ui
gltf_file_path = export_settings['gltf_filepath']
gltf_file_base = os.path.splitext(os.path.basename(gltf_file_path))[0]
gltf_file_extension = os.path.splitext(os.path.basename(gltf_file_path))[1]
gltf_file_directory = os.path.dirname(gltf_file_path)
gltf_output_file_directory = os.path.join(gltf_file_directory, "gltfpacked")
if (os.path.exists(gltf_output_file_directory) is False):
os.makedirs(gltf_output_file_directory)
gltf_input_file_path = gltf_file_path
gltf_output_file_path = os.path.join(gltf_output_file_directory, gltf_file_base + gltf_file_extension)
options = []
if (export_settings['gltf_gltfpack_tc']):
options.append("-tc")
if (export_settings['gltf_gltfpack_tq']):
options.append("-tq")
options.append(f"{export_settings['gltf_gltfpack_tq']}")
if (export_settings['gltf_gltfpack_si'] != 1.0):
options.append("-si")
options.append(f"{export_settings['gltf_gltfpack_si']}")
if (export_settings['gltf_gltfpack_sa']):
options.append("-sa")
if (export_settings['gltf_gltfpack_slb']):
options.append("-slb")
if (export_settings['gltf_gltfpack_noq']):
options.append("-noq")
else:
options.append("-vp")
options.append(f"{export_settings['gltf_gltfpack_vp']}")
options.append("-vt")
options.append(f"{export_settings['gltf_gltfpack_vt']}")
options.append("-vn")
options.append(f"{export_settings['gltf_gltfpack_vn']}")
options.append("-vc")
options.append(f"{export_settings['gltf_gltfpack_vc']}")
match export_settings['gltf_gltfpack_vpi']:
case "Integer":
options.append("-vpi")
case "Normalized":
options.append("-vpn")
case "Floating-point":
options.append("-vpf")
parameters = []
parameters.append("-i")
parameters.append(gltf_input_file_path)
parameters.append("-o")
parameters.append(gltf_output_file_path)
try:
subprocess.run([gltfpack_binary_file_path] + options + parameters, check=True)
except subprocess.CalledProcessError as e:
print_console('ERROR', "Calling gltfpack was not successful")
def __write_file(json, buffer, export_settings): def __write_file(json, buffer, export_settings):
try: try:
@ -117,6 +183,9 @@ def __write_file(json, buffer, export_settings):
export_settings, export_settings,
gltf2_blender_json.BlenderJSONEncoder, gltf2_blender_json.BlenderJSONEncoder,
buffer) buffer)
if (export_settings['gltf_use_gltfpack'] == True):
__postprocess_with_gltfpack(export_settings)
except AssertionError as e: except AssertionError as e:
_, _, tb = sys.exc_info() _, _, tb = sys.exc_info()
traceback.print_tb(tb) # Fixed format traceback.print_tb(tb) # Fixed format

View File

@ -446,13 +446,15 @@ class PrimitiveCreator:
vc_color_name = material_info['vc_info']['color'] vc_color_name = material_info['vc_info']['color']
elif material_info['vc_info']['color_type'] == "active": elif material_info['vc_info']['color_type'] == "active":
# Get active (render) Vertex Color # Get active (render) Vertex Color
vc_color_name = self.blender_mesh.color_attributes[self.blender_mesh.color_attributes.render_color_index].name if self.blender_mesh.color_attributes.render_color_index != -1:
vc_color_name = self.blender_mesh.color_attributes[self.blender_mesh.color_attributes.render_color_index].name
if material_info['vc_info']['alpha_type'] == "name": if material_info['vc_info']['alpha_type'] == "name":
vc_alpha_name = material_info['vc_info']['alpha'] vc_alpha_name = material_info['vc_info']['alpha']
elif material_info['vc_info']['alpha_type'] == "active": elif material_info['vc_info']['alpha_type'] == "active":
# Get active (render) Vertex Color # Get active (render) Vertex Color
vc_alpha_name = self.blender_mesh.color_attributes[self.blender_mesh.color_attributes.render_color_index].name if self.blender_mesh.color_attributes.render_color_index != -1:
vc_alpha_name = self.blender_mesh.color_attributes[self.blender_mesh.color_attributes.render_color_index].name
if vc_color_name is not None: if vc_color_name is not None:
@ -472,7 +474,7 @@ class PrimitiveCreator:
# We need to check if we need to add alpha # We need to check if we need to add alpha
add_alpha = vc_alpha_name is not None add_alpha = vc_alpha_name is not None
mat = get_material_from_idx(material_idx, self.materials, self.export_settings) mat = get_material_from_idx(material_idx, self.materials, self.export_settings)
add_alpha = add_alpha and not (mat.blend_method is None or mat.blend_method == 'OPAQUE') add_alpha = mat is not None and add_alpha and not (mat.blend_method is None or mat.blend_method == 'OPAQUE')
# Manage Vertex Color (RGB and Alpha if needed) # Manage Vertex Color (RGB and Alpha if needed)
self.__manage_color_attribute(vc_color_name, vc_alpha_name if add_alpha else None) self.__manage_color_attribute(vc_color_name, vc_alpha_name if add_alpha else None)
else: else:
@ -950,6 +952,33 @@ class PrimitiveCreator:
# Must calculate the type of the field : FLOAT_COLOR or BYTE_COLOR # Must calculate the type of the field : FLOAT_COLOR or BYTE_COLOR
additional_fields.append(('COLOR_0' + str(i), gltf2_blender_conversion.get_numpy_type('FLOAT_COLOR' if max_index == 3 else 'BYTE_COLOR'))) additional_fields.append(('COLOR_0' + str(i), gltf2_blender_conversion.get_numpy_type('FLOAT_COLOR' if max_index == 3 else 'BYTE_COLOR')))
if self.export_settings['gltf_loose_edges']:
additional_fields_edges = []
for i in range(max_index):
# Must calculate the type of the field : FLOAT_COLOR or BYTE_COLOR
additional_fields_edges.append(('COLOR_0' + str(i), gltf2_blender_conversion.get_numpy_type('FLOAT_COLOR' if max_index == 3 else 'BYTE_COLOR')))
new_dt = np.dtype(self.dots_edges.dtype.descr + additional_fields_edges)
dots_edges = np.zeros(self.dots_edges.shape, dtype=new_dt)
for f in self.dots_edges.dtype.names:
dots_edges[f] = self.dots_edges[f]
self.dots_edges = dots_edges
if self.export_settings['gltf_loose_points']:
additional_fields_points = []
for i in range(max_index):
# Must calculate the type of the field : FLOAT_COLOR or BYTE_COLOR
additional_fields_points.append(('COLOR_0' + str(i), gltf2_blender_conversion.get_numpy_type('FLOAT_COLOR' if max_index == 3 else 'BYTE_COLOR')))
new_dt = np.dtype(self.dots_points.dtype.descr + additional_fields_points)
dots_points = np.zeros(self.dots_points.shape, dtype=new_dt)
for f in self.dots_points.dtype.names:
dots_points[f] = self.dots_points[f]
self.dots_points = dots_points
# Keep the existing custom attribute # Keep the existing custom attribute
# Data will be exported twice, one for COLOR_O, one for the custom attribute # Data will be exported twice, one for COLOR_O, one for the custom attribute
new_dt = np.dtype(self.dots.dtype.descr + additional_fields) new_dt = np.dtype(self.dots.dtype.descr + additional_fields)
@ -964,7 +993,7 @@ class PrimitiveCreator:
self.dots['COLOR_0' +str(i)] = data_dots[:, i] self.dots['COLOR_0' +str(i)] = data_dots[:, i]
if self.export_settings['gltf_loose_edges'] and attr.domain == "POINT": if self.export_settings['gltf_loose_edges'] and attr.domain == "POINT":
self.dots_edges['COLOR_0' + str(i)] = data_dots_edges[:, i] self.dots_edges['COLOR_0' + str(i)] = data_dots_edges[:, i]
if self.export_settings['gltf_loose_points'] and attr['blender_domain'] == "POINT": if self.export_settings['gltf_loose_points'] and attr.domain == "POINT":
self.dots_points['COLOR_0' + str(i)] = data_dots_points[:, i] self.dots_points['COLOR_0' + str(i)] = data_dots_points[:, i]
# Add COLOR_0 in attribute list # Add COLOR_0 in attribute list

View File

@ -545,6 +545,12 @@ def get_base_material(material_idx, materials, export_settings):
mat, mat,
export_settings export_settings
) )
if material is None:
# If no material, the mesh can still have vertex color
# So, retrieving it
material_info["vc_info"] = {"color_type": "active", "alpha_type": "active"}
return material, material_info return material, material_info
def get_all_textures(idx=0): def get_all_textures(idx=0):

View File

@ -6,8 +6,8 @@
bl_info = { bl_info = {
"name": "Bsurfaces GPL Edition", "name": "Bsurfaces GPL Edition",
"author": "Eclectiel, Vladimir Spivak (cwolf3d)", "author": "Eclectiel, Vladimir Spivak (cwolf3d)",
"version": (1, 8, 1), "version": (1, 8, 2),
"blender": (2, 80, 0), "blender": (4, 0, 0),
"location": "View3D EditMode > Sidebar > Edit Tab", "location": "View3D EditMode > Sidebar > Edit Tab",
"description": "Modeling and retopology tool", "description": "Modeling and retopology tool",
"doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/bsurfaces.html", "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/bsurfaces.html",
@ -3539,11 +3539,10 @@ class MESH_OT_SURFSK_init(Operator):
global_mesh_object = mesh_object.name global_mesh_object = mesh_object.name
bpy.context.scene.bsurfaces.SURFSK_mesh = bpy.data.objects[global_mesh_object] bpy.context.scene.bsurfaces.SURFSK_mesh = bpy.data.objects[global_mesh_object]
bpy.context.scene.tool_settings.snap_elements = {'FACE'} bpy.context.scene.tool_settings.snap_elements = {'FACE_PROJECT'}
bpy.context.scene.tool_settings.use_snap = True bpy.context.scene.tool_settings.use_snap = True
bpy.context.scene.tool_settings.use_snap_self = False bpy.context.scene.tool_settings.use_snap_self = False
bpy.context.scene.tool_settings.use_snap_align_rotation = True bpy.context.scene.tool_settings.use_snap_align_rotation = True
bpy.context.scene.tool_settings.use_snap_project = True
bpy.context.scene.tool_settings.use_snap_rotate = True bpy.context.scene.tool_settings.use_snap_rotate = True
bpy.context.scene.tool_settings.use_snap_scale = True bpy.context.scene.tool_settings.use_snap_scale = True

View File

@ -16,7 +16,7 @@ bl_info = {
'author': 'Robin Hohnsbeen', 'author': 'Robin Hohnsbeen',
'description': 'Bake vector displacement brushes easily from a plane', 'description': 'Bake vector displacement brushes easily from a plane',
'blender': (3, 5, 0), 'blender': (3, 5, 0),
'version': (1, 0, 2), 'version': (1, 0, 3),
'location': 'Sculpt Mode: View3D > Sidebar > Tool Tab', 'location': 'Sculpt Mode: View3D > Sidebar > Tool Tab',
'warning': '', 'warning': '',
'category': 'Baking', 'category': 'Baking',
@ -88,7 +88,6 @@ class PT_VDMBaker(bpy.types.Panel):
layout.use_property_split = True layout.use_property_split = True
layout.use_property_decorate = False layout.use_property_decorate = False
layout.operator(create_sculpt_plane.bl_idname, icon='ADD') layout.operator(create_sculpt_plane.bl_idname, icon='ADD')
layout.separator() layout.separator()
@ -187,7 +186,6 @@ class create_vdm_brush(bpy.types.Operator):
return {'CANCELLED'} return {'CANCELLED'}
vdm_plane = context.active_object vdm_plane = context.active_object
addon_data = get_addon_data() addon_data = get_addon_data()
new_brush_name = addon_data.draft_brush_name new_brush_name = addon_data.draft_brush_name
reference_brush_name = addon_data.draft_brush_name reference_brush_name = addon_data.draft_brush_name
@ -214,6 +212,7 @@ class create_vdm_brush(bpy.types.Operator):
bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode='OBJECT')
vdm_bake_material = bakematerial.get_vdm_bake_material() vdm_bake_material = bakematerial.get_vdm_bake_material()
vdm_texture_image = None
try: try:
# Prepare baking # Prepare baking
scene.render.engine = 'CYCLES' scene.render.engine = 'CYCLES'

View File

@ -12,56 +12,62 @@ def get_vdm_bake_material():
material: Baking material material: Baking material
""" """
material_name = 'VDM_baking_material' material_name = 'VDM_baking_material'
if material_name not in bpy.data.materials: if material_name in bpy.data.materials:
new_material = bpy.data.materials.new(name=material_name) # Recreate material every time to ensure it is unchanged by the user which could lead to issues.
# I would like to keep it directly after bake though so people could look
# at how it is made, if they are interested.
bpy.data.materials.remove(bpy.data.materials[material_name])
new_material.use_nodes = True new_material = bpy.data.materials.new(name=material_name)
nodes = new_material.node_tree.nodes
nodes.remove(nodes['Principled BSDF'])
material_output = nodes['Material Output']
# Create relevant nodes new_material.use_nodes = True
combine_node = nodes.new('ShaderNodeCombineXYZ') nodes = new_material.node_tree.nodes
principled_node = next(n for n in new_material.node_tree.nodes if n.type == "BSDF_PRINCIPLED")
nodes.remove(principled_node)
material_output = next(n for n in new_material.node_tree.nodes if n.type == "OUTPUT_MATERIAL")
separate_node1 = nodes.new('ShaderNodeSeparateXYZ') # Create relevant nodes
separate_node2 = nodes.new('ShaderNodeSeparateXYZ') combine_node = nodes.new('ShaderNodeCombineXYZ')
vector_subtract_node = nodes.new('ShaderNodeVectorMath') separate_node1 = nodes.new('ShaderNodeSeparateXYZ')
vector_subtract_node.operation = 'SUBTRACT' separate_node2 = nodes.new('ShaderNodeSeparateXYZ')
vector_multiply_node = nodes.new('ShaderNodeVectorMath') vector_subtract_node = nodes.new('ShaderNodeVectorMath')
vector_multiply_node.operation = 'MULTIPLY' vector_subtract_node.operation = 'SUBTRACT'
vector_multiply_node.inputs[1].default_value = [2.0, 2.0, 2.0]
vector_add_node = nodes.new('ShaderNodeVectorMath') vector_multiply_node = nodes.new('ShaderNodeVectorMath')
vector_add_node.operation = 'ADD' vector_multiply_node.operation = 'MULTIPLY'
vector_add_node.inputs[1].default_value = [-0.5, -0.5, -0.5] vector_multiply_node.inputs[1].default_value = [2.0, 2.0, 2.0]
tex_coord_node = nodes.new('ShaderNodeTexCoord') vector_add_node = nodes.new('ShaderNodeVectorMath')
vector_add_node.operation = 'ADD'
vector_add_node.inputs[1].default_value = [-0.5, -0.5, -0.5]
image_node = nodes.new('ShaderNodeTexImage') tex_coord_node = nodes.new('ShaderNodeTexCoord')
image_node.name = "VDMTexture"
# Connect nodes image_node = nodes.new('ShaderNodeTexImage')
tree = new_material.node_tree image_node.name = "VDMTexture"
tree.links.new(combine_node.outputs[0], material_output.inputs[0])
tree.links.new(separate_node1.outputs[0], combine_node.inputs[0]) # Connect nodes
tree.links.new(separate_node1.outputs[1], combine_node.inputs[1]) tree = new_material.node_tree
tree.links.new(combine_node.outputs[0], material_output.inputs[0])
tree.links.new( tree.links.new(separate_node1.outputs[0], combine_node.inputs[0])
vector_subtract_node.outputs[0], separate_node1.inputs[0]) tree.links.new(separate_node1.outputs[1], combine_node.inputs[1])
tree.links.new( tree.links.new(
vector_multiply_node.outputs[0], vector_subtract_node.inputs[1]) vector_subtract_node.outputs[0], separate_node1.inputs[0])
tree.links.new( tree.links.new(
vector_add_node.outputs[0], vector_multiply_node.inputs[0]) vector_multiply_node.outputs[0], vector_subtract_node.inputs[1])
tree.links.new(tex_coord_node.outputs[2], vector_add_node.inputs[0]) tree.links.new(
tree.links.new( vector_add_node.outputs[0], vector_multiply_node.inputs[0])
tex_coord_node.outputs[3], vector_subtract_node.inputs[0])
tree.links.new(tex_coord_node.outputs[3], separate_node2.inputs[0]) tree.links.new(tex_coord_node.outputs[2], vector_add_node.inputs[0])
tree.links.new(separate_node2.outputs[2], combine_node.inputs[2]) tree.links.new(
tex_coord_node.outputs[3], vector_subtract_node.inputs[0])
tree.links.new(tex_coord_node.outputs[3], separate_node2.inputs[0])
tree.links.new(separate_node2.outputs[2], combine_node.inputs[2])
return bpy.data.materials[material_name] return bpy.data.materials[material_name]