<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://danayagi.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://danayagi.github.io/" rel="alternate" type="text/html" /><updated>2026-10-22T14:07:35+09:00</updated><id>https://danayagi.github.io/feed.xml</id><title type="html">チーム犬スタジオ開発備忘録</title><subtitle>いかがわしいゲームを作っています</subtitle><entry><title type="html">ボトムアップ型のモーションコントローラーを作った</title><link href="https://danayagi.github.io/live2d/unity/animation/2026/06/28/Live2DMotionController.html" rel="alternate" type="text/html" title="ボトムアップ型のモーションコントローラーを作った" /><published>2026-06-28T00:00:00+09:00</published><updated>2026-06-28T00:00:00+09:00</updated><id>https://danayagi.github.io/live2d/unity/animation/2026/06/28/Live2DMotionController</id><content type="html" xml:base="https://danayagi.github.io/live2d/unity/animation/2026/06/28/Live2DMotionController.html"><![CDATA[<h3 id="1-概要">1. 概要</h3>

<p>Live2DモデルをUnity上で動かすための自作コントローラーを作った。</p>

<p>最大の特徴は、単一のシステムが全体を統括するトップダウン型の管理ではなく、個々のモーションデータの側に再生条件やパラメータ変動などのロジックを個別に持たせる「ボトムアップ型」の設計を採用している点。</p>

<p><br /></p>

<p>エディタ側ではノードベースでロジックを組み立てられるようになっており、数値やフラグの管理、条件分岐、反復処理などを視覚的に組むことができる。</p>

<p class="caption-text">▼Unityエディタ拡張で作成したノードベースエディタ
<img src="/assets/img/20260628/1-1.png" alt="fig1-1.png" /></p>

<p><br />
<br /></p>

<p>また、アニメーションカーブのデータもUnityエディタ上で作成できる仕組みにしており、後述するランダマイズ処理にも対応している。</p>

<p class="caption-text">▼Unityエディタ拡張で作成したアニメーションカーブエディタ
<img src="/assets/img/20260628/1-2a.png" alt="fig1-2a.png" /></p>

<p class="caption-text">▼作成したアニメーションカーブの情報を受け取るノード。後述する「汎用キー」とアニメーションカーブを紐づけている。
<img src="/assets/img/20260628/1-2b.png" alt="fig1-2b.png" /></p>

<p><br />
<br /></p>

<ul>
  <li><strong>🔞使用例（リンク先R18注意）</strong>
    <ul>
      <li>https://www.pixiv.net/artworks/144581643</li>
    </ul>
  </li>
</ul>

<h3 id="2-なぜ作ったのかなぜ必要だったのか">2. なぜ作ったのか（なぜ必要だったのか）</h3>

<p>Live2DモデルをUnityで動かす際、既存の手法では「動的なロジックの構築」と「時系列の細かな微調整」を両立させるのが難しく、複雑な制御をする上で限界を感じていたため。</p>

<p><br /></p>

<p>具体的には以下の2つの問題に直面していた。</p>

<ul>
  <li>
    <p><strong>① Mecanim（Animation Controller）の限界</strong></p>

    <p>レイヤーを大量に重ねると挙動が不安定になりやすい。</p>

    <p>同じパラメータを操作していることによるキーフレームの干渉であれば原因も分かりやすいのだが、そうでない場合でも、レイヤーを一つ足しただけで途端におかしくなることが度々あった（当然、Write Defaults設定をオフにしていても発生する）。</p>

    <p>細かい発生条件もよくわからないため、「とにかくレイヤーを多用しない」という消極的な選択肢しかとれず、結果として表現の幅を狭めてしまっていた。</p>

    <p><br /></p>
  </li>
  <li>
    <p><strong>② C#スクリプト（LateUpdate等）による直接制御の限界</strong></p>

    <p>プログラムから直接パラメータの値を操作する方法もある。</p>

    <p>しかし、この方法にも限界はある。Live2Dの場合、「汗」のパーツをゆっくり、少しずつ垂らすような単純な処理ならやりやすいが、以下のような複雑な要件になると途端にハードルが上がる。</p>

    <ul>
      <li>「腰の動き」パラメータが一定値以下なら「浮かせる」モーション（大小2パターン）を再生する。</li>
      <li>逆に一定値以上の場合は「カクっと下げる」モーションを再生する。</li>
      <li>毎回全く同じ動きにならないよう、再生のたびにパラメータの変動幅やタイミングを変化させる。</li>
      <li>モーションに合わせて、フォロースルーやオーバーシュートの役割を担う二次的なパラメータを少し遅らせて連動させる。</li>
    </ul>

    <p><br /></p>

    <p>「仕様」が複雑でも、事前に計画していた通りに作るだけなら不可能ではないのだが、C#スクリプトで書いてしまうと「調整」が極めて難しい。</p>

    <p>理由は大きく分けて2つある。</p>

    <ol>
      <li><strong>コード上のどこをいじるとどうなるかが予測しづらい:</strong>
        <ul>
          <li>どの処理がどのパラメータを操作しているのか追いづらく、「コードの数値をいじったら結果的にモーションがどうなるか」が予測しにくい。</li>
        </ul>
      </li>
      <li><strong>時系列データを扱う難しさ:</strong>
        <ul>
          <li>カーブの加減速（イージング）やタイミングをC#で直接書こうとすると、経過時間の管理や補間処理などでコードがやたらと肥大化しがちになる。</li>
          <li>加えて、「このタイミングからカーブの角度を急にしたい」といった視覚的な微調整をしたい場合にも、いちいち計算式や数値を書き換える必要があり、柔軟性がない。</li>
        </ul>
      </li>
    </ol>
  </li>
</ul>

<p><br /></p>

<p><strong>まとめ</strong></p>

<p>以上をまとめると、既存の手法には以下の課題があった。</p>

<ol>
  <li>動的な条件分岐を噛ませつつ、複数の箇所（パラメータ）を時系列で「細かく」動かす実用的な方法がない。</li>
  <li>あるロジックに従ってモーションを再生する要件は実装できても、その後の微調整が難しすぎる。</li>
</ol>

<p><br /></p>

<p>これらを解決し、ロジックの構築と直感的なモーション調整を両立させるために、今回のコントローラーが必要だったという経緯になる。</p>

<h3 id="3-実際に作ったもの">3. 実際に作ったもの</h3>

<h4 id="3-1-ノードベースエディタ">3-1. ノードベースエディタ</h4>

<p>Unityのエディタ拡張を用いて、ノードベースでロジックを組める仕組みを構築した。</p>

<p>順次実行・並列実行・条件分岐（IF）・反復処理（While）などを視覚的に組み立てることができ、作成したノードツリーはScriptable Objectとして保存できる。</p>

<p>かなり簡易的なものではあるが、ビジュアルスクリプティング環境として機能している。</p>

<p class="caption-text">▼実際に組んだノードの例。順次実行（SequenceTask）に加え、「Live2Dパラメータの現在の値」による分岐や、コンテキスト変数の値による分岐ノードなどを配置している。
<img src="/assets/img/20260628/3-1a.png" alt="fig3-1a.png" /></p>

<p><br /></p>

<p>この「ノードツリー」アセットは、ロジック本体だけでなく「コンテキスト変数」と「汎用キー」という2つの情報を保持している。</p>

<ul>
  <li><strong>コンテキスト変数:</strong>
    <ul>
      <li>ツリーの実行を制御するための変数として機能する。ツリー内部・および外部のスクリプトからこの変数を操作することで、モーションの再生などをトリガーする。</li>
    </ul>
  </li>
  <li><strong>汎用キー:</strong>
    <ul>
      <li>ロジック内で扱うパラメータの「仮の名前（ただの文字列）」として扱われる。</li>
    </ul>
  </li>
</ul>

<p class="caption-text">▼コンテキスト変数の例<br />
<img src="/assets/img/20260628/3-1b.png" alt="fig3-1b.png" /></p>

<p class="caption-text">▼汎用キーの例<br />
<img src="/assets/img/20260628/3-1c.png" alt="fig3-1c.png" /></p>

<p><br /></p>

<p>そして、この「汎用キー（仮の名前）」と「実際のLive2Dパラメータ」を紐づける役割として、もう一つのコア要素となる「マッピング」アセットを用意した。このアセットが実行時（MonoBehaviour側）に直接参照される。</p>

<p>マッピングアセットは、コンテキスト変数の初期値を持たせることや、モーションの優先度を設定することもできる。</p>

<p class="caption-text">▼「マッピング」アセットの例。「Parameter Mapping」内で汎用キーとLive2Dパラメータを紐づけている。<br />
<img src="/assets/img/20260628/3-1d.png" alt="fig3-1d.png" /></p>

<p><br />
<br /></p>

<p>なぜわざわざ「ノードツリー」と「マッピング」を分けたのかというと、モーションデータの抽象化（流用）をしたかったというのが主な理由として挙げられる。</p>

<p>例えば、「左足の汗」と「右足の汗」という別々のパラメータがあったとする。これらが「似たような速度で動く」という同じロジックで動くのであれば、マッピングの参照先を変えるだけで同じノードツリーを使い回せる。わざわざノードツリーごと新規作成（コピペ）すると保守性が下がる。</p>

<p>（ノードツリー上では、汎用キーは単なる文字列として扱われているため、将来的にはLive2Dのパラメータ以外ともマッピングできる拡張性を持たせている。この点については後述する。）</p>

<p>ただ、この設計のトレードオフとして、特定のモデル専用・特定のパラメータ専用の「一点もの」のモーションを作る場合であっても、必ず「ノードツリー」と「マッピング」の両方のアセットを作らなければならないため、一長一短な部分はあった。</p>

<p><br /></p>

<h4 id="3-2-アニメーションカーブ">3-2. アニメーションカーブ</h4>

<p>時系列データを直感的に扱うため、「アニメーションカーブ」アセットもUnityエディタ上で作成できるようにした。</p>

<p class="caption-text">▼「アニメーションカーブ」作成画面
<img src="/assets/img/20260628/3-2a.gif" alt="fig3-2a.png" /></p>

<p><br /></p>

<p>また、このエディタではモーションの「ランダマイズ」機能を実装している。</p>

<p><strong>全く同じ</strong>動きを繰り返すループモーションは、見ていると目が慣れてしまい機械的な印象を与えてしまう（個人的な見解）。これを避けるため、同じモーションでも再生のたびにパラメータの変動量やモーションの長さを若干変える、という手法をよく用いている。</p>

<p>これをランタイムでも実現したかった（というよりも、する必要があった）。つまり、プレイヤーの入力によってゲーム内でモーションが再生される際、「毎回少しずつ違うモーション」として再生したいという要件があった。この仕組みを実装するため、<strong>キーフレームの概念を若干拡張している。</strong></p>

<p>Live2Dエディタをはじめとする一般的なアニメーションソフトのキーフレームは「時間も数値も固定的」となっているが、今回のシステムでは「前のキーフレームからの経過時間と差異」を基準にした、浮動的なキーフレーム（値に幅を持たせたキーフレーム）を設定できるようにしている。</p>

<p class="caption-text">▼「ランダマイズ」に対応しているアニメーションカーブの例。ランダマイズ係数が0の時は基準となる曲線を、係数が1の時は一番離れている曲線を辿る。
<img src="/assets/img/20260628/3-2b.png" alt="fig3-2b.png" /></p>

<p><br /></p>

<h4 id="3-3-その他検索窓フィールドによる複雑性の吸収">3-3. その他：検索窓フィールドによる複雑性の吸収</h4>

<p>システムの大枠とは直接関係ないが、実装して非常に開発体験が良かった機能として「検索窓フィールド」が挙げられる。</p>

<p class="caption-text">▼「検索窓フィールド」の例。ここでは「対象となるLive2Dモデルが持つ全てのパラメータID」だけが入力可能な候補となる。
<img src="/assets/img/20260628/3-3a.png" alt="fig3-3a.png" /></p>

<p class="caption-text">▼実際に文字を打ち込んでいくと候補が絞られていく。
<img src="/assets/img/20260628/3-3b.gif" alt="fig3-3b.gif" /></p>

<p><br /></p>

<p>UI上は単なる文字列の指定に見えるが、内部でシリアライズできる機能として作っている。</p>

<p>ノードツリー内の変数や「汎用キー」、あるいは「プロジェクト内に含まれるLive2Dモデルの一覧」や「そのモデルに含まれるパラメータの一覧」を候補としてリストアップし、インクリメンタルサーチで絞り込んで指定できる。</p>

<p>今回のシステムは、オブジェクト指向的に設計したことで柔軟性が高まった反面、扱うパーツ（特に汎用キーなど）が増えて複雑性が増してしまった部分もあった。さらに言えば、そもそもLive2Dモデル自体もパラメータが膨大になりやすい。</p>

<p>この検索窓フィールドがあるおかげで、パラメータ名を一々暗記する必要がなくなり、タイポによるバグも完全に防げるようになったため、システムの複雑さを上手く吸収してくれたと思う。</p>

<h3 id="4-問題点課題が多い所">4. 問題点・課題が多い所</h3>

<p>ノードベースエディタには課題が多く残っている。</p>

<ul>
  <li>そもそも「ロジックをデータ化して視覚的に組み立てる」というのは、簡易的とはいえプログラミング特有の複雑さをエディタ上に持ち込んでいる状態だとも言える。そのため、少し踏み込んだことをしようとすると、様々な問題に直面することになった。
    <ul>
      <li>例えば、「別のノードツリーを呼び出して処理をさせる（プログラミングでいうメソッド化）」という機能を実装してみたものの、変数の受け渡し（コンテキストの解決）周りが上手く動作していない。</li>
      <li>また、特定のノードを組み合わせるとツリー全体の挙動が破綻する現象も起きたが、その原因を特定することが非常に困難だった。</li>
    </ul>
  </li>
  <li>こうした問題の背景にある最大の課題は、現在のシステムの「デバッグ機能」が貧弱すぎることにある。</li>
  <li>抽象度の高いコードを書くと処理の流れを追うのが難しくなり、結果的に自動テストなどの仕組みが不可欠になっていく……という、ソフトウェア開発ではよくある問題にそのままぶつかっている状態と言える。まずはこのデバッグ周りから改善していく必要があると感じている。</li>
