このサイトは、只今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

効率の良い AI駆動開発について考える

AI・Bot・algorithm

2025.11.09

MacとClaude Codeで構築する cc65(NES)開発環境

Game

2025.10.24

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

RANKING

Follow

SPONSOR

現在、掲載募集中です。



Links

About Us

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

Entry Profile

Graphical FrontEnd Engineer
- Daisuke Takayama

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

FOLLOW US