UE 自動シーケンサー作成 キャラ 自動レンダリングのpythonを作った

I created a Python script for Unreal Engine that automatically generates sequences and renders characters.

“H:\SandBox\UE583SequenceRender\run_auto_render.bat”

@echo off
rem Auto Sequence Render: launch UE -> build sequences -> render (MRQ) -> MP4 -> quit
rem Usage: run_auto_render.bat [config.json]
setlocal EnableExtensions

rem ===================== Settings (edit per PC) =====================
rem Leave empty for auto-detect. Write WITHOUT extra quotes inside:  set "X=C:\path"
rem   USER_UE_EDITOR : UnrealEditor.exe (needed for source/custom engine builds)
rem   USER_PROJECT   : .uproject to open (default: UE583SequenceRender next to this bat)
set "USER_UE_EDITOR="
set "USER_PROJECT="
rem ==================================================================

set "ROOT=%~dp0"
set "PROJECT=%ROOT%UE583SequenceRender\UE583SequenceRender.uproject"
if defined USER_PROJECT set "PROJECT=%USER_PROJECT%"
if defined USER_UE_EDITOR set "UE_EDITOR=%USER_UE_EDITOR%"
rem Strip quotes in case they were written as  set X="C:\path"
set "PROJECT=%PROJECT:"=%"
if defined UE_EDITOR set "UE_EDITOR=%UE_EDITOR:"=%"
for %%p in ("%PROJECT%") do set "PROJ_DIR=%%~dpp"
set "SCRIPT=%ROOT%auto_sequence_render.py"
set "CONFIG=%~1"
if "%CONFIG%"=="" set "CONFIG=sequence_jobs.json"
set "DIAG=%ROOT%run_auto_render_last.txt"

if not exist "%PROJECT%" goto :err_project
if not exist "%SCRIPT%" goto :err_script

rem --- Engine version from the .uproject (EngineAssociation) ---
set "UE_VER="
for /f "usebackq delims=" %%v in (`powershell -NoProfile -Command "(Get-Content -LiteralPath $env:PROJECT -Raw | ConvertFrom-Json).EngineAssociation"`) do set "UE_VER=%%v"
if "%UE_VER%"=="" set "UE_VER=5.8"

rem --- Find UnrealEditor.exe: env UE_EDITOR -> Epic Launcher install list -> common paths ---
if defined UE_EDITOR if exist "%UE_EDITOR%" goto :found_ue
set "UE_EDITOR="
for /f "usebackq delims=" %%i in (`powershell -NoProfile -Command "$f='C:\ProgramData\Epic\UnrealEngineLauncher\LauncherInstalled.dat'; if (Test-Path $f) { ((Get-Content $f -Raw | ConvertFrom-Json).InstallationList | Where-Object { $_.AppName -eq ('UE_' + $env:UE_VER) } | Select-Object -First 1).InstallLocation }"`) do set "UE_ROOT=%%i"
if defined UE_ROOT set "UE_EDITOR=%UE_ROOT%\Engine\Binaries\Win64\UnrealEditor.exe"
if defined UE_EDITOR if exist "%UE_EDITOR%" goto :found_ue
for %%d in ("C:\Program Files\Epic Games" "D:\Program Files\Epic Games" "E:\Program Files\Epic Games" "C:\Epic Games" "D:\Epic Games") do (
  if exist "%%~d\UE_%UE_VER%\Engine\Binaries\Win64\UnrealEditor.exe" set "UE_EDITOR=%%~d\UE_%UE_VER%\Engine\Binaries\Win64\UnrealEditor.exe"
)
if defined UE_EDITOR if exist "%UE_EDITOR%" goto :found_ue
goto :err_ue

:found_ue
rem UE treats backslashes in -ExecutePythonScript as escapes (\U...), so pass forward slashes
set "SCRIPT_FS=%SCRIPT:\=/%"
set "CONFIG_FS=%CONFIG:\=/%"
set "PROJECT_FS=%PROJECT:\=/%"

> "%DIAG%" echo [run_auto_render] %DATE% %TIME%
>>"%DIAG%" echo UE_VER    = %UE_VER%
>>"%DIAG%" echo UE_EDITOR = %UE_EDITOR%
>>"%DIAG%" echo PROJECT   = %PROJECT_FS%
>>"%DIAG%" echo SCRIPT    = %SCRIPT_FS%
>>"%DIAG%" echo CONFIG    = %CONFIG_FS%
type "%DIAG%"

powershell -NoProfile -Command "if ($env:PROJECT -match '[^\x00-\x7F]') { exit 1 }"
if errorlevel 1 echo [WARN] The project path contains non-ASCII characters. If UE opens the Project Browser, move the folder to an ASCII-only path such as C:\UEWork\

rem -noxgeshadercompile: compile shaders locally (an idle IncrediBuild/XGE install can stall "Compiling Shaders")
echo Launching Unreal Editor... (render runs automatically, editor quits when done)
"%UE_EDITOR%" "%PROJECT_FS%" -ExecutePythonScript="%SCRIPT_FS% --config %CONFIG_FS% --quit" -noxgeshadercompile -log
echo Done. Output: %PROJ_DIR%Saved\AutoRender
goto :eof

:err_project
echo [ERROR] .uproject not found. Keep this folder layout:
echo   ^<folder^>\run_auto_render.bat
echo   ^<folder^>\auto_sequence_render.py
echo   ^<folder^>\sequence_jobs.json
echo   ^<folder^>\UE583SequenceRender\UE583SequenceRender.uproject
echo Looked for: "%PROJECT%"
echo Or set USER_PROJECT at the top of this bat.
pause
exit /b 1

:err_script
echo [ERROR] auto_sequence_render.py not found next to this bat: "%SCRIPT%"
pause
exit /b 1

:err_ue
echo [ERROR] UnrealEditor.exe for UE %UE_VER% not found.
echo Set it manually, e.g.:  set "UE_EDITOR=C:\Program Files\Epic Games\UE_%UE_VER%\Engine\Binaries\Win64\UnrealEditor.exe"
pause
exit /b 1

“H:\SandBox\UE583SequenceRender\auto_sequence_render.py”

# -*- coding: utf-8 -*-
"""
UE 5.8 自動シーケンサー作成 + キャラクター自動レンダリング

使い方:
  (A) エディタ内:  Output Log の Python で
        import auto_sequence_render as asr; asr.main()
      または  py "H:/SandBox/UE583SequenceRender/auto_sequence_render.py" --config sequence_jobs.json
  (B) バッチ起動:  run_auto_render.bat  (エディタを起動 → 生成 → レンダー → 終了)
  (C) 選択して撮影:  コンテンツブラウザで BP_ThirdPersonCharacter 等 (+任意で AnimSequence) を選択、
      またはレベル上のキャラを選択 → 右クリック「自動撮影」 (init_unreal.py がメニュー登録)
        import auto_sequence_render as asr; asr.run_selected()

引数:
  --config <path>   ジョブ定義 JSON (省略時はこの .py と同じフォルダの sequence_jobs.json, 無ければ DEFAULT_CONFIG)
  --no-render       シーケンス生成のみ
  --quit            レンダー完了後にエディタを終了
"""
import json
import glob
import math
import os
import re
import shutil
import subprocess
import sys

import unreal

# ---------------------------------------------------------------------------
# デフォルト設定 (JSON が無い場合に使用)
# ---------------------------------------------------------------------------
DEFAULT_CONFIG = {
    "map": "/Game/ThirdPerson/Lvl_ThirdPerson",
    "sequence_dir": "/Game/AutoSequences",
    "output_dir": "{project_dir}/Saved/AutoRender",
    "fps": 30,
    "resolution": [1920, 1080],
    "output_format": "png",          # png / jpg / exr / bmp
    # レンダー後に ffmpeg で連番 → MP4 (H.264) に変換
    "mp4": {
        "enabled": True,
        "ffmpeg": "",                # 空なら PATH / 既知の場所から自動検出
        "codec": "libx264",
        "crf": 18,                   # 小さいほど高画質 (18 ≒ 見た目ロスレス)
        "preset": "medium",
        "keep_images": True,         # False なら変換成功後に連番画像を削除
    },
    "spatial_samples": 1,
    "temporal_samples": 1,
    "warmup_frames": 10,
    "jobs": [
        {
            "name": "SEQ_Manny_Attack",
            "characters": [
                {
                    "mesh": "/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple",
                    "anims": [
                        "/Game/Characters/Mannequins/Anims/Unarmed/MM_Idle",
                        "/Game/Characters/Mannequins/Anims/Unarmed/Attack/MM_Attack_01",
                        "/Game/Characters/Mannequins/Anims/Unarmed/Attack/MM_Attack_02",
                    ],
                    "location": [0, 0, 0],
                    "yaw": 0,
                }
            ],
            "camera": {"type": "orbit", "sweep": 360, "start_angle": 0, "orbit_seconds": 8,
                   "radius": "auto", "framing_margin": 1.15, "elevation": 5, "focal_length": 35},
        },
    ],
    # 選択撮影 (run_selected) 用テンプレート。アニメを選択していればそちらを優先
    "selection_job": {
        "name_prefix": "SEQ_Auto_",
        "anims": [
            "/Game/Characters/Mannequins/Anims/Unarmed/MM_Idle",
            "/Game/Characters/Mannequins/Anims/Unarmed/Attack/MM_Attack_01",
        ],
        "duration_frames_without_anims": 150,
        "camera": {"type": "orbit", "sweep": 360, "start_angle": 0, "orbit_seconds": 8,
                   "radius": "auto", "framing_margin": 1.15, "elevation": 5, "focal_length": 35},
    },
}

