better basic example
This commit is contained in:
1 parent
1530b74d04
commit
934067f154
9 files changed
+128
-10
No files matched your search
@@ -138,3 +138,9 @@ class FighterAnimations(AnimationClip):
|
|||||||
"""2.400 seconds at speed=1; clip: two_handed_sword_death."""
|
"""2.400 seconds at speed=1; clip: two_handed_sword_death."""
|
||||||
TWO_HANDED_SWORD_DEATH_2 = ('two_handed_sword_death_2', 2.5999999046325684)
|
TWO_HANDED_SWORD_DEATH_2 = ('two_handed_sword_death_2', 2.5999999046325684)
|
||||||
"""2.600 seconds at speed=1; clip: two_handed_sword_death_2."""
|
"""2.600 seconds at speed=1; clip: two_handed_sword_death_2."""
|
||||||
|
|
||||||
|
|
||||||
|
class MatTestRigAnimations(AnimationClip):
|
||||||
|
"""Clip names and durations: type MatTestRigAnimations. for editor completion."""
|
||||||
|
MATTESTRIG_ANIM = ('MatTestRig_Anim', 10.375)
|
||||||
|
"""10.375 seconds at speed=1; clip: MatTestRig_Anim."""
|
||||||
@@ -6,13 +6,25 @@ import unreal
|
|||||||
from .paths import GENERATED, PROJECT, slug
|
from .paths import GENERATED, PROJECT, slug
|
||||||
|
|
||||||
|
|
||||||
|
def _disable_nanite(mesh):
|
||||||
|
# Workshop renders showed Nanite's coarse fallback mesh instead of the full model.
|
||||||
|
settings = mesh.get_editor_property("nanite_settings")
|
||||||
|
if settings.enabled:
|
||||||
|
settings.enabled = False
|
||||||
|
unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem).set_nanite_settings(mesh, settings, True)
|
||||||
|
unreal.EditorAssetLibrary.save_loaded_asset(mesh)
|
||||||
|
|
||||||
|
|
||||||
def _existing_model(name):
|
def _existing_model(name):
|
||||||
destination = f"{GENERATED}/Imports/{slug(name)}"
|
destination = f"{GENERATED}/Imports/{slug(name)}"
|
||||||
if not unreal.EditorAssetLibrary.does_directory_exist(destination):
|
if not unreal.EditorAssetLibrary.does_directory_exist(destination):
|
||||||
return None
|
return None
|
||||||
meshes = [unreal.load_asset(path) for path in unreal.EditorAssetLibrary.list_assets(destination, recursive=True)]
|
meshes = [unreal.load_asset(path) for path in unreal.EditorAssetLibrary.list_assets(destination, recursive=True)]
|
||||||
meshes = [obj for obj in meshes if isinstance(obj, unreal.StaticMesh)]
|
meshes = [obj for obj in meshes if isinstance(obj, unreal.StaticMesh)]
|
||||||
return meshes[0] if len(meshes) == 1 else None
|
if len(meshes) != 1:
|
||||||
|
return None
|
||||||
|
_disable_nanite(meshes[0])
|
||||||
|
return meshes[0]
|
||||||
|
|
||||||
|
|
||||||
def import_model(filename, name="Character", reimport=True):
|
def import_model(filename, name="Character", reimport=True):
|
||||||
@@ -80,6 +92,7 @@ def import_model(filename, name="Character", reimport=True):
|
|||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Import {source.name}: oczekiwano jednego StaticMesh, otrzymano {len(meshes)}. "
|
f"Import {source.name}: oczekiwano jednego StaticMesh, otrzymano {len(meshes)}. "
|
||||||
"Plik musi zawierac statyczna geometrie modelu.")
|
"Plik musi zawierac statyczna geometrie modelu.")
|
||||||
|
_disable_nanite(meshes[0])
|
||||||
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.
|
||||||
|
|||||||
@@ -185,3 +185,5 @@ Nie sprawdzano retargetingu, Animation Blueprintów ani eksportu wielu akcji/NLA
|
|||||||
- Bez `lighting.environment` odsłonięte tło może być białe, np. przy samej żarówce.
|
- Bez `lighting.environment` odsłonięte tło może być białe, np. przy samej żarówce.
|
||||||
Przy niebie na małej podłodze widać ciemny pas pod horyzontem; mgła go ukrywa.
|
Przy niebie na małej podłodze widać ciemny pas pod horyzontem; mgła go ukrywa.
|
||||||
- `shot.visible` wyłącza też światło: sprawdzone na żarówce między dwiema klatkami.
|
- `shot.visible` wyłącza też światło: sprawdzone na żarówce między dwiema klatkami.
|
||||||
|
- Nanite jest wyłączany przy `assets.import_model` i przy użyciu istniejącego assetu.
|
||||||
|
Z włączonym render pokazywał siatkę zastępczą: około 1064 z 26 496 trójkątów.
|
||||||
+15
-8
@@ -7,22 +7,28 @@ import math
|
|||||||
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
|
from ue_workshop.paths import PROJECT
|
||||||
|
|
||||||
REIMPORT = False # True po zmianie modelu w Blenderze; False uzywa assetu z UE
|
REIMPORT = True # True po zmianie modelu w Blenderze; False uzywa assetu z UE
|
||||||
|
SKY = True # True: niebo i slonce; False: ciemne studio z trzema swiatlami
|
||||||
|
|
||||||
# 1. SCENA. Zmien model, kolor podlogi i swiatla.
|
# 1. SCENA. Zmien model, podloge i swiatla.
|
||||||
scene = Scene("Basics")
|
scene = Scene("Basics")
|
||||||
character = scene.model(
|
character = scene.model(
|
||||||
PROJECT / "source_assets/character/character_custom.blend",
|
PROJECT / "source_assets/character/character_custom.blend",
|
||||||
name="CustomCharacter", height=200, reimport=REIMPORT,
|
name="CustomCharacter", height=200, reimport=REIMPORT,
|
||||||
)
|
)
|
||||||
floor = materials.color("Floor", (0.07, 0.08, 0.11))
|
# tiling=40: tekstura powtorzona 40 razy na kazdej krawedzi podlogi (200 x 200 m).
|
||||||
scene.primitive("Cube", name="Floor", location=(0, 0, -10), scale=(20, 20, 0.2), material=floor)
|
floor = materials.texture_set("TiledFloor", base_color="source_assets/textures/floor_tiles.png", tiling=40)
|
||||||
|
scene.primitive("Cube", name="Floor", location=(0, 0, -10), scale=(200, 200, 0.2), material=floor)
|
||||||
|
if SKY:
|
||||||
|
# Zachod slonca: dodaj sun_angle=5. Mocniejsza mgla: fog=0.05.
|
||||||
|
lighting.environment(scene, sun_intensity=1500, fog=0.01)
|
||||||
|
else:
|
||||||
lighting.studio(scene)
|
lighting.studio(scene)
|
||||||
|
|
||||||
# 2. KAMERA. Jednostki: centymetry; FOV w stopniach.
|
# 2. KAMERA. Jednostki: centymetry; FOV w stopniach.
|
||||||
radius = 550
|
radius = 550
|
||||||
height = 260
|
height = 140 # nisko, zeby w kadrze byl horyzont
|
||||||
target = (0, 0, 100)
|
target = (0, 0, 110)
|
||||||
cam = camera.create(scene, location=(radius, 0, height), target=target, fov=45)
|
cam = camera.create(scene, location=(radius, 0, height), target=target, fov=45)
|
||||||
|
|
||||||
# 3. ANIMACJA. 192 klatki przy 24 FPS = 8 sekund.
|
# 3. ANIMACJA. 192 klatki przy 24 FPS = 8 sekund.
|
||||||
@@ -37,7 +43,8 @@ for frame in range(shot.frames):
|
|||||||
shot.transform_key(cam, frame, location=(x, y, height), rotation=(0, pitch, angle + 180))
|
shot.transform_key(cam, frame, location=(x, y, height), rotation=(0, pitch, angle + 180))
|
||||||
shot.save()
|
shot.save()
|
||||||
|
|
||||||
# 4. RENDER. Najpierw obejrzyj scene w Sequencerze.
|
# 4. RENDER. Najpierw obejrzyj scene w Sequencerze. Muzyke uslyszysz dopiero w MP4.
|
||||||
renderer = Renderer(width=1280, height=720, samples=4, warmup=16, mp4=True)
|
renderer = Renderer(width=1280, height=720, samples=4, warmup=16, mp4=True,
|
||||||
|
audio="source_assets/audio/basics_music.wav")
|
||||||
# renderer.render(shot)
|
# renderer.render(shot)
|
||||||
# Zamiast filmu: renderer.render(shot, single_frame=96)
|
# Zamiast filmu: renderer.render(shot, single_frame=96)
|
||||||
@@ -13,7 +13,7 @@ def main():
|
|||||||
raise RuntimeError("Ten plik uruchom z terminala zwyklym Pythonem, poza edytorem UE.")
|
raise RuntimeError("Ten plik uruchom z terminala zwyklym Pythonem, poza edytorem UE.")
|
||||||
# --render zapewnia film takze wtedy, gdy w my_shot.py jest RENDER=False.
|
# --render zapewnia film takze wtedy, gdy w my_shot.py jest RENDER=False.
|
||||||
# Argumenty launchera, np. --editor i --timeout, mozna dopisac w terminalu.
|
# Argumenty launchera, np. --editor i --timeout, mozna dopisac w terminalu.
|
||||||
script = Path(__file__).with_name("advanced_example.py")
|
script = Path(__file__).with_name("basics.py")
|
||||||
return run_workshop([str(script), "--render", *sys.argv[1:]])
|
return run_workshop([str(script), "--render", *sys.argv[1:]])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 239 KiB |
@@ -227,6 +227,27 @@ class MaterialAssignTests(unittest.TestCase):
|
|||||||
self.module.assign(actor, "red")
|
self.module.assign(actor, "red")
|
||||||
|
|
||||||
|
|
||||||
|
class NaniteTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.unreal = MagicMock()
|
||||||
|
self.module = load(self.unreal, "assets.py")
|
||||||
|
self.mesh = MagicMock()
|
||||||
|
self.settings = self.mesh.get_editor_property.return_value
|
||||||
|
self.subsystem = self.unreal.get_editor_subsystem.return_value
|
||||||
|
|
||||||
|
def test_enabled_nanite_is_disabled_and_saved(self):
|
||||||
|
self.settings.enabled = True
|
||||||
|
self.module._disable_nanite(self.mesh)
|
||||||
|
self.assertFalse(self.settings.enabled)
|
||||||
|
self.subsystem.set_nanite_settings.assert_called_once_with(self.mesh, self.settings, True)
|
||||||
|
self.unreal.EditorAssetLibrary.save_loaded_asset.assert_called_once_with(self.mesh)
|
||||||
|
|
||||||
|
def test_disabled_mesh_is_not_rebuilt(self):
|
||||||
|
self.settings.enabled = False
|
||||||
|
self.module._disable_nanite(self.mesh)
|
||||||
|
self.subsystem.set_nanite_settings.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
class RendererAudioTests(unittest.TestCase):
|
class RendererAudioTests(unittest.TestCase):
|
||||||
def test_audio_requires_mp4(self):
|
def test_audio_requires_mp4(self):
|
||||||
module = load(MagicMock(), "render.py")
|
module = load(MagicMock(), "render.py")
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
"""Generate the floor texture and music used by exercises/basics.py (no licensed sources)."""
|
||||||
|
import array
|
||||||
|
import binascii
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
|
import random
|
||||||
|
import struct
|
||||||
|
import wave
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def save_png(path, size, pixels):
|
||||||
|
def chunk(kind, data):
|
||||||
|
return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", binascii.crc32(kind + data) & 0xffffffff)
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_bytes(b"\x89PNG\r\n\x1a\n" +
|
||||||
|
chunk(b"IHDR", struct.pack(">IIBBBBB", size, size, 8, 2, 0, 0, 0)) +
|
||||||
|
chunk(b"IDAT", zlib.compress(pixels, 9)) + chunk(b"IEND", b""))
|
||||||
|
|
||||||
|
|
||||||
|
def floor_tiles(path, size=512, tiles=4, grout=5):
|
||||||
|
"""Slate tiles that repeat seamlessly: 4x4 in one texture, each with its own shade."""
|
||||||
|
rng = random.Random(7)
|
||||||
|
step = size // tiles
|
||||||
|
shades = [[rng.uniform(0.75, 1.15) for _ in range(tiles)] for _ in range(tiles)]
|
||||||
|
pixels = bytearray()
|
||||||
|
for y in range(size):
|
||||||
|
pixels.append(0)
|
||||||
|
for x in range(size):
|
||||||
|
if x % step < grout or y % step < grout:
|
||||||
|
pixels += bytes((24, 24, 26))
|
||||||
|
else:
|
||||||
|
shade = shades[y // step][x // step] * rng.uniform(0.94, 1.06)
|
||||||
|
pixels += bytes(min(255, int(value * shade)) for value in (92, 100, 112))
|
||||||
|
save_png(path, size, bytes(pixels))
|
||||||
|
|
||||||
|
|
||||||
|
def music(path, seconds=8.0, rate=44100):
|
||||||
|
"""Minor-key pad, bass and arpeggio: two bars at 120 BPM, fading out at the end."""
|
||||||
|
chords = [(220.00, 261.63, 329.63), (174.61, 220.00, 261.63),
|
||||||
|
(261.63, 329.63, 392.00), (196.00, 246.94, 293.66)]
|
||||||
|
beat = 0.5
|
||||||
|
samples = array.array("h")
|
||||||
|
for index in range(int(seconds * rate)):
|
||||||
|
time = index / rate
|
||||||
|
chord = chords[int(time / (beat * 4)) % len(chords)]
|
||||||
|
pad = sum(math.sin(2 * math.pi * frequency * time) for frequency in chord) / 3
|
||||||
|
bass = math.sin(2 * math.pi * chord[0] / 2 * time)
|
||||||
|
step = int(time / (beat / 2))
|
||||||
|
note = chord[step % 3] * 2
|
||||||
|
arpeggio = math.sin(2 * math.pi * note * time) * math.exp(-6 * (time - step * beat / 2))
|
||||||
|
kick_time = time % beat if int(time / beat) % 2 == 0 else 1.0
|
||||||
|
kick = math.sin(2 * math.pi * 60 * kick_time) * math.exp(-18 * kick_time)
|
||||||
|
fade = min(1.0, time / 0.3, (seconds - time) / 1.0)
|
||||||
|
value = 0.28 * pad + 0.22 * bass + 0.2 * arpeggio + 0.3 * kick
|
||||||
|
samples.append(int(32767 * 0.8 * fade * max(-1.0, min(1.0, value))))
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with wave.open(str(path), "wb") as target:
|
||||||
|
target.setnchannels(1)
|
||||||
|
target.setsampwidth(2)
|
||||||
|
target.setframerate(rate)
|
||||||
|
target.writeframes(samples.tobytes())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
floor_tiles(ROOT / "source_assets/textures/floor_tiles.png")
|
||||||
|
music(ROOT / "source_assets/audio/basics_music.wav")
|
||||||
Reference in new issue
Block a user