</ul>

<h3 id="5-今後の展望">5. 今後の展望</h3>

<ul>
  <li><strong>ロジックとデバッグ機能の強化</strong>
    <ul>
      <li>前述の通り、（開発を続けるのであれば）まずはバグの特定と修正を容易にするための仕組みを整えたい。</li>
    </ul>
  </li>
  <li><strong>データの扱いの強化とフローの効率化</strong>
    <ul>
      <li>ノードのテンプレート機能を追加したり、Live2Dのmotion3.jsonファイルをそのまま受け取れるようにしたりするなど。</li>
    </ul>
  </li>
  <li><strong>Live2Dモデルに関する応用</strong>
    <ul>
      <li>プレスコ（音声を先行収録する手法）で制作された音声の受け皿として。口パクの話ではなく、音声ファイルを起点として、ループモーション全体をある程度動的に生成するといったアプローチを考えている。
  今回、キーフレームの概念を拡張したことで、ランタイムでも動きを柔軟にコントロールできる仕組みが整った。これを利用すれば、音声をトリガーとして動きに揺らぎを持たせたり、他のパーツを連動させたりと、より自然なループモーションをリアルタイムに生成できる見込みがある。</li>
      <li>また、この仕組みの大きな利点は、音声ファイルの長さ（例えば30秒間）に合わせて固定的なキーフレームが延々と打ち込まれているわけではないという点にある。モーションの調整が必要になった際も修正箇所は限定的になるので、データとしての保守性もかなり高くなる（と思う）。</li>
    </ul>
  </li>
  <li><strong>Live2D以外への応用</strong>
    <ul>
      <li>UIアニメーション</li>
      <li>チュートリアル</li>
      <li>（完全に思いつきで書いているが）SE〇IRO風のアクションゲームのようなものにも使えるかもしれない。今回のシステムは「モーションデータ」と「ロジック」が非常に近い位置にあるため、「特定のアクションから別の技へ派生させる」「攻撃モーションが途中でキャンセルされたら、専用の怯みモーションに繋ぐ」といった複雑な処理がやりやすい……ような気がしている（本当に機能するかは未知数）。</li>
      <li>現在のシステムはLive2Dのパラメータ（単一の数値）を操作する前提の作りになっている。ここを拡張し、3Dモデルのボーン（座標や回転といった多次元のデータ）に紐づける仕組みや、それに対応したアニメーションカーブのデータ構造さえ用意できれば、理論上は3Dモデルの制御にも流用できるのではないかと考えている。
        <ul>
          <li>*余談だがUEのAnimBPなるものの存在をこの記事の執筆中に知った。3Dモデルの制御はわざわざ自作しなくてもいいかもしれない</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h3 id="6-最後に">6. 最後に</h3>

<ul>
  <li>
    <p>「アニメーションを作ること」と「それをプログラム上で再生・制御すること」の間には、相対的に未開拓の領域が広がっているらしい（開発中に使用していたAIがそう言っていた。真偽のほどは不明）。これがいわゆるブルーオーシャンなのか、単純に「魚がいない（＝誰もそんなことを気にしていない・いたとしてもかなり少数）」だけなのかは分からないが、おそらく後者に近いのではないかと思っている。
とはいえ、個人的には面白い分野ではあった。高く評価してくれる人もいたので、今後も開発は続ける予定ではいる。</p>
  </li>
  <li>
    <p>今回はAI駆動開発（厳密にはAIにコードを書かせて人間が確認する「レビュー駆動開発」）の手法を取り入れたが、なんだかんだで1ヶ月強もかかってしまった。しかし、システムの規模や「実際にある程度動く」という水準まで持っていけたことを考えると、少し早すぎるペースだと感じる。もしこれを人力で一からやっていたら、Unityのエディタ拡張の文法を調べるところから始まり、半年から1年、あるいはそれ以上の時間を要していてもおかしくはない。</p>
  </li>
  <li>
    <p>余談になるが、今回の開発の根底には「プレイヤーがそのように操作する<strong>から</strong> 、そういう反応が返ってくる」という、「プレイできるアニメ」としての水準を達成したいという目標があった。「頭をなでるから、鬱陶しがられる」といったような、インタラクションにおける説得力を持たせるための試みでもあったが、今回のシステムだけでそれを十分に表現しきれたとは言えない。結局のところ演出面も大きく関わってくる部分なので、技術的なシステムだけで解決するのはなかなか難しいというのが率直な印象として残った。</p>
  </li>
</ul>]]></content><author><name></name></author><category term="Live2D" /><category term="Unity" /><category term="Animation" /><summary type="html"><![CDATA[1. 概要]]></summary></entry><entry><title type="html">【Unity×Live2D】複数モデル配置時にクリッピング設定したパーツが消える</title><link href="https://danayagi.github.io/live2d/rpa/2026/05/15/unity-live2d-clipping-mask.html" rel="alternate" type="text/html" title="【Unity×Live2D】複数モデル配置時にクリッピング設定したパーツが消える" /><published>2026-05-15T00:00:00+09:00</published><updated>2026-05-15T00:00:00+09:00</updated><id>https://danayagi.github.io/live2d/rpa/2026/05/15/unity-live2d-clipping-mask</id><content type="html" xml:base="https://danayagi.github.io/live2d/rpa/2026/05/15/unity-live2d-clipping-mask.html"><![CDATA[<h3 id="1-概要発生した問題">1. 概要・発生した問題</h3>

<p>UnityでLive2D（Cubism SDK for Unity）を使用している際、1つのシーン内に複数のLive2Dモデルを配置すると、<strong>クリッピング設定をしているパーツだけが表示されなくなる現象</strong>に遭遇した。<br />
特定のモデル単体では問題なく表示されるものの、複数のモデルを同時にシーンに存在させた場合にのみ発生する。</p>

<ul>
  <li><strong>【動作環境】</strong>
    <ul>
      <li>Unity 2022.3.62f3</li>
      <li>Live2D SDKのバージョン: 5-r.4 (2025/5/9)</li>
    </ul>
  </li>
</ul>

<h3 id="2-原因globalmasktextureのマスク上限数">2. 原因：GlobalMaskTextureの「マスク上限数」</h3>

<p>原因は、Cubism SDK for Unityの仕様による <strong>「クリッピングマスクの使用上限数」の超過</strong> だった。
<br />
<br />
<br /></p>

<ul>
  <li>2-1. Unity版のLive2D SDKでは、デフォルト設定の場合、シーン内の <strong>すべてのモデルのクリッピングマスクを1枚のテクスチャ（GlobalMaskTexture）で共有して処理</strong> している。</li>
</ul>

<p class="caption-text">▼シーン内に配置したLive2Dモデルを選択した際のインスペクタ。「GlobalMaskTexture」を選択すると、アセットとして存在するGlobalMaskTextureを表示できる。<br />
<img src="/assets/img/20260515/fig1-1.png" alt="fig1-1.png" /></p>

<p class="caption-text">▼GlobalMaskTexture（アセット）は「Live2D\Cubism\Rendering\Resources\Live2D\Cubism」内にある。<br />
<img src="/assets/img/20260515/fig1-2.png" alt="fig1-2.png" /></p>

<p class="caption-text">▼GlobalMaskTextureのインスペクタ。後述するが「RenderTextureCount」の数値を調整するとマスクテクスチャの枚数が増える（※Cubism SDK for Unity R7以降）。<br />
<img src="/assets/img/20260515/fig1-3.png" alt="fig1-3.png" /></p>

<p><br />
<br /></p>
<ul>
  <li>2-2. この共用テクスチャ1枚につき、同時に扱えるマスクの数（≒クリッピング設定したパーツの数）は、デフォルトで「64個」に制限されている。</li>
</ul>

<p class="caption-text">▼Live2D公式SDKマニュアルより引用（ <a href="https://docs.live2d.com/cubism-sdk-manual/cubismsdk-compare/">SDK機能比較</a> ）
<br />
<img src="/assets/img/20260515/fig1-4.png" alt="fig1-4.png" /></p>

<p><br />
<br /></p>
<ul>
  <li>2-3. つまり、1つのシーン内に存在するLive2Dモデルのクリッピングパーツの合計が64個を超えると、超過した分のパーツに割り当てる領域がなくなり、結果として「クリッピング」設定のあるパーツが表示されなくなる（落ち影が消えるなど）。</li>
</ul>

<blockquote>
  <p>マスクの上限に達した場合は実行時の登録順の遅いほうから、全面にマスクが掛かり表示が消えます。</p>
</blockquote>

<ul>
  <li>出典: <a href="https://docs.live2d.com/cubism-sdk-manual/cubismsdk-compare/">SDK機能比較 - Live2D Manuals &amp; Tutorials</a></li>
</ul>

<p><br />
<br />
<br /></p>

<h4 id="注意点非表示setactivefalseにしても枠は解放されない">注意点：非表示（SetActive(false)）にしても枠は解放されない</h4>

<p>注意点として、<strong>モデルを非表示（<code class="language-plaintext highlighter-rouge">SetActive(false)</code>）にするだけでは、マスクの枠は解放されない</strong>。
デフォルトでは <code class="language-plaintext highlighter-rouge">CubismMaskController</code> の <code class="language-plaintext highlighter-rouge">Start()</code> で登録され、<code class="language-plaintext highlighter-rouge">OnDestroy()</code> で解放される仕様になっている。下記コード内の <code class="language-plaintext highlighter-rouge">AddSource</code> メソッドと <code class="language-plaintext highlighter-rouge">RemoveSource</code> メソッドがそれぞれ対応している。</p>

<p>（<code class="language-plaintext highlighter-rouge">CubismMaskController.cs</code> の該当箇所。<code class="language-plaintext highlighter-rouge">Live2D\Cubism\Rendering\Masking</code> 内に存在する）</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">/// &lt;summary&gt;</span>
<span class="c1">/// Initializes instance.</span>
<span class="c1">/// &lt;/summary&gt;</span>
<span class="k">private</span> <span class="k">void</span> <span class="nf">Start</span><span class="p">()</span>
<span class="p">{</span>
    <span class="c1">// Fail silently.</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">MaskTexture</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="n">MaskTexture</span><span class="p">.</span><span class="nf">AddSource</span><span class="p">(</span><span class="k">this</span><span class="p">);</span>

    <span class="c1">// Get cubism update controller.</span>
    <span class="n">HasUpdateController</span> <span class="p">=</span> <span class="p">(</span><span class="n">GetComponent</span><span class="p">&lt;</span><span class="n">CubismUpdateController</span><span class="p">&gt;()</span> <span class="p">!=</span> <span class="k">null</span><span class="p">);</span>
<span class="p">}</span>

<span class="p">~~~</span>

<span class="c1">/// &lt;summary&gt;</span>
<span class="c1">/// Finalizes instance.</span>
<span class="c1">/// &lt;/summary&gt;</span>
<span class="k">private</span> <span class="k">void</span> <span class="nf">OnDestroy</span><span class="p">()</span>
<span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">MaskTexture</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="n">MaskTexture</span><span class="p">.</span><span class="nf">RemoveSource</span><span class="p">(</span><span class="k">this</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>そのため、「現在は画面に映っていない待機中のモデル」であっても、シーン内に存在（Start済み）しているだけでマスクの枠を消費し続けてしまう。</p>

<h3 id="3-解決策">3. 解決策</h3>

<h4 id="3-1-マスク用テクスチャの枚数を増やすpc向け">3-1. マスク用テクスチャの枚数を増やす（PC向け）</h4>

<p>最も手っ取り早く、かつシームレスな遷移を妨げない方法として、<strong>GlobalMaskTextureが内部で保持するテクスチャの枚数を増やす</strong>ことで、マスクの上限を上げる方法がある。</p>

<ul>
  <li><strong>手順</strong>
    <ol>
      <li>UnityのProjectウィンドウで、以下の場所にある <code class="language-plaintext highlighter-rouge">GlobalMaskTexture</code> アセットを選択する。</li>
      <li>Inspectorウィンドウから、<strong><code class="language-plaintext highlighter-rouge">RenderTextureCount</code></strong> の値をデフォルトの <code class="language-plaintext highlighter-rouge">0</code>（または <code class="language-plaintext highlighter-rouge">1</code>）から、<strong><code class="language-plaintext highlighter-rouge">2</code> や <code class="language-plaintext highlighter-rouge">3</code> など任意の数に増やす</strong>。</li>
    </ol>
  </li>
</ul>

<p><img src="/assets/img/20260515/fig1-3.png" alt="fig1-3.png" /></p>

<p>これだけで扱えるマスクの上限数は増加する（ベースが64個なので、2枚にすれば128個、3枚なら192個まで対応可能）。</p>

<p><br />
<br /></p>

<p>※ GlobalMaskTextureのデフォルトサイズは <code class="language-plaintext highlighter-rouge">1024x1024</code> （ピクセル）。<br />
Live2DのマスクはRGBAの4チャンネルを使用するため、<strong>1枚あたりのVRAM消費量は約4MB</strong>（1024 × 1024 × 4バイト）となる。PC向けのゲームであればVRAM消費は微小なため、基本的に問題にはならない。<br /></p>

<p><br />
<br /></p>

<h4 id="3-2-明示的にremovesourceメソッドを呼び出す">3-2. 明示的にRemoveSourceメソッドを呼び出す</h4>

<p>どのタイミングでどのLive2Dモデルを表示・非表示にするか明確に管理できている場合、明示的に <code class="language-plaintext highlighter-rouge">RemoveSource</code> メソッドを呼び出してマスク枠を解放することも可能。<br /></p>

<p><code class="language-plaintext highlighter-rouge">CubismMaskController</code> の <code class="language-plaintext highlighter-rouge">MaskTexture</code> は外部からアクセス可能なうえ、シーン内に配置されているLive2Dオブジェクトは <code class="language-plaintext highlighter-rouge">CubismMaskController</code> をコンポーネントとして保持している。<br />
そのため、「対象のLive2Dオブジェクトの」<code class="language-plaintext highlighter-rouge">CubismMaskController</code> への参照があれば、</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">maskController</span><span class="p">.</span><span class="n">MaskTexture</span><span class="p">.</span><span class="nf">RemoveSource</span><span class="p">(</span><span class="n">maskController</span><span class="p">);</span>
</code></pre></div></div>

