2025-12-01

This commit is contained in:
2026-03-17 14:58:51 -06:00
parent 183e865f8b
commit 4b82b57113
6846 changed files with 954887 additions and 162606 deletions
@@ -16,7 +16,7 @@
#
# ##### END GPL LICENSE BLOCK #####
from typing import List, Tuple
from typing import Dict, List, Tuple
import os
import bpy
@@ -57,6 +57,8 @@ LOD_DESCS = {
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"
@@ -223,7 +225,7 @@ class POLIIGON_OT_model(Operator):
else:
empty = None
if self.do_use_collection is True:
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.
@@ -235,25 +237,22 @@ class POLIIGON_OT_model(Operator):
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:
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)
# 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:
if cache_coll is None:
err = _t("Failed to get new collection to instance")
self.report({"ERROR"}, err)
return {"CANCELLED"}
elif not blend_import:
# 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)
@@ -282,6 +281,7 @@ class POLIIGON_OT_model(Operator):
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"}
@@ -302,7 +302,12 @@ class POLIIGON_OT_model(Operator):
break
return all_lod_fbxs
def get_model_data(self, asset_data: AssetData):
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.
@@ -393,7 +398,7 @@ class POLIIGON_OT_model(Operator):
return files_project, files_tex, size, lod
def _load_blend(self, path_proj: str):
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)
@@ -412,7 +417,8 @@ class POLIIGON_OT_model(Operator):
s = splits[0]
return s
def _reuse_materials(self, imported_objs: List, imported_mats: List):
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:
@@ -481,6 +487,272 @@ class POLIIGON_OT_model(Operator):
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],
@@ -503,8 +775,6 @@ class POLIIGON_OT_model(Operator):
Tuple[3] - True, if an error occurred during FBX loading
"""
PROP_LIBRARY_LINKED = "poliigon_linked"
asset_name = self.asset_data.asset_name
meshes_all = []
@@ -518,7 +788,8 @@ class POLIIGON_OT_model(Operator):
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}"
err = ("Couldn't load project file: "
f"{self.asset_id} {path_proj}")
reporting.capture_message("model_fbx_missing", err, "info")
continue
@@ -526,91 +797,12 @@ class POLIIGON_OT_model(Operator):
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)
self._run_fresh_import_blend(
context, path_proj, filename_base, size)
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))
import_ok = self._run_fresh_import_fbx(path_proj)
if not import_ok:
did_full_import = False
meshes_all = []
blend_import = False
@@ -621,13 +813,13 @@ class POLIIGON_OT_model(Operator):
fbx_error)
imported_proj.append(path_proj)
vMeshes = [_obj for _obj in list(context.scene.objects)
if _obj not in list_objs_before]
objs_from_file = [_obj for _obj in list(context.scene.objects)
if _obj not in list_objs_before]
meshes_all += vMeshes
meshes_all += objs_from_file
if ext_proj == ".blend":
for _mesh in vMeshes:
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}"
@@ -636,160 +828,15 @@ class POLIIGON_OT_model(Operator):
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)
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.
@@ -852,7 +899,8 @@ class POLIIGON_OT_model(Operator):
return empty
def create_instance(self, context, coll, size, lod):
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
@@ -861,8 +909,8 @@ class POLIIGON_OT_model(Operator):
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)
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
@@ -873,11 +921,12 @@ class POLIIGON_OT_model(Operator):
context.view_layer.objects.active = inst
return inst
def append_cleanup(self, context, root_empty):
def append_cleanup(self, context, root_empty: bpy.types.Object) -> None:
"""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")
if root_empty is None:
cTB.logger.error(
"root_empty was not a valid object, exiting cleanup")
return
# Set empty location