[UE5.7.4][C++]遂に完成したC++プラグインUE用HLSL.EditorのMaterial作成機能

もうこれでノードスパゲッティから解放される

導入方法

0,以下のリンクをダウンロード (特に理由がなければ最新版使ってください)

furcraeaHLSLEditor_UE5.7.4V7.zip
https://drive.google.com/file/d/1ieBRb5zpW36LGldmREROGcWJ8WZot1Xg/view?usp=sharing

furcraeaHLSLEditor_UE5.7.4V6_2.zip
https://drive.google.com/file/d/1BzMiQt4dXcu8p-dIfkyH3v1SRVSKjiCn/view?usp=sharing

furcraeaHLSLEditor_UE5.7.4V6.zip
https://drive.google.com/file/d/1APYn65u1yo3xkJlDFmqJ2yuCE5zHViqc/view?usp=sharing

furcraeaHLSLEditor_UE5.7.4V5.zip
https://drive.google.com/file/d/1YiUHmR1g6d9WwvAtd1rGADsiNZEXRMNX/view?usp=sharing

furcraeaHLSLEditor_UE5.7.4.zip (V4)
https://drive.google.com/file/d/1gInP7W5hFB3pwlwbrbkekSzC8xOwusap/view?usp=sharing

furcraeaHLSLEditor_UE5.7.3V4.zip
https://drive.google.com/file/d/1XhVZgV6FRiyGOOquqO5nJVFXbmg_8DA-/view?usp=sharing

furcraeaHLSLEditor_UE5.7.3V3.zip
https://drive.google.com/file/d/1Qmz5Y_uT0VGKkrk1wskvw2DRbnND6or4/view?
usp=drive_link

furcraeaHLSLEditor_UE5.7.3V2.zip
https://drive.google.com/file/d/1CSXmSebk7hLM02qM5jKDv2ndNJ4jhUi7/view?usp=sharing

1,Pluginsフォルダにzipを解凍して入れてください。

  パスの例:UEプロジェクトフォルダ\Plugins\furcraeaHLSLEditor\Shaders

2,プラグインで「furcraeaHLSLEditor」を有効化

使用方法

コンテンツブラウザで右クリック>Miscellaneous>Code Material Assetを作成

できた New Code Material AssetをダブルクリックでHLSL編集画面が開く

Saveをクリックすると。。。GeneratedフォルダーにM_NewCodeMaterialAssetが作成されます。>CodeMaterialAssetと同じフォルダーに保存するように変更しました。


まだまだ、1つしかCodeMatUser.ushがないので一つしかマテリアルが成立しないなどの不備があるので更新していく予定です。<-以下の画像のように修正済みです。

furcraeaHLSLEditorをGitHub で公開しました。
https://github.com/nobolu-ootsuka-unrealengine/furcraeaHLSLEditor/commits?author=nobolu-ootsuka-unrealengine

修正履歴



1つしかCodeMatUser.ushがないので一つしかマテリアルが成立しないなどの不備を修正

furcraeaHLSLEditor_UE5.7.3V2

Projects と RenderCore を bBuildEditorをガード外に移動(ランタイムでも必要なモジュールのため)


furcraeaHLSLEditor_UE5.7.3V3

CodeMaterialAsset が吐くMaterialのフォルダを (固定)Generated から (ユーザーが作った)CodeMaterialAssetと同じフォルダに変更した

CodeMaterialCompiler.cpp に2箇所変更しました。

① MakeMaterialPackagePath 関数(生成先パスを決定)

  • 変更前:/Game/Generated/M_xxx 固定
  • 変更後:Asset->GetOutermost()->GetName() でアセット自身のパッケージパスを取得し、FPackageName::GetLongPackagePath()
    でフォルダ部分を抜き出して {同じフォルダ}/M_xxx を返す ② CreateOrLoadMaterial の MakeDirectory 呼び出し
  • 変更前:MakeDirectory(“/Game/Generated”) 固定
  • 変更後:MakeDirectory(FPackageName::GetLongPackagePath(MakeMaterialPackagePath(Asset))) —
    生成先パスからフォルダを算出して作成 これにより、例えば Content/0_furcraeaTokyo/CodeMaterialAsset/NewCodeMaterialAsset に保存された CodeMaterialAsset
    に対しては、生成マテリアルも同じ Content/0_furcraeaTokyo/CodeMaterialAsset/M_NewCodeMaterialAsset に出力されます。

