🤖

ブレンドシェイプの調整に便利なツールを社内開発!

に公開

こんにちは!
株式会社マトリックス、ゲーム事業部デザイン課のKです!
今回は、ブレンドシェイプの調整に便利なツールを、社内のプログラマーに開発していただきましたので、そのツール制作過程をお伝えしていきます。

ブレンドシェイプとは

ブレンドシェイプは、3Dモデルの形状を変形させる技術です。
元となるモデル(ベースモデル)と、そのモデルを変形させたもの(ターゲットモデル)をブレンドして変形するもので、主に表情アニメーションに利用されます。

ブレンドシェイプの難点

便利なブレンドシェイプですが、使いづらい面もあります。
それは、ベースモデルに加えた変更がターゲットモデルには反映されないということです。
例えばターゲットモデル①を作成後に、ベースモデルの目の大きさを変更する必要がでてきたとします。
しかし、そのベースの変更はターゲットには反映されず、このまま①をブレンドシェイプして使っても目の大きさは変更前に戻ってしまいます。
そこでこちらの問題を解決するツールを社内で作って頂くことにしました!

ツール作成

なぜ社内でツール作成を?

Matrix社内にはツールを開発してくださる方々います!
プロジェクトで実際に使う際に安全性も確かで、機能の追加や調整もして頂けるので、社内でのツール開発をしております。

AIでのツール作成

今回はエンジニア側でちょうど触っていたコード生成AI(Github Copilot)で作成しました。
AIによるコーディングと結構相性もよさそうでしたのでいいタイミングでした。

最初に入力したプロンプト
mayaで使用できるmelを作成したいです。
最初に変更前のメッシュを選択、次に変更後のメッシュを選択、その後、ブレンドシェイプのメッシュを複数選択して、実行ボタンを押したときに処理を開始します。
変更前から変更後への頂点位置の変更差分を、ブレンドシェイプのメッシュにも反映させて頂点位置を更新してください。
小さな専用ウィンドウを出し、処理実行に必要な手順の説明を記載します。このウィンドウに実行ボタンを配置してください。
まずはこの実装対応に関連するドキュメントを作成し、この作業をするに適当なフォルダを作成した中に配置してください

これに沿って実装仕様書、ユーザーガイド、README、melまで作成してくれました。
実際、このままでは動作しませんでしたので、エラーログと戦わせたり、ドキュメントと異なる実装になっていないか確認させたりを繰り返します。
今回の場合は10回もやり取りなく動作するようにできました。
やったね!

継続的に機能を提供していきたいという点もあり、
また作成物を一か所にまとめることで精度向上を期待しています。
今後、たくさん作っていける環境を作っていきたいですね。

(今回はGithub Copilotを使用しましたが、ClaudeやCursorでも問題なく作成できると思います)

実際にツールを使ってみると

それでは今回作って頂いてツールを使って、ベースモデルの変更をターゲットモデルに加えてみましょう。
このツールは、ベースモデルとターゲットモデル、変更を加えたベースモデルの3つを用意します。
変更を加えたベースモデルを選択後、ベースモデルを選択、更にターゲットモデルを選択してmelを実行します。
https://youtu.be/rafh7V-EPXg

無事、ベースモデルの変更をターゲットモデルに追加できました!
このツールを使えば複数のターゲットモデルに同時に変更を加えることも可能です。
ぜひ活用してみてください!
今回作って頂いたmelはこちらになります。

作成されたMEL

blendShapeVertexTransfer.mel
// ============================================================================
// Blend Shape Vertex Transfer Tool
// Maya MEL Script for transferring vertex position differences to blend shapes
// 
// Author: t-yoshino
// Version: 1.0.0
// Date: 2025-08-27
// ============================================================================

// Global variables for UI
global string $g_vertexTransferWindow = "vertexTransferWindow";
global string $g_statusText = "";

