From b410a70fa9ba09bf9c1091d87f38f0ed8dae4895 Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Wed, 9 Jan 2019 19:46:45 +0100 Subject: glTF export: enhancement & fixes: * implement KHR_materials_unlit export * Fix primitive restart values * Fix jpg uri/mime type * Fix bug when image has no color channels * Check animation has actions * Ignore meshes without primitives * Fix materials when not selected in export settings * Improve error message for invalid animation target type * Animation with errors are ignored, but export continues * Export of BaseColorFactor --- .../blender/exp/gltf2_blender_extract.py | 7 ++- ...2_blender_gather_animation_sampler_keyframes.py | 2 +- .../blender/exp/gltf2_blender_gather_animations.py | 23 ++++++---- .../blender/exp/gltf2_blender_gather_image.py | 23 +++++++--- .../blender/exp/gltf2_blender_gather_materials.py | 11 +++-- ...nder_gather_materials_pbr_metallic_roughness.py | 51 ++++++++++++++++++++-- .../blender/exp/gltf2_blender_gather_mesh.py | 4 ++ .../blender/exp/gltf2_blender_gather_primitives.py | 12 +++-- io_scene_gltf2/blender/exp/gltf2_blender_get.py | 3 ++ .../blender/exp/gltf2_blender_gltf2_exporter.py | 2 +- .../blender/exp/gltf2_blender_search_node_tree.py | 10 +++-- 11 files changed, 115 insertions(+), 33 deletions(-) (limited to 'io_scene_gltf2/blender/exp') diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py index 0add794a..42be29b7 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py @@ -975,7 +975,12 @@ def extract_primitives(glTF, blender_mesh, blender_vertex_groups, modifiers, exp # - range_indices = 65536 + # NOTE: Values used by some graphics APIs as "primitive restart" values are disallowed. + # Specifically, the value 65535 (in UINT16) cannot be used as a vertex index. + # https://github.com/KhronosGroup/glTF/issues/1142 + # https://github.com/KhronosGroup/glTF/pull/1476/files + + range_indices = 65535 # diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py index 6ef7fb0b..351a036a 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py @@ -46,7 +46,7 @@ class Keyframe: }.get(self.__target) if length is None: - raise RuntimeError("Unknown target type {}".format(self.__target)) + raise RuntimeError("Animations with target type '{}' are not supported.".format(self.__target)) return length diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py index bfbc03ed..7a7cda02 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py @@ -17,6 +17,7 @@ import typing from io_scene_gltf2.io.com import gltf2_io from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_channels +from io_scene_gltf2.io.com.gltf2_io_debug import print_console def gather_animations(blender_object: bpy.types.Object, export_settings) -> typing.List[gltf2_io.Animation]: @@ -48,13 +49,18 @@ def __gather_animation(blender_action: bpy.types.Action, if not __filter_animation(blender_action, blender_object, export_settings): return None - animation = gltf2_io.Animation( - channels=__gather_channels(blender_action, blender_object, export_settings), - extensions=__gather_extensions(blender_action, blender_object, export_settings), - extras=__gather_extras(blender_action, blender_object, export_settings), - name=__gather_name(blender_action, blender_object, export_settings), - samplers=__gather_samplers(blender_action, blender_object, export_settings) - ) + name = __gather_name(blender_action, blender_object, export_settings) + try: + animation = gltf2_io.Animation( + channels=__gather_channels(blender_action, blender_object, export_settings), + extensions=__gather_extensions(blender_action, blender_object, export_settings), + extras=__gather_extras(blender_action, blender_object, export_settings), + name=name, + samplers=__gather_samplers(blender_action, blender_object, export_settings) + ) + except RuntimeError as error: + print_console("WARNING", "Animation '{}' could not be exported. Cause: {}".format(name, error)) + return None # To allow reuse of samplers in one animation, __link_samplers(animation, export_settings) @@ -159,7 +165,8 @@ def __get_blender_actions(blender_object: bpy.types.Object if blender_object.type == "MESH" \ and blender_object.data is not None \ and blender_object.data.shape_keys is not None \ - and blender_object.data.shape_keys.animation_data is not None: + and blender_object.data.shape_keys.animation_data is not None \ + and blender_object.data.shape_keys.animation_data.action is not None: blender_actions.append(blender_object.data.shape_keys.animation_data.action) # Remove duplicate actions. diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py index a694cac3..6b969668 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import re import bpy import typing @@ -30,13 +31,17 @@ def gather_image( export_settings): if not __filter_image(blender_shader_sockets_or_texture_slots, export_settings): return None + + uri = __gather_uri(blender_shader_sockets_or_texture_slots, export_settings) + mime_type = __gather_mime_type(uri.filepath if uri is not None else "") + image = gltf2_io.Image( buffer_view=__gather_buffer_view(blender_shader_sockets_or_texture_slots, export_settings), extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings), extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings), - mime_type=__gather_mime_type(blender_shader_sockets_or_texture_slots, export_settings), + mime_type=mime_type, name=__gather_name(blender_shader_sockets_or_texture_slots, export_settings), - uri=__gather_uri(blender_shader_sockets_or_texture_slots, export_settings) + uri=uri ) return image @@ -51,7 +56,7 @@ def __gather_buffer_view(sockets_or_slots, export_settings): if export_settings[gltf2_blender_export_keys.FORMAT] != 'GLTF_SEPARATE': image = __get_image_data(sockets_or_slots, export_settings) return gltf2_io_binary_data.BinaryData( - data=image.to_image_data(__gather_mime_type(sockets_or_slots, export_settings))) + data=image.to_image_data(__gather_mime_type())) return None @@ -63,9 +68,13 @@ def __gather_extras(sockets_or_slots, export_settings): return None -def __gather_mime_type(sockets_or_slots, export_settings): - return 'image/png' - # return 'image/jpeg' +def __gather_mime_type(filepath=""): + extension_types = {'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg'} + default_extension = extension_types['.png'] + + matches = re.findall(r'\.\w+$', filepath) + extension = matches[0] if len(matches) > 0 else default_extension + return extension_types[extension] if extension.lower() in extension_types.keys() else default_extension def __gather_name(sockets_or_slots, export_settings): @@ -98,6 +107,8 @@ def __get_image_data(sockets_or_slots, export_settings): # in a helper class. During generation of the glTF in the exporter these will then be combined to actual binary # ressources. def split_pixels_by_channels(image: bpy.types.Image, export_settings) -> typing.List[typing.List[float]]: + assert image.channels > 0, "Image '{}' has no color channels and cannot be exported.".format(image.name) + channelcache = export_settings['gltf_channelcache'] if image.name in channelcache: return channelcache[image.name] diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py index 427f07ce..e1c17480 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py @@ -16,7 +16,8 @@ import bpy from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached from io_scene_gltf2.io.com import gltf2_io -from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info +from io_scene_gltf2.io.com.gltf2_io_extensions import Extension +from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info, gltf2_blender_export_keys from io_scene_gltf2.blender.exp import gltf2_blender_gather_material_normal_texture_info_class from io_scene_gltf2.blender.exp import gltf2_blender_gather_material_occlusion_texture_info_class @@ -69,11 +70,7 @@ def gather_material(blender_material, export_settings): def __filter_material(blender_material, export_settings): - # if not blender_material.use_nodes: - # return False - # if not blender_material.node_tree: - # return False - return True + return export_settings[gltf2_blender_export_keys.MATERIALS] def __gather_alpha_cutoff(blender_material, export_settings): @@ -113,6 +110,8 @@ def __gather_emissive_texture(blender_material, export_settings): def __gather_extensions(blender_material, export_settings): extensions = {} + if gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Background") is not None: + extensions["KHR_materials_unlit"] = Extension("KHR_materials_unlit", {}, False) # TODO specular glossiness extension diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py index 2a6315bf..67e7fa12 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py @@ -13,11 +13,13 @@ # limitations under the License. import bpy +from mathutils import Color from io_scene_gltf2.io.com import gltf2_io -from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info +from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info, gltf2_blender_search_node_tree from io_scene_gltf2.blender.exp import gltf2_blender_get from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached +from io_scene_gltf2.io.com.gltf2_io_debug import print_console @cached @@ -48,9 +50,41 @@ def __gather_base_color_factor(blender_material, export_settings): base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "BaseColor") if base_color_socket is None: base_color_socket = gltf2_blender_get.get_socket_or_texture_slot_old(blender_material, "BaseColorFactor") - if isinstance(base_color_socket, bpy.types.NodeSocket) and not base_color_socket.is_linked: + if base_color_socket is None: + base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Background") + if not isinstance(base_color_socket, bpy.types.NodeSocket): + return None + if not base_color_socket.is_linked: return list(base_color_socket.default_value) - return None + + texture_node = __get_tex_from_socket(base_color_socket) + if texture_node is None: + return None + + def is_valid_multiply_node(node): + return isinstance(node, bpy.types.ShaderNodeMixRGB) and \ + node.blend_type == "MULTIPLY" and \ + len(node.inputs) == 3 + + multiply_node = next((link.from_node for link in texture_node.path if is_valid_multiply_node(link.from_node)), None) + if multiply_node is None: + return None + + def is_factor_socket(socket): + return isinstance(socket, bpy.types.NodeSocketColor) and \ + (not socket.is_linked or socket.links[0] not in texture_node.path) + + factor_socket = next((socket for socket in multiply_node.inputs if is_factor_socket(socket)), None) + if factor_socket is None: + return None + + if factor_socket.is_linked: + print_console("WARNING", "BaseColorFactor only supports sockets without links (in Node '{}')." + .format(multiply_node.name)) + return None + + return list(factor_socket.default_value) + def __gather_base_color_texture(blender_material, export_settings): base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Base Color") @@ -58,9 +92,20 @@ def __gather_base_color_texture(blender_material, export_settings): base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "BaseColor") if base_color_socket is None: base_color_socket = gltf2_blender_get.get_socket_or_texture_slot_old(blender_material, "BaseColor") + if base_color_socket is None: + base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Background") return gltf2_blender_gather_texture_info.gather_texture_info((base_color_socket,), export_settings) +def __get_tex_from_socket(blender_shader_socket: bpy.types.NodeSocket): + 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 __gather_extensions(blender_material, export_settings): return None diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_mesh.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_mesh.py index f32eb733..d6058c60 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_mesh.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_mesh.py @@ -18,6 +18,7 @@ from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached from io_scene_gltf2.io.com import gltf2_io from io_scene_gltf2.blender.exp import gltf2_blender_gather_primitives from io_scene_gltf2.blender.exp import gltf2_blender_generate_extras +from io_scene_gltf2.io.com.gltf2_io_debug import print_console @cached @@ -38,6 +39,9 @@ def gather_mesh(blender_mesh: bpy.types.Mesh, weights=__gather_weights(blender_mesh, vertex_groups, modifiers, export_settings) ) + if len(mesh.primitives) == 0: + print_console("WARNING", "Mesh '{}' has no primitives and will be omitted.".format(mesh.name)) + return None return mesh diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py index 23d51fa6..bf81e808 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py @@ -72,13 +72,19 @@ def __gather_materials(blender_primitive, blender_mesh, modifiers, export_settin def __gather_indices(blender_primitive, blender_mesh, modifiers, export_settings): indices = blender_primitive['indices'] + # NOTE: Values used by some graphics APIs as "primitive restart" values are disallowed. + # Specifically, the values 65535 (in UINT16) and 4294967295 (in UINT32) cannot be used as indices. + # https://github.com/KhronosGroup/glTF/issues/1142 + # https://github.com/KhronosGroup/glTF/pull/1476/files + # Also, UINT8 mode is not supported: + # https://github.com/KhronosGroup/glTF/issues/1471 max_index = max(indices) - if max_index < (1 << 16): + if max_index < 65535: component_type = gltf2_io_constants.ComponentType.UnsignedShort - elif max_index < (1 << 32): + elif max_index < 4294967295: component_type = gltf2_io_constants.ComponentType.UnsignedInt else: - print_console('ERROR', 'Invalid max_index: ' + str(max_index)) + print_console('ERROR', 'A mesh contains too many vertices (' + str(max_index) + ') and needs to be split before export.') return None element_type = gltf2_io_constants.DataType.Scalar diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_get.py b/io_scene_gltf2/blender/exp/gltf2_blender_get.py index 7801190b..08e3a307 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_get.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_get.py @@ -48,6 +48,9 @@ def get_socket_or_texture_slot(blender_material: bpy.types.Material, name: str): if name == "Emissive": type = bpy.types.ShaderNodeEmission name = "Color" + elif name == "Background": + type = bpy.types.ShaderNodeBackground + name = "Color" else: type = bpy.types.ShaderNodeBsdfPrincipled nodes = [n for n in blender_material.node_tree.nodes if isinstance(n, type)] diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py b/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py index 65efafef..ea26eb1c 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py @@ -215,7 +215,7 @@ class GlTF2Exporter: # TODO: we need to know the image url at this point already --> maybe add all options to the constructor of the # exporter # TODO: allow embedding of images (base64) - return image.name + ".png" + return image.name + image.get_extension() @classmethod def __get_key_path(cls, d: dict, keypath: List[str], default=[]): diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_search_node_tree.py b/io_scene_gltf2/blender/exp/gltf2_blender_search_node_tree.py index 92b63c7d..72eb5252 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_search_node_tree.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_search_node_tree.py @@ -82,14 +82,16 @@ def from_socket(start_socket: bpy.types.NodeSocket, for link in start_socket.links: # follow the link to a shader node linked_node = link.from_node - # add the link to the current path - search_path.append(link) # check if the node matches the filter if shader_node_filter(linked_node): - results.append(NodeTreeSearchResult(linked_node, search_path)) + results.append(NodeTreeSearchResult(linked_node, search_path + [link])) # traverse into inputs of the node for input_socket in linked_node.inputs: - results += __search_from_socket(input_socket, shader_node_filter, search_path) + linked_results = __search_from_socket(input_socket, shader_node_filter, search_path + [link]) + if linked_results: + # add the link to the current path + search_path.append(link) + results += linked_results return results -- cgit v1.2.3