Przeglądaj źródła

Merge pull request #56 from Jason0214/support_shader_node_tree

Support cycle material node tree
sdfgeoff 7 lat temu
rodzic
commit
2d200b70ce
22 zmienionych plików z 2333 dodań i 26 usunięć
  1. 36 13
      io_scene_godot/converters/material.py
  2. 3 0
      io_scene_godot/converters/material_node_tree/__init__.py
  3. 122 0
      io_scene_godot/converters/material_node_tree/exporters.py
  4. 519 0
      io_scene_godot/converters/material_node_tree/node_vistors.py
  5. 619 0
      io_scene_godot/converters/material_node_tree/shader_functions.py
  6. 583 0
      io_scene_godot/converters/material_node_tree/shaders.py
  7. 59 8
      io_scene_godot/converters/mesh.py
  8. 4 5
      tests/godot_project/default_env.tres
  9. 88 0
      tests/reference_exports/material/object_link_material.escn
  10. BIN
      tests/reference_exports/material_cycle/brick_4_bump_1k.jpg.jpeg
  11. BIN
      tests/reference_exports/material_cycle/brick_4_diff_1k.jpg
  12. BIN
      tests/reference_exports/material_cycle/brick_floor_nor_1k.jpg.jpeg
  13. 101 0
      tests/reference_exports/material_cycle/material_anistropy.escn
  14. 48 0
      tests/reference_exports/material_cycle/material_cycle.escn
  15. 74 0
      tests/reference_exports/material_cycle/material_normal.escn
  16. 77 0
      tests/reference_exports/material_cycle/material_unpack_texture.escn
  17. BIN
      tests/test_scenes/material/object_link_material.blend
  18. BIN
      tests/test_scenes/material_cycle/brick_4_diff_1k.jpg
  19. BIN
      tests/test_scenes/material_cycle/material_anistropy.blend
  20. BIN
      tests/test_scenes/material_cycle/material_cycle.blend
  21. BIN
      tests/test_scenes/material_cycle/material_normal.blend
  22. BIN
      tests/test_scenes/material_cycle/material_unpack_texture.blend

+ 36 - 13
io_scene_godot/converters/material.py

