blender customization
This commit is contained in:
1 parent
fb39e290c0
commit
8c0a639fba
12 files changed
+264
-8
No files matched your search
@@ -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]
|
||||
@@ -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
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user