<p>とするだけで、そのオブジェクトの分だけ明示的にマスクを解放できる。</p>

<h3 id="4-その他の解決策公式マニュアルより">4. その他の解決策（公式マニュアルより）</h3>

<p>公式マニュアルには以下の手法も紹介されている。</p>

<ol>
  <li>共用のマスクテクスチャを使うのではなく、モデルごとにマスク用テクスチャを設定する手法</li>
  <li>OnEnable / OnDisable のタイミングでマスク枠を解放するようにスクリプト（<code class="language-plaintext highlighter-rouge">CubismMaskController.cs</code>）を書き換える手法</li>
</ol>

<ul>
  <li>参考: <a href="https://docs.live2d.com/cubism-sdk-manual/maximum-number-of-masks-used/">SDK for Unityにおけるマスクの使用上限について - Live2D Manuals &amp; Tutorials</a></li>
</ul>

<h3 id="5-まとめ">5. まとめ</h3>

<ul>
  <li>1つのシーンに大量のLive2Dモデルを配置した際、<strong>クリッピングパーツだけ</strong>が表示されなくなったら、クリッピングマスクの上限（デフォルト64個）を疑う。</li>
  <li>もっとも簡単な解決策は、<code class="language-plaintext highlighter-rouge">GlobalMaskTexture</code> の <code class="language-plaintext highlighter-rouge">RenderTextureCount</code> を増やすこと。
    <ul>
      <li>PC向けならVRAM消費（1枚約4MB）は気にならないレベルであるため、テクスチャを増やして解決するのが安全かつシームレス。</li>
    </ul>
  </li>
  <li>非表示（<code class="language-plaintext highlighter-rouge">SetActive(false)</code>）にしただけではマスク枠は解放されない点に注意が必要。</li>
</ul>]]></content><author><name></name></author><category term="Live2D" /><category term="RPA" /><summary type="html"><![CDATA[1. 概要・発生した問題]]></summary></entry><entry><title type="html">OAuth不要でGoogleスプレッドシートのデータをUnityエディタに直接ダウンロードする</title><link href="https://danayagi.github.io/unity/ci/cd/2026/02/25/UnitySpreadSheetDL.html" rel="alternate" type="text/html" title="OAuth不要でGoogleスプレッドシートのデータをUnityエディタに直接ダウンロードする" /><published>2026-02-25T00:00:00+09:00</published><updated>2026-02-25T00:00:00+09:00</updated><id>https://danayagi.github.io/unity/ci/cd/2026/02/25/UnitySpreadSheetDL</id><content type="html" xml:base="https://danayagi.github.io/unity/ci/cd/2026/02/25/UnitySpreadSheetDL.html"><![CDATA[<h3 id="1-はじめに">1. はじめに</h3>

<ul>
  <li><strong>【動作環境】</strong>
    <ul>
      <li><u>Unity 2022.3.62f3</u></li>
    </ul>
  </li>
  <li><strong>やったこと:</strong>
    <ul>
      <li>Unityエディタのメニューからボタン一つで、Googleスプレッドシートのデータを直接ダウンロードできるエディタ拡張を実装した。</li>
    </ul>
  </li>
  <li><strong>仕組み:</strong>
    <ul>
      <li>スプレッドシートのアクセス権を「リンクを知っている全員（閲覧者）」に設定し、エクスポート用のURL（<code class="language-plaintext highlighter-rouge">/export?format=tsv</code>）を生成する。</li>
      <li>それをUnity側から <code class="language-plaintext highlighter-rouge">UnityWebRequest</code> を用いて叩くことで、複雑なOAuth認証などを省略し、データを取得している。</li>
    </ul>
  </li>
  <li><strong>記事の趣旨（スコープ）:</strong>
    <ul>
      <li>本記事では、実際のコード例を交えながら「データのダウンロード部分」に焦点を絞って解説する。取得したデータをUnity内でどう展開・パースするかについては、プロジェクトに大きく依存するため今回は割愛する。</li>
    </ul>
  </li>
</ul>

<h3 id="2-手順">2. 手順</h3>

<ul>
  <li><strong>1. スプレッドシート側の設定</strong>
    <ul>
      <li><strong>1-1. 共有設定の変更</strong>
        <ul>
          <li>画面右上の「共有」ボタンから、一般的なアクセスを <strong>「リンクを知っている全員」（閲覧者）</strong> に変更する。これにより、OAuth認証などの複雑なプログラムを省略し、直接データにアクセスできるようになる。</li>
        </ul>
      </li>
      <li><strong>1-2. 必要な2つのIDの取得</strong>
        <ul>
          <li>
            <p>プログラム側がシートにアクセスできるように、スプレッドシートのURLから以下の2カ所をコピーしておく必要がある。</p>

            <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  https://docs.google.com/spreadsheets/d/【①Spreadsheet ID】/edit#gid=【②Gid（シートID）】
</code></pre></div>            </div>
          </li>
          <li>①スプレッドシートのID</li>
          <li>②シートのID</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<p><br /></p>

<ul>
  <li>
    <p><strong>2. プログラム上の手順：設定情報の管理とURL生成</strong></p>

    <p>Unity側で各IDを保持し、ダウンロード用URLへ変換する手順。</p>

    <ul>
      <li><strong>2-1. 「シート設定」保存用のScriptableObject</strong>
        <ul>
          <li>
            <p>コード例</p>

            <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">using</span> <span class="nn">UnityEngine</span><span class="p">;</span>
  <span class="k">using</span> <span class="nn">System.Collections.Generic</span><span class="p">;</span>
            
  <span class="p">[</span><span class="nf">CreateAssetMenu</span><span class="p">(</span><span class="n">fileName</span> <span class="p">=</span> <span class="s">"ScenarioImportSettings"</span><span class="p">,</span> <span class="n">menuName</span> <span class="p">=</span> <span class="s">"Scenario /Scenario Import Settings"</span><span class="p">)]</span>
  <span class="k">public</span> <span class="k">class</span> <span class="nc">ScenarioImportSettings</span> <span class="p">:</span> <span class="n">ScriptableObject</span>
  <span class="p">{</span>
      <span class="p">[</span><span class="nf">Header</span><span class="p">(</span><span class="s">"Google Sheets Settings"</span><span class="p">)]</span>
      <span class="k">public</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">SheetDefinition</span><span class="p">&gt;</span> <span class="n">Sheets</span> <span class="p">=</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">SheetDefinition</span><span class="p">&gt;();</span>
            
      <span class="p">[</span><span class="n">System</span><span class="p">.</span><span class="n">Serializable</span><span class="p">]</span>
      <span class="k">public</span> <span class="k">class</span> <span class="nc">SheetDefinition</span>
      <span class="p">{</span>
          <span class="p">[</span><span class="nf">Tooltip</span><span class="p">(</span><span class="s">"ログ表示や仮想ファイル名として使用する識別名（例: MainData）"</span><span class="p">)]</span>
          <span class="k">public</span> <span class="kt">string</span> <span class="n">SheetName</span><span class="p">;</span>
                    
          <span class="p">[</span><span class="nf">Tooltip</span><span class="p">(</span><span class="s">"スプレッドシートのURLに含まれるID部分 (/d/xxxxx/のxxxxx)"</span><span class="p">)]</span>
          <span class="k">public</span> <span class="kt">string</span> <span class="n">SpreadsheetId</span><span class="p">;</span>
                    
          <span class="p">[</span><span class="nf">Tooltip</span><span class="p">(</span><span class="s">"シート(タブ)固有のID (URL末尾の gid=yyyyy のyyyyy)"</span><span class="p">)]</span>
          <span class="k">public</span> <span class="kt">string</span> <span class="n">Gid</span><span class="p">;</span>
                    
          <span class="p">[</span><span class="nf">Tooltip</span><span class="p">(</span><span class="s">"チェックを外すとインポート対象から除外されます"</span><span class="p">)]</span>
          <span class="k">public</span> <span class="kt">bool</span> <span class="n">IsEnabled</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span>
            
          <span class="c1">// ダウンロード用URLを生成</span>
          <span class="k">public</span> <span class="kt">string</span> <span class="nf">GetDownloadUrl</span><span class="p">()</span>
          <span class="p">{</span>
              <span class="k">if</span> <span class="p">(</span><span class="kt">string</span><span class="p">.</span><span class="nf">IsNullOrEmpty</span><span class="p">(</span><span class="n">SpreadsheetId</span><span class="p">)</span> <span class="p">||</span> <span class="kt">string</span><span class="p">.</span><span class="nf">IsNullOrEmpty</span><span class="p">(</span><span class="n">Gid</span><span class="p">))</span> <span class="k">return</span> <span class="s">""</span><span class="p">;</span>
              <span class="k">return</span> <span class="s">$"https://docs.google.com/spreadsheets/d/</span><span class="p">{</span><span class="n">SpreadsheetId</span><span class="p">}</span><span class="s">/export?format=tsv&amp;gid=</span><span class="p">{</span><span class="n">Gid</span><span class="p">}</span><span class="s">"</span><span class="p">;</span>
          <span class="p">}</span>
      <span class="p">}</span>
  <span class="p">}</span>