@@ -7,7 +7,8 @@ tree into a flat bunch of parameters is not trivial. So for someone else:"""
 import logging
 import os
 import bpy
-from ..structures import InternalResource, ExternalResource
+from .material_node_tree.exporters import export_node_tree
+from ..structures import InternalResource, ExternalResource, ValidationError
 
 
 def export_image(escn_file, export_settings, image):
@@ -35,7 +36,7 @@ def export_image(escn_file, export_settings, image):
 
 
 def export_material(escn_file, export_settings, material):
-    """ Exports a blender internal material as best it can"""
+    """Exports blender internal/cycles material as best it can"""
     external_material = find_material(export_settings, material)
     if external_material is not None:
         resource_id = escn_file.get_external_resource(material)
@@ -47,21 +48,43 @@ def export_material(escn_file, export_settings, material):
             resource_id = escn_file.add_external_resource(ext_mat, material)
         return "ExtResource({})".format(resource_id)
 
+    resource_id = generate_material_resource(
+        escn_file, export_settings, material
+    )
+    return "SubResource({})".format(resource_id)
+
+
+def generate_material_resource(escn_file, export_settings, material):
+    """Export blender material as an internal resource"""
     resource_id = escn_file.get_internal_resource(material)
-    # Existing internal resource
     if resource_id is not None:
-        return "SubResource({})".format(resource_id)
-    mat = InternalResource("SpatialMaterial")
+        return resource_id
 
-    mat['flags_unshaded'] = material.use_shadeless
-    mat['flags_vertex_lighting'] = material.use_vertex_color_light
-    mat['flags_transparent'] = material.use_transparency
-    mat['vertex_color_use_as_albedo'] = material.use_vertex_color_paint
-    mat['albedo_color'] = material.diffuse_color
-    mat['subsurf_scatter_enabled'] = material.subsurface_scattering.use
+    engine = bpy.context.scene.render.engine
 
-    resource_id = escn_file.add_internal_resource(mat, material)
-    return "SubResource({})".format(resource_id)
+    mat = None
+    if engine == 'CYCLES' and material.node_tree is not None:
+        mat = InternalResource("ShaderMaterial")
+        try:
+            export_node_tree(
+                escn_file, export_settings, material, mat
+            )
+        except ValidationError as exception:
+            mat = None  # revert to SpatialMaterial
+            logging.error(
+                str(exception) + ", in material '{}'".format(material.name)
+            )
+
+    if mat is None:
+        mat = InternalResource("SpatialMaterial")
+
+        mat['flags_unshaded'] = material.use_shadeless
+        mat['flags_vertex_lighting'] = material.use_vertex_color_light
+        mat['flags_transparent'] = material.use_transparency
+        mat['vertex_color_use_as_albedo'] = material.use_vertex_color_paint
+        mat['albedo_color'] = material.diffuse_color
+        mat['subsurf_scatter_enabled'] = material.subsurface_scattering.use
+    return escn_file.add_internal_resource(mat, material)
 
 
 # ------------------- Tools for finding existing materials -------------------

+ 3 - 0
io_scene_godot/converters/material_node_tree/__init__.py

@@ -0,0 +1,3 @@
+"""Module for export Blender CYCLES and EEVEE material node tree
+to Godot ShaderMaterial"""
+from .exporters import export_node_tree

+ 122 - 0
io_scene_godot/converters/material_node_tree/exporters.py

@@ -0,0 +1,122 @@
+"""Interface for node tree exporter"""
+import os
+import logging
+from shutil import copyfile
+import bpy
+from .shaders import ShaderGlobals
+from .node_vistors import find_node_visitor
+from ...structures import InternalResource, ExternalResource
+
+
+def find_material_output_node(node_tree):
+    """Find materia output node in the material node tree, if
+    two output nodes found, raise error"""
+    output_node = None
+    for node in node_tree.nodes:
+        if node.bl_idname == 'ShaderNodeOutputMaterial':
+            if output_node is None:
+                output_node = node
+            else:
+                logging.warning(
+                    "More than one material output node find",
+                )
+    return output_node
+
+
+def export_texture(escn_file, export_settings, image):
+    """Export texture image as an external resource"""
+    resource_id = escn_file.get_external_resource(image)
+    if resource_id is not None:
+        return resource_id
+
+    dst_dir_path = os.path.dirname(export_settings['path'])
+    dst_path = dst_dir_path + os.sep + image.name
+
+    if image.packed_file is not None:
+        # image is packed into .blend file
+        image_extension = '.' + image.file_format.lower()
+        if not image.name.endswith(image_extension):
+            dst_path = dst_path + image_extension
+        image.filepath_raw = dst_path
+        image.save()
+    else:
+        if image.filepath_raw.startswith("//"):
+            src_path = bpy.path.abspath(image.filepath_raw)
+        else:
+            src_path = image.filepath_raw
+        copyfile(src_path, dst_path)
+
+    img_resource = ExternalResource(dst_path, "Texture")
+    return escn_file.add_external_resource(img_resource, image)
+
+
+def traversal_tree_from_socket(shader, root_socket):
+    """Deep frist traversal the node tree from a root socket"""
+    if shader.is_socket_cached(root_socket):
+        return shader.fetch_variable_from_socket(root_socket)
+
+    def get_unvisited_depend_node(node):
+        """Return an unvisited node linked to the current node"""
+        for socket in node.inputs:
+            if socket.is_linked and not shader.is_socket_cached(socket):
+                return socket.links[0].from_node
+        return None
+
+    stack = list()
+    cur_node = root_socket.links[0].from_node
+
+    while stack or cur_node is not None:
+        while True:
+            next_node = get_unvisited_depend_node(cur_node)
+            if next_node is None:
+                break
+            stack.append(cur_node)
+            cur_node = next_node
+
+        visitor = find_node_visitor(shader, cur_node)
+        visitor(shader, cur_node)
+
+        if stack:
+            cur_node = stack.pop()
+        else:
+            cur_node = None
+
+    return shader.fetch_variable_from_socket(root_socket)
+
+
+def export_node_tree(escn_file, export_settings, cycle_mat, shader_mat):
+    """Export cycles material to godot shader script"""
+    shader_globals = ShaderGlobals()
+    fragment_shader = shader_globals.fragment_shader
+    vertex_shader = shader_globals.vertex_shader
+
+    mat_output_node = find_material_output_node(cycle_mat.node_tree)
+    if mat_output_node is not None:
+        surface_socket = mat_output_node.inputs['Surface']
+        displacement_socket = mat_output_node.inputs['Displacement']
+
+        if surface_socket.is_linked:
+            fragment_shader.add_bsdf_surface(
+                traversal_tree_from_socket(
+                    fragment_shader, surface_socket
+                )
+            )
+
+        if displacement_socket.is_linked:
+            fragment_shader.add_bump_displacement(
+                traversal_tree_from_socket(
+                    fragment_shader, displacement_socket
+                )
+            )
+
+    shader_resource = InternalResource('Shader')
+    shader_resource['code'] = '"{}"'.format(shader_globals.to_string())
+    resource_id = escn_file.add_internal_resource(
+        shader_resource, cycle_mat.node_tree
+    )
+    shader_mat['shader'] = "SubResource({})".format(resource_id)
+
+    for image, uniform_var in shader_globals.textures.items():
+        resource_id = export_texture(escn_file, export_settings, image)
+        shader_param_key = 'shader_param/{}'.format(str(uniform_var))
+        shader_mat[shader_param_key] = "ExtResource({})".format(resource_id)

+ 519 - 0
io_scene_godot/converters/material_node_tree/node_vistors.py

@@ -0,0 +1,519 @@
+"""A set of node visitor functions to convert material node to shader script"""
+import logging
+import mathutils
+from .shaders import (FragmentShader, VertexShader,
+                      Value, Variable, FragmentBSDFContainer)
+from .shader_functions import find_node_function
+from ...structures import ValidationError
+
+
+def visit_add_shader_node(shader, node):
+    """apply addition to albedo and it may have HDR, alpha is added with
+    its complementary, other attributes are averaged"""
+    output = FragmentBSDFContainer()
+
+    shader_socket_a = node.inputs[0]
+    if shader_socket_a.is_linked:
+        in_shader_a = shader.fetch_variable_from_socket(shader_socket_a)
+    else:
+        in_shader_a = FragmentBSDFContainer.default()
+
+    shader_socket_b = node.inputs[1]
+    if shader_socket_b.is_linked:
+        in_shader_b = shader.fetch_variable_from_socket(shader_socket_b)
+    else:
+        in_shader_b = FragmentBSDFContainer.default()
+
+    for attr_name in FragmentBSDFContainer.attribute_names_iterable():
+        attr_a = in_shader_a.get_attribute(attr_name)
+        attr_b = in_shader_b.get_attribute(attr_name)
+
+        if attr_a and attr_b:
+            attr_type = FragmentBSDFContainer.attribute_type(attr_name)
+            if attr_name in ("normal", "tangent"):
+                # don't mix normal and tangent, use default
+                continue
+            elif attr_name == "alpha":
+                code_pattern = '{} = 1 - clamp(2 - {} - {}, 0.0, 1.0);'
+            elif attr_name == "albedo":
+                # HDR
+                code_pattern = ('{} = {} + {};')
+            else:
+                code_pattern = '{} = mix({}, {}, 0.5);'
+            added_attr = shader.define_variable(
+                attr_type, node.name + '_' + attr_name
+            )
+            shader.append_code_line(
+                code_pattern,
+                (added_attr, attr_a, attr_b)
+            )
+            output.set_attribute(attr_name, added_attr)
+        elif attr_a:
+            output.set_attribute(attr_name, attr_a)
+        elif attr_b:
+            output.set_attribute(attr_name, attr_b)
+
+    shader.assign_variable_to_socket(node.outputs[0], output)
+
+
+def visit_mix_shader_node(shader, node):
+    """simply a mix of each attribute, note that for unconnect shader input,
+    albedo would default to black and alpha would default to 1.0."""
+    output = FragmentBSDFContainer()
+
+    in_fac_socket = node.inputs['Fac']
+    in_fac = Value('float', in_fac_socket.default_value)
+    if in_fac_socket.is_linked:
+        in_fac = shader.fetch_variable_from_socket(in_fac_socket)
+
+    in_shader_a = FragmentBSDFContainer.default()
+    in_shader_socket_a = node.inputs[1]
+    if in_shader_socket_a.is_linked:
+        in_shader_a = shader.fetch_variable_from_socket(in_shader_socket_a)
+
+    in_shader_b = FragmentBSDFContainer.default()
+    in_shader_socket_b = node.inputs[2]
+    if in_shader_socket_b.is_linked:
+        in_shader_b = shader.fetch_variable_from_socket(in_shader_socket_b)
+
+    for attribute_name in FragmentBSDFContainer.attribute_names_iterable():
+        attr_a = in_shader_a.get_attribute(attribute_name)
+        attr_b = in_shader_b.get_attribute(attribute_name)
+        # if one shader input has alpha, the other one should default to have
+        # alpha = 1.0
+        if attribute_name == 'alpha' and attr_a and not attr_b:
+            attr_b = Value('float', 1.0)
+        if attribute_name == 'alpha' and attr_b and not attr_a:
+            attr_a = Value('float', 1.0)
+
+        if attr_a and attr_b:
+            attr_type = FragmentBSDFContainer.attribute_type(attribute_name)
+            if attribute_name in ("normal", "tangent"):
+                # don't mix normal and tangent, use default
+                continue
+            mix_code_pattern = '{} = mix({}, {}, {});'
+            attr_mixed = shader.define_variable(
+                attr_type, node.name + '_' + attribute_name
+            )
+            shader.append_code_line(
+                mix_code_pattern,
+                (attr_mixed, attr_a, attr_b, in_fac)
+            )
+            output.set_attribute(attribute_name, attr_mixed)
+        elif attr_a:
+            output.set_attribute(attribute_name, attr_a)
+        elif attr_b:
+            output.set_attribute(attribute_name, attr_b)
+
+    shader.assign_variable_to_socket(node.outputs[0], output)
+
+
+def visit_bsdf_node(shader, node):
+    """Visitor for trivial bsdf nodes,
+    output in the format of a FragmentBSDFContainer"""
+    function = find_node_function(node)
+
+    output = FragmentBSDFContainer()
+    in_arguments = list()
+    out_arguments = list()
+
+    for socket_name in function.in_sockets:
+        socket = node.inputs[socket_name]
+        if socket.is_linked:
+            var = shader.fetch_variable_from_socket(socket)
+            in_arguments.append(var)
+        else:
+            input_value = Value.create_from_blender_value(
+                socket.default_value)
+            variable = shader.define_variable_from_socket(
+                node, socket
+            )
+            shader.append_assignment_code(variable, input_value)
+            in_arguments.append(variable)
+
+    for attr_name in function.out_sockets:
+        var_type = FragmentBSDFContainer.attribute_type(attr_name)
+        new_var = shader.define_variable(
+            var_type, node.name + '_output_' + attr_name
+        )
+
+        output.set_attribute(attr_name, new_var)
+        out_arguments.append(new_var)
+
+    shader.add_function_call(function, in_arguments, out_arguments)
+
+    # normal and tangent don't go to bsdf functions
+    normal_socket = node.inputs.get('Normal', None)
+    tangent_socket = node.inputs.get('Tangent', None)
+    # normal and tangent input to shader node is in view space
+    for name, socket in (
+            ('normal', normal_socket), ('tangent', tangent_socket)):
+        if socket is not None and socket.is_linked:
+            world_space_dir = shader.fetch_variable_from_socket(socket)
+            # convert to y-up axis
+            shader.zup_to_yup(world_space_dir)
+            # convert direction to view space
+            shader.world_to_view(world_space_dir)
+            output.set_attribute(name, world_space_dir)
+
+    if node.bl_idname in ('ShaderNodeBsdfGlass', 'ShaderNodeBsdfPrincipled'):
+        shader.glass_effect = True
+
+    shader.assign_variable_to_socket(node.outputs[0], output)
+
+
+def visit_reroute_node(shader, node):
+    """For reroute node, traversal it's child and cache the output result"""
+    input_socket = node.inputs[0]
+    if input_socket.is_linked:
+        var = shader.fetch_variable_from_socket(input_socket)
+    else:
+        logging.warning(
+            "'%s' has no input, at '%s'", node.bl_idname, node.name
+        )
+        var = Value('vec3', (1.0, 1.0, 1.0))
+
+    for output_socket in node.outputs:
+        shader.assign_variable_to_socket(output_socket, var)
+
+
+def visit_bump_node(shader, node):
+    """Convert bump node to shader script"""
+    function = find_node_function(node)
+
+    in_arguments = list()
+    for socket in node.inputs:
+        if socket.is_linked:
+            var = shader.fetch_variable_from_socket(socket)
+            if socket.identifier == 'Normal':
+                # convert from model, z-up to view y-up
+                # bump function is calculate in view space y-up
+                shader.zup_to_yup(var)
+                shader.world_to_view(var)
+                in_arguments.append(var)
+            else:
+                in_arguments.append(var)
+        else:
+            if socket.identifier == 'Normal':
+                in_arguments.append(Variable('vec3', 'NORMAL'))
+            else:
+                in_arguments.append(
+                    Value.create_from_blender_value(socket.default_value)
+                )
+
+    in_arguments.append(Variable('vec3', 'VERTEX'))
+    if node.invert:
+        in_arguments.append(Value('float', 1.0))
+    else:
+        in_arguments.append(Value('float', 0.0))
+
+    out_normal = shader.define_variable(
+        'vec3', node.name + '_out_normal'
+    )
+    shader.add_function_call(function, in_arguments, [out_normal])
+
+    if isinstance(shader, FragmentShader):
+        # convert output normal to world_space
+        shader.view_to_world(out_normal)
+    shader.yup_to_zup(out_normal)
+
+    shader.assign_variable_to_socket(node.outputs[0], out_normal)
+
+
+def visit_normal_map_node(shader, node):
+    """Convert normal map node to shader script, note that it can not
+    be used in vertex shader"""
+    if isinstance(shader, VertexShader):
+        raise ValidationError(
+            "'{}' not support in true displacement, at '{}'".format(
+                node.bl_idname,
+                node.name
+            )
+        )
+
+    in_arguments = list()
+    for socket in node.inputs:
+        if socket.is_linked:
+            in_arguments.append(
+                shader.fetch_variable_from_socket(socket)
+            )
+        else:
+            in_arguments.append(
+                Value.create_from_blender_value(socket.default_value)
+            )
+    function = find_node_function(node)
+    output_normal = shader.define_variable('vec3', node.name + '_out_normal')
+    if node.space == 'TANGENT':
+        in_arguments.append(Variable('vec3', 'NORMAL'))
+        in_arguments.append(Variable('vec3', 'TANGENT'))
+        in_arguments.append(Variable('vec3', 'BINORMAL'))
+        shader.add_function_call(function, in_arguments, [output_normal])
+        shader.view_to_world(output_normal)
+        shader.yup_to_zup(output_normal)
+
+    elif node.space == 'WORLD':
+        in_arguments.append(Variable('vec3', 'NORMAL'))
+        in_arguments.append(shader.invert_view_mat)
+        shader.add_function_call(function, in_arguments, [output_normal])
+        shader.yup_to_zup(output_normal)
+
+    elif node.space == 'OBJECT':
+        in_arguments.append(Variable('vec3', 'NORMAL'))
+        in_arguments.append(shader.invert_view_mat)
+        in_arguments.append(Variable('mat4', 'WORLD_MATRIX'))
+        shader.add_function_call(function, in_arguments, [output_normal])
+        shader.yup_to_zup(output_normal)
+
+    shader.assign_variable_to_socket(node.outputs[0], output_normal)
+
+
+def visit_texture_coord_node(shader, node):
+    """Convert texture coordinate node to shader script"""
+    if node.outputs['UV'].is_linked:
+        shader.assign_variable_to_socket(
+            node.outputs['UV'],
+            Value("vec3", ('UV', 0.0)),
+        )
+
+    if isinstance(shader, FragmentShader):
+        if node.outputs['Window'].is_linked:
+            shader.assign_variable_to_socket(
+                node.outputs['Window'],
+                Value("vec3", ('SCREEN_UV', 0.0)),
+            )
+        if node.outputs['Camera'].is_linked:
+            shader.assign_variable_to_socket(
+                node.outputs['Camera'],
+                Value("vec3", ('VERTEX.xy', '-VERTEX.z')),
+            )
+
+    view_mat = Variable('mat4', 'INV_CAMERA_MATRIX')
+    world_mat = Variable('mat4', 'WORLD_MATRIX')
+    normal = Variable('vec3', 'NORMAL')
+    position = Variable('vec3', 'VERTEX')
+
+    if node.outputs['Normal'].is_linked:
+        normal_socket = node.outputs['Normal']
+        output_normal = shader.define_variable_from_socket(
+            node, normal_socket
+        )
+        shader.append_assignment_code(output_normal, normal)
+        if isinstance(shader, FragmentShader):
+            shader.view_to_model(output_normal)
+        shader.yup_to_zup(output_normal)
+        shader.assign_variable_to_socket(
+            normal_socket, output_normal
+        )
+
+    if node.outputs['Object'].is_linked:
+        obj_socket = node.outputs['Object']
+        output_obj_pos = shader.define_variable_from_socket(
+            node, obj_socket
+        )
+        shader.append_assignment_code(output_obj_pos, position)
+        if isinstance(shader, FragmentShader):
+            shader.view_to_model(output_obj_pos, False)
+        shader.yup_to_zup(output_obj_pos)
+        shader.assign_variable_to_socket(obj_socket, output_obj_pos)
+
+    if node.outputs['Reflection'].is_linked:
+        ref_socket = node.outputs['Reflection']
+        reflect_output = shader.define_variable_from_socket(
+            node, ref_socket
+        )
+        if isinstance(shader, FragmentShader):
+            shader.append_code_line(
+                ('{} = (inverse({}) * vec4('
+                 'reflect(normalize({}), {}), 0.0)).xyz;'),
+                (reflect_output, view_mat, position, normal)
+            )
+        else:
+            shader.append_code_line(
+                '{} = (reflect(normalize({}, {}), 0.0)).xyz;',
+                (reflect_output, position, normal)
+            )
+        shader.yup_to_zup(reflect_output)
+        shader.assign_variable_to_socket(ref_socket, reflect_output)
+
+    if node.outputs['Generated'].is_linked:
+        logging.warning(
+            'Texture coordinates `Generated` not supported'
+        )
+        shader.assign_variable_to_socket(
+            node.outputs['Generated'],
+            Value('vec3', (1.0, 1.0, 1.0))
+        )
+
+
+def visit_rgb_node(shader, node):
+    """Convert rgb input node to shader scripts"""
+    output = node.outputs[0]
+    shader.assign_variable_to_socket(
+        output,
+        Value.create_from_blender_value(output.default_value)
+    )
+
+
+def visit_image_texture_node(shader, node):
+    """Store image texture as a uniform"""
+    function = find_node_function(node)
+
+    in_arguments = list()
+
+    tex_coord = Value.create_from_blender_value(
+        node.inputs[0].default_value)
+    if node.inputs[0].is_linked:
+        tex_coord = shader.fetch_variable_from_socket(node.inputs[0])
+
+    if node.image is None:
+        logging.warning(
+            "Image Texture node '%s' has no image being set",
+            node.name
+        )
+
+    if node.image is None or node.image not in shader.global_ref.textures:
+        tex_image_var = shader.global_ref.define_uniform(
+            "sampler2D", node.name + "texture_image"
+        )
+        shader.global_ref.add_image_texture(
+            tex_image_var, node.image
+        )
+    else:
+        tex_image_var = shader.global_ref.textures[node.image]
+
+    in_arguments.append(tex_coord)
+    in_arguments.append(tex_image_var)
+
+    out_arguments = list()
+
+    for socket in node.outputs:
+        output_var = shader.define_variable_from_socket(
+            node, socket
+        )
+        out_arguments.append(output_var)
+        shader.assign_variable_to_socket(socket, output_var)
+
+    shader.add_function_call(function, in_arguments, out_arguments)
+
+
+def visit_mapping_node(shader, node):
+    """Mapping node which apply transform onto point or direction"""
+    function = find_node_function(node)
+
+    rot_mat = node.rotation.to_matrix().to_4x4()
+    loc_mat = mathutils.Matrix.Translation(node.translation)
+    sca_mat = mathutils.Matrix((
+        (node.scale[0], 0, 0),
+        (0, node.scale[1], 0),
+        (0, 0, node.scale[2]),
+    )).to_4x4()
+
+    in_vec = Value("vec3", (0.0, 0.0, 0.0))
+    if node.inputs[0].is_linked:
+        in_vec = shader.fetch_variable_from_socket(node.inputs[0])
+
+    if node.vector_type == "TEXTURE":
+        # Texture: Transform a texture by inverse
+        # mapping the texture coordinate
+        transform_mat = (loc_mat * rot_mat * sca_mat).inverted_safe()
+    elif node.vector_type == "POINT":
+        transform_mat = loc_mat * rot_mat * sca_mat
+    else:  # node.vector_type in ("VECTOR", "NORMAL")
+        # no translation for vectors
+        transform_mat = rot_mat * sca_mat
+
+    mat = Value.create_from_blender_value(transform_mat)
+    clamp_min = Value.create_from_blender_value(node.min)
+    clamp_max = Value.create_from_blender_value(node.max)
+    use_min = Value("float", 1.0 if node.use_min else 0.0)
+    use_max = Value("float", 1.0 if node.use_max else 0.0)
+
+    in_arguments = list()
+    in_arguments.append(in_vec)
+    in_arguments.append(mat)
+    in_arguments.append(clamp_min)
+    in_arguments.append(clamp_max)
+    in_arguments.append(use_min)
+    in_arguments.append(use_max)
+
+    out_vec = shader.define_variable_from_socket(
+        node, node.outputs[0]
+    )
+    shader.add_function_call(function, in_arguments, [out_vec])
+
+    if node.vector_type == "NORMAL":
+        # need additonal normalize
+        shader.append_code_line(
+            '{} = normalize({});',
+            (out_vec, out_vec)
+        )
+    shader.assign_variable_to_socket(node.outputs[0], out_vec)
+
+
+def visit_converter_node(shader, node):
+    """For genearl converter node, which has inputs and outputs and can be
+    parsed as a shader function"""
+    function = find_node_function(node)
+    in_arguments = list()
+
+    for socket in node.inputs:
+        if socket.is_linked:
+            # iput socket only has one link
+            in_arguments.append(
+                shader.fetch_variable_from_socket(socket)
+            )
+        else:
+            input_value = Value.create_from_blender_value(
+                socket.default_value)
+            variable = shader.define_variable_from_socket(
+                node, socket
+            )
+            shader.append_assignment_code(variable, input_value)
+            in_arguments.append(variable)
+
+    out_arguments = list()
+
+    for socket in node.outputs:
+        new_var = shader.define_variable_from_socket(node, socket)
+        shader.assign_variable_to_socket(socket, new_var)
+        out_arguments.append(new_var)
+
+    shader.add_function_call(function, in_arguments, out_arguments)
+
+
+def visit_tangent_node(shader, node):
+    """Visit tangent node"""
+    if node.direction_type != 'UV_MAP':
+        logging.warning(
+            'tangent space Radial not supported at %s',
+            node.name
+        )
+    shader.assign_variable_to_socket(
+        node.outputs[0], Variable('vec3', 'TANGENT')
+    )
+
+
+NODE_VISITOR_FUNCTIONS = {
+    'ShaderNodeMapping': visit_mapping_node,
+    'ShaderNodeTexImage': visit_image_texture_node,
+    'ShaderNodeTexCoord': visit_texture_coord_node,
+    'ShaderNodeRGB': visit_rgb_node,
+    'ShaderNodeNormalMap': visit_normal_map_node,
+    'ShaderNodeBump': visit_bump_node,
+    'NodeReroute': visit_reroute_node,
+    'ShaderNodeMixShader': visit_mix_shader_node,
+    'ShaderNodeAddShader': visit_add_shader_node,
+    'ShaderNodeTangent': visit_tangent_node,
+}
+
+
+def find_node_visitor(shader, node):
+    """Return a visitor function for the node"""
+    if node.bl_idname in NODE_VISITOR_FUNCTIONS:
+        return NODE_VISITOR_FUNCTIONS[node.bl_idname]
+
+    if node.outputs[0].identifier in ('Emission', 'BSDF', 'BSSRDF'):
+        # for shader node output bsdf closure
+        return visit_bsdf_node
+
+    return visit_converter_node