_RENDER_STATE = {}  # executor を GC から守るためのグローバル参照


# ---------------------------------------------------------------------------
# ユーティリティ
# ---------------------------------------------------------------------------
def log(msg):
    unreal.log("[AutoSeq] " + str(msg))


def warn(msg):
    unreal.log_warning("[AutoSeq] " + str(msg))


def project_dir():
    return os.path.abspath(unreal.Paths.project_dir())


def load_asset(path):
    asset = unreal.EditorAssetLibrary.load_asset(path)
    if asset is None:
        raise RuntimeError("アセットが見つかりません: " + path)
    return asset


def vec(v):
    return unreal.Vector(float(v[0]), float(v[1]), float(v[2]))


def parse_args(argv):
    args = {"config": None, "render": True, "quit": False, "selected": False}
    i = 0
    while i < len(argv):
        a = argv[i]
        if a == "--config" and i + 1 < len(argv):
            args["config"] = argv[i + 1]
            i += 1
        elif a == "--no-render":
            args["render"] = False
        elif a == "--quit":
            args["quit"] = True
        elif a == "--selected":
            args["selected"] = True
        i += 1
    return args


def load_config(path=None):
    if path is None:
        candidate = os.path.join(os.path.dirname(os.path.abspath(__file__)), "sequence_jobs.json")
        path = candidate if os.path.exists(candidate) else None
    if path is None:
        log("JSON が無いため DEFAULT_CONFIG を使用")
        return dict(DEFAULT_CONFIG)
    if not os.path.isabs(path):
        path = os.path.join(os.path.dirname(os.path.abspath(__file__)), path)
    with open(path, "r", encoding="utf-8") as f:
        cfg = json.load(f)
    merged = dict(DEFAULT_CONFIG)
    merged.update(cfg)
    log("設定読み込み: " + path)
    return merged


def anim_length_seconds(anim):
    try:
        return float(anim.get_play_length())
    except Exception:
        return float(unreal.AnimationLibrary.get_sequence_length(anim))


def get_channels(section):
    try:
        return section.get_all_channels()
    except AttributeError:
        return section.get_channels()


def binding_id(sequence, binding):
    try:
        return unreal.MovieSceneSequenceExtensions.get_portable_binding_id(sequence, sequence, binding)
    except Exception:
        return sequence.get_binding_id(binding)


# ---------------------------------------------------------------------------
# シーケンス生成
# ---------------------------------------------------------------------------
def create_sequence_asset(seq_dir, name):
    full = "{}/{}".format(seq_dir, name)
    if unreal.EditorAssetLibrary.does_asset_exist(full):
        unreal.LevelSequenceEditorBlueprintLibrary.close_level_sequence()
        unreal.EditorAssetLibrary.delete_asset(full)
    tools = unreal.AssetToolsHelpers.get_asset_tools()
    seq = tools.create_asset(name, seq_dir, unreal.LevelSequence, unreal.LevelSequenceFactoryNew())
    if seq is None:
        raise RuntimeError("LevelSequence 作成失敗: " + full)
    return seq


def add_spawnable(sequence, actor):
    """UE5.5+ はシーケンスを Sequencer で開いた状態で Subsystem 経由で追加する。"""
    binding = None
    try:
        subsystem = unreal.get_editor_subsystem(unreal.LevelSequenceEditorSubsystem)
        binding = subsystem.add_spawnable_from_instance(sequence, actor)
    except Exception as e:
        warn("Subsystem で spawnable 追加失敗: {}".format(e))
    if binding is None or not binding.is_valid():
        binding = sequence.add_spawnable_from_instance(actor)  # 旧 API フォールバック
    remove_auto_tracks(binding)
    return binding


def remove_auto_tracks(binding):
    """Sequencer がバインド時に自動追加する空の Transform トラックを削除。
    残すと自前の Transform トラックと平均ブレンドされ、位置・向きが半分になる。"""
    for t in list(binding.find_tracks_by_type(unreal.MovieScene3DTransformTrack)):
        binding.remove_track(t)


def cleanup_auto_bindings(sequence):
    """UE 5.8 が SkeletalMesh に自動追加する FK Control Rig トラックを削除し、空になった子バインドも消す。"""
    cr_cls = getattr(unreal, "MovieSceneControlRigParameterTrack", None)
    if cr_cls is None:
        return
    # BP はメッシュの子バインド、SkeletalMeshActor はアクターのバインド自体に付くので全バインドを見る
    for b in list(sequence.get_bindings()):
        for t in list(b.find_tracks_by_type(cr_cls)):
            b.remove_track(t)
            log("  自動追加の Control Rig トラックを削除: {}".format(b.get_display_name()))
    for b in list(sequence.get_possessables()):
        if not b.get_tracks() and not b.get_child_possessables():
            b.remove()


def add_transform_keys(binding, keys, linear=False):
    """keys: [(frame, location(Vector), rotation(Rotator)), ...]
    linear=True で等速 (カメラの周回用)"""
    track = binding.add_track(unreal.MovieScene3DTransformTrack)
    section = track.add_section()
    section.set_range(keys[0][0], keys[-1][0] + 1)
    ch = get_channels(section)
    interp = unreal.MovieSceneKeyInterpolation.LINEAR if linear else unreal.MovieSceneKeyInterpolation.AUTO
    for frame, loc, rot in keys:
        fn = unreal.FrameNumber(int(frame))
        for idx, val in ((0, loc.x), (1, loc.y), (2, loc.z),
                         (3, rot.roll), (4, rot.pitch), (5, rot.yaw)):
            ch[idx].add_key(fn, float(val), 0.0, unreal.MovieSceneTimeUnit.DISPLAY_RATE, interp)
    return section


def capsule_half_height(actor):
    comp = actor.get_component_by_class(unreal.CapsuleComponent)
    return float(comp.get_scaled_capsule_half_height()) if comp else 0.0


def character_height(actor):
    """キャラの身長 (cm)。Character はカプセル、それ以外は SkeletalMesh のバウンズから。"""
    hh = capsule_half_height(actor)
    if hh > 0:
        return hh * 2.0
    comp = actor.get_component_by_class(unreal.SkeletalMeshComponent)
    if comp:
        try:
            bmin, bmax = comp.get_local_bounds()
            return float(bmax.z - bmin.z) * float(comp.get_world_scale().z)
        except Exception:
            pass
    return 180.0


def facing_yaw(actor):
    """キャラ正面の yaw。Mannequin メッシュ単体は +Y 向きなので +90。"""
    yaw = actor.get_actor_rotation().yaw
    return yaw + 90.0 if isinstance(actor, unreal.SkeletalMeshActor) else yaw


def snap_to_floor(feet, up=500.0, down=5000.0):
    """足元 XY の真下の床に Z を合わせる (床や台の中に埋まらないように)。"""
    world = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem).get_editor_world()
    start = unreal.Vector(feet.x, feet.y, feet.z + up)
    end = unreal.Vector(feet.x, feet.y, feet.z - down)
    try:
        trace = unreal.TraceTypeQuery.ECC_VISIBILITY
    except AttributeError:
        trace = unreal.TraceTypeQuery.TRACE_TYPE_QUERY1
    hit = unreal.SystemLibrary.line_trace_single(world, start, end, trace, False, [],
                                                 unreal.DrawDebugTrace.NONE, True)
    if hit:
        z = hit.to_tuple()[4].z
        if abs(z - feet.z) > 1.0:
            log("  床にスナップ: Z {:.0f} -> {:.0f}".format(feet.z, z))
        return unreal.Vector(feet.x, feet.y, z)
    warn("  床が見つかりません (Z はそのまま): {}".format(feet))
    return feet


