生成AIでモンスターの合体

ワクワクが止まらない!AI合成合体の魅力とは?

2体のモンスターを融合させて、まったく新しいモンスターを生み出す――そんなゲーム、遊んだことはありませんか?

たとえば、「真・女神転生」や「ラストハルマゲドン」のようなゲームが思い浮かぶ方もいるでしょう。このタイプのゲームでは、モンスターを合体させた瞬間に訪れる「次はどんなモンスターが生まれるんだろう?」という期待感がたまりませんよね。

ゲーム内では、モンスター合体の結果は事前に決まっていることが多いです。「この2体を組み合わせたら、このモンスターができる!」というシステムが基本。そのため、最強の組み合わせを探す楽しさや、試行錯誤の奥深さがプレイヤーを夢中にさせてきました。

ただし、プレイヤー心理としては、できれば毎回違うモンスターを見てみたいもの。以前見たことのあるモンスターが再び登場すると、少しがっかりしてしまいますよね。しかし、この「毎回違うモンスターを生み出す」という仕組み、実現するには大きな壁があります。

なぜなら、モンスターの種類が増えるほど、組み合わせの数は爆発的に増えるからです。
そのすべてに対応する合体後のモンスターのデザインを用意するには、膨大な労力と時間が必要になります。ゲーム開発者にとって、これは避けられない課題と言えるでしょう。

では、どうすれば無限の可能性を提供できるのか?

「生成AIにモンスターの合体デザインを任せればいいんじゃない?」
そう考えると、この問題を一気に解決できるかもしれません。生成AIの活用により、プレイヤーが合体させたモンスターに応じて、毎回新しいデザインを自動生成することが可能になります。結果、無限のバリエーションを持つゲーム体験が実現するのです。

これこそが、今回のテーマです。
AIを活用した次世代のモンスター合体ゲーム――その無限の可能性に、今からワクワクが止まりません!

生成AIで実現するモンスター合体の楽しみ方

モンスター合体の醍醐味といえば、まずはその見た目がどのように変化するかですよね?もちろん、能力やスキルの融合も重要ですが、やはり一番の楽しみは、細かなデザインや特徴がどのように組み合わさり、新しい姿を生み出すのかにあると思います。

では、これを生成AIを使ってどのように実現できるのでしょうか?少し考えてみましょう。

例えば、ChatGPTのマルチモーダル機能を使って、2枚の画像を組み合わせて新しい1枚の画像を生成する…というようなことが可能だと思うかもしれません。しかし実際には、現在の技術では、画像を直接融合させるのではなく、一度それぞれの画像を言語化する必要があります。

もし「チャトモン」というシステムをご存知であれば、こう考えるかもしれません。

「画像をテキスト化して、そのテキストをもとに新しいモンスター画像を生成するんだね?」

そうです。具体的には、以下のようなプロセスがイメージできます:

  1. 元となる画像の言語化
    2枚のモンスター画像をそれぞれテキストに変換し、デザインや特徴を詳細に記述します。
  2. テキストの融合
    2つのテキストを組み合わせて、新しいモンスターの特徴を定義します。
  3. 画像の再生成
    その融合テキストをもとに、AIが新しいモンスターの画像を生成します。

このように、生成AIを使うことで、モンスター合体の新しい楽しみ方を提案できるのです。

モンスター画像を2枚まとめてChatGPTのAPIに投げる方法をとります!
そして、回答として、合体モンスターの画像を生成するプロンプトを返させるように指示します。

Webアプリのプログラムソース