// ============================================================================
// Main UI Creation Function
// ============================================================================
global proc createVertexTransferUI()
{
    global string $g_vertexTransferWindow;
    
    // Close existing window if it exists
    if (`window -exists $g_vertexTransferWindow`)
        deleteUI $g_vertexTransferWindow;
    
    // Create main window
    window -title "Blend Shape Vertex Transfer Tool"
           -widthHeight 350 250
           -resizeToFitChildren true
           -sizeable false
           $g_vertexTransferWindow;
    
    columnLayout -adjustableColumn true -columnOffset "both" 10 -rowSpacing 10;
    
    // Title
    text -label "Blend Shape Vertex Transfer Tool" -font "boldLabelFont" -height 25;
    separator -height 10 -style "none";
    
    // Instructions
    frameLayout -label "Usage" -collapsable false -borderStyle "etchedIn";
    columnLayout -adjustableColumn true -columnOffset "both" 5;
    text -label "Steps:" -font "boldLabelFont" -align "left";
    text -label "1. Select AFTER mesh (modified)" -align "left";
    text -label "2. Select BEFORE mesh (original)" -align "left";
    text -label "3. Select blend shape meshes (multiple)" -align "left";
    text -label "4. Click 'Transfer Vertices' button" -align "left";
    setParent..;
    setParent..;
    
    separator -height 10 -style "none";
    
    // Status display
    frameLayout -label "Status" -collapsable false -borderStyle "etchedIn";
    columnLayout -adjustableColumn true -columnOffset "both" 5;
    text -label "Please check selected objects" -align "left" statusDisplay;
    setParent..;
    setParent..;
    
    separator -height 15 -style "none";
    
    // Buttons
    rowLayout -numberOfColumns 2 -columnWidth 1 175 -columnWidth 2 175;
    button -label "Transfer Vertices" -command "transferVertices()";
    button -label "Close" -command ("deleteUI " + $g_vertexTransferWindow);
    setParent..;
    
    // Show window
    showWindow $g_vertexTransferWindow;
    
    // Update status
    updateSelectionStatus();
}

// ============================================================================
// Update Selection Status
// ============================================================================
global proc updateSelectionStatus()
{
    string $selection[] = `ls -selection`;
    string $statusMsg = "";
    
    if (size($selection) == 0) {
        $statusMsg = "No objects selected";
    } else if (size($selection) < 3) {
        $statusMsg = "Select at least 3 meshes";
    } else {
        $statusMsg = ("Selected: " + size($selection) + " objects");
    }
    
    text -edit -label $statusMsg statusDisplay;
}

// ============================================================================
// Main Transfer Function
// ============================================================================
global proc transferVertices()
{
    string $selection[] = `ls -selection`;
    
    // Validate selection
    if (!validateSelection($selection)) {
        return;
    }
    
    string $afterMesh = $selection[0];
    string $beforeMesh = $selection[1];
    string $blendShapes[];
    
    // Get blend shape meshes (from index 2 onwards)
    for ($i = 2; $i < size($selection); $i++) {
        $blendShapes[size($blendShapes)] = $selection[$i];
    }
    
    print ("After mesh: " + $afterMesh + "\n");
    print ("Before mesh: " + $beforeMesh + "\n");
    print ("Blend shapes: " + size($blendShapes) + "\n");
    
    // Calculate vertex differences
    vector $vertexDeltas[] = calculateVertexDelta($afterMesh, $beforeMesh);
    
    if (size($vertexDeltas) == 0) {
        text -edit -label "Error: Failed to calculate vertex deltas" statusDisplay;
        return;
    }
    
    // Apply deltas to each blend shape
    int $successCount = 0;
    for ($blendShape in $blendShapes) {
        if (applyDeltaToMesh($blendShape, $vertexDeltas)) {
            $successCount++;
        }
    }
    
    // Update status
    string $resultMsg = ("Complete: " + $successCount + "/" + size($blendShapes) + " meshes updated");
    text -edit -label $resultMsg statusDisplay;
    print ($resultMsg + "\n");
}

// ============================================================================
// Validate Selection
// ============================================================================
global proc int validateSelection(string $selection[])
{
    if (size($selection) < 3) {
        text -edit -label "Error: Select at least 3 meshes" statusDisplay;
        return false;
    }
    
    // Check if all selected objects are meshes
    for ($obj in $selection) {
        string $shapes[] = `listRelatives -shapes $obj`;
        if (size($shapes) == 0) {
            text -edit -label ("Error: " + $obj + " is not a mesh") statusDisplay;
            return false;
        }
        
        string $nodeType = `nodeType $shapes[0]`;
        if ($nodeType != "mesh") {
            text -edit -label ("Error: " + $obj + " is not a mesh") statusDisplay;
            return false;
        }
    }
    
    // Check vertex count consistency
    string $afterMesh = $selection[0];
    int $vertexCountArray[] = `polyEvaluate -vertex $afterMesh`;
    int $vertexCount = $vertexCountArray[0];
    
    for ($i = 1; $i < size($selection); $i++) {
        int $currentVertexCountArray[] = `polyEvaluate -vertex $selection[$i]`;
        int $currentVertexCount = $currentVertexCountArray[0];
        if ($currentVertexCount != $vertexCount) {
            text -edit -label ("Error: Vertex count mismatch (" + $selection[$i] + ")") statusDisplay;
            return false;
        }
    }
    
    return true;
}

// ============================================================================
// Calculate Vertex Delta
// ============================================================================
global proc vector[] calculateVertexDelta(string $afterMesh, string $beforeMesh)
{
    vector $deltas[];
    
    int $vertexCountArray[] = `polyEvaluate -vertex $afterMesh`;
    int $vertexCount = $vertexCountArray[0];
    
    for ($i = 0; $i < $vertexCount; $i++) {
        // Get vertex positions
        float $afterPos[] = `pointPosition ($afterMesh + ".vtx[" + $i + "]")`;
        float $beforePos[] = `pointPosition ($beforeMesh + ".vtx[" + $i + "]")`;
        
        // Calculate delta
        vector $delta = <<($afterPos[0] - $beforePos[0]), 
                         ($afterPos[1] - $beforePos[1]), 
                         ($afterPos[2] - $beforePos[2])>>;
        
        $deltas[size($deltas)] = $delta;
    }
    
    print ("Vertex delta calculation complete: " + size($deltas) + " vertices\n");
    return $deltas;
}

// ============================================================================
// Apply Delta to Mesh
// ============================================================================
global proc int applyDeltaToMesh(string $meshName, vector $deltas[])
{
    int $vertexCountArray[] = `polyEvaluate -vertex $meshName`;
    int $vertexCount = $vertexCountArray[0];
    
    if (size($deltas) != $vertexCount) {
        print ("Error: " + $meshName + " vertex count mismatch\n");
        return false;
    }
    
    for ($i = 0; $i < $vertexCount; $i++) {
        // Get current position
        float $currentPos[] = `pointPosition ($meshName + ".vtx[" + $i + "]")`;
        
        // Apply delta
        vector $delta = $deltas[$i];
        float $newPos[] = {$currentPos[0] + $delta.x, 
                          $currentPos[1] + $delta.y, 
                          $currentPos[2] + $delta.z};
        
        // Set new position
        move -absolute $newPos[0] $newPos[1] $newPos[2] ($meshName + ".vtx[" + $i + "]");
    }
    
    print ("Mesh update complete: " + $meshName + "\n");
    return true;
}

// ============================================================================
// Utility Functions
// ============================================================================

// Refresh UI status when selection changes
global proc refreshVertexTransferUI()
{
    global string $g_vertexTransferWindow;
    
    if (`window -exists $g_vertexTransferWindow`) {
        updateSelectionStatus();
    }
}

// This scriptJob automatically updates the UI when selection changes.
// If Maya performance is a concern, comment out this line to disable auto-update.
// Enable auto-update on selection change
scriptJob -event "SelectionChanged" "refreshVertexTransferUI()";

print "Blend Shape Vertex Transfer Tool loaded successfully.\n";
print "Use 'createVertexTransferUI()' to open the tool.\n";

使用の際は、上記のスクリプトをMAYAのscriptsフォルダにを入れた後、以下のコードをMELのスクリプトエディタに入れてください。

source "blendShapeVertexTransfer.mel";
createVertexTransferUI();

まとめ

ここまでお読みいただきありがとうございました!
ゲームのモデルを作っていく過程では、簡単だけど何回もする必要がある作業や、標準機能では難しい作業がよくあります。
そんな時はプログラマーの方にどういった機能が欲しいのかを相談して、作業を効率化していきましょう!

Discussion