def add_character(sequence, actor_sub, char_cfg, fps, fill_frames=0):
    """char_cfg のキャラ指定:
         "actor": レベル上の Actor (possessable として使用)
         "blueprint": BP パス (spawnable)
         "mesh": SkeletalMesh パス (SkeletalMeshActor の spawnable)
       "location" は足元の位置。
    戻り値: (終了フレーム, 足元位置, 正面 yaw, 身長)
    """
    if char_cfg.get("actor") is not None:
        actor = char_cfg["actor"]
        binding = sequence.add_possessable(actor)
        binding.set_display_name(actor.get_actor_label())
        remove_auto_tracks(binding)
    else:
        if char_cfg.get("location") in (None, "auto"):
            spot, ps_yaw = _player_start_spot(actor_sub)
            feet = vec(spot)
            yaw = float(char_cfg.get("yaw", ps_yaw))
        else:
            feet = vec(char_cfg["location"])
            yaw = float(char_cfg.get("yaw", 0))
        if char_cfg.get("snap_to_floor", True):
            feet = snap_to_floor(feet)
        rot = unreal.Rotator(0.0, 0.0, yaw)
        if char_cfg.get("blueprint"):
            cls = unreal.EditorAssetLibrary.load_blueprint_class(char_cfg["blueprint"])
            if cls is None:
                raise RuntimeError("Blueprint クラスを読み込めません: " + char_cfg["blueprint"])
            actor = actor_sub.spawn_actor_from_class(cls, feet, rot)
            hh = capsule_half_height(actor)
            if hh > 0:  # Character はカプセル中心が原点なので持ち上げる
                actor.set_actor_location(feet + unreal.Vector(0, 0, hh), False, False)
            name = cls.get_name().replace("_C", "")
        else:
            mesh = load_asset(char_cfg["mesh"])
            actor = actor_sub.spawn_actor_from_class(unreal.SkeletalMeshActor, feet, rot)
            comp = actor.skeletal_mesh_component
            try:
                comp.set_skeletal_mesh_asset(mesh)
            except AttributeError:
                comp.set_skeletal_mesh(mesh)
            name = mesh.get_name()
        binding = add_spawnable(sequence, actor)
        binding.set_display_name(name)

    loc = actor.get_actor_location()
    rot = actor.get_actor_rotation()
    feet = loc - unreal.Vector(0, 0, capsule_half_height(actor))
    face = facing_yaw(actor)
    height = character_height(actor)
    if char_cfg.get("actor") is None:
        actor_sub.destroy_actor(actor)

    # アニメーションを順番に並べる (Force Custom Mode で AnimBP を上書き)
    # fill_frames より短ければアニメ列をループして埋める (途中で T ポーズにならないように)
    cursor = 0
    anims = [load_asset(a) if isinstance(a, str) else a for a in char_cfg.get("anims", [])]
    if anims:
        anim_track = binding.add_track(unreal.MovieSceneSkeletalAnimationTrack)
        playlist = list(anims)
        total = sum(max(1, int(round(anim_length_seconds(a) * fps))) for a in anims)
        while fill_frames and total < fill_frames:
            playlist += anims
            total += sum(max(1, int(round(anim_length_seconds(a) * fps))) for a in anims)
        for anim in playlist:
            frames = max(1, int(round(anim_length_seconds(anim) * fps)))
            if fill_frames and len(playlist) > len(anims):
                if cursor >= fill_frames:
                    break
                frames = min(frames, fill_frames - cursor)
            section = anim_track.add_section()
            section.set_range(cursor, cursor + frames)
            params = section.get_editor_property("params")
            params.set_editor_property("animation", anim)
            params.set_editor_property("force_custom_mode", bool(char_cfg.get("force_custom_mode", True)))
            section.set_editor_property("params", params)
            log("  anim {} : {} - {}".format(anim.get_name(), cursor, cursor + frames))
            cursor += frames

    end = max(cursor, int(char_cfg.get("duration_frames", 1)), fill_frames)
    add_transform_keys(binding, [(0, loc, rot), (end - 1, loc, rot)])
    return end, feet, face, height


def look_at(cam_loc, target):
    d = target - cam_loc
    horiz = math.sqrt(d.x * d.x + d.y * d.y)
    yaw = math.degrees(math.atan2(d.y, d.x))
    pitch = math.degrees(math.atan2(d.z, horiz))
    return unreal.Rotator(0.0, pitch, yaw)  # (roll, pitch, yaw)


def cine_half_tans(cine):
    """シネカメラの (水平, 垂直) 半画角の tan をフィルムバックと焦点距離から求める。"""
    try:
        fb = cine.get_editor_property("filmback")
        sensor_w = float(fb.get_editor_property("sensor_width"))
        sensor_h = float(fb.get_editor_property("sensor_height"))
    except Exception:
        sensor_w, sensor_h = 23.76, 13.365  # 16:9 DSLR 既定
    focal = float(cine.get_editor_property("current_focal_length"))
    return (sensor_w * 0.5) / focal, (sensor_h * 0.5) / focal


def fit_distance(tan_h, tan_v, subject_height, subject_spread, margin):
    """全身 (身長 × margin) と横幅が画面に収まるカメラ距離を画角から算出。"""
    # 被写体の奥行き (周回中に手前へ来る分) を足しておく
    d_vertical = (subject_height * margin * 0.5) / tan_v + subject_spread
    d_horizontal = (subject_spread * margin) / tan_h + subject_spread
    return max(d_vertical, d_horizontal)


def add_camera(sequence, actor_sub, cam_cfg, center, end_frame, base_yaw=0.0,
               subject_height=180.0, subject_spread=40.0, aspect=16.0 / 9.0):
    """center: 足元中心。キャラの全身を中心に捉えて周回する。
    cam_cfg["camera_actor"]: "cine" (CineCameraActor, 既定) / "camera" (通常の CameraActor)"""
    kind = str(cam_cfg.get("camera_actor", "cine")).lower()
    margin = float(cam_cfg.get("framing_margin", 1.15))

    if kind == "camera":
        # 通常カメラ: 水平 FOV 指定。被写界深度 (ボケ) なし
        cam_actor = actor_sub.spawn_actor_from_class(unreal.CameraActor, center, unreal.Rotator())
        comp = cam_actor.get_component_by_class(unreal.CameraComponent)
        fov = float(cam_cfg.get("fov", 60))
        comp.set_editor_property("field_of_view", fov)
        tan_h = math.tan(math.radians(fov) * 0.5)
        tan_v = tan_h / aspect
    else:
        cam_actor = actor_sub.spawn_actor_from_class(unreal.CineCameraActor, center, unreal.Rotator())
        comp = cam_actor.get_cine_camera_component()
        comp.set_editor_property("current_focal_length", float(cam_cfg.get("focal_length", 35)))
        tan_h, tan_v = cine_half_tans(comp)

    radius_cfg = cam_cfg.get("radius", "auto")
    if radius_cfg in (None, "auto"):
        radius = fit_distance(tan_h, tan_v, subject_height, subject_spread, margin)
    else:
        radius = float(radius_cfg)

    if kind != "camera":
        focus = comp.get_editor_property("focus_settings")
        if cam_cfg.get("depth_of_field", True):
            # キャラにピントを合わせ、背景をぼかす
            focus.set_editor_property("focus_method", unreal.CameraFocusMethod.MANUAL)
            focus.set_editor_property("manual_focus_distance", radius)
            if cam_cfg.get("aperture") is not None:
                comp.set_editor_property("current_aperture", float(cam_cfg["aperture"]))
        else:
            focus.set_editor_property("focus_method", unreal.CameraFocusMethod.DISABLE)
        comp.set_editor_property("focus_settings", focus)

    binding = add_spawnable(sequence, cam_actor)
    actor_sub.destroy_actor(cam_actor)
    binding.set_display_name("AutoCamera")
    log("  camera actor: {}".format("CameraActor" if kind == "camera" else "CineCameraActor"))

    # 注視点 = 体の中心 (身長の半分) → 頭からつま先までが画面中央に入る
    target = center + unreal.Vector(0, 0, subject_height * float(cam_cfg.get("target_ratio", 0.5)))
    elev = math.radians(float(cam_cfg.get("elevation", 5)))   # 少し見下ろす角度
    horiz = radius * math.cos(elev)
    cam_z = radius * math.sin(elev)
    # 0 度 = キャラの正面 (base_yaw 方向) から撮る
    start = base_yaw + float(cam_cfg.get("start_angle", 0))
    sweep = float(cam_cfg.get("sweep", 360)) if cam_cfg.get("type", "orbit") == "orbit" else 0.0
    log("  camera: 距離 {:.0f}cm  身長 {:.0f}cm  回転 {:.0f}°".format(radius, subject_height, sweep))

    # 10 度刻みのリニアキーで等速回転
    key_count = max(2, int(math.ceil(abs(sweep) / 10.0)) + 1)
    keys = []
    prev_yaw = None
    for i in range(key_count):
        t = i / float(key_count - 1)
        ang = math.radians(start + sweep * t)
        pos = target + unreal.Vector(math.cos(ang) * horiz, math.sin(ang) * horiz, cam_z)
        rot = look_at(pos, target)
        # yaw の ±180 跨ぎで回転が飛ばないよう連続化
        if prev_yaw is not None:
            while rot.yaw - prev_yaw > 180:
                rot.yaw -= 360
            while rot.yaw - prev_yaw < -180:
                rot.yaw += 360
        prev_yaw = rot.yaw
        keys.append((int(round(t * (end_frame - 1))), pos, rot))
    add_transform_keys(binding, keys, linear=True)

    cut_track = sequence.add_track(unreal.MovieSceneCameraCutTrack)
    cut = cut_track.add_section()
    cut.set_range(0, end_frame)
    cut.set_camera_binding_id(binding_id(sequence, binding))


def build_sequence(cfg, job):
    fps = int(cfg["fps"])
    seq = create_sequence_asset(cfg["sequence_dir"], job["name"])
    seq.set_display_rate(unreal.FrameRate(fps, 1))
    unreal.LevelSequenceEditorBlueprintLibrary.open_level_sequence(seq)

    actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
    end_frame = 1
    centers = []
    heights = []
    base_yaw = None
    # 1 周にかける最低秒数 (アニメが短くても回転が速すぎないように)
    fill = int(float(job.get("camera", {}).get("orbit_seconds", 0)) * fps)
    for char_cfg in job["characters"]:
        e, feet, face, h = add_character(seq, actor_sub, char_cfg, fps, fill_frames=fill)
        end_frame = max(end_frame, e)
        centers.append(feet)
        heights.append(h)
        if base_yaw is None:
            base_yaw = face

    if job.get("duration_frames"):
        end_frame = int(job["duration_frames"])

    center = unreal.Vector(sum(c.x for c in centers) / len(centers),
                           sum(c.y for c in centers) / len(centers),
                           sum(c.z for c in centers) / len(centers))
    # 横幅 = 中心から一番遠いキャラまでの距離 + 体の半径 (腕を広げても収まる程度)
    spread = max(math.hypot(c.x - center.x, c.y - center.y) for c in centers) + 60.0
    add_camera(seq, actor_sub, job.get("camera", {}), center, end_frame, base_yaw or 0.0,
               subject_height=max(heights), subject_spread=spread,
               aspect=float(cfg["resolution"][0]) / float(cfg["resolution"][1]))

    seq.set_playback_start(0)
    seq.set_playback_end(end_frame)
    try:
        seq.set_view_range_start(0)
        seq.set_view_range_end(end_frame / float(fps))
        seq.set_work_range_start(0)
        seq.set_work_range_end(end_frame / float(fps))
    except Exception:
        pass

    cleanup_auto_bindings(seq)
    unreal.LevelSequenceEditorBlueprintLibrary.close_level_sequence()
    unreal.EditorAssetLibrary.save_loaded_asset(seq)
    log("シーケンス作成: {} ({} frames @ {}fps)".format(seq.get_path_name(), end_frame, fps))
    return seq