プログラムのソースファイルは下記の通りとなります。htmlフォルダに適当なサブフォルダを作成し、下記のソースをindex.htmlというファイル名で保存しましょう。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>モンスター合成</title>
  
  <style>
    /* アニメーション用のCSS */
    @keyframes pulse {
      0% { background-color: #fff; }
      50% { background-color: #CEF; }
      100% { background-color: #fff; }
    }
    /* 通信中に背景色をアニメーションさせるクラス */
    .communicating {
      animation: pulse 1.5s infinite;
    }
  </style>

</head>
<body>

  <div style="display: flex; justify-content: flex-start; gap: 20px;">

    <div style="padding: 10px; border: 1px solid black; text-align: center;">
      <h2>合成元モンスター1</h2>
      <img id="previewImage1" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" style="width: 256px; height: 256px; border: 1px solid black;">
      <br><br>
      <input type="file" accept="image/*" id="fileInput1" onchange="handleFileChange(1)">
      <br><br>
    </div>

    <div style="padding: 10px; border: 1px solid black; text-align: center;">
      <h2>合成元モンスター2</h2>
      <img id="previewImage2" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" style="width: 256px; height: 256px; border: 1px solid black;">
      <br><br>
      <input type="file" accept="image/*" id="fileInput2" onchange="handleFileChange(2)">
      <br><br>
    </div>

  </div>
  <div style="display: flex; justify-content: center; margin-top: 5px; padding:10px;width: 620px;">
    <button onclick="synthesis()">合成する</button>
  </div>
  
  <div style="display: flex; flex-direction: column; align-items: center; margin-top: 5px; padding: 10px; width: 620px; border: 1px solid black;">
    <h2>合成:新モンスター</h2>
    <canvas id="monsterCanvas" width="512" height="512" style="border: 1px solid black;"></canvas>
  </div>

  <script>
    let chatGptApiKey = window.prompt("Please enter your OpenAI API key:");
    const endPoint = "https://api.openai.com/v1/chat/completions";
    const endPointImg = "https://api.openai.com/v1/images/generations";

    let base64Strs = []; // 画像テータをテキストにしたものを格納

    // 画面をロックする関数
    function lockScreen() {
      // ボディ要素に通信中を示すクラスを追加
      document.body.classList.add('communicating');
      // すべてのfile inputを無効化
      document.querySelectorAll('input[type="file"]').forEach(input => {
        input.disabled = true;
      });
      document.querySelector('button').disabled = true;
    }

    // 画面のロックを解除する関数
    function unlockScreen() {
      // ボディ要素から通信中を示すクラスを削除
      document.body.classList.remove('communicating');      
      // すべてのfile inputを有効化
      document.querySelectorAll('input[type="file"]').forEach(input => {
        input.disabled = false;
      });
      document.querySelector('button').disabled = false;
    }

    // 画像のアップロード
    function handleFileChange(monsterNumber) {
      lockScreen();
      const fileInput = document.getElementById(`fileInput${monsterNumber}`);
      const selectedFile = fileInput.files[0];
      if (selectedFile) {
        const reader = new FileReader();
        reader.onload = function(event) {
          const base64String = event.target.result.split(',')[1];
          document.getElementById(`previewImage${monsterNumber}`).src = event.target.result;
          unlockScreen();
          base64Strs[monsterNumber-1] = base64String;

        };
        reader.readAsDataURL(selectedFile);
      }
    }
    
    // モンスターの合成
    async function synthesis() {
      lockScreen();
      const modelName = "gpt-4o";
      const messages = [
        {
          role: "system",
          content: "非常に創造力豊かに回答してください。",
        },
        {
          role: "user",
            content: [
              {
                type: "text",
                text: `
                    ### 二つのアップロードされた画像の特徴を組み合わせて、ユニークなモンスターを作成してください。結果として得られるモンスターは、両方の画像の特徴をシームレスにブレンドし、体の形状、色、質感、およびその他の注目すべき特徴を取り入れたものにしてください。最終的なデザインは一貫性があり、両方の画像の最も顕著な側面を一つの調和の取れた生物に統合するようにしてください。
                    ### 見た目の説明及びそれを補完する情報のみ出力し、モンスターの名前や起源伝承、詳細な説明は書かないで下さい。2体と誤解を受ける表現も避けて下さい。
                    ### 1体のモンスターを描く為のプロンプトとして、下記のJSONの???を埋めて出力してください。"
                    {
                      "モンスター": {
                        "モンスター全体の視覚的特徴": "???",
                        "特徴": {
                          "頭": {
                            "サイズ": "???",
                            "スタイル": "???"
                          },
                          "体": {
                            "形状": {
                              "基本": "???",
                              "詳細": "???"
                            },
                            "色": "???"
                          },
                          "腕": {
                            "数": "???",
                            "詳細": "???"
                          },
                          "脚": {
                            "数": "???",
                            "詳細": "???"
                          },
                          "翼": {
                            "後翼": "???",
                            "前翼": "???"
                          },
                          "尾": {
                            "存在": "???",
                            "詳細": "???"
                          },
                          "スタイル": {
                            "テーマ": "コミカルながらクールで現実味のある画風",
                            "コンテキスト": "ゲームキャラクター",
                          }
                        }
                      }
                    }
                `
              },
              {
                type: "image_url",
                image_url: {
                  url: `data:image/jpeg;base64,${base64Strs[0]}`,
                  detail: "low",
                },
              },
              {
                type: "image_url",
                image_url: {
                  url: `data:image/jpeg;base64,${base64Strs[1]}`,
                  detail: "low",
                },
              },
            ],
        },
      ];

      const requestOptions = {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${chatGptApiKey}`
        },
        body: JSON.stringify({
          model: modelName,
          messages: messages,
          max_tokens: 1000,
        }),
      };

      console.log("Send:" + JSON.stringify(requestOptions));
      try {
        const response = await fetch(new Request(endPoint, requestOptions));
        const json = await response.json();
        const imgPrompt = extractTextBetweenBackticks(json.choices[0].message.content).replace(/,/g, '\n');
        const prompt = "### あなたは非常に優秀な画家です。説明や文字は一切描けません(***文字なし***。***テキストなし***。***説明画像なし***。***サブ画像なし***)。下記のJSONを元に(ランダムの場所にいる)正面からの1視点から描いた1体のモンスターを描いて下さい。\n" + imgPrompt;
        await displayImage(prompt, chatGptApiKey, document.getElementById('monsterCanvas'));
      } catch (error) {
        console.error('Error:', error);
        alert("エラーが発生しました。")
      }
      unlockScreen();
    }

    // 画像を表示する
    async function displayImage(prompt, apiKey, canvasElement) {
      const imageUrl = await generateImage(prompt, apiKey);
      const imgElement = document.createElement('img');
      imgElement.src = imageUrl;
      imgElement.onload = function () {
        const context = canvasElement.getContext('2d');
        context.drawImage(imgElement, 0, 0, canvasElement.width, canvasElement.height);
        canvasElement.style.display = 'block';
      }
    }

    // JSONデータのみ抜き出し
    function extractTextBetweenBackticks(jsonText) {
      const regex = /```([^`]*)```/;
      const match = regex.exec(jsonText);
      return match ? match[1] : jsonText;
    }

    // 画像を生成する
    async function generateImage(prompt, apiKey) {
      try {
        const response = await fetch(endPointImg, {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${apiKey}`,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            model: 'dall-e-3',
            quality: 'standard',
            style: 'vivid',
            prompt: prompt,
            n: 1,
            size: "1024x1024",
          }),
        });
        const data = await response.json();
        const image = data.data[0].url;
        return image;
      } catch (error) {
        throw new Error(error);
      }
    }
    
  </script>
</body>
</html>

copy

ソースファイルです。zipでも提供いたします。

https://note.com/api/v2/attachments/download/0e623b575cc2c23b776e166bf02df83d

html/MonsterSynthesisというフォルダーにindex.htmlを保存した場合は…

http://localhost/MonsterSynthesis

というURLを呼び出せば実行できます!

なお、記事では「モンスターの合体」と書いていますがプログラムソース上は「モンスター合成」と書かれています。ご了承下さい。

ソースの説明はあえてしません!実際に動かして試し、分からないところはChatGPTに聞いて理解してみて下さい!

操作方法

URLを入力してブラウザを開くと、OpenAIのAPIキーを要求されます。ユーザー様が取得された、正しいAPIキーを入力してください。

完了すると、下記のように「合成元モンスター1」「合成元モンスター2」の名前が書かれた、アップロードするファイルの選択画面が表示されているかと思います。

画像

①でまず、合成(合体)するモンスターの1体目を選択します。
②で、合成(合体)するモンスターの2体目を選択します。
③で①と②のモンスターの合成(合体)を開始します。この処理は時間がかかるかと思います。

しばらくすると、下記のように融合された新しいモンスターの画像が表示されます!

画像

見た目がモンスター同士の合体

画像

CDLE名古屋公式キャラクター「グリーザー」の合体

画像

人型と人型ではないモンスターの合体

画像

女の子キャラクター同士の合体

画像

日常の写真からのモンスター合体

画像

ザ・フライみたいな人と何かを

画像