</code></pre></div>            </div>

            <ul>
              <li><strong>役割と仕組み</strong>: スプレッドシートごとの情報をScriptableObjectで管理する設計。</li>
              <li><strong>解説</strong>: 単なるスプレッドシートのURLではなく、エクスポート用のURLを組み立てているのがポイント。URLに <code class="language-plaintext highlighter-rouge">/export?format=tsv</code> を付与することで、Google Sheets APIを使わずとも、直接TSV（またはCSV）形式のテキストデータとしてダウンロード可能になる。</li>
            </ul>

            <p class="caption-text">▼scriptable objectの例。シナリオインポート設定で「本編」と「おまけ」の2シート分のデータがある（それぞれスプレッドシートのリンクから対応するIDを入力した状態）<br />
  <img src="/assets/img/20260225/2-1.png" alt="scriptable objectの例" class="figure-image" /></p>

            <p><br /></p>
          </li>
          <li>
            <p><strong>2-2. 「シート設定」の検索メソッド</strong></p>
            <ul>
              <li>コード例</li>
            </ul>

            <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">using</span> <span class="nn">UnityEngine</span><span class="p">;</span>
  <span class="k">using</span> <span class="nn">UnityEditor</span><span class="p">;</span> <span class="c1">// AssetDatabase に必要</span>
            
  <span class="c1">// 中略</span>
            
  <span class="k">private</span> <span class="k">static</span> <span class="n">ScenarioImportSettings</span> <span class="nf">LoadSettings</span><span class="p">()</span>
  <span class="p">{</span>
      <span class="kt">string</span><span class="p">[]</span> <span class="n">guids</span> <span class="p">=</span> <span class="n">AssetDatabase</span><span class="p">.</span><span class="nf">FindAssets</span><span class="p">(</span><span class="s">"t:ScenarioImportSettings"</span><span class="p">);</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">guids</span><span class="p">.</span><span class="n">Length</span> <span class="p">==</span> <span class="m">0</span><span class="p">)</span>
      <span class="p">{</span>
          <span class="n">Debug</span><span class="p">.</span><span class="nf">LogError</span><span class="p">(</span><span class="s">"Error: 'ScenarioImportSettings' asset not found in project. Please create one."</span><span class="p">);</span>
          <span class="k">return</span> <span class="k">null</span><span class="p">;</span>
      <span class="p">}</span>
            
      <span class="kt">string</span> <span class="n">path</span> <span class="p">=</span> <span class="n">AssetDatabase</span><span class="p">.</span><span class="nf">GUIDToAssetPath</span><span class="p">(</span><span class="n">guids</span><span class="p">[</span><span class="m">0</span><span class="p">]);</span>
      <span class="kt">var</span> <span class="n">settings</span> <span class="p">=</span> <span class="n">AssetDatabase</span><span class="p">.</span><span class="n">LoadAssetAtPath</span><span class="p">&lt;</span><span class="n">ScenarioImportSettings</span><span class="p">&gt;(</span><span class="n">path</span><span class="p">);</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">settings</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span>
      <span class="p">{</span>
          <span class="n">Debug</span><span class="p">.</span><span class="nf">LogError</span><span class="p">(</span><span class="s">$"Error: Failed to load settings at </span><span class="p">{</span><span class="n">path</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
      <span class="p">}</span>
      <span class="k">return</span> <span class="n">settings</span><span class="p">;</span>
  <span class="p">}</span>
</code></pre></div>            </div>

            <ul>
              <li><strong>役割と仕組み</strong>: エディタ上で設定ファイル（<code class="language-plaintext highlighter-rouge">ScenarioImportSettings</code>）を読み込む処理。</li>
              <li><strong>解説</strong>: <code class="language-plaintext highlighter-rouge">AssetDatabase.FindAssets("t:ScenarioImportSettings")</code> を使用している。パスを直接指定（ハードコード）せず、型（<code class="language-plaintext highlighter-rouge">t:</code>）で検索させることで、プロジェクト内のどこにアセットを配置しても自動でロードできる。この手の設定ファイルを複数作ることもあまりないのでこれで事足りる（と思う）。</li>
            </ul>

            <p><br />
  <br /></p>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li>
    <p><strong>3. プログラム上の手順：通信とデータ取得の全体フロー</strong></p>
    <ul>
      <li><strong>3-1. 全体の起点</strong>
        <ul>
          <li>コード例</li>
        </ul>

        <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">using</span> <span class="nn">UnityEngine</span><span class="p">;</span>
  <span class="k">using</span> <span class="nn">UnityEditor</span><span class="p">;</span> <span class="c1">// MenuItem, EditorUtility に必要</span>
  <span class="k">using</span> <span class="nn">System.Collections.Generic</span><span class="p">;</span> <span class="c1">// Dictionary に必要</span>
  <span class="k">using</span> <span class="nn">System.Linq</span><span class="p">;</span> <span class="c1">// IEnumerableの拡張メソッド(Where, ToListなど)に必要</span>
        
  <span class="c1">// 中略</span>
        
  <span class="p">[</span><span class="nf">MenuItem</span><span class="p">(</span><span class="s">"Tools/Import Scenario/From Google Sheets"</span><span class="p">)]</span>
  <span class="k">public</span> <span class="k">static</span> <span class="k">async</span> <span class="k">void</span> <span class="nf">ImportFromGoogleSheets</span><span class="p">()</span>
  <span class="p">{</span>
      <span class="k">if</span> <span class="p">(!</span><span class="n">EditorUtility</span><span class="p">.</span><span class="nf">DisplayDialog</span><span class="p">(</span><span class="s">"Import from Google Sheets"</span><span class="p">,</span> 
          <span class="s">"Are you sure you want to import scenario data from Google Sheets?\nThis will overwrite existing ScriptableObjects."</span><span class="p">,</span> 
          <span class="s">"Import"</span><span class="p">,</span> <span class="s">"Cancel"</span><span class="p">))</span>
      <span class="p">{</span>
          <span class="k">return</span><span class="p">;</span>
      <span class="p">}</span>
        
      <span class="n">Debug</span><span class="p">.</span><span class="nf">Log</span><span class="p">(</span><span class="s">"[TSVImporter] Import from Google Sheets started..."</span><span class="p">);</span>
        
      <span class="c1">// 1. 設定ファイルのロード</span>
      <span class="kt">var</span> <span class="n">settings</span> <span class="p">=</span> <span class="nf">LoadSettings</span><span class="p">();</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">settings</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
        
      <span class="c1">// 2. 有効なシートのリストアップ</span>
      <span class="kt">var</span> <span class="n">targetSheets</span> <span class="p">=</span> <span class="n">settings</span><span class="p">.</span><span class="n">Sheets</span><span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">s</span> <span class="p">=&gt;</span> <span class="n">s</span><span class="p">.</span><span class="n">IsEnabled</span><span class="p">).</span><span class="nf">ToList</span><span class="p">();</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">targetSheets</span><span class="p">.</span><span class="n">Count</span> <span class="p">==</span> <span class="m">0</span><span class="p">)</span>
      <span class="p">{</span>
          <span class="n">Debug</span><span class="p">.</span><span class="nf">LogWarning</span><span class="p">(</span><span class="s">"No enabled sheets found in settings."</span><span class="p">);</span>
          <span class="k">return</span><span class="p">;</span>
      <span class="p">}</span>
        
      <span class="c1">// 3. ダウンロード実行</span>
      <span class="kt">var</span> <span class="n">tsvContents</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;();</span>
      <span class="kt">bool</span> <span class="n">hasDownloadError</span> <span class="p">=</span> <span class="k">false</span><span class="p">;</span>
        
      <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">sheet</span> <span class="k">in</span> <span class="n">targetSheets</span><span class="p">)</span>
      <span class="p">{</span>
          <span class="kt">string</span> <span class="n">url</span> <span class="p">=</span> <span class="n">sheet</span><span class="p">.</span><span class="nf">GetDownloadUrl</span><span class="p">();</span>
          <span class="k">if</span> <span class="p">(</span><span class="kt">string</span><span class="p">.</span><span class="nf">IsNullOrEmpty</span><span class="p">(</span><span class="n">url</span><span class="p">))</span>
          <span class="p">{</span>
              <span class="n">Debug</span><span class="p">.</span><span class="nf">LogError</span><span class="p">(</span><span class="s">$"Invalid URL settings for sheet: </span><span class="p">{</span><span class="n">sheet</span><span class="p">.</span><span class="n">SheetName</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
              <span class="n">hasDownloadError</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span>
              <span class="k">continue</span><span class="p">;</span>
          <span class="p">}</span>
        
          <span class="n">Debug</span><span class="p">.</span><span class="nf">Log</span><span class="p">(</span><span class="s">$"Downloading: </span><span class="p">{</span><span class="n">sheet</span><span class="p">.</span><span class="n">SheetName</span><span class="p">}</span><span class="s">..."</span><span class="p">);</span>
          <span class="k">try</span>
          <span class="p">{</span>
              <span class="kt">string</span> <span class="n">content</span> <span class="p">=</span> <span class="k">await</span> <span class="nf">FetchText</span><span class="p">(</span><span class="n">url</span><span class="p">);</span>
                    
              <span class="c1">// 仮想的なファイル名としてシート名を使用（拡張子.tsvを付与）</span>
              <span class="kt">string</span> <span class="n">virtualFileName</span> <span class="p">=</span> <span class="s">$"</span><span class="p">{</span><span class="n">sheet</span><span class="p">.</span><span class="n">SheetName</span><span class="p">}</span><span class="s">.tsv"</span><span class="p">;</span>
                    
              <span class="k">if</span> <span class="p">(</span><span class="n">tsvContents</span><span class="p">.</span><span class="nf">ContainsKey</span><span class="p">(</span><span class="n">virtualFileName</span><span class="p">))</span>
              <span class="p">{</span>
                  <span class="n">Debug</span><span class="p">.</span><span class="nf">LogError</span><span class="p">(</span><span class="s">$"Duplicate SheetName detected: </span><span class="p">{</span><span class="n">sheet</span><span class="p">.</span><span class="n">SheetName</span><span class="p">}</span><span class="s">. Please rename in settings."</span><span class="p">);</span>
                  <span class="n">hasDownloadError</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span>
                  <span class="k">continue</span><span class="p">;</span>
              <span class="p">}</span>
        
              <span class="n">tsvContents</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">virtualFileName</span><span class="p">,</span> <span class="n">content</span><span class="p">);</span>
          <span class="p">}</span>
          <span class="k">catch</span> <span class="p">(</span><span class="n">System</span><span class="p">.</span><span class="n">Exception</span> <span class="n">e</span><span class="p">)</span>
          <span class="p">{</span>
              <span class="n">Debug</span><span class="p">.</span><span class="nf">LogError</span><span class="p">(</span><span class="s">$"Failed to download </span><span class="p">{</span><span class="n">sheet</span><span class="p">.</span><span class="n">SheetName</span><span class="p">}</span><span class="s">: </span><span class="p">{</span><span class="n">e</span><span class="p">.</span><span class="n">Message</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
              <span class="n">hasDownloadError</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span>
          <span class="p">}</span>
      <span class="p">}</span>
        
      <span class="k">if</span> <span class="p">(</span><span class="n">hasDownloadError</span><span class="p">)</span>
      <span class="p">{</span>
          <span class="n">Debug</span><span class="p">.</span><span class="nf">LogError</span><span class="p">(</span><span class="s">"Import aborted due to download errors."</span><span class="p">);</span>
          <span class="k">return</span><span class="p">;</span>
      <span class="p">}</span>
        
      <span class="c1">// 4. TSVデータの展開</span>
      <span class="nf">ProcessImport</span><span class="p">(</span><span class="n">tsvContents</span><span class="p">);</span>
  <span class="p">}</span>
</code></pre></div>        </div>

        <ul>
          <li><strong>役割と仕組み</strong>: <code class="language-plaintext highlighter-rouge">[MenuItem]</code> 属性により、Unityのメニューバーから呼び出されるインポート処理の起点。</li>
          <li><strong>解説</strong>: 処理は主に以下のフローで進行する。
            <ol>
              <li><code class="language-plaintext highlighter-rouge">LoadSettings</code> で設定ファイルを取得する。</li>
              <li>有効（<code class="language-plaintext highlighter-rouge">IsEnabled = true</code>）なシートのみをリストアップする。</li>
              <li><code class="language-plaintext highlighter-rouge">GetDownloadUrl</code> でエクスポートURLを生成し、<code class="language-plaintext highlighter-rouge">FetchText</code> で順番にダウンロードを実行する。</li>
              <li>仮想的なファイル名とダウンロードした生のテキスト（TSV文字列）をDictionaryに格納し、実際のパース処理（<code class="language-plaintext highlighter-rouge">ProcessImport</code>）へ渡す。</li>
            </ol>
          </li>
          <li>また、処理の冒頭で <code class="language-plaintext highlighter-rouge">EditorUtility.DisplayDialog</code> を表示し、誤操作によるデータの上書きを防ぐ設計にしている。</li>
        </ul>

        <p><br /></p>
      </li>
      <li><strong>3-2. FetchText()メソッド （非同期通信処理）</strong>
        <ul>
          <li>コード例</li>
        </ul>

        <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">using</span> <span class="nn">UnityEngine.Networking</span><span class="p">;</span>
  <span class="k">using</span> <span class="nn">System.Threading.Tasks</span><span class="p">;</span>
        
  <span class="c1">// 中略</span>
        
  <span class="k">private</span> <span class="k">static</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="nf">FetchText</span><span class="p">(</span><span class="kt">string</span> <span class="n">url</span><span class="p">)</span>
  <span class="p">{</span>
      <span class="k">using</span> <span class="p">(</span><span class="kt">var</span> <span class="n">request</span> <span class="p">=</span> <span class="n">UnityWebRequest</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="n">url</span><span class="p">))</span>
      <span class="p">{</span>
          <span class="kt">var</span> <span class="n">operation</span> <span class="p">=</span> <span class="n">request</span><span class="p">.</span><span class="nf">SendWebRequest</span><span class="p">();</span>
          <span class="k">while</span> <span class="p">(!</span><span class="n">operation</span><span class="p">.</span><span class="n">isDone</span><span class="p">)</span> <span class="k">await</span> <span class="n">Task</span><span class="p">.</span><span class="nf">Yield</span><span class="p">();</span>
        
          <span class="k">if</span> <span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="n">result</span> <span class="p">!=</span> <span class="n">UnityWebRequest</span><span class="p">.</span><span class="n">Result</span><span class="p">.</span><span class="n">Success</span><span class="p">)</span>
          <span class="p">{</span>
              <span class="k">throw</span> <span class="k">new</span> <span class="n">System</span><span class="p">.</span><span class="nf">Exception</span><span class="p">(</span><span class="s">$"Request Error: </span><span class="p">{</span><span class="n">request</span><span class="p">.</span><span class="n">error</span><span class="p">}</span><span class="s"> (Code: </span><span class="p">{</span><span class="n">request</span><span class="p">.</span><span class="n">responseCode</span><span class="p">}</span><span class="s">)"</span><span class="p">);</span>
          <span class="p">}</span>
          <span class="k">return</span> <span class="n">request</span><span class="p">.</span><span class="n">downloadHandler</span><span class="p">.</span><span class="n">text</span><span class="p">;</span>
      <span class="p">}</span>
  <span class="p">}</span>
</code></pre></div>        </div>

        <ul>
          <li><strong>役割と仕組み</strong>: 指定されたURLに対し、<code class="language-plaintext highlighter-rouge">UnityWebRequest.Get</code> を用いてテキストデータをダウンロードする処理。</li>
          <li><strong>解説</strong>: HTTP通信の待機に <code class="language-plaintext highlighter-rouge">async / await</code> を導入している。エディタ拡張の処理中に <code class="language-plaintext highlighter-rouge">while (!operation.isDone) await Task.Yield();</code> を挟むことで、ダウンロード中もUnityエディタがフリーズしない（バックグラウンドで処理が進む）設計になっている。</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h3 id="3-おわりに">3. おわりに</h3>

<ul>
  <li>厳密なことを言うと、OAuth等を利用してAPIで認証した方がセキュアではある。しかしまあ、個人開発においてそこまで厳密なものを追求しなくても良いように思う。今回は手軽さを優先した。</li>
  <li>今回は「ダウンロード」する手順の解説のみだが、逆に「アップロード」も自動化が可能。 <strong>GitHub Actionsを利用することで、コミットする度に指定したシートの更新を行うこともできる。</strong> ただ、環境構築の手順が少し複雑になるため今回は割愛した。そのうちこの話も書くかもしれない。</li>
</ul>]]></content><author><name></name></author><category term="Unity" /><category term="CI/CD" /><summary type="html"><![CDATA[1. はじめに]]></summary></entry><entry><title type="html">外部リンク</title><link href="https://danayagi.github.io/links/2026/02/25/links.html" rel="alternate" type="text/html" title="外部リンク" /><published>2026-02-25T00:00:00+09:00</published><updated>2026-02-25T00:00:00+09:00</updated><id>https://danayagi.github.io/links/2026/02/25/links</id><content type="html" xml:base="https://danayagi.github.io/links/2026/02/25/links.html"><![CDATA[<p>※リンク先R-18注意</p>

<ul>
  <li>
    <p>ci-en: https://ci-en.dlsite.com/creator/21277</p>
  </li>
  <li>
    <p>fantia: https://fantia.jp/fanclubs/150728</p>
  </li>
</ul>]]></content><author><name></name></author><category term="links" /><summary type="html"><![CDATA[※リンク先R-18注意]]></summary></entry><entry><title type="html">Live2Dの表情差分設定をChatGPTとPythonで自動化する</title><link href="https://danayagi.github.io/live2d/rpa/2026/01/03/Live2DFacialPartsRPA.html" rel="alternate" type="text/html" title="Live2Dの表情差分設定をChatGPTとPythonで自動化する" /><published>2026-01-03T00:00:00+09:00</published><updated>2026-01-03T00:00:00+09:00</updated><id>https://danayagi.github.io/live2d/rpa/2026/01/03/Live2DFacialPartsRPA</id><content type="html" xml:base="https://danayagi.github.io/live2d/rpa/2026/01/03/Live2DFacialPartsRPA.html"><![CDATA[<h3 id="1-はじめに概要">1. はじめに（概要）</h3>

