[UE5.7.4][Unreal Insights] Unreal Insightsを利用してのC++最適化方法

計測開始

計測終了

Unreal Insightsの起動

計測結果の選択

時間範囲計測の方法

左右ドラッグで位置移動、

マウスオーバーで このマテリアルの処理だとかが、わかる

左クリックで選択

右クリックでメニュー表示

シングルクリックしてから、別の位置で Ctrl + クリックで 範囲の時間を表示できる。


TRACE_CPUPROFILER_EVENT_SCOPEで関数をマークする。

public:
	virtual void Tick(float DeltaTime) override;

cpp

#include "ProfilingDebugging/CpuProfilerTrace.h"

void ASideScrollingCharacter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	///////////////////////////////////////////////////////////////////
	TRACE_CPUPROFILER_EVENT_SCOPE(ASideScrollingCharacter::Tick);

	// 重たい処理1
	for (int32 i = 0; i < 100; ++i)
	{
		UE_LOG(LogTemp, Log, TEXT("Omotai 1"));
	}
	// 重たい処理2
	for (int32 i = 0; i < 1000; ++i)
	{
		UE_LOG(LogTemp, Log, TEXT("Omotai 2"));
	}

	////////////////////////////////////////////////////////////////////
	//////////////////////他の処理//////////////////////////////////////
	if (!bIsOnLadder)
	{
		return;
	}
}

ビルドしてから実行

TRACE_CPUPROFILER_EVENT_SCOPE の処理の箇所を検索

1,Timersで「ASideScrollingCharacter::Tick」で検索し

2,出てきたASideScrollingCharacter::Tickを右クリックしHighlighting Eventを使い

3,出てきたASideScrollingCharacter::Tickを右クリックしFind Instance>First Instanceでスレッドが見つかる

参考URL

式会社アドグローブの公式ブログ:TRACE_CPUPROFILER_EVENT_SCOPEによるお手軽C++プロファイリング![UE5.1]
https://blog.adglobe.co.jp/entry/2023/05/12/100000

※はてなブログ(mawasiの備忘録)
https://mawasi.hateblo.jp/entry/2025/06/16/023611
によると、以前のQUICK_SCOPE_CYCLE_COUNTERより、こちらのマクロが推奨されています。
Unreal Insightsの起動: Engine/Binaries/Win64/UnrealInsights.exe を実行します。
プロファイリングの実行: エディタまたはパッケージ化したゲームを、コマンドライン引数 -trace=cpu,frame を使用して起動します。
データの確認: Unreal Insights上の「Timing Insights」タブで、埋め込んだタグ(例: MyFunction_Tag)を検索して計測結果を確認します。

関連・補足情報
詳細なボトルネック調査: Qiitaの記事

https://qiita.com/nonbiri15/items/7968e55bbf4fd61ab4df

で述べられている通り、CPUとGPUのボトルネックを特定し、ボトルネックの修正に活用できます。
関数の呼び出し回数: TRACE_CPUPROFILER_EVENT_SCOPEを使用すると、処理時間だけでなく、関数の呼び出しツリーや回数も確認できます。
詳細な情報源: 株式会社ヒストリアさんの改訂版記事

https://historia.co.jp/archives/48769

では、より詳細なUnreal Insightsの使用方法が解説されています。

この方法を使うことで、ゲームパフォーマンスの改善に役立つ正確なデータを得ることができます。

TexturePath Replace Python Script for Maya

Select HyperShade Textures And Do Script
import maya.cmds as cmds
import pymel.core as pm
import os

def remap_selected_texture(z_path):
    
    # figure out what's selected
    selected = pm.ls(sl=True)
    workspace_path = cmds.workspace(query=True, rootDirectory=True)
    print("workspace_path= "+workspace_path)
    for item in selected:
        test_path = pm.getAttr(item+".ftn")
        fileName = test_path.split('/')[-1]
        fileName = fileName.replace('//', '')
        fileName = fileName.replace('/', '')
        print("fileName2= "+fileName)
        #if ':' not in test_path:
        if(fileName=="GraceYong_ArmsColorD1001.jpg"):
            fileName="GraceYong_ArmsColorD_1001.jpg"
        if (1==1):   
            
            #print("fileName1= "+fileName)
            
            
            #new_path = os.path.join(z_path, 'cartoon_room', second)
            new_path = os.path.join(workspace_path,z_path, fileName)
            new_path = new_path.replace('\\', '/')
            new_path = new_path.replace('//', '/')
            relative_path = os.path.join(z_path, fileName)
            relative_path = relative_path.replace('\\', '/')
            new_path = new_path.replace('//', '/')
            if os.path.exists(new_path):
                if(fileName==""):
                    print("item= "+str(item)+" is No fileName= "+ fileName)
                    cmds.select( str(item), r=True )
                    cmds.delete( str(item) )
                else:
                    print("new_path= "+new_path+' is exists! OK')
                    pm.setAttr(item+'.ftn', new_path)
                #pm.setAttr(item+'.ftn', relative_path)
            else:
                print("new_path= "+new_path+ ' not exists NG' )
        if os.path.exists(test_path):
            pass
        else:
            PersonalIndex=test_path.find("Personal")
            if(PersonalIndex==-1):
                pass
            else:
                print("test_path= "+test_path+ ' not exists NG' )
                pm.setAttr(item+'.ftn', "")
                cmds.select( str(item), r=True )
                cmds.delete( str(item) )
         
    cmds.select(selected)
    print("--------------remap_selected_texture----------END")
    
remap_selected_texture("GraceYong.images")

モデルフォルダの自動認識と出力フォルダ引数のパターン

#remap_selected_filenode_texture_path
import maya.cmds as cmds
import pymel.core as pm
import os

def remap_selected_texture(z_path):
    
    # figure out what's selected
    selected = pm.ls(sl=True)
    # workspace_project_path
    workspace_path = cmds.workspace(query=True, rootDirectory=True)
    print("workspace_path= "+workspace_path)
    # ma_file_path
    ma_file_path=cmds.file(q=True, sn=True)
    print("ma_file_path= "+ma_file_path)
    ma_file_path_arr = ma_file_path.split('/')
    ma_Folder=ma_file_path_arr[-2]
    
    print("ma_Folder= "+ma_Folder)
    for item in selected:
        item_path = pm.getAttr(item+".ftn")
        print("item_path= "+item_path)
        item_path_arr = item_path.split('/')
        texFolderName = item_path_arr[-2]
        print("texFolderName= "+texFolderName)
        fileName = item_path_arr[-1]
        print("fileName= "+fileName)
        fileName = fileName.replace('//', '')
        fileName = fileName.replace('/', '')
        print("fileName2= "+fileName)
        
        
        
        #if ':' not in test_path:

        if (1==1):   
            
            #print("fileName1= "+fileName)
            
            
            #new_path = os.path.join(z_path, 'cartoon_room', second)
            new_path = os.path.join(workspace_path,z_path, fileName)
            new_path = new_path.replace('\\', '/')
            new_path = new_path.replace('//', '/')
            #relative_path = os.path.join(z_path, fileName)
            #====================================================
            relative_path = ma_Folder+"/"+z_path+"/"+fileName
            #====================================================
            print("relative_path= "+relative_path)
            #relative_path = relative_path.replace('\\', '/')
            abs_path = os.path.abspath(workspace_path+"/"+relative_path)
            abs_path = abs_path.replace('//', '/')
            print(" abs_path= "+abs_path)
            new_path= abs_path
            print(" new_path= "+new_path)
            print("----------------------------end--------------------------")
            
            if os.path.exists(new_path):
                if(fileName==""):
                    print("item= "+str(item)+" is No fileName= "+ fileName)
                    cmds.select( str(item), r=True )
                    cmds.delete( str(item) )
                else:
                    print("new_path= "+new_path+' is exists! OK')
                    pm.setAttr(item+'.ftn', new_path)
                
            else:
                print("new_path= "+new_path+ ' not exists NG' )

    cmds.select(selected)
    print("--------------remap_selected_texture----------END")
    
remap_selected_texture("texture")

Scene押してシーンディレクトリ取得、押してWalkでテクスチャファイルをwalkで自動検索

#remap_selected_filenode_texture_path
import maya.cmds as cmds
import pymel.core as pm
import os
   
def dir_walk(walkDir,TextureFileName):
    resultPath=""
    for curDir, dirs, files in os.walk(walkDir):
        print('===================')
        print("現在のディレクトリ: " + curDir)
        curDir=curDir.replace("\\","/")
        curDir_arr=curDir.split("/")
        
        curDir_endDirName = curDir_arr[-1]
        print("curDir_endDirName= "+curDir_endDirName)
        if(curDir_endDirName==".mayaSwatches"):
            print("処理をスキップします。。。")
            pass
        else:
            
            print("内包するディレクトリ:" + str(dirs))
            print("内包するファイル: " + str(files))
            for fileName in files: 
                if(fileName==TextureFileName):
                    print("Hit : fileName= "+fileName+ "== TextureFileName= "+TextureFileName)
                    resultPath=curDir+"/"+fileName
        print('===================')
    return resultPath
     
def remap_selected_texture2(sceme_path):
    
    # figure out what's selected
    selected = cmds.ls(sl=True)
    # workspace_project_path
    workspace_path = cmds.workspace(query=True, rootDirectory=True)
    print("workspace_path= "+workspace_path)
    # ma_file_path
    ma_file_path=cmds.file(q=True, sn=True)
    print("ma_file_path= "+ma_file_path)
    ma_file_path_arr = ma_file_path.split('/')
    ma_Folder=ma_file_path_arr[-2]
    
    print("ma_Folder= "+ma_Folder)
    for item in selected:
        item_path = cmds.getAttr(item+".ftn")
        print("item_path= "+item_path)
        item_path_arr = item_path.split('/')
        texFolderName = item_path_arr[-2]
        print("texFolderName= "+texFolderName)
        fileName = item_path_arr[-1]
        print("fileName= "+fileName)
        fileName = fileName.replace('//', '')
        fileName = fileName.replace('/', '')
        print("fileName2= "+fileName)
        
        fixTexturePath=dir_walk(sceme_path,fileName)
        print("fixTexturePath= "+fixTexturePath)
        #if ':' not in test_path:

        if (1==1):   
            
            #print("fileName1= "+fileName)
            
            
            #new_path = os.path.join(z_path, 'cartoon_room', second)
            #new_path = os.path.join(workspace_path,z_path, fileName)
            new_path = fixTexturePath
            new_path = new_path.replace('\\', '/')
            new_path = new_path.replace('//', '/')
            #relative_path = os.path.join(z_path, fileName)
            #====================================================
            #relative_path = ma_Folder+"/"+z_path+"/"+fileName
            #====================================================
            #print("relative_path= "+relative_path)
            #relative_path = relative_path.replace('\\', '/')
            #abs_path = os.path.abspath(workspace_path+"/"+relative_path)
            #abs_path = abs_path.replace('//', '/')
            #print(" abs_path= "+abs_path)
            #new_path= abs_path
            print(" new_path= "+new_path)
            print("----------------------------end--------------------------")
            
            if os.path.exists(new_path):
                if(fileName==""):
                    print("item= "+str(item)+" is No fileName= "+ fileName)
                    cmds.select( str(item), r=True )
                    cmds.delete( str(item) )
                else:
                    print("new_path= "+new_path+' is exists! OK')
                    #cmds.setAttr(item+'.ftn', new_path)
                    cmds.setAttr(item+'.fileTextureName', new_path,type='string')
            else:
                print("new_path= "+new_path+ ' not exists NG' )

    cmds.select(selected)
    print("--------------remap_selected_texture----------END")
    