# ---------------------------------------------------------------------------
# Movie Render Queue
# ---------------------------------------------------------------------------
_FORMATS = {
    "png": "MoviePipelineImageSequenceOutput_PNG",
    "jpg": "MoviePipelineImageSequenceOutput_JPG",
    "exr": "MoviePipelineImageSequenceOutput_EXR",
    "bmp": "MoviePipelineImageSequenceOutput_BMP",
}


_MP4_DEFAULTS = DEFAULT_CONFIG["mp4"]


def mp4_settings(cfg):
    s = dict(_MP4_DEFAULTS)
    s.update(cfg.get("mp4") or {})
    return s


def find_ffmpeg(cfg):
    path = mp4_settings(cfg).get("ffmpeg")
    if path and os.path.exists(path):
        return path
    found = shutil.which("ffmpeg")
    if found:
        return found
    for p in (r"C:\python3.11.0\ffmpeg.exe", r"C:\ffmpeg\bin\ffmpeg.exe",
              r"C:\Program Files\ffmpeg\bin\ffmpeg.exe"):
        if os.path.exists(p):
            return p
    return None


def encode_mp4(cfg, seq_name, image_dir, ext, out_path):
    """{seq_name}.{0000}.{ext} の連番を MP4 に変換。成功で True。"""
    ffmpeg = find_ffmpeg(cfg)
    if not ffmpeg:
        warn("ffmpeg が見つかりません。config の mp4.ffmpeg にパスを指定してください")
        return False

    pat = re.compile(re.escape(seq_name) + r"\.(\d+)\." + ext + "$", re.IGNORECASE)
    files = sorted(f for f in glob.glob(os.path.join(image_dir, "*." + ext)) if pat.search(os.path.basename(f)))
    if not files:
        warn("連番画像が見つかりません: {}".format(image_dir))
        return False
    first = pat.search(os.path.basename(files[0])).group(1)

    s = mp4_settings(cfg)
    cmd = [
        ffmpeg, "-y", "-hide_banner", "-loglevel", "error",
        "-framerate", str(int(cfg["fps"])),
        "-start_number", str(int(first)),
        "-i", os.path.join(image_dir, "{}.%0{}d.{}".format(seq_name, len(first), ext)),
        "-c:v", s["codec"], "-crf", str(s["crf"]), "-preset", s["preset"],
        "-pix_fmt", "yuv420p",
        "-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2",   # yuv420p は偶数解像度が必要
        "-movflags", "+faststart",
        out_path,
    ]
    log("MP4 変換: " + " ".join('"{}"'.format(c) if " " in c else c for c in cmd))
    flags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
    r = subprocess.run(cmd, capture_output=True, text=True, creationflags=flags)
    if r.returncode != 0:
        warn("ffmpeg 失敗 ({}): {}".format(r.returncode, r.stderr.strip()))
        return False
    log("MP4 出力: {} ({} frames)".format(out_path, len(files)))

    if not s.get("keep_images", True):
        for f in files:
            os.remove(f)
    return True


def set_keep_alive(value):
    try:
        unreal.EditorPythonScripting.set_keep_python_script_alive(bool(value))
    except Exception:
        pass


def queue_render(cfg, sequences, quit_after=False, map_path=None):
    mrq = unreal.get_editor_subsystem(unreal.MoviePipelineQueueSubsystem)
    if mrq.is_rendering():
        warn("既にレンダー中のためスキップします")
        return
    queue = mrq.get_queue()
    queue.delete_all_jobs()

    map_soft = unreal.SoftObjectPath(map_path or cfg["map"])
    out_root = cfg["output_dir"].replace("{project_dir}", project_dir().replace("\\", "/"))
    fmt = cfg.get("output_format", "png")
    fmt_cls = getattr(unreal, _FORMATS.get(fmt, _FORMATS["png"]))
    want_mp4 = bool(mp4_settings(cfg).get("enabled"))
    # EXR は ffmpeg 入力に向かないので、MP4 用に PNG も併せて出力
    mp4_ext = fmt if fmt in ("png", "jpg", "bmp") else "png"
    encode_list = []

    for seq in sequences:
        job = queue.allocate_new_job(unreal.MoviePipelineExecutorJob)
        job.job_name = seq.get_name()
        job.sequence = unreal.SoftObjectPath(seq.get_path_name())
        job.map = map_soft

        conf = job.get_configuration()
        out = conf.find_or_add_setting_by_class(unreal.MoviePipelineOutputSetting)
        out.output_directory = unreal.DirectoryPath("{}/{}".format(out_root, seq.get_name()))
        out.output_resolution = unreal.IntPoint(int(cfg["resolution"][0]), int(cfg["resolution"][1]))
        out.file_name_format = "{sequence_name}.{frame_number}"
        out.zero_pad_frame_numbers = 4

        conf.find_or_add_setting_by_class(unreal.MoviePipelineDeferredPassBase)
        conf.find_or_add_setting_by_class(fmt_cls)
        if want_mp4 and mp4_ext != fmt:
            conf.find_or_add_setting_by_class(unreal.MoviePipelineImageSequenceOutput_PNG)
        if want_mp4:
            encode_list.append((seq.get_name(), out.output_directory.path))

        # GameMode を MRQ 用 (既定: MoviePipelineGameMode) に上書き
        # → プレイヤーポーン (BP_ThirdPersonCharacter) や HUD が映り込まない
        conf.find_or_add_setting_by_class(unreal.MoviePipelineGameOverrideSetting)

        aa = conf.find_or_add_setting_by_class(unreal.MoviePipelineAntiAliasingSetting)
        aa.spatial_sample_count = int(cfg.get("spatial_samples", 1))
        aa.temporal_sample_count = int(cfg.get("temporal_samples", 1))
        aa.engine_warm_up_count = int(cfg.get("warmup_frames", 10))
        aa.render_warm_up_frames = True
        log("レンダージョブ追加: {} -> {}".format(seq.get_name(), out.output_directory.path))

    def _on_finished(executor, success):
        log("レンダー完了 success={}  出力: {}".format(success, out_root))
        _RENDER_STATE.clear()
        if success and encode_list:
            with unreal.ScopedSlowTask(len(encode_list), "MP4 に変換中...") as task:
                task.make_dialog()
                for name, img_dir in encode_list:
                    task.enter_progress_frame(1, "MP4 変換: " + name)
                    encode_mp4(cfg, name, img_dir, mp4_ext, "{}/{}.mp4".format(out_root, name))
        set_keep_alive(False)
        if quit_after:
            unreal.SystemLibrary.quit_editor()

    executor = mrq.render_queue_with_executor(unreal.MoviePipelinePIEExecutor)
    executor.on_executor_finished_delegate.add_callable_unique(_on_finished)
    _RENDER_STATE["executor"] = executor
    # -ExecutePythonScript 起動時は、スクリプトが返った次のフレームでエディタが終了してしまう。
    # レンダー (非同期) が終わるまでエディタを生かしておく。
    set_keep_alive(True)
    log("レンダー開始 ({} jobs)".format(len(sequences)))


# ---------------------------------------------------------------------------
# 選択から自動撮影
# ---------------------------------------------------------------------------
def _editor_world():
    return unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem).get_editor_world()


def _player_start_spot(actor_sub):
    """PlayerStart の足元位置と yaw。無ければ原点。"""
    for a in actor_sub.get_all_level_actors():
        if isinstance(a, unreal.PlayerStart):
            feet = a.get_actor_location() - unreal.Vector(0, 0, capsule_half_height(a))
            return [feet.x, feet.y, feet.z], a.get_actor_rotation().yaw
    return [0, 0, 0], 0.0


def collect_selection(cfg):
    """レベル選択アクター → possessable、コンテンツブラウザの BP / SkeletalMesh → spawnable。
    同時に選択された AnimSequence はアニメとして使う。"""
    actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
    targets = []
    anims = []

    for asset in unreal.EditorUtilityLibrary.get_selected_assets():
        if isinstance(asset, unreal.AnimSequence):
            anims.append(asset)
        elif isinstance(asset, unreal.Blueprint):
            targets.append({"blueprint": asset.get_path_name().split(".")[0], "_name": asset.get_name()})
        elif isinstance(asset, unreal.SkeletalMesh):
            targets.append({"mesh": asset.get_path_name().split(".")[0], "_name": asset.get_name()})

    if not targets:
        for a in actor_sub.get_selected_level_actors():
            if a.get_component_by_class(unreal.SkeletalMeshComponent):
                targets.append({"actor": a, "_name": a.get_actor_label()})

    if targets and any("actor" not in t for t in targets):
        spot, yaw = _player_start_spot(actor_sub)
        for t in targets:
            if "actor" not in t:
                t["location"] = cfg["selection_job"].get("location", spot)
                t["yaw"] = cfg["selection_job"].get("yaw", yaw)
    return targets, anims


