From 042fbefac686666190915d206600a5dab8e03066 Mon Sep 17 00:00:00 2001 From: Julien Duroure <julien.duroure@gmail.com> Date: Thu, 7 Jul 2022 08:03:39 +0200 Subject: [PATCH] glTF importer/exporter: Manage some official Khronos Extensions about Materials KHR_materials_ior KHR_materials_sheen KHR_materials_specular KHR_materials_transmission KHR_materials_variants KHR_materials_emissive_strength KHR_materials_volume Documentation update is still in progress --- io_scene_gltf2/__init__.py | 106 +++- .../blender/com/gltf2_blender_default.py | 6 + .../com/gltf2_blender_material_helpers.py | 20 +- .../blender/com/gltf2_blender_ui.py | 472 ++++++++++++++++++ .../blender/exp/gltf2_blender_export_keys.py | 1 - .../blender/exp/gltf2_blender_gather.py | 2 + .../blender/exp/gltf2_blender_gather_image.py | 95 +++- .../exp/gltf2_blender_gather_materials.py | 260 +++------- ...ltf2_blender_gather_materials_clearcoat.py | 81 +++ ...gltf2_blender_gather_materials_emission.py | 61 +++ .../exp/gltf2_blender_gather_materials_ior.py | 35 ++ ...gather_materials_pbr_metallic_roughness.py | 8 +- .../gltf2_blender_gather_materials_sheen.py | 68 +++ ...gltf2_blender_gather_materials_specular.py | 168 +++++++ ...2_blender_gather_materials_transmission.py | 47 ++ .../gltf2_blender_gather_materials_unlit.py | 6 +- ...gltf2_blender_gather_materials_variants.py | 18 + .../gltf2_blender_gather_materials_volume.py | 75 +++ .../exp/gltf2_blender_gather_primitives.py | 56 ++- .../exp/gltf2_blender_gather_texture.py | 17 +- .../exp/gltf2_blender_gather_texture_info.py | 44 +- .../blender/exp/gltf2_blender_gather_tree.py | 17 + .../blender/exp/gltf2_blender_get.py | 41 +- .../blender/exp/gltf2_blender_image.py | 46 +- .../exp/gltf2_blender_texture_specular.py | 94 ++++ .../imp/gltf2_blender_KHR_materials_ior.py | 12 + ...der_KHR_materials_pbrSpecularGlossiness.py | 17 +- .../imp/gltf2_blender_KHR_materials_sheen.py | 88 ++++ .../gltf2_blender_KHR_materials_specular.py | 350 +++++++++++++ ...ltf2_blender_KHR_materials_transmission.py | 64 +++ .../imp/gltf2_blender_KHR_materials_unlit.py | 5 +- .../imp/gltf2_blender_KHR_materials_volume.py | 82 +++ .../blender/imp/gltf2_blender_gltf.py | 28 ++ .../blender/imp/gltf2_blender_material.py | 5 + .../blender/imp/gltf2_blender_mesh.py | 56 ++- .../imp/gltf2_blender_pbrMetallicRoughness.py | 238 ++++++++- .../blender/imp/gltf2_blender_texture.py | 19 +- io_scene_gltf2/io/com/gltf2_io_constants.py | 2 + io_scene_gltf2/io/com/gltf2_io_variants.py | 29 ++ io_scene_gltf2/io/imp/gltf2_io_gltf.py | 9 +- 40 files changed, 2558 insertions(+), 290 deletions(-) create mode 100644 io_scene_gltf2/blender/com/gltf2_blender_default.py create mode 100644 io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_clearcoat.py create mode 100644 io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_emission.py create mode 100644 io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_ior.py create mode 100644 io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_sheen.py create mode 100644 io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_specular.py create mode 100644 io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_transmission.py create mode 100644 io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_variants.py create mode 100644 io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_volume.py create mode 100644 io_scene_gltf2/blender/exp/gltf2_blender_texture_specular.py create mode 100644 io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_ior.py create mode 100644 io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_sheen.py create mode 100644 io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_specular.py create mode 100644 io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_transmission.py create mode 100644 io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_volume.py create mode 100644 io_scene_gltf2/io/com/gltf2_io_variants.py diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index bf23d0171..358759fa0 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -4,7 +4,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": (3, 3, 12), + "version": (3, 3, 13), 'blender': (3, 3, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', @@ -259,6 +259,14 @@ class ExportGLTF2_Base: default='EXPORT' ) + export_original_specular: BoolProperty( + name='Export original PBR Specular', + description=( + 'Export original glTF PBR Specular, instead of Blender Principled Shader Specular' + ), + default=False, + ) + export_colors: BoolProperty( name='Vertex Colors', description='Export vertex colors with meshes', @@ -447,13 +455,6 @@ class ExportGLTF2_Base: default=False ) - export_displacement: BoolProperty( - name='Displacement Textures (EXPERIMENTAL)', - description='EXPERIMENTAL: Export displacement textures. ' - 'Uses incomplete "KHR_materials_displacement" glTF extension', - default=False - ) - will_save_settings: BoolProperty( name='Remember Export Settings', description='Store glTF export settings in the Blender project', @@ -564,6 +565,7 @@ class ExportGLTF2_Base: export_settings['gltf_colors'] = self.export_colors export_settings['gltf_cameras'] = self.export_cameras + export_settings['gltf_original_specular'] = self.export_original_specular export_settings['gltf_visible'] = self.use_visible export_settings['gltf_renderable'] = self.use_renderable @@ -611,7 +613,6 @@ class ExportGLTF2_Base: export_settings['gltf_morph_tangent'] = False export_settings['gltf_lights'] = self.export_lights - export_settings['gltf_displacement'] = self.export_displacement export_settings['gltf_binary'] = bytearray() export_settings['gltf_binaryfilename'] = ( @@ -756,6 +757,22 @@ class GLTF_PT_export_geometry(bpy.types.Panel): return operator.bl_idname == "EXPORT_SCENE_OT_gltf" + def draw(self, context): + pass + +class GLTF_PT_export_geometry_mesh(bpy.types.Panel): + bl_space_type = 'FILE_BROWSER' + bl_region_type = 'TOOL_PROPS' + bl_label = "Mesh" + bl_parent_id = "GLTF_PT_export_geometry" + bl_options = {'DEFAULT_CLOSED'} + + @classmethod + def poll(cls, context): + 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 @@ -776,11 +793,56 @@ class GLTF_PT_export_geometry(bpy.types.Panel): col.prop(operator, 'use_mesh_edges') col.prop(operator, 'use_mesh_vertices') + +class GLTF_PT_export_geometry_material(bpy.types.Panel): + bl_space_type = 'FILE_BROWSER' + bl_region_type = 'TOOL_PROPS' + bl_label = "Material" + bl_parent_id = "GLTF_PT_export_geometry" + bl_options = {'DEFAULT_CLOSED'} + + @classmethod + def poll(cls, context): + 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 + layout.prop(operator, 'export_materials') col = layout.column() col.active = operator.export_materials == "EXPORT" col.prop(operator, 'export_image_format') +class GLTF_PT_export_geometry_original_pbr(bpy.types.Panel): + bl_space_type = 'FILE_BROWSER' + bl_region_type = 'TOOL_PROPS' + bl_label = "PBR Extensions" + bl_parent_id = "GLTF_PT_export_geometry_material" + bl_options = {'DEFAULT_CLOSED'} + + @classmethod + def poll(cls, context): + 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 + + layout.prop(operator, 'export_original_specular') + class GLTF_PT_export_geometry_compression(bpy.types.Panel): bl_space_type = 'FILE_BROWSER' @@ -1182,6 +1244,14 @@ class ImportGLTF2(Operator, ImportHelper): self.loglevel = logging.NOTSET +def gltf_variant_ui_update(self, context): + from .blender.com.gltf2_blender_ui import variant_register, variant_unregister + if self.KHR_materials_variants_ui is True: + # register all needed types + variant_register() + else: + variant_unregister() + class GLTF_AddonPreferences(bpy.types.AddonPreferences): bl_idname = __package__ @@ -1190,11 +1260,18 @@ class GLTF_AddonPreferences(bpy.types.AddonPreferences): description="Displays glTF Settings node in Shader Editor (Menu Add > Output)" ) + KHR_materials_variants_ui : bpy.props.BoolProperty( + default= False, + description="Displays glTF UI to manage material variants", + update=gltf_variant_ui_update + ) + def draw(self, context): layout = self.layout row = layout.row() row.prop(self, "settings_node_ui", text="Shader Editor Add-ons") + row.prop(self, "KHR_materials_variants_ui", text="Material Variants") def menu_func_import(self, context): self.layout.operator(ImportGLTF2.bl_idname, text='glTF 2.0 (.glb/.gltf)') @@ -1206,6 +1283,9 @@ classes = ( GLTF_PT_export_include, GLTF_PT_export_transform, GLTF_PT_export_geometry, + GLTF_PT_export_geometry_mesh, + GLTF_PT_export_geometry_material, + GLTF_PT_export_geometry_original_pbr, GLTF_PT_export_geometry_compression, GLTF_PT_export_animation, GLTF_PT_export_animation_export, @@ -1225,6 +1305,8 @@ def register(): # bpy.utils.register_module(__name__) blender_ui.register() + if bpy.context.preferences.addons['io_scene_gltf2'].preferences.KHR_materials_variants_ui is True: + blender_ui.variant_register() # add to the export / import menu bpy.types.TOPBAR_MT_file_export.append(menu_func_export) @@ -1233,6 +1315,10 @@ def register(): def unregister(): import io_scene_gltf2.blender.com.gltf2_blender_ui as blender_ui + blender_ui.unregister() + if bpy.context.preferences.addons['io_scene_gltf2'].preferences.KHR_materials_variants_ui is True: + blender_ui.variant_unregister() + for c in classes: bpy.utils.unregister_class(c) for f in exporter_extension_panel_unregister_functors: @@ -1243,8 +1329,6 @@ def unregister(): f() importer_extension_panel_unregister_functors.clear() - blender_ui.unregister() - # bpy.utils.unregister_module(__name__) # remove from the export / import menu diff --git a/io_scene_gltf2/blender/com/gltf2_blender_default.py b/io_scene_gltf2/blender/com/gltf2_blender_default.py new file mode 100644 index 000000000..c3951f4e0 --- /dev/null +++ b/io_scene_gltf2/blender/com/gltf2_blender_default.py @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2018-2022 The glTF-Blender-IO authors. + +BLENDER_IOR = 1.45 +BLENDER_SPECULAR = 0.5 +BLENDER_SPECULAR_TINT = 0.0 \ No newline at end of file diff --git a/io_scene_gltf2/blender/com/gltf2_blender_material_helpers.py b/io_scene_gltf2/blender/com/gltf2_blender_material_helpers.py index 7b90b0a38..a44562a1d 100755 --- a/io_scene_gltf2/blender/com/gltf2_blender_material_helpers.py +++ b/io_scene_gltf2/blender/com/gltf2_blender_material_helpers.py @@ -9,7 +9,25 @@ def get_gltf_node_name(): def create_settings_group(name): gltf_node_group = bpy.data.node_groups.new(name, 'ShaderNodeTree') gltf_node_group.inputs.new("NodeSocketFloat", "Occlusion") + thicknessFactor = gltf_node_group.inputs.new("NodeSocketFloat", "Thickness") + thicknessFactor.default_value = 1.0 gltf_node_group.nodes.new('NodeGroupOutput') gltf_node_group_input = gltf_node_group.nodes.new('NodeGroupInput') gltf_node_group_input.location = -200, 0 - return gltf_node_group \ No newline at end of file + return gltf_node_group + +def get_gltf_pbr_non_converted_name(): + return "original glTF PBR data" + +def create_gltf_pbr_non_converted_group(name): + gltf_node_group = bpy.data.node_groups.new(name, 'ShaderNodeTree') + + specular = gltf_node_group.inputs.new("NodeSocketFloat", "specular glTF") + specular.default_value = 1.0 + specularColor = gltf_node_group.inputs.new("NodeSocketColor", "specularColor glTF") + specularColor.default_value = [1.0,1.0,1.0,1.0] + + gltf_node_group.nodes.new('NodeGroupOutput') + gltf_node_group_input = gltf_node_group.nodes.new('NodeGroupInput') + gltf_node_group_input.location = -400, 0 + return gltf_node_group \ No newline at end of file diff --git a/io_scene_gltf2/blender/com/gltf2_blender_ui.py b/io_scene_gltf2/blender/com/gltf2_blender_ui.py index 59c364fb5..7895d3d47 100644 --- a/io_scene_gltf2/blender/com/gltf2_blender_ui.py +++ b/io_scene_gltf2/blender/com/gltf2_blender_ui.py @@ -3,6 +3,9 @@ import bpy from ..com.gltf2_blender_material_helpers import get_gltf_node_name, create_settings_group +from ..com.gltf2_blender_material_helpers import get_gltf_pbr_non_converted_name, create_gltf_pbr_non_converted_group + +################ glTF Settings node ########################################### def create_gltf_ao_group(operator, group_name): @@ -40,10 +43,479 @@ def add_gltf_settings_to_menu(self, context) : if bpy.context.preferences.addons['io_scene_gltf2'].preferences.settings_node_ui is True: self.layout.operator("node.gltf_settings_node_operator") +class NODE_OT_GLTF_PBR_NON_CONVERTED_EXTENSIONS(bpy.types.Operator): + bl_idname = "node.gltf_pbr_non_converted_extensions_operator" + bl_label = "glTF PBR Non Converted Extensions" + + + @classmethod + def poll(cls, context): + space = context.space_data + return space.type == "NODE_EDITOR" \ + and context.object and context.object.active_material \ + and context.object.active_material.use_nodes is True \ + and bpy.context.preferences.addons['io_scene_gltf2'].preferences.settings_node_ui is True + + def execute(self, context): + gltf_node_name = get_gltf_pbr_non_converted_name() + if gltf_node_name in bpy.data.node_groups: + my_group = bpy.data.node_groups[get_gltf_pbr_non_converted_name()] + else: + my_group = create_gltf_pbr_non_converted_group(gltf_node_name) + node_tree = context.object.active_material.node_tree + new_node = node_tree.nodes.new("ShaderNodeGroup") + new_node.node_tree = bpy.data.node_groups[my_group.name] + return {"FINISHED"} + + +def add_gltf_pbr_non_converted_extensions_to_menu(self, context) : + if bpy.context.preferences.addons['io_scene_gltf2'].preferences.settings_node_ui is True: + self.layout.operator("node.gltf_pbr_non_converted_extensions_operator") + + +################################### KHR_materials_variants #################### + +# Global UI panel + +class gltf2_KHR_materials_variants_variant(bpy.types.PropertyGroup): + variant_idx : bpy.props.IntProperty() + name : bpy.props.StringProperty(name="Variant Name") + +class SCENE_UL_gltf2_variants(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + + if self.layout_type in {'DEFAULT', 'COMPACT'}: + layout.prop(item, "name", text="", emboss=False) + + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + +class SCENE_PT_gltf2_variants(bpy.types.Panel): + bl_label = "glTF Material Variants" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = "glTF Variants" + + @classmethod + def poll(self, context): + return bpy.context.preferences.addons['io_scene_gltf2'].preferences.KHR_materials_variants_ui is True + + def draw(self, context): + layout = self.layout + row = layout.row() + + if bpy.data.scenes[0].get('gltf2_KHR_materials_variants_variants') and len(bpy.data.scenes[0].gltf2_KHR_materials_variants_variants) > 0: + + row.template_list("SCENE_UL_gltf2_variants", "", bpy.data.scenes[0], "gltf2_KHR_materials_variants_variants", bpy.data.scenes[0], "gltf2_active_variant") + col = row.column() + row = col.column(align=True) + row.operator("scene.gltf2_variant_add", icon="ADD", text="") + row.operator("scene.gltf2_variant_remove", icon="REMOVE", text="") + + row = layout.row() + row.operator("scene.gltf2_display_variant", text="Display Variant") + row = layout.row() + row.operator("scene.gltf2_assign_to_variant", text="Assign To Variant") + row = layout.row() + row.operator("scene.gltf2_reset_to_original", text="Reset To Original") + row.operator("scene.gltf2_assign_as_original", text="Assign as Original") + else: + row.operator("scene.gltf2_variant_add", text="Add Material Variant") + +class SCENE_OT_gltf2_variant_add(bpy.types.Operator): + """Add a new Material Variant""" + bl_idname = "scene.gltf2_variant_add" + bl_label = "Add Material Variant" + bl_options = {'REGISTER'} + + @classmethod + def poll(self, context): + return True + + def execute(self, context): + var = bpy.data.scenes[0].gltf2_KHR_materials_variants_variants.add() + var.variant_idx = len(bpy.data.scenes[0].gltf2_KHR_materials_variants_variants) - 1 + var.name = "VariantName" + bpy.data.scenes[0].gltf2_active_variant = len(bpy.data.scenes[0].gltf2_KHR_materials_variants_variants) - 1 + return {'FINISHED'} + +class SCENE_OT_gltf2_variant_remove(bpy.types.Operator): + """Add a new Material Variant""" + bl_idname = "scene.gltf2_variant_remove" + bl_label = "Remove Variant" + bl_options = {'REGISTER'} + + @classmethod + def poll(self, context): + return len(bpy.data.scenes[0].gltf2_KHR_materials_variants_variants) > 0 + + def execute(self, context): + bpy.data.scenes[0].gltf2_KHR_materials_variants_variants.remove(bpy.data.scenes[0].gltf2_active_variant) + + # loop on all mesh + for obj in [o for o in bpy.data.objects if o.type == "MESH"]: + mesh = obj.data + remove_idx_data = [] + for idx, i in enumerate(mesh.gltf2_variant_mesh_data): + remove_idx_variants = [] + for idx_var, v in enumerate(i.variants): + if v.variant.variant_idx == bpy.data.scenes[0].gltf2_active_variant: + remove_idx_variants.append(idx_var) + elif v.variant.variant_idx > bpy.data.scenes[0].gltf2_active_variant: + v.variant.variant_idx -= 1 + + if len(remove_idx_variants) > 0: + for idx_var in remove_idx_variants: + i.variants.remove(idx_var) + + if len(i.variants) == 0: + remove_idx_data.append(idx) + + if len(remove_idx_data) > 0: + for idx_data in remove_idx_data: + mesh.gltf2_variant_mesh_data.remove(idx_data) + + return {'FINISHED'} + + +# Operator to display a variant +class SCENE_OT_gltf2_display_variant(bpy.types.Operator): + bl_idname = "scene.gltf2_display_variant" + bl_label = "Display Variant" + bl_options = {'REGISTER'} + + + @classmethod + def poll(self, context): + return len(bpy.data.scenes[0].gltf2_KHR_materials_variants_variants) > 0 + + def execute(self, context): + + gltf2_active_variant = bpy.data.scenes[0].gltf2_active_variant + + # loop on all mesh + for obj in [o for o in bpy.data.objects if o.type == "MESH"]: + mesh = obj.data + for i in mesh.gltf2_variant_mesh_data: + if i.variants and gltf2_active_variant in [v.variant.variant_idx for v in i.variants]: + mat = i.material + slot = i.material_slot_index + if slot < len(obj.material_slots): # Seems user remove some slots... + obj.material_slots[slot].material = mat + + return {'FINISHED'} + +# Operator to assign current mesh materials to a variant +class SCENE_OT_gltf2_assign_to_variant(bpy.types.Operator): + bl_idname = "scene.gltf2_assign_to_variant" + bl_label = "Assign To Variant" + bl_options = {'REGISTER'} + + @classmethod + def poll(self, context): + return len(bpy.data.scenes[0].gltf2_KHR_materials_variants_variants) > 0 \ + and bpy.context.object.type == "MESH" + + def execute(self, context): + gltf2_active_variant = bpy.data.scenes[0].gltf2_active_variant + obj = bpy.context.object + + # loop on material slots ( primitives ) + for mat_slot_idx, s in enumerate(obj.material_slots): + # Check if there is already data for this slot + found = False + for i in obj.data.gltf2_variant_mesh_data: + if i.material_slot_index == mat_slot_idx and i.material == s.material: + found = True + variant_primitive = i + + if found is False: + variant_primitive = obj.data.gltf2_variant_mesh_data.add() + variant_primitive.material_slot_index = mat_slot_idx + variant_primitive.material = s.material + + vari = variant_primitive.variants.add() + vari.variant.variant_idx = bpy.data.scenes[0].gltf2_active_variant + + return {'FINISHED'} + +# Operator to reset mesh to orignal (using default material when exists) +class SCENE_OT_gltf2_reset_to_original(bpy.types.Operator): + bl_idname = "scene.gltf2_reset_to_original" + bl_label = "Reset to Orignal" + bl_options = {'REGISTER'} + + @classmethod + def poll(self, context): + return bpy.context.object.type == "MESH" and len(context.object.data.gltf2_variant_default_materials) > 0 + + def execute(self, context): + obj = bpy.context.object + + # loop on material slots ( primitives ) + for mat_slot_idx, s in enumerate(obj.material_slots): + # Check if there is a default material for this slot + found = False + for i in obj.data.gltf2_variant_default_materials: + if i.material_slot_index == mat_slot_idx: + s.material = i.default_material + break + + return {'FINISHED'} + +# Operator to assign current materials as default materials +class SCENE_OT_gltf2_assign_as_original(bpy.types.Operator): + bl_idname = "scene.gltf2_assign_as_original" + bl_label = "Assign as Original" + bl_options = {'REGISTER'} + + @classmethod + def poll(self, context): + return bpy.context.object.type == "MESH" + + def execute(self, context): + obj = bpy.context.object + + # loop on material slots ( primitives ) + for mat_slot_idx, s in enumerate(obj.material_slots): + # Check if there is a default material for this slot + found = False + for i in obj.data.gltf2_variant_default_materials: + if i.material_slot_index == mat_slot_idx: + found = True + # Update if needed + i.default_material = s.material + break + + if found is False: + default_mat = obj.data.gltf2_variant_default_materials.add() + default_mat.material_slot_index = mat_slot_idx + default_mat.default_material = s.material + + return {'FINISHED'} + +# Mesh Panel + +class gltf2_KHR_materials_variant_pointer(bpy.types.PropertyGroup): + variant: bpy.props.PointerProperty(type=gltf2_KHR_materials_variants_variant) + +class gltf2_KHR_materials_variants_default_material(bpy.types.PropertyGroup): + material_slot_index: bpy.props.IntProperty(name="Material Slot Index") + default_material: bpy.props.PointerProperty(type=bpy.types.Material) + +class gltf2_KHR_materials_variants_primitive(bpy.types.PropertyGroup): + material_slot_index : bpy.props.IntProperty(name="Material Slot Index") + material: bpy.props.PointerProperty(type=bpy.types.Material) + variants: bpy.props.CollectionProperty(type=gltf2_KHR_materials_variant_pointer) + active_variant_idx: bpy.props.IntProperty() + +class MESH_UL_gltf2_mesh_variants(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + + vari = item.variant + layout.context_pointer_set("id", vari) + + if self.layout_type in {'DEFAULT', 'COMPACT'}: + layout.prop(bpy.data.scenes[0].gltf2_KHR_materials_variants_variants[vari.variant_idx], "name", text="", emboss=False) + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + +class MESH_PT_gltf2_mesh_variants(bpy.types.Panel): + bl_label = "glTF Material Variants" + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = "material" + + @classmethod + def poll(self, context): + return bpy.context.preferences.addons['io_scene_gltf2'].preferences.KHR_materials_variants_ui is True \ + and len(bpy.context.object.material_slots) > 0 + + def draw(self, context): + layout = self.layout + + active_material_slots = bpy.context.object.active_material_index + + found = False + if 'gltf2_variant_mesh_data' in bpy.context.object.data.keys(): + for idx, prim in enumerate(bpy.context.object.data.gltf2_variant_mesh_data): + if prim.material_slot_index == active_material_slots and id(prim.material) == id(bpy.context.object.material_slots[active_material_slots].material): + found = True + break + + row = layout.row() + if found is True: + row.template_list("MESH_UL_gltf2_mesh_variants", "", prim, "variants", prim, "active_variant_idx") + col = row.column() + row = col.column(align=True) + row.operator("scene.gltf2_variants_slot_add", icon="ADD", text="") + row.operator("scene.gltf2_remove_material_variant", icon="REMOVE", text="") + + row = layout.row() + if 'gltf2_KHR_materials_variants_variants' in bpy.data.scenes[0].keys() and len(bpy.data.scenes[0].gltf2_KHR_materials_variants_variants) > 0: + row.prop_search(context.object.data, "gltf2_variant_pointer", bpy.data.scenes[0], "gltf2_KHR_materials_variants_variants", text="Variant") + row = layout.row() + row.operator("scene.gltf2_material_to_variant", text="Assign To Variant") + else: + row.label(text="Please Create a Variant First") + else: + if 'gltf2_KHR_materials_variants_variants' in bpy.data.scenes[0].keys() and len(bpy.data.scenes[0].gltf2_KHR_materials_variants_variants) > 0: + row.operator("scene.gltf2_variants_slot_add", text="Add a new Variant Slot") + else: + row.label(text="Please Create a Variant First") + + +class SCENE_OT_gltf2_variant_slot_add(bpy.types.Operator): + """Add a new Slot""" + bl_idname = "scene.gltf2_variants_slot_add" + bl_label = "Add new Slot" + bl_options = {'REGISTER'} + + @classmethod + def poll(self, context): + return len(bpy.context.object.material_slots) > 0 + + def execute(self, context): + mesh = context.object.data + # Check if there is already a data for this slot_idx + material + + found = False + for i in mesh.gltf2_variant_mesh_data: + if i.material_slot_index == context.object.active_material_index and i.material == context.object.material_slots[context.object.active_material_index].material: + found = True + variant_primitive = i + + if found is False: + variant_primitive = mesh.gltf2_variant_mesh_data.add() + variant_primitive.material_slot_index = context.object.active_material_index + variant_primitive.material = context.object.material_slots[context.object.active_material_index].material + + vari = variant_primitive.variants.add() + vari.variant.variant_idx = bpy.data.scenes[0].gltf2_active_variant + + return {'FINISHED'} + +class SCENE_OT_gltf2_material_to_variant(bpy.types.Operator): + """Assign Variant to Slot""" + bl_idname = "scene.gltf2_material_to_variant" + bl_label = "Assign Material To Variant" + bl_options = {'REGISTER'} + + @classmethod + def poll(self, context): + return len(bpy.context.object.material_slots) > 0 and context.object.data.gltf2_variant_pointer != "" + + def execute(self, context): + mesh = context.object.data + + found = False + for i in mesh.gltf2_variant_mesh_data: + if i.material_slot_index == context.object.active_material_index and i.material == context.object.material_slots[context.object.active_material_index].material: + found = True + variant_primitive = i + + if found is False: + return {'CANCELLED'} + + vari = variant_primitive.variants[variant_primitive.active_variant_idx] + + # Retrieve variant idx + found = False + for v in bpy.data.scenes[0].gltf2_KHR_materials_variants_variants: + if v.name == context.object.data.gltf2_variant_pointer: + found = True + break + + if found is False: + return {'CANCELLED'} + + vari.variant.variant_idx = v.variant_idx + + return {'FINISHED'} + +class SCENE_OT_gltf2_remove_material_variant(bpy.types.Operator): + """Remove a variant Slot""" + bl_idname = "scene.gltf2_remove_material_variant" + bl_label = "Remove a variant Slot" + bl_options = {'REGISTER'} + + @classmethod + def poll(self, context): + return len(bpy.context.object.material_slots) > 0 and len(bpy.context.object.data.gltf2_variant_mesh_data) > 0 + + def execute(self, context): + mesh = context.object.data + + found = False + found_idx = -1 + for idx, i in enumerate(mesh.gltf2_variant_mesh_data): + if i.material_slot_index == context.object.active_material_index and i.material == context.object.material_slots[context.object.active_material_index].material: + found = True + variant_primitive = i + found_idx = idx + + if found is False: + return {'CANCELLED'} + + variant_primitive.variants.remove(variant_primitive.active_variant_idx) + + if len(variant_primitive.variants) == 0: + mesh.gltf2_variant_mesh_data.remove(found_idx) + + return {'FINISHED'} + + +############################################################################### def register(): bpy.utils.register_class(NODE_OT_GLTF_SETTINGS) + bpy.utils.register_class(NODE_OT_GLTF_PBR_NON_CONVERTED_EXTENSIONS) bpy.types.NODE_MT_category_SH_NEW_OUTPUT.append(add_gltf_settings_to_menu) + bpy.types.NODE_MT_category_SH_NEW_OUTPUT.append(add_gltf_pbr_non_converted_extensions_to_menu) + +def variant_register(): + bpy.utils.register_class(SCENE_OT_gltf2_display_variant) + bpy.utils.register_class(SCENE_OT_gltf2_assign_to_variant) + bpy.utils.register_class(SCENE_OT_gltf2_reset_to_original) + bpy.utils.register_class(SCENE_OT_gltf2_assign_as_original) + bpy.utils.register_class(SCENE_OT_gltf2_remove_material_variant) + bpy.utils.register_class(gltf2_KHR_materials_variants_variant) + bpy.utils.register_class(gltf2_KHR_materials_variant_pointer) + bpy.utils.register_class(gltf2_KHR_materials_variants_primitive) + bpy.utils.register_class(gltf2_KHR_materials_variants_default_material) + bpy.utils.register_class(SCENE_UL_gltf2_variants) + bpy.utils.register_class(SCENE_PT_gltf2_variants) + bpy.utils.register_class(MESH_UL_gltf2_mesh_variants) + bpy.utils.register_class(MESH_PT_gltf2_mesh_variants) + bpy.utils.register_class(SCENE_OT_gltf2_variant_add) + bpy.utils.register_class(SCENE_OT_gltf2_variant_remove) + bpy.utils.register_class(SCENE_OT_gltf2_material_to_variant) + bpy.utils.register_class(SCENE_OT_gltf2_variant_slot_add) + bpy.types.Mesh.gltf2_variant_mesh_data = bpy.props.CollectionProperty(type=gltf2_KHR_materials_variants_primitive) + bpy.types.Mesh.gltf2_variant_default_materials = bpy.props.CollectionProperty(type=gltf2_KHR_materials_variants_default_material) + bpy.types.Mesh.gltf2_variant_pointer = bpy.props.StringProperty() + bpy.types.Scene.gltf2_KHR_materials_variants_variants = bpy.props.CollectionProperty(type=gltf2_KHR_materials_variants_variant) + bpy.types.Scene.gltf2_active_variant = bpy.props.IntProperty() def unregister(): bpy.utils.unregister_class(NODE_OT_GLTF_SETTINGS) + bpy.utils.unregister_class(NODE_OT_GLTF_PBR_NON_CONVERTED_EXTENSIONS) + +def variant_unregister(): + bpy.utils.unregister_class(SCENE_OT_gltf2_variant_add) + bpy.utils.unregister_class(SCENE_OT_gltf2_variant_remove) + bpy.utils.unregister_class(SCENE_OT_gltf2_material_to_variant) + bpy.utils.unregister_class(SCENE_OT_gltf2_variant_slot_add) + bpy.utils.unregister_class(SCENE_OT_gltf2_display_variant) + bpy.utils.unregister_class(SCENE_OT_gltf2_assign_to_variant) + bpy.utils.unregister_class(SCENE_OT_gltf2_reset_to_original) + bpy.utils.unregister_class(SCENE_OT_gltf2_assign_as_original) + bpy.utils.unregister_class(SCENE_OT_gltf2_remove_material_variant) + bpy.utils.unregister_class(SCENE_PT_gltf2_variants) + bpy.utils.unregister_class(SCENE_UL_gltf2_variants) + bpy.utils.unregister_class(MESH_PT_gltf2_mesh_variants) + bpy.utils.unregister_class(MESH_UL_gltf2_mesh_variants) + bpy.utils.unregister_class(gltf2_KHR_materials_variants_default_material) + bpy.utils.unregister_class(gltf2_KHR_materials_variants_primitive) + bpy.utils.unregister_class(gltf2_KHR_materials_variants_variant) + bpy.utils.unregister_class(gltf2_KHR_materials_variant_pointer) diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_export_keys.py b/io_scene_gltf2/blender/exp/gltf2_blender_export_keys.py index 812db3f99..96f97af14 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_export_keys.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_export_keys.py @@ -20,7 +20,6 @@ RENDERABLE = 'gltf_renderable' ACTIVE_COLLECTION = 'gltf_active_collection' SKINS = 'gltf_skins' DEF_BONES_ONLY = 'gltf_def_bones' -DISPLACEMENT = 'gltf_displacement' FORCE_SAMPLING = 'gltf_force_sampling' FRAME_RANGE = 'gltf_frame_range' FRAME_STEP = 'gltf_frame_step' diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather.py index 4eb8baed4..721ef1157 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather.py @@ -59,6 +59,8 @@ def __gather_scene(blender_scene, export_settings): # Now, we can filter tree if needed vtree.filter() + vtree.variants_reset_to_original() + export_user_extensions('vtree_after_filter_hook', export_settings, vtree) export_settings['vtree'] = vtree 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 0dfce9f9b..69900c1be 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py @@ -11,7 +11,7 @@ from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree from io_scene_gltf2.io.exp import gltf2_io_binary_data from io_scene_gltf2.io.exp import gltf2_io_image_data from io_scene_gltf2.io.com import gltf2_io_debug -from io_scene_gltf2.blender.exp.gltf2_blender_image import Channel, ExportImage, FillImage +from io_scene_gltf2.blender.exp.gltf2_blender_image import Channel, ExportImage, FillImage, StoreImage, StoreData from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions @@ -21,26 +21,31 @@ def gather_image( blender_shader_sockets: typing.Tuple[bpy.types.NodeSocket], export_settings): if not __filter_image(blender_shader_sockets, export_settings): - return None + return None, None image_data = __get_image_data(blender_shader_sockets, export_settings) if image_data.empty(): # The export image has no data - return None + return None, None mime_type = __gather_mime_type(blender_shader_sockets, image_data, export_settings) name = __gather_name(image_data, export_settings) + factor = None + if image_data.original is None: - uri = __gather_uri(image_data, mime_type, name, export_settings) + uri, factor_uri = __gather_uri(image_data, mime_type, name, export_settings) else: # Retrieve URI relative to exported glTF files uri = __gather_original_uri(image_data.original.filepath, export_settings) # In case we can't retrieve image (for example packed images, with original moved) # We don't create invalid image without uri + factor_uri = None if uri is None: return None - buffer_view = __gather_buffer_view(image_data, mime_type, name, export_settings) + buffer_view, factor_buffer_view = __gather_buffer_view(image_data, mime_type, name, export_settings) + + factor = factor_uri if uri is not None else factor_buffer_view image = __make_image( buffer_view, @@ -54,7 +59,7 @@ def gather_image( export_user_extensions('gather_image_hook', export_settings, image, blender_shader_sockets) - return image + return image, factor def __gather_original_uri(original_uri, export_settings): @@ -98,8 +103,9 @@ def __filter_image(sockets, export_settings): @cached def __gather_buffer_view(image_data, mime_type, name, export_settings): if export_settings[gltf2_blender_export_keys.FORMAT] != 'GLTF_SEPARATE': - return gltf2_io_binary_data.BinaryData(data=image_data.encode(mime_type)) - return None + data, factor = image_data.encode(mime_type) + return gltf2_io_binary_data.BinaryData(data=data), factor + return None, None def __gather_extensions(sockets, export_settings): @@ -165,13 +171,14 @@ def __gather_name(export_image, export_settings): def __gather_uri(image_data, mime_type, name, export_settings): if export_settings[gltf2_blender_export_keys.FORMAT] == 'GLTF_SEPARATE': # as usual we just store the data in place instead of already resolving the references + data, factor = image_data.encode(mime_type=mime_type) return gltf2_io_image_data.ImageData( - data=image_data.encode(mime_type=mime_type), + data=data, mime_type=mime_type, name=name - ) + ), factor - return None + return None, None def __get_image_data(sockets, export_settings) -> ExportImage: @@ -179,6 +186,18 @@ def __get_image_data(sockets, export_settings) -> ExportImage: # 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] + + # Check if we need a simple mapping or more complex calculation + if any([socket.name == "Specular" for socket in sockets]): + return __get_image_data_specular(sockets, results, export_settings) + else: + return __get_image_data_mapping(sockets, results, export_settings) + +def __get_image_data_mapping(sockets, results, export_settings) -> ExportImage: + """ + Simple mapping + Will fit for most of exported textures : RoughnessMetallic, Basecolor, normal, ... + """ composed_image = ExportImage() for result, socket in zip(results, sockets): # Assume that user know what he does, and that channels/images are already combined correctly for pbr @@ -217,6 +236,12 @@ def __get_image_data(sockets, export_settings) -> ExportImage: dst_chan = Channel.R elif socket.name == 'Clearcoat Roughness': dst_chan = Channel.G + elif socket.name == 'Thickness': # For KHR_materials_volume + dst_chan = Channel.G + elif socket.name == "specular glTF": # For original KHR_material_specular + dst_chan = Channel.A + elif socket.name == "Sigma": # For KHR_materials_sheen + dst_chan = Channel.A if dst_chan is not None: composed_image.fill_image(result.shader_node.image, dst_chan, src_chan) @@ -243,6 +268,54 @@ def __get_image_data(sockets, export_settings) -> ExportImage: return composed_image +def __get_image_data_specular(sockets, results, export_settings) -> ExportImage: + """ + calculating Specular Texture, settings needed data + """ + from io_scene_gltf2.blender.exp.gltf2_blender_texture_specular import specular_calculation + composed_image = ExportImage() + composed_image.set_calc(specular_calculation) + + 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 + + mapping = { + 0: "specular", + 1: "specular_tint", + 2: "base_color", + 3: "transmission" + } + + for idx, result in enumerate(results): + if __get_tex_from_socket(sockets[idx], export_settings): + + composed_image.store_data(mapping[idx], result.shader_node.image, type="Image") + + # rudimentarily try follow the node tree to find the correct image data. + src_chan = None if idx == 2 else Channel.R + for elem in result.path: + if isinstance(elem.from_node, bpy.types.ShaderNodeSeparateColor): + src_chan = { + 'Red': Channel.R, + 'Green': Channel.G, + 'Blue': Channel.B, + }[elem.from_socket.name] + if elem.from_socket.name == 'Alpha': + src_chan = Channel.A + # For base_color, keep all channels, as this is a Vec, not scalar + if idx != 2: + composed_image.store_data(mapping[idx] + "_channel", src_chan, type="Data") + else: + if src_chan is not None: + composed_image.store_data(mapping[idx] + "_channel", src_chan, type="Data") + + else: + composed_image.store_data(mapping[idx], sockets[idx].default_value, type="Data") + + 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( 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 56c3acff3..c0d17fd38 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py @@ -1,21 +1,27 @@ # SPDX-License-Identifier: Apache-2.0 -# Copyright 2018-2021 The glTF-Blender-IO authors. +# Copyright 2018-2022 The glTF-Blender-IO authors. from copy import deepcopy import bpy from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached, cached_by_key from io_scene_gltf2.io.com import gltf2_io -from io_scene_gltf2.io.com.gltf2_io_extensions import Extension +from io_scene_gltf2.blender.exp import gltf2_blender_gather_materials_unlit 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_search_node_tree - from io_scene_gltf2.blender.exp import gltf2_blender_gather_materials_pbr_metallic_roughness -from io_scene_gltf2.blender.exp import gltf2_blender_gather_materials_unlit from ..com.gltf2_blender_extras import generate_extras from io_scene_gltf2.blender.exp import gltf2_blender_get from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions from io_scene_gltf2.io.com.gltf2_io_debug import print_console +from io_scene_gltf2.blender.exp.gltf2_blender_gather_materials_volume import export_volume +from io_scene_gltf2.blender.exp.gltf2_blender_gather_materials_emission import export_emission_factor, \ + export_emission_texture, export_emission_strength_extension +from io_scene_gltf2.blender.exp.gltf2_blender_gather_materials_sheen import export_sheen +from io_scene_gltf2.blender.exp.gltf2_blender_gather_materials_specular import export_specular +from io_scene_gltf2.blender.exp.gltf2_blender_gather_materials_transmission import export_transmission +from io_scene_gltf2.blender.exp.gltf2_blender_gather_materials_clearcoat import export_clearcoat +from io_scene_gltf2.blender.exp.gltf2_blender_gather_materials_ior import export_ior +from io_scene_gltf2.io.com.gltf2_io_extensions import Extension @cached def get_material_cache_key(blender_material, active_uvmap_index, export_settings): @@ -39,24 +45,31 @@ def gather_material(blender_material, active_uvmap_index, export_settings): if not __filter_material(blender_material, export_settings): return None - mat_unlit = __gather_material_unlit(blender_material, active_uvmap_index, export_settings) + mat_unlit = __export_unlit(blender_material, active_uvmap_index, export_settings) if mat_unlit is not None: export_user_extensions('gather_material_hook', export_settings, mat_unlit, blender_material) return mat_unlit orm_texture = __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, 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) 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: + # Strength is set on extension + emission_strength = max(emissive_factor) + emissive_factor = [f / emission_strength for f in emissive_factor] + + base_material = gltf2_io.Material( alpha_cutoff=__gather_alpha_cutoff(blender_material, export_settings), alpha_mode=__gather_alpha_mode(blender_material, export_settings), double_sided=__gather_double_sided(blender_material, export_settings), - emissive_factor=__gather_emissive_factor(blender_material, export_settings), + emissive_factor=emissive_factor, emissive_texture=emissive_texture, extensions=extensions, extras=__gather_extras(blender_material, export_settings), @@ -106,6 +119,14 @@ def gather_material(blender_material, active_uvmap_index, export_settings): material.extensions["KHR_materials_clearcoat"].extension['clearcoatNormalTexture'].tex_coord = active_uvmap_index elif tex == "transmissionTexture": #TODO not tested yet material.extensions["KHR_materials_transmission"].extension['transmissionTexture'].tex_coord = active_uvmap_index + elif tex == "specularTexture": + material.extensions["KHR_materials_specular"].extension['specularTexture'].tex_coord = active_uvmap_index + elif tex == "specularColorTexture": + material.extensions["KHR_materials_specular"].extension['specularColorTexture'].tex_coord = active_uvmap_index + elif tex == "sheenColorTexture": + material.extensions["KHR_materials_sheen"].extension['sheenColorTexture'].tex_coord = active_uvmap_index + elif tex == "sheenRoughnessTexture": + material.extensions["KHR_materials_sheen"].extension['sheenRoughnessTexture'].tex_coord = active_uvmap_index # If material is not using active UVMap, we need to return the same material, # Even if multiples meshes are using different active UVMap @@ -188,72 +209,61 @@ def __gather_double_sided(blender_material, export_settings): def __gather_emissive_factor(blender_material, export_settings): - emissive_socket = gltf2_blender_get.get_socket(blender_material, "Emissive") - if emissive_socket is None: - emissive_socket = gltf2_blender_get.get_socket_old(blender_material, "EmissiveFactor") - if isinstance(emissive_socket, bpy.types.NodeSocket): - if export_settings['gltf_image_format'] != "NONE": - factor = gltf2_blender_get.get_factor_from_socket(emissive_socket, kind='RGB') - else: - factor = gltf2_blender_get.get_const_from_default_value_socket(emissive_socket, kind='RGB') - - if factor is None and emissive_socket.is_linked: - # In glTF, the default emissiveFactor is all zeros, so if an emission texture is connected, - # we have to manually set it to all ones. - factor = [1.0, 1.0, 1.0] - - if factor is None: factor = [0.0, 0.0, 0.0] - - # Handle Emission Strength - strength_socket = None - if emissive_socket.node.type == 'EMISSION': - strength_socket = emissive_socket.node.inputs['Strength'] - elif 'Emission Strength' in emissive_socket.node.inputs: - strength_socket = emissive_socket.node.inputs['Emission Strength'] - strength = ( - gltf2_blender_get.get_const_from_socket(strength_socket, kind='VALUE') - if strength_socket is not None - else None - ) - if strength is not None: - factor = [f * strength for f in factor] - - # Clamp to range [0,1] - factor = [min(1.0, f) for f in factor] - - if factor == [0, 0, 0]: factor = None - - return factor - - return None - + return export_emission_factor(blender_material, export_settings) def __gather_emissive_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) - return emissive_texture, ["emissiveTexture"] if use_actives_uvmap_emissive else None + return export_emission_texture(blender_material, export_settings) -def __gather_extensions(blender_material, export_settings): +def __gather_extensions(blender_material, emissive_factor, export_settings): extensions = {} # KHR_materials_clearcoat actives_uvmaps = [] - clearcoat_extension, use_actives_uvmap_clearcoat = __gather_clearcoat_extension(blender_material, export_settings) + clearcoat_extension, use_actives_uvmap_clearcoat = export_clearcoat(blender_material, export_settings) if clearcoat_extension: extensions["KHR_materials_clearcoat"] = clearcoat_extension actives_uvmaps.extend(use_actives_uvmap_clearcoat) # KHR_materials_transmission - transmission_extension, use_actives_uvmap_transmission = __gather_transmission_extension(blender_material, export_settings) + transmission_extension, use_actives_uvmap_transmission = export_transmission(blender_material, export_settings) if transmission_extension: extensions["KHR_materials_transmission"] = transmission_extension actives_uvmaps.extend(use_actives_uvmap_transmission) + # KHR_materials_emissive_strength + if any([i>1.0 for i in emissive_factor or []]): + emissive_strength_extension = export_emission_strength_extension(emissive_factor, export_settings) + if emissive_strength_extension: + extensions["KHR_materials_emissive_strength"] = emissive_strength_extension + + # KHR_materials_volume + + volume_extension, use_actives_uvmap_volume_thickness = export_volume(blender_material, export_settings) + if volume_extension: + extensions["KHR_materials_volume"] = volume_extension + actives_uvmaps.extend(use_actives_uvmap_volume_thickness) + + # KHR_materials_specular + specular_extension, use_actives_uvmap_specular = export_specular(blender_material, export_settings) + if specular_extension: + extensions["KHR_materials_specular"] = specular_extension + actives_uvmaps.extend(use_actives_uvmap_specular) + + # KHR_materials_sheen + sheen_extension, use_actives_uvmap_sheen = export_sheen(blender_material, export_settings) + if sheen_extension: + extensions["KHR_materials_sheen"] = sheen_extension + actives_uvmaps.extend(use_actives_uvmap_sheen) + + # KHR_materials_ior + # Keep this extension at the end, because we export it only if some others are exported + ior_extension = export_ior(blender_material, extensions, export_settings) + if ior_extension: + extensions["KHR_materials_ior"] = ior_extension + return extensions, actives_uvmaps if extensions else None @@ -271,7 +281,7 @@ def __gather_normal_texture(blender_material, export_settings): normal = gltf2_blender_get.get_socket(blender_material, "Normal") if normal is None: normal = gltf2_blender_get.get_socket_old(blender_material, "Normal") - normal_texture, use_active_uvmap_normal = gltf2_blender_gather_texture_info.gather_material_normal_texture_info_class( + normal_texture, use_active_uvmap_normal, _ = gltf2_blender_gather_texture_info.gather_material_normal_texture_info_class( normal, (normal,), export_settings) @@ -283,20 +293,20 @@ def __gather_orm_texture(blender_material, export_settings): # If not fully shared, return None, so the images will be cached and processed separately. occlusion = gltf2_blender_get.get_socket(blender_material, "Occlusion") - if occlusion is None or not __has_image_node_from_socket(occlusion): + 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 __has_image_node_from_socket(occlusion): + if occlusion is None or not gltf2_blender_get.has_image_node_from_socket(occlusion): return None 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 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) 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 gltf2_blender_get.has_image_node_from_socket(metallic_roughness): return None result = (occlusion, metallic_roughness) elif not hasMetal: @@ -313,7 +323,7 @@ def __gather_orm_texture(blender_material, export_settings): 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, export_settings) if info is None: return None @@ -323,7 +333,7 @@ def __gather_occlusion_texture(blender_material, orm_texture, 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_texture, use_active_uvmap_occlusion, _ = gltf2_blender_gather_texture_info.gather_material_occlusion_texture_info_class( occlusion, orm_texture or (occlusion,), export_settings) @@ -336,129 +346,7 @@ def __gather_pbr_metallic_roughness(blender_material, orm_texture, export_settin orm_texture, export_settings) -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 __gather_clearcoat_extension(blender_material, export_settings): - clearcoat_enabled = False - has_clearcoat_texture = False - has_clearcoat_roughness_texture = False - - clearcoat_extension = {} - clearcoat_roughness_slots = () - - clearcoat_socket = gltf2_blender_get.get_socket(blender_material, 'Clearcoat') - clearcoat_roughness_socket = gltf2_blender_get.get_socket(blender_material, 'Clearcoat Roughness') - clearcoat_normal_socket = gltf2_blender_get.get_socket(blender_material, 'Clearcoat Normal') - - if isinstance(clearcoat_socket, bpy.types.NodeSocket) and not clearcoat_socket.is_linked: - clearcoat_extension['clearcoatFactor'] = clearcoat_socket.default_value - clearcoat_enabled = clearcoat_extension['clearcoatFactor'] > 0 - elif __has_image_node_from_socket(clearcoat_socket): - fac = gltf2_blender_get.get_factor_from_socket(clearcoat_socket, kind='VALUE') - # default value in glTF is 0.0, but if there is a texture without factor, use 1 - clearcoat_extension['clearcoatFactor'] = fac if fac != None else 1.0 - has_clearcoat_texture = True - clearcoat_enabled = True - - if not clearcoat_enabled: - return None, None - - if isinstance(clearcoat_roughness_socket, bpy.types.NodeSocket) and not clearcoat_roughness_socket.is_linked: - clearcoat_extension['clearcoatRoughnessFactor'] = clearcoat_roughness_socket.default_value - elif __has_image_node_from_socket(clearcoat_roughness_socket): - fac = gltf2_blender_get.get_factor_from_socket(clearcoat_roughness_socket, kind='VALUE') - # default value in glTF is 0.0, but if there is a texture without factor, use 1 - clearcoat_extension['clearcoatRoughnessFactor'] = fac if fac != None else 1.0 - has_clearcoat_roughness_texture = True - - # Pack clearcoat (R) and clearcoatRoughness (G) channels. - if has_clearcoat_texture and has_clearcoat_roughness_texture: - clearcoat_roughness_slots = (clearcoat_socket, clearcoat_roughness_socket,) - elif has_clearcoat_texture: - clearcoat_roughness_slots = (clearcoat_socket,) - elif has_clearcoat_roughness_texture: - clearcoat_roughness_slots = (clearcoat_roughness_socket,) - - use_actives_uvmaps = [] - - if len(clearcoat_roughness_slots) > 0: - if has_clearcoat_texture: - 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 - if clearcoat_texture_use_active_uvmap: - use_actives_uvmaps.append("clearcoatTexture") - if has_clearcoat_roughness_texture: - 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 - if clearcoat_roughness_texture_use_active_uvmap: - use_actives_uvmaps.append("clearcoatRoughnessTexture") - if __has_image_node_from_socket(clearcoat_normal_socket): - clearcoat_normal_texture, clearcoat_normal_texture_use_active_uvmap = gltf2_blender_gather_texture_info.gather_material_normal_texture_info_class( - clearcoat_normal_socket, - (clearcoat_normal_socket,), - export_settings - ) - clearcoat_extension['clearcoatNormalTexture'] = clearcoat_normal_texture - if clearcoat_normal_texture_use_active_uvmap: - use_actives_uvmaps.append("clearcoatNormalTexture") - - return Extension('KHR_materials_clearcoat', clearcoat_extension, False), use_actives_uvmaps - -def __gather_transmission_extension(blender_material, export_settings): - transmission_enabled = False - has_transmission_texture = False - - transmission_extension = {} - transmission_slots = () - - transmission_socket = gltf2_blender_get.get_socket(blender_material, 'Transmission') - - if isinstance(transmission_socket, bpy.types.NodeSocket) and not transmission_socket.is_linked: - transmission_extension['transmissionFactor'] = transmission_socket.default_value - transmission_enabled = transmission_extension['transmissionFactor'] > 0 - elif __has_image_node_from_socket(transmission_socket): - transmission_extension['transmissionFactor'] = 1.0 - has_transmission_texture = True - transmission_enabled = True - - if not transmission_enabled: - return None, None - - # Pack transmission channel (R). - if has_transmission_texture: - transmission_slots = (transmission_socket,) - - use_actives_uvmaps = [] - - if len(transmission_slots) > 0: - combined_texture, use_active_uvmap = gltf2_blender_gather_texture_info.gather_texture_info( - transmission_socket, - transmission_slots, - export_settings, - ) - if has_transmission_texture: - transmission_extension['transmissionTexture'] = combined_texture - if use_active_uvmap: - use_actives_uvmaps.append("transmissionTexture") - - return Extension('KHR_materials_transmission', transmission_extension, False), use_actives_uvmaps - - -def __gather_material_unlit(blender_material, active_uvmap_index, export_settings): +def __export_unlit(blender_material, active_uvmap_index, export_settings): gltf2_unlit = gltf2_blender_gather_materials_unlit info = gltf2_unlit.detect_shadeless_material(blender_material, export_settings) diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_clearcoat.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_clearcoat.py new file mode 100644 index 000000000..65c164b4b --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_clearcoat.py @@ -0,0 +1,81 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2018-2022 The glTF-Blender-IO authors. + +import bpy +from io_scene_gltf2.io.com.gltf2_io_extensions import Extension +from io_scene_gltf2.blender.exp import gltf2_blender_get +from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info + +def export_clearcoat(blender_material, export_settings): + clearcoat_enabled = False + has_clearcoat_texture = False + has_clearcoat_roughness_texture = False + + clearcoat_extension = {} + clearcoat_roughness_slots = () + + clearcoat_socket = gltf2_blender_get.get_socket(blender_material, 'Clearcoat') + clearcoat_roughness_socket = gltf2_blender_get.get_socket(blender_material, 'Clearcoat Roughness') + clearcoat_normal_socket = gltf2_blender_get.get_socket(blender_material, 'Clearcoat Normal') + + if isinstance(clearcoat_socket, bpy.types.NodeSocket) and not clearcoat_socket.is_linked: + clearcoat_extension['clearcoatFactor'] = clearcoat_socket.default_value + clearcoat_enabled = clearcoat_extension['clearcoatFactor'] > 0 + elif gltf2_blender_get.has_image_node_from_socket(clearcoat_socket): + fac = gltf2_blender_get.get_factor_from_socket(clearcoat_socket, kind='VALUE') + # default value in glTF is 0.0, but if there is a texture without factor, use 1 + clearcoat_extension['clearcoatFactor'] = fac if fac != None else 1.0 + has_clearcoat_texture = True + clearcoat_enabled = True + + if not clearcoat_enabled: + return None, None + + if isinstance(clearcoat_roughness_socket, bpy.types.NodeSocket) and not clearcoat_roughness_socket.is_linked: + clearcoat_extension['clearcoatRoughnessFactor'] = clearcoat_roughness_socket.default_value + elif gltf2_blender_get.has_image_node_from_socket(clearcoat_roughness_socket): + fac = gltf2_blender_get.get_factor_from_socket(clearcoat_roughness_socket, kind='VALUE') + # default value in glTF is 0.0, but if there is a texture without factor, use 1 + clearcoat_extension['clearcoatRoughnessFactor'] = fac if fac != None else 1.0 + has_clearcoat_roughness_texture = True + + # Pack clearcoat (R) and clearcoatRoughness (G) channels. + if has_clearcoat_texture and has_clearcoat_roughness_texture: + clearcoat_roughness_slots = (clearcoat_socket, clearcoat_roughness_socket,) + elif has_clearcoat_texture: + clearcoat_roughness_slots = (clearcoat_socket,) + elif has_clearcoat_roughness_texture: + clearcoat_roughness_slots = (clearcoat_roughness_socket,) + + use_actives_uvmaps = [] + + if len(clearcoat_roughness_slots) > 0: + if has_clearcoat_texture: + 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 + if clearcoat_texture_use_active_uvmap: + use_actives_uvmaps.append("clearcoatTexture") + if has_clearcoat_roughness_texture: + 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 + if clearcoat_roughness_texture_use_active_uvmap: + use_actives_uvmaps.append("clearcoatRoughnessTexture") + if gltf2_blender_get.has_image_node_from_socket(clearcoat_normal_socket): + clearcoat_normal_texture, clearcoat_normal_texture_use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_material_normal_texture_info_class( + clearcoat_normal_socket, + (clearcoat_normal_socket,), + export_settings + ) + clearcoat_extension['clearcoatNormalTexture'] = clearcoat_normal_texture + if clearcoat_normal_texture_use_active_uvmap: + use_actives_uvmaps.append("clearcoatNormalTexture") + + return Extension('KHR_materials_clearcoat', clearcoat_extension, False), use_actives_uvmaps \ No newline at end of file diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_emission.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_emission.py new file mode 100644 index 000000000..562fc19d9 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_emission.py @@ -0,0 +1,61 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2018-2022 The glTF-Blender-IO authors. + +import bpy +from io_scene_gltf2.io.com.gltf2_io_extensions import Extension +from io_scene_gltf2.blender.exp import gltf2_blender_get +from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info + +def export_emission_factor(blender_material, export_settings): + emissive_socket = gltf2_blender_get.get_socket(blender_material, "Emissive") + if emissive_socket is None: + emissive_socket = gltf2_blender_get.get_socket_old(blender_material, "EmissiveFactor") + if isinstance(emissive_socket, bpy.types.NodeSocket): + if export_settings['gltf_image_format'] != "NONE": + factor = gltf2_blender_get.get_factor_from_socket(emissive_socket, kind='RGB') + else: + factor = gltf2_blender_get.get_const_from_default_value_socket(emissive_socket, kind='RGB') + + if factor is None and emissive_socket.is_linked: + # In glTF, the default emissiveFactor is all zeros, so if an emission texture is connected, + # we have to manually set it to all ones. + factor = [1.0, 1.0, 1.0] + + if factor is None: factor = [0.0, 0.0, 0.0] + + # Handle Emission Strength + strength_socket = None + if emissive_socket.node.type == 'EMISSION': + strength_socket = emissive_socket.node.inputs['Strength'] + elif 'Emission Strength' in emissive_socket.node.inputs: + strength_socket = emissive_socket.node.inputs['Emission Strength'] + strength = ( + gltf2_blender_get.get_const_from_socket(strength_socket, kind='VALUE') + if strength_socket is not None + else None + ) + if strength is not None: + factor = [f * strength for f in factor] + + # Clamp to range [0,1] + # Official glTF clamp to range [0,1] + # If we are outside, we need to use extension KHR_materials_emissive_strength + + if factor == [0, 0, 0]: factor = None + + return factor + + return None + +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) + return emissive_texture, ["emissiveTexture"] if use_actives_uvmap_emissive else None + +def export_emission_strength_extension(emissive_factor, export_settings): + emissive_strength_extension = {} + emissive_strength_extension['emissiveStrength'] = max(emissive_factor) + + return Extension('KHR_materials_emissive_strength', emissive_strength_extension, False) \ No newline at end of file diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_ior.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_ior.py new file mode 100644 index 000000000..fc219c010 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_ior.py @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2018-2022 The glTF-Blender-IO authors. + +from io_scene_gltf2.io.com.gltf2_io_extensions import Extension +from io_scene_gltf2.blender.exp import gltf2_blender_get +from io_scene_gltf2.io.com.gltf2_io_constants import GLTF_IOR + +def export_ior(blender_material, extensions, export_settings): + ior_socket = gltf2_blender_get.get_socket(blender_material, 'IOR') + + if not ior_socket: + return None + + # We don't manage case where socket is linked, always check default value + if ior_socket.is_linked: + # TODOExt: add warning? + return None + + if ior_socket.default_value == GLTF_IOR: + return None + + # Export only if the following extensions are exported: + need_to_export_ior = [ + 'KHR_materials_transmission', + 'KHR_materials_volume', + 'KHR_materials_specular' + ] + + if not any([e in extensions.keys() for e in need_to_export_ior]): + return None + + ior_extension = {} + ior_extension['ior'] = ior_socket.default_value + + return Extension('KHR_materials_ior', ior_extension, False) \ No newline at end of file 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 0b40ffd60..a5929c05b 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 @@ -15,8 +15,8 @@ def gather_material_pbr_metallic_roughness(blender_material, orm_texture, export if not __filter_pbr_material(blender_material, export_settings): return None, None - base_color_texture, use_active_uvmap_base_color_texture = __gather_base_color_texture(blender_material, export_settings) - metallic_roughness_texture, use_active_uvmap_metallic_roughness_texture = __gather_metallic_roughness_texture(blender_material, orm_texture, export_settings) + base_color_texture, use_active_uvmap_base_color_texture, _ = __gather_base_color_texture(blender_material, export_settings) + metallic_roughness_texture, use_active_uvmap_metallic_roughness_texture, _ = __gather_metallic_roughness_texture(blender_material, orm_texture, export_settings) material = gltf2_io.MaterialPBRMetallicRoughness( base_color_factor=__gather_base_color_factor(blender_material, export_settings), @@ -92,7 +92,7 @@ def __gather_base_color_texture(blender_material, export_settings): if socket is not None and __has_image_node_from_socket(socket) ) if not inputs: - return None, None + return None, None, None return gltf2_blender_gather_texture_info.gather_texture_info(inputs[0], inputs, export_settings) @@ -128,7 +128,7 @@ def __gather_metallic_roughness_texture(blender_material, orm_texture, export_se 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): - return None, None + return None, None, None texture_input = (metallic_roughness,) elif not hasMetal: texture_input = (roughness_socket,) diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_sheen.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_sheen.py new file mode 100644 index 000000000..03625ecbc --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_sheen.py @@ -0,0 +1,68 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2018-2022 The glTF-Blender-IO authors. + +import bpy +from io_scene_gltf2.io.com.gltf2_io_extensions import Extension +from io_scene_gltf2.blender.exp import gltf2_blender_get +from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info + + +def export_sheen(blender_material, export_settings): + sheen_extension = {} + + sheenColor_socket = gltf2_blender_get.get_socket(blender_material, "sheenColor") + sheenRoughness_socket = gltf2_blender_get.get_socket(blender_material, "sheenRoughness") + + if sheenColor_socket is None or sheenRoughness_socket is None: + return None, None + + sheenColor_non_linked = isinstance(sheenColor_socket, bpy.types.NodeSocket) and not sheenColor_socket.is_linked + sheenRoughness_non_linked = isinstance(sheenRoughness_socket, bpy.types.NodeSocket) and not sheenRoughness_socket.is_linked + + + use_actives_uvmaps = [] + + if sheenColor_non_linked is True: + color = sheenColor_socket.default_value[:3] + if color != (0.0, 0.0, 0.0): + sheen_extension['sheenColorFactor'] = color + else: + # Factor + fac = gltf2_blender_get.get_factor_from_socket(sheenColor_socket, kind='RGB') + if fac is not None and fac != [0.0, 0.0, 0.0]: + sheen_extension['sheenColorFactor'] = fac + + # Texture + if gltf2_blender_get.has_image_node_from_socket(sheenColor_socket): + 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 + if original_sheenColor_use_active_uvmap: + use_actives_uvmaps.append("sheenColorTexture") + + + if sheenRoughness_non_linked is True: + fac = sheenRoughness_socket.default_value + if fac != 0.0: + sheen_extension['sheenRoughnessFactor'] = fac + else: + # Factor + fac = gltf2_blender_get.get_factor_from_socket(sheenRoughness_socket, kind='VALUE') + if fac is not None and fac != 0.0: + sheen_extension['sheenRoughnessFactor'] = fac + + # Texture + if gltf2_blender_get.has_image_node_from_socket(sheenRoughness_socket): + 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 + if original_sheenRoughness_use_active_uvmap: + use_actives_uvmaps.append("sheenRoughnessTexture") + + return Extension('KHR_materials_sheen', sheen_extension, False), use_actives_uvmaps diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_specular.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_specular.py new file mode 100644 index 000000000..30b321981 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_specular.py @@ -0,0 +1,168 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2018-2022 The glTF-Blender-IO authors. + +import bpy +from io_scene_gltf2.io.com.gltf2_io_extensions import Extension +from io_scene_gltf2.blender.exp import gltf2_blender_get +from io_scene_gltf2.io.com.gltf2_io_constants import GLTF_IOR +from io_scene_gltf2.blender.com.gltf2_blender_default import BLENDER_SPECULAR, BLENDER_SPECULAR_TINT +from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info + + + +def export_original_specular(blender_material, export_settings): + specular_extension = {} + + original_specular_socket = gltf2_blender_get.get_socket_original(blender_material, 'specular glTF') + original_specularcolor_socket = gltf2_blender_get.get_socket_original(blender_material, 'specularColor glTF') + + if original_specular_socket is None or original_specularcolor_socket is None: + return None, None + + specular_non_linked = isinstance(original_specular_socket, bpy.types.NodeSocket) and not original_specular_socket.is_linked + specularcolor_non_linked = isinstance(original_specularcolor_socket, bpy.types.NodeSocket) and not original_specularcolor_socket.is_linked + + + use_actives_uvmaps = [] + + if specular_non_linked is True: + fac = original_specular_socket.default_value + if fac != 1.0: + specular_extension['specularFactor'] = fac + else: + # Factor + fac = gltf2_blender_get.get_factor_from_socket(original_specular_socket, kind='VALUE') + if fac is not None and fac != 1.0: + specular_extension['specularFactor'] = fac + + # 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_socket, + (original_specular_socket,), + export_settings, + ) + specular_extension['specularTexture'] = original_specular_texture + if original_specular_use_active_uvmap: + use_actives_uvmaps.append("specularTexture") + + + if specularcolor_non_linked is True: + color = original_specularcolor_socket.default_value[:3] + if color != [1.0, 1.0, 1.0]: + specular_extension['specularColorFactor'] = color + else: + # Factor + fac = gltf2_blender_get.get_factor_from_socket(original_specularcolor_socket, kind='RGB') + if fac is not None and fac != [1.0, 1.0, 1.0]: + specular_extension['specularColorFactor'] = fac + + # 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_socket, + (original_specularcolor_socket,), + export_settings, + ) + specular_extension['specularColorTexture'] = original_specularcolor_texture + if original_specularcolor_use_active_uvmap: + use_actives_uvmaps.append("specularColorTexture") + + return Extension('KHR_materials_specular', specular_extension, False), use_actives_uvmaps + +def export_specular(blender_material, export_settings): + + if export_settings['gltf_original_specular'] is True: + return export_original_specular(blender_material, export_settings) + + specular_extension = {} + specular_ext_enabled = False + + specular_socket = gltf2_blender_get.get_socket(blender_material, 'Specular') + specular_tint_socket = gltf2_blender_get.get_socket(blender_material, 'Specular Tint') + base_color_socket = gltf2_blender_get.get_socket(blender_material, 'Base Color') + transmission_socket = gltf2_blender_get.get_socket(blender_material, 'Transmission') + ior_socket = gltf2_blender_get.get_socket(blender_material, 'IOR') + + 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 = specular_socket.default_value if specular_not_linked else None + specular_tint = specular_tint_socket.default_value if specular_tint_not_linked else None + transmission = transmission_socket.default_value if transmission_not_linked else None + ior = ior_socket.default_value if ior_not_linked else GLTF_IOR # textures not supported #TODOExt add warning? + base_color = base_color_socket.default_value[0:3] + + no_texture = (transmission_not_linked and specular_not_linked and specular_tint_not_linked and + (specular_tint == 0.0 or (specular_tint != 0.0 and base_color_not_linked))) + + use_actives_uvmaps = [] + + if no_texture: + if specular != BLENDER_SPECULAR or specular_tint != BLENDER_SPECULAR_TINT: + import numpy as np + # See https://gist.github.com/proog128/d627c692a6bbe584d66789a5a6437a33 + specular_ext_enabled = True + + def normalize(c): + luminance = lambda c: 0.3 * c[0] + 0.6 * c[1] + 0.1 * c[2] + assert(len(c) == 3) + l = luminance(c) + if l == 0: + return np.array(c) + return np.array([c[0] / l, c[1] / l, c[2] / l]) + + f0_from_ior = ((ior - 1)/(ior + 1))**2 + tint_strength = (1 - specular_tint) + normalize(base_color) * specular_tint + specular_color = (1 - transmission) * (1 / f0_from_ior) * 0.08 * specular * tint_strength + transmission * tint_strength + specular_extension['specularColorFactor'] = list(specular_color) + else: + if specular_not_linked and specular == BLENDER_SPECULAR and specular_tint_not_linked and specular_tint == BLENDER_SPECULAR_TINT: + return None, None + + # Trying to identify cases where exporting a texture will not be needed + if specular_not_linked and transmission_not_linked and \ + specular == 0.0 and transmission == 0.0: + + specular_extension['specularColorFactor'] = [0.0, 0.0, 0.0] + return specular_extension, [] + + + # There will be a texture, with a complex calculation (no direct channel mapping) + sockets = (specular_socket, specular_tint_socket, base_color_socket, transmission_socket, ior_socket) + # Set primary socket having a texture + primary_socket = specular_socket + if specular_not_linked: + primary_socket = specular_tint_socket + if specular_tint_not_linked: + primary_socket = base_color_socket + if base_color_not_linked: + primary_socket = transmission_socket + + specularColorTexture, use_active_uvmap, specularColorFactor = gltf2_blender_gather_texture_info.gather_texture_info( + primary_socket, + sockets, + export_settings, + filter_type='ANY') + if specularColorTexture is None: + return None, None + if use_active_uvmap: + use_actives_uvmaps.append("specularColorTexture") + + specular_ext_enabled = True + specular_extension['specularColorTexture'] = specularColorTexture + + + if specularColorFactor is not None: + specular_extension['specularColorFactor'] = specularColorFactor + + + specular_extension = Extension('KHR_materials_specular', specular_extension, False) if specular_ext_enabled else None + return specular_extension, use_actives_uvmaps \ No newline at end of file diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_transmission.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_transmission.py new file mode 100644 index 000000000..fdc5d8c75 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_transmission.py @@ -0,0 +1,47 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2018-2022 The glTF-Blender-IO authors. + +import bpy +from io_scene_gltf2.io.com.gltf2_io_extensions import Extension +from io_scene_gltf2.blender.exp import gltf2_blender_get +from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info + +def export_transmission(blender_material, export_settings): + transmission_enabled = False + has_transmission_texture = False + + transmission_extension = {} + transmission_slots = () + + transmission_socket = gltf2_blender_get.get_socket(blender_material, 'Transmission') + + if isinstance(transmission_socket, bpy.types.NodeSocket) and not transmission_socket.is_linked: + transmission_extension['transmissionFactor'] = transmission_socket.default_value + transmission_enabled = transmission_extension['transmissionFactor'] > 0 + elif gltf2_blender_get.has_image_node_from_socket(transmission_socket): + fac = gltf2_blender_get.get_factor_from_socket(transmission_socket, kind='VALUE') + transmission_extension['transmissionFactor'] = fac if fac is not None else 1.0 + has_transmission_texture = True + transmission_enabled = True + + if not transmission_enabled: + return None, None + + # Pack transmission channel (R). + if has_transmission_texture: + transmission_slots = (transmission_socket,) + + use_actives_uvmaps = [] + + if len(transmission_slots) > 0: + combined_texture, use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_texture_info( + transmission_socket, + transmission_slots, + export_settings, + ) + if has_transmission_texture: + transmission_extension['transmissionTexture'] = combined_texture + if use_active_uvmap: + use_actives_uvmaps.append("transmissionTexture") + + return Extension('KHR_materials_transmission', transmission_extension, False), use_actives_uvmaps \ No newline at end of file diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_unlit.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_unlit.py index e104b7f1f..b501f98f0 100644 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_unlit.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_unlit.py @@ -1,9 +1,9 @@ # SPDX-License-Identifier: Apache-2.0 -# Copyright 2018-2021 The glTF-Blender-IO authors. +# Copyright 2018-2022 The glTF-Blender-IO authors. from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info from io_scene_gltf2.blender.exp import gltf2_blender_get - +from io_scene_gltf2.io.com.gltf2_io_extensions import Extension def detect_shadeless_material(blender_material, export_settings): """Detect if this material is "shadeless" ie. should be exported @@ -127,7 +127,7 @@ def gather_base_color_texture(info, export_settings): # because gather_image determines how to pack images based on the # names of sockets, and the names are hard-coded to a Principled # style graph. - unlit_texture, unlit_use_active_uvmap = gltf2_blender_gather_texture_info.gather_texture_info( + unlit_texture, unlit_use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_texture_info( sockets[0], sockets, export_settings, diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_variants.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_variants.py new file mode 100644 index 000000000..4c452e6a6 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_variants.py @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2018-2022 The glTF-Blender-IO authors. + +import bpy +from typing import Dict, Any +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached +from io_scene_gltf2.io.com import gltf2_io_variants + + +@cached +def gather_variant(variant_idx, export_settings) -> Dict[str, Any]: + + variant = gltf2_io_variants.Variant( + name=bpy.data.scenes[0].gltf2_KHR_materials_variants_variants[variant_idx].name, + extensions=None, + extras=None + ) + return variant.to_dict() diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_volume.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_volume.py new file mode 100644 index 000000000..57b007000 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_volume.py @@ -0,0 +1,75 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2018-2022 The glTF-Blender-IO authors. + +import bpy +from io_scene_gltf2.io.com.gltf2_io_extensions import Extension +from io_scene_gltf2.blender.exp import gltf2_blender_get +from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info + + +def export_volume(blender_material, export_settings): + # Implementation based on https://github.com/KhronosGroup/glTF-Blender-IO/issues/1454#issuecomment-928319444 + + # If no transmission --> No volume + transmission_enabled = False + transmission_socket = gltf2_blender_get.get_socket(blender_material, 'Transmission') + if isinstance(transmission_socket, bpy.types.NodeSocket) and not transmission_socket.is_linked: + transmission_enabled = transmission_socket.default_value > 0 + elif gltf2_blender_get.has_image_node_from_socket(transmission_socket): + transmission_enabled = True + + if transmission_enabled is False: + return None, None + + volume_extension = {} + has_thickness_texture = False + thickness_slots = () + + thicknesss_socket = gltf2_blender_get.get_socket_old(blender_material, 'Thickness') + if thicknesss_socket is None: + volume_extension['thicknessFactor'] = 1.0 + + density_socket = gltf2_blender_get.get_socket(blender_material, 'Density', volume=True) + attenuation_color_socket = gltf2_blender_get.get_socket(blender_material, 'Color', volume=True) + if density_socket is None or attenuation_color_socket is None: + return None, None + + if isinstance(attenuation_color_socket, bpy.types.NodeSocket): + rgb = gltf2_blender_get.get_const_from_default_value_socket(attenuation_color_socket, kind='RGB') + volume_extension['attenuationColor'] = rgb + + if isinstance(density_socket, bpy.types.NodeSocket): + density = gltf2_blender_get.get_const_from_default_value_socket(density_socket, kind='VALUE') + volume_extension['attenuationDistance'] = 1.0 / density if density != 0 else None # infinity (Using None as glTF default) + + + if isinstance(thicknesss_socket, bpy.types.NodeSocket) and not thicknesss_socket.is_linked: + val = thicknesss_socket.default_value + if val == 0.0: + # If no thickness, no volume extension export + return None, None + volume_extension['thicknessFactor'] = val + elif gltf2_blender_get.has_image_node_from_socket(thicknesss_socket): + fac = gltf2_blender_get.get_factor_from_socket(thicknesss_socket, kind='VALUE') + # default value in glTF is 0.0, but if there is a texture without factor, use 1 + volume_extension['thicknessFactor'] = fac if fac != None else 1.0 + has_thickness_texture = True + + # Pack thickness channel (R). + if has_thickness_texture: + thickness_slots = (thicknesss_socket,) + + use_actives_uvmaps = [] + + if len(thickness_slots) > 0: + combined_texture, use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_texture_info( + thicknesss_socket, + thickness_slots, + export_settings, + ) + if has_thickness_texture: + volume_extension['thicknessTexture'] = combined_texture + if use_active_uvmap: + use_actives_uvmaps.append("thicknessTexture") + + return Extension('KHR_materials_volume', volume_extension, False), use_actives_uvmaps \ No newline at end of file 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 367c30f57..b2ffb6b33 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py @@ -12,10 +12,12 @@ from io_scene_gltf2.blender.exp import gltf2_blender_extract from io_scene_gltf2.blender.exp import gltf2_blender_gather_accessors from io_scene_gltf2.blender.exp import gltf2_blender_gather_primitive_attributes from io_scene_gltf2.blender.exp import gltf2_blender_gather_materials +from io_scene_gltf2.blender.exp import gltf2_blender_gather_materials_variants from io_scene_gltf2.io.com import gltf2_io from io_scene_gltf2.io.exp import gltf2_io_binary_data from io_scene_gltf2.io.com import gltf2_io_constants +from io_scene_gltf2.io.com import gltf2_io_extensions from io_scene_gltf2.io.com.gltf2_io_debug import print_console @@ -86,7 +88,7 @@ def gather_primitives( primitive = gltf2_io.MeshPrimitive( attributes=internal_primitive['attributes'], - extensions=None, + extensions=__gather_extensions(blender_mesh, material_idx, active_uvmap_idx, export_settings), extras=None, indices=internal_primitive['indices'], material=material, @@ -214,3 +216,55 @@ def __gather_targets(blender_primitive, blender_mesh, modifiers, export_settings morph_index += 1 return targets return None + +def __gather_extensions(blender_mesh, + material_idx: int, + active_uvmap_idx, + export_settings): + extensions = {} + + if bpy.context.preferences.addons['io_scene_gltf2'].preferences.KHR_materials_variants_ui is False: + return None + + if bpy.data.scenes[0].get('gltf2_KHR_materials_variants_variants') is None: + return None + if len(bpy.data.scenes[0]['gltf2_KHR_materials_variants_variants']) == 0: + return None + + # Material idx is the slot idx. Retrieve associated variant, if any + mapping = [] + for i in [v for v in blender_mesh.gltf2_variant_mesh_data if v.material_slot_index == material_idx]: + variants = [] + for idx, v in enumerate(i.variants): + if v.variant.variant_idx in [o.variant.variant_idx for o in i.variants[:idx]]: + # Avoid duplicates + continue + vari = gltf2_blender_gather_materials_variants.gather_variant(v.variant.variant_idx, export_settings) + if vari is not None: + variant_extension = gltf2_io_extensions.ChildOfRootExtension( + name="KHR_materials_variants", + path=["variants"], + extension=vari + ) + variants.append(variant_extension) + if len(variants) > 0: + if i.material: + mat = gltf2_blender_gather_materials.gather_material( + i.material, + active_uvmap_idx, + export_settings + ) + else: + # empty slot + mat = None + mapping.append({'material': mat, 'variants': variants}) + + if len(mapping) > 0: + extensions["KHR_materials_variants"] = gltf2_io_extensions.Extension( + name="KHR_materials_variants", + extension={ + "mappings": mapping + } + ) + + return extensions if extensions else None diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture.py index e8c6baf18..ccfd42e52 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture.py @@ -26,24 +26,26 @@ def gather_texture( """ if not __filter_texture(blender_shader_sockets, export_settings): - return None + return None, None + + source, factor = __gather_source(blender_shader_sockets, export_settings) texture = gltf2_io.Texture( extensions=__gather_extensions(blender_shader_sockets, export_settings), extras=__gather_extras(blender_shader_sockets, export_settings), name=__gather_name(blender_shader_sockets, export_settings), sampler=__gather_sampler(blender_shader_sockets, export_settings), - source=__gather_source(blender_shader_sockets, export_settings) + source= source ) # although valid, most viewers can't handle missing source properties # This can have None source for "keep original", when original can't be found if texture.source is None: - return None + return None, None export_user_extensions('gather_texture_hook', export_settings, texture, blender_shader_sockets) - return texture + return texture, factor def __filter_texture(blender_shader_sockets, export_settings): @@ -66,13 +68,14 @@ def __gather_name(blender_shader_sockets, export_settings): def __gather_sampler(blender_shader_sockets, export_settings): - shader_nodes = [__get_tex_from_socket(socket).shader_node 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. " "The resulting glTF sampler will behave like the first shader node tex image.") + first_valid_shader_node = next(filter(lambda x: x is not None, shader_nodes)).shader_node return gltf2_blender_gather_sampler.gather_sampler( - shader_nodes[0], + first_valid_shader_node, export_settings) @@ -81,7 +84,7 @@ def __gather_source(blender_shader_sockets, export_settings): # Helpers - +# TODOExt deduplicate def __get_tex_from_socket(socket): result = gltf2_blender_search_node_tree.from_socket( socket, diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture_info.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture_info.py index 15b101ade..5fe2da329 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture_info.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture_info.py @@ -19,14 +19,14 @@ from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extension # 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): - return __gather_texture_info_helper(primary_socket, blender_shader_sockets, 'DEFAULT', export_settings) +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) -def gather_material_normal_texture_info_class(primary_socket, blender_shader_sockets, export_settings): - return __gather_texture_info_helper(primary_socket, blender_shader_sockets, 'NORMAL', 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) -def gather_material_occlusion_texture_info_class(primary_socket, blender_shader_sockets, export_settings): - return __gather_texture_info_helper(primary_socket, blender_shader_sockets, 'OCCLUSION', 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) @cached @@ -34,16 +34,19 @@ def __gather_texture_info_helper( primary_socket: bpy.types.NodeSocket, blender_shader_sockets: typing.Tuple[bpy.types.NodeSocket], kind: str, + filter_type: str, export_settings): - if not __filter_texture_info(primary_socket, blender_shader_sockets, export_settings): - return None, None + if not __filter_texture_info(primary_socket, blender_shader_sockets, filter_type, export_settings): + return None, None, None 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) + fields = { 'extensions': __gather_extensions(tex_transform, export_settings), 'extras': __gather_extras(blender_shader_sockets, export_settings), - 'index': __gather_index(blender_shader_sockets, export_settings), + 'index': index, 'tex_coord': tex_coord } @@ -59,14 +62,14 @@ def __gather_texture_info_helper( texture_info = gltf2_io.MaterialOcclusionTextureInfoClass(**fields) if texture_info.index is None: - return None, None + return None, None, None export_user_extensions('gather_texture_info_hook', export_settings, texture_info, blender_shader_sockets) - return texture_info, use_active_uvmap + return texture_info, use_active_uvmap, factor -def __filter_texture_info(primary_socket, blender_shader_sockets, export_settings): +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: @@ -75,9 +78,18 @@ def __filter_texture_info(primary_socket, blender_shader_sockets, export_setting return False if not all([elem is not None for elem in blender_shader_sockets]): return False - 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 + 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]): + # 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]): + return False + elif filter_type == "NONE": + # No check + pass return True @@ -163,7 +175,7 @@ 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, diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_tree.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_tree.py index 0377a6b7c..367726678 100644 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_tree.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_tree.py @@ -468,3 +468,20 @@ class VExportTree: skin = gather_skin(n.uuid, self.export_settings) skins.append(skin) return skins + + def variants_reset_to_original(self): + # Only if Variants are displayed and exported + if bpy.context.preferences.addons['io_scene_gltf2'].preferences.KHR_materials_variants_ui is False: + return + objects = [self.nodes[o].blender_object for o in self.get_all_node_of_type(VExportNode.OBJECT) if self.nodes[o].blender_object.type == "MESH" \ + and self.nodes[o].blender_object.data.get('gltf2_variant_default_materials') is not None] + for obj in objects: + # loop on material slots ( primitives ) + for mat_slot_idx, s in enumerate(obj.material_slots): + # Check if there is a default material for this slot + for i in obj.data.gltf2_variant_default_materials: + if i.material_slot_index == mat_slot_idx: + s.material = i.default_material + break + + # If not found, keep current material as default diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_get.py b/io_scene_gltf2/blender/exp/gltf2_blender_get.py index e38906e64..17eacf860 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_get.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_get.py @@ -4,9 +4,10 @@ import bpy from mathutils import Vector, Matrix -from ..com.gltf2_blender_material_helpers import get_gltf_node_name +from ..com.gltf2_blender_material_helpers import get_gltf_node_name, get_gltf_pbr_non_converted_name from ...blender.com.gltf2_blender_conversion import texture_transform_blender_to_gltf from io_scene_gltf2.io.com import gltf2_io_debug +from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree def get_animation_target(action_group: bpy.types.ActionGroup): @@ -47,7 +48,7 @@ def get_node_socket(blender_material, type, name): return None -def get_socket(blender_material: bpy.types.Material, name: str): +def get_socket(blender_material: bpy.types.Material, name: str, volume=False): """ For a given material input name, retrieve the corresponding node tree socket. @@ -70,8 +71,15 @@ def get_socket(blender_material: bpy.types.Material, name: str): elif name == "Background": type = bpy.types.ShaderNodeBackground name = "Color" + elif name == "sheenColor": + return get_node_socket(blender_material, bpy.types.ShaderNodeBsdfVelvet, "Color") + elif name == "sheenRoughness": + return get_node_socket(blender_material, bpy.types.ShaderNodeBsdfVelvet, "Sigma") else: - type = bpy.types.ShaderNodeBsdfPrincipled + if volume is False: + type = bpy.types.ShaderNodeBsdfPrincipled + else: + type = bpy.types.ShaderNodeVolumeAbsorption return get_node_socket(blender_material, type, name) @@ -97,6 +105,24 @@ def get_socket_old(blender_material: bpy.types.Material, name: str): return None +def get_socket_original(blender_material: bpy.types.Material, name: str): + """ + For a given material input name, retrieve the corresponding node tree socket in the special glTF node group. + + :param blender_material: a blender material for which to get the socket + :param name: the name of the socket + :return: a blender NodeSocket + """ + gltf_node_group_name = get_gltf_pbr_non_converted_name().lower() + if blender_material.node_tree and blender_material.use_nodes: + nodes = [n for n in blender_material.node_tree.nodes if \ + isinstance(n, bpy.types.ShaderNodeGroup) and n.node_tree.name.lower() == gltf_node_group_name] + inputs = sum([[input for input in node.inputs if input.name == name] for node in nodes], []) + if inputs: + return inputs[0] + + return None + def check_if_is_linked_to_active_output(shader_socket): for link in shader_socket.links: if isinstance(link.to_node, bpy.types.ShaderNodeOutputMaterial) and link.to_node.is_active_output is True: @@ -297,3 +323,12 @@ def previous_node(socket): if prev_socket is not None: 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): + 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 diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_image.py b/io_scene_gltf2/blender/exp/gltf2_blender_image.py index 8b9db89a3..6730f4791 100644 --- a/io_scene_gltf2/blender/exp/gltf2_blender_image.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_image.py @@ -27,6 +27,18 @@ class FillWhite: """Fills a channel with all ones (1.0).""" pass +class StoreData: + def __init__(self, data): + """Store numeric data (not an image channel""" + self.data = data + +class StoreImage: + """ + Store a channel with the channel src_chan from a Blender image. + This channel will be used for numpy calculation (no direct channel mapping) + """ + def __init__(self, image: bpy.types.Image): + self.image = image class ExportImage: """Custom image class. @@ -55,9 +67,13 @@ class ExportImage: def __init__(self, original=None): self.fills = {} + self.stored = {} - # In case of keeping original texture images - self.original = original + self.original = original # In case of keeping original texture images + self.numpy_calc = None + + def set_calc(self, numpy_calc): + self.numpy_calc = numpy_calc # In case of numpy calculation (no direct channel mapping) @staticmethod def from_blender_image(image: bpy.types.Image): @@ -73,6 +89,12 @@ class ExportImage: def fill_image(self, image: bpy.types.Image, dst_chan: Channel, src_chan: Channel): self.fills[dst_chan] = FillImage(image, src_chan) + def store_data(self, identifier, data, type='Image'): + if type == "Image": # This is an image + self.stored[identifier] = StoreImage(data) + else: # This is a numeric value + self.stored[identifier] = StoreData(data) + def fill_white(self, dst_chan: Channel): self.fills[dst_chan] = FillWhite() @@ -81,7 +103,7 @@ class ExportImage: def empty(self) -> bool: if self.original is None: - return not self.fills + return not (self.fills or self.stored) else: return False @@ -103,7 +125,7 @@ class ExportImage: len(set(fill.image.name for fill in self.fills.values())) == 1 ) - def encode(self, mime_type: Optional[str]) -> bytes: + def encode(self, mime_type: Optional[str]) -> Tuple[bytes, bool]: self.file_format = { "image/jpeg": "JPEG", "image/png": "PNG" @@ -111,10 +133,14 @@ class ExportImage: # Happy path = we can just use an existing Blender image if self.__on_happy_path(): - return self.__encode_happy() + return self.__encode_happy(), None - # Unhappy path = we need to create the image self.fills describes. - return self.__encode_unhappy() + # Unhappy path = we need to create the image self.fills describes or self.stores describes + if self.numpy_calc is None: + return self.__encode_unhappy(), None + else: + pixels, width, height, factor = self.numpy_calc(self.stored) + return self.__encode_from_numpy_array(pixels, (width, height)), factor def __encode_happy(self) -> bytes: return self.__encode_from_image(self.blender_image()) @@ -147,7 +173,7 @@ class ExportImage: else: # Image is the wrong size; make a temp copy and scale it. with TmpImageGuard() as guard: - _make_temp_image_copy(guard, src_image=image) + make_temp_image_copy(guard, src_image=image) tmp_image = guard.image tmp_image.scale(width, height) tmp_image.pixels.foreach_get(tmp_buf) @@ -197,7 +223,7 @@ class ExportImage: # Copy to a temp image and save. with TmpImageGuard() as guard: - _make_temp_image_copy(guard, src_image=image) + make_temp_image_copy(guard, src_image=image) tmp_image = guard.image return _encode_temp_image(tmp_image, self.file_format) @@ -228,7 +254,7 @@ class TmpImageGuard: bpy.data.images.remove(self.image, do_unlink=True) -def _make_temp_image_copy(guard: TmpImageGuard, src_image: bpy.types.Image): +def make_temp_image_copy(guard: TmpImageGuard, src_image: bpy.types.Image): """Makes a temporary copy of src_image. Will be cleaned up with guard.""" guard.image = src_image.copy() tmp_image = guard.image diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_texture_specular.py b/io_scene_gltf2/blender/exp/gltf2_blender_texture_specular.py new file mode 100644 index 000000000..6321f1285 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_texture_specular.py @@ -0,0 +1,94 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2018-2022 The glTF-Blender-IO authors. + +import bpy +import numpy as np +from .gltf2_blender_gather_image import StoreImage, StoreData +from .gltf2_blender_image import TmpImageGuard, make_temp_image_copy + +def specular_calculation(stored): + + # See https://gist.github.com/proog128/d627c692a6bbe584d66789a5a6437a33 + + # Find all Blender images used + images = [] + for fill in stored.values(): + if isinstance(fill, StoreImage): + if fill.image not in images: + images.append(fill.image) + + if not images: + # No ImageFills; use a 1x1 white pixel + pixels = np.array([1.0, 1.0, 1.0, 1.0], np.float32) + return pixels, 1, 1 + + width = max(image.size[0] for image in images) + height = max(image.size[1] for image in images) + + buffers = {} + + for identifier, image in [(ident, store.image) for (ident, store) in stored.items() if isinstance(store, StoreImage)]: + tmp_buf = np.empty(width * height * 4, np.float32) + + if image.size[0] == width and image.size[1] == height: + image.pixels.foreach_get(tmp_buf) + else: + # Image is the wrong size; make a temp copy and scale it. + with TmpImageGuard() as guard: + make_temp_image_copy(guard, src_image=image) + tmp_image = guard.image + tmp_image.scale(width, height) + tmp_image.pixels.foreach_get(tmp_buf) + + buffers[identifier] = np.reshape(tmp_buf, [width, height, 4]) + + # keep only needed channels + ## scalar + for i in ['specular', 'specular_tint', 'transmission']: + if i in buffers.keys(): + buffers[i] = buffers[i][:,:,stored[i + "_channel"].data] + else: + buffers[i] = np.full((width, height, 1), stored[i].data) + + # Vector 3 + for i in ['base_color']: + if i in buffers.keys(): + if i + "_channel" not in stored.keys(): + buffers[i] = buffers[i][:,:,:3] + else: + # keep only needed channel + for c in range(3): + if c != stored[i+"_channel"].data: + buffers[i][:, :, c] = 0.0 + buffers[i] = buffers[i][:,:,:3] + else: + buffers[i] = np.full((width, height, 3), stored[i].data[0:3]) + + ior = stored['ior'].data + + # calculation + stack3 = lambda v: np.dstack([v]*3) + + def normalize(c): + luminance = lambda c: 0.3 * c[:,:,0] + 0.6 * c[:,:,1] + 0.1 * c[:,:,2] + l = luminance(c) + # TODOExt Manage all 0 + return c / stack3(l) + + + f0_from_ior = ((ior - 1)/(ior + 1))**2 + tint_strength = (1 - stack3(buffers['specular_tint'])) + normalize(buffers['base_color']) * stack3(buffers['specular_tint']) + out_buf = (1 - stack3(buffers['transmission'])) * (1 / f0_from_ior) * 0.08 * stack3(buffers['specular']) * tint_strength + stack3(buffers['transmission']) * tint_strength + + # Manage values > 1.0 -> Need to apply factor + factor = None + factors = [np.amax(out_buf[:, :, i]) for i in range(3)] + + if any([f > 1.0 for f in factors]): + factor = [1.0 if f < 1.0 else f for f in factors] + out_buf /= factor + + out_buf = np.dstack((out_buf, np.ones((width, height)))) # Set alpha (glTF specular) to 1 + out_buf = np.reshape(out_buf, (width * height * 4)) + + return np.float32(out_buf), width, height, [float(f) for f in factor] if factor else None diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_ior.py b/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_ior.py new file mode 100644 index 000000000..ab5b2e7e3 --- /dev/null +++ b/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_ior.py @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2018-2021 The glTF-Blender-IO authors. + +from ...io.com.gltf2_io_constants import GLTF_IOR + +def ior(mh, ior_socket): + try: + ext = mh.pymat.extensions['KHR_materials_ior'] + except Exception: + return + ior = ext.get('ior', GLTF_IOR) + ior_socket.default_value = ior diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_pbrSpecularGlossiness.py b/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_pbrSpecularGlossiness.py index bed63f7f6..19a394b99 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_pbrSpecularGlossiness.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_pbrSpecularGlossiness.py @@ -22,19 +22,24 @@ def pbr_specular_glossiness(mh): mh.node_tree.links.new(add_node.inputs[0], glossy_node.outputs[0]) mh.node_tree.links.new(add_node.inputs[1], diffuse_node.outputs[0]) - emission_socket, alpha_socket = make_output_nodes( + emission_socket, alpha_socket, _, _ = make_output_nodes( mh, location=(370, 250), + additional_location=None, #No additional location needed for SpecGloss shader_socket=add_node.outputs[0], make_emission_socket=mh.needs_emissive(), make_alpha_socket=not mh.is_opaque(), + make_volume_socket=None, # No possible to have KHR_materials_volume with specular/glossiness + make_velvet_socket=None # No possible to have KHR_materials_volume with specular/glossiness ) - emission( - mh, - location=(-200, 860), - color_socket=emission_socket, - ) + if emission_socket: + emission( + mh, + location=(-200, 860), + color_socket=emission_socket, + strength_socket=emission_socket.node.inputs['Strength'] + ) base_color( mh, diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_sheen.py b/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_sheen.py new file mode 100644 index 000000000..3560d0949 --- /dev/null +++ b/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_sheen.py @@ -0,0 +1,88 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2018-2022 The glTF-Blender-IO authors. + +from ...io.com.gltf2_io import TextureInfo +from .gltf2_blender_texture import texture +from .gltf2_blender_image import BlenderImage +from ..exp.gltf2_blender_image import TmpImageGuard, make_temp_image_copy +import numpy as np +import bpy + +def sheen( mh, + location_sheenColor, + location_sheenRoughness, + sheenColor_socket, + sheenRoughness_socket + ): + + x_sheenColor, y_sheenColor = location_sheenColor + x_sheenRoughness, y_sheenRoughness = location_sheenRoughness + + try: + ext = mh.pymat.extensions['KHR_materials_sheen'] + except Exception: + return + + sheenColorFactor = ext.get('sheenColorFactor', [0.0, 0.0, 0.0]) + tex_info_color = ext.get('sheenColorTexture') + if tex_info_color is not None: + tex_info_color = TextureInfo.from_dict(tex_info_color) + + sheenRoughnessFactor = ext.get('sheenRoughnessFactor', 0.0) + tex_info_roughness = ext.get('sheenRoughnessTexture') + if tex_info_roughness is not None: + tex_info_roughness = TextureInfo.from_dict(tex_info_roughness) + + if tex_info_color is None: + sheenColorFactor.extend([1.0]) + sheenColor_socket.default_value = sheenColorFactor + else: + # Mix sheenColor factor + sheenColorFactor = sheenColorFactor + [1.0] + if sheenColorFactor != [1.0, 1.0, 1.0, 1.0]: + node = mh.node_tree.nodes.new('ShaderNodeMixRGB') + node.label = 'sheenColor Factor' + node.location = x_sheenColor - 140, y_sheenColor + node.blend_type = 'MULTIPLY' + # Outputs + mh.node_tree.links.new(sheenColor_socket, node.outputs[0]) + # Inputs + node.inputs['Fac'].default_value = 1.0 + sheenColor_socket = node.inputs['Color1'] + node.inputs['Color2'].default_value = sheenColorFactor + x_sheenColor -= 200 + + texture( + mh, + tex_info=tex_info_color, + label='SHEEN COLOR', + location=(x_sheenColor, y_sheenColor), + color_socket=sheenColor_socket + ) + + if tex_info_roughness is None: + sheenRoughness_socket.default_value = sheenRoughnessFactor + else: + # Mix sheenRoughness factor + if sheenRoughnessFactor != 1.0: + node = mh.node_tree.nodes.new('ShaderNodeMath') + node.label = 'shennRoughness Factor' + node.location = x_sheenRoughness - 140, y_sheenRoughness + node.operation = 'MULTIPLY' + # Outputs + mh.node_tree.links.new(sheenRoughness_socket, node.outputs[0]) + # Inputs + sheenRoughness_socket = node.inputs[0] + node.inputs[1].default_value = sheenRoughnessFactor + x_sheenRoughness -= 200 + + texture( + mh, + tex_info=tex_info_roughness, + label='SHEEN ROUGHNESS', + location=(x_sheenRoughness, y_sheenRoughness), + is_data=True, + color_socket=None, + alpha_socket=sheenRoughness_socket + ) + return \ No newline at end of file diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_specular.py b/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_specular.py new file mode 100644 index 000000000..e4a0c52d6 --- /dev/null +++ b/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_specular.py @@ -0,0 +1,350 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2018-2021 The glTF-Blender-IO authors. + +import bpy +from ...io.com.gltf2_io import TextureInfo +from .gltf2_blender_texture import texture +from io_scene_gltf2.io.com.gltf2_io_constants import GLTF_IOR +from .gltf2_blender_image import BlenderImage +from ..exp.gltf2_blender_image import TmpImageGuard, make_temp_image_copy + + +def specular(mh, location_specular, + location_specular_tint, + specular_socket, + specular_tint_socket, + original_specular_socket, + original_specularcolor_socket, + location_original_specular, + location_original_specularcolor): + x_specular, y_specular = location_specular + x_tint, y_tint = location_specular_tint + + if specular_socket is None: + return + if specular_tint_socket is None: + return + + try: + ext = mh.pymat.extensions['KHR_materials_specular'] + except Exception: + return + + import numpy as np + + # Retrieve image names + try: + tex_info = mh.pymat.pbr_metallic_roughness.base_color_texture + pytexture = mh.gltf.data.textures[tex_info.index] + pyimg = mh.gltf.data.images[pytexture.source] + base_color_image_name = pyimg.blender_image_name + except: + base_color_image_name = None + + # First check if we need a texture or not -> retrieve all info needed + specular_factor = ext.get('specularFactor', 1.0) + tex_specular_info = ext.get('specularTexture') + if tex_specular_info is not None: + tex_specular_info = TextureInfo.from_dict(tex_specular_info) + + specular_color_factor = np.array(ext.get('specularColorFactor', [1.0, 1.0, 1.0])[:3]) + tex_specular_color_info = ext.get('specularColorTexture') + if tex_specular_color_info is not None: + tex_specular_color_info = TextureInfo.from_dict(tex_specular_color_info) + + base_color_not_linked = base_color_image_name is None + base_color = np.array(mh.pymat.pbr_metallic_roughness.base_color_factor or [1, 1, 1]) + tex_base_color = mh.pymat.pbr_metallic_roughness.base_color_texture + base_color = base_color[:3] + + try: + ext_transmission = mh.pymat.extensions['KHR_materials_transmission'] + transmission_factor = ext_transmission.get('transmissionFactor', 0) + tex_transmission_info = ext_transmission.get('transmissionTexture') + if tex_transmission_info is not None: + tex_transmission_info = TextureInfo.from_dict(tex_transmission_info) + pytexture = mh.gltf.data.textures[tex_transmission_info.index] + pyimg = mh.gltf.data.images[pytexture.source] + transmission_image_name = pyimg.blender_image_name + else: + transmission_image_name = None + except Exception: + transmission_factor = 0 + tex_transmission_info = None + transmission_image_name = None + + transmission_not_linked = transmission_image_name is None + + try: + ext_ior = mh.pymat.extensions['KHR_materials_ior'] + ior = ext_ior.get('ior', GLTF_IOR) + except: + ior = GLTF_IOR + + use_texture = tex_specular_info is not None or tex_specular_color_info is not None \ + or transmission_not_linked is False or base_color_not_linked is False + + + # Before creating converted textures, + # Also plug non converted data into glTF PBR Non Converted Extensions node + original_specular( mh, + specular_factor, + tex_specular_info, + specular_color_factor, + tex_specular_color_info, + original_specular_socket, + original_specularcolor_socket, + location_original_specular, + location_original_specularcolor + ) + + + if not use_texture: + + def luminance(c): + return 0.3 * c[0] + 0.6 * c[1] + 0.1 * c[2] + + def normalize(c): + assert(len(c) == 3) + l = luminance(c) + if l == 0: + return c + return np.array([c[0] / l, c[1] / l, c[2] / l]) + + f0_from_ior = ((ior - 1)/(ior + 1))**2 + lum_specular_color = luminance(specular_color_factor) + blender_specular = ((lum_specular_color - transmission_factor) / (1 - transmission_factor)) * (1 / 0.08) * f0_from_ior + blender_specular_tint = luminance((normalize(specular_color_factor) - 1) / (normalize(base_color) - 1)) + if blender_specular_tint < 0 or blender_specular_tint > 1: + # TODOExt Warning clamping + blender_specular_tint = np.maximum(np.minimum(blender_specular_tint, 1), 0) + + specular_socket.default_value = blender_specular + specular_tint_socket.default_value = blender_specular_tint + # Note: blender_specular can be greater 1. The Blender documentation permits this. + + return + else: + # Need to create a texture + # First, retrieve and create all images needed + + # Base Color is already created + # Transmission is already created + # specularTexture is just created by original specular function + specular_image_name = None + try: + pytexture = mh.gltf.data.textures[tex_specular_info.index] + pyimg = mh.gltf.data.images[pytexture.source] + specular_image_name = pyimg.blender_image_name + except: + specular_image_name = None + + + # specularColorTexture is just created by original specular function + specularcolor_image_name = None + try: + pytexture = mh.gltf.data.textures[tex_specular_color_info.index] + pyimg = mh.gltf.data.images[pytexture.source] + specularcolor_image_name = pyimg.blender_image_name + except: + specularcolor_image_name = None + + stack3 = lambda v: np.dstack([v]*3) + + texts = { + base_color_image_name : 'basecolor', + transmission_image_name : 'transmission', + specularcolor_image_name : 'speccolor', + specular_image_name: 'spec' + } + images = [(name, bpy.data.images[name]) for name in [base_color_image_name, transmission_image_name, specularcolor_image_name, specular_image_name] if name is not None] + + width = max(image[1].size[0] for image in images) + height = max(image[1].size[1] for image in images) + + buffers = {} + for name, image in images: + tmp_buf = np.empty(width * height * 4, np.float32) + + if image.size[0] == width and image.size[1] == height: + image.pixels.foreach_get(tmp_buf) + else: + # Image is the wrong size; make a temp copy and scale it. + with TmpImageGuard() as guard: + make_temp_image_copy(guard, src_image=image) + tmp_image = guard.image + tmp_image.scale(width, height) + tmp_image.pixels.foreach_get(tmp_buf) + + buffers[texts[name]] = np.reshape(tmp_buf, [width, height, 4]) + buffers[texts[name]] = buffers[texts[name]][:,:,:3] + + # Manage factors + if name == transmission_image_name: + buffers[texts[name]] = stack3(buffers[texts[name]][:,:,0]) # Transmission : keep only R channel + + buffers[texts[name]] *= stack3(transmission_factor) + + elif name == base_color_image_name: + buffers[texts[name]] *= base_color + + elif name == specularcolor_image_name: + buffers[texts[name]] *= specular_color_factor + + # Create buffer if there is no image + if 'basecolor' not in buffers.keys(): + buffers['basecolor'] = np.full((width, height, 3), base_color) + if 'transmission' not in buffers.keys(): + buffers['transmission'] = np.full((width, height, 3), transmission_factor) + if 'speccolor' not in buffers.keys(): + buffers['speccolor'] = np.full((width, height, 3), specular_color_factor) + + # Calculation + + luminance = lambda c: 0.3 * c[:,:,0] + 0.6 * c[:,:,1] + 0.1 * c[:,:,2] + def normalize(c): + l = luminance(c) + if np.all(l == 0.0): + return np.array(c) + return c / stack3(l) + + f0_from_ior = ((ior - 1)/(ior + 1))**2 + lum_specular_color = stack3(luminance(buffers['speccolor'])) + blender_specular = ((lum_specular_color - buffers['transmission']) / (1 - buffers['transmission'])) * (1 / 0.08) * f0_from_ior + blender_specular_tint = luminance((normalize(buffers['speccolor']) - 1) / (normalize(buffers['basecolor']) - 1)) + np.nan_to_num(blender_specular_tint, copy=False) + blender_specular_tint = np.clip(blender_specular_tint, 0.0, 1.0) + blender_specular_tint = stack3(blender_specular_tint) + + blender_specular = np.dstack((blender_specular, np.ones((width, height)))) # Set alpha to 1 + blender_specular_tint = np.dstack((blender_specular_tint, np.ones((width, height)))) # Set alpha to 1 + + # Check if we really need to create a texture + blender_specular_tex_not_needed = np.all(np.isclose(blender_specular, blender_specular[0][0])) + blender_specular_tint_tex_not_needed = np.all(np.isclose(blender_specular_tint, blender_specular_tint[0][0])) + + if blender_specular_tex_not_needed == True: + lum = lambda c: 0.3 * c[0] + 0.6 * c[1] + 0.1 * c[2] + specular_socket.default_value = lum(blender_specular[0][0][:3]) + else: + blender_specular = np.reshape(blender_specular, width * height * 4) + # Create images in Blender, width and height are dummy values, then set packed file data + blender_image_spec = bpy.data.images.new('Specular', width, height) + blender_image_spec.pixels.foreach_set(np.float32(blender_specular)) + blender_image_spec.pack() + + # Create Textures in Blender + tex_info = tex_specular_info + if tex_info is None: + tex_info = tex_specular_color_info + if tex_info is None: + tex_info = tex_transmission_info + if tex_info is None: + tex_info = tex_base_color + + texture( + mh, + tex_info=tex_info, + label='SPECULAR', + location=(x_specular, y_specular), + is_data=True, + color_socket=specular_socket, + forced_image=blender_image_spec + ) + + if blender_specular_tint_tex_not_needed == True: + lum = lambda c: 0.3 * c[0] + 0.6 * c[1] + 0.1 * c[2] + specular_tint_socket.default_value = lum(blender_specular_tint[0][0]) + else: + blender_specular_tint = np.reshape(blender_specular_tint, width * height * 4) + # Create images in Blender, width and height are dummy values, then set packed file data + blender_image_tint = bpy.data.images.new('Specular Tint', width, height) + blender_image_tint.pixels.foreach_set(np.float32(blender_specular_tint)) + blender_image_tint.pack() + + # Create Textures in Blender + tex_info = tex_specular_color_info + if tex_info is None: + tex_info = tex_specular_info + if tex_info is None: + tex_info = tex_transmission_info + if tex_info is None: + tex_info = tex_base_color + + texture( + mh, + tex_info=tex_info, + label='SPECULAR TINT', + location=(x_tint, y_tint), + is_data=True, + color_socket=specular_tint_socket, + forced_image=blender_image_tint + ) + +def original_specular( mh, + specular_factor, + tex_specular_info, + specular_color_factor, + tex_specular_color_info, + original_specular_socket, + original_specularcolor_socket, + location_original_specular, + location_original_specularcolor + ): + + x_specular, y_specular = location_original_specular + x_specularcolor, y_specularcolor = location_original_specularcolor + + if tex_specular_info is None: + original_specular_socket.default_value = specular_factor + else: + # Mix specular factor + if specular_factor != 1.0: + node = mh.node_tree.nodes.new('ShaderNodeMath') + node.label = 'Specular Factor' + node.location = x_specular - 140, y_specular + node.operation = 'MULTIPLY' + # Outputs + mh.node_tree.links.new(original_specular_socket, node.outputs[0]) + # Inputs + original_specular_socket = node.inputs[0] + node.inputs[1].default_value = specular_factor + x_specular -= 200 + + texture( + mh, + tex_info=tex_specular_info, + label='SPECULAR', + location=(x_specular, y_specular), + is_data=True, + color_socket=None, + alpha_socket=original_specular_socket + ) + + if tex_specular_color_info is None: + specular_color_factor = list(specular_color_factor) + specular_color_factor.extend([1.0]) + original_specularcolor_socket.default_value = specular_color_factor + else: + specular_color_factor = list(specular_color_factor) + [1.0] + if specular_color_factor != [1.0, 1.0, 1.0, 1.0]: + # Mix specularColorFactor + node = mh.node_tree.nodes.new('ShaderNodeMixRGB') + node.label = 'SpecularColor Factor' + node.location = x_specularcolor - 140, y_specularcolor + node.blend_type = 'MULTIPLY' + # Outputs + mh.node_tree.links.new(original_specularcolor_socket, node.outputs[0]) + # Inputs + node.inputs['Fac'].default_value = 1.0 + original_specularcolor_socket = node.inputs['Color1'] + node.inputs['Color2'].default_value = specular_color_factor + x_specularcolor -= 200 + + texture( + mh, + tex_info=tex_specular_color_info, + label='SPECULAR COLOR', + location=(x_specularcolor, y_specularcolor), + color_socket=original_specularcolor_socket, + ) \ No newline at end of file diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_transmission.py b/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_transmission.py new file mode 100644 index 000000000..dab25d146 --- /dev/null +++ b/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_transmission.py @@ -0,0 +1,64 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2018-2022 The glTF-Blender-IO authors. + + +from ...io.com.gltf2_io import TextureInfo, MaterialNormalTextureInfoClass +from .gltf2_blender_texture import texture + + +# [Texture] => [Separate R] => [Transmission Factor] => +def transmission(mh, location, transmission_socket): + x, y = location + try: + ext = mh.pymat.extensions['KHR_materials_transmission'] + except Exception: + return + transmission_factor = ext.get('transmissionFactor', 0) + + # Default value is 0, so no transmission + if transmission_factor == 0: + return + + tex_info = ext.get('transmissionTexture') + if tex_info is not None: + tex_info = TextureInfo.from_dict(tex_info) + + if transmission_socket is None: + return + + if tex_info is None: + transmission_socket.default_value = transmission_factor + return + + # Mix transmission factor + if transmission_factor != 1: + node = mh.node_tree.nodes.new('ShaderNodeMath') + node.label = 'Transmission Factor' + node.location = x - 140, y + node.operation = 'MULTIPLY' + # Outputs + mh.node_tree.links.new(transmission_socket, node.outputs[0]) + # Inputs + transmission_socket = node.inputs[0] + node.inputs[1].default_value = transmission_factor + + x -= 200 + + # Separate RGB + node = mh.node_tree.nodes.new('ShaderNodeSeparateColor') + node.location = x - 150, y - 75 + # Outputs + mh.node_tree.links.new(transmission_socket, node.outputs['Red']) + # Inputs + transmission_socket = node.inputs[0] + + x -= 200 + + texture( + mh, + tex_info=tex_info, + label='TRANSMISSION', + location=(x, y), + is_data=True, + color_socket=transmission_socket, + ) diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_unlit.py b/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_unlit.py index 48ad46fdd..1ffdc7e41 100644 --- a/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_unlit.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_unlit.py @@ -24,12 +24,15 @@ def unlit(mh): mh.node_tree.links.new(mix_node.inputs[1], transparent_node.outputs[0]) mh.node_tree.links.new(mix_node.inputs[2], emission_node.outputs[0]) - _emission_socket, alpha_socket = make_output_nodes( + _emission_socket, alpha_socket, _, _ = make_output_nodes( mh, location=(420, 280) if mh.is_opaque() else (150, 130), + additional_location=None, #No additional location needed for Unlit shader_socket=mix_node.outputs[0], make_emission_socket=False, make_alpha_socket=not mh.is_opaque(), + make_volume_socket=None, # Not possible to have KHR_materials_volume with unlit + make_velvet_socket=None #Not possible to have KHR_materials_sheen with unlit ) base_color( diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_volume.py b/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_volume.py new file mode 100644 index 000000000..f909c7f63 --- /dev/null +++ b/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_volume.py @@ -0,0 +1,82 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2018-2021 The glTF-Blender-IO authors. + +from ...io.com.gltf2_io import TextureInfo, MaterialNormalTextureInfoClass +from .gltf2_blender_texture import texture + +def volume(mh, location, volume_socket, thickness_socket): + # implementation based on https://github.com/KhronosGroup/glTF-Blender-IO/issues/1454#issuecomment-928319444 + try: + ext = mh.pymat.extensions['KHR_materials_volume'] + except Exception: + return + + # Attenuation Color + attenuationColor = \ + mh.pymat.extensions['KHR_materials_volume'] \ + .get('attenuationColor') + # glTF is color3, Blender adds alpha + if attenuationColor is None: + attenuationColor = [1.0, 1.0, 1.0, 1.0] + else: + attenuationColor.extend([1.0]) + volume_socket.node.inputs[0].default_value = attenuationColor + + # Attenuation Distance / Density + attenuationDistance = mh.pymat.extensions['KHR_materials_volume'].get('attenuationDistance') + if attenuationDistance is None: + density = 0 + else: + density = 1.0 / attenuationDistance + volume_socket.node.inputs[1].default_value = density + + # thicknessFactor / thicknessTexture + x, y = location + try: + ext = mh.pymat.extensions['KHR_materials_volume'] + except Exception: + return + thickness_factor = ext.get('thicknessFactor', 0) + tex_info = ext.get('thicknessTexture') + if tex_info is not None: + tex_info = TextureInfo.from_dict(tex_info) + + if thickness_socket is None: + return + + if tex_info is None: + thickness_socket.default_value = thickness_factor + return + + # Mix thickness factor + if thickness_factor != 1: + node = mh.node_tree.nodes.new('ShaderNodeMath') + node.label = 'Thickness Factor' + node.location = x - 140, y + node.operation = 'MULTIPLY' + # Outputs + mh.node_tree.links.new(thickness_socket, node.outputs[0]) + # Inputs + thickness_socket = node.inputs[0] + node.inputs[1].default_value = thickness_factor + + x -= 200 + + # Separate RGB + node = mh.node_tree.nodes.new('ShaderNodeSeparateColor') + node.location = x - 150, y - 75 + # Outputs + mh.node_tree.links.new(thickness_socket, node.outputs['Green']) + # Inputs + thickness_socket = node.inputs[0] + + x -= 200 + + texture( + mh, + tex_info=tex_info, + label='THICKNESS', + location=(x, y), + is_data=True, + color_socket=thickness_socket, + ) diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_gltf.py b/io_scene_gltf2/blender/imp/gltf2_blender_gltf.py index f25564653..33713b974 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_gltf.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_gltf.py @@ -4,6 +4,8 @@ import bpy from mathutils import Vector, Quaternion, Matrix from .gltf2_blender_scene import BlenderScene +from ..com.gltf2_blender_ui import gltf2_KHR_materials_variants_variant, gltf2_KHR_materials_variants_primitive, gltf2_KHR_materials_variants_default_material +from .gltf2_blender_material import BlenderMaterial class BlenderGlTF(): @@ -180,6 +182,10 @@ class BlenderGlTF(): mesh.shapekey_names.append(shapekey_name) + # Manage KHR_materials_variants + BlenderGlTF.manage_material_variants(gltf) + + @staticmethod def find_unused_name(haystack, desired_name): """Finds a name not in haystack and <= 63 UTF-8 bytes. @@ -201,3 +207,25 @@ class BlenderGlTF(): suffix = '.%03d' % cntr cntr += 1 + + + @staticmethod + def manage_material_variants(gltf): + if not (gltf.data.extensions is not None and 'KHR_materials_variants' in gltf.data.extensions.keys()): + gltf.KHR_materials_variants = False + return + + gltf.KHR_materials_variants = True + # If there is no KHR_materials_variants data in scene, create it + if bpy.context.preferences.addons['io_scene_gltf2'].preferences.KHR_materials_variants_ui is False: + bpy.context.preferences.addons['io_scene_gltf2'].preferences.KHR_materials_variants_ui = True + # Setting preferences as dirty, to be sure that option is saved + bpy.context.preferences.is_dirty = True + + if len(bpy.data.scenes[0].gltf2_KHR_materials_variants_variants) > 0: + bpy.data.scenes[0].gltf2_KHR_materials_variants_variants.clear() + + for idx_variant, variant in enumerate(gltf.data.extensions['KHR_materials_variants']['variants']): + var = bpy.data.scenes[0].gltf2_KHR_materials_variants_variants.add() + var.name = variant['name'] + var.variant_idx = idx_variant diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_material.py b/io_scene_gltf2/blender/imp/gltf2_blender_material.py index 1d18c65d0..9a582f7e6 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_material.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_material.py @@ -48,6 +48,11 @@ class BlenderMaterial(): else: pbr_metallic_roughness(mh) + # Manage KHR_materials_variants + # We need to store link between material idx in glTF and Blender Material id + if gltf.KHR_materials_variants is True: + gltf.variant_mapping[str(material_idx) + str(vertex_color)] = mat + import_user_extensions('gather_import_material_after_hook', gltf, pymaterial, vertex_color, mat) @staticmethod diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py b/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py index a3c1bd349..b886dd258 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py @@ -11,6 +11,7 @@ from .gltf2_blender_material import BlenderMaterial from ...io.com.gltf2_io_debug import print_console from .gltf2_io_draco_compression_extension import decode_primitive from io_scene_gltf2.io.imp.gltf2_io_user_extensions import import_user_extensions +from ..com.gltf2_blender_ui import gltf2_KHR_materials_variants_primitive, gltf2_KHR_materials_variants_variant, gltf2_KHR_materials_variants_default_material class BlenderMesh(): @@ -343,12 +344,19 @@ def do_primitives(gltf, mesh_idx, skin_idx, mesh, ob): # ---- # Assign materials to faces has_materials = any(prim.material is not None for prim in pymesh.primitives) + # Even if no primitive have material, we need to create slots if some primitives have some variant + if has_materials is False: + has_materials = any(prim.extensions is not None and 'KHR_materials_variants' in prim.extensions.keys() for prim in pymesh.primitives) + + has_variant = prim.extensions is not None and 'KHR_materials_variants' in prim.extensions.keys() \ + and 'mappings' in prim.extensions['KHR_materials_variants'].keys() + if has_materials: material_indices = np.empty(num_faces, dtype=np.uint32) empty_material_slot_index = None f = 0 - for prim in pymesh.primitives: + for idx_prim, prim in enumerate(pymesh.primitives): if prim.material is not None: # Get the material pymaterial = gltf.data.materials[prim.material] @@ -358,19 +366,55 @@ def do_primitives(gltf, mesh_idx, skin_idx, mesh, ob): material_name = pymaterial.blender_material[vertex_color] # Put material in slot (if not there) - if material_name not in mesh.materials: + if not has_variant: + if material_name not in mesh.materials: + mesh.materials.append(bpy.data.materials[material_name]) + material_index = mesh.materials.find(material_name) + else: + # In case of variant, do not merge slots mesh.materials.append(bpy.data.materials[material_name]) - material_index = mesh.materials.find(material_name) + material_index = len(mesh.materials) - 1 else: - if empty_material_slot_index is None: + if not has_variant: + if empty_material_slot_index is None: + mesh.materials.append(None) + empty_material_slot_index = len(mesh.materials) - 1 + material_index = empty_material_slot_index + else: + # In case of variant, do not merge slots mesh.materials.append(None) - empty_material_slot_index = len(mesh.materials) - 1 - material_index = empty_material_slot_index + material_index = len(mesh.materials) - 1 material_indices[f:f + prim.num_faces].fill(material_index) f += prim.num_faces + # Manage variants + if has_variant: + + # Store default material + default_mat = mesh.gltf2_variant_default_materials.add() + default_mat.material_slot_index = material_index + default_mat.default_material = bpy.data.materials[material_name] if prim.material is not None else None + + for mapping in prim.extensions['KHR_materials_variants']['mappings']: + # Store, for each variant, the material link to this primitive + + variant_primitive = mesh.gltf2_variant_mesh_data.add() + variant_primitive.material_slot_index = material_index + if 'material' not in mapping.keys(): + # Default material + variant_primitive.material = None + else: + vertex_color = 'COLOR_0' if 'COLOR_0' in prim.attributes else None + if str(mapping['material']) + str(vertex_color) not in gltf.variant_mapping.keys(): + BlenderMaterial.create(gltf, mapping['material'], vertex_color) + variant_primitive.material = gltf.variant_mapping[str(mapping['material']) + str(vertex_color)] + + for variant in mapping['variants']: + vari = variant_primitive.variants.add() + vari.variant.variant_idx = variant + mesh.polygons.foreach_set('material_index', material_indices) # ---- diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_pbrMetallicRoughness.py b/io_scene_gltf2/blender/imp/gltf2_blender_pbrMetallicRoughness.py index 4c32c35d4..6f6279dcf 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_pbrMetallicRoughness.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_pbrMetallicRoughness.py @@ -1,13 +1,20 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright 2018-2021 The glTF-Blender-IO authors. +from re import M import bpy from ...io.com.gltf2_io import TextureInfo, MaterialPBRMetallicRoughness from ..com.gltf2_blender_material_helpers import get_gltf_node_name, create_settings_group +from ..com.gltf2_blender_material_helpers import get_gltf_pbr_non_converted_name, create_gltf_pbr_non_converted_group from .gltf2_blender_texture import texture from .gltf2_blender_KHR_materials_clearcoat import \ clearcoat, clearcoat_roughness, clearcoat_normal - +from .gltf2_blender_KHR_materials_transmission import transmission +from .gltf2_blender_KHR_materials_ior import ior +from .gltf2_blender_KHR_materials_volume import volume +from .gltf2_blender_KHR_materials_specular import specular +from .gltf2_blender_KHR_materials_sheen import sheen +from ...io.com.gltf2_io_constants import GLTF_IOR class MaterialHelper: """Helper class. Stores material stuff to be passed around everywhere.""" @@ -20,6 +27,8 @@ class MaterialHelper: if pymat.pbr_metallic_roughness is None: pymat.pbr_metallic_roughness = \ MaterialPBRMetallicRoughness.from_dict({}) + self.settings_node = None + self.original_pbr_node = None def is_opaque(self): alpha_mode = self.pymat.alpha_mode @@ -36,15 +45,58 @@ def pbr_metallic_roughness(mh: MaterialHelper): """Creates node tree for pbrMetallicRoughness materials.""" pbr_node = mh.node_tree.nodes.new('ShaderNodeBsdfPrincipled') pbr_node.location = 10, 300 + additional_location = 40, -370 # For occlusion and/or volume / original PBR extensions + + # Set IOR to 1.5, this is the default in glTF + # This value may be overidden later if IOR extension is set on file + pbr_node.inputs['IOR'].default_value = GLTF_IOR - make_output_nodes( + if mh.pymat.occlusion_texture is not None: + if mh.settings_node is None: + mh.settings_node = make_settings_node(mh) + mh.settings_node.location = additional_location + mh.settings_node.width = 180 + additional_location = additional_location[0], additional_location[1] - 150 + + need_volume_node = False + if mh.pymat.extensions and 'KHR_materials_volume' in mh.pymat.extensions: + if 'thicknessFactor' in mh.pymat.extensions['KHR_materials_volume'] \ + and mh.pymat.extensions['KHR_materials_volume']['thicknessFactor'] != 0.0: + + need_volume_node = True + + # We also need glTF Settings Node, to set thicknessFactor and thicknessTexture + mh.settings_node = make_settings_node(mh) + mh.settings_node.location = additional_location + mh.settings_node.width = 180 + volume_location = additional_location + additional_location = additional_location[0], additional_location[1] - 150 + + need_velvet_node = False + if mh.pymat.extensions and 'KHR_materials_sheen' in mh.pymat.extensions: + need_velvet_node = True + + _, _, volume_socket, velvet_node = make_output_nodes( mh, location=(250, 260), + additional_location=additional_location, shader_socket=pbr_node.outputs[0], - make_emission_socket=False, - make_alpha_socket=False, + make_emission_socket=False, # is managed by Principled shader node + make_alpha_socket=False, # is managed by Principled shader node + make_volume_socket=need_volume_node, + make_velvet_socket=need_velvet_node ) + if mh.pymat.extensions and 'KHR_materials_specular' in mh.pymat.extensions: + # We need glTF PBR Non Converted Extensions Node + mh.original_pbr_node = make_pbr_non_converted_extensions_node(mh) + mh.original_pbr_node.location = additional_location + mh.original_pbr_node.width = 180 + additional_location = additional_location[0], additional_location[1] - 150 + + if mh.pymat.extensions and 'KHR_materials_sheen': + pass #TOTOEXT + locs = calc_locations(mh) emission( @@ -75,13 +127,10 @@ def pbr_metallic_roughness(mh: MaterialHelper): ) if mh.pymat.occlusion_texture is not None: - node = make_settings_node(mh) - node.location = 40, -370 - node.width = 180 occlusion( mh, location=locs['occlusion'], - occlusion_socket=node.inputs['Occlusion'], + occlusion_socket=mh.settings_node.inputs['Occlusion'], ) clearcoat( @@ -102,6 +151,46 @@ def pbr_metallic_roughness(mh: MaterialHelper): normal_socket=pbr_node.inputs['Clearcoat Normal'], ) + transmission( + mh, + location=locs['transmission'], + transmission_socket=pbr_node.inputs['Transmission'] + ) + + if need_volume_node: + volume( + mh, + location=locs['volume_thickness'], + volume_socket=volume_socket, + thickness_socket=mh.settings_node.inputs[1] if mh.settings_node else None + ) + + specular( + mh, + location_specular=locs['specularTexture'], + location_specular_tint=locs['specularColorTexture'], + specular_socket=pbr_node.inputs['Specular'], + specular_tint_socket=pbr_node.inputs['Specular Tint'], + original_specular_socket=mh.original_pbr_node.inputs[0] if mh.original_pbr_node else None, + original_specularcolor_socket=mh.original_pbr_node.inputs[1] if mh.original_pbr_node else None, + location_original_specular=locs['original_specularTexture'], + location_original_specularcolor=locs['original_specularColorTexture'] + ) + + if need_velvet_node: + sheen( + mh, + location_sheenColor=locs['sheenColorTexture'], + location_sheenRoughness=locs['sheenRoughnessTexture'], + sheenColor_socket=velvet_node.inputs[0], + sheenRoughness_socket=velvet_node.inputs[1] + ) + + ior( + mh, + ior_socket=pbr_node.inputs['IOR'] + ) + def calc_locations(mh): """Calculate locations to place each bit of the node graph at.""" @@ -116,18 +205,53 @@ def calc_locations(mh): except Exception: clearcoat_ext = {} + try: + transmission_ext = mh.pymat.exntesions['KHR_materials_transmission'] + except: + transmission_ext = {} + + try: + volume_ext = mh.pymat.extensions['KHR_materials_volume'] + except Exception: + volume_ext = {} + + try: + specular_ext = mh.pymat.extensions['KHR_materials_specular'] + except: + specular_ext = {} + + try: + sheen_ext = mh.pymat.extensions['KHR_materials_sheen'] + except: + sheen_ext = {} + + locs['sheenColorTexture'] = (x, y) + if 'sheenColorTexture' in sheen_ext: + y -= height + locs['sheenRoughnessTexture'] = (x, y) + if 'sheenRoughnessTexture' in sheen_ext: + y -= height locs['base_color'] = (x, y) if mh.pymat.pbr_metallic_roughness.base_color_texture is not None or mh.vertex_color: y -= height locs['metallic_roughness'] = (x, y) if mh.pymat.pbr_metallic_roughness.metallic_roughness_texture is not None: y -= height + locs['specularTexture'] = (x, y) + if 'specularTexture' in specular_ext: + y -= height + locs['specularColorTexture'] = (x, y) + if 'specularColorTexture' in specular_ext: + y -= height locs['clearcoat'] = (x, y) if 'clearcoatTexture' in clearcoat_ext: y -= height locs['clearcoat_roughness'] = (x, y) if 'clearcoatRoughnessTexture' in clearcoat_ext: y -= height + locs['transmission'] = (x, y) + if 'transmissionTexture' in transmission_ext: + y -= height locs['emission'] = (x, y) if mh.pymat.emissive_texture is not None: y -= height @@ -140,6 +264,22 @@ def calc_locations(mh): locs['occlusion'] = (x, y) if mh.pymat.occlusion_texture is not None: y -= height + locs['volume_thickness'] = (x, y) + if 'thicknessTexture' in volume_ext: + y -= height + locs['original_specularTexture'] = (x, y) + if 'specularTexture' in specular_ext: + y -= height + locs['original_specularColorTexture'] = (x, y) + if 'specularColorTexture' in specular_ext: + y -= height + locs['original_sheenColorTexture'] = (x, y) + if 'sheenColorTexture' in sheen_ext: + y -= height + locs['original_sheenRoughnessTexture'] = (x, y) + if 'sheenRoughnessTexture' in sheen_ext: + y -= height + # Center things total_height = -y @@ -157,21 +297,29 @@ def calc_locations(mh): # [Texture] => [Emissive Factor] => -def emission(mh: MaterialHelper, location, color_socket, strength_socket=None): +def emission(mh: MaterialHelper, location, color_socket, strength_socket): x, y = location emissive_factor = mh.pymat.emissive_factor or [0, 0, 0] + strength = 1 + try: + # Get strength from KHR_materials_emissive_strength if exists + strength = mh.pymat.extensions['KHR_materials_emissive_strength']['emissiveStrength'] + except Exception: + pass + if color_socket is None: return if mh.pymat.emissive_texture is None: color_socket.default_value = emissive_factor + [1] + strength_socket.default_value = strength return # Put grayscale emissive factors into the Emission Strength e0, e1, e2 = emissive_factor if strength_socket and e0 == e1 == e2: - strength_socket.default_value = e0 + strength_socket.default_value = e0 * strength # Otherwise, use a multiply node for it else: @@ -189,6 +337,8 @@ def emission(mh: MaterialHelper, location, color_socket, strength_socket=None): x -= 200 + strength_socket.default_value = strength + texture( mh, tex_info=mh.pymat.emissive_texture, @@ -470,13 +620,16 @@ def occlusion(mh: MaterialHelper, location, occlusion_socket): def make_output_nodes( mh: MaterialHelper, location, + additional_location, shader_socket, make_emission_socket, make_alpha_socket, + make_volume_socket, + make_velvet_socket, # For sheen ): """ Creates the Material Output node and connects shader_socket to it. - If requested, it can also create places to hookup the emission/alpha + If requested, it can also create places to hookup the emission/alpha.sheen in between shader_socket and the Output node too. :return: a pair containing the sockets you should put emission and alpha @@ -484,6 +637,7 @@ def make_output_nodes( """ x, y = location emission_socket = None + velvet_node = None alpha_socket = None # Create an Emission node and add it to the shader. @@ -512,6 +666,31 @@ def make_output_nodes( x += 380 y += 125 + # Create an Velvet node add add it to the shader + # Note that you can not have Emission & Velvet at the same time + if make_velvet_socket: + # Velvet + node = mh.node_tree.nodes.new("ShaderNodeBsdfVelvet") + node.location = x + 50, y + 250 + # Node + velvet_node = node + # Outputs + velvet_output = node.outputs[0] + + # Add + node = mh.node_tree.nodes.new('ShaderNodeAddShader') + node.location = x + 250, y + 160 + # Inputs + mh.node_tree.links.new(node.inputs[0], velvet_output) + mh.node_tree.links.new(node.inputs[1], shader_socket) + # Outputs + shader_socket = node.outputs[0] + + + x += 380 + y += 125 + + # Mix with a Transparent BSDF. Mixing factor is the alpha value. if make_alpha_socket: # Transparent BSDF @@ -535,12 +714,23 @@ def make_output_nodes( y -= 210 # Material output - node = mh.node_tree.nodes.new('ShaderNodeOutputMaterial') - node.location = x + 70, y + 10 + node_output = mh.node_tree.nodes.new('ShaderNodeOutputMaterial') + node_output.location = x + 70, y + 10 + # Outputs - mh.node_tree.links.new(node.inputs[0], shader_socket) + mh.node_tree.links.new(node_output.inputs[0], shader_socket) + + # Volume Node + volume_socket = None + if make_volume_socket: + node = mh.node_tree.nodes.new('ShaderNodeVolumeAbsorption') + node.location = additional_location + # Outputs + mh.node_tree.links.new(node_output.inputs[1], node.outputs[0]) + volume_socket = node.outputs[0] - return emission_socket, alpha_socket + + return emission_socket, alpha_socket, volume_socket, velvet_node def make_settings_node(mh): @@ -560,3 +750,21 @@ def get_settings_group(): # Create a new node group gltf_node_group = create_settings_group(gltf_node_group_name) return gltf_node_group + +def make_pbr_non_converted_extensions_node(mh): + """ + Make a Group node with a hookup for PBR Non Converted Extensions. No effect in Blender, but + used to tell the exporter what the original map(s) should be. + """ + node = mh.node_tree.nodes.new('ShaderNodeGroup') + node.node_tree = get_pbr_non_converted_extensions_group() + return node + +def get_pbr_non_converted_extensions_group(): + gltf_node_group_name = get_gltf_pbr_non_converted_name() + if gltf_node_group_name in bpy.data.node_groups: + gltf_node_group = bpy.data.node_groups[gltf_node_group_name] + else: + # Create a new node group + gltf_node_group = create_gltf_pbr_non_converted_group(gltf_node_group_name) + return gltf_node_group \ No newline at end of file diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_texture.py b/io_scene_gltf2/blender/imp/gltf2_blender_texture.py index 24c9df7c5..12e6d5949 100644 --- a/io_scene_gltf2/blender/imp/gltf2_blender_texture.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_texture.py @@ -17,6 +17,7 @@ def texture( color_socket, alpha_socket=None, is_data=False, + forced_image=None ): """Creates nodes for a TextureInfo and hooks up the color/alpha outputs.""" x, y = location @@ -36,12 +37,15 @@ def texture( tex_img.location = x - 240, y tex_img.label = label # Get image - if pytexture.source is not None: - BlenderImage.create(mh.gltf, pytexture.source) - pyimg = mh.gltf.data.images[pytexture.source] - blender_image_name = pyimg.blender_image_name - if blender_image_name: - tex_img.image = bpy.data.images[blender_image_name] + if forced_image is None: + if pytexture.source is not None: + BlenderImage.create(mh.gltf, pytexture.source) + pyimg = mh.gltf.data.images[pytexture.source] + blender_image_name = pyimg.blender_image_name + if blender_image_name: + tex_img.image = bpy.data.images[blender_image_name] + else: + tex_img.image = forced_image # Set colorspace for data images if is_data: if tex_img.image: @@ -49,7 +53,8 @@ def texture( # Set filtering set_filtering(tex_img, pysampler) # Outputs - mh.node_tree.links.new(color_socket, tex_img.outputs['Color']) + if color_socket is not None: + mh.node_tree.links.new(color_socket, tex_img.outputs['Color']) if alpha_socket is not None: mh.node_tree.links.new(alpha_socket, tex_img.outputs['Alpha']) # Inputs diff --git a/io_scene_gltf2/io/com/gltf2_io_constants.py b/io_scene_gltf2/io/com/gltf2_io_constants.py index 19ead516e..175804a37 100755 --- a/io_scene_gltf2/io/com/gltf2_io_constants.py +++ b/io_scene_gltf2/io/com/gltf2_io_constants.py @@ -145,3 +145,5 @@ GLTF_DATA_TYPE_VEC4 = "VEC4" GLTF_DATA_TYPE_MAT2 = "MAT2" GLTF_DATA_TYPE_MAT3 = "MAT3" GLTF_DATA_TYPE_MAT4 = "MAT4" + +GLTF_IOR = 1.5 \ No newline at end of file diff --git a/io_scene_gltf2/io/com/gltf2_io_variants.py b/io_scene_gltf2/io/com/gltf2_io_variants.py new file mode 100644 index 000000000..3824fee40 --- /dev/null +++ b/io_scene_gltf2/io/com/gltf2_io_variants.py @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2018-2022 The glTF-Blender-IO authors. + +from io_scene_gltf2.io.com.gltf2_io import from_dict, from_union, from_none, from_float, from_str, from_list +from io_scene_gltf2.io.com.gltf2_io import to_float, to_class + +class Variant: + """defines variant for use with glTF 2.0.""" + def __init__(self, name, extensions, extras): + self.name = name + self.extensions = extensions + self.extras = extras + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + name = from_union([from_str, from_none], obj.get("name")) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + return Variant(name, extensions, extras) + + def to_dict(self): + result = {} + result["name"] = from_union([from_str, from_none], self.name) + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + return result diff --git a/io_scene_gltf2/io/imp/gltf2_io_gltf.py b/io_scene_gltf2/io/imp/gltf2_io_gltf.py index 9f096e691..75ce7265b 100755 --- a/io_scene_gltf2/io/imp/gltf2_io_gltf.py +++ b/io_scene_gltf2/io/imp/gltf2_io_gltf.py @@ -28,6 +28,7 @@ class glTFImporter(): self.accessor_cache = {} self.decode_accessor_cache = {} self.import_user_extensions = import_settings['import_user_extensions'] + self.variant_mapping = {} # Used to map between mgltf material idx and blender material, for Variants if 'loglevel' not in self.import_settings.keys(): self.import_settings['loglevel'] = logging.ERROR @@ -44,7 +45,13 @@ class glTFImporter(): 'KHR_texture_transform', 'KHR_materials_clearcoat', 'KHR_mesh_quantization', - 'KHR_draco_mesh_compression' + 'KHR_draco_mesh_compression', + 'KHR_materials_variants', + 'KHR_materials_emissive_strength', + 'KHR_materials_transmission', + 'KHR_materials_specular', + 'KHR_materials_sheen', + 'KHR_materials_ior' ] # Add extensions required supported by custom import extensions -- GitLab