<ul>
  <li><strong>1-1. やったこと:</strong>
    <ul>
      <li>Live2Dモデルにおける「表情差分」のデフォーマ構成作業を自動化した。メッシュの変形（モデリング）そのものではなく、定型的だが手間のかかる「デフォーマ階層の作成・設定」を自動で行うことができる。透明度設定やキーフォーム追加までは行うが、頂点編集などは行えない。</li>
    </ul>
  </li>
  <li><strong>1-2. 仕組み</strong>:
    <ul>
      <li>①Live2Dエディタのスクリーンショットを撮影。</li>
      <li>②ChatGPT APIが画像を解析し、パーツパレット上の表情パーツを検出・分類。</li>
      <li>③Pythonが解析結果に基づき、マウス・キーボード操作 (RPA) を実行する。</li>
    </ul>
  </li>
  <li><strong>1-3. 成果</strong>:
    <ul>
      <li>半年～1年ほど実際の案件で運用し、概ね実用レベルで稼働した。</li>
    </ul>
  </li>
  <li><strong>1-4. 記事の趣旨</strong>:
    <ul>
      <li>ツール配布ではなく、こういうアプローチでの自動化事例の紹介。</li>
    </ul>
  </li>
</ul>

<p class="caption-text">▼動作例（GIF）。マウス操作もキー入力も全て自動で行っている。
<img src="/assets/img/20260103/GIF_動作例.gif" alt="GIF_動作例.gif" /></p>

<h3 id="2-背景なぜ自動化が必要だったか">2. 背景：なぜ自動化が必要だったか</h3>

<ul>
  <li><strong>2-1. 要件：CG集の映像化</strong>
    <ul>
      <li>「CG集の映像化」というタイプの企画に何件か関わり、実際の映像制作（Live2Dモデルの作成～MP4出力まで）を担当していた。
  その際、各シーン（一枚絵）＝各Live2Dモデルごとに「表情差分」の設定が必要となったのだが、1つの動画につき差分の総数はおおよそ <strong>30～40パターン</strong> に及び、単純作業ながら工数が多い箇所となっていた。</li>
    </ul>
  </li>
  <li><strong>2-2. 定型化されたデータ構造</strong>
    <ul>
      <li>それぞれの「表情差分」については、原画の段階でほとんどフォーマットが決まっていた。具体的には、
        <ol>
          <li><strong>フォルダ構成</strong>: 表情差分パターンごとに、1つのフォルダ内に必要なパーツがまとめられている。</li>
          <li><strong>目・口パーツ</strong>: 「開いた状態」と「閉じた状態」のペアが存在する（※「開いた目」がない場合もある）。</li>
          <li><strong>その他のパーツ</strong>: 眉や鼻など。変形はしないが、差分ごとに切り替える必要がある。</li>
        </ol>
      </li>
    </ul>

    <p class="caption-text">▼クリスタ上でのレイヤー構成（例）。こういう「表情差分」が各モデルごとに3～6パターンくらいある。<br />
  <img src="/assets/img/20260103/2-1.png" alt="クリスタ上でのレイヤー構成" class="figure-image" /></p>

    <p>▼画像としてはこのような状態<br />
  <img src="/assets/img/20260103/2-2.png" alt="画像としてはこのような状態" class="figure-image small" /></p>

    <ul>
      <li>原画が同じ形式であるため、Live2D上のデフォーマ構成も（それがどのようなシーン・表情の差分であれ）同じ形式になる。具体的には、以下のようなデフォーマ構成を毎回作成することになる。
        <ol>
          <li><strong>開いた目・口</strong>
            <ul>
              <li>表示状態管理用の「スイッチャー」、変形用の「スケーラー」の2種類のデフォーマ。</li>
            </ul>
          </li>
          <li><strong>閉じた目・口</strong>
            <ul>
              <li>「スイッチャー」のみ。</li>
            </ul>
          </li>
          <li><strong>その他のパーツ</strong>
            <ul>
              <li>必要なし。</li>
            </ul>
          </li>
          <li><strong>差分全体</strong>
            <ul>
              <li>これらをまとめる親デフォーマと「スイッチャー」。</li>
            </ul>
          </li>
        </ol>
      </li>
    </ul>

    <p class="caption-text">▼Live2Dエディタ上での最終的なデフォーマ構成<br />
  <img src="/assets/img/20260103/2-3.png" alt="Live2Dエディタ上での最終的なデフォーマ構成" class="figure-image" /></p>
  </li>
  <li><strong>2-3. データの仕様と「表記揺れ」の壁</strong>
    <ul>
      <li>構造自体は定型化されていたため、差分フォルダに含まれる「表情パーツ」のリストさえあれば自動化自体は容易だった。しかし、その「リストを正確に生成する」ことが困難だった。
        <ul>
          <li><strong>問題①: APIの不在</strong>
            <ul>
              <li>そもそもLive2DエディタにはAPIが存在せず（厳密にはあるらしいのだが、少なくともパーツ名称の取得やデフォーマ作成を行う手段はない）、 <strong>外部からパーツ情報を取得することが難しい。</strong></li>
            </ul>
          </li>
          <li><strong>問題②：レイヤー名の「表記揺れ」と「文脈依存」</strong>
            <ul>
              <li>仮にパーツ情報を取得できたとしても、レイヤー名には「目」「開き目」「開いた目 コピー」といった<strong>表記揺れ</strong>が存在する。</li>
              <li>また、「開いた目」を慣例的に単に「目」と命名しているケースなど、「フォルダ内の他パーツ（閉じ目など）の有無」から正体を推論しなければならないことも多い。そのため、正規表現などで<strong>機械的に判別するのは困難</strong>だった。</li>
            </ul>
          </li>
          <li><strong>問題③：正規化（手動リネーム）運用の煩わしさ</strong>
            <ul>
              <li>問題②については、誰が悪いとかそういう話ではなく、もし仮に「開いた目のレイヤーは”開いた目”として命名する」という規則を設けたとしても、そもそも（誰であれ）人間が目視で確認し、正規化のために手動リネームを行う運用は、ミスが起きやすく精神的負荷も高い（有り体に言うと面倒くさい）。</li>
              <li>例えば、クリスタのオートアクション等で事前にレイヤー名を設定することは可能だが、Live2D工程側でミスが発覚した場合、結局は手作業での修正が必要になってしまう。</li>
            </ul>
          </li>
        </ul>
      </li>
      <li><strong>結論</strong>:
        <ul>
          <li>表情差分の数だけ発生する「何回も繰り返す処理」であることを考慮すると、厳格なルールを前提とするよりも、<strong>多少の表記揺れがあっても柔軟に吸収できる仕組み</strong>が理想的だった。</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h3 id="3-技術的アプローチ解決策">3. 技術的アプローチ（解決策）</h3>

<ul>
  <li><strong>3-1. なぜLLM（ChatGPT）を採用したか</strong>
    <ul>
      <li>スクリーンショットの情報を起点にすれば、「そのフォルダ内にある」パーツリストの作成が可能であることに気づいたため。
        <ul>
          <li>「日本語」のOCR（画像からの文字認識）については、Pythonのライブラリ（pytesseract）では精度に問題があった。対して、<strong>LLMは精度・費用ともに現実的なラインにある</strong>。</li>
        </ul>

        <p class="caption-text">▼ChatGPTで2回スクリーンショットを解析した費用は0.01ドル<br />
  <img src="/assets/img/20260103/3-1.png" alt="ChatGPTでの解析費用" class="figure-image" /></p>
      </li>
      <li>また、LLMであれば <strong>「曖昧な入力」を分類できる</strong> ため、前述した「表記揺れがあるデータ」にも対応が可能。</li>
      <li>これら2つの機能が、単一のAPIで利用できる点が採用の決め手となった。</li>
    </ul>
  </li>
</ul>

<p><br /></p>
<ul>
  <li><strong>3-2. システム構成</strong>
    <ol>
      <li><strong>入力</strong>: 画面のスクリーンショット。</li>
      <li><strong>脳（AI）</strong>:
        <ul>
          <li><strong>ChatGPT API</strong>。画像を解析し、JSONデータを返す。</li>
          <li>出力されるJSONはパーツパレット上の並び順に対応しており、これを元に操作タスクリストを生成する。</li>
        </ul>
      </li>
      <li><strong>手（Python）</strong>:
        <ul>
          <li>返ってきたJSONに基づき、<strong>PyAutoGUI</strong>等を使用してGUI操作を実行する。</li>
          <li>※ワープデフォーマの作成やパラメータ作成については、既にモジュール化したものを使用している。</li>
        </ul>

        <p><br />
 ▼デフォーマを作成するモジュールの例。色々やばい実装だが、こういう水準のコードでも動作自体はする。依存関係は割愛。</p>

        <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">class</span> <span class="nc">DeformerManager</span><span class="p">:</span>
     <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">screen_tool</span><span class="p">,</span> <span class="n">image_locate_caches</span><span class="p">):</span>
         <span class="n">self</span><span class="p">.</span><span class="n">screen_tool</span> <span class="o">=</span> <span class="n">screen_tool</span>
         <span class="n">self</span><span class="p">.</span><span class="n">image_locate_caches</span> <span class="o">=</span> <span class="n">image_locate_caches</span>
         <span class="n">self</span><span class="p">.</span><span class="n">deformer_icon_coords</span> <span class="o">=</span> <span class="bp">None</span>

     <span class="k">def</span> <span class="nf">create_warp_deformer</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">target_part_name</span><span class="p">):</span>
         <span class="c1"># 現在の作業ディレクトリを保存
</span>         <span class="n">original_dir</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="nf">getcwd</span><span class="p">()</span>
         <span class="c1"># スクリプトファイルのディレクトリに変更
</span>         <span class="n">os</span><span class="p">.</span><span class="nf">chdir</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">dirname</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">abspath</span><span class="p">(</span><span class="n">__file__</span><span class="p">)))</span>

         <span class="c1"># 座標が必要になるタイミングで取得
</span>         <span class="n">warp_icon_coords</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">image_locate_caches</span><span class="p">.</span><span class="nf">get_coordinate</span><span class="p">(</span><span class="sh">'</span><span class="s">warp_deformer_icon</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">images/warp_deformer_icon.png</span><span class="sh">'</span><span class="p">)</span>
         <span class="c1"># デフォーマ作成処理...
</span>         <span class="n">pyautogui</span><span class="p">.</span><span class="nf">click</span><span class="p">(</span><span class="n">warp_icon_coords</span><span class="p">)</span>

         <span class="n">time</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="mf">0.1</span><span class="p">)</span>

         <span class="n">pyperclip</span><span class="p">.</span><span class="nf">copy</span><span class="p">(</span><span class="n">target_part_name</span><span class="p">)</span> 
         <span class="n">warp_deformer_window_center_coords</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">image_locate_caches</span><span class="p">.</span><span class="nf">get_coordinate</span><span class="p">(</span><span class="sh">'</span><span class="s">warp_deformer_window</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">images/warp_deformer_window.png</span><span class="sh">'</span><span class="p">)</span>
         <span class="n">self</span><span class="p">.</span><span class="nf">new_deformer_setup</span><span class="p">(</span><span class="n">warp_deformer_window_center_coords</span><span class="p">)</span>

         <span class="c1"># 作業ディレクトリを元に戻す
</span>         <span class="n">os</span><span class="p">.</span><span class="nf">chdir</span><span class="p">(</span><span class="n">original_dir</span><span class="p">)</span>

     <span class="c1"># ワープデフォーマウィンドウを操作して、ワープデフォーマを作成する処理
</span>     <span class="k">def</span> <span class="nf">new_deformer_setup</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">coords</span><span class="p">):</span>
         <span class="c1"># ウィンドウ内の相対座標を計算してクリック
</span>         <span class="n">name_area</span> <span class="o">=</span> <span class="p">(</span><span class="n">coords</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">coords</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">-</span> <span class="mi">115</span><span class="p">)</span>
         <span class="n">pyautogui</span><span class="p">.</span><span class="nf">click</span><span class="p">(</span><span class="n">name_area</span><span class="p">)</span>
         <span class="n">time</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="mf">0.01</span><span class="p">)</span>

         <span class="c1"># 「名前」欄を全選択する
</span>         <span class="n">pyautogui</span><span class="p">.</span><span class="nf">hotkey</span><span class="p">(</span><span class="sh">'</span><span class="s">ctrl</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">a</span><span class="sh">'</span><span class="p">)</span> 
         <span class="n">time</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="mf">0.01</span><span class="p">)</span>

         <span class="n">pyautogui</span><span class="p">.</span><span class="nf">press</span><span class="p">(</span><span class="sh">'</span><span class="s">delete</span><span class="sh">'</span><span class="p">)</span>
         <span class="n">time</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="mf">0.01</span><span class="p">)</span>

         <span class="c1"># コピペして文字入力を行う
</span>         <span class="n">pyautogui</span><span class="p">.</span><span class="nf">hotkey</span><span class="p">(</span><span class="sh">'</span><span class="s">ctrl</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">v</span><span class="sh">'</span><span class="p">)</span> 
         <span class="n">time</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="mf">0.01</span><span class="p">)</span>

         <span class="c1"># 「名前」を選択中であるという想定なので、タブを4回押して「OK」にカーソルを合わせる
</span>         <span class="n">pyautogui</span><span class="p">.</span><span class="nf">press</span><span class="p">(</span><span class="sh">'</span><span class="s">tab</span><span class="sh">'</span><span class="p">)</span>  <span class="c1"># Tabキーを押す
</span>         <span class="n">time</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="mf">0.01</span><span class="p">)</span>
         <span class="n">pyautogui</span><span class="p">.</span><span class="nf">press</span><span class="p">(</span><span class="sh">'</span><span class="s">tab</span><span class="sh">'</span><span class="p">)</span>  <span class="c1"># Tabキーを押す
</span>         <span class="n">time</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="mf">0.01</span><span class="p">)</span>
         <span class="n">pyautogui</span><span class="p">.</span><span class="nf">press</span><span class="p">(</span><span class="sh">'</span><span class="s">tab</span><span class="sh">'</span><span class="p">)</span>  <span class="c1"># Tabキーを押す
</span>         <span class="n">time</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="mf">0.01</span><span class="p">)</span>
         <span class="n">pyautogui</span><span class="p">.</span><span class="nf">press</span><span class="p">(</span><span class="sh">'</span><span class="s">tab</span><span class="sh">'</span><span class="p">)</span>  <span class="c1"># Tabキーを押す