furcraeaHLSLEditor_UE5.7.3V4

アウトライン表示用のVertexNormalを拡張のために

FragmentShader VertexShader(Optional)に分離しました!

安定性のためにincludeを辞めました。

furcraeaHLSLEditor_UE5.7.4V5.zip


ルール: FragmentShaderCode / VertexShaderCode には 関数定義を書かない。ヘルパーが必要なら全てインライン化する。
コンパイラの整理

  • ExtractHelperFunctions + エスケープトリック (将来の一般的ヘルパー対応) は保持
    // @param float3 ColorC = 0.90,0.20,1.20//でBPでパラメーター化

furcraeaHLSLEditor_UE5.7.4V6.zip


・日本語コメントがあるとVertex Shader側のコードカラーリングが効いてない不具合を修正
・こんどは両ペインで改行が入力できないを解決
・ユーザーが .ush のボディ内で Phase * Time のように Time を直接書いても View.GameTime として展開され、HLSL
コンパイルエラー → クラッシュが防がれます。

furcraeaHLSLEditor_UE5.7.4V7.zip

ポストプロセスマテリアルを CodeMaterialAssetで作れるようになりました。
Domain が「Post Process」の時に Blendable Location ドロップダウンが横に表示されます:

CustomDepth のアウトライン系には Before Tonemapping が一般的です。

自動生成パラメーターにテクスチャが指定できるようになりました。
// ===== Fragment Shader =====
// @param Texture2D AlbedoTex
// @param Texture2D HatchTex = /Game/3Dnchu/Textures/T_Stroke-SD11.T_Stroke-SD11
// @param float HatchTiling = 10.0

なぜか裏面だけに色が乗ってる場合

● Vertex Shader Code に何か入っているはずです。

Vertex Shader Code が空でないとプラグインが「シェル法アウトライン用」と判断して:

  • TwoSided = true
  • BlendMode = Masked
  • OpacityMask = TwoSidedSign × -1(裏面のみ表示) を自動設定します。これが原因です。 修正手順:
  1. CodeMaterialAsset エディタで Vertex Shader Code を完全に空 にする
  2. Save をクリックして再生成 Vertex Shader Code が空になると:
  • TwoSided = false
  • BlendMode = Opaque
  • OpacityMask なし → 表面だけに通常表示

[UE5][Shader]furcraeaToonLock — UE5で作る Unlit + MPC ベースのトゥーンシェーダ

Material Parameter Collection(MPC)を使ってライト方向を制御する Toon Shader

最近、UE5でセルルック表現を色々試しているのですが、今回は Forward Renderer でも比較的安定して見える Toon Shader を作ってみました。

名前は furcraeaToonLock。

このシェーダでは、UE標準のライティングモデルに強く依存せず、Unlit + Emissive ベースで独自にライティングを計算しています。

そのため、

  • Forward Renderer
  • Deferred Renderer

の違いによる見た目の変化をかなり抑えられる構成になっています。

今回の記事では、

  • なぜ Unlit ベースで作ったのか
  • どうやって Toon Band を制御しているのか
  • ライト方向をどう扱っているのか
  • なぜ RampTexture 方式を考えているのか

などをまとめてみます。


なぜ Unlit + Emissive 構成にしたのか


■ Lit版 vs Unlit版 比較画像

左:

  • UE標準 Lit

右:

  • furcraeaToonLock

UE5の標準ライティングは非常に強力ですが、セルルックでは環境によって見え方が変化しやすいという問題があります。

特に、

  • Forward / Deferred の差
  • GI
  • Reflection
  • Specular
  • ポストプロセス

などが強く影響すると、「狙ったトゥーン感」が崩れやすくなります。

そこで今回は、

  • PixelNormalWS
  • DirectionalLight の方向
  • 独自の NdotL 計算

を使い、自前でセルシェーディングを行う構成にしました。

最終的な色は Emissive に出力しています。


ライト方向は Blueprint + MPC で制御

  • DirectionalLight
  • GetForwardVector
  • SetVectorParameterValue

が見える範囲

ライト方向は、Blueprint から Material Parameter Collection に渡しています。

今回使っているのは、

DirectionalLight -> GetForwardVector

です。

取得したベクトルを、

