944 lines
35 KiB
Python
944 lines
35 KiB
Python
# #### BEGIN GPL LICENSE BLOCK #####
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; either version 2
|
|
# of the License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software Foundation,
|
|
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
#
|
|
# ##### END GPL LICENSE BLOCK #####
|
|
|
|
from typing import Dict, List, Tuple
|
|
import os
|
|
|
|
import bpy
|
|
from bpy.types import Operator
|
|
from bpy.props import (
|
|
BoolProperty,
|
|
EnumProperty,
|
|
IntProperty,
|
|
StringProperty,
|
|
)
|
|
|
|
from ..modules.poliigon_core.assets import (
|
|
AssetData,
|
|
LODS,
|
|
MAP_EXT_LOWER,
|
|
ModelType,
|
|
VARIANTS)
|
|
from ..modules.poliigon_core.multilingual import _t
|
|
from ..material_import_utils import replace_tex_size
|
|
from .utils_operator import fill_size_drop_down
|
|
from ..toolbox import get_context
|
|
from ..utils import (
|
|
construct_model_name,
|
|
f_Ex,
|
|
f_FExt,
|
|
f_FName)
|
|
from .. import reporting
|
|
|
|
|
|
LOD_DESCS = {
|
|
"NONE": _t("med. poly"),
|
|
"LOD0": _t("high poly"),
|
|
"LOD1": _t("med. poly"),
|
|
"LOD2": _t("low poly"),
|
|
"LOD3": _t("lower poly"),
|
|
"LOD4": _t("min. poly"),
|
|
}
|
|
LOD_NAME = "{0} ({1})"
|
|
LOD_DESCRIPTION_FBX = _t("Import the {0} level of detail (LOD) FBX file")
|
|
|
|
PROP_LIBRARY_LINKED = "poliigon_linked"
|
|
|
|
|
|
class POLIIGON_OT_model(Operator):
|
|
bl_idname = "poliigon.poliigon_model"
|
|
bl_label = _t("Import model")
|
|
bl_description = _t("Import Model")
|
|
bl_options = {"REGISTER", "INTERNAL", "UNDO"}
|
|
|
|
def _fill_size_drop_down(self, context):
|
|
return fill_size_drop_down(cTB, self.asset_id)
|
|
|
|
def _fill_lod_drop_down(self, context):
|
|
# Get list of locally available sizes
|
|
local_lods = []
|
|
|
|
local_lods = cTB._asset_index.check_asset_local_lods(
|
|
self.asset_id, ModelType.FBX)
|
|
|
|
items_lod = [
|
|
("NONE",
|
|
LOD_NAME.format("NONE", LOD_DESCS["NONE"]),
|
|
_t("Import the med. poly level of detail (LOD) .blend file"))
|
|
]
|
|
for _lod, is_local in local_lods.items():
|
|
if not is_local:
|
|
continue
|
|
# Tuple: (id, name, description[, icon, [enum value]])
|
|
lod_tuple = (_lod,
|
|
LOD_NAME.format(_lod, LOD_DESCS[_lod]),
|
|
LOD_DESCRIPTION_FBX.format(LOD_DESCS[_lod]))
|
|
# Note: Usually we rather do a list(set()) afterwards,
|
|
# but in this case order is important!
|
|
if lod_tuple not in items_lod:
|
|
items_lod.append(lod_tuple)
|
|
|
|
return items_lod
|
|
|
|
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
|
|
asset_id: IntProperty(options={"HIDDEN"}) # noqa: F821
|
|
do_use_collection: BoolProperty(
|
|
name=_t("Import as collection"), # noqa: F722
|
|
description=_t("Instance model from a reusable collection"), # noqa: F722
|
|
default=False
|
|
)
|
|
do_reuse_materials: BoolProperty(
|
|
name=_t("Reuse materials"), # noqa: F722
|
|
description=_t("Reuse already imported materials to avoid duplicates"), # noqa: F722
|
|
default=False)
|
|
do_link_blend: BoolProperty(
|
|
name=_t("Link .blend file"), # noqa: F722
|
|
description=_t("Link the .blend file instead of appending"), # noqa: F722
|
|
default=False)
|
|
size: EnumProperty(
|
|
name=_t("Texture"), # noqa: F821
|
|
items=_fill_size_drop_down,
|
|
description=_t("Change size of assigned textures.")) # noqa: F722
|
|
lod: EnumProperty(
|
|
name=_t("LOD"), # noqa: F821
|
|
items=_fill_lod_drop_down,
|
|
description=_t("Change LOD of the Model.")) # noqa: F722
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
"""Runs once per operator call before drawing occurs."""
|
|
super().__init__(*args, **kwargs)
|
|
|
|
# Infer the default value on each press from the cached session
|
|
# setting, which will be updated by redo last but not saved to prefs.
|
|
self.do_link_blend = cTB.link_blend_session
|
|
|
|
self.blend_exists = False
|
|
self.lod_import = False
|
|
|
|
self.asset_data = None
|
|
self.exec_count = 0
|
|
|
|
@staticmethod
|
|
def init_context(addon_version: str) -> None:
|
|
"""Called from operators.py to init global addon context."""
|
|
|
|
global cTB
|
|
cTB = get_context(addon_version)
|
|
|
|
@classmethod
|
|
def description(cls, context, properties):
|
|
return properties.tooltip
|
|
|
|
def draw(self, context):
|
|
prefer_blend = cTB.settings["download_prefer_blend"] and self.blend_exists
|
|
if not self.blend_exists:
|
|
label = _t("No local .blend file :")
|
|
row_link_enabled = False
|
|
row_sizes_enabled = True
|
|
elif not prefer_blend:
|
|
label = _t("Enable preference 'Download + Import .blend' :")
|
|
row_link_enabled = False
|
|
row_sizes_enabled = True
|
|
elif self.lod_import:
|
|
label = _t("Set 'LOD' to 'NONE' to load .blend :")
|
|
row_link_enabled = False
|
|
row_sizes_enabled = True
|
|
else:
|
|
label = None
|
|
row_link_enabled = True
|
|
row_sizes_enabled = not self.do_link_blend
|
|
|
|
row = self.layout.row()
|
|
row.prop(self, "lod")
|
|
row.enabled = True
|
|
|
|
row = self.layout.row()
|
|
row.prop(self, "size")
|
|
row.enabled = row_sizes_enabled
|
|
|
|
self.layout.prop(self, "do_use_collection")
|
|
|
|
row = self.layout.row()
|
|
row.prop(self, "do_reuse_materials")
|
|
row.enabled = not (row_link_enabled and self.do_link_blend)
|
|
|
|
if label is not None:
|
|
self.layout.label(text=label)
|
|
|
|
row = self.layout.row()
|
|
row.prop(self, "do_link_blend")
|
|
row.enabled = row_link_enabled
|
|
|
|
@reporting.handle_operator()
|
|
def execute(self, context):
|
|
"""Runs at least once before first draw call occurs."""
|
|
|
|
self.asset_data = cTB._asset_index.get_asset(self.asset_id)
|
|
asset_name = self.asset_data.asset_name
|
|
|
|
# Save any updated preference (to link or not), if changed via the
|
|
# redo last menu (without changing the saved preferences value).
|
|
cTB.link_blend_session = self.do_link_blend
|
|
|
|
project_files, textures, size, lod = self.get_model_data(
|
|
self.asset_data)
|
|
|
|
asset_name = construct_model_name(asset_name, size, lod)
|
|
did_fresh_import = False
|
|
inst = None
|
|
new_objs = []
|
|
|
|
# Import the model.
|
|
blend_import = False
|
|
coll_exists = bpy.data.collections.get(asset_name) is not None
|
|
if self.do_use_collection is False or not coll_exists:
|
|
ok, new_objs, blend_import, fbx_fail = self.run_fresh_import(
|
|
context,
|
|
project_files,
|
|
textures,
|
|
size,
|
|
lod)
|
|
if fbx_fail:
|
|
return {'CANCELLED'}
|
|
did_fresh_import = True
|
|
|
|
# If imported, perform these steps regardless of collection or not.
|
|
if did_fresh_import:
|
|
empty = self.setup_empty_parent(context, new_objs, asset_name)
|
|
if blend_import and not self.do_use_collection:
|
|
empty.location = bpy.context.scene.cursor.location
|
|
else:
|
|
empty = None
|
|
|
|
if self.do_use_collection:
|
|
# Move the objects into a subcollection, and place in scene.
|
|
if did_fresh_import:
|
|
# Always create a new collection if did a fresh import.
|
|
cache_coll = bpy.data.collections.new(asset_name)
|
|
# Ensure all objects are only part of this new collection.
|
|
for _obj in [empty] + new_objs:
|
|
for _collection in _obj.users_collection:
|
|
_collection.objects.unlink(_obj)
|
|
cache_coll.objects.link(_obj)
|
|
|
|
# Now add the cache collection to the layer, but unchecked.
|
|
layer_coll = context.view_layer.active_layer_collection
|
|
layer_coll.collection.children.link(cache_coll)
|
|
for _child in layer_coll.children:
|
|
if _child.collection == cache_coll:
|
|
_child.exclude = True
|
|
else:
|
|
cache_coll = bpy.data.collections.get(asset_name)
|
|
|
|
if cache_coll is None:
|
|
err = _t("Failed to get new collection to instance")
|
|
self.report({"ERROR"}, err)
|
|
return {"CANCELLED"}
|
|
|
|
# Now finally add the instance to the scene.
|
|
inst = self.create_instance(context, cache_coll, size, lod)
|
|
else:
|
|
# Make sure the objects imported are all part of the same coll.
|
|
self.append_cleanup(context, empty)
|
|
|
|
# Final notifications and reporting.
|
|
cTB.f_GetSceneAssets()
|
|
|
|
if blend_import:
|
|
# Fix import info message containing LOD info
|
|
asset_name = construct_model_name(asset_name, size, "")
|
|
|
|
if did_fresh_import is True:
|
|
fmt = "blend" if blend_import else "FBX"
|
|
self.report(
|
|
{"INFO"},
|
|
_t("Model Imported ({0}) : {1}").format(fmt, asset_name))
|
|
elif self.do_use_collection and inst is not None:
|
|
self.report(
|
|
{"INFO"}, _t("Instance created : ").format(inst.name))
|
|
else:
|
|
err = _t(
|
|
"Failed to import model correctly: {0}").format(asset_name)
|
|
self.report({"ERROR"}, err)
|
|
err = f"Failed to import model correctly: {asset_name}"
|
|
reporting.capture_message("import-model-failed", err, "error")
|
|
return {"CANCELLED"}
|
|
|
|
if self.exec_count == 0:
|
|
cTB.signal_import_asset(asset_id=self.asset_id)
|
|
cTB.set_first_local_asset()
|
|
self.exec_count += 1
|
|
return {"FINISHED"}
|
|
|
|
def _filter_lod_fbxs(self,
|
|
file_list: List[str]
|
|
) -> List[str]:
|
|
"""Returns a list with all FBX files with LOD tag in filename"""
|
|
|
|
all_lod_fbxs = []
|
|
for asset_path in file_list:
|
|
if f_FExt(asset_path) != ".fbx":
|
|
continue
|
|
filename_parts = f_FName(asset_path).split("_")
|
|
for lod_level in LODS:
|
|
if lod_level not in filename_parts:
|
|
continue
|
|
all_lod_fbxs.append(asset_path)
|
|
break
|
|
return all_lod_fbxs
|
|
|
|
def get_model_data(
|
|
self,
|
|
asset_data: AssetData
|
|
) -> Tuple[List[str], List[str], str, str]:
|
|
"""Returns details for Model import from a Model asset."""
|
|
|
|
asset_type_data = asset_data.get_type_data()
|
|
|
|
# Get the intended material size and LOD import to use.
|
|
size_desired = self.size if self.size is not None else cTB.settings["mres"]
|
|
size = asset_type_data.get_size(
|
|
size_desired,
|
|
local_only=True,
|
|
addon_convention=cTB._asset_index.addon_convention,
|
|
local_convention=asset_data.get_convention(local=True))
|
|
|
|
prefer_blend = cTB.settings["download_prefer_blend"]
|
|
|
|
lod = self.lod
|
|
if lod == "NONE":
|
|
if not prefer_blend:
|
|
lod = "LOD1"
|
|
else:
|
|
lod = cTB.settings["lod"]
|
|
lod = asset_type_data.get_lod(lod)
|
|
if lod == "NONE":
|
|
lod = None
|
|
|
|
files = {}
|
|
asset_type_data.get_files(files)
|
|
files = list(files.keys())
|
|
all_lod_fbxs = self._filter_lod_fbxs(files)
|
|
|
|
files_lod_fbx = [
|
|
_file for _file in all_lod_fbxs
|
|
if f_FExt(_file) == ".fbx" and str(lod) in f_FName(_file).split("_")
|
|
]
|
|
# Most likely redundant, but rather safe than sorry
|
|
files_lod_fbx = list(set(files_lod_fbx))
|
|
|
|
files_fbx = []
|
|
files_blend = []
|
|
for _file in files:
|
|
filename_ext = f_FExt(_file)
|
|
is_fbx = filename_ext == ".fbx"
|
|
is_blend = filename_ext == ".blend" and "_LIB.blend" not in _file
|
|
|
|
if not is_fbx and not is_blend:
|
|
continue
|
|
|
|
if is_fbx and lod is not None and len(files_lod_fbx):
|
|
if _file not in files_lod_fbx:
|
|
continue
|
|
|
|
if is_fbx and lod is None and _file in all_lod_fbxs:
|
|
continue
|
|
|
|
if is_fbx:
|
|
files_fbx.append(_file)
|
|
elif is_blend:
|
|
files_blend.append(_file)
|
|
|
|
cTB.logger.debug("POLIIGON_OT_model get_model_data "
|
|
f"{os.path.basename(_file)}")
|
|
|
|
self.blend_exists = len(files_blend) > 0
|
|
self.lod_import = lod is not None and self.lod != "NONE"
|
|
|
|
if prefer_blend and self.blend_exists and not self.lod_import:
|
|
files_project = files_blend
|
|
elif not prefer_blend and not files_fbx and self.blend_exists:
|
|
# Settings specify to import fbx files, but only blend exists.
|
|
# Needed for asset browser imports and right-click
|
|
# TODO(Patrick): Migrate to using a use_blend operator arg, so it
|
|
# can also be a backdoor option.
|
|
files_project = files_blend
|
|
else:
|
|
files_project = files_fbx
|
|
|
|
files_tex = []
|
|
for _file in files:
|
|
if f_FExt(_file) not in MAP_EXT_LOWER:
|
|
continue
|
|
|
|
if size not in f_FName(_file).split("_"):
|
|
continue
|
|
|
|
if any(
|
|
_lod in f_FName(_file).split("_") for _lod in LODS
|
|
) and lod not in f_FName(_file).split("_"):
|
|
continue
|
|
|
|
files_tex.append(_file)
|
|
|
|
return files_project, files_tex, size, lod
|
|
|
|
def _load_blend(self, path_proj: str) -> List[bpy.types.Object]:
|
|
"""Loads all objects from a .blend file."""
|
|
|
|
path_proj_norm = os.path.normpath(path_proj)
|
|
with bpy.data.libraries.load(path_proj_norm,
|
|
link=self.do_link_blend
|
|
) as (data_from,
|
|
data_to):
|
|
data_to.objects = data_from.objects
|
|
return data_to.objects
|
|
|
|
def _cut_identity_counter(self, s: str) -> str:
|
|
"""Reduces strings like 'walter.042' to 'walter'"""
|
|
|
|
splits = s.rsplit(".", maxsplit=1)
|
|
if len(splits) > 1 and splits[1].isdecimal():
|
|
s = splits[0]
|
|
return s
|
|
|
|
def _reuse_materials(
|
|
self, imported_objs: List, imported_mats: List) -> None:
|
|
"""Re-uses previously imported materials after a .blend import"""
|
|
|
|
if not self.do_reuse_materials or self.do_link_blend:
|
|
return
|
|
|
|
# Mark all materials from this import
|
|
PROP_FRESH_IMPORT = "poliigon_fresh_import"
|
|
for mat in imported_mats:
|
|
mat[PROP_FRESH_IMPORT] = True
|
|
# Find any previously imported materials with same name
|
|
# and make the objects use those
|
|
mats_remap = [] # list of tuples (from_mat, to_mat)
|
|
for obj in imported_objs:
|
|
mat_on_obj = obj.active_material
|
|
if mat_on_obj is None:
|
|
continue
|
|
materials = reversed(sorted(list(bpy.data.materials.keys())))
|
|
for name_mat in materials:
|
|
# Unfortunately we seem to have little control,
|
|
# where and when Blender adds counter suffixes for
|
|
# identically named materials.
|
|
# Therefore we compare names without any counter suffix.
|
|
name_on_obj_cmp = self._cut_identity_counter(mat_on_obj.name)
|
|
name_mat_cmp = self._cut_identity_counter(name_mat)
|
|
if name_on_obj_cmp != name_mat_cmp:
|
|
continue
|
|
mat_reuse = bpy.data.materials[name_mat]
|
|
is_fresh = mat_reuse.get(PROP_FRESH_IMPORT, False)
|
|
if is_fresh:
|
|
continue
|
|
if (mat_on_obj, mat_reuse) not in mats_remap:
|
|
mats_remap.append((mat_on_obj, mat_reuse))
|
|
break
|
|
# Remove previously added marker
|
|
for mat in imported_mats:
|
|
if PROP_FRESH_IMPORT in mat.keys():
|
|
del mat[PROP_FRESH_IMPORT]
|
|
# Finally remap the materials and remove those freshly imported ones
|
|
did_send_sentry = False
|
|
for from_mat, to_mat in mats_remap:
|
|
from_mat.user_remap(to_mat)
|
|
from_mat.user_clear()
|
|
if from_mat in imported_mats:
|
|
imported_mats.remove(from_mat)
|
|
if from_mat.users != 0 and not did_send_sentry:
|
|
msg = ("User count not zero on material replaced by reuse: "
|
|
f"Asset: {self.asset_id}, Material: {from_mat.name}")
|
|
reporting.capture_message(
|
|
"import_model_mat_reuse_user_count",
|
|
msg,
|
|
"info")
|
|
did_send_sentry = True
|
|
continue
|
|
try:
|
|
bpy.data.materials.remove(from_mat)
|
|
except Exception as e:
|
|
reporting.capture_exception(e)
|
|
self.report({"WARNING"},
|
|
_t("Failed to remove material after reuse."))
|
|
|
|
# TODO(Andreas): replace usage with AssetIndex/AssetData alternative
|
|
@staticmethod
|
|
def f_GetVar(name: str) -> str:
|
|
for _variant in VARIANTS:
|
|
if _variant in name:
|
|
return _variant
|
|
return None
|
|
|
|
def _run_fresh_import_blend(
|
|
self,
|
|
context,
|
|
path_proj: str,
|
|
filename_base: str,
|
|
size: str
|
|
) -> None:
|
|
"""Imports from blend files."""
|
|
|
|
cTB.logger.debug("POLIIGON_OT_model BLEND IMPORT")
|
|
filename = filename_base + ".blend"
|
|
|
|
if self.do_link_blend and filename in bpy.data.libraries.keys():
|
|
lib = bpy.data.libraries[filename]
|
|
if lib[PROP_LIBRARY_LINKED]:
|
|
linked_objs = []
|
|
for obj in bpy.data.objects:
|
|
if obj.library == lib:
|
|
linked_objs.append(obj)
|
|
|
|
imported_objs = []
|
|
for obj in linked_objs:
|
|
imported_objs.append(obj.copy())
|
|
else:
|
|
imported_objs = self._load_blend(path_proj)
|
|
else:
|
|
imported_objs = self._load_blend(path_proj)
|
|
|
|
if filename in bpy.data.libraries.keys():
|
|
lib = bpy.data.libraries[filename]
|
|
lib[PROP_LIBRARY_LINKED] = self.do_link_blend
|
|
|
|
for obj in context.view_layer.objects:
|
|
obj.select_set(False)
|
|
layer = context.view_layer.active_layer_collection
|
|
imported_mats = []
|
|
for obj in imported_objs:
|
|
if obj is None:
|
|
continue
|
|
obj_copy = obj.copy()
|
|
layer.collection.objects.link(obj_copy)
|
|
obj_copy.select_set(True)
|
|
if obj_copy.active_material is None:
|
|
pass
|
|
elif obj_copy.active_material not in imported_mats:
|
|
imported_mats.append(obj_copy.active_material)
|
|
|
|
files_dict = {}
|
|
asset_type_data = self.asset_data.get_type_data()
|
|
asset_type_data.get_files(files_dict)
|
|
asset_files = list(files_dict.keys())
|
|
replace_tex_size(
|
|
imported_mats,
|
|
asset_files,
|
|
size,
|
|
self.do_link_blend
|
|
)
|
|
self._reuse_materials(imported_objs, imported_mats)
|
|
|
|
def _run_fresh_import_fbx(self, path_proj: str) -> bool:
|
|
"""Imports from FBX files."""
|
|
|
|
cTB.logger.debug("POLIIGON_OT_model FBX IMPORT")
|
|
if "fbx" not in dir(bpy.ops.import_scene):
|
|
try:
|
|
bpy.ops.preferences.addon_enable(module="io_scene_fbx")
|
|
self.report(
|
|
{"INFO"},
|
|
_t("FBX importer addon enabled for import")
|
|
)
|
|
except RuntimeError:
|
|
self.report(
|
|
{"ERROR"},
|
|
_t("Built-in FBX importer could not be found, "
|
|
"check Blender install")
|
|
)
|
|
return False
|
|
try:
|
|
# Note on use_custom_normals parameter:
|
|
# It always defaulted to True. But in Blender 4.0+
|
|
# it started to cause issues.
|
|
# Mateusz sync'ed with Stephen and recommended to turn it
|
|
# off regardless of Blender version.
|
|
bpy.ops.import_scene.fbx(filepath=path_proj,
|
|
axis_up="-Z",
|
|
use_custom_normals=False)
|
|
except Exception as e:
|
|
self.report({"ERROR"},
|
|
_t("FBX importer exception:") + str(e))
|
|
return False
|
|
|
|
return True
|
|
|
|
def _run_fresh_import_create_materials(
|
|
self,
|
|
*,
|
|
path_proj: str,
|
|
filename_base: str,
|
|
imported_proj: List[str],
|
|
files_tex: List[str],
|
|
objs_from_file: List[bpy.types.Object],
|
|
dict_mats_imported: Dict[str, bpy.types.Material],
|
|
size: str,
|
|
lod: str
|
|
) -> None:
|
|
"""Creates materials after successful FBX import."""
|
|
|
|
asset_name = self.asset_data.asset_name
|
|
|
|
for _mesh in objs_from_file:
|
|
if _mesh.type == "EMPTY":
|
|
continue
|
|
|
|
name_mat_imported = ""
|
|
if _mesh.active_material is not None:
|
|
name_mat_imported = _mesh.active_material.name
|
|
|
|
# Note: Of course the check if "_mat" is contained could be
|
|
# written in one line. But I wouldn't consider "_mat"
|
|
# unlikely in arbitrary filenames. Thus I chose to
|
|
# explicitly compare for "_mat" at the end and
|
|
# additionally check if "_mat_" is contained, in order
|
|
# to at least reduce the chance of false positives a bit.
|
|
name_mat_imported_lower = name_mat_imported.lower()
|
|
name_tex_remastered = ""
|
|
name_tex_on_obj = ""
|
|
ends_remastered = name_mat_imported_lower.endswith("_mat")
|
|
contains_remastered = "_mat_" in name_mat_imported_lower
|
|
if ends_remastered or contains_remastered:
|
|
pos_remastered = name_mat_imported_lower.rfind("_mat", 1)
|
|
name_tex_remastered = name_mat_imported[:pos_remastered]
|
|
else:
|
|
name_tex_on_obj = name_mat_imported.split("_")[0]
|
|
|
|
name_mesh_base = _mesh.name.split(".")[0].split("_")[0]
|
|
|
|
variant_mesh = self.f_GetVar(_mesh.name)
|
|
variant_name = variant_mesh
|
|
if variant_mesh is None:
|
|
# This is a fallback for models,
|
|
# where the object name does not contain a variant indicator.
|
|
# As None is covered explicitly in the loop below,
|
|
# this should do no harm.
|
|
variant_mesh = "VAR1"
|
|
variant_name = ""
|
|
|
|
name_mat = name_mesh_base
|
|
files_tex_filtered = []
|
|
if len(name_tex_remastered) > 0:
|
|
# Remastered textures
|
|
files_tex_filtered = [
|
|
_file
|
|
for _file in files_tex
|
|
if os.path.basename(_file).startswith(
|
|
name_tex_remastered)
|
|
]
|
|
name_mat = name_mat_imported
|
|
else:
|
|
for vCheck in [name_mesh_base,
|
|
filename_base.split("_")[0],
|
|
asset_name,
|
|
name_tex_on_obj]:
|
|
files_tex_filtered = [
|
|
_file
|
|
for _file in files_tex
|
|
if os.path.basename(_file).startswith(vCheck)
|
|
if self.f_GetVar(f_FName(_file)) in [None,
|
|
variant_mesh]
|
|
]
|
|
name_mat = vCheck
|
|
if len(files_tex_filtered) > 0:
|
|
break
|
|
|
|
if len(files_tex_filtered) == 0:
|
|
err = f"No Textures found for: {self.asset_id} {_mesh.name}"
|
|
reporting.capture_message(
|
|
"model_texture_missing", err, "info")
|
|
continue
|
|
|
|
name_mat += f"_{size}"
|
|
|
|
if size not in name_mat:
|
|
name_mat += f"_{size}"
|
|
|
|
if variant_name != "":
|
|
name_mat += f"_{variant_name}"
|
|
|
|
# TODO(Andreas): Not sure, why these lines are commented out.
|
|
# Looks reasonable to me.
|
|
# if name_mat in bpy.data.materials and self.do_reuse_materials:
|
|
# mat = bpy.data.materials[name_mat]
|
|
# el
|
|
# TODO(Andreas): Not sure, this should also be depending on
|
|
# self.do_reuse_materials?
|
|
if name_mat in dict_mats_imported.keys():
|
|
# Already built in previous iteration
|
|
mat = dict_mats_imported[name_mat]
|
|
else:
|
|
mat = cTB.mat_import.import_material(
|
|
asset_data=self.asset_data,
|
|
do_apply=False,
|
|
workflow="METALNESS",
|
|
size=size,
|
|
size_bg=None,
|
|
lod=lod,
|
|
variant=variant_name if variant_name != "" else None,
|
|
name_material=name_mat,
|
|
name_mesh=name_mesh_base,
|
|
ref_objs=None,
|
|
projection="UV",
|
|
use_16bit=True,
|
|
mode_disp="NORMAL", # We never want model dispalcement
|
|
translate_x=0.0,
|
|
translate_y=0.0,
|
|
scale=1.0,
|
|
global_rotation=0.0,
|
|
aspect_ratio=1.0,
|
|
displacement=0.0,
|
|
keep_unused_tex_nodes=False,
|
|
reuse_existing=self.do_reuse_materials
|
|
)
|
|
if mat is None:
|
|
msg = f"{self.asset_id}: Failed to build matrial: {name_mat}"
|
|
reporting.capture_message(
|
|
"could_not_create_fbx_mat", msg, "error")
|
|
self.report(
|
|
{"ERROR"}, _t("Material could not be created."))
|
|
imported_proj.remove(path_proj)
|
|
break
|
|
|
|
dict_mats_imported[name_mat] = mat
|
|
|
|
# This sequence is important!
|
|
# 1) Setting the material slot to None
|
|
# 2) Changing the link mode
|
|
# 3) Assigning our generated material
|
|
# Any other order of these statements will get us into trouble
|
|
# one way or another.
|
|
_mesh.active_material = None
|
|
if len(_mesh.material_slots) > 0:
|
|
_mesh.material_slots[0].link = "OBJECT"
|
|
_mesh.active_material = mat
|
|
|
|
if variant_mesh is not None:
|
|
if len(_mesh.material_slots) == 0:
|
|
_mesh.data.materials.append(mat)
|
|
else:
|
|
_mesh.material_slots[0].link = "OBJECT"
|
|
_mesh.material_slots[0].material = mat
|
|
_mesh.material_slots[0].link = "OBJECT"
|
|
|
|
# Ensure we can identify the mesh & LOD even on name change.
|
|
# TODO(Andreas): Remove old properties?
|
|
_mesh.poliigon = f"Models;{asset_name}"
|
|
if lod is not None:
|
|
_mesh.poliigon_lod = lod
|
|
self.set_poliigon_props_model(_mesh, lod)
|
|
|
|
# Finally try to remove the originally imported materials
|
|
if name_mat_imported not in bpy.data.materials:
|
|
continue
|
|
mat_imported = bpy.data.materials[name_mat_imported]
|
|
if mat_imported.users == 0:
|
|
mat_imported.user_clear()
|
|
bpy.data.materials.remove(mat_imported)
|
|
|
|
def run_fresh_import(self,
|
|
context,
|
|
project_files: List[str],
|
|
files_tex: List[str],
|
|
size: str,
|
|
lod: str
|
|
) -> Tuple[bool,
|
|
List[bpy.types.Object],
|
|
bool,
|
|
bool]:
|
|
"""Performs a fresh import of the whole model.
|
|
|
|
There can be multiple FBX models, therefore we want to import all of
|
|
them and verify each one was properly imported.
|
|
|
|
Return values:
|
|
Tuple[0] - True, if all FBX files have been imported successfully
|
|
Tuple[1] - List of all imported mesh objects
|
|
Tuple[2] - True, if it was a .blend import instead of an FBX import
|
|
Tuple[3] - True, if an error occurred during FBX loading
|
|
"""
|
|
|
|
asset_name = self.asset_data.asset_name
|
|
|
|
meshes_all = []
|
|
dict_mats_imported = {}
|
|
imported_proj = []
|
|
blend_import = False
|
|
for path_proj in project_files:
|
|
filename_base = f_FName(path_proj)
|
|
|
|
if not f_Ex(path_proj):
|
|
err = _t("Couldn't load project file: {0} {1}").format(
|
|
asset_name, path_proj)
|
|
self.report({"ERROR"}, err)
|
|
err = ("Couldn't load project file: "
|
|
f"{self.asset_id} {path_proj}")
|
|
reporting.capture_message("model_fbx_missing", err, "info")
|
|
continue
|
|
|
|
list_objs_before = list(context.scene.objects)
|
|
|
|
ext_proj = f_FExt(path_proj)
|
|
if ext_proj == ".blend":
|
|
self._run_fresh_import_blend(
|
|
context, path_proj, filename_base, size)
|
|
blend_import = True
|
|
else:
|
|
import_ok = self._run_fresh_import_fbx(path_proj)
|
|
if not import_ok:
|
|
did_full_import = False
|
|
meshes_all = []
|
|
blend_import = False
|
|
fbx_error = True
|
|
return (did_full_import,
|
|
meshes_all,
|
|
blend_import,
|
|
fbx_error)
|
|
|
|
imported_proj.append(path_proj)
|
|
objs_from_file = [_obj for _obj in list(context.scene.objects)
|
|
if _obj not in list_objs_before]
|
|
|
|
meshes_all += objs_from_file
|
|
|
|
if ext_proj == ".blend":
|
|
for _mesh in objs_from_file:
|
|
# Ensure we can identify the mesh & LOD even on name change
|
|
# TODO(Andreas): Remove old properties?
|
|
_mesh.poliigon = f"Models;{asset_name}"
|
|
if lod is not None:
|
|
_mesh.poliigon_lod = lod
|
|
self.set_poliigon_props_model(_mesh, lod)
|
|
continue
|
|
|
|
self._run_fresh_import_create_materials(
|
|
path_proj=path_proj,
|
|
filename_base=filename_base,
|
|
imported_proj=imported_proj,
|
|
files_tex=files_tex,
|
|
objs_from_file=objs_from_file,
|
|
dict_mats_imported=dict_mats_imported,
|
|
size=size,
|
|
lod=lod)
|
|
|
|
# There could have been multiple FBXs, consider fully imported
|
|
# for user-popup reporting if all FBX files imported.
|
|
did_full_import = len(imported_proj) == len(project_files)
|
|
|
|
return did_full_import, meshes_all, blend_import, False
|
|
|
|
def set_poliigon_props_model(
|
|
self, obj: bpy.types.Object, lod: str) -> None:
|
|
"""Sets Poliigon properties on an imported object."""
|
|
|
|
obj.poliigon_props.asset_id = self.asset_data.asset_id
|
|
obj.poliigon_props.asset_name = self.asset_data.asset_name
|
|
obj.poliigon_props.asset_type = self.asset_data.asset_type.name
|
|
obj.poliigon_props.size = self.size
|
|
if lod is not None:
|
|
obj.poliigon_props.lod = lod
|
|
obj.poliigon_props.use_collection = self.do_use_collection
|
|
obj.poliigon_props.link_blend = self.do_link_blend
|
|
|
|
def object_has_children(self, obj_parent: bpy.types.Object) -> bool:
|
|
"""Returns True, if obj_parent has children."""
|
|
|
|
for _obj in bpy.data.objects:
|
|
if _obj.parent == obj_parent:
|
|
return True
|
|
return False
|
|
|
|
def setup_empty_parent(self,
|
|
context,
|
|
new_meshes: List[bpy.types.Object],
|
|
asset_name: str
|
|
) -> bpy.types.Object:
|
|
"""Parents newly imported objects to a central empty object."""
|
|
|
|
radius = 0
|
|
for _mesh in new_meshes:
|
|
dimensions = _mesh.dimensions
|
|
if dimensions.x > radius:
|
|
radius = dimensions.x
|
|
if dimensions.y > radius:
|
|
radius = dimensions.y
|
|
|
|
empty = bpy.data.objects.new(
|
|
name=f"{asset_name}_Empty", object_data=None)
|
|
if self.do_use_collection:
|
|
empty.empty_display_size = 0.01
|
|
else:
|
|
empty.empty_display_size = radius * 0.5
|
|
|
|
layer = context.view_layer.active_layer_collection
|
|
layer.collection.objects.link(empty)
|
|
|
|
for _mesh in new_meshes.copy():
|
|
if _mesh.type == "EMPTY" and not self.object_has_children(_mesh):
|
|
bpy.data.objects.remove(_mesh, do_unlink=True)
|
|
new_meshes.remove(_mesh)
|
|
continue
|
|
_mesh.parent = empty
|
|
|
|
return empty
|
|
|
|
def create_instance(
|
|
self, context, coll, size: str, lod: str) -> bpy.types.Object:
|
|
"""Creates an instance of an existing collection int he active view."""
|
|
|
|
asset_name = self.asset_data.asset_name
|
|
|
|
inst_name = construct_model_name(asset_name, size, lod) + "_Instance"
|
|
inst = bpy.data.objects.new(name=inst_name, object_data=None)
|
|
inst.instance_collection = coll
|
|
inst.instance_type = "COLLECTION"
|
|
layer_coll = context.view_layer.active_layer_collection
|
|
layer_coll.collection.objects.link(inst)
|
|
inst.location = context.scene.cursor.location
|
|
inst.empty_display_size = 0.01
|
|
|
|
# Set selection and active object.
|
|
for _obj in context.scene.collection.all_objects:
|
|
_obj.select_set(False)
|
|
inst.select_set(True)
|
|
context.view_layer.objects.active = inst
|
|
return inst
|
|
|
|
def append_cleanup(self, context, root_empty: bpy.types.Object) -> None:
|
|
"""Performs selection and placement cleanup after an import/append."""
|
|
|
|
if root_empty is None:
|
|
cTB.logger.error(
|
|
"root_empty was not a valid object, exiting cleanup")
|
|
return
|
|
|
|
# Set empty location
|
|
root_empty.location = context.scene.cursor.location
|
|
|
|
# Deselect all others in scene (faster than using operator call).
|
|
for _obj in context.scene.collection.all_objects:
|
|
_obj.select_set(False)
|
|
|
|
# Make empty active and select it + children.
|
|
root_empty.select_set(True)
|
|
context.view_layer.objects.active = root_empty
|
|
for _obj in root_empty.children:
|
|
_obj.select_set(True)
|