</span>         <span class="n">time</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="mf">0.01</span><span class="p">)</span>

         <span class="n">pyautogui</span><span class="p">.</span><span class="nf">press</span><span class="p">(</span><span class="sh">'</span><span class="s">enter</span><span class="sh">'</span><span class="p">)</span>  <span class="c1"># Enterキーを押す
</span></code></pre></div>        </div>
      </li>
    </ol>
  </li>
</ul>

<h3 id="4-実際の処理フロー">4. 実際の処理フロー</h3>

<ul>
  <li>
    <p><strong>4-1. ①APIを叩いてスクリーンショットから「顔パーツリスト」のJSONを生成。配置されているパーツを5種類に分類する</strong>。</p>

    <p>▼プロンプトの実装例（python）</p>

    <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="n">prompt</span> <span class="o">=</span> <span class="sh">"""</span><span class="s">
      以下に添付するのはLive2DのUI上に表示されているパーツ群です。
      このとき、左端の目のアイコンが設定されている、表示中の表情グループについてのみ、以下の文字列（＝パーツ）が表示されている順番をリストアップしてください。絶対に数値でソートしないでください。画面に表示されている順番のまま、上から順番にリストアップしてください。絶対にソートしないでください。迷惑です。
      表示されているパーツ名称：（下記の5分類のうちどれに該当するか、1～5の数値）、というフォーマットで記述してください。絶対に数値でソートしないでください。また、半角の数字（1,2,3...というように）で出力してください。他の形式にはしないこと。
      解析は手動で行い、結果はjson形式で出力してください。json形式のみで、どのような場合であっても説明や解説などは一切不要です。
      また、コピー元のパーツがない場合でも、「コピー」のような名前になっているパーツがあるので、注意してください。そのようなパーツがあっても元のパーツが存在するとは限りません。どのような場合であっても、名前を取得する際は画面情報から読み取ってください。
      パーツ名に英数字が含まれている場合は半角にしてください。
      ①～⑤の数値に該当するパーツが存在しない場合もまれにあるので、あくまでも画像データに準拠した解析データを作成してください。絶対に数値でソートせず、そのままの順番でjsonにしてください。

      例：
      [
      {</span><span class="sh">"</span><span class="s">displayed_expression_group</span><span class="sh">"</span><span class="s">: </span><span class="sh">"</span><span class="s">（「表示中の表情グループ」のフォルダ名）</span><span class="sh">"</span><span class="s">},
      {</span><span class="sh">"</span><span class="s">part</span><span class="sh">"</span><span class="s">: </span><span class="sh">"</span><span class="s">右目_o</span><span class="sh">"</span><span class="s">, </span><span class="sh">"</span><span class="s">category</span><span class="sh">"</span><span class="s">: 1}, ...
      ]

      ①開いた状態（ = </span><span class="sh">'</span><span class="s">o</span><span class="sh">'</span><span class="s">）の目、または特に指定のない「目」のパーツ
      ②閉じた状態（ = </span><span class="sh">'</span><span class="s">c</span><span class="sh">'</span><span class="s">) の目
      ③開いた状態（ = </span><span class="sh">'</span><span class="s">o</span><span class="sh">'</span><span class="s">）の口、または特に指定のない「口」のパーツ
      ④閉じた状態（ = </span><span class="sh">'</span><span class="s">c</span><span class="sh">'</span><span class="s">) の口
      ⑤その他の顔パーツ（全てリストアップすること。）
      </span><span class="sh">"""</span>
        
      <span class="n">headers</span> <span class="o">=</span> <span class="p">{</span>
          <span class="sh">"</span><span class="s">Content-Type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">application/json</span><span class="sh">"</span><span class="p">,</span>
          <span class="sh">"</span><span class="s">Authorization</span><span class="sh">"</span><span class="p">:</span> <span class="sa">f</span><span class="sh">"</span><span class="s">Bearer </span><span class="si">{</span><span class="n">api_key</span><span class="si">}</span><span class="sh">"</span>
      <span class="p">}</span>
        
      <span class="n">payload</span> <span class="o">=</span> <span class="p">{</span>
          <span class="sh">"</span><span class="s">model</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">gpt-4o-2024-08-06</span><span class="sh">"</span><span class="p">,</span>
          <span class="sh">"</span><span class="s">messages</span><span class="sh">"</span><span class="p">:</span> <span class="p">[</span>
              <span class="p">{</span>
                  <span class="sh">"</span><span class="s">role</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">user</span><span class="sh">"</span><span class="p">,</span>
                  <span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">:</span> <span class="p">[</span>
                      <span class="p">{</span>
                          <span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">text</span><span class="sh">"</span><span class="p">,</span>
                          <span class="sh">"</span><span class="s">text</span><span class="sh">"</span><span class="p">:</span> <span class="n">prompt</span>
                      <span class="p">},</span>
                      <span class="p">{</span>
                          <span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">image_url</span><span class="sh">"</span><span class="p">,</span>
                          <span class="sh">"</span><span class="s">image_url</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span>
                              <span class="sh">"</span><span class="s">url</span><span class="sh">"</span><span class="p">:</span> <span class="sa">f</span><span class="sh">"</span><span class="s">data:image/jpeg;base64,</span><span class="si">{</span><span class="n">base64_image</span><span class="si">}</span><span class="sh">"</span>
                          <span class="p">}</span>
                      <span class="p">}</span>
                  <span class="p">]</span>
              <span class="p">}</span>
          <span class="p">],</span>
          <span class="sh">"</span><span class="s">max_tokens</span><span class="sh">"</span><span class="p">:</span> <span class="mi">500</span>
      <span class="p">}</span>