MPC_LightDir

LightDir

パラメータにセットし、マテリアル側で参照しています。

これによって、

  • ライト回転
  • Toon境界
  • ハイライト方向

などがリアルタイムに更新されます。


2Band / 3Band の Toon 制御

今回のシェーダでは、2Band / 3Band を切り替えられるようにしています。

最初は smoothstep ベースも試しましたが、グラデーション感が強くなりすぎて、セルルック特有の「段差感」が弱くなってしまいました。

そのため、最終的には step ベースで制御しています。

2Band はかなりグラフィック寄りで、強いアニメ感があります。

3Band は少しリッチで、柔らかいアニメ表現になります。


アウトラインについて

  • ON / OFF 比較

アウトラインは、別メッシュ + 別マテリアル構成で作っています。

今回は、

  • TwoSided
  • TwoSidedSign
  • OpacityMask
  • World Position Offset

を使って背面のみを描画し、法線方向へ膨らませる方式にしました。

最終的には、かなり軽量で扱いやすい構成になっています。


Material Graph 構成


■ Material Graph 全体スクショ

必須:

  • PixelNormalWS
  • MPC_LightDir
  • NdotL
  • Emissive

が見える範囲

マテリアル全体はかなりシンプルです。

基本的には、

PixelNormalWS

NdotL

step

Shadow Color / Light Color

Emissive

という流れで構成しています。

現在はさらに次の展開として、

RampTexture

を使った Toon Ramp Shader 化も進めています。


RampTexture ベースの次世代版について

現在制作中の次バージョンでは、RampTexture を使って Toon 色自体を制御する構成を試しています。

通常の toon shader は、

step(NdotL)

だけで明暗を切り替えることが多いですが、RampTexture を使うことで、

  • 暖色系
  • 紫系
  • 夜景風
  • アニメ風

など、絵作り全体をコントロールできるようになります。

現在は、

furcraeaToonRampShader

として開発中です。


Forward Renderer でも比較的安定した Toon 表現

おすすめ内容:

  • ライト回転
  • 2Band ↔ 3Band
  • Outline
  • Rim
  • Highlight

今回の構成では、ライト方向を Blueprint + MPC 経由で渡しているため、Forward Renderer 環境でも比較的安定した Toon 表現を維持できます。

特に、

  • Toon境界
  • 段差感
  • アウトライン
  • 色のコントロール

などが崩れにくい点が気に入っています。


Download / BOOTH

今回紹介した内容は、BOOTHで公開中の furcraeaToonLock をベースにした実装メモです。

  • 2Band / 3Band
  • Outline
  • Rim
  • Highlight
  • MPC Light Direction

などを含めた構成になっています。

BOOTH:
https://furcraea.booth.pm/

商品ページ:
https://furcraea.booth.pm/items/8219556

次はさらに、RampTexture を使って色味自体をコントロールできる

furcraeaToonRampShader

も作っていく予定です。

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

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

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

Deferred ShadingかForward Shading設定

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

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

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

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

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

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

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

フォワード(Forward Shading)

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

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

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

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

まとめ

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

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

左がDeferred 右がForward

今回の場合

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

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

[UE5.7.3][RenderDoc]RenderDocでGPU計測する方法

https://renderdoc.org

Latest Stable Build(.zip) を選ぶ

それは初回起動時の匿名データ収集の確認画面
一番下を選べばOK

「Do not gather or submit any statistics.」

起動する

Executable Pathに UEのパスを入れる

D:\Program Files\Epic Games\UE_5.7\Engine\Binaries\Win64\UnrealEditor.exe

Command Line ArgumentsにUEプロジェクトのパスを入れる


D:\Sandbox\UE573petit25Cl\UE574petit25Cl_Lighting\UE574petit25Cl.uproject

Lunchで起動すると

RenderDoc経由でUEが起動する

UE開いたら

UEにフォーカス当てて👉 F12押す

出てきたキャプチャをダブルクリックすると

Filterを使って描画やCompute処理だけに絞って確認しています。
DrawCallはDrawIndexed DrawInstanced Dispatch Clearなので

Dispatch をFillterしてみると見えた。

Filterはこう使う👇

■ ■ 重い描画だけ見たい

Draw

■ ■ Computeだけ

Dispatch

■ ■ 特定のPass

BasePass