def remap_fileNode_texture(self):
    text_Field_id="remap_selected_filenode_texture_path_Window|USD_layout|pathTxtFld"
    textField_outputFolder = cmds.textField(text_Field_id, q=True, text=True)
    remap_selected_texture2(textField_outputFolder)

def get_scenePath():
    scenefilePath = cmds.file(q=1, sceneName=1)
    mayaPath,mayaFile = os.path.split(scenefilePath)
    #mayaPath = mayaPath + "/Usd/"
    #mayaPath = mayaPath + "/"
    mayaPath=os.path.abspath(mayaPath)
    mayaPath=mayaPath.replace('\\', '/')
    print("mayaPath= "+mayaPath)
    mayaPath_len=len(mayaPath)
    last_str=mayaPath[mayaPath_len-1:]
    print("mayaPath= "+mayaPath+ " last_str= "+last_str)
    if(last_str=="/"):
        pass
    else:
        mayaPath=mayaPath+"/"
    
    return mayaPath

def btn_scene(self):
    scenePath = get_scenePath()
    
    #textureFolder= cmds.textField('textField_outputFolder', q=True, text=True)
    
    
    
    #lastExportDirPath=scenePath+textureFolder+"/"
    
    
    
    set_lastExportDirPath(scenePath)


def set_lastExportDirPath(lastExportDirPath):
    
    selectList=cmds.ls(sl=True)
    if(str(selectList)== "[]"):
        print("なにも選択されていません。0 set_lastExportDirPath")

            
    text_Field_id="remap_selected_filenode_texture_path_Window|USD_layout|pathTxtFld"
    cmds.textField(text_Field_id, edit=True, text=lastExportDirPath)
    cmds.select(selectList)



def createWindow():
    scenefilePath = cmds.file(q=1, sceneName=1)

    USD_window = cmds.window("remap_selected_filenode_texture_path_Window", widthHeight=(400, 200))
    USD_layout = cmds.columnLayout("USD_layout",adjustableColumn=True, parent=USD_window)
    cmds.text (label="選択したハイパーシェードのテクスチャのパスを置換するツールです。", align='left', parent=USD_layout)
    cmds.text (label="シーンのパス以下のフォルダーから検索して自動的に置換します。", align='left', parent=USD_layout)
    cmds.separator(parent=USD_layout)
    cmds.text (label="", align='left', parent=USD_layout)
    
    cmds.text (label="1、Sceneを押してください。", align='left', parent=USD_layout)
    cmds.button(label="Scene", command=btn_scene, parent=USD_layout)
    text_field = cmds.textField("pathTxtFld", parent="USD_layout",text="")
    #cmds.text (label="2、テクスチャフォルダ名を指定してください。", align='left', parent=USD_layout)
    #text_field = cmds.textField("textField_outputFolder", parent="USD_layout",text="texture")
    cmds.separator(parent=USD_layout)
    cmds.text (label="", align='left', parent=USD_layout)

    
    
    
    cmds.text (label="2、HyperShadeのテクスチャTabで(例:file1,file2,複数可)を選択して、ボタンを押してください。", align='left', parent=USD_layout)
    #CheckBox_Absolute_Path_Bool = cmds.checkBox('CheckBox_Absolute_Path', q=True, value=True)
    cmds.button(label="Remap FileNode Texture ", command=remap_fileNode_texture, parent=USD_layout)
    
    #cmds.separator(parent=USD_layout)

    #cmds.button(label="Add USD Referernce/Payload... ", command=add_prim_xform_for_stage_layer, parent=USD_layout)

    cmds.showWindow(USD_window)
    return None

    
def remap_selected_filenode_texture_path():
    if cmds.window("remap_selected_filenode_texture_path_Window",exists=True):
        cmds.deleteUI("remap_selected_filenode_texture_path_Window")
    createWindow()



remap_selected_filenode_texture_path()

fileノードのパスのバックスラッシュに対応しました。

#remap_selected_filenode_texture_path
import maya.cmds as cmds
import pymel.core as pm
import os
   
def dir_walk(walkDir,TextureFileName):
    resultPath=""
    for curDir, dirs, files in os.walk(walkDir):
        print('===================')
        print("現在のディレクトリ: " + curDir)
        curDir=curDir.replace("\\","/")
        curDir_arr=curDir.split("/")
        
        curDir_endDirName = curDir_arr[-1]
        print("curDir_endDirName= "+curDir_endDirName)
        if(curDir_endDirName==".mayaSwatches"):
            print("処理をスキップします。。。")
            pass
        else:
            
            print("内包するディレクトリ:" + str(dirs))
            print("内包するファイル: " + str(files))
            for fileName in files: 
                if(fileName==TextureFileName):
                    print("Hit : fileName= "+fileName+ "== TextureFileName= "+TextureFileName)
                    resultPath=curDir+"/"+fileName
        print('===================')
    return resultPath
     
def remap_selected_texture2(sceme_path):
    
    # figure out what's selected
    selected = cmds.ls(sl=True)
    # workspace_project_path
    workspace_path = cmds.workspace(query=True, rootDirectory=True)
    print("workspace_path= "+workspace_path)
    # ma_file_path
    ma_file_path=cmds.file(q=True, sn=True)
    print("ma_file_path= "+ma_file_path)
    ma_file_path_arr = ma_file_path.split('/')
    ma_Folder=ma_file_path_arr[-2]
    
    print("ma_Folder= "+ma_Folder)
    for item in selected:
        item_path = cmds.getAttr(item+".ftn")
        print("item_path= "+item_path)
        safe_path = item_path.replace("\\", "/")
        print("safe_path= "+safe_path)
        item_path_arr = safe_path.split('/')
        print("item_path_arr[0]= "+item_path_arr[0])
        
        print("item_path_arr[-1]= "+item_path_arr[-1])
        print("item_path_arr[-2]= "+item_path_arr[-2])
        texFolderName = item_path_arr[-2]
        print("texFolderName= "+texFolderName)
        fileName = item_path_arr[-1]
        print("fileName= "+fileName)
        fileName = fileName.replace('//', '')
        fileName = fileName.replace('/', '')
        print("fileName2= "+fileName)
        
        fixTexturePath=dir_walk(sceme_path,fileName)
        print("fixTexturePath= "+fixTexturePath)
        #if ':' not in test_path:

        if (1==1):   
            
            #print("fileName1= "+fileName)
            
            
            #new_path = os.path.join(z_path, 'cartoon_room', second)
            #new_path = os.path.join(workspace_path,z_path, fileName)
            new_path = fixTexturePath
            new_path = new_path.replace('\\', '/')
            new_path = new_path.replace('//', '/')
            #relative_path = os.path.join(z_path, fileName)
            #====================================================
            #relative_path = ma_Folder+"/"+z_path+"/"+fileName
            #====================================================
            #print("relative_path= "+relative_path)
            #relative_path = relative_path.replace('\\', '/')
            #abs_path = os.path.abspath(workspace_path+"/"+relative_path)
            #abs_path = abs_path.replace('//', '/')
            #print(" abs_path= "+abs_path)
            #new_path= abs_path
            print(" new_path= "+new_path)
            print("----------------------------end--------------------------")
            
            if os.path.exists(new_path):
                if(fileName==""):
                    print("item= "+str(item)+" is No fileName= "+ fileName)
                    cmds.select( str(item), r=True )
                    cmds.delete( str(item) )
                else:
                    print("new_path= "+new_path+' is exists! OK')
                    #cmds.setAttr(item+'.ftn', new_path)
                    cmds.setAttr(item+'.fileTextureName', new_path,type='string')
            else:
                print("new_path= "+new_path+ ' not exists NG' )

    cmds.select(selected)
    print("--------------remap_selected_texture----------END")
    

def remap_fileNode_texture(self):
    text_Field_id="remap_selected_filenode_texture_path_Window|USD_layout|pathTxtFld"
    textField_outputFolder = cmds.textField(text_Field_id, q=True, text=True)
    remap_selected_texture2(textField_outputFolder)

def get_scenePath():
    scenefilePath = cmds.file(q=1, sceneName=1)
    mayaPath,mayaFile = os.path.split(scenefilePath)
    #mayaPath = mayaPath + "/Usd/"
    #mayaPath = mayaPath + "/"
    mayaPath=os.path.abspath(mayaPath)
    mayaPath=mayaPath.replace('\\', '/')
    print("mayaPath= "+mayaPath)
    mayaPath_len=len(mayaPath)
    last_str=mayaPath[mayaPath_len-1:]
    print("mayaPath= "+mayaPath+ " last_str= "+last_str)
    if(last_str=="/"):
        pass
    else:
        mayaPath=mayaPath+"/"
    
    return mayaPath

def btn_scene(self):
    scenePath = get_scenePath()
    
    #textureFolder= cmds.textField('textField_outputFolder', q=True, text=True)
    
    
    
    #lastExportDirPath=scenePath+textureFolder+"/"
    
    
    
    set_lastExportDirPath(scenePath)


def set_lastExportDirPath(lastExportDirPath):
    
    selectList=cmds.ls(sl=True)
    if(str(selectList)== "[]"):
        print("なにも選択されていません。0 set_lastExportDirPath")

            
    text_Field_id="remap_selected_filenode_texture_path_Window|USD_layout|pathTxtFld"
    cmds.textField(text_Field_id, edit=True, text=lastExportDirPath)
    cmds.select(selectList)