def run_selected(config_path=None, render=True):
    cfg = load_config(config_path)
    sel_cfg = dict(DEFAULT_CONFIG["selection_job"])
    sel_cfg.update(cfg.get("selection_job", {}))
    cfg["selection_job"] = sel_cfg

    targets, anims = collect_selection(cfg)
    if not targets:
        unreal.EditorDialog.show_message(
            "自動撮影", "キャラクター (Blueprint / SkeletalMesh / レベル上のアクター) を選択してください。",
            unreal.AppMsgType.OK)
        return []

    anim_list = anims or sel_cfg.get("anims", [])
    jobs = []
    for t in targets:
        char = {k: v for k, v in t.items() if not k.startswith("_")}
        char["anims"] = anim_list
        if not anim_list:
            char["duration_frames"] = int(sel_cfg.get("duration_frames_without_anims", 150))
        jobs.append({
            "name": sel_cfg.get("name_prefix", "SEQ_Auto_") + t["_name"],
            "characters": [char],
            "camera": sel_cfg.get("camera", {}),
        })

    world = _editor_world()
    sequences = [build_sequence(cfg, job) for job in jobs]
    if render:
        queue_render(cfg, sequences, map_path=world.get_path_name())
    return sequences


# ---------------------------------------------------------------------------
def main(argv=None):
    args = parse_args(sys.argv[1:] if argv is None else argv)
    if args["selected"]:
        return run_selected(args["config"], args["render"])

    cfg = load_config(args["config"])

    level_sub = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
    level_sub.load_level(cfg["map"])

    sequences = [build_sequence(cfg, job) for job in cfg["jobs"]]

    if args["render"]:
        queue_render(cfg, sequences, quit_after=args["quit"])
    elif args["quit"]:
        unreal.SystemLibrary.quit_editor()
    return sequences


if __name__ == "__main__":
    main()

“H:\SandBox\UE583SequenceRender\sequence_jobs.json”

{
  "map": "/Game/ThirdPerson/Lvl_ThirdPerson",
  "sequence_dir": "/Game/AutoSequences",
  "output_dir": "{project_dir}/Saved/AutoRender",
  "fps": 30,
  "resolution": [
    1920,
    1080
  ],
  "output_format": "png",
  "mp4": {
    "enabled": true,
    "ffmpeg": "",
    "codec": "libx264",
    "crf": 18,
    "preset": "medium",
    "keep_images": true
  },
  "spatial_samples": 1,
  "temporal_samples": 1,
  "warmup_frames": 10,
  "selection_job": {
    "name_prefix": "SEQ_Auto_",
    "anims": [
      "/Game/Characters/Mannequins/Anims/Unarmed/MM_Idle",
      "/Game/Characters/Mannequins/Anims/Unarmed/Attack/MM_Attack_01"
    ],
    "duration_frames_without_anims": 150,
    "camera": { "camera_actor": "camera", "depth_of_field": true, "type": "orbit", "sweep": 360, "start_angle": 0, "orbit_seconds": 8, "radius": "auto", "framing_margin": 1.15, "elevation": 5, "focal_length": 35 }
  },
  "jobs": [
    {
      "name": "SEQ_BP_ThirdPerson",
      "characters": [
        {
          "blueprint": "/Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter",
          "anims": [
            "/Game/Characters/Mannequins/Anims/Unarmed/MM_Idle",
            "/Game/Characters/Mannequins/Anims/Unarmed/Jump/MM_Dash"
          ],
          "location": [0, 0, 0],
          "yaw": 0
        }
      ],
      "camera": { "camera_actor": "camera", "depth_of_field": true, "type": "orbit", "sweep": 360, "start_angle": 0, "orbit_seconds": 8, "radius": "auto", "framing_margin": 1.15, "elevation": 5, "focal_length": 35 }
    },
    {
      "name": "SEQ_Manny_Attack",
      "characters": [
        {
          "mesh": "/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple",
          "anims": [
            "/Game/Characters/Mannequins/Anims/Unarmed/MM_Idle",
            "/Game/Characters/Mannequins/Anims/Unarmed/Attack/MM_Attack_01",
            "/Game/Characters/Mannequins/Anims/Unarmed/Attack/MM_Attack_02",
            "/Game/Characters/Mannequins/Anims/Unarmed/Attack/MM_Attack_03"
          ],
          "location": [0, 0, 0],
          "yaw": 0
        }
      ],
      "camera": { "camera_actor": "camera", "depth_of_field": true, "type": "orbit", "sweep": 360, "start_angle": 0, "orbit_seconds": 8, "radius": "auto", "framing_margin": 1.15, "elevation": 5, "focal_length": 35 }
    },
    {
      "name": "SEQ_Duo_Reaction",
      "characters": [
        {
          "mesh": "/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple",
          "anims": [
            "/Game/Characters/Mannequins/Anims/Unarmed/Attack/MM_ChargedAttack"
          ],
          "location": [0, -80, 0],
          "yaw": 90
        },
        {
          "mesh": "/Game/Characters/Mannequins/Meshes/SKM_Quinn_Simple",
          "anims": [
            "/Game/Characters/Mannequins/Anims/Rifle/HitReact/MM_HitReact_Front_Hvy_01",
            "/Game/Characters/Mannequins/Anims/Death/MM_Death_Back_01"
          ],
          "location": [0, 80, 0],
          "yaw": -90
        }
      ],
      "camera": { "camera_actor": "cine", "depth_of_field": true, "type": "orbit", "sweep": 360, "start_angle": 0, "orbit_seconds": 8, "radius": "auto", "framing_margin": 1.15, "elevation": 5, "focal_length": 35 }
    }
  ]
}

“H:\SandBox\UE583SequenceRender\UE583SequenceRender\Content\Python\init_unreal.py”

# -*- coding: utf-8 -*-
# エディタ起動時に自動実行され、「自動撮影」メニューを登録する。
# auto_sequence_render.py / sequence_jobs.json はプロジェクトの 1 つ上 (run_auto_render.bat と同じフォルダ) に置く。
import os
import sys

import unreal

TOOL_DIR = os.path.abspath(os.path.join(unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir()), ".."))
if TOOL_DIR not in sys.path:
    sys.path.append(TOOL_DIR)

_COMMAND = (
    "import importlib, auto_sequence_render as asr; "
    "importlib.reload(asr); asr.run_selected()"
)

_MENUS = [
    "ContentBrowser.AssetContextMenu",   # コンテンツブラウザでアセットを右クリック
    "LevelEditor.ActorContextMenu",      # レベル上のアクターを右クリック
    "LevelEditor.MainMenu.Tools",        # メインメニュー Tools
]


def register_menus():
    menus = unreal.ToolMenus.get()
    for menu_name in _MENUS:
        menu = menus.extend_menu(menu_name)
        if menu is None:
            continue
        menu.add_section("AutoSequenceRender", "Auto Sequence Render")
        entry = unreal.ToolMenuEntry(
            name="AutoSequenceRender_Selected",
            type=unreal.MultiBlockType.MENU_ENTRY,
        )
        entry.set_label("自動撮影 (シーケンス作成 + レンダー)")
        entry.set_tool_tip("選択中のキャラクター (BP / SkeletalMesh / レベルのアクター) でシーケンスを作成し、Movie Render Queue でレンダーします。AnimSequence も選択するとそのアニメを使用します。")
        entry.set_string_command(unreal.ToolMenuStringCommandType.PYTHON, "", _COMMAND)
        menu.add_menu_entry("AutoSequenceRender", entry)
    menus.refresh_all_widgets()
    unreal.log("[AutoSeq] 自動撮影メニューを登録しました (スクリプト: {})".format(TOOL_DIR))


register_menus()

以下のものが必要です。

部品役割ライセンス
unreal モジュール(UE 5.8 に標準で入っている Python API)シーケンスの作成、キャラやカメラの配置、レンダーUnreal Engine EULA
├ PythonScriptPluginUE で Python を動かす同上(UE 同梱)
├ SequencerScriptingシーケンサーの操作同上
└ MovieRenderPipelineMovie Render Queue でのレンダー同上
Python 標準ライブラリ(json, math, os, re, glob, shutil, subprocess)設定の読み込み、計算、ファイル操作PSF ライセンス(商用利用 OK)
ffmpeg(C:\python3.11.0\ffmpeg.exe)PNG 連番を MP4 に変換GPL v3(gyan.dev のビルドで、libx264 を含む)

https://www.gyan.dev/ffmpeg/builds

「release builds」の中にある ffmpeg-release-essentials.zip を選んでください。

https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip

[UE5.4.4]プロシージャルな動く水のコースティクス マテリアル関数をフローマップによって流れ方向をコントロールできるようにしました。

[UE5.4.4] I enabled flow direction control via flow maps for the procedural, animated water caustics material function.

MF_Caustics

M_FlowMap textureObject と TextureSampleはLinea Clolorにしないとエラー出ます。

テクスチャ設定はこれ VectorDisplacementmap (RGBA8)

結果

参考

https://gamemakers.jp/article/2024_10_18_82738

[Blender5.1][Rig][UE5.8] Auto Rig Pro を購入から使用UE5インポートまで

[Blender 5.1][Rig][UE5.8] From purchasing to using Auto Rig Pro and importing into UE5

https://superhivemarket.com/products/auto-rig-pro

