diff --git a/addons/io_scene_gltf2/blender/com/gltf2_blender_material_helpers.py b/addons/io_scene_gltf2/blender/com/gltf2_blender_material_helpers.py index c1fa803c7..90831c7c5 100644 --- a/addons/io_scene_gltf2/blender/com/gltf2_blender_material_helpers.py +++ b/addons/io_scene_gltf2/blender/com/gltf2_blender_material_helpers.py @@ -18,6 +18,10 @@ def get_gltf_node_old_name(): return "glTF Settings" +# Old group name +def get_gltf_old_group_node_name(): + return "glTF Metallic Roughness" + def get_gltf_node_name(): return "glTF Material Output" diff --git a/addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_sampler.py b/addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_sampler.py index e8469ef8b..9145ea602 100644 --- a/addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_sampler.py +++ b/addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_sampler.py @@ -17,16 +17,27 @@ from ...io.exp.gltf2_io_user_extensions import export_user_extensions from ...io.com.gltf2_io_constants import TextureFilter, TextureWrap from .gltf2_blender_gather_cache import cached -from .gltf2_blender_get import ( - previous_node, - previous_socket, - get_const_from_socket, -) +from .material.gltf2_blender_search_node_tree import previous_node, previous_socket, get_const_from_socket, NodeSocket @cached -def gather_sampler(blender_shader_node: bpy.types.Node, export_settings): - wrap_s, wrap_t = __gather_wrap(blender_shader_node, export_settings) +def gather_sampler(blender_shader_node: bpy.types.Node, group_path_str, export_settings): + # reconstruct group_path from group_path_str + sep_item = "##~~gltf-sep~~##" + sep_inside_item = "##~~gltf-inside-sep~~##" + group_path = [] + tab = group_path_str.split(sep_item) + if len(tab) > 0: + group_path.append(bpy.data.materials[tab[0]]) + for idx, i in enumerate(tab[1:]): + subtab = i.split(sep_inside_item) + if idx == 0: + group_path.append(bpy.data.materials[tab[0]].node_tree.nodes[subtab[1]]) + else: + group_path.append(bpy.data.node_groups[subtab[0]].nodes[subtab[1]]) + + + wrap_s, wrap_t = __gather_wrap(blender_shader_node, group_path, export_settings) sampler = gltf2_io.Sampler( extensions=__gather_extensions(blender_shader_node, export_settings), @@ -90,7 +101,7 @@ def __gather_name(blender_shader_node, export_settings): return None -def __gather_wrap(blender_shader_node, export_settings): +def __gather_wrap(blender_shader_node, group_path, export_settings): # First gather from the Texture node if blender_shader_node.extension == 'EXTEND': wrap_s = TextureWrap.ClampToEdge @@ -108,7 +119,7 @@ def __gather_wrap(blender_shader_node, export_settings): # But still works for old files # Still needed for heterogen heterogeneous sampler, like MIRROR x REPEAT, for example # Take manual wrapping into account - result = detect_manual_uv_wrapping(blender_shader_node) + result = detect_manual_uv_wrapping(blender_shader_node, group_path) if result: if result['wrap_s'] is not None: wrap_s = result['wrap_s'] if result['wrap_t'] is not None: wrap_t = result['wrap_t'] @@ -120,7 +131,7 @@ def __gather_wrap(blender_shader_node, export_settings): return wrap_s, wrap_t -def detect_manual_uv_wrapping(blender_shader_node): +def detect_manual_uv_wrapping(blender_shader_node, group_path): # Detects UV wrapping done using math nodes. This is for emulating wrap # modes Blender doesn't support. It looks like # @@ -134,38 +145,38 @@ def detect_manual_uv_wrapping(blender_shader_node): # mode in each direction (or None), and next_socket. result = {} - comb = previous_node(blender_shader_node.inputs['Vector']) - if comb is None or comb.type != 'COMBXYZ': return None + comb = previous_node(NodeSocket(blender_shader_node.inputs['Vector'], group_path)) + if comb.node is None or comb.node.type != 'COMBXYZ': return None for soc in ['X', 'Y']: - node = previous_node(comb.inputs[soc]) - if node is None: return None + node = previous_node(NodeSocket(comb.node.inputs[soc], comb.group_path)) + if node.node is None: return None - if node.type == 'SEPXYZ': + if node.node.type == 'SEPXYZ': # Passed through without change wrap = None - prev_socket = previous_socket(comb.inputs[soc]) - elif node.type == 'MATH': + prev_socket = previous_socket(NodeSocket(comb.node.inputs[soc], comb.group_path)) + elif node.node.type == 'MATH': # Math node applies a manual wrap - if (node.operation == 'PINGPONG' and - get_const_from_socket(node.inputs[1], kind='VALUE') == 1.0): # scale = 1 + if (node.node.operation == 'PINGPONG' and + get_const_from_socket(NodeSocket(node.node.inputs[1], node.group_path), kind='VALUE') == 1.0): # scale = 1 wrap = TextureWrap.MirroredRepeat - elif (node.operation == 'WRAP' and - get_const_from_socket(node.inputs[1], kind='VALUE') == 0.0 and # min = 0 - get_const_from_socket(node.inputs[2], kind='VALUE') == 1.0): # max = 1 + elif (node.node.operation == 'WRAP' and + get_const_from_socket(NodeSocket(node.node.inputs[1], node.group_path), kind='VALUE') == 0.0 and # min = 0 + get_const_from_socket(NodeSocket(node.node.inputs[2], node.group_path), kind='VALUE') == 1.0): # max = 1 wrap = TextureWrap.Repeat else: return None - prev_socket = previous_socket(node.inputs[0]) + prev_socket = previous_socket(NodeSocket(node.node.inputs[0], node.group_path)) else: return None - if prev_socket is None: return None - prev_node = prev_socket.node + if prev_socket.socket is None: return None + prev_node = prev_socket.socket.node if prev_node.type != 'SEPXYZ': return None # Make sure X goes to X, etc. - if prev_socket.name != soc: return None + if prev_socket.socket.name != soc: return None # Make sure both attach to the same SeparateXYZ node if soc == 'X': sep = prev_node @@ -174,5 +185,5 @@ def detect_manual_uv_wrapping(blender_shader_node): result['wrap_s' if soc == 'X' else 'wrap_t'] = wrap - result['next_socket'] = sep.inputs[0] + result['next_socket'] = NodeSocket(sep.inputs[0], prev_socket.group_path) return result diff --git a/addons/io_scene_gltf2/blender/exp/gltf2_blender_get.py b/addons/io_scene_gltf2/blender/exp/gltf2_blender_get.py index dda964e67..aaf34cd53 100644 --- a/addons/io_scene_gltf2/blender/exp/gltf2_blender_get.py +++ b/addons/io_scene_gltf2/blender/exp/gltf2_blender_get.py @@ -13,12 +13,6 @@ # limitations under the License. import bpy -from mathutils import Vector, Matrix -from ...blender.com.gltf2_blender_conversion import texture_transform_blender_to_gltf -from ...io.com import gltf2_io_debug -from ..com.gltf2_blender_material_helpers import get_gltf_node_name, get_gltf_node_old_name -from .material import gltf2_blender_search_node_tree - def get_animation_target(action_group: bpy.types.ActionGroup): return action_group.channels[0].data_path.split('.')[-1] @@ -41,292 +35,3 @@ def get_object_from_datapath(blender_object, data_path: str): # path_attr = data_path return prop - - -def get_node_socket(blender_material, type, name): - """ - For a given material input name, retrieve the corresponding node tree socket for a given node type. - - :param blender_material: a blender material for which to get the socket - :return: a blender NodeSocket for a given type - """ - nodes = [n for n in blender_material.node_tree.nodes if isinstance(n, type) and not n.mute] - nodes = [node for node in nodes if check_if_is_linked_to_active_output(node.outputs[0])] - inputs = sum([[input for input in node.inputs if input.name == name] for node in nodes], []) - if inputs: - return inputs[0] - return None - - -def get_socket(blender_material: bpy.types.Material, name: str, volume=False): - """ - For a given material input name, retrieve the corresponding node tree socket. - - :param blender_material: a blender material for which to get the socket - :param name: the name of the socket - :return: a blender NodeSocket - """ - if blender_material.node_tree and blender_material.use_nodes: - #i = [input for input in blender_material.node_tree.inputs] - #o = [output for output in blender_material.node_tree.outputs] - if name == "Emissive": - # Check for a dedicated Emission node first, it must supersede the newer built-in one - # because the newer one is always present in all Principled BSDF materials. - emissive_socket = get_node_socket(blender_material, bpy.types.ShaderNodeEmission, "Color") - if emissive_socket: - return emissive_socket - # If a dedicated Emission node was not found, fall back to the Principled BSDF Emission Color socket. - name = "Emission Color" - type = bpy.types.ShaderNodeBsdfPrincipled - elif name == "Background": - type = bpy.types.ShaderNodeBackground - name = "Color" - else: - if volume is False: - type = bpy.types.ShaderNodeBsdfPrincipled - else: - type = bpy.types.ShaderNodeVolumeAbsorption - - return get_node_socket(blender_material, type, name) - - return None - - -def get_socket_old(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_names = [get_gltf_node_name().lower(), get_gltf_node_old_name().lower()] - if blender_material.node_tree and blender_material.use_nodes: - # Some weird node groups with missing datablock can have no node_tree, so checking n.node_tree (See #1797) - nodes = [n for n in blender_material.node_tree.nodes if \ - isinstance(n, bpy.types.ShaderNodeGroup) and \ - n.node_tree is not None and - (n.node_tree.name.startswith('glTF Metallic Roughness') or n.node_tree.name.lower() in gltf_node_group_names)] - 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: - return True - - if len(link.to_node.outputs) > 0: # ignore non active output, not having output sockets - ret = check_if_is_linked_to_active_output(link.to_node.outputs[0]) # recursive until find an output material node - if ret is True: - return True - - return False - -def find_shader_image_from_shader_socket(shader_socket, max_hops=10): - """Find any ShaderNodeTexImage in the path from the socket.""" - if shader_socket is None: - return None - - if max_hops <= 0: - return None - - for link in shader_socket.links: - if isinstance(link.from_node, bpy.types.ShaderNodeTexImage): - return link.from_node - - for socket in link.from_node.inputs.values(): - image = find_shader_image_from_shader_socket(shader_socket=socket, max_hops=max_hops - 1) - if image is not None: - return image - - return None - - -def get_texture_transform_from_mapping_node(mapping_node): - if mapping_node.vector_type not in ["TEXTURE", "POINT", "VECTOR"]: - gltf2_io_debug.print_console("WARNING", - "Skipping exporting texture transform because it had type " + - mapping_node.vector_type + "; recommend using POINT instead" - ) - return None - - - rotation_0, rotation_1 = mapping_node.inputs['Rotation'].default_value[0], mapping_node.inputs['Rotation'].default_value[1] - if rotation_0 or rotation_1: - # TODO: can we handle this? - gltf2_io_debug.print_console("WARNING", - "Skipping exporting texture transform because it had non-zero " - "rotations in the X/Y direction; only a Z rotation can be exported!" - ) - return None - - mapping_transform = {} - mapping_transform["offset"] = [mapping_node.inputs['Location'].default_value[0], mapping_node.inputs['Location'].default_value[1]] - mapping_transform["rotation"] = mapping_node.inputs['Rotation'].default_value[2] - mapping_transform["scale"] = [mapping_node.inputs['Scale'].default_value[0], mapping_node.inputs['Scale'].default_value[1]] - - if mapping_node.vector_type == "TEXTURE": - # This means use the inverse of the TRS transform. - def inverted(mapping_transform): - offset = mapping_transform["offset"] - rotation = mapping_transform["rotation"] - scale = mapping_transform["scale"] - - # Inverse of a TRS is not always a TRS. This function will be right - # at least when the following don't occur. - if abs(rotation) > 1e-5 and abs(scale[0] - scale[1]) > 1e-5: - return None - if abs(scale[0]) < 1e-5 or abs(scale[1]) < 1e-5: - return None - - new_offset = Matrix.Rotation(-rotation, 3, 'Z') @ Vector((-offset[0], -offset[1], 1)) - new_offset[0] /= scale[0]; new_offset[1] /= scale[1] - return { - "offset": new_offset[0:2], - "rotation": -rotation, - "scale": [1/scale[0], 1/scale[1]], - } - - mapping_transform = inverted(mapping_transform) - if mapping_transform is None: - gltf2_io_debug.print_console("WARNING", - "Skipping exporting texture transform with type TEXTURE because " - "we couldn't convert it to TRS; recommend using POINT instead" - ) - return None - - elif mapping_node.vector_type == "VECTOR": - # Vectors don't get translated - mapping_transform["offset"] = [0, 0] - - texture_transform = texture_transform_blender_to_gltf(mapping_transform) - - if all([component == 0 for component in texture_transform["offset"]]): - del(texture_transform["offset"]) - if all([component == 1 for component in texture_transform["scale"]]): - del(texture_transform["scale"]) - if texture_transform["rotation"] == 0: - del(texture_transform["rotation"]) - - if len(texture_transform) == 0: - return None - - return texture_transform - - -def get_node(data_path): - """Return Blender node on a given Blender data path.""" - if data_path is None: - return None - - index = data_path.find("[\"") - if (index == -1): - return None - - node_name = data_path[(index + 2):] - - index = node_name.find("\"") - if (index == -1): - return None - - return node_name[:(index)] - - -def get_factor_from_socket(socket, kind): - """ - For baseColorFactor, metallicFactor, etc. - Get a constant value from a socket, or a constant value - from a MULTIPLY node just before the socket. - kind is either 'RGB' or 'VALUE'. - """ - fac = get_const_from_socket(socket, kind) - if fac is not None: - return fac - - node = previous_node(socket) - if node is not None: - x1, x2 = None, None - if kind == 'RGB': - if node.type == 'MIX' and node.data_type == "RGBA" and node.blend_type == 'MULTIPLY': - # TODO: handle factor in inputs[0]? - x1 = get_const_from_socket(node.inputs[6], kind) - x2 = get_const_from_socket(node.inputs[7], kind) - if kind == 'VALUE': - if node.type == 'MATH' and node.operation == 'MULTIPLY': - x1 = get_const_from_socket(node.inputs[0], kind) - x2 = get_const_from_socket(node.inputs[1], kind) - if x1 is not None and x2 is None: return x1 - if x2 is not None and x1 is None: return x2 - - return None - -def get_const_from_default_value_socket(socket, kind): - if kind == 'RGB': - if socket.type != 'RGBA': return None - return list(socket.default_value)[:3] - if kind == 'VALUE': - if socket.type != 'VALUE': return None - return socket.default_value - return None - - -def get_const_from_socket(socket, kind): - if not socket.is_linked: - if kind == 'RGB': - if socket.type != 'RGBA': return None - return list(socket.default_value)[:3] - if kind == 'VALUE': - if socket.type != 'VALUE': return None - return socket.default_value - - # Handle connection to a constant RGB/Value node - prev_node = previous_node(socket) - if prev_node is not None: - if kind == 'RGB' and prev_node.type == 'RGB': - return list(prev_node.outputs[0].default_value)[:3] - if kind == 'VALUE' and prev_node.type == 'VALUE': - return prev_node.outputs[0].default_value - - return None - - -def previous_socket(socket): - while True: - if not socket.is_linked: - return None - - from_socket = socket.links[0].from_socket - - # Skip over reroute nodes - if from_socket.node.type == 'REROUTE': - socket = from_socket.node.inputs[0] - continue - - return from_socket - - -def previous_node(socket): - prev_socket = previous_socket(socket) - if prev_socket is not None: - return prev_socket.node - return None - -def get_tex_from_socket(socket): - result = gltf2_blender_search_node_tree.from_socket( - socket, - gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage)) - if not result: - return None - if result[0].shader_node.image is None: - return None - return result[0] - -def has_image_node_from_socket(socket): - return get_tex_from_socket(socket) is not None - -def image_tex_is_valid_from_socket(socket): - res = get_tex_from_socket(socket) - return res is not None and res.shader_node.image is not None and res.shader_node.image.channels != 0 diff --git a/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_clearcoat.py b/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_clearcoat.py index 753ae1415..c29def9c9 100644 --- a/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_clearcoat.py +++ b/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_clearcoat.py @@ -14,8 +14,8 @@ import bpy from .....io.com.gltf2_io_extensions import Extension -from ....exp import gltf2_blender_get from ...material import gltf2_blender_gather_texture_info +from ..gltf2_blender_search_node_tree import has_image_node_from_socket, get_socket, get_factor_from_socket def export_clearcoat(blender_material, export_settings): clearcoat_enabled = False @@ -25,15 +25,15 @@ def export_clearcoat(blender_material, export_settings): clearcoat_extension = {} clearcoat_roughness_slots = () - clearcoat_socket = gltf2_blender_get.get_socket(blender_material, 'Coat Weight') - clearcoat_roughness_socket = gltf2_blender_get.get_socket(blender_material, 'Coat Roughness') - clearcoat_normal_socket = gltf2_blender_get.get_socket(blender_material, 'Coat Normal') + clearcoat_socket = get_socket(blender_material, 'Coat Weight') + clearcoat_roughness_socket = get_socket(blender_material, 'Coat Roughness') + clearcoat_normal_socket = get_socket(blender_material, 'Coat Normal') - if isinstance(clearcoat_socket, bpy.types.NodeSocket) and not clearcoat_socket.is_linked: - clearcoat_extension['clearcoatFactor'] = clearcoat_socket.default_value + if isinstance(clearcoat_socket.socket, bpy.types.NodeSocket) and not clearcoat_socket.socket.is_linked: + clearcoat_extension['clearcoatFactor'] = clearcoat_socket.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') + elif has_image_node_from_socket(clearcoat_socket, export_settings): + fac = 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 @@ -42,10 +42,10 @@ def export_clearcoat(blender_material, export_settings): if not clearcoat_enabled: return 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') + if isinstance(clearcoat_roughness_socket.socket, bpy.types.NodeSocket) and not clearcoat_roughness_socket.socket.is_linked: + clearcoat_extension['clearcoatRoughnessFactor'] = clearcoat_roughness_socket.socket.default_value + elif has_image_node_from_socket(clearcoat_roughness_socket, export_settings): + fac = 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 @@ -81,7 +81,7 @@ def export_clearcoat(blender_material, export_settings): clearcoat_extension['clearcoatRoughnessTexture'] = clearcoat_roughness_texture uvmap_infos.update({'clearcoatRoughnessTexture': uvmap_info}) - if gltf2_blender_get.has_image_node_from_socket(clearcoat_normal_socket): + if has_image_node_from_socket(clearcoat_normal_socket, export_settings): clearcoat_normal_texture, uvmap_info, _ = gltf2_blender_gather_texture_info.gather_material_normal_texture_info_class( clearcoat_normal_socket, (clearcoat_normal_socket,), diff --git a/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_emission.py b/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_emission.py index 812143476..889ed7ea6 100644 --- a/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_emission.py +++ b/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_emission.py @@ -14,20 +14,26 @@ import bpy from .....io.com.gltf2_io_extensions import Extension -from ....exp import gltf2_blender_get from ...material import gltf2_blender_gather_texture_info +from ..gltf2_blender_search_node_tree import \ + get_const_from_default_value_socket, \ + get_socket, \ + get_factor_from_socket, \ + get_const_from_socket, \ + NodeSocket, \ + get_socket_from_gltf_material_node 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): + emissive_socket = get_socket(blender_material, "Emissive") + if emissive_socket.socket is None: + emissive_socket = get_socket_from_gltf_material_node(blender_material, "EmissiveFactor") + if isinstance(emissive_socket.socket, bpy.types.NodeSocket): if export_settings['gltf_image_format'] != "NONE": - factor = gltf2_blender_get.get_factor_from_socket(emissive_socket, kind='RGB') + factor = get_factor_from_socket(emissive_socket, kind='RGB') else: - factor = gltf2_blender_get.get_const_from_default_value_socket(emissive_socket, kind='RGB') + factor = get_const_from_default_value_socket(emissive_socket, kind='RGB') - if factor is None and emissive_socket.is_linked: + if factor is None and emissive_socket.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] @@ -36,12 +42,12 @@ def export_emission_factor(blender_material, export_settings): # 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'] + if emissive_socket.socket.node.type == 'EMISSION': + strength_socket = emissive_socket.socket.node.inputs['Strength'] + elif 'Emission Strength' in emissive_socket.socket.node.inputs: + strength_socket = emissive_socket.socket.node.inputs['Emission Strength'] strength = ( - gltf2_blender_get.get_const_from_socket(strength_socket, kind='VALUE') + get_const_from_socket(NodeSocket(strength_socket, emissive_socket.group_path), kind='VALUE') if strength_socket is not None else None ) @@ -59,9 +65,9 @@ def export_emission_factor(blender_material, export_settings): 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 = get_socket(blender_material, "Emissive") + if emissive.socket is None: + emissive = get_socket_from_gltf_material_node(blender_material, "Emissive") emissive_texture, uvmap_info, _ = gltf2_blender_gather_texture_info.gather_texture_info(emissive, (emissive,), (), export_settings) return emissive_texture, {'emissiveTexture': uvmap_info} diff --git a/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_ior.py b/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_ior.py index 90691ea98..97b4d08fa 100644 --- a/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_ior.py +++ b/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_ior.py @@ -14,20 +14,20 @@ from .....io.com.gltf2_io_extensions import Extension from .....io.com.gltf2_io_constants import GLTF_IOR -from ....exp import gltf2_blender_get +from ..gltf2_blender_search_node_tree import get_socket def export_ior(blender_material, extensions, export_settings): - ior_socket = gltf2_blender_get.get_socket(blender_material, 'IOR') + ior_socket = get_socket(blender_material, 'IOR') - if not ior_socket: + if not ior_socket.socket: return None # We don't manage case where socket is linked, always check default value - if ior_socket.is_linked: + if ior_socket.socket.is_linked: # TODOExt: add warning? return None - if ior_socket.default_value == GLTF_IOR: + if ior_socket.socket.default_value == GLTF_IOR: return None # Export only if the following extensions are exported: @@ -41,6 +41,6 @@ def export_ior(blender_material, extensions, export_settings): return None ior_extension = {} - ior_extension['ior'] = ior_socket.default_value + ior_extension['ior'] = ior_socket.socket.default_value return Extension('KHR_materials_ior', ior_extension, False) diff --git a/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_sheen.py b/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_sheen.py index faac0a2da..3788381aa 100644 --- a/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_sheen.py +++ b/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_sheen.py @@ -14,47 +14,48 @@ import bpy from .....io.com.gltf2_io_extensions import Extension -from ....exp import gltf2_blender_get from ...material import gltf2_blender_gather_texture_info +from ..gltf2_blender_search_node_tree import \ + has_image_node_from_socket, \ + get_socket, \ + get_factor_from_socket def export_sheen(blender_material, export_settings): sheen_extension = {} - sheenTint_socket = gltf2_blender_get.get_socket(blender_material, "Sheen Tint") - sheenRoughness_socket = gltf2_blender_get.get_socket(blender_material, "Sheen Roughness") - sheen_socket = gltf2_blender_get.get_socket(blender_material, "Sheen Weight") + sheenTint_socket = get_socket(blender_material, "Sheen Tint") + sheenRoughness_socket = get_socket(blender_material, "Sheen Roughness") + sheen_socket = get_socket(blender_material, "Sheen Weight") - if sheenTint_socket is None or sheenRoughness_socket is None or sheen_socket is None: + if sheenTint_socket.socket is None or sheenRoughness_socket.socket is None or sheen_socket.socket is None: return None, {} - if sheen_socket.is_linked is False and sheen_socket.default_value == 0.0: + if sheen_socket.socket.is_linked is False and sheen_socket.socket.default_value == 0.0: return None, {} uvmap_infos = {} #TODOExt : What to do if sheen_socket is linked? or is not between 0 and 1? - sheenTint_non_linked = isinstance(sheenTint_socket, bpy.types.NodeSocket) and not sheenTint_socket.is_linked - sheenRoughness_non_linked = isinstance(sheenRoughness_socket, bpy.types.NodeSocket) and not sheenRoughness_socket.is_linked + sheenTint_non_linked = isinstance(sheenTint_socket.socket, bpy.types.NodeSocket) and not sheenTint_socket.socket.is_linked + sheenRoughness_non_linked = isinstance(sheenRoughness_socket.socket, bpy.types.NodeSocket) and not sheenRoughness_socket.socket.is_linked - use_actives_uvmaps = [] - if sheenTint_non_linked is True: - color = sheenTint_socket.default_value[:3] + color = sheenTint_socket.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(sheenTint_socket, kind='RGB') + fac = get_factor_from_socket(sheenTint_socket, kind='RGB') if fac is None: fac = [1.0, 1.0, 1.0] # Default is 0.0/0.0/0.0, so we need to set it to 1 if no factor 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(sheenTint_socket): + if has_image_node_from_socket(sheenTint_socket, export_settings): original_sheenColor_texture, uvmap_info, _ = gltf2_blender_gather_texture_info.gather_texture_info( sheenTint_socket, (sheenTint_socket,), @@ -65,19 +66,19 @@ def export_sheen(blender_material, export_settings): uvmap_infos.update({'sheenColorTexture': uvmap_info}) if sheenRoughness_non_linked is True: - fac = sheenRoughness_socket.default_value + fac = sheenRoughness_socket.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') + fac = get_factor_from_socket(sheenRoughness_socket, kind='VALUE') if fac is None: fac = 1.0 # Default is 0.0 so we need to set it to 1.0 if no factor 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): + if has_image_node_from_socket(sheenRoughness_socket, export_settings): original_sheenRoughness_texture, uvmap_info , _ = gltf2_blender_gather_texture_info.gather_texture_info( sheenRoughness_socket, (sheenRoughness_socket,), diff --git a/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_specular.py b/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_specular.py index 2a78884bf..003fd3584 100644 --- a/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_specular.py +++ b/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_specular.py @@ -14,32 +14,37 @@ import bpy from .....io.com.gltf2_io_extensions import Extension -from ....exp import gltf2_blender_get from ...material.gltf2_blender_gather_texture_info import gather_texture_info +from ..gltf2_blender_search_node_tree import \ + has_image_node_from_socket, \ + get_socket_from_gltf_material_node, \ + get_socket, \ + get_factor_from_socket + def export_specular(blender_material, export_settings): specular_extension = {} - specular_socket = gltf2_blender_get.get_socket(blender_material, 'Specular IOR Level') - speculartint_socket = gltf2_blender_get.get_socket(blender_material, 'Specular Tint') + specular_socket = get_socket(blender_material, 'Specular IOR Level') + speculartint_socket = get_socket(blender_material, 'Specular Tint') - if specular_socket is None or speculartint_socket is None: + if specular_socket.socket is None or speculartint_socket.socket is None: return None, {} uvmap_infos = {} - specular_non_linked = isinstance(specular_socket, bpy.types.NodeSocket) and not specular_socket.is_linked - specularcolor_non_linked = isinstance(speculartint_socket, bpy.types.NodeSocket) and not speculartint_socket.is_linked + specular_non_linked = isinstance(specular_socket.socket, bpy.types.NodeSocket) and not specular_socket.socket.is_linked + specularcolor_non_linked = isinstance(speculartint_socket.socket, bpy.types.NodeSocket) and not speculartint_socket.socket.is_linked if specular_non_linked is True: - fac = specular_socket.default_value + fac = specular_socket.socket.default_value if fac != 1.0: specular_extension['specularFactor'] = fac if fac == 0.0: return None, {} else: # Factor - fac = gltf2_blender_get.get_factor_from_socket(specular_socket, kind='VALUE') + fac = get_factor_from_socket(specular_socket, kind='VALUE') if fac is not None and fac != 1.0: specular_extension['specularFactor'] = fac @@ -47,7 +52,7 @@ def export_specular(blender_material, export_settings): return None, {} # Texture - if gltf2_blender_get.has_image_node_from_socket(specular_socket): + if has_image_node_from_socket(specular_socket, export_settings): original_specular_texture, uvmap_info, _ = gather_texture_info( specular_socket, (specular_socket,), @@ -58,17 +63,17 @@ def export_specular(blender_material, export_settings): uvmap_infos.update({'specularTexture': uvmap_info}) if specularcolor_non_linked is True: - color = speculartint_socket.default_value[:3] + color = speculartint_socket.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(speculartint_socket, kind='RGB') + fac = get_factor_from_socket(speculartint_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(speculartint_socket): + if has_image_node_from_socket(speculartint_socket, export_settings): original_specularcolor_texture, uvmap_info, _ = gather_texture_info( speculartint_socket, (speculartint_socket,), diff --git a/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_transmission.py b/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_transmission.py index 7fb6638ec..d8085ba93 100644 --- a/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_transmission.py +++ b/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_transmission.py @@ -14,8 +14,11 @@ import bpy from .....io.com.gltf2_io_extensions import Extension -from ....exp import gltf2_blender_get from ...material import gltf2_blender_gather_texture_info +from ..gltf2_blender_search_node_tree import \ + has_image_node_from_socket, \ + get_socket, \ + get_factor_from_socket def export_transmission(blender_material, export_settings): transmission_enabled = False @@ -24,13 +27,13 @@ def export_transmission(blender_material, export_settings): transmission_extension = {} transmission_slots = () - transmission_socket = gltf2_blender_get.get_socket(blender_material, 'Transmission Weight') + transmission_socket = get_socket(blender_material, 'Transmission Weight') - if isinstance(transmission_socket, bpy.types.NodeSocket) and not transmission_socket.is_linked: - transmission_extension['transmissionFactor'] = transmission_socket.default_value + if isinstance(transmission_socket.socket, bpy.types.NodeSocket) and not transmission_socket.socket.is_linked: + transmission_extension['transmissionFactor'] = transmission_socket.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') + elif has_image_node_from_socket(transmission_socket, export_settings): + fac = 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 diff --git a/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_volume.py b/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_volume.py index 938161b1a..610f321e0 100644 --- a/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_volume.py +++ b/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_volume.py @@ -14,8 +14,13 @@ import bpy from .....io.com.gltf2_io_extensions import Extension -from ....exp import gltf2_blender_get from ...material import gltf2_blender_gather_texture_info +from ..gltf2_blender_search_node_tree import \ + has_image_node_from_socket, \ + get_const_from_default_value_socket, \ + get_socket_from_gltf_material_node, \ + get_socket, \ + get_factor_from_socket def export_volume(blender_material, export_settings): @@ -23,10 +28,10 @@ def export_volume(blender_material, export_settings): # If no transmission --> No volume transmission_enabled = False - transmission_socket = gltf2_blender_get.get_socket(blender_material, 'Transmission Weight') - 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_socket = get_socket(blender_material, 'Transmission Weight') + if isinstance(transmission_socket.socket, bpy.types.NodeSocket) and not transmission_socket.socket.is_linked: + transmission_enabled = transmission_socket.socket.default_value > 0 + elif has_image_node_from_socket(transmission_socket, export_settings): transmission_enabled = True if transmission_enabled is False: @@ -37,43 +42,43 @@ def export_volume(blender_material, export_settings): thickness_slots = () uvmap_info = {} - thicknesss_socket = gltf2_blender_get.get_socket_old(blender_material, 'Thickness') - if thicknesss_socket is None: + thickness_socket = get_socket_from_gltf_material_node(blender_material, 'Thickness') + if thickness_socket.socket is None: # If no thickness (here because there is no glTF Material Output node), no volume extension export return None, {} - 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) + density_socket = get_socket(blender_material, 'Density', volume=True) + attenuation_color_socket = get_socket(blender_material, 'Color', volume=True) # Even if density or attenuation are not set, we export volume extension - if isinstance(attenuation_color_socket, bpy.types.NodeSocket): - rgb = gltf2_blender_get.get_const_from_default_value_socket(attenuation_color_socket, kind='RGB') + if isinstance(attenuation_color_socket.socket, bpy.types.NodeSocket): + rgb = 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') + if isinstance(density_socket.socket, bpy.types.NodeSocket): + density = 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 isinstance(thickness_socket.socket, bpy.types.NodeSocket) and not thickness_socket.socket.is_linked: + val = thickness_socket.socket.default_value if val == 0.0: # If no thickness, no volume extension export return 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') + elif has_image_node_from_socket(thickness_socket, export_settings): + fac = get_factor_from_socket(thickness_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,) + thickness_slots = (thickness_socket,) if len(thickness_slots) > 0: combined_texture, uvmap_info, _ = gltf2_blender_gather_texture_info.gather_texture_info( - thicknesss_socket, + thickness_socket, thickness_slots, (), export_settings, diff --git a/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_gather_image.py b/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_gather_image.py index 248875d0f..9ef3764e2 100644 --- a/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_gather_image.py +++ b/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_gather_image.py @@ -23,7 +23,7 @@ from ....io.exp.gltf2_io_user_extensions import export_user_extensions from ..gltf2_blender_gather_cache import cached from .extensions.gltf2_blender_image import Channel, ExportImage, FillImage -from ..gltf2_blender_get import get_tex_from_socket +from .gltf2_blender_search_node_tree import get_texture_node_from_socket, NodeSocket @cached def gather_image( @@ -124,7 +124,7 @@ def __gather_extras(sockets, export_settings): def __gather_mime_type(sockets, export_image, export_settings): # force png or webp if Alpha contained so we can export alpha for socket in sockets: - if socket.name == "Alpha": + if socket.socket.name == "Alpha": if export_settings["gltf_image_format"] == "WEBP": return "image/webp" else: @@ -201,7 +201,7 @@ def __get_image_data(sockets, default_sockets, export_settings) -> ExportImage: # For shared resources, such as images, we just store the portion of data that is needed in the glTF property # in a helper class. During generation of the glTF in the exporter these will then be combined to actual binary # resources. - results = [get_tex_from_socket(socket) for socket in sockets] + results = [get_texture_node_from_socket(socket, export_settings) for socket in sockets] # Check if we need a simple mapping or more complex calculation # There is currently no complex calculation for any textures @@ -246,23 +246,23 @@ def __get_image_data_mapping(sockets, default_sockets, results, export_settings) dst_chan = None # some sockets need channel rewriting (gltf pbr defines fixed channels for some attributes) - if socket.name == 'Metallic': + if socket.socket.name == 'Metallic': dst_chan = Channel.B - elif socket.name == 'Roughness': + elif socket.socket.name == 'Roughness': dst_chan = Channel.G - elif socket.name == 'Occlusion': + elif socket.socket.name == 'Occlusion': dst_chan = Channel.R - elif socket.name == 'Alpha': + elif socket.socket.name == 'Alpha': dst_chan = Channel.A - elif socket.name == 'Coat Weight': + elif socket.socket.name == 'Coat Weight': dst_chan = Channel.R - elif socket.name == 'Coat Roughness': + elif socket.socket.name == 'Coat Roughness': dst_chan = Channel.G - elif socket.name == 'Thickness': # For KHR_materials_volume + elif socket.socket.name == 'Thickness': # For KHR_materials_volume dst_chan = Channel.G - elif socket.name == "Specular IOR Level": # For KHR_material_specular + elif socket.socket.name == "Specular IOR Level": # For KHR_material_specular dst_chan = Channel.A - elif socket.name == "Sheen Roughness": # For KHR_materials_sheen + elif socket.socket.name == "Sheen Roughness": # For KHR_materials_sheen dst_chan = Channel.A if dst_chan is not None: @@ -270,12 +270,12 @@ def __get_image_data_mapping(sockets, default_sockets, results, export_settings) # Since metal/roughness are always used together, make sure # the other channel is filled. - if socket.name == 'Metallic' and not composed_image.is_filled(Channel.G): + if socket.socket.name == 'Metallic' and not composed_image.is_filled(Channel.G): if default_roughness is not None: composed_image.fill_with(Channel.G, default_roughness) else: composed_image.fill_white(Channel.G) - elif socket.name == 'Roughness' and not composed_image.is_filled(Channel.B): + elif socket.socket.name == 'Roughness' and not composed_image.is_filled(Channel.B): if default_metallic is not None: composed_image.fill_with(Channel.B, default_metallic) else: diff --git a/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_gather_materials.py b/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_gather_materials.py index c693bdf5d..69066b02c 100644 --- a/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_gather_materials.py +++ b/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_gather_materials.py @@ -20,7 +20,6 @@ from ....io.exp.gltf2_io_user_extensions import export_user_extensions from ....io.com.gltf2_io_debug import print_console from ...com.gltf2_blender_extras import generate_extras -from ...exp import gltf2_blender_get from ..gltf2_blender_gather_cache import cached, cached_by_key from . import gltf2_blender_gather_materials_unlit from . import gltf2_blender_gather_texture_info @@ -33,6 +32,11 @@ from .extensions.gltf2_blender_gather_materials_transmission import export_transmission from .extensions.gltf2_blender_gather_materials_clearcoat import export_clearcoat from .extensions.gltf2_blender_gather_materials_ior import export_ior +from .gltf2_blender_search_node_tree import \ + has_image_node_from_socket, \ + get_socket_from_gltf_material_node, \ + get_socket, \ + get_node_socket @cached def get_material_cache_key(blender_material, export_settings): @@ -100,7 +104,7 @@ def gather_material(blender_material, export_settings): # If emissive is set, from an emissive node (not PBR) # We need to set manually default values for # pbr_metallic_roughness.baseColor - if material.emissive_factor is not None and gltf2_blender_get.get_node_socket(blender_material, bpy.types.ShaderNodeBsdfPrincipled, "Base Color") is None: + if material.emissive_factor is not None and get_node_socket(blender_material, bpy.types.ShaderNodeBsdfPrincipled, "Base Color").socket is None: material.pbr_metallic_roughness = gltf2_blender_gather_materials_pbr_metallic_roughness.get_default_pbr_for_emissive_node() export_user_extensions('gather_material_hook', export_settings, material, blender_material) @@ -153,12 +157,6 @@ def __gather_double_sided(blender_material, extensions, export_settings): if not blender_material.use_backface_culling: return True - - old_double_sided_socket = gltf2_blender_get.get_socket_old(blender_material, "DoubleSided") - if old_double_sided_socket is not None and\ - not old_double_sided_socket.is_linked and\ - old_double_sided_socket.default_value > 0.5: - return True return None @@ -232,9 +230,7 @@ def __gather_name(blender_material, export_settings): 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 = get_socket(blender_material, "Normal") normal_texture, uvmap_info, _ = gltf2_blender_gather_texture_info.gather_material_normal_texture_info_class( normal, (normal,), @@ -246,35 +242,37 @@ def __gather_orm_texture(blender_material, export_settings): # Check for the presence of Occlusion, Roughness, Metallic sharing a single image. # 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 gltf2_blender_get.has_image_node_from_socket(occlusion): - occlusion = gltf2_blender_get.get_socket_old(blender_material, "Occlusion") - if occlusion is None or not gltf2_blender_get.has_image_node_from_socket(occlusion): + occlusion = get_socket(blender_material, "Occlusion") + if occlusion.socket is None or not has_image_node_from_socket(occlusion, export_settings): + occlusion = get_socket_from_gltf_material_node(blender_material, "Occlusion") + if occlusion.socket is None or not has_image_node_from_socket(occlusion, export_settings): return None, None - metallic_socket = gltf2_blender_get.get_socket(blender_material, "Metallic") - roughness_socket = gltf2_blender_get.get_socket(blender_material, "Roughness") + metallic_socket = get_socket(blender_material, "Metallic") + roughness_socket = get_socket(blender_material, "Roughness") - 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) + hasMetal = metallic_socket.socket is not None and has_image_node_from_socket(metallic_socket, export_settings) + hasRough = roughness_socket.socket is not None and has_image_node_from_socket(roughness_socket, export_settings) default_sockets = () + # Warning: for default socket, do not use NodeSocket object, because it will break cache + # Using directlty the Blender socket object if not hasMetal and not hasRough: - metallic_roughness = gltf2_blender_get.get_socket_old(blender_material, "MetallicRoughness") - if metallic_roughness is None or not gltf2_blender_get.has_image_node_from_socket(metallic_roughness): + metallic_roughness = get_socket_from_gltf_material_node(blender_material, "MetallicRoughness") + if metallic_roughness.socket is None or not has_image_node_from_socket(metallic_roughness, export_settings): return None, default_sockets result = (occlusion, metallic_roughness) elif not hasMetal: result = (occlusion, roughness_socket) - default_sockets = (metallic_socket,) + default_sockets = (metallic_socket.socket,) elif not hasRough: result = (occlusion, metallic_socket) - default_sockets = (roughness_socket,) + default_sockets = (roughness_socket.socket,) else: result = (occlusion, roughness_socket, metallic_socket) default_sockets = () - if not gltf2_blender_gather_texture_info.check_same_size_images(result): + if not gltf2_blender_gather_texture_info.check_same_size_images(result, export_settings): print_console("INFO", "Occlusion and metal-roughness texture will be exported separately " "(use same-sized images if you want them combined)") @@ -288,9 +286,9 @@ def __gather_orm_texture(blender_material, export_settings): return result, default_sockets def __gather_occlusion_texture(blender_material, orm_texture, default_sockets, export_settings): - occlusion = gltf2_blender_get.get_socket(blender_material, "Occlusion") - if occlusion is None: - occlusion = gltf2_blender_get.get_socket_old(blender_material, "Occlusion") + occlusion = get_socket(blender_material, "Occlusion") + if occlusion.socket is None: + occlusion = get_socket_from_gltf_material_node(blender_material, "Occlusion") occlusion_texture, uvmap_info, _ = gltf2_blender_gather_texture_info.gather_material_occlusion_texture_info_class( occlusion, orm_texture or (occlusion,), diff --git a/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_gather_materials_pbr_metallic_roughness.py b/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_gather_materials_pbr_metallic_roughness.py index bb8083d7f..c0a3faaf2 100644 --- a/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_gather_materials_pbr_metallic_roughness.py +++ b/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_gather_materials_pbr_metallic_roughness.py @@ -14,13 +14,18 @@ import bpy + from ....io.com import gltf2_io from ....io.exp.gltf2_io_user_extensions import export_user_extensions -from ...exp import gltf2_blender_get from ..gltf2_blender_gather_cache import cached -from ..gltf2_blender_get import image_tex_is_valid_from_socket from .gltf2_blender_search_node_tree import get_vertex_color_info from .gltf2_blender_gather_texture_info import gather_texture_info +from .gltf2_blender_search_node_tree import \ + get_socket_from_gltf_material_node, \ + has_image_node_from_socket, \ + get_const_from_default_value_socket, \ + get_socket, \ + get_factor_from_socket @cached def gather_material_pbr_metallic_roughness(blender_material, orm_texture, export_settings): @@ -59,23 +64,23 @@ def __gather_base_color_factor(blender_material, export_settings): rgb, alpha = None, None - alpha_socket = gltf2_blender_get.get_socket(blender_material, "Alpha") - if isinstance(alpha_socket, bpy.types.NodeSocket): + alpha_socket = get_socket(blender_material, "Alpha") + if isinstance(alpha_socket.socket, bpy.types.NodeSocket): if export_settings['gltf_image_format'] != "NONE": - alpha = gltf2_blender_get.get_factor_from_socket(alpha_socket, kind='VALUE') + alpha = get_factor_from_socket(alpha_socket, kind='VALUE') else: - alpha = gltf2_blender_get.get_const_from_default_value_socket(alpha_socket, kind='VALUE') + alpha = get_const_from_default_value_socket(alpha_socket, kind='VALUE') - base_color_socket = gltf2_blender_get.get_socket(blender_material, "Base Color") - if base_color_socket is None: - base_color_socket = gltf2_blender_get.get_socket(blender_material, "BaseColor") + base_color_socket = get_socket(blender_material, "Base Color") + if base_color_socket.socket is None: + base_color_socket = get_socket(blender_material, "BaseColor") if base_color_socket is None: - base_color_socket = gltf2_blender_get.get_socket_old(blender_material, "BaseColorFactor") - if isinstance(base_color_socket, bpy.types.NodeSocket): + base_color_socket = get_socket_from_gltf_material_node(blender_material, "BaseColorFactor") + if isinstance(base_color_socket.socket, bpy.types.NodeSocket): if export_settings['gltf_image_format'] != "NONE": - rgb = gltf2_blender_get.get_factor_from_socket(base_color_socket, kind='RGB') + rgb = get_factor_from_socket(base_color_socket, kind='RGB') else: - rgb = gltf2_blender_get.get_const_from_default_value_socket(base_color_socket, kind='RGB') + rgb = get_const_from_default_value_socket(base_color_socket, kind='RGB') if rgb is None: rgb = [1.0, 1.0, 1.0] if alpha is None: alpha = 1.0 @@ -90,18 +95,18 @@ def __gather_base_color_factor(blender_material, export_settings): def __gather_base_color_texture(blender_material, export_settings): - base_color_socket = gltf2_blender_get.get_socket(blender_material, "Base Color") - if base_color_socket is None: - base_color_socket = gltf2_blender_get.get_socket(blender_material, "BaseColor") + base_color_socket = get_socket(blender_material, "Base Color") + if base_color_socket.socket is None: + base_color_socket = get_socket(blender_material, "BaseColor") if base_color_socket is None: - base_color_socket = gltf2_blender_get.get_socket_old(blender_material, "BaseColor") + base_color_socket = get_socket_from_gltf_material_node(blender_material, "BaseColor") - alpha_socket = gltf2_blender_get.get_socket(blender_material, "Alpha") + alpha_socket = get_socket(blender_material, "Alpha") # keep sockets that have some texture : color and/or alpha inputs = tuple( socket for socket in [base_color_socket, alpha_socket] - if socket is not None and image_tex_is_valid_from_socket(socket) + if socket.socket is not None and has_image_node_from_socket(socket, export_settings) ) if not inputs: return None, {}, {"uv_info": {}, "vc_info": {}}, None @@ -123,34 +128,35 @@ def __gather_metallic_factor(blender_material, export_settings): if not blender_material.use_nodes: return blender_material.metallic - metallic_socket = gltf2_blender_get.get_socket(blender_material, "Metallic") + metallic_socket = get_socket(blender_material, "Metallic") if metallic_socket is None: - metallic_socket = gltf2_blender_get.get_socket_old(blender_material, "MetallicFactor") - if isinstance(metallic_socket, bpy.types.NodeSocket): - fac = gltf2_blender_get.get_factor_from_socket(metallic_socket, kind='VALUE') + metallic_socket = get_socket_from_gltf_material_node(blender_material, "MetallicFactor") + if isinstance(metallic_socket.socket, bpy.types.NodeSocket): + fac = get_factor_from_socket(metallic_socket, kind='VALUE') return fac if fac != 1 else None return None def __gather_metallic_roughness_texture(blender_material, orm_texture, export_settings): - metallic_socket = gltf2_blender_get.get_socket(blender_material, "Metallic") - roughness_socket = gltf2_blender_get.get_socket(blender_material, "Roughness") + metallic_socket = get_socket(blender_material, "Metallic") + roughness_socket = get_socket(blender_material, "Roughness") - hasMetal = metallic_socket is not None and image_tex_is_valid_from_socket(metallic_socket) - hasRough = roughness_socket is not None and image_tex_is_valid_from_socket(roughness_socket) + hasMetal = metallic_socket.socket is not None and has_image_node_from_socket(metallic_socket, export_settings) + hasRough = roughness_socket.socket is not None and has_image_node_from_socket(roughness_socket, export_settings) default_sockets = () + # Warning: for default socket, do not use NodeSocket object, because it will break cache + # Using directlty the Blender socket object if not hasMetal and not hasRough: - metallic_roughness = gltf2_blender_get.get_socket_old(blender_material, "MetallicRoughness") - if metallic_roughness is None or not image_tex_is_valid_from_socket(metallic_roughness): + metallic_roughness = get_socket_from_gltf_material_node(blender_material, "MetallicRoughness") + if metallic_roughness is None or not has_image_node_from_socket(metallic_roughness, export_settings): return None, {}, None - texture_input = (metallic_roughness,) elif not hasMetal: texture_input = (roughness_socket,) - default_sockets = (metallic_socket,) + default_sockets = (metallic_socket.socket,) elif not hasRough: texture_input = (metallic_socket,) - default_sockets = (roughness_socket,) + default_sockets = (roughness_socket.socket,) else: texture_input = (metallic_socket, roughness_socket) default_sockets = () @@ -168,11 +174,11 @@ def __gather_roughness_factor(blender_material, export_settings): if not blender_material.use_nodes: return blender_material.roughness - roughness_socket = gltf2_blender_get.get_socket(blender_material, "Roughness") + roughness_socket = get_socket(blender_material, "Roughness") if roughness_socket is None: - roughness_socket = gltf2_blender_get.get_socket_old(blender_material, "RoughnessFactor") - if isinstance(roughness_socket, bpy.types.NodeSocket): - fac = gltf2_blender_get.get_factor_from_socket(roughness_socket, kind='VALUE') + roughness_socket = get_socket_from_gltf_material_node(blender_material, "RoughnessFactor") + if isinstance(roughness_socket.socket, bpy.types.NodeSocket): + fac = get_factor_from_socket(roughness_socket, kind='VALUE') return fac if fac != 1 else None return None diff --git a/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_gather_materials_unlit.py b/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_gather_materials_unlit.py index 212de5569..7ffc334c0 100644 --- a/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_gather_materials_unlit.py +++ b/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_gather_materials_unlit.py @@ -12,10 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ....io.com.gltf2_io_extensions import Extension -from ...exp import gltf2_blender_get from . import gltf2_blender_gather_texture_info from .gltf2_blender_search_node_tree import get_vertex_color_info +from .gltf2_blender_search_node_tree import \ + get_socket, \ + NodeSocket, \ + previous_socket, \ + previous_node, \ + get_factor_from_socket def detect_shadeless_material(blender_material, export_settings): """Detect if this material is "shadeless" ie. should be exported @@ -25,8 +29,8 @@ def detect_shadeless_material(blender_material, export_settings): if not blender_material.use_nodes: return None # Old Background node detection (unlikely to happen) - bg_socket = gltf2_blender_get.get_socket(blender_material, "Background") - if bg_socket is not None: + bg_socket = get_socket(blender_material, "Background") + if bg_socket.socket is not None: return {'rgb_socket': bg_socket} # Look for @@ -37,6 +41,7 @@ def detect_shadeless_material(blender_material, export_settings): info = {} + #TODOSNode this can be a function call for node in blender_material.node_tree.nodes: if node.type == 'OUTPUT_MATERIAL' and node.is_active_output: socket = node.inputs[0] @@ -44,6 +49,8 @@ def detect_shadeless_material(blender_material, export_settings): else: return None + socket = NodeSocket(socket, []) + # Be careful not to misidentify a lightpath trick as mix-alpha. result = __detect_lightpath_trick(socket) if result is not None: @@ -59,10 +66,10 @@ def detect_shadeless_material(blender_material, export_settings): socket = result['next_socket'] # Check if a color socket, or connected to a color socket - if socket.type != 'RGBA': - from_socket = gltf2_blender_get.previous_socket(socket) - if from_socket is None: return None - if from_socket.type != 'RGBA': return None + if socket.socket.type != 'RGBA': + from_socket = previous_socket(socket) + if from_socket.socket is None: return None + if from_socket.socket.type != 'RGBA': return None info['rgb_socket'] = socket return info @@ -78,13 +85,13 @@ def __detect_mix_alpha(socket): # # Returns None if not detected. Otherwise, a dict containing alpha_socket # and next_socket. - prev = gltf2_blender_get.previous_node(socket) - if prev is None or prev.type != 'MIX_SHADER': return None - in1 = gltf2_blender_get.previous_node(prev.inputs[1]) - if in1 is None or in1.type != 'BSDF_TRANSPARENT': return None + prev = previous_node(socket) + if prev.node is None or prev.node.type != 'MIX_SHADER': return None + in1 = previous_node(NodeSocket(prev.node.inputs[1], prev.group_path)) + if in1.node is None or in1.node.type != 'BSDF_TRANSPARENT': return None return { - 'alpha_socket': prev.inputs[0], - 'next_socket': prev.inputs[2], + 'alpha_socket': NodeSocket(prev.node.inputs[0], prev.group_path), + 'next_socket': NodeSocket(prev.node.inputs[2], prev.group_path), } @@ -100,17 +107,17 @@ def __detect_lightpath_trick(socket): # The Emission node can be omitted. # Returns None if not detected. Otherwise, a dict containing # next_socket. - prev = gltf2_blender_get.previous_node(socket) - if prev is None or prev.type != 'MIX_SHADER': return None - in0 = gltf2_blender_get.previous_socket(prev.inputs[0]) - if in0 is None or in0.node.type != 'LIGHT_PATH': return None - if in0.name != 'Is Camera Ray': return None - next_socket = prev.inputs[2] + prev = previous_node(socket) + if prev.node is None or prev.node.type != 'MIX_SHADER': return None + in0 = previous_socket(NodeSocket(prev.node.inputs[0], prev.group_path)) + if in0.socket is None or in0.socket.node.type != 'LIGHT_PATH': return None + if in0.socket.name != 'Is Camera Ray': return None + next_socket = NodeSocket(prev.node.inputs[2], prev.group_path) # Detect emission - prev = gltf2_blender_get.previous_node(next_socket) - if prev is not None and prev.type == 'EMISSION': - next_socket = prev.inputs[0] + prev = previous_node(next_socket) + if prev.node is not None and prev.node.type == 'EMISSION': + next_socket = NodeSocket(prev.node.inputs[0], prev.group_path) return {'next_socket': next_socket} @@ -119,9 +126,9 @@ def gather_base_color_factor(info, export_settings): rgb, alpha = None, None if 'rgb_socket' in info: - rgb = gltf2_blender_get.get_factor_from_socket(info['rgb_socket'], kind='RGB') + rgb = get_factor_from_socket(info['rgb_socket'], kind='RGB') if 'alpha_socket' in info: - alpha = gltf2_blender_get.get_factor_from_socket(info['alpha_socket'], kind='VALUE') + alpha = get_factor_from_socket(info['alpha_socket'], kind='VALUE') if rgb is None: rgb = [1.0, 1.0, 1.0] if alpha is None: alpha = 1.0 @@ -132,8 +139,8 @@ def gather_base_color_factor(info, export_settings): def gather_base_color_texture(info, export_settings): - sockets = (info.get('rgb_socket'), info.get('alpha_socket')) - sockets = tuple(s for s in sockets if s is not None) + sockets = (info.get('rgb_socket', NodeSocket(None, None)), info.get('alpha_socket', NodeSocket(None, None))) + sockets = tuple(s for s in sockets if s.socket is not None) if sockets: # NOTE: separate RGB and Alpha textures will not get combined # because gather_image determines how to pack images based on the diff --git a/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_gather_texture.py b/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_gather_texture.py index 4175ddcb5..be98ba08d 100644 --- a/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_gather_texture.py +++ b/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_gather_texture.py @@ -14,21 +14,23 @@ import typing import bpy -from ....io.com import gltf2_io_debug + from ....io.exp.gltf2_io_user_extensions import export_user_extensions from ....io.com.gltf2_io_extensions import Extension from ....io.exp.gltf2_io_image_data import ImageData from ....io.exp.gltf2_io_binary_data import BinaryData +from ....io.com import gltf2_io_debug from ....io.com import gltf2_io -from ..gltf2_blender_gather_cache import cached from ..gltf2_blender_gather_sampler import gather_sampler -from ..gltf2_blender_get import get_tex_from_socket +from ..gltf2_blender_gather_cache import cached +from .gltf2_blender_search_node_tree import get_texture_node_from_socket, NodeSocket from . import gltf2_blender_gather_image + @cached def gather_texture( blender_shader_sockets: typing.Tuple[bpy.types.NodeSocket], - default_sockets: typing.Tuple[bpy.types.NodeSocket], + default_sockets, export_settings): """ Gather texture sampling information and image channels from a blender shader texture attached to a shader socket. @@ -174,14 +176,33 @@ def __gather_name(blender_shader_sockets, export_settings): def __gather_sampler(blender_shader_sockets, export_settings): - shader_nodes = [get_tex_from_socket(socket) for socket in blender_shader_sockets] + shader_nodes = [get_texture_node_from_socket(socket, export_settings) 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 + first_valid_shader_node = next(filter(lambda x: x is not None, shader_nodes)) + + # group_path can't be a list, so transform it to str + + sep_item = "##~~gltf-sep~~##" + sep_inside_item = "##~~gltf-inside-sep~~##" + group_path_str = "" + if len(first_valid_shader_node.group_path) > 0: + group_path_str += first_valid_shader_node.group_path[0].name + if len(first_valid_shader_node.group_path) > 1: + for idx, i in enumerate(first_valid_shader_node.group_path[1:]): + group_path_str += sep_item + if idx == 0: + group_path_str += first_valid_shader_node.group_path[0].name + else: + group_path_str += i.id_data.name + group_path_str += sep_inside_item + group_path_str += i.name + return gather_sampler( - first_valid_shader_node, + first_valid_shader_node.shader_node, + group_path_str, export_settings) diff --git a/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_gather_texture_info.py b/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_gather_texture_info.py index d4ef68b03..e9a0d2a91 100644 --- a/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_gather_texture_info.py +++ b/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_gather_texture_info.py @@ -17,12 +17,17 @@ from ....io.com import gltf2_io from ....io.com.gltf2_io_extensions import Extension from ....io.exp.gltf2_io_user_extensions import export_user_extensions -from ...exp import gltf2_blender_get -from ..gltf2_blender_get import previous_node, get_tex_from_socket from ..gltf2_blender_gather_sampler import detect_manual_uv_wrapping from ..gltf2_blender_gather_cache import cached from . import gltf2_blender_gather_texture -from . import gltf2_blender_search_node_tree +from .gltf2_blender_search_node_tree import \ + get_texture_node_from_socket, \ + from_socket, \ + FilterByType, \ + previous_node, \ + get_const_from_socket, \ + NodeSocket, \ + get_texture_transform_from_mapping_node # blender_shader_sockets determine the texture and primary_socket determines # the textransform and UVMap. Ex: when combining an ORM texture, for @@ -47,7 +52,7 @@ def gather_material_occlusion_texture_info_class(primary_socket, blender_shader_ def __gather_texture_info_helper( primary_socket: bpy.types.NodeSocket, blender_shader_sockets: typing.Tuple[bpy.types.NodeSocket], - default_sockets: typing.Tuple[bpy.types.NodeSocket], + default_sockets, kind: str, filter_type: str, export_settings): @@ -87,7 +92,7 @@ def __gather_texture_info_helper( def __filter_texture_info(primary_socket, blender_shader_sockets, filter_type, export_settings): if primary_socket is None: return False - if get_tex_from_socket(primary_socket) is None: + if get_texture_node_from_socket(primary_socket, export_settings) is None: return False if not blender_shader_sockets: return False @@ -95,12 +100,12 @@ def __filter_texture_info(primary_socket, blender_shader_sockets, filter_type, e return False if filter_type == "ALL": # Check that all sockets link to texture - if any([get_tex_from_socket(socket) is None for socket in blender_shader_sockets]): + if any([get_texture_node_from_socket(socket, export_settings) is None for socket in blender_shader_sockets]): # sockets do not lead to a texture --> discard return False elif filter_type == "ANY": # Check that at least one socket link to texture - if all([get_tex_from_socket(socket) is None for socket in blender_shader_sockets]): + if all([get_texture_node_from_socket(socket, export_settings) is None for socket in blender_shader_sockets]): return False elif filter_type == "NONE": # No check @@ -122,9 +127,9 @@ def __gather_extras(blender_shader_sockets, export_settings): # MaterialNormalTextureInfo only def __gather_normal_scale(primary_socket, export_settings): - result = gltf2_blender_search_node_tree.from_socket( + result = from_socket( primary_socket, - gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeNormalMap)) + FilterByType(bpy.types.ShaderNodeNormalMap)) if not result: return None strengthInput = result[0].shader_node.inputs['Strength'] @@ -137,11 +142,11 @@ def __gather_normal_scale(primary_socket, export_settings): def __gather_occlusion_strength(primary_socket, export_settings): # Look for a MixRGB node that mixes with pure white in front of # primary_socket. The mix factor gives the occlusion strength. - node = gltf2_blender_get.previous_node(primary_socket) - if node and node.type == 'MIX' and node.blend_type == 'MIX': - fac = gltf2_blender_get.get_const_from_socket(node.inputs['Factor'], kind='VALUE') - col1 = gltf2_blender_get.get_const_from_socket(node.inputs[6], kind='RGB') - col2 = gltf2_blender_get.get_const_from_socket(node.inputs[7], kind='RGB') + node = previous_node(primary_socket) + if node and node.node.type == 'MIX' and node.node.blend_type == 'MIX': + fac = get_const_from_socket(NodeSocket(node.node.inputs['Factor'], node.group_path), kind='VALUE') + col1 = get_const_from_socket(NodeSocket(node.node.inputs[6], node.group_path), kind='RGB') + col2 = get_const_from_socket(NodeSocket(node.node.inputs[7], node.group_path), kind='RGB') if fac is not None: if col1 == [1.0, 1.0, 1.0] and col2 is None: return fac @@ -163,31 +168,32 @@ def __gather_texture_transform_and_tex_coord(primary_socket, export_settings): # # The [UV Wrapping] is for wrap modes like MIRROR that use nodes, # [Mapping] is for KHR_texture_transform, and [UV Map] is for texCoord. - blender_shader_node = get_tex_from_socket(primary_socket).shader_node + result_tex = get_texture_node_from_socket(primary_socket, export_settings) + blender_shader_node = result_tex.shader_node # Skip over UV wrapping stuff (it goes in the sampler) - result = detect_manual_uv_wrapping(blender_shader_node) + result = detect_manual_uv_wrapping(blender_shader_node, result_tex.group_path) if result: node = previous_node(result['next_socket']) else: - node = previous_node(blender_shader_node.inputs['Vector']) + node = previous_node(NodeSocket(blender_shader_node.inputs['Vector'], result_tex.group_path)) texture_transform = None - if node and node.type == 'MAPPING': - texture_transform = gltf2_blender_get.get_texture_transform_from_mapping_node(node) - node = previous_node(node.inputs['Vector']) + if node.node and node.node.type == 'MAPPING': + texture_transform = get_texture_transform_from_mapping_node(node) + node = previous_node(NodeSocket(node.node.inputs['Vector'], node.group_path)) uvmap_info = {} - if node and node.type == 'UVMAP' and node.uv_map: + if node.node and node.node.type == 'UVMAP' and node.node.uv_map: uvmap_info['type'] = "Fixed" - uvmap_info['value'] = node.uv_map + uvmap_info['value'] = node.node.uv_map - elif node and node.type == 'ATTRIBUTE' \ - and node.attribute_type == "GEOMETRY" \ - and node.attribute_name: + elif node and node.node and node.node.type == 'ATTRIBUTE' \ + and node.node.attribute_type == "GEOMETRY" \ + and node.node.attribute_name: uvmap_info['type'] = 'Attribute' - uvmap_info['value'] = node.attribute_name + uvmap_info['value'] = node.node.attribute_name else: uvmap_info['type'] = 'Active' @@ -197,6 +203,7 @@ def __gather_texture_transform_and_tex_coord(primary_socket, export_settings): def check_same_size_images( blender_shader_sockets: typing.Tuple[bpy.types.NodeSocket], + export_settings ) -> bool: """Check that all sockets leads to images of the same size.""" if not blender_shader_sockets or not all(blender_shader_sockets): @@ -204,7 +211,7 @@ def check_same_size_images( sizes = set() for socket in blender_shader_sockets: - tex = get_tex_from_socket(socket) + tex = get_texture_node_from_socket(socket, export_settings) if tex is None: return False size = tex.shader_node.image.size diff --git a/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_search_node_tree.py b/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_search_node_tree.py index a70267d4c..de6387be1 100644 --- a/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_search_node_tree.py +++ b/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_search_node_tree.py @@ -17,6 +17,11 @@ # import bpy +from mathutils import Vector, Matrix +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached +from ...com.gltf2_blender_material_helpers import get_gltf_node_name, get_gltf_node_old_name, get_gltf_old_group_node_name +from ....blender.com.gltf2_blender_conversion import texture_transform_blender_to_gltf +from io_scene_gltf2.io.com import gltf2_io_debug import typing @@ -58,13 +63,14 @@ def __call__(self, shader_node): class NodeTreeSearchResult: - def __init__(self, shader_node: bpy.types.Node, path: typing.List[bpy.types.NodeLink]): + def __init__(self, shader_node: bpy.types.Node, path: typing.List[bpy.types.NodeLink], group_path: typing.List[bpy.types.Node]): self.shader_node = shader_node self.path = path + self.group_path = group_path # TODO: cache these searches -def from_socket(start_socket: bpy.types.NodeSocket, +def from_socket(start_socket: NodeTreeSearchResult, shader_node_filter: typing.Union[Filter, typing.Callable]) -> typing.List[NodeTreeSearchResult]: """ Find shader nodes where the filter expression is true. @@ -76,18 +82,39 @@ def from_socket(start_socket: bpy.types.NodeSocket, # hide implementation (especially the search path) def __search_from_socket(start_socket: bpy.types.NodeSocket, shader_node_filter: typing.Union[Filter, typing.Callable], - search_path: typing.List[bpy.types.NodeLink]) -> typing.List[NodeTreeSearchResult]: + search_path: typing.List[bpy.types.NodeLink], + group_path: typing.List[bpy.types.Node]) -> typing.List[NodeTreeSearchResult]: results = [] - for link in start_socket.links: # follow the link to a shader node linked_node = link.from_node + + if linked_node.type == "GROUP": + group_output_node = [node for node in linked_node.node_tree.nodes if node.type == "GROUP_OUTPUT"][0] + socket = [sock for sock in group_output_node.inputs if sock.name == link.from_socket.name][0] + group_path.append(linked_node) + linked_results = __search_from_socket(socket, shader_node_filter, search_path + [link], group_path.copy()) + if linked_results: + # add the link to the current path + search_path.append(link) + results += linked_results + continue + + if linked_node.type == "GROUP_INPUT": + socket = [sock for sock in group_path[-1].inputs if sock.name == link.from_socket.name][0] + linked_results = __search_from_socket(socket, shader_node_filter, search_path + [link], group_path[:-1]) + if linked_results: + # add the link to the current path + search_path.append(link) + results += linked_results + continue + # check if the node matches the filter if shader_node_filter(linked_node): - results.append(NodeTreeSearchResult(linked_node, search_path + [link])) + results.append(NodeTreeSearchResult(linked_node, search_path + [link], group_path)) # traverse into inputs of the node for input_socket in linked_node.inputs: - linked_results = __search_from_socket(input_socket, shader_node_filter, search_path + [link]) + linked_results = __search_from_socket(input_socket, shader_node_filter, search_path + [link], group_path.copy()) if linked_results: # add the link to the current path search_path.append(link) @@ -95,10 +122,330 @@ def __search_from_socket(start_socket: bpy.types.NodeSocket, return results - if start_socket is None: + if start_socket.socket is None: return [] - return __search_from_socket(start_socket, shader_node_filter, []) + return __search_from_socket(start_socket.socket, shader_node_filter, [], start_socket.group_path) + +@cached +def get_texture_node_from_socket(socket, export_settings): + result = from_socket( + socket, + FilterByType(bpy.types.ShaderNodeTexImage)) + if not result: + return None + if result[0].shader_node.image is None: + return None + return result[0] + +def has_image_node_from_socket(socket, export_settings): + result = get_texture_node_from_socket(socket, export_settings) + return result is not None + +# return the default value of a socket, even if this socket is linked +def get_const_from_default_value_socket(socket, kind): + if kind == 'RGB': + if socket.socket.type != 'RGBA': return None + return list(socket.socket.default_value)[:3] + if kind == 'VALUE': + if socket.socket.type != 'VALUE': return None + return socket.socket.default_value + return None + +#TODOSNode : @cached? If yes, need to use id of node tree, has this is probably not fully hashable +# For now, not caching it. If we encounter performance issue, we will see later +def get_material_nodes(node_tree: bpy.types.NodeTree, group_path, type): + """ + For a given tree, recursively return all nodes including node groups. + """ + + nodes = [] + for node in [n for n in node_tree.nodes if isinstance(n, type) and not n.mute]: + nodes.append((node, group_path.copy())) + + # Some weird node groups with missing datablock can have no node_tree, so checking n.node_tree (See #1797) + for node in [n for n in node_tree.nodes if n.type == "GROUP" and n.node_tree is not None and not n.mute and n.node_tree.name != get_gltf_old_group_node_name()]: # Do not enter the olf glTF node group + new_group_path = group_path.copy() + new_group_path.append(node) + nodes.extend(get_material_nodes(node.node_tree, new_group_path, type)) + + return nodes + +def get_socket_from_gltf_material_node(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_names = [get_gltf_node_name().lower(), get_gltf_node_old_name().lower()] + if blender_material.node_tree and blender_material.use_nodes: + nodes = get_material_nodes(blender_material.node_tree, [blender_material], bpy.types.ShaderNodeGroup) + # Some weird node groups with missing datablock can have no node_tree, so checking n.node_tree (See #1797) + nodes = [n for n in nodes if n[0].node_tree is not None and ( n[0].node_tree.name.lower().startswith(get_gltf_old_group_node_name()) or n[0].node_tree.name.lower() in gltf_node_group_names)] + inputs = sum([[(input, node[1]) for input in node[0].inputs if input.name == name] for node in nodes], []) + if inputs: + return NodeSocket(inputs[0][0], inputs[0][1]) + + return NodeSocket(None, None) + +class NodeSocket: + def __init__(self, socket, group_path): + self.socket = socket + self.group_path = group_path + +class ShNode: + def __init__(self, node, group_path): + self.node = node + self.group_path = group_path + +def get_node_socket(blender_material, type, name): + """ + For a given material input name, retrieve the corresponding node tree socket for a given node type. + + :param blender_material: a blender material for which to get the socket + :return: a blender NodeSocket for a given type + """ + nodes = get_material_nodes(blender_material.node_tree, [blender_material], type) + #TODOSNode : Why checking outputs[0] ? What about alpha for texture node, that is outputs[1] ???? + nodes = [node for node in nodes if check_if_is_linked_to_active_output(node[0].outputs[0], node[1])] + inputs = sum([[(input, node[1]) for input in node[0].inputs if input.name == name] for node in nodes], []) + if inputs: + return NodeSocket(inputs[0][0], inputs[0][1]) + return NodeSocket(None, None) + + +def get_socket(blender_material: bpy.types.Material, name: str, volume=False): + """ + For a given material input name, retrieve the corresponding node tree socket. + + :param blender_material: a blender material for which to get the socket + :param name: the name of the socket + :return: a blender NodeSocket + """ + if blender_material.node_tree and blender_material.use_nodes: + #i = [input for input in blender_material.node_tree.inputs] + #o = [output for output in blender_material.node_tree.outputs] + if name == "Emissive": + # Check for a dedicated Emission node first, it must supersede the newer built-in one + # because the newer one is always present in all Principled BSDF materials. + emissive_socket = get_node_socket(blender_material, bpy.types.ShaderNodeEmission, "Color") + if emissive_socket.socket is not None: + return emissive_socket + # If a dedicated Emission node was not found, fall back to the Principled BSDF Emission socket. + name = "Emission Color" + type = bpy.types.ShaderNodeBsdfPrincipled + elif name == "Background": + type = bpy.types.ShaderNodeBackground + name = "Color" + else: + if volume is False: + type = bpy.types.ShaderNodeBsdfPrincipled + else: + type = bpy.types.ShaderNodeVolumeAbsorption + + return get_node_socket(blender_material, type, name) + + return NodeSocket(None, None) + +def get_factor_from_socket(socket, kind): + """ + For baseColorFactor, metallicFactor, etc. + Get a constant value from a socket, or a constant value + from a MULTIPLY node just before the socket. + kind is either 'RGB' or 'VALUE'. + """ + fac = get_const_from_socket(socket, kind) + if fac is not None: + return fac + + node = previous_node(socket) + if node.node is not None: + x1, x2 = None, None + if kind == 'RGB': + if node.node.type == 'MIX' and node.node.data_type == "RGBA" and node.node.blend_type == 'MULTIPLY': + # TODO: handle factor in inputs[0]? + x1 = get_const_from_socket(NodeSocket(node.node.inputs[6], node.group_path), kind) + x2 = get_const_from_socket(NodeSocket(node.node.inputs[7], node.group_path), kind) + if kind == 'VALUE': + if node.node.type == 'MATH' and node.node.operation == 'MULTIPLY': + x1 = get_const_from_socket(NodeSocket(node.node.inputs[0], node.group_path), kind) + x2 = get_const_from_socket(NodeSocket(node.node.inputs[1], node.group_path), kind) + if x1 is not None and x2 is None: return x1 + if x2 is not None and x1 is None: return x2 + + return None + +def get_const_from_socket(socket, kind): + if not socket.socket.is_linked: + if kind == 'RGB': + if socket.socket.type != 'RGBA': return None + return list(socket.socket.default_value)[:3] + if kind == 'VALUE': + if socket.socket.type != 'VALUE': return None + return socket.socket.default_value + + # Handle connection to a constant RGB/Value node + prev_node = previous_node(socket) + if prev_node.node is not None: + if kind == 'RGB' and prev_node.node.type == 'RGB': + return list(prev_node.node.outputs[0].default_value)[:3] + if kind == 'VALUE' and prev_node.node.type == 'VALUE': + return prev_node.node.outputs[0].default_value + + return None + + +def previous_socket(socket: NodeSocket): + soc = socket.socket + group_path = socket.group_path.copy() + while True: + if not soc.is_linked: + return NodeSocket(None, None) + + from_socket = soc.links[0].from_socket + + # If we are entering a node group (from outputs) + if from_socket.node.type == "GROUP": + socket_name = from_socket.name + sockets = [n for n in from_socket.node.node_tree.nodes if n.type == "GROUP_OUTPUT"][0].inputs + socket = [s for s in sockets if s.name == socket_name][0] + group_path.append(from_socket.node) + soc = socket + continue + + # If we are exiting a node group (from inputs) + if from_socket.node.type == "GROUP_INPUT": + socket_name = from_socket.name + sockets = group_path[-1].inputs + socket = [s for s in sockets if s.name == socket_name][0] + group_path = group_path[:-1] + soc = socket + continue + + # Skip over reroute nodes + if from_socket.node.type == 'REROUTE': + soc = from_socket.node.inputs[0] + continue + + return NodeSocket(from_socket, group_path) + + +def previous_node(socket: NodeSocket): + prev_socket = previous_socket(socket) + if prev_socket.socket is not None: + return ShNode(prev_socket.socket.node, prev_socket.group_path) + return ShNode(None, None) + +def get_texture_transform_from_mapping_node(mapping_node): + if mapping_node.node.vector_type not in ["TEXTURE", "POINT", "VECTOR"]: + gltf2_io_debug.print_console("WARNING", + "Skipping exporting texture transform because it had type " + + mapping_node.node.vector_type + "; recommend using POINT instead" + ) + return None + + + rotation_0, rotation_1 = mapping_node.node.inputs['Rotation'].default_value[0], mapping_node.node.inputs['Rotation'].default_value[1] + if rotation_0 or rotation_1: + # TODO: can we handle this? + gltf2_io_debug.print_console("WARNING", + "Skipping exporting texture transform because it had non-zero " + "rotations in the X/Y direction; only a Z rotation can be exported!" + ) + return None + + mapping_transform = {} + mapping_transform["offset"] = [mapping_node.node.inputs['Location'].default_value[0], mapping_node.node.inputs['Location'].default_value[1]] + mapping_transform["rotation"] = mapping_node.node.inputs['Rotation'].default_value[2] + mapping_transform["scale"] = [mapping_node.node.inputs['Scale'].default_value[0], mapping_node.node.inputs['Scale'].default_value[1]] + + if mapping_node.node.vector_type == "TEXTURE": + # This means use the inverse of the TRS transform. + def inverted(mapping_transform): + offset = mapping_transform["offset"] + rotation = mapping_transform["rotation"] + scale = mapping_transform["scale"] + + # Inverse of a TRS is not always a TRS. This function will be right + # at least when the following don't occur. + if abs(rotation) > 1e-5 and abs(scale[0] - scale[1]) > 1e-5: + return None + if abs(scale[0]) < 1e-5 or abs(scale[1]) < 1e-5: + return None + + new_offset = Matrix.Rotation(-rotation, 3, 'Z') @ Vector((-offset[0], -offset[1], 1)) + new_offset[0] /= scale[0]; new_offset[1] /= scale[1] + return { + "offset": new_offset[0:2], + "rotation": -rotation, + "scale": [1/scale[0], 1/scale[1]], + } + + mapping_transform = inverted(mapping_transform) + if mapping_transform is None: + gltf2_io_debug.print_console("WARNING", + "Skipping exporting texture transform with type TEXTURE because " + "we couldn't convert it to TRS; recommend using POINT instead" + ) + return None + + elif mapping_node.node.vector_type == "VECTOR": + # Vectors don't get translated + mapping_transform["offset"] = [0, 0] + + texture_transform = texture_transform_blender_to_gltf(mapping_transform) + + if all([component == 0 for component in texture_transform["offset"]]): + del(texture_transform["offset"]) + if all([component == 1 for component in texture_transform["scale"]]): + del(texture_transform["scale"]) + if texture_transform["rotation"] == 0: + del(texture_transform["rotation"]) + + if len(texture_transform) == 0: + return None + + return texture_transform + +def check_if_is_linked_to_active_output(shader_socket, group_path): + for link in shader_socket.links: + + # If we are entering a node group + if link.to_node.type == "GROUP": + socket_name = link.to_socket.name + sockets = [n for n in link.to_node.node_tree.nodes if n.type == "GROUP_INPUT"][0].outputs + socket = [s for s in sockets if s.name == socket_name][0] + group_path.append(link.to_node) + #TODOSNode : Why checking outputs[0] ? What about alpha for texture node, that is outputs[1] ???? + ret = check_if_is_linked_to_active_output(socket, group_path) # recursive until find an output material node + if ret is True: + return True + continue + + # If we are exiting a node group + if link.to_node.type == "GROUP_OUTPUT": + socket_name = link.to_socket.name + sockets = group_path[-1].outputs + socket = [s for s in sockets if s.name == socket_name][0] + group_path = group_path[:-1] + #TODOSNode : Why checking outputs[0] ? What about alpha for texture node, that is outputs[1] ???? + ret = check_if_is_linked_to_active_output(socket, group_path) # recursive until find an output material node + if ret is True: + return True + continue + + if isinstance(link.to_node, bpy.types.ShaderNodeOutputMaterial) and link.to_node.is_active_output is True: + return True + + if len(link.to_node.outputs) > 0: # ignore non active output, not having output sockets + #TODOSNode : Why checking outputs[0] ? What about alpha for texture node, that is outputs[1] ???? + ret = check_if_is_linked_to_active_output(link.to_node.outputs[0], group_path) # recursive until find an output material node + if ret is True: + return True + + return False def get_vertex_color_info(primary_socket, sockets, export_settings): return {"color": None, "alpha": None} #TODO, placeholder for now diff --git a/tests/scenes/01_factors.blend b/tests/scenes/01_factors.blend index 529e4ed6d..2781ff96b 100644 Binary files a/tests/scenes/01_factors.blend and b/tests/scenes/01_factors.blend differ diff --git a/tests/scenes/22_node_groups.blend b/tests/scenes/22_node_groups.blend new file mode 100644 index 000000000..33c8db3a1 Binary files /dev/null and b/tests/scenes/22_node_groups.blend differ diff --git a/tests/test/test.js b/tests/test/test.js index decbc90b0..4da316df3 100644 --- a/tests/test/test.js +++ b/tests/test/test.js @@ -952,6 +952,25 @@ describe('Exporter', function() { assert.strictEqual(asset.meshes.length, 6); }); + it('manages node groups', function() { + let gltfPath = path.resolve(outDirPath, '22_node_groups.gltf'); + const asset = JSON.parse(fs.readFileSync(gltfPath)); + + const mat_ref = asset.materials.find(mat => mat.name === "Ref"); + const mat_out = asset.materials.find(mat => mat.name === "out"); + const mat_in = asset.materials.find(mat => mat.name === "in"); + const mat_nested = asset.materials.find(mat => mat.name === "nested"); + const mat_2groups = asset.materials.find(mat => mat.name === "2groups"); + + assert.ok("baseColorTexture" in mat_ref.pbrMetallicRoughness); + assert.ok("baseColorTexture" in mat_out.pbrMetallicRoughness); + assert.ok("baseColorTexture" in mat_in.pbrMetallicRoughness); + assert.ok("baseColorTexture" in mat_nested.pbrMetallicRoughness); + assert.ok("baseColorTexture" in mat_2groups.pbrMetallicRoughness); + + }); + + it('exports Attributes', function() { let gltfPath = path.resolve(outDirPath, '22_vertex_colors_and_attributes.gltf'); const asset = JSON.parse(fs.readFileSync(gltfPath));