+ 619 - 0
io_scene_godot/converters/material_node_tree/shader_functions.py

@@ -0,0 +1,619 @@
+"""Prewritten shader scripts for node in material node tree"""
+import re
+from ...structures import ValidationError
+
+FUNCTION_HEAD_PATTERN = re.compile(
+    (r'void\s+([a-zA-Z]\w*)\s*\(((\s*(out\s+)?'
+     r'(vec2|vec3|vec4|float|mat4|sampler2D)\s+[a-zA-Z]\w*\s*,?)*)\)'),
+)
+
+
+class ShaderFunction:
+    """Shader function for a blender node"""
+    def __init__(self, code):
+        # at most one group
+        self.code = code
+        self.in_param_types = list()
+        self.out_param_types = list()
+
+        matched_group = FUNCTION_HEAD_PATTERN.findall(code)[0]
+        self.name = matched_group[0]
+        parameters_str = matched_group[1]
+
+        for param_str in parameters_str.strip().split(','):
+            tokens = tuple([x.strip() for x in param_str.split()])
+            if tokens[0] == 'out':
+                self.out_param_types.append(tokens[1])
+            else:
+                self.in_param_types.append(tokens[0])
+
+
+class BsdfShaderFunction(ShaderFunction):
+    """Function for bsdf shader node, has additional information of
+    input and output socket"""
+    def __init__(self, code, input_sockets, output_sockets):
+        super().__init__(code)
+        # linked socket ids of material node
+        self.in_sockets = tuple(input_sockets)
+        self.out_sockets = tuple(output_sockets)
+
+
+# Shader function nameing convention:
+#
+# The approach adopted by this addon to export material node is to
+# convert it to a shader function. In order to simplify the mapping
+# from Blender node to shader function, there is a naming convention
+# for all the node functions.
+#
+# Blender node all have a entry `bl_idname` which is in the format
+# of `ShaderNodexxx`. For an arbitrary Blender node, we drop the prefix
+# in its `bl_idname` and convert it to snake case as the corresponding
+# function name.
+#
+#   e.g. Blender node 'ShaderNodeBsdfPrincipled',
+#           it's function name is node_bsdf_principled
+#
+# You may notice some Blender node has options like `space`, `operation`.
+# In that case, if we want to convert node with differnet options to
+# different functions, we are append the lowercased option string to the
+# end of a function name.
+#
+#   e.g. Blender node 'ShaderNodeMath' with `operation` set as 'ADD',
+#     `clamp` not checked. It's function name is 'node_math_add_no_clamp'.
+
+
+FUNCTION_LIBS = [
+    # bsdf shader node functions
+    BsdfShaderFunction(
+        code="""
+void node_bsdf_principled(vec4 color, float subsurface, vec4 subsurface_color,
+        float metallic, float specular, float roughness, float clearcoat,
+        float clearcoat_roughness, float anisotropy, float transmission,
+        float IOR, out vec3 albedo, out float sss_strength_out,
+        out float metallic_out, out float specular_out,
+        out float roughness_out, out float clearcoat_out,
+        out float clearcoat_gloss_out, out float anisotropy_out,
+        out float transmission_out, out float ior) {
+    metallic = clamp(metallic, 0.0, 1.0);
+    transmission = clamp(transmission, 0.0, 1.0);
+
+    subsurface = subsurface * (1.0 - metallic);
+    transmission = transmission * (1.0 - metallic);
+
+    albedo = mix(color.rgb, subsurface_color.rgb, subsurface);
+    sss_strength_out = subsurface;
+    metallic_out = metallic;
+    specular_out = pow((IOR - 1.0)/(IOR + 1.0), 2)/0.08;
+    roughness_out = sqrt(roughness);
+    clearcoat_out = clearcoat * (1.0 - transmission);
+    clearcoat_gloss_out = 1.0 - clearcoat_roughness;
+    anisotropy_out = clamp(anisotropy, 0.0, 1.0);
+    transmission_out = transmission;
+    ior = IOR;
+}
+""",
+        input_sockets=[
+            "Base Color",
+            "Subsurface",
+            "Subsurface Color",
+            "Metallic",
+            "Specular",
+            "Roughness",
+            "Clearcoat",
+            "Clearcoat Roughness",
+            "Anisotropic",
+            "Transmission",
+            "IOR",
+        ],
+        output_sockets=[
+            "albedo",
+            "sss_strength",
+            "metallic",
+            "specular",
+            "roughness",
+            "clearcoat",
+            "clearcoat_gloss",
+            "anisotropy",
+            "transmission",
+            "ior",
+        ]
+    ),
+
+    BsdfShaderFunction(
+        code="""
+void node_emission(vec4 emission_color, float strength,
+        out vec3 emission_out){
+    emission_out = emission_color.rgb * strength;
+}
+""",
+        input_sockets=["Color", "Strength"],
+        output_sockets=["emission"]
+    ),
+
+    BsdfShaderFunction(
+        code="""
+void node_bsdf_diffuse(vec4 color, float roughness, out vec3 albedo,
+        out float specular_out, out float roughness_out) {
+    albedo = color.rgb;
+    specular_out = 0.5;
+    roughness_out = 1.0;
+}
+""",
+        input_sockets=[
+            "Color",
+            "Roughness",
+        ],
+        output_sockets=[
+            "albedo",
+            "specular",
+            "roughness",
+        ]
+    ),
+
+    BsdfShaderFunction(
+        code="""
+void node_bsdf_glossy(vec4 color, float roughness, out vec3 albedo,
+        out float metallic_out, out float roughness_out){
+    albedo = color.rgb;
+    roughness_out = sqrt(roughness);
+    metallic_out = 1.0;
+}
+""",
+        input_sockets=[
+            "Color",
+            "Roughness",
+        ],
+        output_sockets=[
+            "albedo",
+            "metallic",
+            "roughness",
+        ]
+    ),
+
+    BsdfShaderFunction(
+        code="""
+void node_bsdf_transparent(vec4 color, out float alpha) {
+    alpha = 0.0;
+}
+""",
+        input_sockets=['Color'],
+        output_sockets=['alpha'],
+    ),
+
+    BsdfShaderFunction(
+        code="""
+void node_bsdf_glass(vec4 color, float roughness, float IOR, out vec3 albedo,
+        out float alpha, out float specular_out, out float roughness_out,
+        out float transmission_out, out float ior) {
+    albedo = color.rgb;
+    alpha = 0.0;
+    specular_out = pow((IOR - 1.0)/(IOR + 1.0), 2)/0.08;
+    roughness_out = roughness;
+    transmission_out = 0.0;
+    ior = IOR;
+}
+""",
+        input_sockets=[
+            "Color",
+            "Roughness",
+            "IOR",
+        ],
+        output_sockets=[
+            "albedo",
+            "alpha",
+            "specular",
+            "roughness",
+            "transmission",
+            "ior",
+        ]
+    ),
+
+    # trivial converter node functions
+    ShaderFunction(code="""
+void node_rgb_to_bw(vec4 color, out float result){
+    result = color.r * 0.2126 + color.g * 0.7152 + color.b * 0.0722;
+}
+"""),
+
+    ShaderFunction(code="""
+void node_separate_xyz(vec3 in_vec, out float x, out float y, out float z) {
+    x = in_vec.x;
+    y = in_vec.y;
+    z = in_vec.z;
+}
+"""),
+
+    ShaderFunction(code="""
+void node_separate_rgb(vec4 color, out float r, out float g, out float b) {
+    r = color.r;
+    g = color.g;
+    b = color.b;
+}
+"""),
+
+    ShaderFunction(code="""
+void node_combine_rgb(float r, float g, float b, out vec4 color) {
+    color = vec4(r, g, b, 1.0);
+}
+"""),
+
+    ShaderFunction(code="""
+void node_bump(float strength, float dist, float height, vec3 normal,
+               vec3 surf_pos, float invert, out vec3 out_normal){
+    if (invert != 0.0) {
+        dist *= -1.0;
+    }
+    vec3 dPdx = dFdx(surf_pos);
+    vec3 dPdy = dFdy(surf_pos);
+
+    /* Get surface tangents from normal. */
+    vec3 Rx = cross(dPdy, normal);
+    vec3 Ry = cross(normal, dPdx);
+
+    /* Compute surface gradient and determinant. */
+    float det = dot(dPdx, Rx);
+    float absdet = abs(det);
+
+    float dHdx = dFdx(height);
+    float dHdy = dFdy(height);
+    vec3 surfgrad = dHdx * Rx + dHdy * Ry;
+
+    strength = max(strength, 0.0);
+
+    out_normal = normalize(absdet * normal - dist * sign(det) * surfgrad);
+    out_normal = normalize(strength * out_normal + (1.0 - strength) * normal);
+}
+"""),
+
+    ShaderFunction(code="""
+void node_normal_map_tangent(float strength, vec4 color, vec3 normal,
+        vec3 tangent, vec3 binormal, out vec3 out_normal) {
+    vec3 signed_color = 2.0 * (color.xyz - vec3(0.5, 0.5, 0.5));
+    out_normal = signed_color.x * tangent +
+                 signed_color.y * binormal +
+                 signed_color.z * normal;
+    // XXX: TBN are no longer orthogonal, does it bring some problem?
+    out_normal = strength * out_normal + (1.0 - strength) * normal;
+}
+"""),
+
+    ShaderFunction(code="""
+void node_normal_map_world(float strength, vec4 color, vec3 view_normal,
+        mat4 inv_view_mat, out vec3 out_normal) {
+    vec3 signed_color = 2.0 * (color.xyz - vec3(0.5, 0.5, 0.5));
+    vec3 world_normal = (inv_view_mat * vec4(view_normal, 0.0)).xyz;
+    out_normal = strength * signed_color + (1.0 - strength) * world_normal;
+}
+"""),
+
+    ShaderFunction(code="""
+void node_normal_map_object(float strength, vec4 color, vec3 view_normal,
+        mat4 inv_view_mat, mat4 model_mat, out vec3 out_normal) {
+    vec3 signed_color = 2.0 * (color.xyz - vec3(0.5, 0.5, 0.5));
+    vec3 world_normal = (inv_view_mat * vec4(view_normal, 0.0)).xyz;
+    out_normal = (model_mat * vec4(signed_color, 0.0)).xyz;
+    out_normal = strength * signed_color + (1.0 - strength) * world_normal;
+}
+"""),
+
+    ShaderFunction(code="""
+void node_tex_image(vec3 co, sampler2D ima, out vec4 color, out float alpha) {
+    color = texture(ima, co.xy);
+    alpha = color.a;
+}
+"""),
+
+    ShaderFunction(code="""
+void node_mapping(vec3 vec, mat4 mat, vec3 minvec, vec3 maxvec, float domin,
+        float domax, out vec3 outvec) {
+    outvec = (mat * vec4(vec, 1.0)).xyz;
+    if (domin == 1.0) {
+        outvec = max(outvec, minvec);
+    }
+    if (domax == 1.0) {
+        outvec = min(outvec, maxvec);
+    }
+}
+"""),
+
+    ShaderFunction(code="""
+void node_math_no_clamp(float value1, float value2, out float result) {
+    result = value1 +  value2;
+}
+"""),
+
+    ShaderFunction(code="""
+void node_math_subtract_no_clamp(float value1, float value2,
+        out float result) {
+    result = value1 - value2;
+}
+"""),
+
+    ShaderFunction(code="""
+void node_math_multiply_no_clamp(float value1, float value2,
+        out float result) {
+    result = value1 * value2;
+}
+"""),
+
+    ShaderFunction(code="""
+void node_math_divide_no_clamp(float value1, float value2, out float result) {
+    if (value2 == 0.0)
+        result = 0.0;
+    else
+        result = value1 / value2;
+}
+"""),
+
+    ShaderFunction(code="""
+void node_math_power_clamp(float val1, float val2, out float outval) {
+    pow(val1, val2);
+}
+"""),
+
+    ShaderFunction(code="""
+void node_math_logarithm_lamp(float val1, float val2, out float outval) {
+    if (val1 > 0.0  && val2 > 0.0)
+        outval = log2(val1) / log2(val2);
+    else
+        outval = 0.0;
+}
+"""),
+
+    ShaderFunction(code="""
+void node_math_sine_no_clamp(float value, out float result) {
+    result = sin(value);
+}
+"""),
+
+    ShaderFunction(code="""
+void node_math_cosine_no_clamp(float value, out float result) {
+    result = cos(value);
+}
+"""),
+
+    ShaderFunction(code="""
+void node_math_tangent_no_clamp(float value, out float result) {
+    result = tan(value);
+}
+"""),
+
+    ShaderFunction(code="""
+void node_math_arc_sine_no_clamp(float value, out float result) {
+    if (value < 0.0 || value > 1.0)
+        result = 0.0;
+    else
+        result = asin(value);
+}
+"""),
+
+    ShaderFunction(code="""
+void node_math_arccosine_no_clamp(float value, out float result) {
+    if (value < 0.0 || value > 1.0)
+        result = 0.0;
+    else
+        result = acos(value);
+}
+"""),
+
+    ShaderFunction(code="""
+void node_math_arctangent_no_clamp(float value, out float result) {
+    result = atan(value);
+}
+"""),
+
+    ShaderFunction(code="""
+void node_math_add_clamp(float value1, float value2, out float result) {
+    result = clamp(value1 + value2, 0.0, 1.0);
+}
+"""),
+
+    ShaderFunction(code="""
+void node_math_subtract_clamp(float value1, float value2, out float result) {
+    result = clamp(value1 - value2, 0.0, 1.0);
+}
+"""),
+
+    ShaderFunction(code="""
+void node_math_multiply_clamp(float value1, float value2, out float result) {
+    result = clamp(value1 * value2, 0.0, 1.0);
+}
+"""),
+
+    ShaderFunction(code="""
+void node_math_divide_clamp(float value1, float value2, out float result) {
+    if (value2 == 0.0)
+        result = 0.0;
+    else
+        result = clamp(value1 / value2, 0.0, 1.0);
+}
+"""),
+
+    ShaderFunction(code="""
+void node_math_sine_clamp(float value, out float result) {
+    result = clamp(sin(value), 0.0, 1.0);
+}
+"""),
+
+    ShaderFunction(code="""
+void node_math_cosine_clamp(float value, out float result) {
+    result = clamp(cos(value), 0.0, 1.0);
+}
+"""),
+
+    ShaderFunction(code="""
+void node_math_tangent_clamp(float value, out float result) {
+    result = clamp(tan(value), 0.0, 1.0);
+}
+"""),
+
+    ShaderFunction(code="""
+void node_math_arcsine_clamp(float value, out float result) {
+    if (value < 0.0 || value > 1.0)
+        result = 0.0;
+    else
+        result = clamp(asin(value), 0.0, 1.0);
+}
+"""),
+
+    ShaderFunction(code="""
+void node_math_arccosine_clamp(float value, out float result) {
+    if (value < 0.0 || value > 1.0)
+        result = 0.0;
+    else
+        result = clamp(acos(value), 0.0, 1.0);
+}
+"""),
+
+    ShaderFunction(code="""
+void node_math_arctangent_clamp(float value, out float result) {
+    result = clamp(atan(value), 0.0, 1.0);
+}
+"""),
+
+    ShaderFunction(code="""
+void node_math_power_clamp(float val1, float val2, out float outval) {
+    outval = clamp(pow(val1, val2), 0.0, 1.0);
+}
+"""),
+
+    ShaderFunction(code="""
+void node_math_logarithm_clamp(float val1, float val2, out float outval){
+    if (val1 > 0.0  && val2 > 0.0)
+        outval = clamp(log2(val1) / log2(val2), 0.0, 1.0);
+    else
+        outval = 0.0;
+}
+"""),
+
+    ShaderFunction(code="""
+void node_vector_math_add(vec3 v1, vec3 v2, out vec3 outvec,
+        out float outval) {
+    outvec = v1 + v2;
+    outval = (abs(outvec[0]) + abs(outvec[1]) + abs(outvec[2])) * 0.333333;
+}
+"""),
+
+    ShaderFunction(code="""
+void node_vector_math_subtract(vec3 v1, vec3 v2, out vec3 outvec,
+        out float outval) {
+    outvec = v1 - v2;
+    outval = (abs(outvec[0]) + abs(outvec[1]) + abs(outvec[2])) * 0.333333;
+}
+"""),
+
+    ShaderFunction(code="""
+void node_vector_math_averate(vec3 v1, vec3 v2, out vec3 outvec,
+        out float outval) {
+    outvec = v1 + v2;
+    outval = length(outvec);
+    outvec = normalize(outvec);
+}
+"""),
+
+    ShaderFunction(code="""
+void node_vector_math_dot_product(vec3 v1, vec3 v2, out vec3 outvec,
+        out float outval) {
+    outvec = vec3(0);
+    outval = dot(v1, v2);
+}
+"""),
+
+    ShaderFunction(code="""
+void node_vector_math_cross_product(vec3 v1, vec3 v2, out vec3 outvec,
+        out float outval) {
+    outvec = cross(v1, v2);
+    outval = length(outvec);
+    outvec /= outval;
+}
+"""),
+
+    ShaderFunction(code="""
+void node_vector_math_normalize(vec3 v, out vec3 outvec, out float outval) {
+  outval = length(v);
+  outvec = normalize(v);
+}
+"""),
+
+    # non-node function:
+    ShaderFunction(code="""
+void refraction_fresnel(vec3 view_dir, vec3 normal, float ior, out float kr) {
+// reference [https://www.scratchapixel.com/lessons/
+// 3d-basic-rendering/introduction-to-shading/reflection-refraction-fresnel]
+    float cosi = clamp(-1.0, 1.0, dot(view_dir, normal));
+    float etai = 1.0, etat = ior;
+    if (cosi > 0.0) {
+        float tmp = etai;
+        etai = etat;
+        etat = tmp;
+    }
+    // Compute sini using Snell's law
+    float sint = etai / etat * sqrt(max(0.0, 1.0 - cosi * cosi));
+    // Total internal reflection
+    if (sint >= 1.0) {
+        kr = 1.0;
+    }
+    else {
+        float cost = sqrt(max(0.0, 1.0 - sint * sint));
+        cosi = abs(cosi);
+        float Rs = ((etat * cosi) - (etai * cost))
+                    / ((etat * cosi) + (etai * cost));
+        float Rp = ((etai * cosi) - (etat * cost))
+                    / ((etai * cosi) + (etat * cost));
+        kr = (Rs * Rs + Rp * Rp) * 0.5;
+    }
+}
+""")
+]
+
+FUNCTION_NAME_MAPPING = {func.name: func for func in FUNCTION_LIBS}
+
+
+CAMEL_TO_SNAKE_FIRST_CAP = re.compile('(.)([A-Z][a-z]+)')
+CAMEL_TO_SNAKE_ALL_CAP = re.compile('([a-z0-9])([A-Z])')
+NODE_BL_IDNAME_PREFIX = 'Shader'
+
+
+def camel_case_to_snake_case(string):
+    """Convert a camel case string to snake case string"""
+    temp = CAMEL_TO_SNAKE_FIRST_CAP.sub(r'\1_\2', string)
+    return CAMEL_TO_SNAKE_ALL_CAP.sub(r'\1_\2', temp).lower()
+
+
+def convert_node_to_function_name(node):
+    """Generate a function name give a blender shader node"""
+    pruned_node_bl_id = node.bl_idname[len(NODE_BL_IDNAME_PREFIX):]
+    function_name_base = camel_case_to_snake_case(pruned_node_bl_id)
+    if node.bl_idname == 'ShaderNodeMath':
+        operation = node.operation.lower()
+        if node.use_clamp:
+            return function_name_base + "_" + operation + "_clamp"
+        return function_name_base + "_" + operation + "_no_clamp"
+
+    if node.bl_idname == 'ShaderNodeVectorMath':
+        operation = node.operation.lower()
+        return function_name_base + "_" + operation
+
+    if node.bl_idname == 'ShaderNodeNormalMap':
+        return function_name_base + "_" + node.space.lower()
+
+    return function_name_base
+
+
+def find_node_function(node):
+    """Given a material node, return its corresponding function"""
+    function_name = convert_node_to_function_name(node)
+    function = FUNCTION_NAME_MAPPING.get(function_name, None)
+    if function is None:
+        raise ValidationError(
+            "Node with type '{}' at '{}' is not supported".format(
+                node.bl_idname, node.name
+            )
+        )
+    return function
+
+
+def find_function_by_name(function_name):
+    """Given identifier of a material node,
+    return its corresponding function"""
+    return FUNCTION_NAME_MAPPING[function_name]