Ado To Cartした。

カートに入った CheckOutして

Create Accountした

Confirm My Accountした

もう一回Checkoutしてこの画面きた。PayPal Checkoutした。

Ordersクリックした。

Blender5.1.1にドラッグした

入らないので

8 downloads
auto_rig_pro_3.78.21.zip
proxy_picker_1.1.11.zip
rig_tools_3.78.18.zip
Al_linux_1.20.zip -winにはいらない
Al_macIntel_1.20.zip -winにはいらない
Al_mac_1.20.zip -winにはいらない
Al_win_1.20.zip あとで必要
io_scene_gltf2.zip Blender5.1.1には不要

zipを選択

入った。


Blendr5.1.1に

D:\Download\auto_rig_pro_Set

入れたいけど

AI_win_1.20.zipがMissing Manifestだって。

それはたぶん 入れるzipを間違えているだけ でした。

確認したところ、AI_win_1.20.zip はBlenderアドオン本体ではなく、Auto-Rig ProのAI用データ/実行ファイルでした。なのでBlenderの「Install from Disk」で直接入れると Missing Manifest になります。

つまり入れる順番はこれです。

  1. Blenderで Edit > Preferences > Get Extensions / Add-ons > Install from Disk
  2. まずこれを入れる
    D:\Download\auto_rig_pro_Set\auto_rig_pro_3.78.21.zip

    auto_rig_pro_3.78.21.zip
    proxy_picker_1.1.11.zip – 開発中の機能
    rig_tools_3.78.18.zip ー開発中の機能入れないほうがいい
    Al_win_1.20.zip あとで必要
    io_scene_gltf2.zip Blender5.1.1には不要

Auto-Rig Proの設定内にある Install AI Files… からこれを選ぶ
D:\Download\auto_rig_pro_Set\AI_win_1.20.zip

  1. AI files Installed successfully と言われる。

Auto-Rig Pro公式ドキュメントでも、本体は auto_rig_pro_x.xx.xx.zip をInstall from Disk、AIファイルはAuto-Rig Pro設定内の Install AI Files… から入れる手順になっています: Auto-Rig Pro Installation

補足: Blender 5.1.1なら io_scene_gltf2.zip は入れなくてOKです。公式説明でもBlender 4.2以上はGLTF exporterの追加インストールをスキップするよう書かれています。rig_tools_3.78.18.zip もAuto-Rig Pro本体を入れるなら基本入れない方がいいです。

正常にインストールできると「N」キー押すとARPがでる

素体をインポートして

メッシュ全部選択して

「Auto-Rig Pro:Smart」機能を試してみた

FullBody選択

Guess Markers押す。(10秒ぐらいかかる)

Skelton Setting Presetsから

今回はUE4 Mannequinsを選択した

Guess Fingers 押す。

指にフィットしたようだ。

GO!押す(10秒ぐらいかかる)

できたっぽい。Match To Rig 押す(10秒)

リグができた。

スキニング

Skin押す

Preserved VolumeをOFFでまだBindおさない。

オブジェクトモードにする[Tab]キー

Blender5.1 オブジェクトモード
  1. オブジェクトモードにする[Tab]キー
  2. Shift を押しながらバインドするMeshオブジェクト(複数可)、Armature の(rig)の順に選択する
  3. Bind ボタンを押す

全メッシュではフリーズしたので、Voxelized に切り替え

段階的にBindした

全部Meshの外に行ったので成功がわかった。

スケールが低すぎるため、バインディングが部分的に、または完全に失敗した可能性があります。

スケール修正を有効にして再度バインディングを実行してください。OKした。

ポーズの確認

ポーズモードへの切り替え方法

アーマチュア(ボーン)を選択した状態で、[Ctrl] + [Tab] キーを押すのが最も早いショートカットです。

Blebnder5.1 ポーズモード

屈伸できた

Export

Auto-Rig Pro: ExportのExport FBXでエクスポート

Unreal Engine

Humanoid と

UE4 Legacy をオンにした

書き出し終わったらしい

UE5へインポート

import Texture オン

import Material オン

プロジェクトのUE4マネキンを壊さないようにSkeletonはNoneでインポートした

インポートできたら、skeletonをチェック root、pelvisがあっていればOK

SkeltalMesh > Create > IK Rig

とりあえずはこれくらい設定した

指も設定

IKRig > Create IK Retargeter

①SK_Mannequinも同じようにRoteteOnlyで設定したIK Rigを選ぶ

②Source選択

③Create > Import Animation Seqense

④T_poseを検索してインポートした。

完成

#UE4 #UE4Study Character #HairCloth #HairShadingModel #AdvancedLocomotionSystem ファサっと天使の輪と束感でUEのヘアーの使い方

服と髪をなびかせるHair Clothはこれをやった
https://docs.unrealengine.com/4.27/ja/InteractiveExperiences/Physics/Cloth/Overview/
1, [Section Selection (セクション選択)]
2,左クリックを使って、クロスとして使用したいメッシュの一部を選択します。次に右クリックして、コンテキスト メニューを開いて Cloth アセットを作成します。
3,コンテキスト メニューから、[Create Cloth Asset from Selection (選択したものから Cloth アセットを作成)] を選びます。

 A,Asset Name – 後で簡単に見つけられるようにアセットに名前を付けてください。

 B,Remove from Mesh – クロスとして関連付けたい別個のジオメトリのメッシュの構成要素があれば、このオプションを有効にします。そうでなければ、チェックを入れずにこのままにします。

 C,Physics Asset – Cloth アセットがキャラクター用ならば、その Physics アセットを使ってクロス シミュレーションで適切なコリジョンが生じるようにします。

4,[Create] ボタンをクリックします。
5,セクションを再度右クリックして、コンテキスト メニューを表示させて、[Apply Clothing Asset] にマウスをかざし、利用可能なクロス アセットから適用するものを選択します。これで作成した任意のクロス アセットが選択したセクションに関連付けられます。

・ペイント – マウスの左ボタン
・消去 – Shift キー + マウスの左ボタン
・クロスのプレビュー – H キー

[Window] を選択し、リストから [Clothing] を選択します。
[Clothing Data] リストから選択します。

[Activate Cloth Paint] ボタンをクリックして、選択した Cloth アセットをペイントするために使用可能なプロパティを有効にします。

・ブラシの半径を5にして
・強度を0.2
・フォールオフ0.5
でヘアをクリックすると塗れる。ピンクが塗ってない場所

走ってみると

髪が前に来ちゃう

ので

物理ボディを修正した

全骨コリジョン作ったほうが調整しやすかった。(指は消した)

Spine5がこういった設定 Radius 10以下ぐらい

以下は昔の設定

で設定することが重要

でやっといい感じ

HairShadingModel 基本はこれをやった。

つまりこれで髪の天使の輪みたいなものができる。 (まわしてみないと分からない)

でも髪の束感は出したかったのでOpacityMaskもありにするとこうなる。

束感のために使ってるテクスチャはこれで

https://drive.google.com/file/d/11JcBV0dAtrexQew0fbKUZcLRfbYn7FuZ/view?usp=sharing

こんな感じにスキャッター、スペキュラ、ラフネスにMultyplyしたらいい感じになった。

使っているMIでの値はこんな感じ

最後にまたこれをやって

やっとこうなった

元のモデルはここ

https://www.deviantart.com/xcrofty/art/NieR-Automata-A2-678696387

[Unreal Engine 5.3.2] Create a Cloth Simulation の使い方と流れ

unreal engine 5.3.2 create a cloth simulation How to use and flow

UnrealEngine5.3.2のCloth Toolの使い方と流れ。

0,再生ヘッドは止めておくべき
1,該当のマテリアルを Isolate でシングル表示後に
2,メッシュ右クリックからData >Create 


3,Window>Clothから作られた>Dataを選択
4,「Activate Cloth Paint」押す


5,ピンクになる Cloth WindowのBrushプロパティでる。


6,Cloth WindowのBrush> Radias 15.0程度に
7,Clothで動かしたいところをを白に塗る
8,「Deactivate Cloth Paint」押す 
9、メッシュ右クリック>Apply Clothing Dataでできる。
9,Preview Animation▼でWalkやRunで確認 再生ヘッドを再生する
10,SkeltalMesh保存
11,PhysicsAssetを開く
12,Details>BodySetup>Primitives>Capsules>Index[0]>
13,Radiusを減らす
14,Rotationも合わせる
15,PhysicsAssetを保存
16,Preview Animation▼でWalkやRunで確認
17,SkeltalMesh開きなおしでPhysicsAssetの修正が反映される様子。

別のメッシュからCloth Dataをコピーするには?

1,Add Clothing で追加できる。

2,9、メッシュ右クリック>Apply Clothing Dataでできる。

修正する場合の使い方と流れ


1,該当のマテリアルを Isolate でシングル表示後に
2,メッシュ右クリックからRemove Clothing Data する。>>必要はない。
3,(4,「Activate Cloth Paint」押す )からの流れをやり直す

・マテリアルが透明のTranslusentだとピンクが表示されないことがある。青に塗ると色が混ざって緑とかになる。
・メッシュの割りsubDivisionLevelは多くないと成功するものも成功しない。
・ClothのDeactiveClothPaintはすごく処理が重い。


・処理の早いマシンでならMarvelous Designerで作ったメッシュでもなんとか塗れる。


・Marvelous Designerで作った複雑なプリーツなどは再現されにくい。


