Files
blender-portable-repo/scripts/addons/poliigon-addon-blender/material_importer.py
T
2026-03-17 14:30:01 -06:00

280 lines
10 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 #####
import json
import os
from typing import Dict, List, Optional
import bpy
from .modules.poliigon_core.assets import AssetData
from .modules.poliigon_core.user import UserDownloadPreferences
from .material_import_cycles import CyclesMaterial, RENDERER_CYCLES
from .material_importer_params import MaterialImportParameters
from . import reporting
SUPPORTED_RENDERERS = [RENDERER_CYCLES]
class MaterialImporter():
def __init__(self, cTB, renderer: str = RENDERER_CYCLES):
self.cTB = cTB
self.renderer = None
self.importer = None
self.params = None
self.asset_data = None
self.set_renderer(renderer)
def set_renderer(self, renderer: str) -> bool:
"""Sets the renderer to import materials for."""
if renderer not in IMPORTERS:
raise RuntimeError(
f"Unsupported renderer: {renderer}\n"
f"Supported: {IMPORTERS.keys}")
self.renderer = renderer
self.importer = IMPORTERS[renderer]()
def reset_asset(self) -> None:
self.asset_data = None
def convert_dict_to_asset_data(
self, asset_dict: Dict) -> Optional[AssetData]:
"""Converts a P4B asset data dictionary into an addon-core AssetData
instance.
"""
asset_id = asset_dict.get("id", -1)
if asset_id >= 0:
# Backdoor import expects negative ID
asset_id *= -1
asset_name = asset_dict["name"]
if len(asset_dict["files"]) == 0:
raise RuntimeError("Material import for asset without any files")
# Separate asset files per library directory
# (if distributed across multiple)
dirs_libraries = self.cTB.get_library_paths()
files_per_dir = {}
asset_files = asset_dict["files"]
for _idx_dir, _dir in enumerate(dirs_libraries):
_dir = os.path.normpath(_dir)
files_per_dir[_idx_dir] = [
_file
for _file in asset_files
if os.path.normpath(_file).startswith(_dir)
]
# Get asset's base directory in each library directory
dir_asset_per_lib = {}
for _idx_dir, _files in files_per_dir.items():
try:
dir_asset_per_lib[_idx_dir] = os.path.commonpath(_files)
except ValueError:
pass # deliberately ignored
# Build backdoor file list (per library dir)
file_list = []
for _dir_asset in dir_asset_per_lib.values():
file_list_from_dir = self.cTB._asset_index_mat.file_list_from_directory(
asset_dir=_dir_asset, ignore_dirs=[])
file_list.extend(file_list_from_dir)
if len(file_list) == 0:
# Backdoor imported asset outside of libraries?
dir_asset = os.path.commonpath(asset_files)
file_list_from_dir = self.cTB._asset_index_mat.file_list_from_directory(
asset_dir=dir_asset, ignore_dirs=[])
file_list.extend(file_list_from_dir)
result = self.cTB._asset_index_mat.load_asset_from_list(
asset_id=asset_id,
asset_name=asset_name,
asset_type=asset_dict["type"],
size=asset_dict["sizes"][0], # any size will do here
lod="", # not in use
workflow_expected=asset_dict.get("workflows", ["METALNESS"])[0],
file_list_json=json.dumps(file_list),
query_string="p4b_mat_import/All Assets",
convention=asset_dict.get("api_convention", 0)
)
if result is False:
msg = f"Failed to convert to AssetData: {asset_id}"
reporting.capture_message(
"build_mat_error_create", msg, "error")
self.cTB._asset_index_mat.flush(all_assets=True)
return None
asset_data = self.cTB._asset_index_mat.get_asset(asset_id)
self.cTB._asset_index_mat.flush(all_assets=True)
return asset_data
def set_parameters(self,
reuse_existing: bool,
do_apply: bool,
workflow: str,
lod: str,
size: str,
size_bg: Optional[str] = None,
variant: Optional[str] = None,
name_material: Optional[str] = None,
name_mesh: Optional[str] = None,
ref_objs: List[any] = [],
projection: str = "FLAT",
use_16bit: bool = True,
mode_disp: str = "NORMAL",
translate_x: float = 0.0,
translate_y: float = 0.0,
scale: float = 1.0,
global_rotation: float = 0.0,
aspect_ratio: float = 1.0,
displacement: float = 0.0,
keep_unused_tex_nodes: bool = False,
map_prefs: Optional[UserDownloadPreferences] = None
) -> None:
"""Sets the parameterts for a material import."""
if self.asset_data is None:
raise RuntimeError("No asset set!")
self.params = MaterialImportParameters(
asset_data=self.asset_data,
reuse_existing=reuse_existing,
do_apply=do_apply,
workflow=workflow,
lod=lod,
size=size,
size_bg=size_bg,
variant=variant,
name_material=name_material,
name_mesh=name_mesh,
ref_objs=ref_objs,
projection=projection,
use_16bit=use_16bit,
mode_disp=mode_disp,
translate_x=translate_x,
translate_y=translate_y,
scale=scale,
global_rotation=global_rotation,
aspect_ratio=aspect_ratio,
displacement=displacement,
keep_unused_tex_nodes=keep_unused_tex_nodes,
addon_convention=self.cTB.addon_convention,
map_prefs=map_prefs
)
def reset_parameters(self) -> None:
"""Resets all parameters."""
self.params = None
def get_existing_material(self) -> Optional[bpy.types.Material]:
"""Returns an already existing material of identical name.
This is what legacy import in P4B did for Model assets.
Texture assets were handeled differently via
find_identical_material().
"""
if not self.params.reuse_existing:
return None
name_mat = self.params.name_material
if name_mat in bpy.data.materials.keys():
return bpy.data.materials[name_mat]
return None
def import_material(self,
*,
asset_data: AssetData,
do_apply: bool,
workflow: str,
size: str,
size_bg: Optional[str] = None,
lod: Optional[str] = None,
variant: Optional[str] = None,
name_material: Optional[str] = None,
name_mesh: Optional[str] = None,
ref_objs: Optional[List[any]] = None,
projection: str = "FLAT",
use_16bit: bool = True,
mode_disp: str = "NORMAL",
translate_x: float = 0.0,
translate_y: float = 0.0,
scale: float = 1.0,
global_rotation: float = 0.0,
aspect_ratio: float = 1.0,
displacement: float = 0.0,
keep_unused_tex_nodes: bool = False,
reuse_existing: bool = True,
map_prefs: Optional[UserDownloadPreferences] = None
) -> Optional[bpy.types.Material]:
"""Imports a single material for an asset regardless of type."""
if asset_data is None:
return None
self.asset_data = asset_data
self.set_parameters(
reuse_existing=reuse_existing,
do_apply=do_apply,
workflow=workflow,
lod=lod,
size=size,
size_bg=size_bg,
variant=variant,
name_material=name_material,
name_mesh=name_mesh,
ref_objs=ref_objs,
projection=projection,
use_16bit=use_16bit,
mode_disp=mode_disp,
translate_x=translate_x,
translate_y=translate_y,
scale=scale,
global_rotation=global_rotation,
aspect_ratio=aspect_ratio,
displacement=displacement,
keep_unused_tex_nodes=keep_unused_tex_nodes,
map_prefs=map_prefs
)
# Case for Model import, Texture assets handle material re-use still
# in operator. TODO(Andreas)
mat = self.get_existing_material()
if mat is not None:
self.reset_parameters()
self.reset_asset()
return mat
mat = self.importer.import_material(self.asset_data, self.params)
self.reset_parameters()
self.reset_asset()
return mat
IMPORTERS = {
RENDERER_CYCLES: CyclesMaterial
}