2025-07-01
This commit is contained in:
@@ -0,0 +1,894 @@
|
||||
# #### 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 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")
|
||||
|
||||
|
||||
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 is True:
|
||||
# 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 = context.view_layer.active_layer_collection
|
||||
layer.collection.children.link(cache_coll)
|
||||
for _child in layer.children:
|
||||
if _child.collection == cache_coll:
|
||||
_child.exclude = True
|
||||
else:
|
||||
cache_coll = bpy.data.collections.get(asset_name)
|
||||
|
||||
# Now finally add the instance to the scene.
|
||||
if cache_coll:
|
||||
inst = self.create_instance(context, cache_coll, size, lod)
|
||||
# layer = context.view_layer.active_layer_collection
|
||||
# layer.collection.objects.link(inst)
|
||||
else:
|
||||
err = _t("Failed to get new collection to instance")
|
||||
self.report({"ERROR"}, err)
|
||||
return {"CANCELLED"}
|
||||
|
||||
elif not blend_import:
|
||||
# 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)
|
||||
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):
|
||||
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):
|
||||
"""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):
|
||||
"""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(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
|
||||
"""
|
||||
|
||||
PROP_LIBRARY_LINKED = "poliigon_linked"
|
||||
|
||||
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 = f"Couldn't load project file: {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":
|
||||
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)
|
||||
|
||||
blend_import = True
|
||||
else:
|
||||
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")
|
||||
)
|
||||
did_full_import = False
|
||||
meshes_all = []
|
||||
blend_import = False
|
||||
fbx_error = True
|
||||
return (did_full_import,
|
||||
meshes_all,
|
||||
blend_import,
|
||||
fbx_error)
|
||||
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))
|
||||
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)
|
||||
vMeshes = [_obj for _obj in list(context.scene.objects)
|
||||
if _obj not in list_objs_before]
|
||||
|
||||
meshes_all += vMeshes
|
||||
|
||||
if ext_proj == ".blend":
|
||||
for _mesh in vMeshes:
|
||||
# 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
|
||||
|
||||
for _mesh in vMeshes:
|
||||
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 dependening 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 in bpy.data.materials:
|
||||
mat_imported = bpy.data.materials[name_mat_imported]
|
||||
if mat_imported.users == 0:
|
||||
mat_imported.user_clear()
|
||||
bpy.data.materials.remove(mat_imported)
|
||||
|
||||
# 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, lod):
|
||||
"""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"
|
||||
lc = context.view_layer.active_layer_collection
|
||||
lc.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):
|
||||
"""Performs selection and placement cleanup after an import/append."""
|
||||
|
||||
if not root_empty:
|
||||
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)
|
||||
Reference in New Issue
Block a user