・Runで前にあるスカートは太ももで蹴り上げてしまうので、ポリゴン数は少なめのほうがいいかも、割りは多い方がいいのでバランスが重要


・今回成功した前面の割りはこんなもの。

困った場合に成功したスカートだけのポリゴンデータを載せておく

Skirt_Short_Cage2_Middle_Koshi.fbx これだけダウンロードできます。

https://drive.google.com/file/d/18SzHq33gl-FMdSjuai_5FXDudgZiWxQc/view?usp=sharing

揺れのためには

全骨子リジョン作ったほうが調整しやすかった(指は消した)

でキチンと設定することが重要

成功した状態

参考URL

自分用リンク

jennie9_6_v12z_v00145_4JacketMiniSkirt_CageSizeedSW.zip

https://drive.google.com/file/d/1KGYQ4AlubC05BAtDRw5o5UKXlnh_XNwg/view?usp=sharing

jennie9UE532.zip

https://drive.google.com/file/d/156VO3HnXSrQEzEfB9UHOAPO3Q82FoZbl/view?usp=sharing

エラー集

DataでCreateでのエラー

三角形が縮退しているため、衣類シミュレーション メッシュの生成に失敗しました。三角形内に一致する頂点が見つかりました
Failed to generate clothing sim mesh due to degenerate triangle, found conincident vertices in triangle

とか言われたらクリーンアップしてバインドしなおしになっちゃう。。。

https://forums.unrealengine.com/t/failed-to-generate-clothing-sim-mesh-due-to-degenerate-triangle/94634/2

おそらく頂点が別の頂点の上に重なっていることを意味します。Blender では、Remove Doubles を実行してこれを修正します。

Remove Doublesは名前がMerge by Distanceに変わっただけです。

治らない。。。。

こうも書いてあった。

「出力単位をメートルではなくセンチメートルに変更してみてください。」

ミリメートルに変更した!

治った。

でも、できたのは後ろ髪だけ。

ほかはFailedなので作り直しが必要だった。MayaではNurbs CircleをLoftで全部作れた。

自分用 Hair_Furcraea_Plant_NurbsLoftV2.zip

https://drive.google.com/file/d/1ByJfw5QSSNDOQsdk_q9LjZPVS-n5bnEb/view?usp=sharing

Clothでひとふさずつ動かそうとしたらひどかった ボリュームがなくなった。つぶれた。

これならばKawaiiPhysicsのほうがいいかも?

と思ったが全体をやってみた。ボリュームは確かになくなるけど、ある程度形状は保っている感じ。いいのかもこれで。

ということで髪の毛全体テスト

自分用 Hair_Furcraea_Plant_NurbsLoftV3.zip

https://drive.google.com/file/d/1z4aipUFdHk19qCcrBb9bWvsf3KpWvlG9/view?usp=sharing

できたのがこれ ちょっとつぶれるし伸びた印象

でキャラごとmmで出力したけど

Preview Animationするとすごいちっさくなった。キャラBPで拡大すればいいでしょとおもっていたらそれは問題無い様子。

Dataを作り塗ることができた。Falloffは1.0にするともっとなだらかになるはず。

けども。DeactiveClothPaint後の様子がすごいバグ。

わーん

ということでmmにしてもキャラクターだとエンジンのサイズ計算がバグってますので使わないことにしました。

フォーラムに投稿しました。

https://forums.unrealengine.com/t/failed-to-generate-clothing-sim-mesh-due-to-degenerate-triangle-found-conincident-vertices-in-triangle/1678556

mmでは調整したスカートも壊れる

単位はcmにして後ろ髪はCloth 、前髪、左髪、右髪はスキンウェイトかKawaiiPhysicsにしておわりだなこりゃ。

完成2

ゲームでの再生

自分用 jennie9_6_v12z_v00148_HairFurcraea_mmSW.zip

https://drive.google.com/file/d/1FDRjjub_SM___Yb-u_kv5cwspP-1NdVf/view?usp=sharing

自分用 Hair_Furcraea_Plant_NurbsLoftV7_FullLoft.zip

https://drive.google.com/file/d/1pnZHcPUl-wykW3DBE_RcYHBIJ2Ks2N4-/view?usp=sharing

[UE5][BackUp]Cppプロジェクトでいらないフォルダを含めずにバックアップzipを作成するバッチを作った件

gitだと大容量なサーバーとか用意するの大変だから・・・

実行内容はこんなかんじ

D:\Sandbox\BookAnimation_Codex\UE574Book\UE5CppFolder_MakeBackUpZip.bat
にフォルダをドラッグしたら
中身の
.vs
Binaries
Build
DerivedDataCache
Intermediate
Saved
(pluginsのプラグイン名フォルダ内BinariesとIntermidiateも消してます)
を除いた内容のzipを
D:\Sandbox\BookAnimation_Codex\UE574Book\ドラッグしたフォルダ名.zipに作るバッチを作った
zip実行中になんでもいいからプログレスバーとパーセントを表示するようにした

※バックアップからの復旧方法

VisualStudioなどでビルドします。

UE5CppFolder_MakeBackUpZip.bat

@echo off
setlocal EnableExtensions

if "%~1"=="" (
    echo Usage:
    echo   Drag a folder onto this bat file.
    echo.
    pause
    exit /b 1
)

set "BACKUP_SOURCE=%~f1"
if not exist "%BACKUP_SOURCE%\" (
    echo Error: folder not found.
    echo   %BACKUP_SOURCE%
    echo.
    pause
    exit /b 1
)

set "BACKUP_ZIP=%~dp1%~nx1.zip"
set "BACKUP_SCRIPT=%~dp0UE5CppFolder_MakeBackUpZip.ps1"

echo Source:
echo   %BACKUP_SOURCE%
echo.
echo Output:
echo   %BACKUP_ZIP%
echo.
echo Excluding folders:
echo   .vs Binaries Build DerivedDataCache Intermediate Intermidiate Saved
echo.

if not exist "%BACKUP_SCRIPT%" (
    echo Error: helper script not found.
    echo   %BACKUP_SCRIPT%
    echo.
    pause
    exit /b 1
)

powershell -NoProfile -ExecutionPolicy Bypass -File "%BACKUP_SCRIPT%" -Source "%BACKUP_SOURCE%" -ZipPath "%BACKUP_ZIP%"
set "BACKUP_ERROR=%ERRORLEVEL%"

if not "%BACKUP_ERROR%"=="0" (
    echo.
    echo Failed.
    pause
    exit /b %BACKUP_ERROR%
)

echo.
echo Done.
pause

UE5CppFolder_MakeBackUpZip.ps1

param(
    [Parameter(Mandatory = $true)]
    [string]$Source,

    [Parameter(Mandatory = $true)]
    [string]$ZipPath
)

$ErrorActionPreference = 'Stop'