</code></pre></div>    </div>

    <p class="caption-text"><br /></p>
    <p>▼ChatGPT（API）に渡すスクリーンショットの例。これもpythonで撮影と送信を行う。<br />
  <img src="/assets/img/20260103/4-1_ss.png" alt="スクリーンショット" class="figure-image" /></p>

    <p><br />
  ▼生成されるjsonデータの例</p>

    <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">  </span><span class="p">[</span><span class="w">
      </span><span class="p">{</span><span class="w">
          </span><span class="nl">"displayed_expression_group"</span><span class="p">:</span><span class="w"> </span><span class="s2">"表情標準"</span><span class="w">
      </span><span class="p">},</span><span class="w">
      </span><span class="p">{</span><span class="w">
          </span><span class="nl">"part"</span><span class="p">:</span><span class="w"> </span><span class="s2">"目のコピー9"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"category"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="w">
      </span><span class="p">},</span><span class="w">
      </span><span class="p">{</span><span class="w">
          </span><span class="nl">"part"</span><span class="p">:</span><span class="w"> </span><span class="s2">"閉じた目"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"category"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="w">
      </span><span class="p">},</span><span class="w">
      </span><span class="p">{</span><span class="w">
          </span><span class="nl">"part"</span><span class="p">:</span><span class="w"> </span><span class="s2">"口"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"category"</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="w">
      </span><span class="p">},</span><span class="w">
      </span><span class="p">{</span><span class="w">
          </span><span class="nl">"part"</span><span class="p">:</span><span class="w"> </span><span class="s2">"閉じ口"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"category"</span><span class="p">:</span><span class="w"> </span><span class="mi">4</span><span class="w">
      </span><span class="p">},</span><span class="w">
      </span><span class="p">{</span><span class="w">
          </span><span class="nl">"part"</span><span class="p">:</span><span class="w"> </span><span class="s2">"眉"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"category"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="w">
      </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span></code></pre></div>    </div>

    <p>※メモ：「開いた目・口」と「閉じた目・口」で処理フローに差はないので、「開いたパーツ」と「閉じたパーツ」という粒度で分類してもよかったかもしれない</p>
  </li>
</ul>

<p><br /></p>

<ul>
  <li><strong>4-2. ②Pythonで「パーツリスト」と「タスクリスト」のJSONを生成する</strong>
    <ul>
      <li>APIから返ってきた分類データを元に、具体的な作業タスクを作成する。「目」や「口」ならデフォーマ作成タスクを追加し、「その他」なら親デフォーマ作成用のマーカーだけを打つ、といった振り分けを行う。</li>
    </ul>

    <p><br />
  ▼実装例（タスク生成ロジック）</p>

    <p><strong>※実装コードについて</strong>
  <br />
  以下は、処理の核心部分を抜粋したコードです。
  実際には画像の座標特定や依存関係の解決を行う自作クラス（<code class="language-plaintext highlighter-rouge">MarkerListClass</code>, <code class="language-plaintext highlighter-rouge">ImageLocateCaches</code> 等）を呼び出しているため、このままコピペしても動作はしませんが、「AIの出力をどうプログラムで受け止めているか」のロジック参照用として掲載します。</p>

    <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">def</span> <span class="nf">MakePartList</span><span class="p">(</span><span class="n">part_count</span><span class="p">,</span><span class="n">expression_group_from_json</span><span class="p">,</span> <span class="n">parts_from_json</span><span class="p">):</span>
      <span class="n">partList</span> <span class="o">=</span> <span class="nc">MarkerListClass</span><span class="p">(</span><span class="n">part_count</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
      <span class="n">taskList</span> <span class="o">=</span> <span class="n">TaskList</span><span class="p">.</span><span class="nc">TaskListClass</span><span class="p">(</span><span class="n">Live2DTaskScenarioNames</span><span class="p">)</span>

      <span class="n">taskNumber</span> <span class="o">=</span> <span class="mi">1</span>

      <span class="c1"># フォルダ作成は初手に置く
</span>      <span class="n">taskList</span><span class="p">.</span><span class="nf">add_task</span><span class="p">(</span><span class="n">taskNumber</span><span class="p">,</span> <span class="n">Live2DTaskScenarioNames</span><span class="p">.</span><span class="n">TASK_CREATE_PARAMETER_FOLDER</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="n">expression_group_from_json</span><span class="p">)</span>

      <span class="c1"># "part"の内部構造に応じてループ
</span>      <span class="k">for</span> <span class="n">index</span><span class="p">,</span> <span class="n">part</span> <span class="ow">in</span> <span class="nf">enumerate</span><span class="p">(</span><span class="n">parts_from_json</span><span class="p">):</span>
          <span class="n">part_name</span> <span class="o">=</span> <span class="n">part</span><span class="p">[</span><span class="sh">'</span><span class="s">part</span><span class="sh">'</span><span class="p">]</span>
          <span class="n">category</span> <span class="o">=</span> <span class="n">part</span><span class="p">[</span><span class="sh">'</span><span class="s">category</span><span class="sh">'</span><span class="p">]</span>

          <span class="c1"># 現在ループしているアイテムから名前を取得し、パーツリストを設定
</span>          <span class="n">partList</span><span class="p">.</span><span class="nf">set_value</span><span class="p">(</span><span class="n">index</span><span class="p">,</span> <span class="n">part_name</span><span class="p">)</span>

          <span class="c1"># カテゴリーが特定の番号であるかを調べる
</span>          <span class="c1"># 「開いた」状態のパーツである場合、「オープナー」マーカーを設置する
</span>          <span class="k">if</span> <span class="n">category</span> <span class="o">==</span> <span class="mi">1</span> <span class="ow">or</span> <span class="n">category</span> <span class="o">==</span> <span class="mi">3</span><span class="p">:</span>
              <span class="n">partList</span><span class="p">.</span><span class="nf">add_marker</span><span class="p">(</span><span class="n">index</span><span class="p">,</span> <span class="sh">'</span><span class="s">OPENER</span><span class="sh">'</span><span class="p">)</span>

              <span class="n">taskNumber</span> <span class="o">+=</span> <span class="mi">1</span>
              <span class="c1"># タスク編集
</span>              <span class="nf">add_scaler_tasks</span><span class="p">(</span><span class="n">taskNumber</span><span class="p">,</span> <span class="n">taskList</span><span class="p">,</span> <span class="n">part_name</span><span class="p">)</span>
              <span class="nf">add_switcher_tasks</span><span class="p">(</span><span class="n">taskNumber</span><span class="p">,</span> <span class="n">taskList</span><span class="p">,</span> <span class="n">part_name</span><span class="p">)</span>

          <span class="c1"># 「閉じた」状態のパーツである場合、「スイッチャー」マーカーを設置する
</span>          <span class="k">elif</span> <span class="n">category</span> <span class="o">==</span> <span class="mi">2</span> <span class="ow">or</span> <span class="n">category</span> <span class="o">==</span> <span class="mi">4</span><span class="p">:</span>
              <span class="n">partList</span><span class="p">.</span><span class="nf">add_marker</span><span class="p">(</span><span class="n">index</span><span class="p">,</span> <span class="sh">'</span><span class="s">SWITCHER</span><span class="sh">'</span><span class="p">)</span>

              <span class="n">taskNumber</span> <span class="o">+=</span> <span class="mi">1</span>
              <span class="c1"># タスク編集
</span>              <span class="nf">add_switcher_tasks</span><span class="p">(</span><span class="n">taskNumber</span><span class="p">,</span> <span class="n">taskList</span><span class="p">,</span> <span class="n">part_name</span><span class="p">)</span>

          <span class="c1"># 「それ以外の顔パーツ」である場合、「ペアレント」マーカーを設置する
</span>          <span class="k">elif</span> <span class="n">category</span> <span class="o">==</span> <span class="mi">5</span><span class="p">:</span>
              <span class="n">partList</span><span class="p">.</span><span class="nf">add_marker</span><span class="p">(</span><span class="n">index</span><span class="p">,</span> <span class="sh">'</span><span class="s">PARENT</span><span class="sh">'</span><span class="p">)</span>

          <span class="c1"># 上記以外の場合は例外処理を行う
</span>          <span class="k">else</span><span class="p">:</span>
              <span class="k">raise</span> <span class="nc">ValueError</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Unexpected category </span><span class="si">{</span><span class="n">category</span><span class="si">}</span><span class="s"> for part: </span><span class="si">{</span><span class="n">part_name</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
            
      <span class="n">taskNumber</span> <span class="o">+=</span> <span class="mi">1</span>

      <span class="c1"># 「whole」デフォーマの作成（タスク）
</span>      <span class="n">wholeDeformerName</span> <span class="o">=</span> <span class="n">expression_group_from_json</span> <span class="o">+</span> <span class="sh">"</span><span class="s">_whole</span><span class="sh">"</span>
      <span class="n">taskList</span><span class="p">.</span><span class="nf">add_task</span><span class="p">(</span><span class="n">taskNumber</span><span class="p">,</span> <span class="n">Live2DTaskScenarioNames</span><span class="p">.</span><span class="n">TASK_CREATE_WHOLE_DEFORMER</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="n">wholeDeformerName</span><span class="p">)</span>

      <span class="c1"># 全体のスイッチャーの作成（タスク）
</span>      <span class="nf">add_switcher_tasks</span><span class="p">(</span><span class="n">taskNumber</span><span class="p">,</span> <span class="n">taskList</span><span class="p">,</span> <span class="n">wholeDeformerName</span><span class="p">)</span>
            
      <span class="k">return</span> <span class="n">partList</span><span class="p">,</span> <span class="n">taskList</span>
</code></pre></div>    </div>
  </li>
</ul>

<p><br /></p>

<ul>
  <li><strong>4-3. ③Python（PyAutoGUI）でタスクを実行する</strong>
    <ul>
      <li>生成されたタスクリストを順に実行し、実際にLive2Dエディタを操作する。
  パーツリストの位置情報は動的に変わる（デフォーマを作ると行が増える）ため、<code class="language-plaintext highlighter-rouge">partList</code> オブジェクト側でインデックスを管理しながらクリック位置を計算している。</li>
    </ul>

    <p><br />
  ▼実装例（実行エンジン）</p>

    <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="c1"># タスクリストの情報を元にタスクを行い、パーツ追加（デフォーマ追加）した際はパーツリストを更新する
</span>  <span class="k">def</span> <span class="nf">doTask</span><span class="p">(</span><span class="n">partList</span><span class="p">,</span> <span class="n">taskList</span><span class="p">):</span>
      <span class="k">global</span> <span class="n">anchorPointCoords</span>

      <span class="n">image_locate_caches</span> <span class="o">=</span> <span class="nc">ImageLocateCaches</span><span class="p">()</span>
      <span class="n">deformer_manager</span> <span class="o">=</span> <span class="nc">DeformerManager</span><span class="p">(</span><span class="n">screen_tools</span><span class="p">,</span> <span class="n">image_locate_caches</span><span class="p">)</span>
      <span class="n">parameter_manager</span> <span class="o">=</span> <span class="nc">ParameterManager</span><span class="p">(</span><span class="n">screen_tools</span><span class="p">,</span> <span class="n">image_locate_caches</span><span class="p">)</span>

      <span class="n">currentTaskNumber</span> <span class="o">=</span> <span class="mi">0</span>
      <span class="k">for</span> <span class="n">task</span> <span class="ow">in</span> <span class="n">taskList</span><span class="p">:</span>
          <span class="c1"># 緊急停止用
</span>          <span class="k">if</span> <span class="n">keyboard</span><span class="p">.</span><span class="nf">is_pressed</span><span class="p">(</span><span class="sh">"</span><span class="s">esc</span><span class="sh">"</span><span class="p">):</span>
              <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">処理が中断されました</span><span class="sh">"</span><span class="p">)</span>
              <span class="k">break</span>

          <span class="c1"># タスク番号の検出。現在処理中のタスク番号に変化があれば、対象パーツのクリック等を行う。
</span>          <span class="k">if</span> <span class="n">task</span><span class="p">.</span><span class="n">task_id</span> <span class="o">!=</span> <span class="n">currentTaskNumber</span><span class="p">:</span>
              <span class="n">currentTaskNumber</span> <span class="o">=</span> <span class="n">task</span><span class="p">.</span><span class="n">task_id</span>
              <span class="n">targetPartIndex</span> <span class="o">=</span> <span class="n">partList</span><span class="p">.</span><span class="nf">search_last_occurrence</span><span class="p">(</span><span class="n">task</span><span class="p">.</span><span class="n">text</span><span class="p">)</span>
              <span class="k">if</span> <span class="n">targetPartIndex</span> <span class="o">!=</span> <span class="o">-</span><span class="mi">1</span><span class="p">:</span>
                  <span class="c1"># 対象パーツのクリック処理
</span>                  <span class="n">currentTargetCoords</span> <span class="o">=</span> <span class="nf">adjust_y_coordinate</span><span class="p">(</span><span class="n">anchorPointCoords</span><span class="p">,</span> <span class="n">targetPartIndex</span><span class="o">+</span><span class="mi">1</span><span class="p">,</span> <span class="sh">'</span><span class="s">down</span><span class="sh">'</span><span class="p">)</span>
                  <span class="n">pyautogui</span><span class="p">.</span><span class="nf">click</span><span class="p">(</span><span class="n">currentTargetCoords</span><span class="p">)</span>
            

          <span class="c1"># 実際のタスクを行う。「完了」以外であれば処理を行う
</span>          <span class="k">try</span><span class="p">:</span>
              <span class="k">if</span> <span class="n">task</span><span class="p">.</span><span class="n">status</span><span class="p">.</span><span class="n">value</span> <span class="o">==</span> <span class="n">TaskList</span><span class="p">.</span><span class="n">TaskStatus</span><span class="p">.</span><span class="n">COMPLETED</span><span class="p">:</span>
                  <span class="k">continue</span>
                
              <span class="k">match</span> <span class="n">task</span><span class="p">.</span><span class="n">name</span><span class="p">:</span>
                  <span class="c1"># 「パラメータフォルダ」の作成
</span>                  <span class="k">case</span> <span class="n">Live2DTaskScenarioNames</span><span class="p">.</span><span class="n">TASK_CREATE_PARAMETER_FOLDER</span><span class="p">:</span>
                      <span class="n">parameter_manager</span><span class="p">.</span><span class="nf">createFolder</span><span class="p">()</span>

                  <span class="c1"># 「スイッチャーデフォーマ」の作成
</span>                  <span class="k">case</span> <span class="n">Live2DTaskScenarioNames</span><span class="p">.</span><span class="n">TASK_CREATE_SWITCHER_DEFORMER</span><span class="p">:</span>
                      <span class="n">deformer_manager</span><span class="p">.</span><span class="nf">create_warp_deformer</span><span class="p">(</span><span class="n">task</span><span class="p">.</span><span class="n">text</span><span class="p">)</span>
                      <span class="n">targetPartIndexTmp</span> <span class="o">=</span> <span class="n">partList</span><span class="p">.</span><span class="nf">search_first_occurrence</span><span class="p">(</span><span class="n">task</span><span class="p">.</span><span class="n">text</span><span class="p">)</span>
                      <span class="k">if</span> <span class="n">targetPartIndexTmp</span> <span class="o">!=</span> <span class="o">-</span><span class="mi">1</span><span class="p">:</span>
                          <span class="n">partList</span><span class="p">.</span><span class="nf">insert_at</span><span class="p">(</span><span class="n">targetPartIndexTmp</span><span class="p">,</span> <span class="n">task</span><span class="p">.</span><span class="n">text</span><span class="p">)</span>
                          <span class="n">partList</span><span class="p">.</span><span class="nf">add_marker</span><span class="p">(</span><span class="n">targetPartIndexTmp</span><span class="p">,</span> <span class="sh">'</span><span class="s">PARENT</span><span class="sh">'</span><span class="p">)</span>
                      <span class="k">else</span><span class="p">:</span>
                          <span class="k">raise</span> <span class="nc">ValueError</span><span class="p">(</span><span class="sa">f</span><span class="sh">"'</span><span class="si">{</span><span class="n">task</span><span class="p">.</span><span class="n">text</span><span class="si">}</span><span class="sh">'</span><span class="s"> was not found in partList.</span><span class="sh">"</span><span class="p">)</span>

                  <span class="c1"># 「スイッチャーパラメータ」の作成
</span>                  <span class="k">case</span> <span class="n">Live2DTaskScenarioNames</span><span class="p">.</span><span class="n">TASK_CREATE_SWITCHER_PARAMETER</span><span class="p">:</span>
                      <span class="n">parameter_manager</span><span class="p">.</span><span class="nf">createParameter</span><span class="p">(</span><span class="n">task</span><span class="p">.</span><span class="n">text</span><span class="p">)</span>

                  <span class="c1"># 「スイッチャーパラメータ」の編集
</span>                  <span class="k">case</span> <span class="n">Live2DTaskScenarioNames</span><span class="p">.</span><span class="n">TASK_MODIFY_SWITCHER_PARAMETER</span><span class="p">:</span>
                      <span class="n">parameter_manager</span><span class="p">.</span><span class="nf">setSwitcherValues</span><span class="p">()</span>

                  <span class="c1"># 「スケーラーデフォーマ」の作成
</span>                  <span class="k">case</span> <span class="n">Live2DTaskScenarioNames</span><span class="p">.</span><span class="n">TASK_CREATE_SCALER_DEFORMER</span><span class="p">:</span>
                      <span class="n">deformer_manager</span><span class="p">.</span><span class="nf">create_warp_deformer</span><span class="p">(</span><span class="n">task</span><span class="p">.</span><span class="n">text</span><span class="p">)</span>
                      <span class="n">targetPartIndexTmp</span> <span class="o">=</span> <span class="n">partList</span><span class="p">.</span><span class="nf">search_first_occurrence</span><span class="p">(</span><span class="n">task</span><span class="p">.</span><span class="n">text</span><span class="p">)</span>
                      <span class="k">if</span> <span class="n">targetPartIndexTmp</span> <span class="o">!=</span> <span class="o">-</span><span class="mi">1</span><span class="p">:</span>
                          <span class="n">partList</span><span class="p">.</span><span class="nf">insert_at</span><span class="p">(</span><span class="n">targetPartIndexTmp</span><span class="p">,</span> <span class="n">task</span><span class="p">.</span><span class="n">text</span><span class="p">)</span>
                      <span class="k">else</span><span class="p">:</span>
                          <span class="k">raise</span> <span class="nc">ValueError</span><span class="p">(</span><span class="sa">f</span><span class="sh">"'</span><span class="si">{</span><span class="n">task</span><span class="p">.</span><span class="n">text</span><span class="si">}</span><span class="sh">'</span><span class="s"> was not found in partList.</span><span class="sh">"</span><span class="p">)</span>

                  <span class="c1"># 「スケーラーパラメータ」の作成
</span>                  <span class="k">case</span> <span class="n">Live2DTaskScenarioNames</span><span class="p">.</span><span class="n">TASK_CREATE_SCALER_PARAMETER</span><span class="p">:</span>
                      <span class="n">parameter_manager</span><span class="p">.</span><span class="nf">createScalerParameter</span><span class="p">(</span><span class="n">task</span><span class="p">.</span><span class="n">text</span><span class="p">)</span>

                  <span class="c1"># 「スケーラーパラメータ」の編集
</span>                  <span class="k">case</span> <span class="n">Live2DTaskScenarioNames</span><span class="p">.</span><span class="n">TASK_MODIFY_SCALER_PARAMETER</span><span class="p">:</span>
                      <span class="n">parameter_manager</span><span class="p">.</span><span class="nf">modifyScalerParameter</span><span class="p">()</span>

                  <span class="c1"># 「全体デフォーマ」の作成
</span>                  <span class="k">case</span> <span class="n">Live2DTaskScenarioNames</span><span class="p">.</span><span class="n">TASK_CREATE_WHOLE_DEFORMER</span><span class="p">:</span>
                      <span class="c1"># まずパーツリストに設定されている「PARENT」マーカーの箇所を複数選択する
</span>                      <span class="n">indices</span> <span class="o">=</span> <span class="n">partList</span><span class="p">.</span><span class="nf">get_indices_with_marker</span><span class="p">(</span><span class="sh">'</span><span class="s">PARENT</span><span class="sh">'</span><span class="p">)</span>
                      <span class="k">if</span> <span class="nf">len</span><span class="p">(</span><span class="n">indices</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="mi">1</span><span class="p">:</span>
                          <span class="k">for</span> <span class="n">index</span> <span class="ow">in</span> <span class="n">indices</span><span class="p">:</span>
                              <span class="c1"># 対象パーツのクリック処理
</span>                              <span class="n">currentTargetCoords</span> <span class="o">=</span> <span class="nf">adjust_y_coordinate</span><span class="p">(</span><span class="n">anchorPointCoords</span><span class="p">,</span> <span class="n">index</span><span class="o">+</span><span class="mi">1</span><span class="p">,</span> <span class="sh">'</span><span class="s">down</span><span class="sh">'</span><span class="p">)</span>
                              <span class="n">pyautogui</span><span class="p">.</span><span class="nf">click</span><span class="p">(</span><span class="n">currentTargetCoords</span><span class="p">)</span>

                              <span class="n">time</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="mf">0.1</span><span class="p">)</span>
                              <span class="n">pyautogui</span><span class="p">.</span><span class="nf">keyDown</span><span class="p">(</span><span class="sh">'</span><span class="s">ctrl</span><span class="sh">'</span><span class="p">)</span>
                              <span class="n">time</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="mf">0.1</span><span class="p">)</span>
                          <span class="n">pyautogui</span><span class="p">.</span><span class="nf">keyUp</span><span class="p">(</span><span class="sh">'</span><span class="s">ctrl</span><span class="sh">'</span><span class="p">)</span>
                          <span class="c1"># この時点で「PARENT」マーカーのあるパーツは全てクリックされている（想定）
</span>                          <span class="n">deformer_manager</span><span class="p">.</span><span class="nf">create_warp_deformer</span><span class="p">(</span><span class="n">task</span><span class="p">.</span><span class="n">text</span><span class="p">)</span>
                          <span class="c1"># 全体デフォーマの位置はどうでもいいはずなのでプログラム上の位置は先頭固定でいいはず
</span>                          <span class="n">partList</span><span class="p">.</span><span class="nf">insert_at</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">task</span><span class="p">.</span><span class="n">text</span><span class="p">)</span>
                      <span class="k">else</span><span class="p">:</span>
                          <span class="k">raise</span> <span class="nc">ValueError</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">No parts with the </span><span class="sh">'</span><span class="s">PARENT</span><span class="sh">'</span><span class="s"> marker were found in partList. Task </span><span class="sh">'</span><span class="si">{</span><span class="n">task</span><span class="p">.</span><span class="n">text</span><span class="si">}</span><span class="sh">'</span><span class="s"> cannot be completed.</span><span class="sh">"</span><span class="p">)</span>

                  <span class="k">case</span><span class="w"> </span><span class="k">_</span><span class="p">:</span>
                      <span class="k">raise</span> <span class="nc">Exception</span><span class="p">(</span><span class="sh">"</span><span class="s">task hanbetu fukanou</span><span class="sh">"</span><span class="p">)</span>
          <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
              <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">An error occurred: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
              <span class="n">taskList</span><span class="p">.</span><span class="nf">update_task_status</span><span class="p">(</span><span class="n">task</span><span class="p">.</span><span class="n">name</span><span class="p">,</span> <span class="n">TaskList</span><span class="p">.</span><span class="n">TaskStatus</span><span class="p">.</span><span class="n">FAILED</span><span class="p">,</span> <span class="n">task</span><span class="p">.</span><span class="n">text</span><span class="p">)</span>
              <span class="k">raise</span> <span class="n">e</span>

          <span class="c1"># ここからが正常系の処理