どのDrawCallを見るべきか?

見る順番はこれでOK。

1. まず「大きいPass名」を見る

UEならこのあたりを探す。

ShadowDepths
影が重いか見る。

BasePass
メッシュ本体の描画。マテリアルやテクスチャ参照を見る。

Lighting / DeferredLighting
ライト計算を見る。

Translucency
半透明。重くなりやすい。

PostProcess
Bloom、Tonemap、DOF、独自ポストを見る。

2. 次に、その中のDrawを見る

Passを開いて、こういう行をクリックする。

DrawIndexed
DrawInstanced
Dispatch

クリックしたら右側で見る場所は3つ。

Texture Viewer
→ 何が描かれているか見る。

Pipeline State
→ どのShader、Texture、Blend設定か見る。

Mesh Viewer
→ どのメッシュを描いているか見る。

まずShadow、BasePass、Translucency、PostProcessなどの大きいPass単位で見て、次にDrawIndexedやDispatchを選んで、RenderTarget・Shader・Texture参照を確認します。

最初に見るおすすめ

迷ったらまずこれ。

BasePassのDrawIndexed  や  PostProcessのDraw/Dispatchだが
→ マテリアル、テクスチャ、メッシュが確認しやすい。
BassPassが出たらクリックしてフラッグを立てるそして、Filterを解除

フラッグが立っている場所までスクロールすると中が見える状態

ExecutecommandListに入ってるのが見えないので。設定を変えてキャプチャし直し。

DX12ではExecuteCommandListsとしてまとまって見える場合があるので、RenderDocではCapture all Cmd Listsを有効にするか、確認用にDX11で起動してDrawIndexed単位で確認します。

■ やり直す設定

RenderDocの Launch Application 画面でチェックする:

  • Capture all Cmd Lists
  • Ref all Resources も付けてOK

DX11でやる

Command Lineにこれ入力👇

"D:\Sandbox\UE573petit25Cl\UE574petit25Cl_Lighting\UE574petit25Cl.uproject" -d3d11 -NoRHIThread

DX12ではDrawCallがコマンドリストにまとまるため、Pass単位で処理を確認しつつ、必要に応じてDX11でDrawIndexed単位でも確認しています。

見つかった!

DrawIndexedInstanced(6, 17)

意味:

  • Index数:6(1ポリの簡単な形)
  • Instance数:17(同じものを17個描画)

👉 パーティクル or 草(インスタンシング)

DrawIndexedInstancedを見て、インスタンシングで同じメッシュを複数描画していることを確認できます。

1回のDrawCallで複数インスタンスを描画しているため、DrawCall削減による最適化がされています。

次見るべきポイント PSSetShader (Pixel Shader)

重そうか(分岐 / テクスチャ数) マテリアル名

PSSetShaderResources テクスチャ数(ここ重要)

見る

  • 何枚使ってるか
  • 無駄に多くないか

目安

  • 1〜3枚 → 軽い
  • 4〜6枚 → 普通
  • 7枚以上 → 重い可能性

このDrawはまだ画面を全部埋めてない


■ ここでやるべき操作(超重要)

■ ① Eventを少しずつ進める

👉 左のEvent Browserで

  • ↓キー or クリックで1個ずつ進む

■ ② 右のTexture Viewerを見る

👉 変化を見る

変わった

■ 今起きてること

👉 Eventを進めたことで

  • 上の黒い部分に
  • 小さい点(葉っぱ)が出てきた

👉 つまり

このDrawが実際に画面に何かを描いてる


■ 今のDrawの正体

左を見ると👇
DrawIndexedInstanced(6, 16)

👉 意味

  • 小さいメッシュ(葉っぱ)
  • 16個まとめて描画

👉 つまり

パーティクル or インスタンス草

■ ① このDrawで何が変わった?

👉 右画面で見る

  • 点が増えた
  • 画面に追加された

👉 これ言える

「このDrawでパーティクルが追加されているのが確認できます」

イベントを進めることで、最初は未描画だった画面が、各Drawによって徐々に埋まっていく様子を確認できます。

チェッカーが消えるタイミングを見ることで、どの描画処理が最終的な画面に影響しているかを特定できます。

RenderDocではイベントを進めながら、どのDrawで画面に要素が追加されるかを確認しています。今回のケースでは、インスタンシングされたパーティクルが描画されているのが確認できます。

