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.image_utils import load_image
from bpy_extras.wm_utils.progress_report import ProgressReport
def line_value(line_split):
Returns 1 string representing the value for this line
None will be returned if there's only 1 word
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).
Bastien Montagne
committed
# 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):
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).
filepath_parts = line.split(b' ')
Bastien Montagne
committed
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)
break;
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
return image
Campbell Barton
committed
def create_materials(filepath, relpath,
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 bpy_extras import node_shader_utils
Campbell Barton
committed
context_material_vars = set()
# 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.
"""
map_options = {}
# 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)
Bastien Montagne
committed
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')
Bastien Montagne
committed
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)
elif type == 'Ka':
# XXX Not supported?
print("WARNING, currently unsupported ambient texture, skipped.")
elif type == 'Ks':
_generic_tex_set(mat_wrap.specular_texture, image, 'UV', map_offset, map_scale)
Bastien Montagne
committed
elif type == 'Ke':
Bastien Montagne
committed
_generic_tex_set(mat_wrap.emission_color_texture, image, 'UV', map_offset, map_scale)
mat_wrap.emission_strength = 1.0
elif type == 'Bump':
bump_mult = map_options.get(b'-bm')
Bastien Montagne
committed
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)
elif type == 'D':
_generic_tex_set(mat_wrap.alpha_texture, image, 'UV', map_offset, map_scale)
Campbell Barton
committed
elif type == 'disp':
# 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)
elif type == 'refl':
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)
Bastien Montagne
committed
def finalize_material(context_material, context_material_vars, spec_colors,
Bastien Montagne
committed
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
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
Bastien Montagne
committed
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
Bastien Montagne
committed
# 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)
del temp_mtl
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):
if not os.path.exists(mtlpath):
print("\tMaterial not found MTL: %r" % mtlpath)
else:
# 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)
context_mat_wrap = None
mtl = open(mtlpath, 'rb')
Campbell Barton
committed
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':
Bastien Montagne
committed
# Finalize previous mat, if any.
Bastien Montagne
committed
finalize_material(context_material, context_material_vars, spec_colors,
Bastien Montagne
committed
do_highlight, do_reflection, do_transparency, do_glass)
context_material_name = line_value(line_split)
context_material = unique_materials.get(context_material_name)
if context_material is not None:
context_mat_wrap = nodal_material_wrap_map[context_material]
Campbell Barton
committed
context_material_vars.clear()
Bastien Montagne
committed
spec_colors[:] = [0.0, 0.0, 0.0]
do_highlight = False
do_reflection = False
do_transparency = False
do_glass = False
elif context_material:
Bastien Montagne
committed
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.
if line_id == b'ka':
Bastien Montagne
committed
refl = sum(_get_colors(line_split)) / 3.0
context_mat_wrap.metallic = refl
context_material_vars.add("metallic")
elif line_id == b'kd':
Bastien Montagne
committed
context_mat_wrap.base_color = _get_colors(line_split)
elif line_id == b'ks':
Bastien Montagne
committed
spec_colors[:] = _get_colors(line_split)
context_material_vars.add("specular")
Bastien Montagne
committed
elif line_id == b'ke':
# We cannot set context_material.emit right now, we need final diffuse color as well for this.
Bastien Montagne
committed
context_mat_wrap.emission_color = _get_colors(line_split)
context_mat_wrap.emission_strength = 1.0
elif line_id == b'ns':
# XXX Totally empirical conversion, trying to adapt it
Bastien Montagne
committed
# (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.")
elif line_id == b'tf':
Campbell Barton
committed
# 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])
Campbell Barton
committed
# 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.")
Campbell Barton
committed
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
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
Campbell Barton
committed
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.")
Campbell Barton
committed
pass
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')
Bastien Montagne
committed
elif line_id == b'map_ke':
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')
Campbell Barton
committed
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')
Campbell Barton
committed
else:
print("WARNING: %r:%r (ignored)" % (filepath, line))
Bastien Montagne
committed
# Finalize last mat, if any.
Bastien Montagne
committed
finalize_material(context_material, context_material_vars, spec_colors,
Bastien Montagne
committed
do_highlight, do_reflection, do_transparency, do_glass)
Loading
Loading full blame...