def create_flip(): name = "FlipX" # only create the material if we haven't already created it, then just grab it if name not in bpy.data.node_groups: # create a group test_group = bpy.data.node_groups.new(name, 'ShaderNodeTree') else: test_group = bpy.data.node_groups[name] for node in test_group.nodes: test_group.nodes.remove(node) for node in test_group.inputs: test_group.inputs.remove(node) for node in test_group.outputs: test_group.outputs.remove(node) # create group inputs group_inputs = test_group.nodes.new('NodeGroupInput') group_inputs.location = (-350, 0) test_group.inputs.new('NodeSocketVectorXYZ', 'in') # create group outputs group_outputs = test_group.nodes.new('NodeGroupOutput') group_outputs.location = (300, 0) test_group.outputs.new('NodeSocketVectorXYZ', 'out') split = test_group.nodes.new('ShaderNodeSeparateXYZ') split.label = "Split" test_group.links.new(group_inputs.outputs["in"], split.inputs[0]) flip = test_group.nodes.new('ShaderNodeMath') flip.operation = 'MULTIPLY' test_group.links.new(split.outputs[0], flip.inputs[0]) flip.inputs[1].default_value = -1.0 join = test_group.nodes.new('ShaderNodeCombineXYZ') join.label = "Join" test_group.links.new(flip.outputs[0], join.inputs[0]) test_group.links.new(split.outputs[1], join.inputs[1]) test_group.links.new(split.outputs[2], join.inputs[2]) # #link output test_group.links.new(join.outputs[0], group_outputs.inputs['out']) nodes_iterate(test_group, group_outputs) return test_group
def create_material(in_dir, matname): print(f"Importing material {matname}") # only create the material if it doesn't exist in the blend file, then just grab it # but we overwrite its contents anyway if matname not in bpy.data.materials: b_mat = bpy.data.materials.new(matname) else: b_mat = bpy.data.materials[matname] fgm_path = os.path.join(in_dir, matname + ".fgm") # print(fgm_path) try: fgm_data = FgmFile() fgm_data.load(fgm_path) except FileNotFoundError: print(f"{fgm_path} does not exist!") return b_mat # base_index = fgm_data.textures[0].layers[1] # height_index = fgm_data.textures[1].layers[1] tree = get_tree(b_mat) output = tree.nodes.new('ShaderNodeOutputMaterial') principled = tree.nodes.new('ShaderNodeBsdfPrincipled') all_textures = [ file for file in os.listdir(in_dir) if file.lower().endswith(".png") ] # map texture names to node tex_dic = {} for fgm_texture in fgm_data.textures: png_base = fgm_texture.name.lower() if "blendweights" in png_base or "warpoffset" in png_base: continue textures = [ file for file in all_textures if file.lower().startswith(png_base) ] if not textures: png_base = png_base.lower().replace("_eyes", "").replace( "_fin", "").replace("_shell", "") textures = [ file for file in all_textures if file.lower().startswith(png_base) ] if not textures: textures = [ png_base + ".png", ] # print(textures) for png_name in textures: png_path = os.path.join(in_dir, png_name) b_tex = load_tex(tree, png_path) k = png_name.lower().split(".")[1] tex_dic[k] = b_tex # get diffuse and AO for diffuse_name in ("pdiffusetexture", "pbasediffusetexture", "pbasecolourtexture", "pbasecolourandmasktexture", "pdiffusealphatexture", "pdiffuse_alphatexture", "palbinobasecolourandmasktexture"): # get diffuse if diffuse_name in tex_dic: diffuse = tex_dic[diffuse_name] # get AO for ao_name in ("paotexture", "pbasepackedtexture_03"): if ao_name in tex_dic: ao = tex_dic[ao_name] ao.image.colorspace_settings.name = "Non-Color" # apply AO to diffuse diffuse_premix = tree.nodes.new('ShaderNodeMixRGB') diffuse_premix.blend_type = "MULTIPLY" diffuse_premix.inputs["Fac"].default_value = .25 tree.links.new(diffuse.outputs[0], diffuse_premix.inputs["Color1"]) tree.links.new(ao.outputs[0], diffuse_premix.inputs["Color2"]) diffuse = diffuse_premix break # get marking fur_names = [ k for k in tex_dic.keys() if "marking" in k and "noise" not in k and "patchwork" not in k ] lut_names = [k for k in tex_dic.keys() if "pclut" in k] if fur_names and lut_names: marking = tex_dic[sorted(fur_names)[0]] lut = tex_dic[sorted(lut_names)[0]] marking.image.colorspace_settings.name = "Non-Color" # PZ LUTs usually occupy half of the texture, so scale the incoming greyscale coordinates so that # 1 lands in the center of the LUT scaler = tree.nodes.new('ShaderNodeMath') scaler.operation = "MULTIPLY" tree.links.new(marking.outputs[0], scaler.inputs[0]) scaler.inputs[1].default_value = 0.5 tree.links.new(scaler.outputs[0], lut.inputs[0]) # apply AO to diffuse diffuse_premix = tree.nodes.new('ShaderNodeMixRGB') diffuse_premix.blend_type = "MIX" tree.links.new(diffuse.outputs[0], diffuse_premix.inputs["Color1"]) tree.links.new(lut.outputs[0], diffuse_premix.inputs["Color2"]) tree.links.new(marking.outputs[0], diffuse_premix.inputs["Fac"]) diffuse = diffuse_premix # link finished diffuse to shader tree.links.new(diffuse.outputs[0], principled.inputs["Base Color"]) break if "pnormaltexture" in tex_dic: normal = tex_dic["pnormaltexture"] normal.image.colorspace_settings.name = "Non-Color" normal_map = tree.nodes.new('ShaderNodeNormalMap') tree.links.new(normal.outputs[0], normal_map.inputs[1]) # normal_map.inputs["Strength"].default_value = 1.0 tree.links.new(normal_map.outputs[0], principled.inputs["Normal"]) # PZ - specularity? for spec_name in ( "proughnesspackedtexture_02", "pspecularmaptexture_00", ): if spec_name in tex_dic: specular = tex_dic[spec_name] specular.image.colorspace_settings.name = "Non-Color" tree.links.new(specular.outputs[0], principled.inputs["Specular"]) # PZ - roughness? for roughness_name in ( "proughnesspackedtexture_01", ): # "pspecularmaptexture_01" ? if roughness_name in tex_dic: roughness = tex_dic[roughness_name] roughness.image.colorspace_settings.name = "Non-Color" tree.links.new(roughness.outputs[0], principled.inputs["Roughness"]) # JWE dinos - metalness for metal_name in ("pbasepackedtexture_02", ): if metal_name in tex_dic: metal = tex_dic[metal_name] metal.image.colorspace_settings.name = "Non-Color" tree.links.new(metal.outputs[0], principled.inputs["Metallic"]) # alpha alpha = None # JWE billboard: Foliage_Billboard if "pdiffusealphatexture" in tex_dic: alpha = tex_dic["pdiffusealphatexture"] alpha_pass = alpha.outputs[1] elif "pdiffuse_alphatexture" in tex_dic: alpha = tex_dic["pdiffuse_alphatexture"] alpha_pass = alpha.outputs[1] # PZ penguin elif "popacitytexture" in tex_dic: alpha = tex_dic["popacitytexture"] alpha_pass = alpha.outputs[0] elif "proughnesspackedtexture_00" in tex_dic and "Foliage_Clip" in fgm_data.shader_name: alpha = tex_dic["proughnesspackedtexture_00"] alpha_pass = alpha.outputs[0] # parrot: Metallic_Roughness_Clip -> 03 elif "proughnesspackedtexture_03" in tex_dic and "Foliage_Clip" not in fgm_data.shader_name: alpha = tex_dic["proughnesspackedtexture_03"] alpha_pass = alpha.outputs[0] if alpha: # transparency b_mat.blend_method = "CLIP" b_mat.shadow_method = "CLIP" for attrib in fgm_data.attributes: if attrib.name.lower() == "palphatestref": b_mat.alpha_threshold = attrib.value[0] break transp = tree.nodes.new('ShaderNodeBsdfTransparent') alpha_mixer = tree.nodes.new('ShaderNodeMixShader') tree.links.new(alpha_pass, alpha_mixer.inputs[0]) tree.links.new(transp.outputs[0], alpha_mixer.inputs[1]) tree.links.new(principled.outputs[0], alpha_mixer.inputs[2]) tree.links.new(alpha_mixer.outputs[0], output.inputs[0]) alpha_mixer.update() # no alpha else: b_mat.blend_method = "OPAQUE" tree.links.new(principled.outputs[0], output.inputs[0]) nodes_iterate(tree, output) return b_mat
def create_material(in_dir, matname): print(f"Importing material {matname}") # only create the material if it doesn't exist in the blend file, then just grab it # but we overwrite its contents anyway if matname not in bpy.data.materials: mat = bpy.data.materials.new(matname) else: mat = bpy.data.materials[matname] fgm_path = os.path.join(in_dir, matname + ".fgm") # print(fgm_path) try: fgm_data = FgmFile() fgm_data.load(fgm_path) except FileNotFoundError: print(f"{fgm_path} does not exist!") return mat # base_index = fgm_data.textures[0].layers[1] # height_index = fgm_data.textures[1].layers[1] tree = get_tree(mat) output = tree.nodes.new('ShaderNodeOutputMaterial') principled = tree.nodes.new('ShaderNodeBsdfPrincipled') all_textures = [ file for file in os.listdir(in_dir) if file.lower().endswith(".png") ] # map texture names to node tex_dic = {} for fgm_texture in fgm_data.textures: png_base = f"{matname}.{fgm_texture.name}".lower() if "blendweights" in png_base or "warpoffset" in png_base: continue textures = [ file for file in all_textures if file.lower().startswith(png_base) ] if not textures: png_base = png_base.lower().replace("_eyes", "").replace( "_fin", "").replace("_shell", "") textures = [ file for file in all_textures if file.lower().startswith(png_base) ] if not textures: textures = [ png_base + ".png", ] # print(textures) for png_name in textures: png_path = os.path.join(in_dir, png_name) b_tex = load_tex(tree, png_path) k = png_name.lower().split(".")[1] tex_dic[k] = b_tex # get diffuse and AO for diffuse_name in ("pbasediffusetexture", "pbasecolourtexture", "pbasecolourandmasktexture"): # get diffuse if diffuse_name in tex_dic: diffuse = tex_dic[diffuse_name] # get AO for ao_name in ("paotexture", "pbasepackedtexture_03"): if ao_name in tex_dic: ao = tex_dic[ao_name] ao.image.colorspace_settings.name = "Non-Color" # apply AO to diffuse diffuse_premix = tree.nodes.new('ShaderNodeMixRGB') diffuse_premix.blend_type = "MULTIPLY" diffuse_premix.inputs["Fac"].default_value = .25 tree.links.new(diffuse.outputs[0], diffuse_premix.inputs["Color1"]) tree.links.new(ao.outputs[0], diffuse_premix.inputs["Color2"]) diffuse = diffuse_premix break # link finished diffuse to shader tree.links.new(diffuse.outputs[0], principled.inputs["Base Color"]) break if "pnormaltexture" in tex_dic: normal = tex_dic["pnormaltexture"] normal.image.colorspace_settings.name = "Non-Color" normal_map = tree.nodes.new('ShaderNodeNormalMap') tree.links.new(normal.outputs[0], normal_map.inputs[1]) # normal_map.inputs["Strength"].default_value = 1.0 tree.links.new(normal_map.outputs[0], principled.inputs["Normal"]) # PZ - specularity? for spec_name in ("proughnesspackedtexture_02", ): if spec_name in tex_dic: specular = tex_dic[spec_name] specular.image.colorspace_settings.name = "Non-Color" tree.links.new(specular.outputs[0], principled.inputs["Specular"]) # PZ - roughness? for roughness_name in ("proughnesspackedtexture_01", ): if roughness_name in tex_dic: roughness = tex_dic[roughness_name] roughness.image.colorspace_settings.name = "Non-Color" tree.links.new(roughness.outputs[0], principled.inputs["Roughness"]) # JWE dinos - metalness for metal_name in ("pbasepackedtexture_02", ): if metal_name in tex_dic: metal = tex_dic[metal_name] metal.image.colorspace_settings.name = "Non-Color" tree.links.new(metal.outputs[0], principled.inputs["Metallic"]) # alpha if "proughnesspackedtexture_03" in tex_dic: # transparency mat.blend_method = "CLIP" mat.shadow_method = "CLIP" for attrib in fgm_data.attributes: if attrib.name.lower() == "palphatestref": mat.alpha_threshold = attrib.value[0] break # if material.AlphaBlendEnable: # mat.blend_method = "BLEND" transp = tree.nodes.new('ShaderNodeBsdfTransparent') alpha_mixer = tree.nodes.new('ShaderNodeMixShader') alpha = tex_dic["proughnesspackedtexture_03"] tree.links.new(alpha.outputs[0], alpha_mixer.inputs[0]) tree.links.new(transp.outputs[0], alpha_mixer.inputs[1]) tree.links.new(principled.outputs[0], alpha_mixer.inputs[2]) tree.links.new(alpha_mixer.outputs[0], output.inputs[0]) alpha_mixer.update() # no alpha else: mat.blend_method = "OPAQUE" tree.links.new(principled.outputs[0], output.inputs[0]) nodes_iterate(tree, output) return mat
def create_material(matcol_path): slots = load_matcol(matcol_path) matdir, mat_ext = os.path.split(matcol_path) matname = os.path.splitext(mat_ext)[0] print("MATERIAL:", matname) #only create the material if we haven't already created it, then just grab it if matname not in bpy.data.materials: mat = bpy.data.materials.new(matname) #only create the material if we haven't already created it, then just grab it else: mat = bpy.data.materials[matname] tree = get_tree(mat) height_group = create_height() transform_group = create_group() output = tree.nodes.new('ShaderNodeOutputMaterial') principled = tree.nodes.new('ShaderNodeBsdfPrincipled') last_mixer = None textures = [] for i, (infos, texture) in enumerate(slots): # skip default materials that have no fgm assigned if not texture: textures.append(None) continue print("Slot", i) slotnum = i # load the tiled texture tex = load_tex(tree, texture) # load the blendweights layer mask mask_path = os.path.join( matdir, matname + ".playered_blendweights_{:02}.png".format(i)) mask = load_tex(tree, mask_path) # height offset attribute print([i for i in infos[1].info.value][:2]) heightscale_lower, heightscale_upper = sorted( [i for i in infos[1].info.value][:2]) if not heightscale_lower and not heightscale_upper: heightscale_upper = 1.0 heightoffset = infos[2].info.value[0] heightscale = infos[3].info.value[0] height = tree.nodes.new("ShaderNodeGroup") height.node_tree = height_group height.inputs["heightScale"].default_value = heightscale height.inputs["heightOffset"].default_value = heightoffset height.inputs[ "heightBlendScale.lower"].default_value = heightscale_lower height.inputs[ "heightBlendScale.upper"].default_value = heightscale_upper tree.links.new(tex.outputs[0], height.inputs[0]) textures.append((height, mask)) transform = tree.nodes.new("ShaderNodeGroup") transform.node_tree = transform_group # m_uvRotationPosition uvrotpos = list(i for i in infos[6].info.value)[:3] transform.inputs["uvRotationPosition"].default_value = uvrotpos # m_UVOffset uvoffset = list(i for i in infos[4].info.value)[:3] transform.inputs["UVOffset"].default_value = uvoffset # m_uvTile uvscale = list(i for i in infos[7].info.value)[:3] transform.inputs["uvTile"].default_value = uvscale # m_uvRotationAngle # matcol stores it as fraction of 180° # in radians for blender internally even though it displays as degree rot = math.radians(infos[5].info.value[0] * 180) # flip since blender flips V coord transform.inputs["uvRotationAngle"].default_value = -rot tree.links.new(transform.outputs[0], tex.inputs[0]) tex.update() mask.update() indices = [] for i_a in range(4): for i_b in range(4): indices.append(i_a + i_b * 4) indices = list(i for i in range(slotnum)) print(indices) normal_path = os.path.join(matdir, matname + ".pnormaltexture.png") normal = load_tex(tree, normal_path) normal.image.colorspace_settings.name = "Non-Color" normal_map = tree.nodes.new('ShaderNodeNormalMap') tree.links.new(normal.outputs[0], normal_map.inputs[1]) normal_map.inputs["Strength"].default_value = 2.0 # # bump = tree.nodes.new('ShaderNodeBump') # bump.inputs["Strength"].default_value = 0.5 # bump.inputs["Distance"].default_value = 0.1 #tree.links.new(normal_map.outputs[0], bump.inputs["Normal"]) last_mixer = normal_map for i in indices: # skip empty slots if textures[i]: height, mask = textures[i] bump = tree.nodes.new('ShaderNodeBump') tree.links.new(mask.outputs[0], bump.inputs[0]) tree.links.new(last_mixer.outputs[0], bump.inputs["Normal"]) tree.links.new(height.outputs[0], bump.inputs["Height"]) last_mixer = bump # tree.links.new(mixRGB.outputs[0], bump.inputs[2]) diffuse_path = os.path.join(matdir, matname + ".pbasediffusetexture.png") diffuse = load_tex(tree, diffuse_path) roughness_path = os.path.join(matdir, matname + ".pbasepackedtexture_01.png") roughness = load_tex(tree, roughness_path) roughness.image.colorspace_settings.name = "Non-Color" ao_path = os.path.join(matdir, matname + ".pbasepackedtexture_03.png") ao = load_tex(tree, ao_path) ao.image.colorspace_settings.name = "Non-Color" # apply AO to diffuse diffuse_premix = tree.nodes.new('ShaderNodeMixRGB') diffuse_premix.blend_type = "MULTIPLY" diffuse_premix.inputs["Fac"].default_value = .25 tree.links.new(diffuse.outputs[0], diffuse_premix.inputs["Color1"]) tree.links.new(ao.outputs[0], diffuse_premix.inputs["Color2"]) tree.links.new(diffuse_premix.outputs[0], principled.inputs["Base Color"]) tree.links.new(roughness.outputs[0], principled.inputs["Metallic"]) tree.links.new(bump.outputs[0], principled.inputs["Normal"]) tree.links.new(principled.outputs[0], output.inputs[0]) nodes_iterate(tree, output) return mat
def create_height(): name = "MatcolHeight" # only create the material if we haven't already created it, then just grab it if name not in bpy.data.node_groups: # create a group test_group = bpy.data.node_groups.new(name, 'ShaderNodeTree') else: test_group = bpy.data.node_groups[name] for node in test_group.nodes: test_group.nodes.remove(node) for node in test_group.inputs: test_group.inputs.remove(node) for node in test_group.outputs: test_group.outputs.remove(node) # create group inputs group_inputs = test_group.nodes.new('NodeGroupInput') group_inputs.location = (-350, 0) test_group.inputs.new('NodeSocketFloat', 'texture') test_group.inputs.new('NodeSocketFloat', 'heightBlendScale.lower') test_group.inputs.new('NodeSocketFloat', 'heightBlendScale.upper') test_group.inputs.new('NodeSocketFloat', 'heightOffset') test_group.inputs.new('NodeSocketFloat', 'heightScale') # create group outputs group_outputs = test_group.nodes.new('NodeGroupOutput') group_outputs.location = (300, 0) test_group.outputs.new('NodeSocketFloat', 'texture') # create three math nodes in a group heightScale = test_group.nodes.new('ShaderNodeMath') heightScale.label = "heightScale" heightScale.operation = 'MULTIPLY' test_group.links.new(group_inputs.outputs["texture"], heightScale.inputs[0]) test_group.links.new(group_inputs.outputs["heightScale"], heightScale.inputs[1]) heightOffset = test_group.nodes.new('ShaderNodeMath') heightOffset.label = "heightOffset" heightOffset.operation = 'ADD' test_group.links.new(heightScale.outputs[0], heightOffset.inputs[0]) test_group.links.new(group_inputs.outputs["heightOffset"], heightOffset.inputs[1]) heightBlendScale = test_group.nodes.new('ShaderNodeMapRange') heightBlendScale.label = "heightBlendScale" heightBlendScale.clamp = False test_group.links.new(heightOffset.outputs[0], heightBlendScale.inputs[0]) test_group.links.new(group_inputs.outputs["heightBlendScale.lower"], heightBlendScale.inputs[1]) test_group.links.new(group_inputs.outputs["heightBlendScale.upper"], heightBlendScale.inputs[2]) scale = test_group.nodes.new('ShaderNodeMath') scale.label = "scale" scale.operation = 'MULTIPLY' test_group.links.new(heightBlendScale.outputs[0], scale.inputs[0]) scale.inputs[1].default_value = 2.0 # #link output test_group.links.new(scale.outputs[0], group_outputs.inputs['texture']) nodes_iterate(test_group, group_outputs) return test_group
def create_group(): flipgr = create_flip() name = "MatcolSlot" #only create the material if we haven't already created it, then just grab it if name not in bpy.data.node_groups: # create a group test_group = bpy.data.node_groups.new(name, 'ShaderNodeTree') else: test_group = bpy.data.node_groups[name] for node in test_group.nodes: test_group.nodes.remove(node) for node in test_group.inputs: test_group.inputs.remove(node) for node in test_group.outputs: test_group.outputs.remove(node) # create group inputs group_inputs = test_group.nodes.new('NodeGroupInput') test_group.inputs.new('NodeSocketVectorTranslation', 'UVOffset') test_group.inputs.new('NodeSocketFloatAngle', 'uvRotationAngle') test_group.inputs.new('NodeSocketVectorTranslation', 'uvRotationPosition') test_group.inputs.new('NodeSocketVectorXYZ', 'uvTile') # create group outputs group_outputs = test_group.nodes.new('NodeGroupOutput') group_outputs.location = (300, 0) test_group.outputs.new('NodeSocketVectorXYZ', 'out') offset_flipx = test_group.nodes.new("ShaderNodeGroup") offset_flipx.node_tree = flipgr test_group.links.new(group_inputs.outputs["UVOffset"], offset_flipx.inputs[0]) rotpos_flipx = test_group.nodes.new("ShaderNodeGroup") rotpos_flipx.node_tree = flipgr test_group.links.new(group_inputs.outputs["uvRotationPosition"], rotpos_flipx.inputs[0]) uv = test_group.nodes.new('ShaderNodeUVMap') uv.label = "UV Input" uv.uv_map = "UV0" scale_pivot = test_group.nodes.new('ShaderNodeMapping') scale_pivot.inputs[1].default_value[1] = -1.0 scale_pivot.label = "Scale Pivot" test_group.links.new(uv.outputs[0], scale_pivot.inputs[0]) uv_offset = test_group.nodes.new('ShaderNodeMapping') uv_offset.label = "UVOffset" test_group.links.new(scale_pivot.outputs[0], uv_offset.inputs[0]) test_group.links.new(offset_flipx.outputs[0], uv_offset.inputs[1]) uv_tile = test_group.nodes.new('ShaderNodeMapping') uv_tile.label = "uvTile" test_group.links.new(uv_offset.outputs[0], uv_tile.inputs[0]) test_group.links.new(group_inputs.outputs["uvTile"], uv_tile.inputs[3]) rot_pivot = test_group.nodes.new('ShaderNodeMapping') rot_pivot.inputs[1].default_value[1] = -1.0 rot_pivot.label = "Rot Pivot" test_group.links.new(uv_tile.outputs[0], rot_pivot.inputs[0]) uv_rot_pos_a = test_group.nodes.new('ShaderNodeMapping') uv_rot_pos_a.label = "uvRotationPosition" test_group.links.new(rot_pivot.outputs[0], uv_rot_pos_a.inputs[0]) test_group.links.new(rotpos_flipx.outputs[0], uv_rot_pos_a.inputs[1]) # extra step to create vector from float uv_rot_combine = test_group.nodes.new('ShaderNodeCombineXYZ') uv_rot_combine.label = "build uvRotation Vector" test_group.links.new(group_inputs.outputs["uvRotationAngle"], uv_rot_combine.inputs[2]) uv_rot = test_group.nodes.new('ShaderNodeMapping') uv_rot.label = "uvRotationAngle" test_group.links.new(uv_rot_pos_a.outputs[0], uv_rot.inputs[0]) test_group.links.new(uv_rot_combine.outputs[0], uv_rot.inputs[2]) # extra step to negate input uv_rot_pos_flip = test_group.nodes.new('ShaderNodeVectorMath') uv_rot_pos_flip.operation = "SCALE" uv_rot_pos_flip.label = "flip uvRotationPosition" # counter intuitive index for non-vector argument! try: uv_rot_pos_flip.inputs[2].default_value = -1.0 except: print("bug with new blender 2.9, unsure how to solve") pass test_group.links.new(rotpos_flipx.outputs[0], uv_rot_pos_flip.inputs[0]) uv_rot_pos_b = test_group.nodes.new('ShaderNodeMapping') uv_rot_pos_b.label = "undo uvRotationPosition" test_group.links.new(uv_rot_pos_flip.outputs[0], uv_rot_pos_b.inputs[1]) test_group.links.new(uv_rot.outputs[0], uv_rot_pos_b.inputs[0]) # #link output test_group.links.new(uv_rot_pos_b.outputs[0], group_outputs.inputs['out']) nodes_iterate(test_group, group_outputs) return test_group