From d9b7c972b4fbe4843d6a9f82750586aebb9cac1b Mon Sep 17 00:00:00 2001 From: Valtteri Koskivuori Date: Mon, 18 Dec 2023 21:00:54 +0200 Subject: [PATCH] blender: Convert node groups Many complex Blender scenes use node groups to share common node graph elements between different node graphs. I'm not sure how Cycles handles them internally, but for c-ray, we just expand these node groups and join them with their respective inputs and outputs in the higher level tree. Initial test with dining room (casa2) scene yields good results. Many nodes still need to be converted, but the node groups seem to work now. The way we handle node group outputs now is quite flaky, we just fetch the first output of type NodeSocketShader, and use that. We should do a mapping like we do with the inputs to deal with those properly. --- bindings/nodes/convert.py | 157 ++++++++++++++++++++++++-------------- 1 file changed, 101 insertions(+), 56 deletions(-) diff --git a/bindings/nodes/convert.py b/bindings/nodes/convert.py index ee376e97..18cdf3d4 100644 --- a/bindings/nodes/convert.py +++ b/bindings/nodes/convert.py @@ -54,11 +54,13 @@ def convert_node_tree(bl_depsgraph, mat, nt): # 8:A --> NodeSocketRotation # 9:B --> NodeSocketRotation -def parse_color(input): +def parse_color(input, group_inputs): match input.bl_idname: case 'NodeSocketColor': if input.is_linked: - return parse_color(input.links[0].from_node) + if input.links[0].from_node.bl_idname == 'NodeGroupInput': + return group_inputs[input.links[0].from_socket.name] + return parse_color(input.links[0].from_node, group_inputs) vals = input.default_value return NodeColorConstant(cr_color(vals[0], vals[1], vals[2], vals[3])) case 'ShaderNodeTexImage': @@ -68,9 +70,9 @@ def parse_color(input): path = input.image.filepath_from_user() return NodeColorImageTexture(path, 0) case 'ShaderNodeTexChecker': - color1 = parse_color(input.inputs['Color1']) - color2 = parse_color(input.inputs['Color2']) - scale = parse_value(input.inputs['Scale']) + color1 = parse_color(input.inputs['Color1'], group_inputs) + color2 = parse_color(input.inputs['Color2'], group_inputs) + scale = parse_value(input.inputs['Scale'], group_inputs) return NodeColorCheckerboard(color1, color2, scale) case 'ShaderNodeTexEnvironment': if input.image is None: @@ -82,12 +84,12 @@ def parse_color(input): color = input.outputs[0].default_value return NodeColorConstant(cr_color(color[0], color[1], color[2], color[3])) case 'ShaderNodeMix': - factor = parse_value(input.inputs[0]) + factor = parse_value(input.inputs[0], group_inputs) if input.inputs[4].is_linked and input.inputs[5].is_linked: - return NodeColorVecToColor(parse_vector(input)) + return NodeColorVecToColor(parse_vector(input, group_inputs)) - a = parse_color(input.inputs[6]) - b = parse_color(input.inputs[7]) + a = parse_color(input.inputs[6], group_inputs) + b = parse_color(input.inputs[7], group_inputs) return NodeColorMix(a, b, factor) # case 'ShaderNodeBlackbody': # return warning_color @@ -177,31 +179,36 @@ def map_math_op(bl_op): print("Unknown math op {}, defaulting to ADD".format(bl_op)) return m.Add -def parse_value(input): +def parse_value(input, group_inputs): match input.bl_idname: case 'NodeSocketFloat': if input.is_linked: - return parse_value(input.links[0].from_node) + if input.links[0].from_node.bl_idname == 'NodeGroupInput': + return group_inputs[input.links[0].from_socket.name] + return parse_value(input.links[0].from_node, group_inputs) return NodeValueConstant(input.default_value) case 'NodeSocketFloatFactor': # note: same as Float, but range is [0,1] if input.is_linked: - return parse_value(input.links[0].from_node) + if input.links[0].from_node.bl_idname == 'NodeGroupInput': + return group_inputs[input.links[0].from_socket.name] + return parse_value(input.links[0].from_node, group_inputs) return NodeValueConstant(input.default_value) case 'ShaderNodeValue': value = input.outputs[0].default_value return NodeValueConstant(value) case 'ShaderNodeMath': - a = parse_value(input.inputs[0]) - b = parse_value(input.inputs[1]) - # c = parse_value(input.inputs[2]) + a = parse_value(input.inputs[0], group_inputs) + b = parse_value(input.inputs[1], group_inputs) + # c = parse_value(input.inputs[2], group_inputs) op = map_math_op(input.operation) return NodeValueMath(a, b, op) case 'ShaderNodeVectorMath': - vec = parse_vector(input) + vec = parse_vector(input, group_inputs) return NodeValueVecToValue(_component.F, vec) case _: print("Unknown value node of type {}, maybe fix.".format(input.bl_idname)) + return NodeValueConstant(0.0) # From here: https://docs.blender.org/api/current/bpy_types_enum_items/node_vec_math_items.html#rna-enum-node-vec-math-items # Commented ones aren't implemented yet, and will emit a notice to stdout @@ -261,23 +268,25 @@ def map_vec_op(bl_op): print("Unknown vector op {}, defaulting to ADD".format(bl_op)) return v.Add -def parse_vector(input): +def parse_vector(input, group_inputs): match input.bl_idname: case 'NodeSocketVector': if input.is_linked: - return parse_vector(input.links[0].from_node) + if input.links[0].from_node.bl_idname == 'NodeGroupInput': + return group_inputs[input.links[0].from_socket.name] + return parse_vector(input.links[0].from_node, group_inputs) vec = input.default_value return NodeVectorConstant(cr_vector(vec[0], vec[1], vec[2])) case 'ShaderNodeMix': - a = parse_vector(input.inputs[4]) - b = parse_vector(input.inputs[5]) - factor = parse_value(input.inputs[0]) + a = parse_vector(input.inputs[4], group_inputs) + b = parse_vector(input.inputs[5], group_inputs) + factor = parse_value(input.inputs[0], group_inputs) return NodeVectorVecMix(a, b, factor) case 'ShaderNodeVectorMath': - a = parse_vector(input.inputs[0]) - b = parse_vector(input.inputs[1]) - c = parse_vector(input.inputs[2]) - f = parse_value(input.inputs[3]) + a = parse_vector(input.inputs[0], group_inputs) + b = parse_vector(input.inputs[1], group_inputs) + c = parse_vector(input.inputs[2], group_inputs) + f = parse_value(input.inputs[3], group_inputs) op = map_vec_op(input.operation) return NodeVectorVecMath(a, b, c, f, op) case 'ShaderNodeTexCoord': @@ -290,73 +299,109 @@ def parse_vector(input): print("{}: {}".format(output.name, output.is_linked)) case _: print("Unknown vector node of type {}, maybe fix.".format(input.bl_idname)) + return NodeVectorConstant(cr_vector(0.0, 0.0, 0.0)) warning_shader = NodeShaderDiffuse(warning_color) -def parse_node(input): +# group_inputs is populated when we're parsing a node group (see ShaderNodeGroup below) +def parse_node(input, group_inputs=None): match input.bl_idname: + case 'ShaderNodeGroup': + if not input.node_tree.nodes['Group Output']: + print("No group output in node group {}".format(input.name)) + return warning_shader + if not input.node_tree.nodes['Group Input']: + print("No group input in node group {}".format(input.name)) + return warning_shader + subroot = input.node_tree.nodes['Group Output'] + # Hack - Just blindly find first shader input and use that + # TODO: Should handle more than one shader output for a node group + first_output = None + for sub_output in subroot.inputs: + if sub_output.is_linked and sub_output.bl_idname == 'NodeSocketShader': + first_output = sub_output + if not first_output: + print("Couldn't find shader output in subtree {}".format(input.name)) + return warning_shader + # if not subroot.inputs['Surface'].is_linked: + # print("Subroot {} output surface socket not linked".format(input.name)) + # return warning_shader + subtree_inputs = {} + for bl_input in input.inputs: + match bl_input.bl_idname: + case 'NodeSocketFloat' | 'NodeSocketFloatFactor': + subtree_inputs[bl_input.name] = parse_value(bl_input, group_inputs) + case 'NodeSocketVector': + subtree_inputs[bl_input.name] = parse_vector(bl_input, group_inputs) + case 'NodeSocketColor': + subtree_inputs[bl_input.name] = parse_color(bl_input, group_inputs) + case 'NodeSocketShader': + subtree_inputs[bl_input.name] = parse_node(bl_input, group_inputs) + return parse_node(first_output.links[0].from_node, subtree_inputs) case 'NodeSocketShader': if input.is_linked: - return parse_node(input.links[0].from_node) + if input.links[0].from_node.bl_idname == 'NodeGroupInput': + return group_inputs[input.links[0].from_socket.name] + return parse_node(input.links[0].from_node, group_inputs) return None case 'ShaderNodeBsdfDiffuse': - color = parse_color(input.inputs['Color']) + color = parse_color(input.inputs['Color'], group_inputs) return NodeShaderDiffuse(color) # note: missing roughness + normal case 'ShaderNodeBsdfGlass': - color = parse_color(input.inputs['Color']) - rough = parse_value(input.inputs['Roughness']) - ior = parse_value(input.inputs['IOR']) + color = parse_color(input.inputs['Color'], group_inputs) + rough = parse_value(input.inputs['Roughness'], group_inputs) + ior = parse_value(input.inputs['IOR'], group_inputs) # note: skipping normal return NodeShaderGlass(color, rough, ior) case 'ShaderNodeBsdfTransparent': - color = parse_color(input.inputs['Color']) + color = parse_color(input.inputs['Color'], group_inputs) return NodeShaderTransparent(color) case 'ShaderNodeBsdfTranslucent': - color = parse_color(input.inputs['Color']) + color = parse_color(input.inputs['Color'], group_inputs) return NodeShaderTranslucent(color) case 'ShaderNodeBackground': - color = parse_color(input.inputs['Color']) + color = parse_color(input.inputs['Color'], group_inputs) # Blender doesn't specify the pose here pose = NodeVectorConstant(cr_vector(0.0, 0.0, 0.0)) - strength = parse_value(input.inputs['Strength']) + strength = parse_value(input.inputs['Strength'], group_inputs) return NodeShaderBackground(color, pose, strength) case 'ShaderNodeMixShader': - factor = parse_value(input.inputs[0]) - a = parse_node(input.inputs[1]) - b = parse_node(input.inputs[2]) + factor = parse_value(input.inputs[0], group_inputs) + a = parse_node(input.inputs[1], group_inputs) + b = parse_node(input.inputs[2], group_inputs) return NodeShaderMix(a, b, factor) case 'ShaderNodeBsdfTransparent': - color = parse_color(input.inputs['Color']) + color = parse_color(input.inputs['Color'], group_inputs) return NodeShaderTransparent(color) case 'ShaderNodeBsdfTranslucent': - color = parse_color(input.inputs['Color']) + color = parse_color(input.inputs['Color'], group_inputs) return NodeShaderTranslucent(color) case 'ShaderNodeEmission': - color = parse_color(input.inputs['Color']) - strength = parse_value(input.inputs['Strength']) + color = parse_color(input.inputs['Color'], group_inputs) + strength = parse_value(input.inputs['Strength'], group_inputs) return NodeShaderEmissive(color, strength) case 'ShaderNodeAddShader': - a = parse_node(input.inputs[0]) - b = parse_node(input.inputs[1]) + a = parse_node(input.inputs[0], group_inputs) + b = parse_node(input.inputs[1], group_inputs) return NodeShaderAdd(a, b) case 'ShaderNodeBsdfPrincipled': # I haven't read how this works, so for now, we just patch in a rough approximation # Patch behaves slightly differently to a real principled shader, primarily because we don't handle the specular portion. We also don't support subsurface or 'sheen' - base_color = parse_color(input.inputs['Base Color']) - base_metallic = parse_value(input.inputs['Metallic']) - base_roughness = parse_value(input.inputs['Roughness']) - base_ior = parse_value(input.inputs['IOR']) - base_alpha = parse_value(input.inputs['Alpha']) + base_color = parse_color(input.inputs['Base Color'], group_inputs) + base_metallic = parse_value(input.inputs['Metallic'], group_inputs) + base_roughness = parse_value(input.inputs['Roughness'], group_inputs) + base_ior = parse_value(input.inputs['IOR'], group_inputs) + base_alpha = parse_value(input.inputs['Alpha'], group_inputs) - transmission_weight = parse_value(input.inputs['Transmission Weight']) + transmission_weight = parse_value(input.inputs['Transmission Weight'], group_inputs) - coat_ior = parse_value(input.inputs['Coat IOR']) - coat_roughness = parse_value(input.inputs['Coat Roughness']) - coat_tint = parse_color(input.inputs['Coat Tint']) - coat_weight = parse_value(input.inputs['Coat Weight']) + coat_ior = parse_value(input.inputs['Coat IOR'], group_inputs) + coat_roughness = parse_value(input.inputs['Coat Roughness'], group_inputs) + coat_tint = parse_color(input.inputs['Coat Tint'], group_inputs) + coat_weight = parse_value(input.inputs['Coat Weight'], group_inputs) - emission_color = parse_color(input.inputs['Emission Color']) - emission_strength = parse_value(input.inputs['Emission Strength']) + emission_color = parse_color(input.inputs['Emission Color'], group_inputs) + emission_strength = parse_value(input.inputs['Emission Strength'], group_inputs) return build_fake_principled( base_color,