blender customization

This commit is contained in:
Vysyr committed 2026-10-31 17:21:00 +01:00
1 parent fb39e290c0
commit 8c0a639fba
12 files changed
+264 -8

No files matched your search

+24 -4
View File
@@ -3,11 +3,12 @@ from pathlib import Path
import unreal
from .paths import GENERATED, slug
from .paths import GENERATED, PROJECT, slug
def import_model(filename, name="Character"):
source = Path(filename).resolve()
source = Path(filename).expanduser()
source = (source if source.is_absolute() else PROJECT / source).resolve()
if not source.is_file():
raise FileNotFoundError(f"Brak modelu: {source}")
if source.suffix.lower() not in {".glb", ".gltf", ".fbx", ".obj"}:
@@ -31,6 +32,10 @@ def import_model(filename, name="Character"):
pipeline = unreal.AssetToolsHelpers.get_asset_tools().duplicate_asset(
"GLTFWorkshop", GENERATED + "/Pipelines", template)
pipeline.set_editor_property("reimport_strategy", unreal.ReimportStrategyFlags.APPLY_PIPELINE_PROPERTIES)
pipeline.common_meshes_properties.set_editor_property(
"force_all_mesh_as_type", unreal.InterchangeForceMeshType.IFMT_STATIC_MESH)
pipeline.mesh_pipeline.set_editor_property("combine_static_meshes", True)
pipeline.mesh_pipeline.set_editor_property("import_skeletal_meshes", False)
pipeline.material_pipeline.set_editor_property("import_materials", True)
# PBR images are imported explicitly into WorkshopTextures below. This
# avoids Interchange's dependency-preserving material reimport behavior.
@@ -47,9 +52,11 @@ def import_model(filename, name="Character"):
unreal.SoftObjectPath(pipeline.get_path_name()),
unreal.SoftObjectPath("/Interchange/Pipelines/DefaultGLTFPipeline.DefaultGLTFPipeline")])
task.set_editor_property("options", options)
if source.suffix.lower() == ".fbx":
if source.suffix.lower() in {".fbx", ".obj"}:
options = unreal.FbxImportUI()
options.set_editor_property("import_mesh", True)
options.set_editor_property("import_as_skeletal", False)
options.set_editor_property("import_animations", False)
options.set_editor_property("mesh_type_to_import", unreal.FBXImportType.FBXIT_STATIC_MESH)
options.set_editor_property("automated_import_should_detect_type", False)
options.static_mesh_import_data.set_editor_property("combine_meshes", True)
@@ -59,10 +66,23 @@ def import_model(filename, name="Character"):
if len(meshes) != 1:
raise RuntimeError(
f"Import {source.name}: oczekiwano jednego StaticMesh, otrzymano {len(meshes)}. "
"Polacz obiekty przed eksportem albo uzyj workshop.blender.import_character().")
"Plik musi zawierac statyczna geometrie modelu.")
if source.suffix.lower() in {".glb", ".gltf"}:
# UE preserves material edits on mesh reimport. In this workflow Blender
# owns the look; explicitly synchronize the supported PBR parameters.
from ._gltf import link_textures
link_textures(source, meshes[0], destination)
else:
# Optional single-atlas convention, also used by the bundled FBX demos.
files = {file.stem.lower(): file for file in source.parent.iterdir()
if file.suffix.lower() in {".png", ".jpg", ".jpeg"}}
maps = {parameter: files.get(source.stem.lower() + suffix)
for suffix, parameter in (("_bc", "base_color"), ("_n", "normal"),
("_r", "roughness"), ("_m", "metallic"), ("_e", "emissive"))}
if maps["base_color"] is not None:
from .materials import texture_set
material = texture_set(name + "Imported", **maps)
for index in range(len(meshes[0].static_materials)):
meshes[0].set_material(index, material)
unreal.EditorAssetLibrary.save_loaded_asset(meshes[0])
return meshes[0]
+57
View File
@@ -1,6 +1,8 @@
"""Small opaque PBR materials, generated without external assets."""
import unreal
from pathlib import Path
from .paths import GENERATED, slug
@@ -32,3 +34,58 @@ def assign(actor, material, slot=0):
if component is None:
raise TypeError("Material mozna przypisac do aktora ze StaticMeshComponent.")
component.set_material(slot, material)
def texture_set(name, *, base_color, normal=None, roughness=None, metallic=None, emissive=None):
"""Create one atlas material from adjacent PBR textures (PNG/JPEG)."""
slug(name)
tools = unreal.AssetToolsHelpers.get_asset_tools()
path = f"{GENERATED}/Materials/M_{name}"
material = unreal.load_asset(path)
if material is None:
material = tools.create_asset("M_" + name, GENERATED + "/Materials",
unreal.Material, unreal.MaterialFactoryNew())
lib = unreal.MaterialEditingLibrary
lib.delete_all_material_expressions(material)
entries = [("BaseColor", base_color, unreal.MaterialProperty.MP_BASE_COLOR, "RGB", "color"),
("Normal", normal, unreal.MaterialProperty.MP_NORMAL, "RGB", "normal"),
("Roughness", roughness, unreal.MaterialProperty.MP_ROUGHNESS, "R", "linear"),
("Metallic", metallic, unreal.MaterialProperty.MP_METALLIC, "R", "linear"),
("Emissive", emissive, unreal.MaterialProperty.MP_EMISSIVE_COLOR, "RGB", "color")]
for index, (label, filename, prop, channel, role) in enumerate(entries):
if filename is None:
if label in {"Roughness", "Metallic"}:
node = lib.create_material_expression(material, unreal.MaterialExpressionConstant, -300, index * 180)
node.set_editor_property("r", 0.6 if label == "Roughness" else 0.0)
lib.connect_material_property(node, "", prop)
continue
source = Path(filename).resolve()
if not source.is_file():
raise FileNotFoundError(source)
task = unreal.AssetImportTask()
task.filename = str(source)
task.destination_path = f"{GENERATED}/Materials/Textures/{name}"
task.destination_name = "T_" + label
task.automated = True
task.replace_existing = True
task.save = True
task.set_editor_property("async_", False)
tools.import_asset_tasks([task])
textures = [obj for obj in task.get_objects() if isinstance(obj, unreal.Texture2D)]
if len(textures) != 1:
raise RuntimeError("Nie mozna zaimportowac tekstury " + str(source))
texture = textures[0]
texture.set_editor_property("srgb", role == "color")
texture.set_editor_property("compression_settings", unreal.TextureCompressionSettings.TC_NORMALMAP
if role == "normal" else unreal.TextureCompressionSettings.TC_MASKS
if role == "linear" else unreal.TextureCompressionSettings.TC_DEFAULT)
unreal.EditorAssetLibrary.save_loaded_asset(texture)
node = lib.create_material_expression(material, unreal.MaterialExpressionTextureSample, -300, index * 180)
node.set_editor_property("texture", texture)
node.set_editor_property("sampler_type", unreal.MaterialSamplerType.SAMPLERTYPE_NORMAL
if role == "normal" else unreal.MaterialSamplerType.SAMPLERTYPE_MASKS
if role == "linear" else unreal.MaterialSamplerType.SAMPLERTYPE_COLOR)
lib.connect_material_property(node, channel, prop)
lib.recompile_material(material)
unreal.EditorAssetLibrary.save_loaded_asset(material)
return material
+6
View File
@@ -16,6 +16,12 @@ def slug(value):
def executable(name, explicit=None):
candidate = explicit or os.environ.get("WORKSHOP_" + name.upper()) or shutil.which(name)
if not candidate and name == "blender" and os.name == "nt":
# Standard Windows installs do not necessarily add Blender to PATH.
root = Path(os.environ.get("PROGRAMFILES", r"C:\Program Files")) / "Blender Foundation"
installed = [path for path in root.glob("Blender */blender.exe") if path.is_file()]
if installed:
candidate = max(installed, key=lambda path: tuple(int(part) for part in re.findall(r"\d+", path.parent.name)))
if candidate and Path(candidate).is_file():
return str(Path(candidate).resolve())
raise FileNotFoundError(
+39 -1
View File
@@ -1,7 +1,10 @@
"""An isolated generated map, simple meshes and a ready-to-use character."""
import math
from pathlib import Path
import unreal
from .paths import GENERATED, slug
from .paths import GENERATED, PROJECT, slug
class Scene:
@@ -65,6 +68,41 @@ class Scene:
raise ValueError("Ksztalt: Cube, Sphere, Cylinder, Cone lub Plane.")
return self.mesh(f"/Engine/BasicShapes/{shape}.{shape}", **kwargs)
def model(self, filename, name="Character", height=200, rotation=(0, 0, 0)):
"""Import a model, fit its height (cm), and give it a floor-centered pivot.
Returns a parent actor for transform animation; imported materials stay
on its mesh child. Rotation is (roll, pitch, yaw), applied before fitting.
"""
if height is not None and (not isinstance(height, (int, float)) or
not math.isfinite(height) or height <= 0):
raise ValueError("Wysokosc modelu musi byc dodatnia; None zachowuje rozmiar pliku.")
source = Path(filename).expanduser()
source = source if source.is_absolute() else PROJECT / source
if source.suffix.lower() == ".blend":
from .blender import import_character
mesh = import_character(source, name=name)
else:
from .assets import import_model
mesh = import_model(source, name=name)
actor = self.mesh(mesh, name=name + "Mesh")
roll, pitch, yaw = rotation
actor.set_actor_rotation(unreal.Rotator(roll=roll, pitch=pitch, yaw=yaw), False)
origin, extent = actor.get_actor_bounds(False)
if extent.z <= 0:
self.subsystem.destroy_actor(actor)
raise ValueError("Model ma zerowa wysokosc. Sprawdz geometrie i MODEL_ROTATION.")
if height is not None:
factor = float(height) / (2 * extent.z)
actor.set_actor_scale3d(unreal.Vector(factor, factor, factor))
origin, extent = actor.get_actor_bounds(False)
actor.set_actor_location(unreal.Vector(-origin.x, -origin.y, extent.z - origin.z), False, False)
root = self.spawn(unreal.StaticMeshActor, name)
root.static_mesh_component.set_mobility(unreal.ComponentMobility.MOVABLE)
actor.attach_to_actor(root, "", unreal.AttachmentRule.KEEP_WORLD,
unreal.AttachmentRule.KEEP_WORLD, unreal.AttachmentRule.KEEP_WORLD, False)
return root
def character(self, material=None, head_scale=1.0):
root = self.spawn(unreal.StaticMeshActor, "Character")
root.static_mesh_component.set_mobility(unreal.ComponentMobility.MOVABLE)