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
|
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]
|
||||||
@@ -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
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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
@@ -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.
@@ -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.
|
||||||
@@ -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())
|
||||||
|
|||||||
@@ -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)
|
||||||
Reference in new issue
Block a user