$sourceFullPath = [System.IO.Path]::GetFullPath($Source).TrimEnd('\', '/')
$zipFullPath = [System.IO.Path]::GetFullPath($ZipPath)

$exclude = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
'.vs', 'Binaries', 'Build', 'DerivedDataCache', 'Intermediate', 'Intermidiate', 'Saved' | ForEach-Object {
    [void]$exclude.Add($_)
}

function Test-IsExcludedBackupFile {
    param([Parameter(Mandatory = $true)][string]$FileFullPath)

    if ([System.StringComparer]::OrdinalIgnoreCase.Equals($FileFullPath, $zipFullPath)) {
        return $true
    }

    $relative = $FileFullPath.Substring($sourceFullPath.Length).TrimStart('\', '/')
    foreach ($part in ($relative -split '[\\/]')) {
        if ($exclude.Contains($part)) {
            return $true
        }
    }

    return $false
}

Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem

if (Test-Path -LiteralPath $zipFullPath) {
    Remove-Item -LiteralPath $zipFullPath -Force
}

Write-Host 'Scanning files...'
$files = @(
    Get-ChildItem -LiteralPath $sourceFullPath -Force -Recurse -File | Where-Object {
        $fileFullPath = [System.IO.Path]::GetFullPath($_.FullName)
        -not (Test-IsExcludedBackupFile -FileFullPath $fileFullPath)
    }
)

$total = $files.Count
Write-Host ("Files to zip: {0}" -f $total)

$zip = [System.IO.Compression.ZipFile]::Open($zipFullPath, [System.IO.Compression.ZipArchiveMode]::Create)
$count = 0

try {
    if ($total -eq 0) {
        Write-Progress -Activity 'Creating backup zip' -Status 'No files to zip' -PercentComplete 100
    }
    else {
        foreach ($file in $files) {
            $count++
            $fileFullPath = [System.IO.Path]::GetFullPath($file.FullName)
            $relative = $fileFullPath.Substring($sourceFullPath.Length).TrimStart('\', '/')
            $entryName = $relative -replace '\\', '/'
            $percent = [int][Math]::Floor(($count * 100.0) / $total)

            Write-Progress `
                -Activity 'Creating backup zip' `
                -Status ("{0}%  {1}/{2}  {3}" -f $percent, $count, $total, $entryName) `
                -PercentComplete $percent

            if (($count % 25) -eq 0 -or $count -eq 1 -or $count -eq $total) {
                Write-Host ("{0,3}%  {1}/{2}  {3}" -f $percent, $count, $total, $entryName)
            }

            [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile(
                $zip,
                $fileFullPath,
                $entryName,
                [System.IO.Compression.CompressionLevel]::Optimal) | Out-Null
        }
    }
}
finally {
    $zip.Dispose()
    Write-Progress -Activity 'Creating backup zip' -Completed
}

Write-Host ("Created: {0}" -f $zipFullPath)
Write-Host ("Files: {0}" -f $count)

[UE5.8.0][Maya][Retarget] TposeのキャラクターモデルをAposeのMannequinのモーションにリターゲットする方法

MM_T_Pose をエクスポート

Export Optionはこう

MayaにFBXインポートした状態で保存する SKM_Quinn_Simple_Tpose.ma

Tposeのキャラクターモデル(ここではfanModelのサイトからダウンロードしたものを用意した)

https://www.deviantart.com/d-presso/art/Nier-Re-in-carnation-Levannia-XPS-DL-880627377

を開いて


先ほどのTposeのFBXをインポートして

Root ( SKM_Quinn_Simple_Tpose:root_Quinn_T_pose ) のトップ のjointをスケールしてキャラクターにあわせる。

複製してフリーズする

以下ツールでスキンウェイトをコピー

SkinWeightCopy_First_Select_MeshGroup_to_Second Select_MeshGroup.mel

// SkinWeightCopy First Select MeshGroup to Second Select MeshGroup 
string $selectedArrFUllPath[] = `ls -long -sl`;
print($selectedArrFUllPath);

string $FirstSelect=$selectedArrFUllPath[0];

string $SecondSelect=$selectedArrFUllPath[1];

string $rejectArr[];

//string $inputNodes[] = `ls -type transform -long -dag $FirstSelect`;
//string $destNodes[] = `ls -type transform -long -dag $SecondSelect`;

string $inputNodes_mesh[] = `ls -type mesh -long -dag $FirstSelect`;
string $destNodes_mesh[] = `ls -type mesh -long -dag $SecondSelect`;

$inputNodes_meshlong=size($inputNodes_mesh);
$destNodes_meshlong=size($destNodes_mesh);

string $inputNodes[];
string $destNodes[];
clear $inputNodes;
clear $destNodes;
string $mesh;
string $parent;
for($d0 = 0; $d0 <$inputNodes_meshlong;$d0++){
    $mesh=$inputNodes_mesh[$d0];
    string $parentS[] = `listRelatives -parent -path -type transform $mesh`;
    $parent=$parentS[0];
    $inputNodes[size($inputNodes)] = $parent;
}
for($d0 = 0; $d0 <$destNodes_meshlong;$d0++){
    $mesh=$destNodes_mesh[$d0];
    string $parentS[] = `listRelatives -parent -path -type transform $mesh`;
    $parent=$parentS[0];
    $destNodes[size($destNodes)] = $parent;
}

print("$inputNodes= ------------------------------------------------ \n");
print($inputNodes);

print("$destNodes= ------------------------------------------------ \n");
print($destNodes);



$inputNodeslong=size($inputNodes);
$destNodeslong=size($destNodes);

string $buffer[];
string $buffer_i[];
$finded = 0;
for($d = 0; $d <$destNodeslong;$d++){
    string $destNode_name=$destNodes[$d];
    
    if($destNode_name==$SecondSelect){
            
    }else{
        print("$destNode_name= "+$destNode_name+"\n");
        
        $numTokens = `tokenize $destNode_name "|" $buffer`;
        $bufferlong=size($buffer);
        string $destNode_name_Short = $buffer[$bufferlong-1]+"";
        print("$destNode_name_Short= "+$destNode_name_Short+"\n");
        $finded = 0;
        for($i = 0; $i <$inputNodeslong;$i++){
            string $inputNode_name=$inputNodes[$i];
            $numTokens_i = `tokenize $inputNode_name "|" $buffer_i`;
            $bufferlong_i=size($buffer_i);
            string $inputNode_name_Short = $buffer_i[$bufferlong_i-1]+"";
            print("$inputNode_name_Short= "+$inputNode_name_Short+"\n");
                
            //$index=`gmatch $inputNode_name $destNode_name_Short`;
            //if($index > 0){
            if($inputNode_name_Short==$destNode_name_Short){
                //$blendShapeName = $blendshapeRet_name;
                print("$destNode_name= "+$destNode_name+" isHit!! "+"\n");
                select -r $inputNode_name;
                select -add $destNode_name;
                copySkinWeights  -noMirror -surfaceAssociation closestPoint -influenceAssociation closestJoint;
                //copySkinWeights -noMirror -surfaceAssociation closestPoint -influenceAssociation closestJoint -sourceSkin $inputNode_name -destinationSkin $destNode_name;
                $finded = 1;
            }//if
        }//for
        if($finded==0){
            $rejectArr[size($rejectArr)] = $destNode_name;
        }//if
    }//if
}//for
print("----rejected_list---start"+"\n");
print($rejectArr);
print("----rejected_list---end"+"\n");
select -r $FirstSelect ;
select -add $SecondSelect ;

適当にスキンウェイトを確認

rootとMesh 選択してFBX出力

UEへインポート

IKRig作成

IKRig設定

マネキンのほうはこう

IK Retargeter 作成

SounceにCreate>Import From Animation Sequence

MM_T_Pose選択

MM_T_Pose.FBX

https://drive.google.com/file/d/1j1O7pulCJ2Gbc2G1R-8TK3AjUmB98Lky/view?usp=sharing

ポーズがそろう

Op Stack >Add Default Ops 押す とそろった

あとは必要なアニメーションを選択してエクスポート

追加で調整した作業 RootMotionを切ってエクスポートしないとだめだった

そのあとは以下を参考にしてください。

[UE5.8]UEをログ付きで起動

Shell

Start-Process -FilePath ‘D:\Program Files\Epic Games\UE_5.8\Engine\Binaries\Win64\UnrealEditor.exe’ -ArgumentList @(‘D:\Sandbox\BookAnimation_Codex\UE574Book\UE574Book 5.8\UE574Book.uproject’,’-log’)


Start-Process -FilePath 'D:\Program Files\Epic Games\UE_5.8\Engine\Binaries\Win64\UnrealEditor.exe' -ArgumentList @('D:\Sandbox\BookAnimation_Codex\UE574Book\UE574Book 5.8\UE574Book.uproject','-log')

UE4の場合 ログを出すので、これでShader Compileが止まってるか見えるようになる。

Start-Process -FilePath ‘D:\Program Files\Epic Games\UE_4.26\Engine\Binaries\Win64\UE4Editor.exe’ -ArgumentList @(‘D:\Sandbox\UE426ArchVizInterior\UE426ArchVizInterior\UE426ArchVizInterior.uproject’,’-log’)

[UE5.7.3] Deferred ShadingかForward Shading設定によって変わるもの変わらないもの

Unreal Engine 5(UE5)におけるディファード(Deferred)とフォワード(Forward)レンダリングの設定は、プロジェクトのターゲット端末(PC/コンソールかモバイルか、VRか)によって異なります。

設定方法とそれぞれの使い分けを解説します。

Deferred ShadingかForward Shading設定

1. 設定の変更方法 (Forward Shading)

UE5はデフォルトで「ディファードレンダリング(Deferred Rendering)」が有効になっています。フォワードレンダリングに変更したい場合、以下の手順で行います。

  1. [Edit (編集)] > [Project Settings (プロジェクト設定)] を開く。
  2. [Engine] > [Rendering] セクションを選択。
  3. [Forward Shading] カテゴリを探す。
  4. [Forward Shading] チェックボックスをオンにする(フォワード)、またはオフ(ディファード)にする。
  5. プロジェクトを再起動する。

2. ディファード vs フォワードの使い分け

特徴 [1, 2, 3]ディファード (Deferred)フォワード (Forward)
主な用途PC, コンソール, ハイエンドVRモバイル, 軽量VR, VR軽量化
光源数制限なし、大量のライト少ない(主要ライトのみ)
機能Lumen, Nanite対応基本非対応(機能制限)
メモリ/負荷大容量、パスが多くメモリ転送多MSAAが利用可能

ディファード(デフォルトDeferred Shading)

  • メリット: 高品質なライティング、多数のライト、LumenやNaniteなどの高機能。
  • デメリット: フォワードに比べてGPU・メモリ負荷が高い。

フォワード(Forward Shading)

  • メリット: 高いパフォーマンス(軽量)、MSAA(高品質なアンチエイリアス)が使える、VRなどで負荷を下げたい場合に有効。
  • デメリット: 多数のライトや複雑な影の表現が苦手。

3. モバイル向けの設定 (モバイルフォワード)

モバイルでは、さらに専用のレンダリングパイプラインが推奨されます。 [1]

  1. Project Settings > Platforms > Android/iOS > Rendering。
  2. [Forward Shading] を有効にするか、[Mobile Deferred] を選択。
  3. 負荷を抑えるため、[Local Lights Buffer] などの詳細設定を調整。

まとめ

  • 高グラフィックPCゲーム: デフォルトのまま(ディファード)。
  • VR / 軽量化したいプロジェクト: Forward Shadingをオン。
  • モバイルゲーム: Mobile Forward または Mobile Deferred を選択。 [1, 2]

設定変更後はシェーダーの再コンパイルが発生するため、少し時間がかかります。

左がDeferred 右がForward

今回の場合

UE標準のライティングモデルに乗せるのではなく、UnlitのEmissive側でNdotLを計算してRampを参照しているため、DeferredかForwardかによるGBufferやライト計算の差分を受けにくい構成にしています。一方で、標準の影やGIを使う場合はLit系に寄せる必要があり、その場合はレンダリングパス差分を考慮します。

Unlit自前ライティングなら、Deferred / Forward差はほぼない。Litに乗せるなら差が出る