このようにDrawごとの変化を見ることで、描画順や負荷のかかる処理を特定しています。

■ ここまでできてること

  • DrawCall見つけた
  • 実際の変化を追えてる
  • インスタンシング理解してる

■ どういう意味?

RenderDocの流れ👇

  1. 最初
    👉 何も描いてない(黒 / チェッカー)
  2. 途中
    👉 一部だけ描画(点・葉っぱ)

  3. 👉 全体が描画で埋まった ← 今ここ

👉 つまり

この辺のDrawで最終結果に近づいた

イベントを進めることで、最初は未描画だった画面が、各Drawによって徐々に埋まっていく様子を確認できます。

チェッカーが消えるタイミングを見ることで、どの描画処理が最終的な画面に影響しているかを特定できます。

状態は「まだ画面を全部塗るDrawに来てない」ってこと。
(チェッカーが残ってる=未描画領域がある)

■ 今の位置の意味

左を見ると👇

  • BasePass
  • DrawIndexedInstanced(6, 17)(葉っぱ)

👉 これは

“オブジェクト描画フェーズ”


👉 だから

  • 一部だけ描かれる
  • 画面全部は埋まらない
  • チェッカー残る

■ 次に探すべきもの(重要)

👉 画面を一気に埋めるDraw


■ 目印

これ探す👇

  • PostProcess
  • Tonemap
  • Fullscreen
  • Resolve
  • Composite

■ 何が起きてるか(技術的に)

👉 今のDrawで

  • SceneColorに
  • あなたのHLSLで描いた月が加算された

👉 つまり

Pixel Shaderが最終色を決めている瞬間

このDrawでは、独自のHLSLシェーダを使って月の表現を描画していて、SceneColorに対してピクセル単位で最終色を出力しています。

RenderDocでこのDrawを確認することで、実際にどのシェーダが最終結果に影響しているかを特定できます。

このようにDrawごとの出力を追うことで、意図したシェーダが正しく画面に反映されているかを検証できます。

■ ここで見るべきポイント(重要)


■ ① Pixel Shader

👉 ここ

PSSetShader

HLSLが動いてる場所

■ ② Shader Resource(テクスチャ)

👉 月の模様

Shader Resource View

■ ③ DrawIndexedInstanced

👉 描画回数

■ さらに強い視点

  • フルスクリーンじゃない → オブジェクト描画
  • 一部だけ変わる → メッシュ単位
  • ピクセルで模様 → Pixel Shader

RenderDocを使うことで、実際の描画結果とシェーダの対応関係を確認しています。

■ 結論(今回のケース)

👉 重くなりやすい要因はこれ

① パーティクル(葉っぱ)のインスタンス描画

  • DrawIndexedInstanced(6, 16/17)
  • 数が増えると一気に負荷増える

② Pixel Shader負荷

  • 独自HLSL(月の模様)
  • ピクセル単位で計算してる

③ テクスチャ参照

  • PSSetShaderResources
  • 複数テクスチャ使ってる

④ オーバードロー(これが一番重要)

👉 葉っぱ(半透明)が重なる
👉 同じピクセル何回も計算される


■ 一言でまとめると

パーティクルのオーバードロー+ピクセルシェーダ負荷


今回のケースでは、パーティクルのインスタンス描画によるオーバードローと、ピクセルシェーダの計算量が主な負荷要因だと考えています。


特に半透明オブジェクトは重なりやすく、ピクセル処理が増えるため注意が必要です。


■ もう一段上

👉 改善案も言う

  • インスタンス数制御
  • テクスチャ削減
  • シェーダ簡略化
  • LODや距離カリング

■ まとめ

👉 今回の分析

  • Draw見た
  • Shader見た
  • 結果から原因言えた

[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の使用方法が解説されています。

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

お家のリノベーション工事のためにmayaでモデリングした話

A story about modeling in Maya for a home renovation project.

とりあえず、古い家をテクスチャも写真を撮って作った。

参考物件

古い家のモデリングをした。

下イメージで施工業者さんとイメージ合わせした。

完成した状態

Maya Data (管理用)
https://drive.google.com/file/d/1QYO2HkdBjbH5wDVlTsArjUGc1v8Y8SLp/view?usp=drive_link

実際にできた家

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 側で素直に使える状態 になっています。

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