見出し画像

生成AIでモンスターの合体を実現!(OpenAI-API + Webアプリ)【ソース有】

ワクワクしかない!モンスター合体ゲーム!

2体のモンスターを合体(融合)させ、ひとつの新しいモンスターを生成する…というゲームを遊んだことはありますか?

「真女神転生」?はたまた、「ラストハルマゲドン」でしょうか?
いずれにしても、2体のモンスターを合わせたら…

「どのようなモンスターになるのか!?ワクワクしかない!」

…という感じでしたよね!
このモンスターの合体パターンはゲーム内であらかじめ決められており、この組み合わせなら、必ずこのモンスターになる!というような感じだったかと思います。最強の組み合わせを探したりなんかして、かなり奥が深かったと記憶しています。

遊んでいるプレイヤーとしては、合体によって生まれるモンスターは、毎回違うものを見たいと思うものです。前に見た事のあるモンスターが出てくると、ガッカリしてしまいますよね!
ですが…この仕組み…モンスターが一体増えるごとに爆発的に組み合わせが増える!ことになります。組み合わせが増えれば、合体によって生まれるモンスターも基本的には増えていきます。そして、プレイヤーが楽しめる数の合体後のモンスターの画像を用意する…というと、その苦労は並大抵ではありません。

ならば…

「生成AIに合体させた絵を描かせてしまえば良いんでない?」

…という感じですよね!
これをゲームに組み込めれば、無限の種類のモンスターを生成する事が可能になりますよね!

これが、今回のテーマとなります!


今回もローカルWebアプリで実現!!

さて、この仕組み、いつも通り、こちらの記事をベースに実装していきます。まだ読んでいらっしゃらない方は、ぜひ、読んでみて下さい!

読み終わって環境が整いましたら、OpenAIのAPIキーを準備して、次へ進みましょう!

生成に使うのは画像のみ!

モンスター合体の楽しみはまず、どのような見た目になるかですよね?
能力の合体も大事ですが…やはりモンスターの見た目の細かい部分がどのように融合されるかが楽しいんじゃないかと思います。

これを生成AIで、どのように実現するのかを考えていきたいと思います。

まず、ChatGPTのマルチモーダル機能ですが、2枚の画像を元に1枚の画像を生成する…なんてことは出来ません。画像を一度言語化するなどの必要があります。
チャトモンをご存知でしたら…

「モンスター画像を一枚づつ言語化し、その2つのテキスト元に画像生成するんでしょ?」

…と思われた方もいらっしゃるかもしれません。
つまり…下記の図のような方法ですが…

…この方法は今回はとっていません!

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

プロンプトが出力されたら、そのプロンプトを元に、合体モンスターをDALL-E3(画像生成AI)に書いてもらう、という流れになりますね!

画像以外にもモンスターの能力が出力されないかな?って方は、下記の記事にある画像から能力を出力する「チャットモンスター」という仕組みを参考にしてみて下さい!

この仕組みを使えば、見た目通りの能力を考えてくれますので、便利に使えるかと思います!

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

このWebアプリは「HTMLファイルひとつでOpenAI-APIを使いこなす!」という過去の記事の通り…

「今回もHTMLファイルひとつで動きます!」

プログラムのソースファイルは下記の通りとなります。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>

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

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

http://localhost/MonsterSynthesis

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

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

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

操作方法

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

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

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

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

色々なモンスター合体を試してみよう!!

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

まずは、普通にモンスターっぽい画像同士から。
流石に、モンスター同士の合体はカッコよくできています!

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

グリーザーとムキムキの格闘家の合体です。
あんなに貧弱だったグリーザーが凄く強そうに!?

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

ジャンルが異なる画像の合体の場合、説明の文字が入ったり、
複数キャラクターが生成されたりする可能性が高いです。
表示されないように頑張ってはいますが中々難しいですね。

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

モンスターを生成するというプロンプトを書いているので
モンスターになりますね!

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

ラーメンや観光地の写真なんかからも合体できます。
文字や説明画像が入りやすい感じになります。

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

倫理的にどうか?って気もしますが、
こんな組み合わせも可能です。

禁断の合体

混ぜちゃいけないものも、合体できたりします!

こんな記事も書いていますので、よろしければ、どうぞ!

あとがき

今回は、別件でこの仕組みを調査する必要があり、たまたまモンスター合体を思いついて試してみました。
思ったより面白かったです!

AIを利用したゲームを作成するときの1つのネタとして使えそうな気がしますね!

ただし、ゲームで利用など、1回で正しい画像を作らなくてはならない場合は「画像に文字が入ってしまう」「複数視点からの画像になる」「説明画像が入る」という問題がネックになります。
これを、プロンプトで完全に除外するのは、試した感じでは難しそうでした。もし対策するのであれば、最終的に生成された画像が要件を満たしているかをAIにチェックさせ、NGなら再生成するような仕組みを導入する必要がありそうですね。

合体させることにより、思いつかないような斬新なデザインのモンスターが生成できますので、ゲームや漫画のキャラクターづくりにも使えそうな気がます。
あとは、プロンプトを変えれば・・・

  • 食べ物を合体させ、新しい食べ物を生成する

  • モビルスーツ同士を合体させ、新しいモビルスーツを生成する

  • 僕とあの娘が結婚した時の子供の顔を生成する

みたいな事も出来そうですね!

ぜひ、みなさんも遊んでみて下さい!
では!

この記事が気に入ったらサポートをしてみませんか?