FBX IO: Only import the first animation curve per channel #104866

Merged
Thomas Barlow merged 3 commits from Mysteryem/blender-addons:fbx_anim_import_discard_extra_curves_pr into main 2023-09-08 00:33:39 +02:00
27 changed files with 180 additions and 133 deletions
Showing only changes of commit 7b90b5af6c - Show all commits

View File

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

View File

@ -635,7 +635,8 @@ def _combine_curve_keyframe_times(times_and_values_tuples, initial_values):
interpolating the keyframe values is a TODO."""
if len(times_and_values_tuples) == 1:
# Nothing to do when there is only a single curve.
return times_and_values_tuples[0]
times, values = times_and_values_tuples[0]
return times, [values]
all_times = [t[0] for t in times_and_values_tuples]

View File

@ -5,7 +5,7 @@
bl_info = {
'name': 'glTF 2.0 format',
'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors',
"version": (4, 0, 7),
"version": (4, 0, 15),
'blender': (4, 0, 0),
'location': 'File > Import-Export',
'description': 'Import-Export as glTF 2.0',

View File

@ -119,7 +119,8 @@ def get_numpy_type(attribute_component_type):
def get_attribute_type(component_type, data_type):
if gltf2_io_constants.DataType.num_elements(data_type) == 1:
return {
gltf2_io_constants.ComponentType.Float: "FLOAT"
gltf2_io_constants.ComponentType.Float: "FLOAT",
gltf2_io_constants.ComponentType.UnsignedByte: "INT" # What is the best for compatibility?
}[component_type]
elif gltf2_io_constants.DataType.num_elements(data_type) == 2:
return {
@ -132,7 +133,8 @@ def get_attribute_type(component_type, data_type):
elif gltf2_io_constants.DataType.num_elements(data_type) == 4:
return {
gltf2_io_constants.ComponentType.Float: "FLOAT_COLOR",
gltf2_io_constants.ComponentType.UnsignedShort: "BYTE_COLOR"
gltf2_io_constants.ComponentType.UnsignedShort: "BYTE_COLOR",
gltf2_io_constants.ComponentType.UnsignedByte: "BYTE_COLOR" # What is the best for compatibility?
}[component_type]
else:
pass

View File

@ -45,10 +45,19 @@ def is_bone_anim_channel(data_path: str) -> bool:
def get_sk_exported(key_blocks):
return [
key_block
for key_block in key_blocks
if not skip_sk(key_block)
k
for k in key_blocks
if not skip_sk(key_blocks, k)
]
def skip_sk(k):
return k == k.relative_key or k.mute
def skip_sk(key_blocks, k):
# Do not export:
# - if muted
# - if relative key is SK itself (this avoid exporting Basis too if user didn't change order)
# - the Basis (the first SK of the list)
return k == k.relative_key \
or k.mute \
or is_first_index(key_blocks, k) is True
def is_first_index(key_blocks, k):
return key_blocks[0].name == k.name

View File

@ -30,7 +30,7 @@ def gather_animation_fcurves_channels(
custom_range = (blender_action.frame_start, blender_action.frame_end)
channels = []
for chan in [chan for chan in channels_to_perform.values() if len(chan['properties']) != 0]:
for chan in [chan for chan in channels_to_perform.values() if len(chan['properties']) != 0 and chan['type'] != "EXTRA"]:
for channel_group in chan['properties'].values():
channel = __gather_animation_fcurve_channel(chan['obj_uuid'], channel_group, chan['bone'], custom_range, export_settings)
if channel is not None:
@ -73,10 +73,13 @@ def get_channel_groups(obj_uuid: str, blender_action: bpy.types.Action, export_s
else:
try:
target = get_object_from_datapath(blender_object, object_path)
if blender_object.type == "ARMATURE" and fcurve.data_path.startswith("pose.bones["):
type_ = "BONE"
else:
type_ = "EXTRA"
if blender_object.type == "MESH" and object_path.startswith("key_blocks"):
shape_key = blender_object.data.shape_keys.path_resolve(object_path)
if skip_sk(shape_key):
if skip_sk(blender_object.data.shape_keys.key_blocks, shape_key):
continue
target = blender_object.data.shape_keys
type_ = "SK"
@ -86,7 +89,7 @@ def get_channel_groups(obj_uuid: str, blender_action: bpy.types.Action, export_s
if blender_object.type == "MESH":
try:
shape_key = blender_object.data.shape_keys.path_resolve(object_path)
if skip_sk(shape_key):
if skip_sk(blender_object.data.shape_keys.key_blocks, shape_key):
continue
target = blender_object.data.shape_keys
type_ = "SK"
@ -181,7 +184,7 @@ def __get_channel_group_sorted(channels: typing.Tuple[bpy.types.FCurve], blender
shapekeys_idx = {}
cpt_sk = 0
for sk in blender_object.data.shape_keys.key_blocks:
if skip_sk(sk):
if skip_sk(blender_object.data.shape_keys.key_blocks, sk):
continue
shapekeys_idx[sk.name] = cpt_sk
cpt_sk += 1

View File

@ -4,7 +4,7 @@
import bpy
import typing
from .....blender.com.gltf2_blender_data_path import skip_sk
from .....blender.com.gltf2_blender_data_path import get_sk_exported
from ....com.gltf2_blender_data_path import get_target_object_path
from ...gltf2_blender_gather_cache import cached
from ..gltf2_blender_gather_keyframes import Keyframe
@ -165,9 +165,7 @@ def __gather_non_keyed_values(
if object_path:
shapekeys_idx = {}
cpt_sk = 0
for sk in blender_object.data.shape_keys.key_blocks:
if skip_sk(sk):
continue
for sk in get_sk_exported(blender_object.data.shape_keys.key_blocks):
shapekeys_idx[cpt_sk] = sk.name
cpt_sk += 1

View File

@ -294,6 +294,8 @@ def gather_action_animations( obj_uuid: int,
channel = gather_sampled_object_channel(obj_uuid, prop, blender_action.name, True, get_gltf_interpolation("LINEAR"), export_settings)
elif type_ == "SK":
channel = gather_sampled_sk_channel(obj_uuid, blender_action.name, export_settings)
elif type_ == "EXTRA":
channel = None
else:
print("Type unknown. Should not happen")

View File

@ -61,7 +61,7 @@ def get_sk_drivers(blender_armature_uuid, export_settings):
sk_name = child.data.shape_keys.path_resolve(get_target_object_path(sk_c.data_path)).name
except:
continue
if skip_sk(child.data.shape_keys.key_blocks[sk_name]):
if skip_sk(child.data.shape_keys.key_blocks, child.data.shape_keys.key_blocks[sk_name]):
continue
idx_channel_mapping.append((shapekeys_idx[sk_name], sk_c))
existing_idx = dict(idx_channel_mapping)

View File

@ -6,6 +6,7 @@ import math
import bpy
from mathutils import Matrix, Quaternion, Vector
from ...io.com.gltf2_io_debug import print_console
from ...io.com import gltf2_io
from ...io.com import gltf2_io_extensions
from ...io.exp.gltf2_io_user_extensions import export_user_extensions
@ -181,7 +182,9 @@ def __gather_mesh(vnode, blender_object, export_settings):
return None
# Be sure that object is valid (no NaN for example)
blender_object.data.validate()
res = blender_object.data.validate()
if res is True:
print_console("WARNING", "Mesh " + blender_object.data.name + " is not valid, and may be exported wrongly")
modifiers = blender_object.modifiers
if len(modifiers) == 0:

View File

@ -35,7 +35,7 @@ def gather_primitive_attributes(blender_primitive, export_settings):
return attributes
def array_to_accessor(array, component_type, data_type, include_max_and_min=False):
def array_to_accessor(array, component_type, data_type, include_max_and_min=False, normalized=None):
amax = None
amin = None
@ -53,7 +53,7 @@ def array_to_accessor(array, component_type, data_type, include_max_and_min=Fals
max=amax,
min=amin,
name=None,
normalized=None,
normalized=normalized,
sparse=None,
type=data_type,
)
@ -183,6 +183,7 @@ def __gather_attribute(blender_primitive, attribute, export_settings):
data['data'],
component_type=data['component_type'],
data_type=data['data_type'],
include_max_and_min=include_max_and_mins.get(attribute, False)
include_max_and_min=include_max_and_mins.get(attribute, False),
normalized=data.get('normalized')
)
}

View File

@ -6,7 +6,7 @@ import numpy as np
from mathutils import Vector
from ...blender.com.gltf2_blender_data_path import get_sk_exported
from ...io.com.gltf2_io_debug import print_console
from ...io.com.gltf2_io_constants import NORMALS_ROUNDING_DIGIT
from ...io.com.gltf2_io_constants import ROUNDING_DIGIT
from ...io.exp.gltf2_io_user_extensions import export_user_extensions
from ...io.com import gltf2_io_constants
from ..com import gltf2_blender_conversion
@ -357,9 +357,16 @@ class PrimitiveCreator:
def primitive_split(self):
# Calculate triangles and sort them into primitives.
try:
self.blender_mesh.calc_loop_triangles()
loop_indices = np.empty(len(self.blender_mesh.loop_triangles) * 3, dtype=np.uint32)
self.blender_mesh.loop_triangles.foreach_get('loops', loop_indices)
except:
# For some not valid meshes, we can't get loops without errors
# We already displayed a Warning message after validate() check, so here
# we can return without a new one
self.prim_indices = {}
return
self.prim_indices = {} # maps material index to TRIANGLES-style indices into dots
@ -715,7 +722,7 @@ class PrimitiveCreator:
self.normals = self.normals.reshape(len(self.blender_mesh.loops), 3)
self.normals = np.round(self.normals, NORMALS_ROUNDING_DIGIT)
self.normals = np.round(self.normals, ROUNDING_DIGIT)
# Force normalization of normals in case some normals are not (why ?)
PrimitiveCreator.normalize_vecs(self.normals)
@ -723,7 +730,7 @@ class PrimitiveCreator:
for key_block in key_blocks:
ns = np.array(key_block.normals_split_get(), dtype=np.float32)
ns = ns.reshape(len(self.blender_mesh.loops), 3)
ns = np.round(ns, NORMALS_ROUNDING_DIGIT)
ns = np.round(ns, ROUNDING_DIGIT)
self.morph_normals.append(ns)
# Transform for skinning
@ -782,6 +789,7 @@ class PrimitiveCreator:
self.tangents = np.empty(len(self.blender_mesh.loops) * 3, dtype=np.float32)
self.blender_mesh.loops.foreach_get('tangent', self.tangents)
self.tangents = self.tangents.reshape(len(self.blender_mesh.loops), 3)
self.tangents = np.round(self.tangents, ROUNDING_DIGIT)
# Transform for skinning
if self.armature and self.blender_object:
@ -789,6 +797,7 @@ class PrimitiveCreator:
tangent_transform = apply_matrix.to_quaternion().to_matrix()
self.tangents = PrimitiveCreator.apply_mat_to_all(tangent_transform, self.tangents)
PrimitiveCreator.normalize_vecs(self.tangents)
self.tangents = np.round(self.tangents, ROUNDING_DIGIT)
if self.export_settings['gltf_yup']:
PrimitiveCreator.zup2yup(self.tangents)

View File

@ -308,11 +308,19 @@ def previous_node(socket):
return prev_socket.node
return None
#TODOExt is this the same as __get_tex_from_socket from gather_image ?
def has_image_node_from_socket(socket):
def get_tex_from_socket(socket):
result = gltf2_blender_search_node_tree.from_socket(
socket,
gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage))
if not result:
return False
return True
return None
if result[0].shader_node.image is None:
return None
return result[0]
def has_image_node_from_socket(socket):
return get_tex_from_socket(socket) is not None
def image_tex_is_valid_from_socket(socket):
res = get_tex_from_socket(socket)
return res is not None and res.shader_node.image is not None and res.shader_node.image.channels != 0

View File

@ -55,6 +55,7 @@ def export_clearcoat(blender_material, export_settings):
clearcoat_texture, clearcoat_texture_use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_texture_info(
clearcoat_socket,
clearcoat_roughness_slots,
(),
export_settings,
)
clearcoat_extension['clearcoatTexture'] = clearcoat_texture
@ -64,6 +65,7 @@ def export_clearcoat(blender_material, export_settings):
clearcoat_roughness_texture, clearcoat_roughness_texture_use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_texture_info(
clearcoat_roughness_socket,
clearcoat_roughness_slots,
(),
export_settings,
)
clearcoat_extension['clearcoatRoughnessTexture'] = clearcoat_roughness_texture

View File

@ -52,7 +52,7 @@ def export_emission_texture(blender_material, export_settings):
emissive = gltf2_blender_get.get_socket(blender_material, "Emissive")
if emissive is None:
emissive = gltf2_blender_get.get_socket_old(blender_material, "Emissive")
emissive_texture, use_actives_uvmap_emissive, _ = gltf2_blender_gather_texture_info.gather_texture_info(emissive, (emissive,), export_settings)
emissive_texture, use_actives_uvmap_emissive, _ = gltf2_blender_gather_texture_info.gather_texture_info(emissive, (emissive,), (), export_settings)
return emissive_texture, ["emissiveTexture"] if use_actives_uvmap_emissive else None
def export_emission_strength_extension(emissive_factor, export_settings):

View File

@ -40,6 +40,7 @@ def export_sheen(blender_material, export_settings):
original_sheenColor_texture, original_sheenColor_use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_texture_info(
sheenColor_socket,
(sheenColor_socket,),
(),
export_settings,
)
sheen_extension['sheenColorTexture'] = original_sheenColor_texture
@ -64,6 +65,7 @@ def export_sheen(blender_material, export_settings):
original_sheenRoughness_texture, original_sheenRoughness_use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_texture_info(
sheenRoughness_socket,
(sheenRoughness_socket,),
(),
export_settings,
)
sheen_extension['sheenRoughnessTexture'] = original_sheenRoughness_texture

View File

@ -7,7 +7,8 @@ from .....io.com.gltf2_io_extensions import Extension
from .....io.com.gltf2_io_constants import GLTF_IOR
from ....exp import gltf2_blender_get
from ....com.gltf2_blender_default import BLENDER_SPECULAR, BLENDER_SPECULAR_TINT
from ...material import gltf2_blender_gather_texture_info
from ...material.gltf2_blender_gather_texture_info import gather_texture_info
from ...gltf2_blender_get import image_tex_is_valid_from_socket
def export_original_specular(blender_material, export_settings):
specular_extension = {}
@ -36,9 +37,10 @@ def export_original_specular(blender_material, export_settings):
# Texture
if gltf2_blender_get.has_image_node_from_socket(original_specular_socket):
original_specular_texture, original_specular_use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_texture_info(
original_specular_texture, original_specular_use_active_uvmap, _ = gather_texture_info(
original_specular_socket,
(original_specular_socket,),
(),
export_settings,
)
specular_extension['specularTexture'] = original_specular_texture
@ -58,9 +60,10 @@ def export_original_specular(blender_material, export_settings):
# Texture
if gltf2_blender_get.has_image_node_from_socket(original_specularcolor_socket):
original_specularcolor_texture, original_specularcolor_use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_texture_info(
original_specularcolor_texture, original_specularcolor_use_active_uvmap, _ = gather_texture_info(
original_specularcolor_socket,
(original_specularcolor_socket,),
(),
export_settings,
)
specular_extension['specularColorTexture'] = original_specularcolor_texture
@ -86,12 +89,11 @@ def export_specular(blender_material, export_settings):
if base_color_socket is None:
return None, None
# TODOExt replace by __has_image_node_from_socket calls
specular_not_linked = isinstance(specular_socket, bpy.types.NodeSocket) and not specular_socket.is_linked
specular_tint_not_linked = isinstance(specular_tint_socket, bpy.types.NodeSocket) and not specular_tint_socket.is_linked
base_color_not_linked = isinstance(base_color_socket, bpy.types.NodeSocket) and not base_color_socket.is_linked
transmission_not_linked = isinstance(transmission_socket, bpy.types.NodeSocket) and not transmission_socket.is_linked
ior_not_linked = isinstance(ior_socket, bpy.types.NodeSocket) and not ior_socket.is_linked
specular_not_linked = not image_tex_is_valid_from_socket(specular_socket)
specular_tint_not_linked = not image_tex_is_valid_from_socket(specular_tint_socket)
base_color_not_linked = not image_tex_is_valid_from_socket(base_color_socket)
transmission_not_linked = not image_tex_is_valid_from_socket(transmission_socket)
ior_not_linked = not image_tex_is_valid_from_socket(ior_socket)
specular = specular_socket.default_value if specular_not_linked else None
specular_tint = specular_tint_socket.default_value if specular_tint_not_linked else None
@ -149,9 +151,10 @@ def export_specular(blender_material, export_settings):
if base_color_not_linked:
primary_socket = transmission_socket
specularColorTexture, use_active_uvmap, specularColorFactor = gltf2_blender_gather_texture_info.gather_texture_info(
specularColorTexture, use_active_uvmap, specularColorFactor = gather_texture_info(
primary_socket,
sockets,
(),
export_settings,
filter_type='ANY')
if specularColorTexture is None:

View File

@ -38,6 +38,7 @@ def export_transmission(blender_material, export_settings):
combined_texture, use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_texture_info(
transmission_socket,
transmission_slots,
(),
export_settings,
)
if has_transmission_texture:

View File

@ -66,6 +66,7 @@ def export_volume(blender_material, export_settings):
combined_texture, use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_texture_info(
thicknesss_socket,
thickness_slots,
(),
export_settings,
)
if has_thickness_texture:

View File

@ -28,6 +28,11 @@ class FillWhite:
"""Fills a channel with all ones (1.0)."""
pass
class FillWith:
"""Fills a channel with all same values"""
def __init__(self, value):
self.value = value
class StoreData:
def __init__(self, data):
"""Store numeric data (not an image channel"""
@ -99,6 +104,9 @@ class ExportImage:
def fill_white(self, dst_chan: Channel):
self.fills[dst_chan] = FillWhite()
def fill_with(self, dst_chan, value):
self.fills[dst_chan] = FillWith(value)
def is_filled(self, chan: Channel) -> bool:
return chan in self.fills
@ -183,6 +191,8 @@ class ExportImage:
for dst_chan, fill in self.fills.items():
if isinstance(fill, FillImage) and fill.image == image:
out_buf[int(dst_chan)::4] = tmp_buf[int(fill.src_chan)::4]
elif isinstance(fill, FillWith):
out_buf[int(dst_chan)::4] = fill.value
tmp_buf = None # GC this

View File

@ -12,17 +12,18 @@ from ....io.exp import gltf2_io_binary_data, gltf2_io_image_data
from ....io.com import gltf2_io_debug
from ....io.exp.gltf2_io_user_extensions import export_user_extensions
from ..gltf2_blender_gather_cache import cached
from . import gltf2_blender_search_node_tree
from .extensions.gltf2_blender_image import Channel, ExportImage, FillImage
from ..gltf2_blender_get import get_tex_from_socket
@cached
def gather_image(
blender_shader_sockets: typing.Tuple[bpy.types.NodeSocket],
default_sockets: typing.Tuple[bpy.types.NodeSocket],
export_settings):
if not __filter_image(blender_shader_sockets, export_settings):
return None, None
image_data = __get_image_data(blender_shader_sockets, export_settings)
image_data = __get_image_data(blender_shader_sockets, default_sockets, export_settings)
if image_data.empty():
# The export image has no data
return None, None
@ -174,24 +175,32 @@ def __gather_uri(image_data, mime_type, name, export_settings):
return None, None
def __get_image_data(sockets, export_settings) -> ExportImage:
def __get_image_data(sockets, default_sockets, export_settings) -> ExportImage:
# For shared resources, such as images, we just store the portion of data that is needed in the glTF property
# in a helper class. During generation of the glTF in the exporter these will then be combined to actual binary
# resources.
results = [__get_tex_from_socket(socket, export_settings) for socket in sockets]
results = [get_tex_from_socket(socket) for socket in sockets]
# Check if we need a simple mapping or more complex calculation
if any([socket.name == "Specular" and socket.node.type == "BSDF_PRINCIPLED" for socket in sockets]):
return __get_image_data_specular(sockets, results, export_settings)
else:
return __get_image_data_mapping(sockets, results, export_settings)
return __get_image_data_mapping(sockets, default_sockets, results, export_settings)
def __get_image_data_mapping(sockets, results, export_settings) -> ExportImage:
def __get_image_data_mapping(sockets, default_sockets, results, export_settings) -> ExportImage:
"""
Simple mapping
Will fit for most of exported textures : RoughnessMetallic, Basecolor, normal, ...
"""
composed_image = ExportImage()
default_metallic = None
default_roughness = None
if "Metallic" in [s.name for s in default_sockets]:
default_metallic = [s for s in default_sockets if s.name == "Metallic"][0].default_value
if "Roughness" in [s.name for s in default_sockets]:
default_roughness = [s for s in default_sockets if s.name == "Roughness"][0].default_value
for result, socket in zip(results, sockets):
# Assume that user know what he does, and that channels/images are already combined correctly for pbr
# If not, we are going to keep only the first texture found
@ -242,8 +251,14 @@ def __get_image_data_mapping(sockets, results, export_settings) -> ExportImage:
# Since metal/roughness are always used together, make sure
# the other channel is filled.
if socket.name == 'Metallic' and not composed_image.is_filled(Channel.G):
if default_roughness is not None:
composed_image.fill_with(Channel.G, default_roughness)
else:
composed_image.fill_white(Channel.G)
elif socket.name == 'Roughness' and not composed_image.is_filled(Channel.B):
if default_metallic is not None:
composed_image.fill_with(Channel.B, default_metallic)
else:
composed_image.fill_white(Channel.B)
else:
# copy full image...eventually following sockets might overwrite things
@ -271,7 +286,7 @@ def __get_image_data_specular(sockets, results, export_settings) -> ExportImage:
composed_image.store_data("ior", sockets[4].default_value, type="Data")
results = [__get_tex_from_socket(socket, export_settings) for socket in sockets[:-1]] #Do not retrieve IOR --> No texture allowed
results = [get_tex_from_socket(socket) for socket in sockets[:-1]] #Do not retrieve IOR --> No texture allowed
mapping = {
0: "specular",
@ -281,7 +296,7 @@ def __get_image_data_specular(sockets, results, export_settings) -> ExportImage:
}
for idx, result in enumerate(results):
if __get_tex_from_socket(sockets[idx], export_settings):
if get_tex_from_socket(sockets[idx]):
composed_image.store_data(mapping[idx], result.shader_node.image, type="Image")
@ -308,16 +323,6 @@ def __get_image_data_specular(sockets, results, export_settings) -> ExportImage:
return composed_image
# TODOExt deduplicate
@cached
def __get_tex_from_socket(blender_shader_socket: bpy.types.NodeSocket, export_settings):
result = gltf2_blender_search_node_tree.from_socket(
blender_shader_socket,
gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage))
if not result:
return None
return result[0]
def __is_blender_image_a_jpeg(image: bpy.types.Image) -> bool:
if image.source != 'FILE':

View File

@ -52,13 +52,13 @@ def gather_material(blender_material, active_uvmap_index, export_settings):
export_user_extensions('gather_material_hook', export_settings, mat_unlit, blender_material)
return mat_unlit
orm_texture = __gather_orm_texture(blender_material, export_settings)
orm_texture, default_sockets = __gather_orm_texture(blender_material, export_settings)
emissive_factor = __gather_emissive_factor(blender_material, export_settings)
emissive_texture, uvmap_actives_emissive_texture = __gather_emissive_texture(blender_material, export_settings)
extensions, uvmap_actives_extensions = __gather_extensions(blender_material, emissive_factor, export_settings)
normal_texture, uvmap_actives_normal_texture = __gather_normal_texture(blender_material, export_settings)
occlusion_texture, uvmap_actives_occlusion_texture = __gather_occlusion_texture(blender_material, orm_texture, export_settings)
occlusion_texture, uvmap_actives_occlusion_texture = __gather_occlusion_texture(blender_material, orm_texture, default_sockets, export_settings)
pbr_metallic_roughness, uvmap_actives_pbr_metallic_roughness = __gather_pbr_metallic_roughness(blender_material, orm_texture, export_settings)
if any([i>1.0 for i in emissive_factor or []]) is True:
@ -303,7 +303,7 @@ def __gather_orm_texture(blender_material, export_settings):
if occlusion is None or not gltf2_blender_get.has_image_node_from_socket(occlusion):
occlusion = gltf2_blender_get.get_socket_old(blender_material, "Occlusion")
if occlusion is None or not gltf2_blender_get.has_image_node_from_socket(occlusion):
return None
return None, None
metallic_socket = gltf2_blender_get.get_socket(blender_material, "Metallic")
roughness_socket = gltf2_blender_get.get_socket(blender_material, "Roughness")
@ -311,38 +311,43 @@ def __gather_orm_texture(blender_material, export_settings):
hasMetal = metallic_socket is not None and gltf2_blender_get.has_image_node_from_socket(metallic_socket)
hasRough = roughness_socket is not None and gltf2_blender_get.has_image_node_from_socket(roughness_socket)
default_sockets = ()
if not hasMetal and not hasRough:
metallic_roughness = gltf2_blender_get.get_socket_old(blender_material, "MetallicRoughness")
if metallic_roughness is None or not gltf2_blender_get.has_image_node_from_socket(metallic_roughness):
return None
return None, default_sockets
result = (occlusion, metallic_roughness)
elif not hasMetal:
result = (occlusion, roughness_socket)
default_sockets = (metallic_socket,)
elif not hasRough:
result = (occlusion, metallic_socket)
default_sockets = (roughness_socket,)
else:
result = (occlusion, roughness_socket, metallic_socket)
default_sockets = ()
if not gltf2_blender_gather_texture_info.check_same_size_images(result):
print_console("INFO",
"Occlusion and metal-roughness texture will be exported separately "
"(use same-sized images if you want them combined)")
return None
return None, ()
# Double-check this will past the filter in texture_info
info, info_use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_texture_info(result[0], result, export_settings)
info, info_use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_texture_info(result[0], result, default_sockets, export_settings)
if info is None:
return None
return None, ()
return result
return result, default_sockets
def __gather_occlusion_texture(blender_material, orm_texture, export_settings):
def __gather_occlusion_texture(blender_material, orm_texture, default_sockets, export_settings):
occlusion = gltf2_blender_get.get_socket(blender_material, "Occlusion")
if occlusion is None:
occlusion = gltf2_blender_get.get_socket_old(blender_material, "Occlusion")
occlusion_texture, use_active_uvmap_occlusion, _ = gltf2_blender_gather_texture_info.gather_material_occlusion_texture_info_class(
occlusion,
orm_texture or (occlusion,),
default_sockets,
export_settings)
return occlusion_texture, ["occlusionTexture"] if use_active_uvmap_occlusion else None

View File

@ -8,8 +8,8 @@ from ....io.com import gltf2_io
from ....io.exp.gltf2_io_user_extensions import export_user_extensions
from ...exp import gltf2_blender_get
from ..gltf2_blender_gather_cache import cached
from . import gltf2_blender_search_node_tree
from . import gltf2_blender_gather_texture_info
from ..gltf2_blender_get import image_tex_is_valid_from_socket
from .gltf2_blender_gather_texture_info import gather_texture_info
@cached
def gather_material_pbr_metallic_roughness(blender_material, orm_texture, export_settings):
@ -93,12 +93,12 @@ def __gather_base_color_texture(blender_material, export_settings):
# keep sockets that have some texture : color and/or alpha
inputs = tuple(
socket for socket in [base_color_socket, alpha_socket]
if socket is not None and __has_image_node_from_socket(socket)
if socket is not None and image_tex_is_valid_from_socket(socket)
)
if not inputs:
return None, None, None
return gltf2_blender_gather_texture_info.gather_texture_info(inputs[0], inputs, export_settings)
return gather_texture_info(inputs[0], inputs, (), export_settings)
def __gather_extensions(blender_material, export_settings):
@ -126,24 +126,29 @@ def __gather_metallic_roughness_texture(blender_material, orm_texture, export_se
metallic_socket = gltf2_blender_get.get_socket(blender_material, "Metallic")
roughness_socket = gltf2_blender_get.get_socket(blender_material, "Roughness")
hasMetal = metallic_socket is not None and __has_image_node_from_socket(metallic_socket)
hasRough = roughness_socket is not None and __has_image_node_from_socket(roughness_socket)
hasMetal = metallic_socket is not None and image_tex_is_valid_from_socket(metallic_socket)
hasRough = roughness_socket is not None and image_tex_is_valid_from_socket(roughness_socket)
default_sockets = ()
if not hasMetal and not hasRough:
metallic_roughness = gltf2_blender_get.get_socket_old(blender_material, "MetallicRoughness")
if metallic_roughness is None or not __has_image_node_from_socket(metallic_roughness):
if metallic_roughness is None or not image_tex_is_valid_from_socket(metallic_roughness):
return None, None, None
texture_input = (metallic_roughness,)
elif not hasMetal:
texture_input = (roughness_socket,)
default_sockets = (metallic_socket,)
elif not hasRough:
texture_input = (metallic_socket,)
default_sockets = (roughness_socket,)
else:
texture_input = (metallic_socket, roughness_socket)
default_sockets = ()
return gltf2_blender_gather_texture_info.gather_texture_info(
return gather_texture_info(
texture_input[0],
orm_texture or texture_input,
default_sockets,
export_settings,
)
@ -160,14 +165,6 @@ def __gather_roughness_factor(blender_material, export_settings):
return fac if fac != 1 else None
return None
def __has_image_node_from_socket(socket):
result = gltf2_blender_search_node_tree.from_socket(
socket,
gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage))
if not result:
return False
return True
def get_default_pbr_for_emissive_node():
return gltf2_io.MaterialPBRMetallicRoughness(
base_color_factor=[0.0,0.0,0.0,1.0],

View File

@ -131,6 +131,7 @@ def gather_base_color_texture(info, export_settings):
unlit_texture, unlit_use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_texture_info(
sockets[0],
sockets,
(),
export_settings,
)
return unlit_texture, ["unlitTexture"] if unlit_use_active_uvmap else None

View File

@ -9,12 +9,13 @@ from ....io.exp.gltf2_io_user_extensions import export_user_extensions
from ....io.com import gltf2_io
from ..gltf2_blender_gather_cache import cached
from ..gltf2_blender_gather_sampler import gather_sampler
from . import gltf2_blender_search_node_tree
from ..gltf2_blender_get import get_tex_from_socket
from . import gltf2_blender_gather_image
@cached
def gather_texture(
blender_shader_sockets: typing.Tuple[bpy.types.NodeSocket],
default_sockets: typing.Tuple[bpy.types.NodeSocket],
export_settings):
"""
Gather texture sampling information and image channels from a blender shader texture attached to a shader socket.
@ -27,7 +28,7 @@ def gather_texture(
if not __filter_texture(blender_shader_sockets, export_settings):
return None, None
source, factor = __gather_source(blender_shader_sockets, export_settings)
source, factor = __gather_source(blender_shader_sockets, default_sockets, export_settings)
texture = gltf2_io.Texture(
extensions=__gather_extensions(blender_shader_sockets, export_settings),
@ -67,7 +68,7 @@ def __gather_name(blender_shader_sockets, export_settings):
def __gather_sampler(blender_shader_sockets, export_settings):
shader_nodes = [__get_tex_from_socket(socket) for socket in blender_shader_sockets]
shader_nodes = [get_tex_from_socket(socket) for socket in blender_shader_sockets]
if len(shader_nodes) > 1:
gltf2_io_debug.print_console("WARNING",
"More than one shader node tex image used for a texture. "
@ -78,16 +79,5 @@ def __gather_sampler(blender_shader_sockets, export_settings):
export_settings)
def __gather_source(blender_shader_sockets, export_settings):
return gltf2_blender_gather_image.gather_image(blender_shader_sockets, export_settings)
# Helpers
# TODOExt deduplicate
def __get_tex_from_socket(socket):
result = gltf2_blender_search_node_tree.from_socket(
socket,
gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage))
if not result:
return None
return result[0]
def __gather_source(blender_shader_sockets, default_sockets, export_settings):
return gltf2_blender_gather_image.gather_image(blender_shader_sockets, default_sockets, export_settings)

View File

@ -8,7 +8,7 @@ from ....io.com import gltf2_io
from ....io.com.gltf2_io_extensions import Extension
from ....io.exp.gltf2_io_user_extensions import export_user_extensions
from ...exp import gltf2_blender_get
from ..gltf2_blender_get import previous_node
from ..gltf2_blender_get import previous_node, get_tex_from_socket
from ..gltf2_blender_gather_sampler import detect_manual_uv_wrapping
from ..gltf2_blender_gather_cache import cached
from . import gltf2_blender_gather_texture
@ -19,20 +19,25 @@ from . import gltf2_blender_search_node_tree
# occlusion the primary_socket would be the occlusion socket, and
# blender_shader_sockets would be the (O,R,M) sockets.
def gather_texture_info(primary_socket, blender_shader_sockets, export_settings, filter_type='ALL'):
return __gather_texture_info_helper(primary_socket, blender_shader_sockets, 'DEFAULT', filter_type, export_settings)
# Default socket parameter is used when there is a mapping between channels, and one of the channel is not a texture
# In that case, we will create a texture with one channel from texture, other from default socket value
# Example: MetallicRoughness
def gather_texture_info(primary_socket, blender_shader_sockets, default_sockets, export_settings, filter_type='ALL'):
return __gather_texture_info_helper(primary_socket, blender_shader_sockets, default_sockets, 'DEFAULT', filter_type, export_settings)
def gather_material_normal_texture_info_class(primary_socket, blender_shader_sockets, export_settings, filter_type='ALL'):
return __gather_texture_info_helper(primary_socket, blender_shader_sockets, 'NORMAL', filter_type, export_settings)
return __gather_texture_info_helper(primary_socket, blender_shader_sockets, (), 'NORMAL', filter_type, export_settings)
def gather_material_occlusion_texture_info_class(primary_socket, blender_shader_sockets, export_settings, filter_type='ALL'):
return __gather_texture_info_helper(primary_socket, blender_shader_sockets, 'OCCLUSION', filter_type, export_settings)
def gather_material_occlusion_texture_info_class(primary_socket, blender_shader_sockets, default_sockets, export_settings, filter_type='ALL'):
return __gather_texture_info_helper(primary_socket, blender_shader_sockets, default_sockets, 'OCCLUSION', filter_type, export_settings)
@cached
def __gather_texture_info_helper(
primary_socket: bpy.types.NodeSocket,
blender_shader_sockets: typing.Tuple[bpy.types.NodeSocket],
default_sockets: typing.Tuple[bpy.types.NodeSocket],
kind: str,
filter_type: str,
export_settings):
@ -41,7 +46,7 @@ def __gather_texture_info_helper(
tex_transform, tex_coord, use_active_uvmap = __gather_texture_transform_and_tex_coord(primary_socket, export_settings)
index, factor = __gather_index(blender_shader_sockets, export_settings)
index, factor = __gather_index(blender_shader_sockets, default_sockets, export_settings)
fields = {
'extensions': __gather_extensions(tex_transform, export_settings),
@ -72,7 +77,7 @@ def __gather_texture_info_helper(
def __filter_texture_info(primary_socket, blender_shader_sockets, filter_type, export_settings):
if primary_socket is None:
return False
if __get_tex_from_socket(primary_socket) is None:
if get_tex_from_socket(primary_socket) is None:
return False
if not blender_shader_sockets:
return False
@ -80,12 +85,12 @@ def __filter_texture_info(primary_socket, blender_shader_sockets, filter_type, e
return False
if filter_type == "ALL":
# Check that all sockets link to texture
if any([__get_tex_from_socket(socket) is None for socket in blender_shader_sockets]):
if any([get_tex_from_socket(socket) is None for socket in blender_shader_sockets]):
# sockets do not lead to a texture --> discard
return False
elif filter_type == "ANY":
# Check that at least one socket link to texture
if all([__get_tex_from_socket(socket) is None for socket in blender_shader_sockets]):
if all([get_tex_from_socket(socket) is None for socket in blender_shader_sockets]):
return False
elif filter_type == "NONE":
# No check
@ -136,9 +141,9 @@ def __gather_occlusion_strength(primary_socket, export_settings):
return None
def __gather_index(blender_shader_sockets, export_settings):
def __gather_index(blender_shader_sockets, default_sockets, export_settings):
# We just put the actual shader into the 'index' member
return gltf2_blender_gather_texture.gather_texture(blender_shader_sockets, export_settings)
return gltf2_blender_gather_texture.gather_texture(blender_shader_sockets, default_sockets, export_settings)
def __gather_texture_transform_and_tex_coord(primary_socket, export_settings):
@ -148,7 +153,7 @@ def __gather_texture_transform_and_tex_coord(primary_socket, export_settings):
#
# The [UV Wrapping] is for wrap modes like MIRROR that use nodes,
# [Mapping] is for KHR_texture_transform, and [UV Map] is for texCoord.
blender_shader_node = __get_tex_from_socket(primary_socket).shader_node
blender_shader_node = get_tex_from_socket(primary_socket).shader_node
# Skip over UV wrapping stuff (it goes in the sampler)
result = detect_manual_uv_wrapping(blender_shader_node)
@ -178,17 +183,6 @@ def __gather_texture_transform_and_tex_coord(primary_socket, export_settings):
return texture_transform, texcoord_idx or None, use_active_uvmap
# TODOExt deduplicate
def __get_tex_from_socket(socket):
result = gltf2_blender_search_node_tree.from_socket(
socket,
gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage))
if not result:
return None
if result[0].shader_node.image is None:
return None
return result[0]
def check_same_size_images(
blender_shader_sockets: typing.Tuple[bpy.types.NodeSocket],
@ -199,7 +193,7 @@ def check_same_size_images(
sizes = set()
for socket in blender_shader_sockets:
tex = __get_tex_from_socket(socket)
tex = get_tex_from_socket(socket)
if tex is None:
return False
size = tex.shader_node.image.size

View File

@ -152,5 +152,5 @@ GLTF_DATA_TYPE_MAT4 = "MAT4"
GLTF_IOR = 1.5
# Rounding digit used for normal rounding
NORMALS_ROUNDING_DIGIT = 4
# Rounding digit used for normal/tangent rounding
ROUNDING_DIGIT = 4