</span>          <span class="n">taskList</span><span class="p">.</span><span class="nf">update_task_status</span><span class="p">(</span><span class="n">task</span><span class="p">.</span><span class="n">name</span><span class="p">,</span> <span class="n">TaskList</span><span class="p">.</span><span class="n">TaskStatus</span><span class="p">.</span><span class="n">COMPLETED</span><span class="p">,</span> <span class="n">task</span><span class="p">.</span><span class="n">text</span><span class="p">)</span>
          <span class="n">time</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="mf">0.5</span><span class="p">)</span>

      <span class="nf">print</span><span class="p">(</span><span class="sh">'</span><span class="s">完了</span><span class="sh">'</span><span class="p">)</span>
</code></pre></div>    </div>
  </li>
</ul>

<p><br /></p>

<ul>
  <li><strong>4-4. 実行結果</strong></li>
</ul>

<p class="caption-text">▼動作例（GIF）。再掲。
<img src="/assets/img/20260103/GIF_動作例.gif" alt="GIF_動作例.gif" /></p>

<p class="caption-text">▼処理完了後。表示切り替え用のパラメータは正常に設定されている
<img src="/assets/img/20260103/4-1_GIF.gif" alt="GIF_動作例_4-1.gif" /></p>

<h3 id="5-開発でのつまづきと知見">5. 開発での「つまづき」と知見</h3>

<ul>
  <li><strong>5-1. プロンプトエンジニアリングの壁</strong>
    <ul>
      <li><strong>課題</strong>:
        <ul>
          <li>AI（ChatGPT）が、画面上の「見た目の並び順」ではなく、カテゴリーの数値順などで勝手に <strong>リストをソートして出力してしまう</strong> 現象が多発した。</li>
        </ul>
      </li>
      <li><strong>影響</strong>:
        <ul>
          <li>スクリプトは上から順番に処理を行う前提で作られているため、「見た目の表示順」と「スクリプトの実行順」がズレると、違うパーツを操作してしまい正常に動作しなくなる。体感では、不具合の原因としてこれが一番多かった。</li>
        </ul>
      </li>
      <li><strong>詳細</strong>:
        <ul>
          <li>
            <p>特に、今回使用した <code class="language-plaintext highlighter-rouge">gpt-4o-2024-08-06</code> は、データを論理的に整理整頓しようとするバイアスが強いのか、「カテゴリー」の数値で昇順ソートしたがる傾向が見られた。</p>

            <p class="caption-text">▼Live2Dエディタでは「眉」が一番 <strong>上</strong> にある。<br />
  <img src="/assets/img/20260103/5-1.png" alt="5-1_ソート例1.png" /></p>

            <p>▼勝手にソートしてしまう例。API経由で解析させたJSONデータでは、「眉」が一番 <strong>下</strong> にある（カテゴリで昇順ソートしてしまっている？）。<br />
  <img src="/assets/img/20260103/5-2.png" alt="5-2_ソート例2.png" /></p>
          </li>
        </ul>
      </li>
    </ul>

    <p><br /></p>

    <ul>
      <li><strong>知見</strong>:
        <ul>
          <li>プロンプトで強く制約することで正常に動くこともあったが、それでも時々ソートされて出力されることがあった。</li>
          <li>最も安定させるには、<strong>「画面上の配置自体を、最初から昇順ソートになるように並べ替えておく（＝AIがソートしても順序が変わらない状態にする）」</strong> という、人間側での運用回避が有効であるように思う。</li>
        </ul>

        <p class="caption-text">▼今回の例だと、Live2Dエディタでも「眉」を一番下に配置すると安定する。RPA処理後に元の位置に戻す手間もほぼない。<br />
  <img src="/assets/img/20260103/5-3.png" alt="5-3_対策.png" /></p>
      </li>
    </ul>
  </li>
</ul>

<p><br /></p>

<ul>
  <li><strong>5-2. 画像認識の壁</strong>
    <ul>
      <li><strong>課題</strong>:
        <ul>
          <li>スクリーンショットを「原寸サイズ（1.0倍）」のままAIに渡すと、解析エラーや認識漏れが発生することが多かった。</li>
        </ul>
      </li>
      <li><strong>解決策</strong>:
        <ul>
          <li><strong>「画像を1.5倍に拡大してからAPIに投げる」</strong> ことで、途端に精度が安定した。</li>
          <li>従来のOCRライブラリ（pytesseract等）では、読み取れなかった文字が関係のない漢字に置き換わるといった誤認識が頻発したが、LLM（1.5倍拡大済み）では <strong>文字認識そのものがボトルネックになることはほぼ無かった</strong>。</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<p><br /></p>

<ul>
  <li><strong>5-3. RPA（GUI自動化）の壁</strong>
    <ul>
      <li><strong>IMEの問題</strong>:
        <ul>
          <li>日本語入力（IME）が「全角モード」になっていると、特に<strong>数値入力</strong>が正常に処理されない。Live2Dエディタ上でのパラメータ設定などで数値が設定できず、フロー全体が異常終了する原因となる。（多分python側で簡単に対策することは出来る）</li>
        </ul>
      </li>
      <li><strong>フォーカスとタイミング</strong>:
        <ul>
          <li>非アクティブウィンドウへの操作や、ラグ等でポップアップが出るのが遅れた場合の例外処理。これらはAPI操作にはない、GUI自動化特有の不安定要素であり、<code class="language-plaintext highlighter-rouge">time.sleep</code> の調整や画像認識による待機処理が必要になる。</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h3 id="6-課題と感想">6. 課題と感想</h3>

<ul>
  <li><strong>6-1. 画面依存の脆さ</strong>
    <ul>
      <li><strong>UIの見切れ</strong>:
        <ul>
          <li>
            <p>「差分」フォルダ全体がパーツパレットの下の方にある場合、デフォーマを追加していく度にそれよりも「下」にあるパーツがさらに下に移動してしまうため、処理の途中で見切れるパーツが発生してしまう。<strong>画面上でスクロールしないと見えない（見切れている）ものは、物理的にクリックができないため操作不能</strong>になる。</p>

            <p class="caption-text">▼このあたりで処理を開始すると確実に見切れる（特に「口」「閉じ口」などがウィンドウの外に出てしまうので、対応するデフォーマを作成できない）。画面上に「存在しない」パーツは選択不可能。<br />
  <img src="/assets/img/20260103/6-1.png" alt="6-1.png" /></p>
          </li>
        </ul>
      </li>
    </ul>

    <p><br /></p>

    <ul>
      <li><strong>レイアウト変更への弱さ</strong>:
        <ul>
          <li>Live2Dエディタのバージョンアップでアイコンやウィンドウのデザインが変わると、Python側の画像認識処理がマッチしなくなり動作しなくなる。実際、デフォーマウィンドウの識別などがバージョンアップで動かなくなることがあった。</li>
        </ul>
      </li>
    </ul>

    <p><br /></p>

    <ul>
      <li><strong>誤検出</strong>:
        <ul>
          <li>
            <p>時折、指定した「差分」フォルダの外にあるパーツまでAIが拾ってしまうことがあった。</p>

            <p class="caption-text">▼処理対象の「表情差分」のすぐ下に、全ての「表情差分」で共通のパーツ（「赤面2」「赤面」）が配置されてしまっている例。<br />
  <img src="/assets/img/20260103/6-2.png" alt="6-2.png" /></p>

            <p>▼生成されたJSON。「赤面2」と「赤面」のパーツが不要。</p>
            <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">  </span><span class="p">[</span><span class="w">
      </span><span class="p">{</span><span class="nl">"displayed_expression_group"</span><span class="p">:</span><span class="w"> </span><span class="s2">"表情_02"</span><span class="p">},</span><span class="w">
      </span><span class="p">{</span><span class="nl">"part"</span><span class="p">:</span><span class="w"> </span><span class="s2">"眉"</span><span class="p">,</span><span class="w"> </span><span class="nl">"category"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">},</span><span class="w">
      </span><span class="p">{</span><span class="nl">"part"</span><span class="p">:</span><span class="w"> </span><span class="s2">"目"</span><span class="p">,</span><span class="w"> </span><span class="nl">"category"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">},</span><span class="w">
      </span><span class="p">{</span><span class="nl">"part"</span><span class="p">:</span><span class="w"> </span><span class="s2">"閉じ目"</span><span class="p">,</span><span class="w"> </span><span class="nl">"category"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">},</span><span class="w">
      </span><span class="p">{</span><span class="nl">"part"</span><span class="p">:</span><span class="w"> </span><span class="s2">"口"</span><span class="p">,</span><span class="w"> </span><span class="nl">"category"</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p">},</span><span class="w">
      </span><span class="p">{</span><span class="nl">"part"</span><span class="p">:</span><span class="w"> </span><span class="s2">"閉じ口"</span><span class="p">,</span><span class="w"> </span><span class="nl">"category"</span><span class="p">:</span><span class="w"> </span><span class="mi">4</span><span class="p">},</span><span class="w">
      </span><span class="p">{</span><span class="nl">"part"</span><span class="p">:</span><span class="w"> </span><span class="s2">"赤面2"</span><span class="p">,</span><span class="w"> </span><span class="nl">"category"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">},</span><span class="w">
      </span><span class="p">{</span><span class="nl">"part"</span><span class="p">:</span><span class="w"> </span><span class="s2">"赤面"</span><span class="p">,</span><span class="w"> </span><span class="nl">"category"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span></code></pre></div>            </div>
          </li>
        </ul>
      </li>
    </ul>

    <p><br /></p>
  </li>
  <li><strong>6-2. 実行速度とコスト、およびリスク</strong>
    <ul>
      <li><strong>速度</strong>:
        <ul>
          <li>APIを叩いてからJSONが返ってくるまで大体<strong>約5秒程度</strong>だった。手作業の手間を考えれば、待ち時間は全く気にならないレベルだった。</li>
        </ul>
      </li>
      <li><strong>コスト</strong>:
        <ul>
          <li>今回の規模感であれば、API利用料も微々たるもので無視できる範囲。</li>
        </ul>
      </li>
      <li><strong>コンテンツリスク（未検証）</strong>:
        <ul>
          <li>解析にスクリーンショットを用いているため、もし画像内にOpenAIの定める「不適切な表現・単語（NSFW等）」が含まれる場合、API側でリジェクトされる可能性がある（今回は発生しなかったが、可能性としては考慮すべき）。</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<p><br /></p>

<ul>
  <li><strong>6-3. 結論</strong>
    <ul>
      <li>まれに想定とは異なる動作をする場合もあったが、今回のような <strong>「誰がやっても同じ結果になる（なるべき）」定型作業</strong> は自動化しやすく、その恩恵も大きい。処理フローの一部としてAIにOCR処理をさせるという手法は、既に十分実用的な水準にあると感じた。</li>
    </ul>
  </li>
</ul>

<h3 id="7-おわりに">7. おわりに</h3>

<ul>
  <li>APIがないツールでも、現行水準のAIなら<strong>人間のように画面の情報を理解できる</strong>ため、それを足がかりに自動化できる場合がある。</li>
</ul>]]></content><author><name></name></author><category term="Live2D" /><category term="RPA" /><summary type="html"><![CDATA[1. はじめに（概要）]]></summary></entry><entry><title type="html">サンプル記事 (Japanese)</title><link href="https://danayagi.github.io/sample/2026/01/01/sample.html" rel="alternate" type="text/html" title="サンプル記事 (Japanese)" /><published>2026-01-01T00:00:00+09:00</published><updated>2026-01-01T00:00:00+09:00</updated><id>https://danayagi.github.io/sample/2026/01/01/sample</id><content type="html" xml:base="https://danayagi.github.io/sample/2026/01/01/sample.html"><![CDATA[<p>これは <code class="language-plaintext highlighter-rouge">_posts</code> 直下に配置された日本語の記事です。
ファイル名は <code class="language-plaintext highlighter-rouge">2026-01-01-sample.md</code> です。</p>]]></content><author><name></name></author><category term="sample" /><summary type="html"><![CDATA[これは _posts 直下に配置された日本語の記事です。 ファイル名は 2026-01-01-sample.md です。]]></summary></entry><entry><title type="html">Sample Post (English)</title><link href="https://danayagi.github.io/en/sample/2026/01/01/sample/" rel="alternate" type="text/html" title="Sample Post (English)" /><published>2026-01-01T00:00:00+09:00</published><updated>2026-01-01T00:00:00+09:00</updated><id>https://danayagi.github.io/en/sample/2026/01/01/sample</id><content type="html" xml:base="https://danayagi.github.io/en/sample/2026/01/01/sample/"><![CDATA[<p>This is the English version of the post.
Located in <code class="language-plaintext highlighter-rouge">_posts/en/2026-01-01-sample.md</code>.
It should have a different URL from the Japanese one.</p>]]></content><author><name></name></author><category term="sample" /><summary type="html"><![CDATA[This is the English version of the post. Located in _posts/en/2026-01-01-sample.md. It should have a different URL from the Japanese one.]]></summary></entry></feed>