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 import unreal
from .paths import GENERATED, slug from .paths import GENERATED, PROJECT, slug
def import_model(filename, name="Character"): 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(): if not source.is_file():
raise FileNotFoundError(f"Brak modelu: {source}") raise FileNotFoundError(f"Brak modelu: {source}")
if source.suffix.lower() not in {".glb", ".gltf", ".fbx", ".obj"}: 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( pipeline = unreal.AssetToolsHelpers.get_asset_tools().duplicate_asset(
"GLTFWorkshop", GENERATED + "/Pipelines", template) "GLTFWorkshop", GENERATED + "/Pipelines", template)
pipeline.set_editor_property("reimport_strategy", unreal.ReimportStrategyFlags.APPLY_PIPELINE_PROPERTIES) 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) pipeline.material_pipeline.set_editor_property("import_materials", True)
# PBR images are imported explicitly into WorkshopTextures below. This # PBR images are imported explicitly into WorkshopTextures below. This
# avoids Interchange's dependency-preserving material reimport behavior. # 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(pipeline.get_path_name()),
unreal.SoftObjectPath("/Interchange/Pipelines/DefaultGLTFPipeline.DefaultGLTFPipeline")]) unreal.SoftObjectPath("/Interchange/Pipelines/DefaultGLTFPipeline.DefaultGLTFPipeline")])
task.set_editor_property("options", options) task.set_editor_property("options", options)
if source.suffix.lower() == ".fbx": if source.suffix.lower() in {".fbx", ".obj"}:
options = unreal.FbxImportUI() options = unreal.FbxImportUI()
options.set_editor_property("import_mesh", True)
options.set_editor_property("import_as_skeletal", False) 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("mesh_type_to_import", unreal.FBXImportType.FBXIT_STATIC_MESH)
options.set_editor_property("automated_import_should_detect_type", False) options.set_editor_property("automated_import_should_detect_type", False)
options.static_mesh_import_data.set_editor_property("combine_meshes", True) 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: if len(meshes) != 1:
raise RuntimeError( raise RuntimeError(
f"Import {source.name}: oczekiwano jednego StaticMesh, otrzymano {len(meshes)}. " 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"}: if source.suffix.lower() in {".glb", ".gltf"}:
# UE preserves material edits on mesh reimport. In this workflow Blender # UE preserves material edits on mesh reimport. In this workflow Blender
# owns the look; explicitly synchronize the supported PBR parameters. # owns the look; explicitly synchronize the supported PBR parameters.
from ._gltf import link_textures from ._gltf import link_textures
link_textures(source, meshes[0], destination) 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] return meshes[0]
+57
View File
@@ -1,6 +1,8 @@
"""Small opaque PBR materials, generated without external assets.""" """Small opaque PBR materials, generated without external assets."""
import unreal import unreal
from pathlib import Path
from .paths import GENERATED, slug from .paths import GENERATED, slug
@@ -32,3 +34,58 @@ def assign(actor, material, slot=0):
if component is None: if component is None:
raise TypeError("Material mozna przypisac do aktora ze StaticMeshComponent.") raise TypeError("Material mozna przypisac do aktora ze StaticMeshComponent.")
component.set_material(slot, material) 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): def executable(name, explicit=None):
candidate = explicit or os.environ.get("WORKSHOP_" + name.upper()) or shutil.which(name) 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(): if candidate and Path(candidate).is_file():
return str(Path(candidate).resolve()) return str(Path(candidate).resolve())
raise FileNotFoundError( raise FileNotFoundError(
+39 -1
View File
@@ -1,7 +1,10 @@
"""An isolated generated map, simple meshes and a ready-to-use character.""" """An isolated generated map, simple meshes and a ready-to-use character."""
import math
from pathlib import Path
import unreal import unreal
from .paths import GENERATED, slug from .paths import GENERATED, PROJECT, slug
class Scene: class Scene:
@@ -65,6 +68,41 @@ class Scene:
raise ValueError("Ksztalt: Cube, Sphere, Cylinder, Cone lub Plane.") raise ValueError("Ksztalt: Cube, Sphere, Cylinder, Cone lub Plane.")
return self.mesh(f"/Engine/BasicShapes/{shape}.{shape}", **kwargs) 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): def character(self, material=None, head_scale=1.0):
root = self.spawn(unreal.StaticMeshActor, "Character") root = self.spawn(unreal.StaticMeshActor, "Character")
root.static_mesh_component.set_mobility(unreal.ComponentMobility.MOVABLE) root.static_mesh_component.set_mobility(unreal.ComponentMobility.MOVABLE)
+56
View File
@@ -128,8 +128,64 @@ python3 tools/run_workshop.py --check --blender
Ścieżki nie są zapisane na sztywno w bibliotece. Launcher ma domyślną lokalizację Ścieżki nie są zapisane na sztywno w bibliotece. Launcher ma domyślną lokalizację
instalacji UE 5.7 na Windows; na Linuksie korzysta ze zmiennej lub PATH. instalacji UE 5.7 na Windows; na Linuksie korzysta ze zmiennej lub PATH.
## Wybór postaci i model pobrany z internetu
W `exercises/my_shot.py` wybierz `MODEL`:
| Wartość | Postać |
|---|---|
| `"primitive"` | Dotychczasowa postać z brył, bez importu. |
| `"robot"` | RobotPolice z dołączonego `DemoAssets`. |
| `"pilot"` | Humanoid z `DemoAssets/pilot`. |
| `"file"` | Własny plik wskazany w `MODEL_FILE`. |
Przykład własnego modelu:
```python
MODEL = "file"
MODEL_FILE = PROJECT / "source_assets/models/my_character.glb"
MODEL_HEIGHT = 200
MODEL_ROTATION = (0, 0, 90)
```
Rozpakuj pobrany model razem z teksturami. Możesz użyć ścieżki absolutnej
albo ścieżki względem projektu. GLB z osadzonymi teksturami PNG/JPEG pozwala
przenieść podstawowe materiały PBR w jednym pliku. FBX i OBJ wymagają zachowania
plików tekstur i powiązanych FBM/MTL. Nie wczytujemy ZIP ani plików `.uasset`.
Import zawsze odbywa się do wydzielonego katalogu generowanych assetów.
Biblioteka łączy części modelu, ustawia jego wysokość w centymetrach i przesuwa
podstawę na Z=0. `MODEL_HEIGHT=None` zachowuje rozmiar źródłowy. Jeśli model leży
na boku, popraw `MODEL_ROTATION` (roll, pitch, yaw); obrót jest stosowany przed
dopasowaniem wysokości. Pivot animacji znajduje się na środku podstawy.
Dla pojedynczego atlasu FBX/OBJ można dołączyć mapy `NazwaModelu_BC.png`,
`_N`, `_R`, `_M`, `_E` (również JPG/JPEG). Biblioteka odtworzy z nich jeden
materiał dla wszystkich slotów. Robot i pilot korzystają z tej konwencji.
Bez pliku `_BC` pozostają materiały zaimportowane przez UE. W GLB zachowywany
jest osobny materiał dla każdego slotu.
Po zmianie postaci reszta skryptu działa tak samo: klucze `location`, `rotation`
i `scale` animują cały model. W przykładzie jest też opcjonalny podskok.
Modele z rigami są w tej wersji importowane jako statyczna geometria:
animacje kości, chodzenie z Mixamo i retargeting nie są jeszcze obsługiwane.
Wybrany model działa zarówno w edytorze, jak i przez `my_shot_headless.py`.
## Blender → UE → film ## Blender → UE → film
Własną kopię wybierzesz też bezpośrednio w `my_shot.py`:
```python
MODEL = "file"
MODEL_FILE = PROJECT / "source_assets/character/character_custom.blend"
```
Zapisz zmiany w Blenderze i ponownie uruchom ćwiczenie. Za każdym razem plik
jest eksportowany w tle i aktualizuje ten sam asset `CustomCharacter` w UE.
Nie trzeba ręcznie usuwać poprzedniego modelu. Eksport obejmuje kolekcję `Character`.
Na Windows biblioteka wykrywa też standardowe instalacje w `Blender Foundation`,
jeśli Blender nie jest w PATH; `WORKSHOP_BLENDER` pozwala wybrać inną instalację.
Przykładowy model znajduje się w `source_assets/character/character.blend`. Przykładowy model znajduje się w `source_assets/character/character.blend`.
Można go też odtworzyć (generator nie nadpisuje istniejącego pliku): Można go też odtworzyć (generator nie nadpisuje istniejącego pliku):
+6
View File
@@ -11,6 +11,8 @@ Import: `from ue_workshop import Scene, Sequence, Renderer, camera, lighting, ma
| Import statycznego modelu | `mesh = assets.import_model("C:/models/model.glb", name="MyModel")` | | Import statycznego modelu | `mesh = assets.import_model("C:/models/model.glb", name="MyModel")` |
| Eksport + import Blendera | `mesh = blender.import_character("C:/models/character.blend")` | | Eksport + import Blendera | `mesh = blender.import_character("C:/models/character.blend")` |
| Model na scenie | `actor = scene.mesh(mesh, name="Hero")` | | Model na scenie | `actor = scene.mesh(mesh, name="Hero")` |
| Import z dopasowaniem rozmiaru i pivotu | `actor = scene.model("source_assets/models/hero.glb", name="Hero", height=200)` |
| Korekta osi modelu przed dopasowaniem | `actor = scene.model("hero.fbx", rotation=(0, 0, 90))` |
| Światła studyjne | `lights = lighting.studio(scene)` | | Światła studyjne | `lights = lighting.studio(scene)` |
| Pojedyncze światło | `lighting.area(scene, location=(200, -200, 350), intensity=10000, color=(1, 0.5, 0.2))` | | Pojedyncze światło | `lighting.area(scene, location=(200, -200, 350), intensity=10000, color=(1, 0.5, 0.2))` |
| Kamera | `cam = camera.create(scene, location=(450, -550, 300), target=(0, 0, 100), focal_length=45)` | | Kamera | `cam = camera.create(scene, location=(450, -550, 300), target=(0, 0, 100), focal_length=45)` |
@@ -35,5 +37,9 @@ Cały obrót: klucze 0 i 360, nie 0 i 0. Kamera i modele to oryginalne obiekty U
możesz wywoływać ich metody, np. `actor.set_actor_label("MyHero")`. możesz wywoływać ich metody, np. `actor.set_actor_label("MyHero")`.
`shot.asset` to oryginalny LevelSequence, `shot.bind(actor)` zwraca binding UE. `shot.asset` to oryginalny LevelSequence, `shot.bind(actor)` zwraca binding UE.
W `my_shot.py`: `MODEL="robot"` lub `MODEL="pilot"` wybiera dołączony model.
`MODEL="file"` wczytuje `MODEL_FILE`. Obiekt z `scene.model()` ma pivot na podstawie
i zachowuje materiały; animacja dotyczy całego modelu, nie jego kości.
Przy pojedynczym podglądzie nie uruchamiaj dodatkowo pełnego renderu: Przy pojedynczym podglądzie nie uruchamiaj dodatkowo pełnego renderu:
jednocześnie może pracować tylko jeden renderer warsztatowy. jednocześnie może pracować tylko jeden renderer warsztatowy.
+7
View File
@@ -27,6 +27,13 @@ Nie uruchamiano testów na Linuksie ani na Blenderze 4.x.
StaticMesh pod tą samą ścieżką po zmianie geometrii. StaticMesh pod tą samą ścieżką po zmianie geometrii.
- Zmiana koloru materiału oraz dodanie osadzonej tekstury: sprawdzone wartości - Zmiana koloru materiału oraz dodanie osadzonej tekstury: sprawdzone wartości
parametrów UE, przypisanie tekstury i render do MP4. parametrów UE, przypisanie tekstury i render do MP4.
- Import lokalnych RobotPolice i pilot z FBX, automatyczne dopasowanie wysokości
do 200 cm i ustawienie podstawy na Z=0. Odtworzenie materiałów z tekstur PBR.
- Pełny `my_shot.py` z robotem: 48 klatek 1920×1080, MP4 i poprawne zamknięcie UE.
- GLB po włączeniu łączenia części modelu: zaliczony test eksportu/importu
Blendera, aktualizacji materiałów i tekstury oraz renderu.
- `character_custom.blend`: dwukrotne wykonanie `my_shot.py`, dwa eksporty
Blendera do GLB i aktualizacja tego samego assetu UE; podgląd renderu zaliczony.
Uruchamianie sprawdzeń jest opisane w README. Wyniki każdego renderu są zapisane Uruchamianie sprawdzeń jest opisane w README. Wyniki każdego renderu są zapisane
w jego `result.json`; logi UE znajdują się w `Saved/Logs`. w jego `result.json`; logi UE znajdują się w `Saved/Logs`.
+24 -2
View File
@@ -17,12 +17,30 @@ if "ue_workshop" in sys.modules:
importlib.reload(sys.modules["ue_workshop"]) importlib.reload(sys.modules["ue_workshop"])
from ue_workshop import Scene, Sequence, Renderer, camera, lighting, materials from ue_workshop import Scene, Sequence, Renderer, camera, lighting, materials
from ue_workshop.paths import PROJECT
# WYBOR POSTACI: "primitive", "robot", "pilot" lub "file".
MODEL = "file"
MODEL_FILE = PROJECT / "source_assets/character/character_custom.blend"
MODEL_HEIGHT = 200 # centymetry; None zachowuje oryginalny rozmiar
MODEL_ROTATION = (0, 0, 0) # roll, pitch, yaw; np. (0, 0, 90) obraca model
# 1. MODEL I SCENA. Jednostki: centymetry. Os Z wskazuje gore. # 1. MODEL I SCENA. Jednostki: centymetry. Os Z wskazuje gore.
scene = Scene("MyShot") scene = Scene("MyShot")
blue = materials.color("Character", rgb=(0.04, 0.35, 0.8), metallic=0.2)
floor = materials.color("Floor", rgb=(0.07, 0.08, 0.11)) floor = materials.color("Floor", rgb=(0.07, 0.08, 0.11))
character = scene.character(material=blue, head_scale=1.0) if MODEL == "primitive":
blue = materials.color("Character", rgb=(0.04, 0.35, 0.8), metallic=0.2)
character = scene.character(material=blue, head_scale=1.0)
else:
sources = {
"robot": (PROJECT / "DemoAssets/RobotPolice/RobotPolice.fbx", "RobotPolice"),
"pilot": (PROJECT / "DemoAssets/pilot/pilot.fbx", "Pilot"),
"file": (MODEL_FILE, "CustomCharacter"),
}
if MODEL not in sources:
raise ValueError("MODEL: primitive, robot, pilot lub file.")
source, name = sources[MODEL]
character = scene.model(source, name=name, height=MODEL_HEIGHT, rotation=MODEL_ROTATION)
scene.primitive("Cube", name="Floor", location=(0, 0, -10), scale=(20, 20, 0.2), material=floor) scene.primitive("Cube", name="Floor", location=(0, 0, -10), scale=(20, 20, 0.2), material=floor)
lighting.studio(scene, key_color=(1.0, 0.75, 0.5), fill_color=(0.3, 0.6, 1.0)) lighting.studio(scene, key_color=(1.0, 0.75, 0.5), fill_color=(0.3, 0.6, 1.0))
cam = camera.create(scene, location=(450, -550, 300), focal_length=45) cam = camera.create(scene, location=(450, -550, 300), focal_length=45)
@@ -33,6 +51,10 @@ shot.camera(cam)
# Rotacja: (roll, pitch, yaw), w stopniach. # Rotacja: (roll, pitch, yaw), w stopniach.
shot.transform_key(character, 0, rotation=(0, 0, 0)) shot.transform_key(character, 0, rotation=(0, 0, 0))
shot.transform_key(character, 47, rotation=(0, 0, 360)) shot.transform_key(character, 47, rotation=(0, 0, 360))
# Opcjonalny podskok (dziala tak samo dla robota i wlasnego modelu):
# shot.transform_key(character, 0, location=(0, 0, 0), interpolation="auto")
# shot.transform_key(character, 24, location=(0, 0, 80), interpolation="auto")
# shot.transform_key(character, 47, location=(0, 0, 0), interpolation="auto")
shot.save() shot.save()
# 3. WYNIK. Najpierw obejrzyj animacje w Sequencerze, potem zmien RENDER na True. # 3. WYNIK. Najpierw obejrzyj animacje w Sequencerze, potem zmien RENDER na True.
Binary file not shown.
+13
View File
@@ -0,0 +1,13 @@
Tutaj możesz wrzucić rozpakowany model i jego tekstury.
W `exercises/my_shot.py` ustaw `MODEL = "file"`, a w `MODEL_FILE` wskaż plik.
Ścieżka może być względem projektu albo absolutna, np. `"D:/Modele/hero.glb"`.
Najwygodniejszy jest GLB z osadzonymi teksturami PBR PNG/JPEG.
FBX i OBJ wymagają zachowania plików tekstur oraz powiązanych plików MTL/FBM.
ZIP trzeba wcześniej rozpakować. Plik `.blend` wymaga zainstalowanego Blendera
i kolekcji `Character`, zgodnie z przykładem w README projektu.
Model jest importowany jako geometria statyczna. Można animować jego pozycję,
obrót i skalę. Rig, retargeting i animacje chodzenia nie są obecnie importowane.
Tekstury i nazwy materiałów powinny być zachowane w eksporcie; obsługujemy
podstawowe PBR, a nie dowolne shadery konkretnego programu.
+1 -1
View File
@@ -66,7 +66,7 @@ def main(argv=None):
command = [editor, str(ROOT / "ExampleProject.uproject"), command = [editor, str(ROOT / "ExampleProject.uproject"),
"-ExecutePythonScript=" + (ROOT / "tools/ue_bootstrap.py").as_posix(), "-ExecutePythonScript=" + (ROOT / "tools/ue_bootstrap.py").as_posix(),
"-RenderOffscreen", "-unattended", "-nosplash", "-nosound", "-stdout", "-RenderOffscreen", "-unattended", "-nosplash", "-nosound", "-stdout",
"-FullStdOutLogOutput", "-NoLoadingScreen"] "-FullStdOutLogOutput", "-NoLoadingScreen", "-nocef"]
if args.verbose: if args.verbose:
# UE writes its own log when console output is streamed directly. # UE writes its own log when console output is streamed directly.
command.append("-abslog=" + log_path.as_posix()) command.append("-abslog=" + log_path.as_posix())
+31
View File
@@ -0,0 +1,31 @@
"""Check local FBX characters, automatic fitting, stable pivots and MRQ."""
import unreal
from ue_workshop import Scene, Sequence, Renderer, camera, lighting
from ue_workshop.paths import PROJECT
scene = Scene("ImportedModelsSmoke")
characters = []
for name, file in (("RobotSmoke", "DemoAssets/RobotPolice/RobotPolice.fbx"),
("PilotSmoke", "DemoAssets/pilot/pilot.fbx")):
root = scene.model(PROJECT / file, name=name, height=200)
child = root.get_attached_actors()[0]
origin, extent = child.get_actor_bounds(False)
assert abs(2 * extent.z - 200) < 0.1, (name, origin, extent)
assert abs(origin.z - extent.z) < 0.1, (name, origin, extent)
assert abs(origin.x) < 0.1 and abs(origin.y) < 0.1
mesh = child.static_mesh_component.static_mesh
assert len(mesh.static_materials) > 0
assert all(slot.material_interface for slot in mesh.static_materials)
assert all(slot.material_interface.get_name() == "M_" + name + "Imported" for slot in mesh.static_materials)
characters.append(root)
characters[1].set_actor_location(unreal.Vector(0, 160, 0), False, False)
scene.primitive("Cube", name="Floor", location=(0, 0, -10), scale=(20, 20, 0.2))
lighting.studio(scene)
cam = camera.create(scene, location=(600, -850, 400), target=(0, 70, 100))
shot = Sequence(scene, frames=2)
shot.camera(cam)
for character in characters:
shot.transform_key(character, 0, rotation=(0, 0, 0))
shot.transform_key(character, 1, rotation=(0, 0, 45))
Renderer(width=640, height=360, samples=2, warmup=8).render(shot)