def createWindow():
    scenefilePath = cmds.file(q=1, sceneName=1)

    USD_window = cmds.window("remap_selected_filenode_texture_path_Window", widthHeight=(400, 200))
    USD_layout = cmds.columnLayout("USD_layout",adjustableColumn=True, parent=USD_window)
    cmds.text (label="選択したハイパーシェードのテクスチャのパスを置換するツールです。", align='left', parent=USD_layout)
    cmds.text (label="シーンのパス以下のフォルダーから検索して自動的に置換します。", align='left', parent=USD_layout)
    cmds.separator(parent=USD_layout)
    cmds.text (label="", align='left', parent=USD_layout)
    
    cmds.text (label="1、Sceneを押してください。", align='left', parent=USD_layout)
    cmds.button(label="Scene", command=btn_scene, parent=USD_layout)
    text_field = cmds.textField("pathTxtFld", parent="USD_layout",text="")
    #cmds.text (label="2、テクスチャフォルダ名を指定してください。", align='left', parent=USD_layout)
    #text_field = cmds.textField("textField_outputFolder", parent="USD_layout",text="texture")
    cmds.separator(parent=USD_layout)
    cmds.text (label="", align='left', parent=USD_layout)

    
    
    
    cmds.text (label="2、HyperShadeのテクスチャTabで(例:file1,file2,複数可)を選択して、ボタンを押してください。", align='left', parent=USD_layout)
    #CheckBox_Absolute_Path_Bool = cmds.checkBox('CheckBox_Absolute_Path', q=True, value=True)
    cmds.button(label="Remap FileNode Texture ", command=remap_fileNode_texture, parent=USD_layout)
    
    #cmds.separator(parent=USD_layout)

    #cmds.button(label="Add USD Referernce/Payload... ", command=add_prim_xform_for_stage_layer, parent=USD_layout)

    cmds.showWindow(USD_window)
    return None

    
def remap_selected_filenode_texture_path():
    if cmds.window("remap_selected_filenode_texture_path_Window",exists=True):
        cmds.deleteUI("remap_selected_filenode_texture_path_Window")
    createWindow()



remap_selected_filenode_texture_path()

[maya][UE5.7.3]USD workflow pipline tool :レイヤー持ち背景のインポート

[Maya][UE5.7.3] USD workflow pipeline tool: Importing backgrounds with layers

https://github.com/nobolu-ootsuka-unrealengine/furcraeaMayaTool

kind を設定版を作ってUE で毎回オプションを触らなくて済むようにします。

USD Export Selection

Window > USD Stage Windowを開き


Openから S_Stg_Furcraea_Gate_Com_Geom.usda を選択


USD Stage Editor >option>collapsing>Use prim kinds for collapsingをoffにしたらStaticMeshが分かれた

Import

インポートフォルダ選択

インポートオプションにも Kind To Collapseがあるがこの設定にした。

一番上のGeom以下の全部選択して>Level>Create Packed Actor

Level Instance

BPP

BPP

Level Instance

レイヤー持ち背景のインポートできあがり。

[maya][mel]選択したグループに入った大量のメッシュのUVを自動配置するselect_Mesh_Group_To_replace_UV.mel

[maya][mel] select_Mesh_Group_To_replace_UV.mel – Automatically places UVs on a large number of meshes in a group.

ひさしぶりに楽しい楽しいmel script codingした。

Nurvs モデリングしたあとUVスケールが1×1になって全メッシュのUVが重なってる状態から

スクリプト実行で

自動配置できる。


select_Mesh_Group_To_replace_UV.mel

//実行
float $widthU = 0.05; //幅
float $heightV = 0.05; //高さ
float $ScaleU_ = 0.0625; //スケールX
float $ScaleV_ = 0.0625; //スケールY
float $newLineU_ =0.4; //改行位置
select_Mesh_Group_To_replace_UV($widthU,$heightV,$ScaleU_,$ScaleV_,$newLineU_);

//select Mesh Group To replace UV
global proc select_Mesh_Group_To_replace_UV(float $width,float $height,float $ScaleU,float $ScaleV ,float $newLineU)
{
    string $selectedArrFUllPath[] = `ls -long -sl`;
    print($selectedArrFUllPath);
    
    string $FirstSelect=$selectedArrFUllPath[0];
    string $inputNodes_mesh[] = `ls -type mesh -long -dag $FirstSelect`;
    $inputNodes_meshlong=size($inputNodes_mesh);
    string $inputNodes[];
    clear $inputNodes;
    string $mesh;
    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;
        $bool=`gmatch $mesh "*Orig"`;
        if($bool==0){
            $inputNodes[size($inputNodes)] = $mesh;
        }
    }
    
    
    
    print("$inputNodes= ------------------------------------------------ \n");
    print($inputNodes);
    print("------------------------------------------------------------- \n");
    
    $inputNodeslong=size($inputNodes);
    
    float $buildX=0;
    float $buildY=0;
    //float $width=0.05;
    //float $height=0.05;
    for($f = 0; $f <$inputNodeslong;$f++){
        $mesh=$inputNodes[$f];
        $face=$mesh+".f[0:]";
        
        select -r $mesh;
        polyAutoProjection -lm 0 -pb 0 -ibd 1 -cm 0 -l 2 -sc 1 -o 1 -p 6 -ps 0.2 -ws 0 $face;
        select -r $face;
        
        $uvPivot=$mesh+".uvPivot";
        //setAttr $uvPivot -type double2 0.5 0.5 ;
        
        $U=$buildX*1-0.0;
        $V=$buildY*1-0.0;
        print("U:"+$U+" V:"+$V+"\n");
        //polyEditUV -relative false -u $buildX -v $buildY;
        $PivotU=$U+$width/2;
        $PivotV=$V+$height/2;
        polyEditUV -relative false -pu $PivotU -pv $PivotV -su $ScaleU -sv $ScaleU -u $buildX -v $buildY;
        //polyEditUV -relative false -pu $PivotU -pv $PivotV -su 0.8 -sv 0.8 ;
        //polyEditUV -relative false -pu $U -pv $V -su 0.2 -sv 0.2 ;
        if($buildX>$newLineU){
            print("new y----------Line\n");
            $buildY = $buildY+$width;
            $buildX = -$width;
        }
        $buildX = $buildX + $width;
    }
}
float $widthU = 0.05; //幅
float $heightV = 0.05; //高さ
float $ScaleU_ = 0.0625; //スケールX
float $ScaleV_ = 0.0625; //スケールY
float $newLineU_ =0.4; //改行位置
select_Mesh_Group_To_replace_UV($widthU,$heightV,$ScaleU_,$ScaleV_,$newLineU_);
/*
参考スクリプト
select -r group24_rearBoost_Up2.f[0:1601] group4_rear_Body1.f[0:7323] ;
setAttr "group24_rearBoost_UpShape2.uvPivot" -type double2 0.5 2.504208 ;
polyEditUV -u 0 -v 2.004208 ;
polyEditUV -pu 0.5 -pv 2.504208 -su 0.25 -sv 0.25 ;
*/

わかった事
polyEditUV コマンドは一発で6パラメータ入れないとだめ、

polyEditUV -relative false -pu $PivotU -pv $PivotV -su $ScaleU -sv $ScaleU -u $buildX -v $buildY;

polyEditUVコマンドは絶対スケール値を指定しても相対スケールが入る

polyEditUV -relative false -pu $PivotU -pv $PivotV -su $ScaleU -sv $ScaleU -u $buildX -v $buildY;

[mGear5.3.1] Guide Template Manager のEPIC_mannequin_y_upとEPIC_mannequin_z_upの違いの検証

EPIC_mannequin_y_up 今回のモデルにあってる。

EPIC_mannequin_z_up たしかにUEマネキンがそういう向きのことあったよね

試しに

UE5.7.4のマネキンを出力してみた。

SKM_Quinn_Simple

でEPIC_mannequin_y_upのほうをあてると 合う

SKM_Manny_Simple

同じくEPIC_mannequin_y_upが合う

うーんZのほうを向いたマネキンはUE4マネキンなのか?

UE4マネキンをContentsExample(機能別サンプル)からもってきて。。

export

SK_Mannequin

EPIC_mannequin_y_upが UE4マネキンにびったびたに合った!!

UE4マネキン基準ということか!!!!!!クインとマニーはまだ受け入れていないのかmGearさんw

mGear > Shifter > Build From Selectionでビルド

ビルド結果

かわいいUE4マネキン

愛しき屈伸SK_Mannequin

[Houdini] Houdini Technical Artist Procedural HDA関連 031 HDAのパラメータ設計とメッシュ交換システム

[Houdini] Houdini Technical Artist Procedural HDA Related 031 Parameters and Interchangeable Meshes

① attribpaint1 を選択

ノードをクリック
② ビューポートで
Enterキー押す
③ すると変わる
カーソルがブラシになる

Geometry Spreadsheet 

Scene View | Animation Editor | Render View | 👉 Geometry Spreadsheet 

Alt + Shift + G

vector Nn = normalize(@N);

// maskで生やす場所制御
if (@mask < 0.5) {
    removepoint(0, @ptnum);
    return;
}

// 急斜面を除外
if (Nn.y < 0.7) {
    removepoint(0, @ptnum);
    return;
}

if (s@name == "tree" && rand(@ptnum) < 0.7) {
    removepoint(0, @ptnum);
}
if (s@name == "tree" && @mask < 0.8) {
    removepoint(0, @ptnum);
}

// 傾き調整
vector worldUp = {0,1,0};
Nn = normalize(lerp(worldUp, Nn, 0.3));

// 向き
matrix3 m = dihedral({0,1,0}, Nn);
//p@orient = quaternion(m);

if (s@name == "tree") {
    // 木は基本まっすぐ
}
else if (s@name == "rock") {
    float angle = rand(@ptnum) * M_PI * 2;
    p@orient = qmultiply(p@orient, quaternion(angle, {0,1,0}));
}
else {
    // 草はちょい倒す
    vector tilt = normalize(Nn + rand(@ptnum));
    p@orient = quaternion(dihedral({0,1,0}, tilt));
}





// ひねり
float angle = rand(@ptnum + 10.0) * M_PI * 2.0;
p@orient = qmultiply(p@orient, quaternion(angle, Nn));

// サイズ
@pscale = fit01(rand(@ptnum), 0.5, 1.5);

float d = length(@P);
@pscale *= fit(d, 0, 10, 1.5, 0.3);

// 浮かせる
@P += Nn * (@pscale * 0.5);

// 種類切り替え
float r = rand(@ptnum * 17.123);

