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);

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

👉 完全に意図通り