Skip to content
Snippets Groups Projects
import_obj.py 55.4 KiB
Newer Older
# SPDX-License-Identifier: GPL-2.0-or-later

# Script copyright (C) Campbell Barton
# Contributors: Campbell Barton, Jiri Hnidek, Paolo Ciccone

"""
This script imports a Wavefront OBJ files to Blender.

Usage:
Run this script from "File->Import" menu and then load the desired OBJ file.
Note, This loads mesh objects and materials only, nurbs and curves are not supported.

http://wiki.blender.org/index.php/Scripts/Manual/Import/wavefront_obj
"""

import os
import time
import bpy
import mathutils
from bpy_extras.io_utils import unpack_list
from bpy_extras.image_utils import load_image
from bpy_extras.wm_utils.progress_report import ProgressReport
    Returns 1 string representing the value for this line
    None will be returned if there's only 1 word
Campbell Barton's avatar
Campbell Barton committed
    length = len(line_split)
    if length == 1:
        return None

    elif length == 2:
        return line_split[1]

    elif length > 2:
        return b' '.join(line_split[1:])
def filenames_group_by_ext(line, ext):
    """
    Splits material libraries supporting spaces, so:
    b'foo bar.mtl baz spam.MTL' -> (b'foo bar.mtl', b'baz spam.MTL')
    Also handle " chars (some software use those to protect filenames with spaces, see T67266... sic).
    # Note that we assume that if there are some " in that line,
    # then all filenames are properly enclosed within those...
    start = line.find(b'"') + 1
    if start != 0:
        while start != 0:
            end = line.find(b'"', start)
            if end != -1:
                yield line[start:end]
                start = line.find(b'"', end + 1) + 1
            else:
                break
        return

    line_lower = line.lower()
    i_prev = 0
    while i_prev != -1 and i_prev < len(line):
        i = line_lower.find(ext, i_prev)
        if i != -1:
            i += len(ext)
        yield line[i_prev:i].strip()
        i_prev = i


def obj_image_load(img_data, context_imagepath_map, line, DIR, recursive, relpath):
    Mainly uses comprehensiveImageLoad
    But we try all space-separated items from current line when file is not found with last one
    (users keep generating/using image files with spaces in a format that does not support them, sigh...)
    Also tries to replace '_' with ' ' for Max's exporter replaces spaces with underscores.
    Also handle " chars (some software use those to protect filenames with spaces, see T67266... sic).
    Also corrects img_data (in case filenames with spaces have been split up in multiple entries, see T72148).

    start = line.find(b'"') + 1
    if start != 0:
        end = line.find(b'"', start)
        if end != 0:
            filepath_parts = (line[start:end],)

    image = None
    for i in range(-1, -len(filepath_parts), -1):
        imagepath = os.fsdecode(b" ".join(filepath_parts[i:]))
        image = context_imagepath_map.get(imagepath, ...)
        if image is ...:
            image = load_image(imagepath, DIR, recursive=recursive, relpath=relpath)
            if image is None and "_" in imagepath:
                image = load_image(imagepath.replace("_", " "), DIR, recursive=recursive, relpath=relpath)
            if image is not None:
                context_imagepath_map[imagepath] = image
                del img_data[i:]
                img_data.append(imagepath)
        else:
            del img_data[i:]
            img_data.append(imagepath)
            break;

    if image is None:
        imagepath = os.fsdecode(filepath_parts[-1])
        image = load_image(imagepath, DIR, recursive=recursive, place_holder=True, relpath=relpath)
        context_imagepath_map[imagepath] = image
                     material_libs, unique_materials,
                     use_image_search, float_func):
    Create all the used materials in this obj,
    assign colors and images to the materials from all referenced material libs
    from math import sqrt
    from bpy_extras import node_shader_utils
Campbell Barton's avatar
Campbell Barton committed
    DIR = os.path.dirname(filepath)
    # Don't load the same image multiple times
    context_imagepath_map = {}

    nodal_material_wrap_map = {}
    def load_material_image(blender_material, mat_wrap, context_material_name, img_data, line, type):
        """
        Set textures defined in .mtl file.
        """
        # Absolute path - c:\.. etc would work here
        image = obj_image_load(img_data, context_imagepath_map, line, DIR, use_image_search, relpath)

        curr_token = []
        for token in img_data[:-1]:
            if token.startswith(b'-') and token[1:].isalpha():
                if curr_token:
                    map_options[curr_token[0]] = curr_token[1:]
                curr_token[:] = []
            curr_token.append(token)
        if curr_token:
            map_options[curr_token[0]] = curr_token[1:]
        map_offset = map_options.get(b'-o')
        map_scale = map_options.get(b'-s')
        if map_offset is not None:
            map_offset = tuple(map(float_func, map_offset))
        if map_scale is not None:
            map_scale = tuple(map(float_func, map_scale))
        def _generic_tex_set(nodetex, image, texcoords, translation, scale):
            nodetex.image = image
            nodetex.texcoords = texcoords
            if translation is not None:
                nodetex.translation = translation
            if scale is not None:
                nodetex.scale = scale

        # Adds textures for materials (rendering)
        if type == 'Kd':
            _generic_tex_set(mat_wrap.base_color_texture, image, 'UV', map_offset, map_scale)
            # XXX Not supported?
            print("WARNING, currently unsupported ambient texture, skipped.")
            _generic_tex_set(mat_wrap.specular_texture, image, 'UV', map_offset, map_scale)
            _generic_tex_set(mat_wrap.emission_color_texture, image, 'UV', map_offset, map_scale)
            bump_mult = map_options.get(b'-bm')
            bump_mult = float(bump_mult[0]) if (bump_mult and len(bump_mult[0]) > 1) else 1.0
            mat_wrap.normalmap_strength_set(bump_mult)
            _generic_tex_set(mat_wrap.normalmap_texture, image, 'UV', map_offset, map_scale)
            _generic_tex_set(mat_wrap.alpha_texture, image, 'UV', map_offset, map_scale)
            # XXX Not supported?
            print("WARNING, currently unsupported displacement texture, skipped.")
            # ~ mat_wrap.bump_image_set(image)
            # ~ mat_wrap.bump_mapping_set(coords='UV', translation=map_offset, scale=map_scale)
            map_type = map_options.get(b'-type')
            if map_type and map_type != [b'sphere']:
                print("WARNING, unsupported reflection type '%s', defaulting to 'sphere'"
                      "" % ' '.join(i.decode() for i in map_type))

            _generic_tex_set(mat_wrap.base_color_texture, image, 'Reflection', map_offset, map_scale)
            mat_wrap.base_color_texture.projection = 'SPHERE'
            raise Exception("invalid type %r" % type)
    def finalize_material(context_material, context_material_vars, spec_colors,
                          do_highlight, do_reflection, do_transparency, do_glass):
        # Finalize previous mat, if any.
        if context_material:
            if "specular" in context_material_vars:
                # XXX This is highly approximated, not sure whether we can do better...
                # TODO: Find a way to guesstimate best value from diffuse color...
                # IDEA: Use standard deviation of both spec and diff colors (i.e. how far away they are
                #       from some grey), and apply the the proportion between those two as tint factor?
                spec = sum(spec_colors) / 3.0
                # ~ spec_var = math.sqrt(sum((c - spec) ** 2 for c in spec_color) / 3.0)
                # ~ diff = sum(context_mat_wrap.base_color) / 3.0
                # ~ diff_var = math.sqrt(sum((c - diff) ** 2 for c in context_mat_wrap.base_color) / 3.0)
                # ~ tint = min(1.0, spec_var / diff_var)
                context_mat_wrap.specular = spec
                context_mat_wrap.specular_tint = 0.0
                if "roughness" not in context_material_vars:
                    context_mat_wrap.roughness = 0.0

            # FIXME, how else to use this?
            if do_highlight:
                if "specular" not in context_material_vars:
                    context_mat_wrap.specular = 1.0
                if "roughness" not in context_material_vars:
                    context_mat_wrap.roughness = 0.0
            else:
                if "specular" not in context_material_vars:
                    context_mat_wrap.specular = 0.0
                if "roughness" not in context_material_vars:
                    context_mat_wrap.roughness = 1.0

            if do_reflection:
                if "metallic" not in context_material_vars:
                    context_mat_wrap.metallic = 1.0
            else:
                # since we are (ab)using ambient term for metallic (which can be non-zero)
                context_mat_wrap.metallic = 0.0

            if do_transparency:
                if "ior" not in context_material_vars:
                    context_mat_wrap.ior = 1.0
                if "alpha" not in context_material_vars:
                    context_mat_wrap.alpha = 1.0
                # EEVEE only
                context_material.blend_method = 'BLEND'

            if do_glass:
                if "ior" not in context_material_vars:
                    context_mat_wrap.ior = 1.5

    # Try to find a MTL with the same name as the OBJ if no MTLs are specified.
    temp_mtl = os.path.splitext((os.path.basename(filepath)))[0] + ".mtl"
    if os.path.exists(os.path.join(DIR, temp_mtl)):
        material_libs.add(temp_mtl)
    # Create new materials
Campbell Barton's avatar
Campbell Barton committed
    for name in unique_materials:  # .keys()
        ma_name = "Default OBJ" if name is None else name.decode('utf-8', "replace")
        ma = unique_materials[name] = bpy.data.materials.new(ma_name)
        ma_wrap = node_shader_utils.PrincipledBSDFWrapper(ma, is_readonly=False)
        nodal_material_wrap_map[ma] = ma_wrap
        ma_wrap.use_nodes = True
    for libname in sorted(material_libs):
Campbell Barton's avatar
Campbell Barton committed
        mtlpath = os.path.join(DIR, libname)
            print("\tMaterial not found MTL: %r" % mtlpath)
            # Note: with modern Principled BSDF shader, things like ambient, raytrace or fresnel are always 'ON'
            # (i.e. automatically controlled by other parameters).
            do_highlight = False
            do_reflection = False
            do_transparency = False
            do_glass = False
            spec_colors = [0.0, 0.0, 0.0]
            # print('\t\tloading mtl: %e' % mtlpath)
Campbell Barton's avatar
Campbell Barton committed
            context_material = None
            context_mat_wrap = None
            mtl = open(mtlpath, 'rb')
Campbell Barton's avatar
Campbell Barton committed
            for line in mtl:  # .readlines():
                line = line.strip()
                if not line or line.startswith(b'#'):
                    continue

                line_split = line.split()
                line_id = line_split[0].lower()

                if line_id == b'newmtl':
                    finalize_material(context_material, context_material_vars, spec_colors,
                                      do_highlight, do_reflection, do_transparency, do_glass)
                    context_material_name = line_value(line_split)
Campbell Barton's avatar
Campbell Barton committed
                    context_material = unique_materials.get(context_material_name)
                    if context_material is not None:
                        context_mat_wrap = nodal_material_wrap_map[context_material]
                    do_highlight = False
                    do_reflection = False
                    do_transparency = False
                    do_glass = False


                    def _get_colors(line_split):
                        # OBJ 'allows' one or two components values, treat single component as greyscale, and two as blue = 0.0.
                        ln = len(line_split)
                        if ln == 2:
                            return [float_func(line_split[1])] * 3
                        elif ln == 3:
                            return [float_func(line_split[1]), float_func(line_split[2]), 0.0]
                        else:
                            return [float_func(line_split[1]), float_func(line_split[2]), float_func(line_split[3])]

                    # we need to make a material to assign properties to it.
                        refl =  sum(_get_colors(line_split)) / 3.0
                        context_mat_wrap.metallic = refl
                        context_material_vars.add("metallic")
                        context_mat_wrap.base_color = _get_colors(line_split)
                        context_material_vars.add("specular")
                    elif line_id == b'ke':
                        # We cannot set context_material.emit right now, we need final diffuse color as well for this.
                        # XXX Unsupported currently
                        context_mat_wrap.emission_color = _get_colors(line_split)
                        context_mat_wrap.emission_strength = 1.0
                        # XXX Totally empirical conversion, trying to adapt it
                        #     (from 0.0 - 1000.0 OBJ specular exponent range to 1.0 - 0.0 Principled BSDF range)...
                        val = max(0.0, min(1000.0, float_func(line_split[1])))
                        context_mat_wrap.roughness = 1.0 - (sqrt(val / 1000))
                        context_material_vars.add("roughness")
                    elif line_id == b'ni':  # Refraction index (between 0.001 and 10).
                        context_mat_wrap.ior = float_func(line_split[1])
                        context_material_vars.add("ior")
                    elif line_id == b'd':  # dissolve (transparency)
                        context_mat_wrap.alpha = float_func(line_split[1])
                        context_material_vars.add("alpha")
                    elif line_id == b'tr':  # translucency
                        print("WARNING, currently unsupported 'tr' translucency option, skipped.")
                        # rgb, filter color, blender has no support for this.
                        print("WARNING, currently unsupported 'tf' filter color option, skipped.")
                    elif line_id == b'illum':
                        # Some MTL files incorrectly use a float for this value, see T60135.
                        illum = any_number_as_int(line_split[1])

                        # inline comments are from the spec, v4.2
                        if illum == 0:
                            # Color on and Ambient off
                            print("WARNING, Principled BSDF shader does not support illumination 0 mode "
                                  "(colors with no ambient), skipped.")
                        elif illum == 1:
                            # Color on and Ambient on
                            pass
                        elif illum == 2:
                            # Highlight on
                            do_highlight = True
                        elif illum == 3:
                            # Reflection on and Ray trace on
                            do_reflection = True
                        elif illum == 4:
                            # Transparency: Glass on
                            # Reflection: Ray trace on
                            do_transparency = True
                            do_reflection = True
                            do_glass = True
                        elif illum == 5:
                            # Reflection: Fresnel on and Ray trace on
                            do_reflection = True
                        elif illum == 6:
                            # Transparency: Refraction on
                            # Reflection: Fresnel off and Ray trace on
                            do_transparency = True
                            do_reflection = True
                        elif illum == 7:
                            # Transparency: Refraction on
                            # Reflection: Fresnel on and Ray trace on
                            do_transparency = True
                            do_reflection = True
                        elif illum == 8:
                            # Reflection on and Ray trace off
                            do_reflection = True
                        elif illum == 9:
                            # Transparency: Glass on
                            # Reflection: Ray trace off
                            do_transparency = True
                            do_reflection = False
                            do_glass = True
                        elif illum == 10:
                            # Casts shadows onto invisible surfaces
                            print("WARNING, Principled BSDF shader does not support illumination 10 mode "
                                  "(cast shadows on invisible surfaces), skipped.")
                    elif line_id == b'map_ka':
                        img_data = line.split()[1:]
                        if img_data:
                            load_material_image(context_material, context_mat_wrap,
                                                context_material_name, img_data, line, 'Ka')
                    elif line_id == b'map_ks':
                        img_data = line.split()[1:]
                        if img_data:
                            load_material_image(context_material, context_mat_wrap,
                                                context_material_name, img_data, line, 'Ks')
                    elif line_id == b'map_kd':
                        img_data = line.split()[1:]
                        if img_data:
                            load_material_image(context_material, context_mat_wrap,
                                                context_material_name, img_data, line, 'Kd')
                        img_data = line.split()[1:]
                        if img_data:
                            load_material_image(context_material, context_mat_wrap,
                                                context_material_name, img_data, line, 'Ke')
                    elif line_id in {b'map_bump', b'bump'}:  # 'bump' is incorrect but some files use it.
                        img_data = line.split()[1:]
                        if img_data:
                            load_material_image(context_material, context_mat_wrap,
                                                context_material_name, img_data, line, 'Bump')
                    elif line_id in {b'map_d', b'map_tr'}:  # Alpha map - Dissolve
                        img_data = line.split()[1:]
                        if img_data:
                            load_material_image(context_material, context_mat_wrap,
                                                context_material_name, img_data, line, 'D')
                    elif line_id in {b'map_disp', b'disp'}:  # displacementmap
                        img_data = line.split()[1:]
                        if img_data:
                            load_material_image(context_material, context_mat_wrap,
                                                context_material_name, img_data, line, 'disp')
                    elif line_id in {b'map_refl', b'refl'}:  # reflectionmap
                        img_data = line.split()[1:]
                        if img_data:
                            load_material_image(context_material, context_mat_wrap,
                                                context_material_name, img_data, line, 'refl')
                        print("WARNING: %r:%r (ignored)" % (filepath, line))
            finalize_material(context_material, context_material_vars, spec_colors,
                              do_highlight, do_reflection, do_transparency, do_glass)
Loading
Loading full blame...