if (r < 0.33) {
    s@name = "tree";
    @pscale *= fit01(rand(@ptnum + 100.0), 0.9, 1.4);
}
else if (r < 0.66) {
    s@name = "rock";
    @pscale *= fit01(rand(@ptnum + 200.0), 0.05, 0.06);
}
else {
    s@name = "grass";
    @pscale *= fit01(rand(@ptnum + 300.0), 0.4, 0.9);
}

1.HDA最短ルート

今のネットワークをそのままHDAにするんじゃなくて、UEで触りたい入口だけ残して整理する。

やる順番はこれ。

1. HDA用の最終ノードを1本にまとめる

今は途中ノードが多いから、最後に Null を1個作って名前を

OUT_FOLIAGE

にする。

UEに持っていく出力はこれだけにする。

2. UEで触りたい値だけパラメータ化する

まず必要なのはこのへんだけでいい。

  • Scatterの数
  • Mountainの高さ
  • maskしきい値
  • tree / rock / grass の出現率
  • 各種類のスケール

たとえば Wrangle でベタ書きしてる数値を消して、チャンネル参照に変える。

今こういうのを

if (@mask < 0.5) {
removepoint(0, @ptnum);
return;
}

こうする。

float mask_threshold = ch("mask_threshold");
if (@mask < mask_threshold) {
removepoint(0, @ptnum);
return;
}

3. 種類の比率もパラメータ化

今のランダム分岐もUEで触れるようにする。

float tree_ratio = ch("tree_ratio");
float rock_ratio = ch("rock_ratio");float r = rand(@ptnum * 17.123);if (r < tree_ratio) {
s@name = "tree";
@pscale *= fit01(rand(@ptnum + 100.0), 0.9, 1.4);
}
else if (r < tree_ratio + rock_ratio) {
s@name = "rock";
@pscale *= fit01(rand(@ptnum + 200.0), 0.25, 0.6);
}
else {
s@name = "grass";
@pscale *= fit01(rand(@ptnum + 300.0), 0.4, 0.9);
}

これなら UE 側で tree_ratio と rock_ratio を触れる。

4. 地形入力を固定生成から「外部入力」に変える

UEで使うなら、Houdini内の grid1 をそのまま使うより、UEの地形やメッシュを入力できる形の方が強い。

最初の段階ではこうすると早い。

方法

grid1 の代わりに Object Merge を置く。
そのObject Mergeの元に、HDA入力を使う。

最初は難しく感じるから、超最短なら今はこうでもいい。

  • 今の grid1 → mountain1 構成のままHDA化
  • UE上では「生成ツール」として使う
  • その後、入力メッシュ対応に拡張する

まず持っていくことが優先なら、これで十分。

5. 不要な表示用ノードを外す

merge1 は確認用だから、HDA出力には入れない方が楽。

出力は基本、

copytopoints1 → OUT_FOLIAGE

だけでいい。

地面まで一緒に出したいなら別Nullにする。

OUT_INSTANCES
OUT_TERRAIN

でも最初は OUT_FOLIAGE だけでいい。

attribwrangle1 の完成版

vector Nn = normalize(@N);

float mask_threshold = ch("mask_threshold");
if (@mask < mask_threshold) {
    removepoint(0, @ptnum);
    return;
}

float slope_threshold = ch("slope_threshold");
if (Nn.y < slope_threshold) {
    removepoint(0, @ptnum);
    return;
}

vector worldUp = {0,1,0};
float upright_blend = ch("upright_blend");
Nn = normalize(lerp(worldUp, Nn, upright_blend));

matrix3 m = dihedral({0,1,0}, Nn);
p@orient = quaternion(m);

float angle = rand(@ptnum + 10.0) * M_PI * 2.0;
p@orient = qmultiply(p@orient, quaternion(angle, Nn));

float base_scale_min = ch("base_scale_min");
float base_scale_max = ch("base_scale_max");
@pscale = fit01(rand(@ptnum), base_scale_min, base_scale_max);

float tree_ratio = ch("tree_ratio");
float rock_ratio = ch("rock_ratio");
float r = rand(@ptnum * 17.123);

if (r < tree_ratio) {
    s@name = "tree";
    @pscale *= fit01(rand(@ptnum + 100.0), ch("tree_scale_min"), ch("tree_scale_max"));
}
else if (r < tree_ratio + rock_ratio) {
    s@name = "rock";
    @pscale *= fit01(rand(@ptnum + 200.0), ch("rock_scale_min"), ch("rock_scale_max"));
}
else {
    s@name = "grass";
    @pscale *= fit01(rand(@ptnum + 300.0), ch("grass_scale_min"), ch("grass_scale_max"));
}

float offset_mult = ch("offset_mult");
@P += Nn * (@pscale * offset_mult);

最初に作るパラメータ

この名前で作ればかなり使いやすい。

mask_threshold   = 0.5
slope_threshold = 0.7
upright_blend = 0.3

base_scale_min = 0.5
base_scale_max = 1.5

tree_ratio = 0.33
rock_ratio = 0.33

tree_scale_min = 0.9
tree_scale_max = 1.4

rock_scale_min = 0.05
rock_scale_max = 0.06

grass_scale_min = 0.4
grass_scale_max = 0.9

offset_mult = 0.5

HDA化の手順

1

最終的に使うノード群を全部選ぶ

2

サブネット化する

3

そのサブネットを右クリックして Create Digital Asset

4

名前を付ける
たとえば

furcraea_foliageScatter

5

Type Properties を開いて、今作ったパラメータを見えるようにする

UEに持っていくときの最初の形

最初は欲張らずこうするのが早い。

  • 入力なし
  • HDAを置く
  • パラメータを触る
  • インスタンスが更新される

これでまず成功体験を作る。

その後に

  • Static Mesh Input
  • Landscape Input
  • World Partition対応
  • bake

を足せばいい。

最終ノード構成

[VARIANTS]
box1
└→ attribwrangle2 // s@name="tree";
└→ pack1
└→ attribwrangle5 // s@name="tree";

sphere1
└→ attribwrangle3 // s@name="rock";
└→ pack2
└→ attribwrangle6 // s@name="rock";

tube1
└→ attribwrangle4 // s@name="grass";
└→ pack3
└→ attribwrangle7 // s@name="grass";

attribwrangle5 / 6 / 7
└→ merge_variants

[PLACEMENT]
grid1
└→ mountain1
└→ normal1
└→ attribpaint1
└→ scatter1
└→ attribwrangle1

[OUTPUT]
merge_variants ───────────────→ copytopoints1 (左)
attribwrangle1 ───────────────→ copytopoints1 (右)
copytopoints1 ────────────────→ OUT_FOLIAGE

HDAで外に出すパラメータ

まずはこれだけで十分。

地形

  • mountain_height
  • scatter_count

マスク・斜面

  • mask_threshold
  • slope_threshold
  • upright_blend

全体サイズ

  • base_scale_min
  • base_scale_max

出現率

  • tree_ratio
  • rock_ratio

種類別サイズ

  • tree_scale_min
  • tree_scale_max
  • rock_scale_min
  • rock_scale_max
  • grass_scale_min
  • grass_scale_max

浮かせ量

  • offset_mult

それぞれどこに仕込むか

mountain1

Height を promote
名前は mountain_height

scatter1

Force Total Count を promote
名前は scatter_count

attribwrangle1

今の ch("...") がそのまま出せる

1回HDAにしたあとの出力方法がわからない

① HDAとしてそのままUEで使う(←メイン)

手順

① OUTノードを確認

最後がこれになってるか👇

copytopoints1 → OUT_FOLIAGE

👉 Nullの名前が重要

OUT_FOLIAGE

② HDA化

  1. objレベルに戻る
  2. box_object1 を右クリック
  3. 👉 Create Digital Asset
FoliageScatter
  1. 保存:
.hda

③ 保存(超重要)

👉 HDAは「Save Node Type」しないと更新されない

右クリック → Save Node Type

④ Unreal Engine 側

  1. Houdini Engine Plugin 有効化
  2. .hda をContent Browserにドラッグ
  3. レベルに置く

⑤ 出てこない場合チェック

  • OUTノードが無い
  • OUTじゃないノードを表示してる
  • copytopointsの後じゃない

これ、エラーじゃなくて**「UIレイアウトどうする?」って確認ダイアログ**だから安心していい。

✅ 今回はこれ押す

👉 Revert Layout


UEのバージョンはどれにする?
Houdini 21.x
👉 UE5.6 / 5.7 が正解

① Houdini側にあるPluginを使う

C:\Program Files\Side Effects Software\Houdini 21.0.xxx\engine\unreal

C:\Program Files\Side Effects Software\Houdini 21.0.671\engineに

unreal

ない

① Houdini Launcher開く

あるなら

C:\Program Files\Side Effects Software\Houdini Engine\Unreal\21.0.671

BLANK

Plugins
にいれる

Houdini Engineにチェックいれる

HDAをドラッグ

正しいノード構成

Subnetwork Input #1

object_merge1 Object1=../Subnetwork Input #1
object_merge1 Object1=../subnetwork_input1
object_merge1 Object1=opinputpath("..", 0)

scatter1

attribwrangle

copytopoints1

OUT_FOLIAGE

いみない


attribwrangle8

vector Nn = {0,1,0};
if (len(@N) > 0.0) {
    Nn = normalize(@N);
}

if (haspointattrib(0, "mask")) {
    float mask_threshold = ch("mask_threshold");
    if (@mask < mask_threshold) {
        removepoint(0, @ptnum);
        return;
    }
}

float slope_threshold = ch("slope_threshold");
if (Nn.y < slope_threshold) {
    removepoint(0, @ptnum);
    return;
}

matrix3 m = dihedral({0,1,0}, Nn);
p@orient = quaternion(m);

float angle = rand(@ptnum + 10.0) * M_PI * 2.0;
p@orient = qmultiply(p@orient, quaternion(angle, Nn));

@pscale = 1.0;



これで見える状態

書き出して



UEでも


これでいい この△だらけのデータをビビらずにUEに持っていく。

UE OK

さらに normalノード復帰

vector Nn = {0,1,0};
if (len(@N) > 0.0) {
    Nn = normalize(@N);
}

if (haspointattrib(0, "mask")) {
    float mask_threshold = ch("mask_threshold");
    if (@mask < mask_threshold) {
        removepoint(0, @ptnum);
        return;
    }
}

float slope_threshold = ch("slope_threshold");
if (Nn.y < slope_threshold) {
    removepoint(0, @ptnum);
    return;
}

if (length(v@N) > 1e-6)
{
    Nn = normalize(v@N);
}

matrix3 m = dihedral({0,1,0}, Nn);
p@orient = quaternion(m);

float angle = rand(@ptnum + 10.0) * M_PI * 2.0;
p@orient = qmultiply(p@orient, quaternion(angle, Nn));

@pscale = 1.0;

HOUDINIで動作確認後

書き出し

Normalノード追加で復帰した

この状態

vector Nn = {0,1,0};
if (length(v@N) > 1e-6)
{
    Nn = normalize(v@N);
}

if (haspointattrib(0, "mask"))
{
    float mask_threshold = ch("mask_threshold");
    if (f@mask < mask_threshold)
    {
        removepoint(0, @ptnum);
        return;
    }
}

float slope_threshold = ch("slope_threshold");
if (Nn.y < slope_threshold)
{
    removepoint(0, @ptnum);
    return;
}

matrix3 m = dihedral({0,1,0}, Nn);
p@orient = quaternion(m);

float angle = rand(@ptnum + 10.0) * M_PI * 2.0;
p@orient = qmultiply(p@orient, quaternion(angle, Nn));

@pscale = fit01(rand(@ptnum), 0.5, 1.5);

@P += Nn * @pscale * 0.2;

ch(“slope_threshold”);のパラメータ化

① attribwrangle8 選択

② 右のパラメータ欄の上にある

👉 ⚙️(歯車) → Edit Parameter Interface


③ 左側から追加

  • Float をドラッグ
  • 名前を設定👇
Name: slope_threshold
Label: slope_threshold

④ Default値入れる

例:

0.3

⑤ Accept
👉 Save Node Type(超重要)

② 一番上のノード(HDA本体)を選択

👉 furcraea_foliagescatter(一番外のやつ)


③ 右クリック

👉 Type Properties


④ Parametersタブ

今の状態👇

  • slope_threshold は「Wrangleの中」
  • HDA側には無い

⑤ ここでやる

左側の一覧から👇

👉 attribwrangle8 の slope_threshold をドラッグ

👉 右側(HDAのUI)にドロップ

../ を追加

vector Nn = {0,1,0};
if (length(v@N) > 1e-6)
{
    Nn = normalize(v@N);
}

if (haspointattrib(0, "mask"))
{
    float mask_threshold = ch("../mask_threshold");
    if (f@mask < mask_threshold)
    {
        removepoint(0, @ptnum);
        return;
    }
}

float slope_threshold = ch("../slope_threshold");

if (Nn.y < slope_threshold)
{
    removepoint(0, @ptnum);
    return;
}

matrix3 m = dihedral({0,1,0}, Nn);
p@orient = quaternion(m);

float angle = rand(@ptnum + 10.0) * M_PI * 2.0;
p@orient = qmultiply(p@orient, quaternion(angle, Nn));

@pscale = fit01(rand(@ptnum), 0.5, 1.5);

@P += Nn * @pscale * 0.2;


ちゃんと 1.01で消えた

👉 UE → Houdini → Wrangle → パラメータ制御

全部つながった

これはかなりデカい


🧠 今の到達点

要素状態
入力
scatter
orient
pscale
offset
slope制御
UEパラメータ連動

👉 プロシージャルツールとして成立してる

つまり問題はここ

👉 法線の分布が単調すぎる

ノード構成

furcraea_foliagescatter3 内で

Subnetwork Input #1

normal_input

attribnoise1

normal1

attribwrangle9 (f@slope を作る)

scatter1

attribwrangle8 (slope_threshold で削除)

copytopoints1

attribwrangle9 に入れるコード (scatter 前に slope を保存するノード)

ノード名: attribwrangle9
役割: scatter 前に slope 属性を作る

VEX 全文をこれに置き換えてください。

vector Nn = {0, 1, 0};
if (length(v@N) > 1e-6)
{
    Nn = normalize(v@N);
}

// slope: 0 = 平地, 1 = 垂直
f@slope = 1.0 - clamp(Nn.y, 0.0, 1.0);

接続:

normal1 → attribwrangle9 → scatter1

attribwrangle8 に入れるコード(scatter 後に slope_threshold で消すノード

ノード名: attribwrangle8
役割: scatter 後のポイントを slope で削除する

VEX 全文をこれに置き換えてください。使いやすいレンジに圧縮します。

float ui_threshold = ch("../slope_threshold");

// UIの0.0~1.0を、実際の有効域に再マップ
float slope_threshold = fit(clamp(ui_threshold, 0.0, 1.0), 0.0, 1.0, 0.8, 1.0);

if (f@slope > slope_threshold)
{
    removepoint(0, @ptnum);
    return;
}

@pscale = 0.05;

接続:
scatter1 → attribwrangle8 → copytopoints1

0.0から増えて1.0から増えなくなった 完璧だね
今の挙動なら slope_threshold が UE 側で素直に使える状態 になっています。

パラメータ制御 + メッシュの入れ替えができてます

[mGear5.3.1] Auto Fit Biped でMatch Guidesの内部のサイズをマッチさせる処理がSmart AdjustがRunでエラー吐いてるからMatch Guidesは使えないから自分で自動化した。

# Traceback (most recent call last):
#   File "D:\Sandbox\mgear\modules\scripts\mgear\shifter\afg_tools_ui.py", line 555, in runSmartAdjust
#     spine_height_only=True)
#   File "D:\Sandbox\mgear\modules\scripts\mgear\core\utils.py", line 265, in wrap
#     raise e
#   File "D:\Sandbox\mgear\modules\scripts\mgear\core\utils.py", line 262, in wrap
#     return func(*args, **kwargs)
#   File "D:\Sandbox\mgear\modules\scripts\mgear\shifter\afg_tools.py", line 700, in smartAdjustEmbedOutput
#     replace=SIDE_MIRROR_INFO[favor_side])
#   File "D:\Sandbox\mgear\modules\scripts\mgear\core\utils.py", line 265, in wrap
#     raise e
#   File "D:\Sandbox\mgear\modules\scripts\mgear\core\utils.py", line 262, in wrap
#     return func(*args, **kwargs)
#   File "D:\Sandbox\mgear\modules\scripts\mgear\shifter\afg_tools.py", line 572, in mirrorEmbedNodesSide
#     mirrorEmbedNodes(node, search=search, replace=replace)
#   File "D:\Sandbox\mgear\modules\scripts\mgear\core\utils.py", line 265, in wrap
#     raise e
#   File "D:\Sandbox\mgear\modules\scripts\mgear\core\utils.py", line 262, in wrap
#     return func(*args, **kwargs)
#   File "D:\Sandbox\mgear\modules\scripts\mgear\shifter\afg_tools.py", line 546, in mirrorEmbedNodes
#     target_mat.rotateTo([0, 0, 0])
# AttributeError: 'TransformationMatrix' object has no attribute 'rotateTo'

今回の Match Guides は内部で Smart Adjust 相当の調整処理に依存していて、そこが落ちるなら実質 Match Guides も使えない と見ていい。トレースバックでも runSmartAdjustsmartAdjustEmbedOutputmirrorEmbedNodesSidemirrorEmbedNodes の流れで、TransformationMatrix.rotateTo が無くて止まっている。mGearのリリースログでも Auto Fit Guide は比較的新しい改善対象で、skip orientation や reference mesh などが後から拡張されてきた機能だと分かるので、版差や環境差の影響を受けやすい領域と見て自然。

なので、今やるべきことは「Match Guides を直そうと頑張る」ではなく、切り分けを完成させること。
結論だけ言うと、こう整理すると強い。

1. AFGの自動フィットは試した。
2. ただし Smart Adjust 系の内部処理で TransformationMatrix.rotateTo エラーが出て止まる。
3. 原因は入力データではなく、mGear側が期待している変換行列APIと実行環境のAPI不整合の可能性が高い。
mGearの公開変換ユーティリティのドキュメントでは、変換行列周りは setMatrixRotation などの関数で扱われていて、少なくとも公開ドキュメント上 TransformationMatrix.rotateTo は確認しにくい。だから、あなたの理解どおり「Smart Adjust 側が詰まっているので Match Guides も成立しない」で問題ない。

mGearのAuto Fit Guideを実機で確認したところ、Guide生成とEmbedまでは触れましたが、Match Guides の内部で Smart Adjust 系の処理が TransformationMatrix.rotateTo で失敗し、自動サイズ合わせまでは通りませんでした。なので、機能の存在だけでなく、どの処理単位で止まるかまで切り分けて確認しました。

Auto Fitは便利ですが、版差やAPI差異の影響を受けやすい処理だと感じました。現場では完全自動前提ではなく、テンプレGuideと手動補正を併用する運用の方が堅いと考えています。

とか、手動なんてめんどくさいので

昔、こんな記事jを書いた mGearのガイドはジョイントから作らないとめんどくさい。

今回は自分のジョイント用だけど書き直したバージョン

# -*- coding: utf-8 -*-
import maya.cmds as cmds



# これは 3点から pole vector 用の位置を出す やつ。
import maya.cmds as cmds
import math

def vec_sub(a, b):
    return [a[0]-b[0], a[1]-b[1], a[2]-b[2]]

def vec_add(a, b):
    return [a[0]+b[0], a[1]+b[1], a[2]+b[2]]

def vec_mul(a, s):
    return [a[0]*s, a[1]*s, a[2]*s]

def vec_len(a):
    return math.sqrt(a[0]**2 + a[1]**2 + a[2]**2)

def vec_norm(a):
    l = vec_len(a)
    if l < 1e-8:
        return [0.0, 0.0, 0.0]
    return [a[0]/l, a[1]/l, a[2]/l]

def vec_dot(a, b):
    return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]

def vec_cross(a, b):
    return [
        a[1]*b[2] - a[2]*b[1],
        a[2]*b[0] - a[0]*b[2],
        a[0]*b[1] - a[1]*b[0]
    ]

def get_pole_vector_position(start, mid, end, distance_scale=1.0):
    """
    start, mid, end: world position [x,y,z]
    """
    start_to_end = vec_sub(end, start)
    start_to_mid = vec_sub(mid, start)

    line_dir = vec_norm(start_to_end)
    proj_len = vec_dot(start_to_mid, line_dir)
    proj = vec_add(start, vec_mul(line_dir, proj_len))

    arrow = vec_sub(mid, proj)
    arrow_len = vec_len(arrow)

    if arrow_len < 1e-6:
        # ほぼ一直線なら適当な補助方向
        fallback = vec_cross(line_dir, [0, 1, 0])
        if vec_len(fallback) < 1e-6:
            fallback = vec_cross(line_dir, [1, 0, 0])
        arrow = vec_norm(fallback)
        chain_len = vec_len(vec_sub(mid, start)) + vec_len(vec_sub(end, mid))
        return vec_add(mid, vec_mul(arrow, chain_len * 0.5 * distance_scale))

    arrow_dir = vec_norm(arrow)
    chain_len = vec_len(vec_sub(mid, start)) + vec_len(vec_sub(end, mid))

    return vec_add(mid, vec_mul(arrow_dir, chain_len * 0.5 * distance_scale))
    

#upv を置く処理
def move_upv_guide(guide_name, start_joint, mid_joint, end_joint, distance_scale=1.0):
    guide = first_existing([guide_name])
    s = find_unique_by_short_name(start_joint)
    m = find_unique_by_short_name(mid_joint)
    e = find_unique_by_short_name(end_joint)

    if not guide:
        print("[SKIP] upv guide missing:", guide_name)
        return
    if not (s and m and e):
        print("[SKIP] joints missing for upv:", guide_name, start_joint, mid_joint, end_joint)
        return

    pos = get_pole_vector_position(ws_pos(s), ws_pos(m), ws_pos(e), distance_scale)
    set_ws_pos(guide, pos)
    print("[OK] moved upv:", guide_name)
    
# 安全な aim のやり方
# Mayaで transform を向けたいだけならこういう感じ。
def aim_node_to_target(node, target, aim=(1,0,0), up=(0,1,0), world_up=(0,1,0)):
    tmp = cmds.aimConstraint(
        target,
        node,
        aimVector=aim,
        upVector=up,
        worldUpType="vector",
        worldUpVector=world_up
    )
    cmds.delete(tmp)

# まずは位置合わせ追加版
def align_finger_chain(guide_prefix, joint_names):
    """
    例:
    guide_prefix = "index_L0"
    joint_names = ["index_metacarpal_l", "index_01_l", "index_02_l", "index_03_l"]
    """
    root_g = first_existing([guide_prefix + "_root"])
    loc0_g = first_existing([guide_prefix + "_0_loc"])
    loc1_g = first_existing([guide_prefix + "_1_loc"])
    loc2_g = first_existing([guide_prefix + "_2_loc"])

    src0 = find_unique_by_short_name(joint_names[0])
    src1 = find_unique_by_short_name(joint_names[1])
    src2 = find_unique_by_short_name(joint_names[2])
    src3 = find_unique_by_short_name(joint_names[3])

    pairs = [
        (root_g, src0, guide_prefix + "_root"),
        (loc0_g, src1, guide_prefix + "_0_loc"),
        (loc1_g, src2, guide_prefix + "_1_loc"),
        (loc2_g, src3, guide_prefix + "_2_loc"),
    ]

    for g, j, label in pairs:
        if not g:
            print("[SKIP] guide missing:", label)
            continue
        if not j:
            print("[SKIP] joint missing for:", label)
            continue
        set_ws_pos(g, ws_pos(j))
        print("[OK] moved", label)
        
# 指はこれが大事。
# *_blade を root → 先端方向 に向ける。 
def aim_blade_from_chain(blade_name, root_joint, next_joint, world_up=(0, 1, 0)):
    blade = first_existing([blade_name])
    j0 = find_unique_by_short_name(root_joint)
    j1 = find_unique_by_short_name(next_joint)

    if not blade:
        print("[SKIP] blade missing:", blade_name)
        return
    if not (j0 and j1):
        print("[SKIP] joints missing for blade:", blade_name, root_joint, next_joint)
        return

    # bladeをroot位置へ
    set_ws_pos(blade, ws_pos(j0))

    # 一時ロケータを先端位置に置いてaim
    temp = cmds.spaceLocator(name="tmpAim_loc#")[0]
    set_ws_pos(temp, ws_pos(j1))

    try:
        con = cmds.aimConstraint(
            temp,
            blade,
            aimVector=(1, 0, 0),
            upVector=(0, 1, 0),
            worldUpType="vector",
            worldUpVector=world_up
        )
        cmds.delete(con)
    except Exception as e:
        print("[ERROR] aim failed:", blade_name, e)

    cmds.delete(temp)
    print("[OK] aimed blade:", blade_name)
    
# =========================================================
# Utility
# =========================================================

def _short_name(node):
    return node.split("|")[-1]

def find_unique_by_short_name(short_name):
    """Scene内から short name 一致ノードを1つだけ返す"""
    matches = cmds.ls("*|" + short_name, long=True) or []
    root_matches = cmds.ls(short_name, long=True) or []
    all_matches = list(set(matches + root_matches))

    if not all_matches:
        return None
    if len(all_matches) > 1:
        print("[WARN] multiple nodes found for short name: {} -> {}".format(short_name, all_matches))
        return all_matches[0]
    return all_matches[0]

def first_existing(candidates):
    """候補名の中で最初に存在するノードを返す"""
    for c in candidates:
        node = find_unique_by_short_name(c)
        if node:
            return node
    return None

def ws_pos(node):
    return cmds.xform(node, q=True, ws=True, t=True)

def set_ws_pos(node, pos):
    cmds.xform(node, ws=True, t=pos)

def avg_pos(nodes):
    pts = [ws_pos(n) for n in nodes]
    count = float(len(pts))
    return [
        sum(p[0] for p in pts) / count,
        sum(p[1] for p in pts) / count,
        sum(p[2] for p in pts) / count,
    ]

def lerp_pos(a, b, t=0.5):
    pa = ws_pos(a)
    pb = ws_pos(b)
    return [
        pa[0] + (pb[0] - pa[0]) * t,
        pa[1] + (pb[1] - pa[1]) * t,
        pa[2] + (pb[2] - pa[2]) * t,
    ]

def list_guide_nodes():
    """guide配下のノード確認用"""
    guide = find_unique_by_short_name("guide")
    if not guide:
        print("[ERROR] guide root not found")
        return
    nodes = cmds.listRelatives(guide, ad=True, f=True) or []
    nodes = sorted(nodes, key=lambda x: _short_name(x))
    print("=" * 60)
    print("Guide nodes:")
    for n in nodes:
        print(_short_name(n))
    print("=" * 60)

# =========================================================
# Guide candidate names
# EPIC_mannequin_y_up を想定しつつ、少し候補を広めに持つ
# 見つからない場合は list_guide_nodes() で確認して差し替えてください
# =========================================================

GUIDE_CANDIDATES = {
    # Center
    "pelvis": ["body_C0_root", "spine_C0_root"],

    "spine1": [
        "spine_C0_spineBase"
    ],
    "spine2": [
        "spine_C0_spineTop"
    ],
    "chest": [
        "spine_C0_chest"
    ],

    "neck": [
        "neck_C0_neck", "neck_C0_root"
    ],
    "head": [
        "neck_C0_head"
    ],

    # Left arm
    "clav_l": ["clavicle_L0_root"],
    "elbow_l": ["arm_L0_elbow"],
    "wrist_l": ["arm_L0_wrist"],

    # Right arm
    "clav_r": ["clavicle_R0_root"],
    "elbow_r": ["arm_R0_elbow"],
    "wrist_r": ["arm_R0_wrist"],

    # Left leg
    "thigh_l": ["leg_L0_root"],
    "knee_l": ["leg_L0_knee"],
    "ankle_l": ["leg_L0_ankle"],

    # Right leg
    "thigh_r": ["leg_R0_root"],
    "knee_r": ["leg_R0_knee"],
    "ankle_r": ["leg_R0_ankle"],
}

# =========================================================
# Source joints
# 今回あなたのジョイント名に合わせている
# =========================================================

JOINTS = {
    "root": "root",
    "pelvis": "pelvis",

    "spine_01": "spine_01",
    "spine_02": "spine_02",
    "spine_03": "spine_03",
    "spine_04": "spine_04",
    "spine_05": "spine_05",

    "neck_01": "neck_01",
    "neck_02": "neck_02",

    "clavicle_l": "clavicle_l",
    "upperarm_l": "upperarm_l",
    "lowerarm_l": "lowerarm_l",
    "hand_l": "hand_l",

    "clavicle_r": "clavicle_r",
    "upperarm_r": "upperarm_r",
    "lowerarm_r": "lowerarm_r",
    "hand_r": "hand_r",

    "thigh_l": "thigh_l",
    "calf_l": "calf_l",
    "foot_l": "foot_l",

    "thigh_r": "thigh_r",
    "calf_r": "calf_r",
    "foot_r": "foot_r",
}

# =========================================================
# Core
# =========================================================

def resolve_guides():
    resolved = {}
    for key, candidates in GUIDE_CANDIDATES.items():
        node = first_existing(candidates)
        if not node:
            print("[WARN] guide not found for {} candidates={}".format(key, candidates))
        resolved[key] = node
    return resolved

def resolve_joints():
    resolved = {}
    for key, short_name in JOINTS.items():
        node = find_unique_by_short_name(short_name)
        if not node:
            print("[WARN] joint not found: {}".format(short_name))
        resolved[key] = node
    return resolved

def safe_move(guide_node, pos, label):
    if not guide_node:
        print("[SKIP] guide missing: {}".format(label))
        return
    try:
        set_ws_pos(guide_node, pos)
        print("[OK] moved {}".format(label))
    except Exception as e:
        print("[ERROR] failed move {} : {}".format(label, e))

def align_mgear_guides_to_epic():
    guides = resolve_guides()
    joints = resolve_joints()

    print("=" * 60)
    print("Start align")
    print("=" * 60)

    # -----------------------------------------------------
    # Center
    # -----------------------------------------------------
    if joints["pelvis"]:
        safe_move(guides["pelvis"], ws_pos(joints["pelvis"]), "pelvis")

    # spine guide locators が複数ある場合に順番に置く
    # spine
    if joints["spine_01"] and joints["spine_03"]:
        safe_move(guides["spine1"], avg_pos([joints["spine_01"], joints["spine_03"]]), "spine1")
    
    if joints["spine_03"] and joints["spine_05"]:
        safe_move(guides["spine2"], avg_pos([joints["spine_03"], joints["spine_05"]]), "spine2")
    
    if joints["spine_05"]:
        safe_move(guides["chest"], ws_pos(joints["spine_05"]), "chest")

    # neck / head
    if joints["neck_01"]:
        safe_move(guides["neck"], ws_pos(joints["neck_01"]), "neck")
    
    if joints["neck_02"]:
        safe_move(guides["head"], ws_pos(joints["neck_02"]), "head")

    # -----------------------------------------------------
    # Left arm
    # -----------------------------------------------------
    if joints["clavicle_l"]:
        safe_move(guides["clav_l"], ws_pos(joints["clavicle_l"]), "clav_l")

    if joints["lowerarm_l"]:
        safe_move(guides["elbow_l"], ws_pos(joints["lowerarm_l"]), "elbow_l")

    if joints["hand_l"]:
        safe_move(guides["wrist_l"], ws_pos(joints["hand_l"]), "wrist_l")

    # -----------------------------------------------------
    # Right arm
    # -----------------------------------------------------
    if joints["clavicle_r"]:
        safe_move(guides["clav_r"], ws_pos(joints["clavicle_r"]), "clav_r")

    if joints["lowerarm_r"]:
        safe_move(guides["elbow_r"], ws_pos(joints["lowerarm_r"]), "elbow_r")

    if joints["hand_r"]:
        safe_move(guides["wrist_r"], ws_pos(joints["hand_r"]), "wrist_r")

    # -----------------------------------------------------
    # Left leg
    # -----------------------------------------------------
    if joints["thigh_l"]:
        safe_move(guides["thigh_l"], ws_pos(joints["thigh_l"]), "thigh_l")

    if joints["calf_l"]:
        safe_move(guides["knee_l"], ws_pos(joints["calf_l"]), "knee_l")

    if joints["foot_l"]:
        safe_move(guides["ankle_l"], ws_pos(joints["foot_l"]), "ankle_l")

    # -----------------------------------------------------
    # Right leg
    # -----------------------------------------------------
    if joints["thigh_r"]:
        safe_move(guides["thigh_r"], ws_pos(joints["thigh_r"]), "thigh_r")

    if joints["calf_r"]:
        safe_move(guides["knee_r"], ws_pos(joints["calf_r"]), "knee_r")

    if joints["foot_r"]:
        safe_move(guides["ankle_r"], ws_pos(joints["foot_r"]), "ankle_r")

    print("=" * 60)
    print("Done")
    print("=" * 60)
    
    
    
    # align_mgear_guides_to_epic() の最後あたりに追加。
    
    # Arm upv
    move_upv_guide("arm_L0_upv", "upperarm_l", "lowerarm_l", "hand_l", 1.0)
    move_upv_guide("arm_R0_upv", "upperarm_r", "lowerarm_r", "hand_r", 1.0)

    # Leg upv
    move_upv_guide("leg_L0_upv", "thigh_l", "calf_l", "foot_l", 1.0)
    move_upv_guide("leg_R0_upv", "thigh_r", "calf_r", "foot_r", 1.0)
    
    # 指を足す
    # -----------------------------
    # Left fingers
    # -----------------------------
    align_finger_chain("thumb_L0",  ["thumb_01_l", "thumb_02_l", "thumb_03_l", "thumb_03_l"])
    align_finger_chain("index_L0",  ["index_metacarpal_l", "index_01_l", "index_02_l", "index_03_l"])
    align_finger_chain("middle_L0", ["middle_metacarpal_l", "middle_01_l", "middle_02_l", "middle_03_l"])
    align_finger_chain("ring_L0",   ["ring_metacarpal_l", "ring_01_l", "ring_02_l", "ring_03_l"])
    align_finger_chain("pinky_L0",  ["pinky_metacarpal_l", "pinky_01_l", "pinky_02_l", "pinky_03_l"])

    aim_blade_from_chain("thumb_L0_blade",  "thumb_01_l", "thumb_02_l")
    aim_blade_from_chain("index_L0_blade",  "index_metacarpal_l", "index_01_l")
    aim_blade_from_chain("middle_L0_blade", "middle_metacarpal_l", "middle_01_l")
    aim_blade_from_chain("ring_L0_blade",   "ring_metacarpal_l", "ring_01_l")
    aim_blade_from_chain("pinky_L0_blade",  "pinky_metacarpal_l", "pinky_01_l")

    # -----------------------------
    # Right fingers
    # -----------------------------
    align_finger_chain("thumb_R0",  ["thumb_01_r", "thumb_02_r", "thumb_03_r", "thumb_03_r"])
    align_finger_chain("index_R0",  ["index_metacarpal_r", "index_01_r", "index_02_r", "index_03_r"])
    align_finger_chain("middle_R0", ["middle_metacarpal_r", "middle_01_r", "middle_02_r", "middle_03_r"])
    align_finger_chain("ring_R0",   ["ring_metacarpal_r", "ring_01_r", "ring_02_r", "ring_03_r"])
    align_finger_chain("pinky_R0",  ["pinky_metacarpal_r", "pinky_01_r", "pinky_02_r", "pinky_03_r"])

    aim_blade_from_chain("thumb_R0_blade",  "thumb_01_r", "thumb_02_r")
    aim_blade_from_chain("index_R0_blade",  "index_metacarpal_r", "index_01_r")
    aim_blade_from_chain("middle_R0_blade", "middle_metacarpal_r", "middle_01_r")
    aim_blade_from_chain("ring_R0_blade",   "ring_metacarpal_r", "ring_01_r")
    aim_blade_from_chain("pinky_R0_blade",  "pinky_metacarpal_r", "pinky_01_r")
    
# 実行
align_mgear_guides_to_epic()

これがこうなる。ぴったりジョイントにガイドがあう状態。

ちょっと整理したバージョン

# -*- coding: utf-8 -*-
# =========================================================
# mGear Guide Auto Align Script (Comment-rich version)
# ---------------------------------------------------------
# 目的:
#   既存ジョイント(UE系など)から mGear Guide を
#   「位置+PoleVector+指のblade方向」まで自動配置する
#
# 設計思想:
#   ・まず位置を100%合わせる
#   ・IKの安定に重要な PoleVector を自動配置
#   ・指は回転ではなく blade(方向ガイド)で補正
#   ・回転は“必要な箇所だけ”後処理(やりすぎない)
#
# 注意:
#   ・GuideのTransformは直接Freezeしない(mGear構造破壊)
#   ・Mesh / Joint 側は Scale=1 を前提
# =========================================================

import maya.cmds as cmds
import math

# =========================================================
# 基本ユーティリティ
# =========================================================

def _short_name(node):
    """フルパスから短い名前だけ取る"""
    return node.split("|")[-1]


def find_unique_by_short_name(short_name):
    """
    シーン内から短い名前一致ノードを取得
    ※複数ある場合は最初の1つを使う(警告出す)
    """
    matches = cmds.ls("*|" + short_name, long=True) or []
    root_matches = cmds.ls(short_name, long=True) or []
    all_matches = list(set(matches + root_matches))

    if not all_matches:
        return None

    if len(all_matches) > 1:
        print("[WARN] multiple nodes found:", short_name)
        return all_matches[0]

    return all_matches[0]


def first_existing(candidates):
    """
    候補リストの中から存在するノードを返す
    ※Guide名の版差対策
    """
    for c in candidates:
        node = find_unique_by_short_name(c)
        if node:
            return node
    return None


def ws_pos(node):
    """ワールド座標取得"""
    return cmds.xform(node, q=True, ws=True, t=True)


def set_ws_pos(node, pos):
    """ワールド座標で移動"""
    cmds.xform(node, ws=True, t=pos)


def avg_pos(nodes):
    """複数ノードの平均位置(spineなどで使用)"""
    pts = [ws_pos(n) for n in nodes]
    c = float(len(pts))
    return [
        sum(p[0] for p in pts)/c,
        sum(p[1] for p in pts)/c,
        sum(p[2] for p in pts)/c
    ]


# =========================================================
# ベクトル計算(PoleVector用)
# =========================================================

def vec_sub(a, b): return [a[0]-b[0], a[1]-b[1], a[2]-b[2]]
def vec_add(a, b): return [a[0]+b[0], a[1]+b[1], a[2]+b[2]]
def vec_mul(a, s): return [a[0]*s, a[1]*s, a[2]*s]

def vec_len(a):
    return math.sqrt(a[0]**2 + a[1]**2 + a[2]**2)

def vec_norm(a):
    l = vec_len(a)
    if l < 1e-8:
        return [0,0,0]
    return [a[0]/l, a[1]/l, a[2]/l]

def vec_dot(a,b):
    return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]

def vec_cross(a,b):
    return [
        a[1]*b[2]-a[2]*b[1],
        a[2]*b[0]-a[0]*b[2],
        a[0]*b[1]-a[1]*b[0]
    ]


# =========================================================
# PoleVector(IK安定の核心)
# =========================================================

def get_pole_vector_position(start, mid, end):
    """
    3点からIK平面を作り、適切なPoleVector位置を算出
    """
    line = vec_norm(vec_sub(end, start))
    proj_len = vec_dot(vec_sub(mid, start), line)
    proj = vec_add(start, vec_mul(line, proj_len))

    arrow = vec_sub(mid, proj)
    if vec_len(arrow) < 1e-6:
        arrow = vec_cross(line, [0,1,0])

    arrow = vec_norm(arrow)
    length = vec_len(vec_sub(mid,start)) + vec_len(vec_sub(end,mid))

    return vec_add(mid, vec_mul(arrow, length*0.5))


def move_upv(guide, j0, j1, j2):
    """PoleVector Guide配置"""
    g = first_existing([guide])
    a = find_unique_by_short_name(j0)
    b = find_unique_by_short_name(j1)
    c = find_unique_by_short_name(j2)

    if not (g and a and b and c):
        print("[SKIP upv]", guide)
        return

    pos = get_pole_vector_position(ws_pos(a), ws_pos(b), ws_pos(c))
    set_ws_pos(g, pos)
    print("[OK upv]", guide)


# =========================================================
# 指チェーン
# =========================================================

def align_finger(prefix, joints):
    """
    指は単純に位置を順に合わせる
    """
    guides = [
        first_existing([prefix+"_root"]),
        first_existing([prefix+"_0_loc"]),
        first_existing([prefix+"_1_loc"]),
        first_existing([prefix+"_2_loc"])
    ]

    src = [find_unique_by_short_name(j) for j in joints]

    for g, j in zip(guides, src):
        if g and j:
            set_ws_pos(g, ws_pos(j))
            print("[OK finger]", g)


def aim_blade(blade, j0, j1, up=(0,1,0)):
    """
    指の向きはbladeで制御
    → root→先端方向に向ける
    """
    b = first_existing([blade])
    a = find_unique_by_short_name(j0)
    c = find_unique_by_short_name(j1)

    if not (b and a and c):
        print("[SKIP blade]", blade)
        return

    set_ws_pos(b, ws_pos(a))

    tmp = cmds.spaceLocator()[0]
    set_ws_pos(tmp, ws_pos(c))

    con = cmds.aimConstraint(tmp, b,
        aimVector=(1,0,0),
        upVector=(0,1,0),
        worldUpType="vector",
        worldUpVector=up
    )
    cmds.delete(con)
    cmds.delete(tmp)

    print("[OK blade]", blade)


# =========================================================
# メイン処理
# =========================================================

def run():
    print("="*50)
    print("Start mGear Guide Align")
    print("="*50)

    # -------------------------
    # 中心
    # -------------------------
    set_ws_pos(first_existing(["body_C0_root"]), ws_pos(find_unique_by_short_name("pelvis")))

    set_ws_pos(first_existing(["spine_C0_spineBase"]),
               avg_pos([find_unique_by_short_name("spine_01"),
                        find_unique_by_short_name("spine_03")]))

    set_ws_pos(first_existing(["spine_C0_spineTop"]),
               avg_pos([find_unique_by_short_name("spine_03"),
                        find_unique_by_short_name("spine_05")]))

    set_ws_pos(first_existing(["spine_C0_chest"]),
               ws_pos(find_unique_by_short_name("spine_05")))

    set_ws_pos(first_existing(["neck_C0_neck"]),
               ws_pos(find_unique_by_short_name("neck_01")))

    set_ws_pos(first_existing(["neck_C0_head"]),
               ws_pos(find_unique_by_short_name("neck_02")))

    # -------------------------
    # 腕
    # -------------------------
    set_ws_pos(first_existing(["clavicle_L0_root"]), ws_pos(find_unique_by_short_name("clavicle_l")))
    set_ws_pos(first_existing(["arm_L0_elbow"]), ws_pos(find_unique_by_short_name("lowerarm_l")))
    set_ws_pos(first_existing(["arm_L0_wrist"]), ws_pos(find_unique_by_short_name("hand_l")))

    set_ws_pos(first_existing(["clavicle_R0_root"]), ws_pos(find_unique_by_short_name("clavicle_r")))
    set_ws_pos(first_existing(["arm_R0_elbow"]), ws_pos(find_unique_by_short_name("lowerarm_r")))
    set_ws_pos(first_existing(["arm_R0_wrist"]), ws_pos(find_unique_by_short_name("hand_r")))

    # -------------------------
    # 脚
    # -------------------------
    set_ws_pos(first_existing(["leg_L0_root"]), ws_pos(find_unique_by_short_name("thigh_l")))
    set_ws_pos(first_existing(["leg_L0_knee"]), ws_pos(find_unique_by_short_name("calf_l")))
    set_ws_pos(first_existing(["leg_L0_ankle"]), ws_pos(find_unique_by_short_name("foot_l")))

    set_ws_pos(first_existing(["leg_R0_root"]), ws_pos(find_unique_by_short_name("thigh_r")))
    set_ws_pos(first_existing(["leg_R0_knee"]), ws_pos(find_unique_by_short_name("calf_r")))
    set_ws_pos(first_existing(["leg_R0_ankle"]), ws_pos(find_unique_by_short_name("foot_r")))

    # -------------------------
    # PoleVector
    # -------------------------
    move_upv("arm_L0_upv","upperarm_l","lowerarm_l","hand_l")
    move_upv("arm_R0_upv","upperarm_r","lowerarm_r","hand_r")
    move_upv("leg_L0_upv","thigh_l","calf_l","foot_l")
    move_upv("leg_R0_upv","thigh_r","calf_r","foot_r")

    # -------------------------
    # 指
    # -------------------------
    align_finger("index_L0",["index_metacarpal_l","index_01_l","index_02_l","index_03_l"])
    align_finger("index_R0",["index_metacarpal_r","index_01_r","index_02_r","index_03_r"])

    # blade
    aim_blade("index_L0_blade","index_metacarpal_l","index_01_l")
    aim_blade("index_R0_blade","index_metacarpal_r","index_01_r")

    print("="*50)
    print("Done")
    print("="*50)


# 実行
run()

ビルドした結果

mGearのAuto Fit Guideを検証した際に、内部処理に依存する部分で不安定になるケースがあったため、既存ジョイントからGuideを配置するスクリプトを自作しました。

まず位置については、各Guideを対応するジョイントのワールド座標に直接配置し、脊椎のような複数構造は中間点を使って補間しています。

その上で、IKの安定性を確保するために、腕と脚は3点から平面を計算し、Pole Vectorを自動配置する処理を入れています。これにより、ビルド後のIKの破綻を抑えています。

回転については一括で処理せず、特に指に関してはbladeを用いて方向のみ補正し、過剰な自動化による崩れを防いでいます。

このように、位置・IK安定・方向補正を段階的に分離することで、完全自動ではなく、実運用に耐える半自動フローとして設計しています。

[Houdini] Houdini Technical Artist HDA関連 021

[Houdini] Houdini Technical Artist HDA related 021

■ 最終形(これが実務ベース)

vector Nn = normalize(@N);

// 傾き制御
vector worldUp = {0,1,0};
Nn = normalize(lerp(worldUp, Nn, 0.3));

// 向き合わせ
matrix3 m = dihedral({0,1,0}, Nn);
p@orient = quaternion(m);

// ランダムひねり
float angle = rand(@ptnum + 10.0) * M_PI * 2;
p@orient = qmultiply(p@orient, quaternion(angle, Nn));

// サイズ
@pscale = fit01(rand(@ptnum), 0.5, 1.5);

// 浮かせる
@P += Nn * (@pscale * 0.5);

ビューポートの右下の目のアイコンでSmoothShade

  • 地面の傾き ✔
  • 棒が法線に沿って傾いてる ✔
  • 密度も出てる ✔
  • 表示もOK ✔

👉 完全に正しく動いてる

■ (TA的に重要)

👉 これ実は👇

UEのFoliage / HISM配置と同じことやってる

  • Scatter → 配置ポイント
  • N → Surface Normal
  • orient → Transform
  • pscale → Scale variation

生える場所を制限(超重要)

// 法線
vector Nn = normalize(@N);

// --- ① 急斜面は削除 ---
if (Nn.y < 0.7) {
    removepoint(0, @ptnum);
    return; // ←これ重要(下の処理止める)
}

// --- ② 傾き制御 ---
vector worldUp = {0,1,0};
Nn = normalize(lerp(worldUp, Nn, 0.3));

// --- ③ 向き合わせ ---
matrix3 m = dihedral({0,1,0}, Nn);
p@orient = quaternion(m);

// --- ④ ランダムひねり ---
float angle = rand(@ptnum + 10.0) * M_PI * 2;
p@orient = qmultiply(p@orient, quaternion(angle, Nn));

// --- ⑤ サイズ ---
@pscale = fit01(rand(@ptnum), 0.5, 1.5);

// --- ⑥ 浮かせる ---
@P += Nn * (@pscale * 0.5);

  • 傾きバラバラ ✔
  • サイズバラバラ ✔
  • ひねり入ってる ✔
  • 急斜面減ってる ✔

👉 完全に意図通り

ASUS ROG MAXIMUS Z790 FORMULA を最新 の bios に アップデートしたよ。

1,BIOSまたはMEファームウェアをアップデートする前に、BitLocker回復キーをバックアップし、オペレーティングシステムでBitLocker暗号化を一時停止してください。

BIOSをアップデートする前にBitLockerを一時停止する

[スタート]メニューをクリックします。
[コントロール パネル]、[システムとセキュリティ]、[BitLockerドライブ暗号化]の順に移動します。

無効なのでよし。

2,ASUS ASUS ROG MAXIMUS Z790 FORMULA 最新のUEFI BIOSは、サポートページからダウンロード
https://rog.asus.com/jp/motherboards/rog-maximus/rog-maximus-z790-formula/helpdesk_bios/

バージョン 2001
14.04 MB 2025/05/19
SHA-256 :5F301C30C43AF188BA0726D9DE5AE83860221197CC57CB92DE23BE20BAC2165D
「1. Intelマイクロコードをバージョン0x12Fにアップデートすることで、Intel第13世代および第14世代システムにおけるVminシフトの不安定性の原因となる可能性のあるシステム状態をさらに改善します。
このBIOSをアップデートすると、対応するIntel MEも同時にバージョン16.1.35.2557にアップデートされます。このBIOSをアップデートした後、後で古いBIOSに戻した場合でも、MEのバージョンは更新されたままになりますのでご注意ください。USB
BIOS Flashbackツールを実行する前に、BIOSRenamerを使用してBIOSファイル(A5471.CAP)の名前を変更してください。」

3,アップデート手順


USBメモリの準備:
USBメモリをFAT32形式でフォーマットする。
ダウンロードした圧縮ファイルを解凍し、中身をUSBメモリのルート直下(フォルダに入れない)にコピーする。
BIOSアップデート (EZ Flash 3):
USBメモリをPCのBIOS USBポートに差し込み、PCを再起動する。
画面が表示されたら [Delete] キーを連打しBIOS (UEFI) 画面に入る。
[F7] キーを押して「Advanced Mode」に入り、

「Tool」メニューの「ASUS EZ Flash 3 Utility」を選択する。

USBメモリ内のBIOSファイルを選択し、

bitlockerのオフにしたかい?の警告が
でるのでYES

EZFLASH
このファイルを読み込みますか?


YES
BIOSを本当にアップデートしますか?YES



アップデート成功!自動的にリセットします
何回も自動で再起動される
1.bios updating.


2.ME Firmware is updating.


3.BIOS is updating Thunderbolt Num firmware.

4.BIOS is updating Thunderbolt Nvm firmware.
Do not shut down or reset the system to prevent system bootup failure.
ImageSize : 0x66000
Written so far : 0x64000 bytes
Image write finished.

いずれも
not shut down or reset the system to prevent system bootup failure.
なので触らない事

自動的に再起動する。絶対に電源を切らない

Before making any adjustments in the BIOS,
please press F5 to load the factory default settings to ensure smooth operation.
If setting up Intel RAID,
configure VD options according to the interface type for better compatibility.
Now, please press F1 to enter BIOS setup.

BIOSで設定を変更する前に、
F5キーを押して工場出荷時の設定を読み込み、スムーズな動作を確保してください。
Intel RAIDを設定する場合は、
互換性を高めるために、インターフェースの種類に応じてVDオプションを設定してください。
次に、F1キーを押してBIOSセットアップに入ります。

F5押したが、反応なしなのでF1押した

無事にBIOS アップデートが成功していた。Intel Default Settingもある。

設定を保存して再起動で

Windowsも起動した。

完了: