このサイトは、只今WEB業界で活躍中のデザイナー、プログラマーの方々の情報を集めたweb統合情報サイトです。

Archives Details

Three.js – ShaderMaterialで、ブレンドシェイプ(MorphTarget)アニメーション対応

JavaScript

2025.10.15

うぉ。

超久々に記事を書くような。。

気がつけば2ヶ月も空いたのか。

その間、色々やった気がするなぁ。。

Rust × TauriでがっつりGUIツール作ったり、ファミリーデーのために子供でも遊べるゲームのためラズパイ5用いて BLEアプリケーション作ったり、Unityで使用するFBXファイルをglTF/ glbに変換してThree .js用いてレンダリングを行ったりと。。。

諸々、記事にしようと思っていたらもう2ヶ月ですよ。おそロシア。

でも、今回は残したいなと思い、重い腰とブラウザを(そんな時代ではない)立ち上げ記事を書こうと。

なんで書こうと思ったか?ですが、「結構ハマった」からです。

やられました。お陰様でShaderMaterialもGLSL Shaderもすっかり仲良くなれた気がします。

そう、表題でもあるように「ShaderMaterialで、ブレンドシェイプ(MorphTarget)アニメーション対応」しようとしたらやたら滅多とぶつかった訳です。

 

FBXファイルをglTF/ glbに変換

何はともあれですよ。

FBXモデルデータをウェブで利用できるように、glTF/ glbに変換しようではありませんか。

ということで、以前書いた記事の「汎用 3D mesh/model viewerを求め。と、簡単に、FBXファイルをglTF(glb)に変換ツールを求め。」で、書いた様に「FBX2glTF」を用いてファイルを変換します。

 

インストール

こちらから環境にあった実行ファイルをダウンロードしましょう。

例で、Macだと「FBX2glTF-darwin-x64」となります。

ダウンロード完了したら、実行権限を付与し、パスの通った任意のフォルダ(/usr/local/bin や、~/bin)に配置します。

chmod +x ~/Downloads/FBX2glTF-darwin-x64

その際、使いやすい様にRenameを行います。

sudo mv FBX2glTF-darwin-x64 /usr/local/bin/FBX2glTF
FBX2glTF -i model.fbx -b
  • -i … 入力ファイル
  • -b … バイナリ形式で出力 (.glb)
  • -o … 出力ファイル名やフォルダ指定
FBX2glTF -i model.fbx -b -o output.glb

これで JSON形式の .gltf ではなく、単一バイナリの .glb が出力されます。

 
他の便利オプション
  • –binary または -b … GLB形式で出力
  • –embed … .gltf にバイナリや画像を埋め込む(JSON + base64一体型)
  • –verbose … 詳細ログ
  • –draco … Draco圧縮
面倒なのでツールで一括変換しよう

ルートディレクトリ指定で、ファイルを探索して変換するようにshell scriptを用意しましょう。

#!/bin/bash

# === 使い方 ===
# ./convert_fbx.sh /path/to/search/root
# =================

# ルートパスを引数から取得
ROOT_PATH="$1"

# 引数が空ならエラー
if [ -z "$ROOT_PATH" ]; then
  echo "使用方法: $0 <探索ルートパス>"
  exit 1
fi

# 指定されたディレクトリが存在するか確認
if [ ! -d "$ROOT_PATH" ]; then
  echo "エラー: 指定したディレクトリが存在しません: $ROOT_PATH"
  exit 1
fi

# 再帰的に .fbx ファイルを探索して処理
find "$ROOT_PATH" -type f -name "*.fbx" | while read -r fbx_file; do
  # 出力ファイルパスを決定(.fbx → .glb に置き換え)
  output_file="${fbx_file%.fbx}.glb"

  echo "変換中: $fbx_file"
  echo "出力先: $output_file"

  # 同階層に .glb を出力
  FBX2glTF -i "$fbx_file" -b -o "$output_file"

  echo "完了: $(basename "$output_file")"
  echo "-----------------------------"
done

echo "全てのFBXファイルの変換が完了しました。"

 

上記のscript用いて、以下の様に任意の実行ルートパスを指定することで一括変換が行えます。

convert_fbx.sh ./path/to/target_root

さて下ごしらえは終わりで、いよいよ本題です。

 

ShaderMaterial

そもそもなんでShaderMaterialを使用するかですが、すでにUnityで制作を進めている方でShaderを用いた色替えなどの処理を行っているため、GLSL Shaderを作成しShaderMaterialでマテリアル化を行った訳で、fbxアニメーションなども適応させたShaderMaterial作成はこんな感じで収まっていたのですが、

return new THREE.ShaderMaterial({
    uniforms,
    vertexShader,
    fragmentShader,
    transparent: blended,
    alphaTest: cutout ? (matInfo.CutoutThreshold ?? 0) : 0,
    depthWrite: !blended
} as any)

 

ブレンドシェイプ(MorphTarget)アニメーションだけまだ適応されていなく、適応するためにChatGPTと相談して進めていたのですが、ShaderMaterialでブレンドシェイプ(MorphTarget)を適応させるには、こんな感じである程度Shader作成が必要になるって言うんですよ。

const mat = new THREE.ShaderMaterial({
  uniforms: { ... },
  vertexShader,
  fragmentShader,
  defines: { USE_MORPHTARGETS: '' },
  morphTargets: true,           
});

その他にも

// definesの継承も勧められたり。。
const material = new THREE.ShaderMaterial({
  uniforms: { ... },
  vertexShader,
  fragmentShader,
  defines: {
    ...(defines ?? {}),
    USE_MORPHTARGETS: '',
  },
  morphTargets: true,   
});

morphTargets: trueが定義されていないのか、IntelliJで定義エラーが出るし。。

シェーダー側はこのような流れ。

■ MorphTarget 属性を受け取る

#ifdef USE_MORPHTARGETS
  attribute vec3 morphTarget0;
  attribute vec3 morphTarget1;
  attribute vec3 morphTarget2;
  attribute vec3 morphTarget3;
  uniform float morphTargetInfluences[8];
#endif

■ 頂点位置を加算する

#ifdef USE_MORPHTARGETS
  transformed += (morphTarget0 - position) * morphTargetInfluences[0];
  transformed += (morphTarget1 - position) * morphTargetInfluences[1];
  transformed += (morphTarget2 - position) * morphTargetInfluences[2];
  transformed += (morphTarget3 - position) * morphTargetInfluences[3];
#endif

 

ネットで検索してもそんな感じで見つかるし、この辺のソースを適応しても

morphtarget_vertex.glsl.js

export default /* glsl */`
#ifdef USE_MORPHTARGETS

    // morphTargetBaseInfluence is set based on BufferGeometry.morphTargetsRelative value:
    // When morphTargetsRelative is false, this is set to 1 - sum(influences); this results in position = sum((target - base) * influence)
    // When morphTargetsRelative is true, this is set to 1; as a result, all morph targets are simply added to the base after weighting
    transformed *= morphTargetBaseInfluence;
    transformed += morphTarget0 * morphTargetInfluences[ 0 ];
    transformed += morphTarget1 * morphTargetInfluences[ 1 ];
    transformed += morphTarget2 * morphTargetInfluences[ 2 ];
    transformed += morphTarget3 * morphTargetInfluences[ 3 ];

    #ifndef USE_MORPHNORMALS

    transformed += morphTarget4 * morphTargetInfluences[ 4 ];
    transformed += morphTarget5 * morphTargetInfluences[ 5 ];
    transformed += morphTarget6 * morphTargetInfluences[ 6 ];
    transformed += morphTarget7 * morphTargetInfluences[ 7 ];

    #endif

#endif
`;

色々やったが、なんか意図していない形で移動や変形が行われる。

複数メッシュ組で複数マテリアルの組を作っていたりするので、こんな感じで修正が必要っても言って来るし。

const mat = hasMorph || hasSkin ? baseMaterial.clone() : baseMaterial;
mat.morphTargets = hasMorph;
mat.skinning = hasSkin;
mat.defines = {
  ...(baseMaterial.defines || {}),
  ...(hasMorph ? { USE_MORPHTARGETS: '' } : {}),
  ...(hasSkin ? { USE_SKINNING: '' } : {}),
};
mat.uniforms = THREE.UniformsUtils.clone(baseMaterial.uniforms);
mat.needsUpdate = true;

しかも、AnimatorもTransformとMorphTargetも分けた方が良いなどとChatGPTは言って来るので対応していたらAnimatorも結構カオスな感じに。。。

// こちらでアニメーションを行っていたのですが、
additiveClip.blendMode = THREE.AdditiveAnimationBlendMode

// 通常ボーンとマージしてみたり
const additiveClip = THREE.AnimationUtils.makeClipAdditive(

// ・名前に "Blend" を含む ・morphTrack が存在する trackは別のAnimationClipで作成したり
const morphClip = new THREE.AnimationClip(clip.name, clip.duration, morphTracks)

原因は他にあり、GPU に attribute が届いてないのか? Morph を持つMeshは、独自の ShaderMaterial インスタンスを持たせる必要があるのか?などなどChatGPTに質問したら「それが原因だね」と常に原因を決めつけてくれるが、全部試すが全然うまくいかず、色々考えてましたが、

無理やり、StandardMaterialを適応すると意図した動きになっているので、

そもそも、Shaderだけが原因ではないかと探っていく中、babylonjsが提供してGoogle Chrome拡張のSpector.jsを使い、意図した動きとなるStandardMaterial用いたときのドローコールを確認していると、ついに見つけたのです!

これ。

#ifdef USE_MORPHTARGETS
    attribute vec3 morphTarget0;
    attribute vec3 morphTarget1;
    attribute vec3 morphTarget2;
    attribute vec3 morphTarget3;
    #ifdef USE_MORPHNORMALS
        attribute vec3 morphNormal0;
        attribute vec3 morphNormal1;
        attribute vec3 morphNormal2;
        attribute vec3 morphNormal3;
    #else
        attribute vec3 morphTarget4;
        attribute vec3 morphTarget5;
        attribute vec3 morphTarget6;
        attribute vec3 morphTarget7;
    #endif
#endif

#ifdef USE_MORPHTARGETS
    #ifndef USE_INSTANCING_MORPH
        uniform float morphTargetBaseInfluence;
        uniform float morphTargetInfluences[MORPHTARGETS_COUNT];
    #endif
    uniform sampler2DArray morphTargetsTexture;
    uniform ivec2 morphTargetsTextureSize;
    vec4 getMorph(const in int vertexIndex, const in int morphTargetIndex, const in int offset) {
        int texelIndex = vertexIndex * MORPHTARGETS_TEXTURE_STRIDE + offset;
        int y = texelIndex / morphTargetsTextureSize.x;
        int x = texelIndex - y * morphTargetsTextureSize.x;
        ivec3 morphUV = ivec3(x, y, morphTargetIndex);
        return texelFetch(morphTargetsTexture, morphUV, 0);
    }
#endif

varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vViewDir;

void main() {
    vUv = uv;
    vec3 transformed = position;

    #ifdef USE_MORPHTARGETS
        transformed *= morphTargetBaseInfluence;
        for (int i = 0; i < MORPHTARGETS_COUNT; i++) {
            if ( morphTargetInfluences[i] != 0.0) transformed += getMorph(gl_VertexID, i, 0).xyz * morphTargetInfluences[i];
        }
    #endif

....

 

これですべてが解決。

USEの定義も必要ないし。。

ずっっっっっとChatGPTは嘘(嘘ではないが)言ってくるし。。

ChatGPTが答えを出せずまま、解決したので勝手にAIに勝った日とする。

AIに頼るのもいいが、大事なのは「今、何が起きていてなんでそうなっているのか」を把握するのが非上に大事だと思った今日このごろでした。

しかし、少々時間を使ってしまった。。

ではではぁ。

Comment

Related Article

Three.js – ShaderMaterialで、ブレンドシェイプ(MorphTarget)アニメーション対応

2025.10.15

JavaScriptで、DOMを放り投げる処理

2024.07.27

2022 VIVA JS World Cup 開幕!! 〜 Vue3で作るサッカーゲーム 〜

2022.12.24

OAuthのフローを可視化できるツールを作ってみました。

2020.05.17

令和の時代に、JavaScriptで Shift-JISファイル作成 全銀データフォーマットに対応する。

2020.03.03

インターネットにて世論調査を行う「世論Web」サービスを始めてみました。

2020.01.31

年末のレトロゲーム熱の際、ファミコンソフト一覧パッケージ作ってました。

2020.01.24

あと10日で「jsdo.it」のサービスが終わってしまう!! ソースダウンロードまだの方は急げぇ〜!

2019.10.21

正規表現 先読み後読み 論理積

2019.07.28

「二段階認証?」という方も 5分で覚える パスワードレス WebAuthnのまとめ

2019.07.07

CATEGORY LIST

LATEST NEWS

Three.js - ShaderMaterialで、ブレンドシェイプ(MorphTarget)アニメーション対応

JavaScript

2025.10.15

ゲーム開発に必要な基本数学入門

Rust

2025.08.15

Godot 4 & WebAssemblyで様々なデータフォーマットを処理

Godot

2025.07.06

Godot 4 & WebAssemblyで、Hello WebAssembly! - godot-wasm

Godot

2025.06.21

Godot 4 & Rust 始め方

Rust

2025.06.19

ご、ごめん。。今はFlutterの気分じゃないんだ。。

Flutter

2025.05.30

AIのために働け。AI リーダブルコーディングな未来。

AI・Bot・algorithm

2025.05.21

Rustでつくる ふっかつのじゅもん

Rust

2025.04.29

Tauri(Rust) × AI で作る GitGUIクライアントアプリ その5

Rust

2025.04.14

う、動くぞ! Mac mini Apple M4 Pro で PS3ソフトを遊ぶ。RPCS3 Mac版を起動

Game

2025.04.12

Tauri(Rust) × AI で作る GitGUIクライアントアプリ その4

Rust

2025.04.10

Tauri(Rust) × AI で作る GitGUIクライアントアプリ その3

Rust

2025.04.08

RANKING

Follow

SPONSOR

現在、掲載募集中です。



Links

About Us

WEBデザイナーの、WEBデザイナーによる、WEBデザイナーの為のサイト。「みんなで書こう!」と仲間を募ってみたが、結局書くのは自分だけとなってしまいました。日々のメモを綴っていきます。

Entry Profile

Graphical FrontEnd Engineer
- Daisuke Takayama

MAD CITY 北九州市で生まれ育つ。20代はバンド活動に明け暮れ、ふと「webデザイナーになりたい。」と思い、デジタルハリウッド福岡校入学。卒業後、数々の賞を受賞、web業界をざわつかせる。
現在、主に、ゲーム制作中心に港区六本木界隈で活動中。

FOLLOW US