+ 583 - 0
io_scene_godot/converters/material_node_tree/shaders.py

@@ -0,0 +1,583 @@
+"""Class represents fragment shader and vertex shader"""
+import re
+import collections
+import bpy
+import mathutils
+from .shader_functions import find_function_by_name
+from ...structures import Array, ValidationError
+
+
+class Variable:
+    """A variable in material shader scripts"""
+
+    def __init__(self, var_type, var_name):
+        self.type = var_type
+        self.name = var_name
+
+    def __str__(self):
+        """Convert to string"""
+        return self.name
+
+
+class Value:
+    """A constant value in material shader scripts"""
+
+    def __init__(self, type_str, data):
+        self.type = type_str
+        self.data = data
+
+    @classmethod
+    def create_from_blender_value(cls, blender_value):
+        """Creaate a Value() from a blender object"""
+        if isinstance(
+                blender_value, (bpy.types.bpy_prop_array, mathutils.Vector)):
+            tmp = list()
+            for val in blender_value:
+                tmp.append(val)
+
+            return Value("vec{}".format(len(tmp)), tuple(tmp))
+
+        if isinstance(blender_value, mathutils.Matrix):
+            # godot mat is column major order
+            mat = blender_value.transposed()
+            column_vec_list = list()
+            for vec in mat:
+                column_vec_list.append(cls.create_from_blender_value(vec))
+
+            return Value(
+                "mat{}".format(len(column_vec_list)),
+                tuple(column_vec_list)
+            )
+
+        return Value("float", blender_value)
+
+    def __str__(self):
+        """Convert to string"""
+        if self.type.startswith(('vec', 'mat')):
+            return "{}({})".format(
+                self.type,
+                ', '.join([str(x) for x in self.data])
+            )
+        return str(self.data)
+
+
+class FragmentBSDFContainer:
+    """Several attributes altogether represents
+    blender shader output closure"""
+    _ATTRIBUTES_META = collections.OrderedDict([
+        ('albedo', 'vec3'),
+        ('alpha', 'float'),
+        ('sss_strength', 'float'),
+        ('specular', 'float'),
+        ('metallic', 'float'),
+        ('roughness', 'float'),
+        ('clearcoat', 'float'),
+        ('clearcoat_gloss', 'float'),
+        ('anisotropy', 'float'),
+        ('transmission', 'float'),
+        ('ior', 'float'),
+        ('emission', 'vec3'),
+        ('normal', 'vec3'),
+        ('tangent', 'vec3'),
+    ])
+
+    def __init__(self):
+        self._data = collections.OrderedDict()
+
+    def get_attribute(self, attr_name):
+        """Get a property value, if the property is empty return None"""
+        return self._data.get(attr_name, None)
+
+    def set_attribute(self, attr_name, attr_value):
+        """Set a property, note that property value can be
+        either Value() or Variable()"""
+        self._data[attr_name] = attr_value
+
+    @classmethod
+    def attribute_names_iterable(cls):
+        """Return an iteralble of all attribute names"""
+        return cls._ATTRIBUTES_META.keys()
+
+    @classmethod
+    def attribute_type(cls, attr_name):
+        """Return a type a given attribute"""
+        return cls._ATTRIBUTES_META[attr_name]
+
+    @classmethod
+    def default(cls):
+        """Default closure for unconnected socket"""
+        new_closure = cls()
+        new_closure.set_attribute('albedo', Value('vec3', (0.0, 0.0, 0.0)))
+        return new_closure
+
+
+class BaseShader:
+    """Shared methods in vertex shader and fragment shader"""
+    def __init__(self, formated_array, global_ref):
+        # array of code
+        self.code_array = formated_array
+
+        # reference of global scripts,
+        # used to create uniform, add function
+        self.global_ref = global_ref
+
+        # maintain a mapping from all output sockets
+        # to already calculated var
+        self._socket_to_var_map = dict()
+        # use to create unique variable name
+        self._variable_count = 0
+
+    def append_code_line(self, code_pattern, variables=()):
+        """Format a line of code string and append it to codes"""
+        assert code_pattern[-1] == ';'
+        self.code_array.append(
+            code_pattern.format(*tuple([str(x) for x in variables]))
+        )
+
+    def _append_defination_code(self, var_to_define):
+        definition_str = '{} {};'.format(
+            var_to_define.type, str(var_to_define)
+        )
+        self.code_array.append(definition_str)
+
+    def define_variable(self, var_type, var_base_name):
+        """Create a unique variable, and define it in the shader script"""
+        self._variable_count += 1
+        raw_var_name = 'var{}_{}'.format(
+            self._variable_count,
+            var_base_name,
+        )
+        var_name = re.sub(r'\W', '', raw_var_name)
+        new_var = Variable(var_type, var_name)
+        self._append_defination_code(new_var)
+        return new_var
+
+    def define_variable_from_socket(self, node, socket):
+        """Create a unique variable, variable name and type generate
+        from socket, and also define it in the shader script"""
+        if socket.type == 'RGBA':
+            var_type = 'vec4'
+        elif socket.type == 'VECTOR':
+            var_type = 'vec3'
+        elif socket.type == 'VALUE':
+            var_type = 'float'
+        else:
+            raise ValidationError(
+                "socket '{}' at '{}' is incorrectly connected".format(
+                    socket.identifier, node.name
+                )
+            )
+
+        self._variable_count += 1
+        raw_var_name = 'var{}_{}_{}'.format(
+            self._variable_count,
+            node.name,
+            socket.identifier
+        )
+        var_name = re.sub(r'\W', '', raw_var_name)
+        new_var = Variable(var_type, var_name)
+        self._append_defination_code(new_var)
+        return new_var
+
+    def append_assignment_code(self, var_to_write, var_to_read):
+        """Assign a variable or value to another variable"""
+        assignment_str = '{} = {};'.format(
+            str(var_to_write), str(var_to_read)
+        )
+        self.code_array.append(assignment_str)
+
+    def is_socket_cached(self, input_socket):
+        """Return bool indicating whether an input socket
+        has a variable cached"""
+        if input_socket.links[0].from_socket in self._socket_to_var_map:
+            return True
+        return False
+
+    def fetch_variable_from_socket(self, input_socket):
+        """Given a input socket, return the variable assigned to that
+        socket, note that the variable is actually from the output socket
+        at the other side of the link"""
+        socket_link = input_socket.links[0]
+        var_from_link = self._socket_to_var_map[socket_link.from_socket]
+
+        if socket_link.from_socket.type == socket_link.to_socket.type:
+            return var_from_link
+
+        # if the type of two sockets are not matched,
+        # insert an implicit conversion
+        return self._implicit_socket_convert(
+            var_from_link,
+            socket_link.from_socket.type,
+            socket_link.to_socket.type,
+        )
+
+    def assign_variable_to_socket(self, output_socket, variable):
+        """Assign an output socket with a variable for later use"""
+        self._socket_to_var_map[output_socket] = variable
+
+    def _implicit_socket_convert(self, src_variable,
+                                 from_socket_type, to_socket_type):
+        """Implicitly convert variable type between a pair of socket with
+        different type. It is performed when you link two socket with
+        different color in node editor"""
+        if (to_socket_type == 'VALUE' and
+                from_socket_type in ('VECTOR', 'RGBA')):
+            converted_var = self.define_variable(
+                'float', 'auto_insert_RGBtoBW'
+            )
+            if from_socket_type == 'VECTOR':
+                src_variable = Value('vec4', (src_variable, 1.0))
+
+            function = find_function_by_name('node_rgb_to_bw')
+            self.add_function_call(function, [src_variable], [converted_var])
+            return converted_var
+
+        if to_socket_type == 'VECTOR' and from_socket_type == 'VALUE':
+            return Value('vec3', (src_variable,) * 3)
+
+        if to_socket_type == 'RGBA' and from_socket_type == 'VALUE':
+            return Value('vec4', (src_variable,) * 4)
+
+        if to_socket_type == 'RGBA' and from_socket_type == 'VECTOR':
+            converted_var = self.define_variable(
+                'vec4', 'auto_insert_VecToColor'
+            )
+            self.append_code_line(
+                ('{} = vec4(clamp({}, vec3(0.0, 0.0, 0.0),'
+                 'vec3(1.0, 1.0, 1.0)).xyz, 1.0);'),
+                (converted_var, src_variable)
+            )
+            return converted_var
+
+        if to_socket_type == 'VECTOR' and from_socket_type == 'RGBA':
+            converted_var = self.define_variable(
+                'vec3', 'auto_insert_ColorToVec'
+            )
+            self.append_code_line(
+                '{} = {}.xyz;', (converted_var, src_variable)
+            )
+            return converted_var
+
+        raise ValidationError(
+            "Cannot link two sockets with type '{}' and '{}'".format(
+                from_socket_type, to_socket_type
+            )
+        )
+
+    @staticmethod
+    def _function_call_type_check(argument_var_list, param_type_list):
+        assert len(argument_var_list) == len(param_type_list)
+        for index, var in enumerate(argument_var_list):
+            assert var.type == param_type_list[index]
+
+    def add_function_call(self, function, in_arguments, out_arguments):
+        """Call function in shader scripts"""
+        self.global_ref.add_function(function)
+
+        # runtime check to make sure generated scripts is valid
+        self._function_call_type_check(in_arguments, function.in_param_types)
+        self._function_call_type_check(out_arguments, function.out_param_types)
+
+        invoke_str = '{}({}, {});'.format(
+            function.name,
+            ', '.join([str(x) for x in in_arguments]),
+            ', '.join([str(x) for x in out_arguments])
+        )
+        self.code_array.append(invoke_str)
+
+    def zup_to_yup(self, var_to_convert):
+        """Convert a vec3 from z-up space to y-up space"""
+        assert var_to_convert.type == 'vec3'
+        self.append_code_line(
+            '{} = mat3(vec3(1, 0, 0), vec3(0, 0, -1), vec3(0, 1, 0)) * {};',
+            (var_to_convert, var_to_convert)
+        )
+
+    def yup_to_zup(self, var_to_convert):
+        """Convert a vec3 from y-up space to z-up space"""
+        assert var_to_convert.type == 'vec3'
+        self.append_code_line(
+            '{} = mat3(vec3(1, 0, 0), vec3(0, 0, 1), vec3(0, -1, 0)) * {};',
+            (var_to_convert, var_to_convert)
+        )
+
+    def to_string(self):
+        """Serialze"""
+        return self.code_array.to_string()
+
+
+class FragmentShader(BaseShader):
+    """Fragment shader Script"""
+    def __init__(self, global_ref):
+        super().__init__(
+            Array(
+                prefix='\nvoid fragment() {\n\t',
+                seperator='\n\t',
+                suffix='\n}\n'
+            ),
+            global_ref
+        )
+
+        # flag would be set when glass_bsdf is used
+        self.glass_effect = False
+
+        self._invert_view_mat = None
+        self._invert_model_mat = None
+
+    @property
+    def invert_view_mat(self):
+        """Return inverted view matrix"""
+        if self._invert_view_mat is None:
+            self._invert_view_mat = self.define_variable(
+                'mat4', 'inverted_view_matrix'
+            )
+            self.append_code_line(
+                '{} = inverse({});',
+                (self._invert_view_mat, Variable('mat4', 'INV_CAMERA_MATRIX'))
+            )
+        return self._invert_view_mat
+
+    @property
+    def invert_model_mat(self):
+        """Return inverted model matrix"""
+        if self._invert_model_mat is None:
+            self._invert_model_mat = self.define_variable(
+                'mat4', 'inverted_model_matrix'
+            )
+            self.append_code_line(
+                '{} = inverse({});',
+                (self._invert_model_mat, Variable('mat4', 'WORLD_MATRIX'))
+            )
+        return self._invert_model_mat
+
+    def add_bsdf_surface(self, bsdf_output):
+        """Link bsdf output to godot fragment builtin out qualifiers"""
+        for name in ('albedo', 'sss_strength', 'specular', 'metallic',
+                     'roughness', 'clearcoat', 'clearcoat_gloss', 'emission',
+                     'normal'):
+            var = bsdf_output.get_attribute(name)
+            if var is not None:
+                self.code_array.append(
+                    '{} = {};'.format(name.upper(), str(var))
+                )
+        # xxx: transmission for thick object is not supported in godot
+        # transmission_var = bsdf_output.get_attribute('transmission')
+        # if transmission_var is not None:
+        #     self.append_code_line(
+        #         'TRANSMISSION = vec3(1.0, 1.0, 1.0) * {};'
+        #         (transmission_var,)
+        #     )
+
+        tangent = bsdf_output.get_attribute('tangent')
+        anisotropy = bsdf_output.get_attribute('anisotropy')
+        if tangent is not None and anisotropy is not None:
+            self.append_code_line('ANISOTROPY = {};', (anisotropy,))
+            self.append_code_line(
+                'TANGENT = normalize(cross(cross({}, NORMAL), NORMAL));',
+                (tangent,),
+            )
+            self.append_code_line('BINORMAL = cross(TANGENT, NORMAL);')
+
+        alpha = bsdf_output.get_attribute('alpha')
+        if alpha is not None:
+            refraction_offset = self.global_ref.define_uniform(
+                'float', 'refraction_offset'
+            )
+            if self.glass_effect:
+                fresnel_func = find_function_by_name('refraction_fresnel')
+                in_arguments = list()
+                in_arguments.append(Variable('vec3', 'VERTEX'))
+                in_arguments.append(Variable('vec3', 'NORMAL'))
+                in_arguments.append(bsdf_output.get_attribute('ior'))
+                self.add_function_call(
+                    fresnel_func, in_arguments, [alpha]
+                )
+            self.append_code_line(
+                'EMISSION += textureLod(SCREEN_TEXTURE, SCREEN_UV - '
+                'NORMAL.xy * {}, ROUGHNESS).rgb * (1.0 - {});',
+                (refraction_offset, alpha)
+            )
+            self.append_code_line(
+                'ALBEDO *= {};',
+                (alpha,),
+            )
+            self.append_code_line(
+                'ALPHA = 1.0;'
+            )
+
+    def add_bump_displacement(self, displacement_output):
+        """Add bump displacement to fragment shader"""
+        # xxx: use tangent space if uv exists?
+        function = find_function_by_name('node_bump')
+
+        in_arguments = list()
+        # default bump parameters
+        in_arguments.append(Value('float', 1.0))
+        in_arguments.append(Value('float', 0.1))
+        in_arguments.append(displacement_output)
+        in_arguments.append(Variable('vec3', 'NORMAL'))
+        in_arguments.append(Variable('vec3', 'VERTEX'))
+        in_arguments.append(Value('float', 0.0))
+
+        out = Variable('vec3', 'NORMAL')
+        self.add_function_call(function, in_arguments, [out])
+
+    def view_to_model(self, var_to_convert, is_direction=True):
+        """Convert a vec3 from view space to model space,
+        note that conversion is done in y-up space"""
+        assert var_to_convert.type == 'vec3'
+        if is_direction:
+            self.append_code_line(
+                '{} = normalize({} * ({} * vec4({}, 0.0))).xyz;',
+                (var_to_convert, self.invert_model_mat,
+                 self.invert_view_mat, var_to_convert)
+            )
+        else:
+            self.append_code_line(
+                '{} = ({} * ({} * vec4({}, 1.0))).xyz;',
+                (var_to_convert, self.invert_model_mat,
+                 self.invert_view_mat, var_to_convert)
+            )
+
+    def model_to_view(self, var_to_convert, is_direction=True):
+        """Convert a vec3 from model space to view space,
+        note that conversion is done in y-up space"""
+        assert var_to_convert.type == 'vec3'
+        view_mat = Variable('mat4', 'INV_CAMERA_MATRIX')
+        model_mat = Variable('mat4', 'WORLD_MATRIX')
+        if is_direction:
+            self.append_code_line(
+                '{} = normalize({} * ({} * vec4({}, 0.0))).xyz;',
+                (var_to_convert, view_mat, model_mat, var_to_convert)
+            )
+        else:
+            self.append_code_line(
+                '{} = ({} * ({} * vec4({}, 1.0))).xyz;',
+                (var_to_convert, view_mat, model_mat, var_to_convert)
+            )
+
+    def view_to_world(self, var_to_convert, is_direction=True):
+        """Convert a vec3 from view space to world space,
+        note that it is done in y-up space"""
+        assert var_to_convert.type == 'vec3'
+        if is_direction:
+            self.append_code_line(
+                '{} = normalize({} * vec4({}, 0.0)).xyz;',
+                (var_to_convert, self.invert_view_mat, var_to_convert)
+            )
+        else:
+            self.append_code_line(
+                '{} = ({} * vec4({}, 1.0)).xyz;',
+                (var_to_convert, self.invert_view_mat, var_to_convert)
+            )
+
+    def world_to_view(self, var_to_convert, is_direction=True):
+        """Convert a vec3 from world space to view space,
+        note that it is done in y-up space"""
+        assert var_to_convert.type == 'vec3'
+        view_mat = Variable('mat4', 'INV_CAMERA_MATRIX')
+        if is_direction:
+            self.append_code_line(
+                '{} = normalize({} * vec4({}, 0.0)).xyz;',
+                (var_to_convert, view_mat, var_to_convert)
+            )
+        else:
+            self.append_code_line(
+                '{} = ({} * vec4({}, 1.0)).xyz;',
+                (var_to_convert, view_mat, var_to_convert)
+            )
+
+
+class VertexShader(BaseShader):
+    """Vertex shader scripts"""
+    def __init__(self, global_ref):
+        super().__init__(
+            Array(
+                prefix='\nvoid vertex() {\n\t',
+                seperator='\n\t',
+                suffix='\n}\n'
+            ),
+            global_ref
+        )
+
+
+class ShaderGlobals:
+    """Global space of shader material, maintains uniforms, functions
+    and rendering configures."""
+    def __init__(self):
+        # render mode and render type is also
+        # placed here
+        self.uniform_codes = Array(
+            prefix='',
+            seperator='\n',
+            suffix='\n'
+        )
+
+        # cache function names to avoid duplicated
+        # function code being added
+        self.function_name_set = set()
+        self.function_codes = Array(
+            prefix='',
+            seperator='\n',
+            suffix='\n'
+        )
+
+        self.textures = dict()
+
+        self._render_mode = Array(
+            prefix='render_mode ',
+            seperator=',',
+            suffix=';'
+        )
+
+        self._set_render_mode()
+        self._uniform_var_count = 0
+
+        self.fragment_shader = FragmentShader(self)
+        self.vertex_shader = VertexShader(self)
+
+    def _set_render_mode(self):
+        self._render_mode.extend([
+            'blend_mix',
+            'depth_draw_always',
+            'cull_back',
+            'diffuse_burley',
+            'specular_schlick_ggx',
+        ])
+
+    def add_function(self, function):
+        """Add function body to global codes"""
+        if function.name not in self.function_name_set:
+            self.function_name_set.add(function.name)
+            self.function_codes.append(function.code)
+
+    def define_uniform(self, uni_type, uni_base_name):
+        """Define an uniform variable"""
+        self._uniform_var_count += 1
+        raw_var_name = 'uni{}_{}'.format(
+            self._uniform_var_count,
+            uni_base_name,
+        )
+        var_name = re.sub(r'\W', '', raw_var_name)
+        new_var = Variable(uni_type, var_name)
+        def_str = 'uniform {} {};'.format(uni_type, var_name)
+        self.uniform_codes.append(def_str)
+        return new_var
+
+    def add_image_texture(self, uniform_var, image):
+        """Define a uniform referring to the texture sampler
+        and store the image object"""
+        # store image
+        if image is not None:
+            self.textures[image] = uniform_var
+
+    def to_string(self):
+        """Serialization"""
+        return '\n'.join([
+            "shader_type spatial;",  # shader type is spatial for 3D scene
+            self._render_mode.to_string(),
+            self.uniform_codes.to_string(),
+            self.function_codes.to_string(),
+            self.vertex_shader.to_string(),
+            self.fragment_shader.to_string(),
+        ])

+ 59 - 8
io_scene_godot/converters/mesh.py

@@ -55,6 +55,10 @@ def export_mesh_node(escn_file, export_settings, node, parent_gd_node):
             mesh_node['transform'] = mathutils.Matrix.Identity(4)
         escn_file.add_node(mesh_node)
 
+        export_object_link_material(
+            escn_file, export_settings, node, mesh_node
+        )
+
         # export shape key animation
         if (export_settings['use_export_shape_key'] and
                 node.data.shape_keys is not None):
@@ -90,6 +94,39 @@ def get_modifier_armature_data(mesh_object):
     return None
 
 
+def export_object_link_material(escn_file, export_settings, mesh_object,
+                                gd_node):
+    """Export object linked material, if multiple object link material,
+    only export the first one in the material slots"""
+    mesh_resource_id = escn_file.get_internal_resource(mesh_object.data)
+    mesh_resource = escn_file.internal_resources[mesh_resource_id - 1]
+    for index, slot in enumerate(mesh_object.material_slots):
+        if slot.link == 'OBJECT' and slot.material is not None:
+            surface_id = mesh_resource.get_surface_id(index)
+            if surface_id is not None:
+                gd_node['material/{}'.format(surface_id)] = export_material(
+                    escn_file,
+                    export_settings,
+                    slot.material
+                )
+
+
+class ArrayMeshResource(InternalResource):
+    """Godot ArrayMesh resource, containing surfaces"""
+    def __init__(self):
+        super().__init__('ArrayMesh')
+        self._mat_to_surf_mapping = dict()
+
+    def get_surface_id(self, material_index):
+        """Given blender material index, return the corresponding
+        surface id"""
+        return self._mat_to_surf_mapping.get(material_index, None)
+
+    def set_surface_id(self, material_index, surface_id):
+        """Set a relation between material and surface"""
+        self._mat_to_surf_mapping[material_index] = surface_id
+
+
 class MeshResourceExporter:
     """Export a mesh resource from a blender mesh object"""
     def __init__(self, mesh_object):
@@ -99,7 +136,6 @@ class MeshResourceExporter:
         self.mesh_resource = None
         self.has_tangents = False
         self.vgroup_to_bone_mapping = dict()
-        self.mat_to_surf_mapping = dict()
 
     def init_mesh_bones_data(self, skeleton_node):
         """Find the mapping relation between vertex groups
@@ -118,7 +154,7 @@ class MeshResourceExporter:
         if mesh_id is not None:
             return mesh_id
 
-        self.mesh_resource = InternalResource('ArrayMesh')
+        self.mesh_resource = ArrayMeshResource()
 
         self.make_arrays(
             escn_file,
@@ -147,6 +183,13 @@ class MeshResourceExporter:
                                    True,
                                    "RENDER")
 
+        # if the original mesh has an object link material,
+        # the new created mesh would use it as data link material,
+        # seems a bug of Blender,
+        # here is a simple fix, not sure if it is robust enough..
+        for idx in range(len(mesh.materials)):
+            mesh.materials[idx] = self.object.data.materials[idx]
+
         # Prepare the mesh for export
         triangulate_mesh(mesh)
 
@@ -259,7 +302,9 @@ class MeshResourceExporter:
             surfaces_morph_data = self.intialize_surfaces_morph_data(surfaces)
 
             for face in shape_key_mesh.polygons:
-                surface_index = self.mat_to_surf_mapping[face.material_index]
+                surface_index = self.mesh_resource.get_surface_id(
+                    face.material_index
+                )
 
                 surface = surfaces[surface_index]
                 morph = surfaces_morph_data[surface_index]
@@ -294,9 +339,16 @@ class MeshResourceExporter:
 
             # Find a surface that matches the material, otherwise create a new
             # surface for it
-            if face.material_index not in self.mat_to_surf_mapping:
-                self.mat_to_surf_mapping[face.material_index] = len(surfaces)
+            surface_index = self.mesh_resource.get_surface_id(
+                face.material_index
+            )
+            if surface_index is None:
+                surface_index = len(surfaces)
+                self.mesh_resource.set_surface_id(
+                    face.material_index, surface_index
+                )
                 surface = Surface()
+                surface.id = surface_index
                 surfaces.append(surface)
                 if mesh.materials:
                     mat = mesh.materials[face.material_index]
@@ -307,7 +359,7 @@ class MeshResourceExporter:
                             mat
                         )
 
-            surface = surfaces[self.mat_to_surf_mapping[face.material_index]]
+            surface = surfaces[surface_index]
             vertex_indices = []
 
             for loop_id in range(face.loop_total):
@@ -339,8 +391,7 @@ class MeshResourceExporter:
             self.export_morphs(export_settings, surfaces)
 
         has_bone = True if self.vgroup_to_bone_mapping else False
-        for surface_id, surface in enumerate(surfaces):
-            surface.id = surface_id
+        for surface in surfaces:
             surface.vertex_data.has_bone = has_bone
             for vert_array in surface.morph_arrays:
                 vert_array.has_bone = has_bone

+ 4 - 5
tests/godot_project/default_env.tres

@@ -3,12 +3,12 @@
 [sub_resource type="ProceduralSky" id=1]
 
 radiance_size = 4
-sky_top_color = Color( 0.0470588, 0.454902, 0.976471, 1 )
-sky_horizon_color = Color( 0.556863, 0.823529, 0.909804, 1 )
+sky_top_color = Color( 1, 1, 1, 1 )
+sky_horizon_color = Color( 0.765625, 0.765625, 0.765625, 1 )
 sky_curve = 0.25
 sky_energy = 1.0
 ground_bottom_color = Color( 0.101961, 0.145098, 0.188235, 1 )
-ground_horizon_color = Color( 0.482353, 0.788235, 0.952941, 1 )
+ground_horizon_color = Color( 0.904654, 0.931082, 0.945312, 1 )
 ground_curve = 0.01
 ground_energy = 1.0
 sun_color = Color( 1, 1, 1, 1 )
@@ -52,7 +52,7 @@ auto_exposure_scale = 0.4
 auto_exposure_min_luma = 0.05
 auto_exposure_max_luma = 8.0
 auto_exposure_speed = 0.5
-ss_reflections_enabled = false
+ss_reflections_enabled = false 
 ss_reflections_max_steps = 64
 ss_reflections_fade_in = 0.15
 ss_reflections_fade_out = 2.0
@@ -98,5 +98,4 @@ adjustment_enabled = false
 adjustment_brightness = 1.0
 adjustment_contrast = 1.0
 adjustment_saturation = 1.0
-_sections_unfolded = [ "Ambient Light", "Background" ]
 

+ 88 - 0
tests/reference_exports/material/object_link_material.escn

@@ -0,0 +1,88 @@
+[gd_scene load_steps=1 format=2]
+
+[sub_resource id=1 type="SpatialMaterial"]
+
+flags_unshaded = false
+flags_vertex_lighting = false
+flags_transparent = false
+vertex_color_use_as_albedo = false
+albedo_color = Color(0.0394116, 0.058329, 0.8, 1.0)
+subsurf_scatter_enabled = false
+
+[sub_resource id=2 type="SpatialMaterial"]
+
+flags_unshaded = false
+flags_vertex_lighting = false
+flags_transparent = false
+vertex_color_use_as_albedo = false
+albedo_color = Color(0.8, 0.8, 0.8, 1.0)
+subsurf_scatter_enabled = false
+
+[sub_resource id=3 type="ArrayMesh"]
+
+surfaces/0 = {
+	"material":SubResource(1),
+	"primitive":4,
+	"arrays":[
+		Vector3Array(1.0, -1.0, 1.0, -1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -0.999999, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -0.999999, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, -1.0, 1.0, 1.0, -0.999999, 0.999999, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0),
+		Vector3Array(2.98023e-08, -1.0, 0.0, 2.98023e-08, -1.0, 0.0, 2.98023e-08, -1.0, 0.0, 1.0, -2.38419e-07, 0.0, 1.0, -2.38419e-07, 0.0, 1.0, -2.38419e-07, 0.0, 2.68221e-07, 2.38419e-07, -1.0, 2.68221e-07, 2.38419e-07, -1.0, 2.68221e-07, 2.38419e-07, -1.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 1.0, 3.27825e-07, 5.96046e-07, 1.0, 3.27825e-07, 5.96046e-07, 1.0, 3.27825e-07, 5.96046e-07, 2.08616e-07, 8.9407e-08, -1.0, 2.08616e-07, 8.9407e-08, -1.0, 2.08616e-07, 8.9407e-08, -1.0),
+		null, ; No Tangents,
+		null, ; no Vertex Colors,
+		null, ; No UV1,
+		null, ; No UV2,
+		null, ; No Bones,
+		null, ; No Weights,
+		IntArray(0, 2, 1, 3, 5, 4, 6, 8, 7, 9, 11, 10, 12, 14, 13, 15, 17, 16)
+	],
+	"morph_arrays":[]
+}
+surfaces/1 = {
+	"material":SubResource(2),
+	"primitive":4,
+	"arrays":[
+		Vector3Array(-1.0, 1.0, -1.0, 0.999999, 1.0, 1.0, 1.0, 1.0, -0.999999, 0.999999, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 0.999999, 1.0, 1.0, 0.999999, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0),
+		Vector3Array(0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, -8.9407e-08, -4.76837e-07, 1.0, -8.9407e-08, -4.76837e-07, 1.0, -8.9407e-08, -4.76837e-07, 1.0, -1.0, -1.19209e-07, -2.38419e-07, -1.0, -1.19209e-07, -2.38419e-07, -1.0, -1.19209e-07, -2.38419e-07, 5.96047e-08, 1.0, 0.0, 5.96047e-08, 1.0, 0.0, 5.96047e-08, 1.0, 0.0, -4.76837e-07, 1.19209e-07, 1.0, -4.76837e-07, 1.19209e-07, 1.0, -4.76837e-07, 1.19209e-07, 1.0, -1.0, -1.49012e-07, -2.38419e-07, -1.0, -1.49012e-07, -2.38419e-07, -1.0, -1.49012e-07, -2.38419e-07),
+		null, ; No Tangents,
+		null, ; no Vertex Colors,
+		null, ; No UV1,
+		null, ; No UV2,
+		null, ; No Bones,
+		null, ; No Weights,
+		IntArray(0, 2, 1, 3, 5, 4, 6, 8, 7, 9, 11, 10, 12, 14, 13, 15, 17, 16)
+	],
+	"morph_arrays":[]
+}
+
+[sub_resource id=4 type="SpatialMaterial"]
+
+flags_unshaded = false
+flags_vertex_lighting = false
+flags_transparent = false
+vertex_color_use_as_albedo = false
+albedo_color = Color(0.8, 0.0201475, 0.0444397, 1.0)
+subsurf_scatter_enabled = false
+
+[sub_resource id=5 type="SpatialMaterial"]
+
+flags_unshaded = false
+flags_vertex_lighting = false
+flags_transparent = false
+vertex_color_use_as_albedo = false
+albedo_color = Color(0.00971971, 0.8, 0.0132156, 1.0)
+subsurf_scatter_enabled = false
+[node type="Spatial" name="Scene"]
+
+
+[node name="Cube000" type="MeshInstance" parent="."]
+
+mesh = SubResource(3)
+visible = true
+transform = Transform(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.804941, -0.198248, 3.55463)
+
+[node name="Cube001" type="MeshInstance" parent="."]
+
+mesh = SubResource(3)
+visible = true
+transform = Transform(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, -0.0116182, 0.681681, -0.190806)
+material/1 = SubResource(4)
+material/0 = SubResource(5)

BIN
tests/reference_exports/material_cycle/brick_4_bump_1k.jpg.jpeg


BIN
tests/reference_exports/material_cycle/brick_4_diff_1k.jpg


BIN
tests/reference_exports/material_cycle/brick_floor_nor_1k.jpg.jpeg


Plik diff jest za duży
+ 101 - 0
tests/reference_exports/material_cycle/material_anistropy.escn


Plik diff jest za duży
+ 48 - 0
tests/reference_exports/material_cycle/material_cycle.escn


Plik diff jest za duży
+ 74 - 0
tests/reference_exports/material_cycle/material_normal.escn


+ 77 - 0
tests/reference_exports/material_cycle/material_unpack_texture.escn

@@ -0,0 +1,77 @@
+[gd_scene load_steps=1 format=2]
+[ext_resource id=1 path="brick_4_diff_1k.jpg" type="Texture"]
+
+[sub_resource id=1 type="Shader"]
+
+code = "shader_type spatial;
+render_mode blend_mix,depth_draw_always,cull_back,diffuse_burley,specular_schlick_ggx;
+uniform sampler2D uni1_ImageTexturetexture_image;
+
+
+void node_tex_image(vec3 co, sampler2D ima, out vec4 color, out float alpha) {
+    color = texture(ima, co.xy);
+    alpha = color.a;
+}
+
+
+void node_bsdf_diffuse(vec4 color, float roughness, out vec3 albedo,
+        out float specular_out, out float roughness_out) {
+    albedo = color.rgb;
+    specular_out = 0.5;
+    roughness_out = 1.0;
+}
+
+
+
+void vertex() {
+	
+}
+
+
+void fragment() {
+	vec4 var1_ImageTexture_Color;
+	float var2_ImageTexture_Alpha;
+	node_tex_image(vec3(UV, 0.0), uni1_ImageTexturetexture_image, var1_ImageTexture_Color, var2_ImageTexture_Alpha);
+	float var3_DiffuseBSDF_Roughness;
+	var3_DiffuseBSDF_Roughness = 0.0;
+	vec3 var4_DiffuseBSDF_output_albedo;
+	float var5_DiffuseBSDF_output_specular;
+	float var6_DiffuseBSDF_output_roughness;
+	node_bsdf_diffuse(var1_ImageTexture_Color, var3_DiffuseBSDF_Roughness, var4_DiffuseBSDF_output_albedo, var5_DiffuseBSDF_output_specular, var6_DiffuseBSDF_output_roughness);
+	ALBEDO = var4_DiffuseBSDF_output_albedo;
+	SPECULAR = var5_DiffuseBSDF_output_specular;
+	ROUGHNESS = var6_DiffuseBSDF_output_roughness;
+}
+"
+
+[sub_resource id=2 type="ShaderMaterial"]
+
+shader = SubResource(1)
+shader_param/uni1_ImageTexturetexture_image = ExtResource(1)
+
+[sub_resource id=3 type="ArrayMesh"]
+
+surfaces/0 = {
+	"material":SubResource(2),
+	"primitive":4,
+	"arrays":[
+		Vector3Array(3.58062, -1.0, 1.0, -3.03377, -1.0, 0.567279, 3.58062, -1.0, 0.567279, -3.03378, 5.77507, 0.567279, 3.58062, 5.77507, 1.0, 3.58062, 5.77507, 0.56728, 3.58062, 5.77507, 0.56728, 3.58062, -1.0, 1.0, 3.58062, -1.0, 0.567279, 3.58062, 5.77507, 1.0, -3.03378, -1.0, 1.0, 3.58062, -1.0, 1.0, -3.03378, 5.77507, 1.0, -3.03377, -1.0, 0.567279, -3.03378, -1.0, 1.0, 3.58062, -1.0, 0.567279, -3.03378, 5.77507, 0.567279, 3.58062, 5.77507, 0.56728, -3.03378, -1.0, 1.0, -3.03378, 5.77507, 0.567279, -3.03378, 5.77507, 1.0, 3.58062, 5.77507, 1.0, 3.58062, 5.77507, 0.56728, 3.58062, 5.77507, 1.0, 3.58062, -1.0, 1.0, 3.58062, 5.77507, 1.0, -3.03378, 5.77507, 1.0, -3.03378, -1.0, 1.0, -3.03378, 5.77507, 1.0, -3.03378, 5.77507, 0.567279, -3.03377, -1.0, 0.567279, 3.58062, -1.0, 0.567279, -3.03377, -1.0, 0.567279, -3.03378, 5.77507, 0.567279),
+		Vector3Array(0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, -7.03811e-08, 0.0, 1.0, -7.03811e-08, 0.0, 1.0, -7.03811e-08, 0.0, -2.70341e-08, -1.27687e-07, 1.0, -2.70341e-08, -1.27687e-07, 1.0, -2.70341e-08, -1.27687e-07, 1.0, -1.0, -7.03812e-08, -1.30118e-06, -1.0, -7.03812e-08, -1.30118e-06, -1.0, -7.03812e-08, -1.30118e-06, 8.11022e-08, 7.44842e-08, -1.0, 8.11022e-08, 7.44842e-08, -1.0, 8.11022e-08, 7.44842e-08, -1.0, 0.0, -1.0, 0.0, 1.67067e-08, 1.0, 0.0, 1.67067e-08, 1.0, 0.0, 1.67067e-08, 1.0, 0.0, 1.0, 1.05571e-07, 3.90355e-06, 1.0, 1.05571e-07, 3.90355e-06, 1.0, 1.05571e-07, 3.90355e-06, -1.44182e-07, 4.25624e-08, 1.0, -1.44182e-07, 4.25624e-08, 1.0, -1.44182e-07, 4.25624e-08, 1.0, -1.0, -7.03811e-08, -1.30118e-06, -1.0, -7.03811e-08, -1.30118e-06, -1.0, -7.03811e-08, -1.30118e-06, 6.30795e-08, 3.049e-08, -1.0, 6.30795e-08, 3.049e-08, -1.0, 6.30795e-08, 3.049e-08, -1.0),
+		FloatArray(1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, -1.0, 0.0, -8.11022e-08, 1.0, -1.0, 0.0, -8.11022e-08, 1.0, -1.0, 0.0, -8.11022e-08, 1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, -1.0, 0.0, -6.30795e-08, 1.0, -1.0, 0.0, -6.30795e-08, 1.0, -1.0, 0.0, -6.30795e-08, 1.0),
+		null, ; no Vertex Colors,
+		Vector2Array(0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 9.87876e-05, 0.999901, 0.97619, 9.88245e-05, 9.87876e-05, 9.87649e-05, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 9.87876e-05, 0.999901, 0.97619, 0.999901, 0.97619, 9.88245e-05),
+		null, ; No UV2,
+		null, ; No Bones,
+		null, ; No Weights,
+		IntArray(0, 2, 1, 3, 5, 4, 6, 8, 7, 9, 11, 10, 12, 14, 13, 15, 17, 16, 0, 1, 18, 19, 21, 20, 22, 24, 23, 25, 27, 26, 28, 30, 29, 31, 33, 32)
+	],
+	"morph_arrays":[]
+}
+[node type="Spatial" name="Scene"]
+
+
+[node name="Cube" type="MeshInstance" parent="."]
+
+mesh = SubResource(3)
+visible = true
+transform = Transform(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0330188, 0.0, 0.0)

BIN
tests/test_scenes/material/object_link_material.blend


BIN
tests/test_scenes/material_cycle/brick_4_diff_1k.jpg


BIN
tests/test_scenes/material_cycle/material_anistropy.blend


BIN
tests/test_scenes/material_cycle/material_cycle.blend


BIN
tests/test_scenes/material_cycle/material_normal.blend


BIN
tests/test_scenes/material_cycle/material_unpack_texture.blend


Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików