LoginSignup
0
1

Unity6(Preview)のWebGL書き出しでAPI叩きしてみたよ

Last updated at Posted at 2024-05-27

前回の続きです。

UnityではアプリならUnityWebRequest使ってしれっと外部API叩き出来ちゃうんですけど
WebGLだとHTMLに乗っけてるのが原因でクロスオリジンリソース共有 (CORS)を
ちゃんとしないといけないようになってます。
特に画像とかのバイナリ系はすぐ引っかかる。
なんでその辺をクリアしてWebGLで書き出したものでも
できるだけアプリと同じことをできるようにする土台作りだと思ってください。
以下はオレがChatGPTに聞きながらなんとかした方法なので
詳しい人に聞いたらもっとあっさりした方法あるかも。

このデモでは毎回始めるたびに
OpenAIのDall-E3のAPIを使って生成したSkyBoxを背景に貼っています。
そしてChatのAPIでJson形式で書き出させたWebGLにまつわるクイズが地面のブロックの側面に表示されます。
処理が重いので時間差ありますが。
スマホで見てみてね。PCでも動くけども。

この記事を読むのに向いてる人

Heroku設定してCORSを掻い潜るためにProxy経由でOpenAIのAPIを叩きたい人
Herokuわからんけど使いたい人
OpenAIのAPI料金設定のわけのわからない仕様の謎をつきとめたい人
何でもいいからUnityWebGLからAPI叩きたい人

詳細書くとクッソ長くなるからサクサク行きますよ!

使用するツールのインストール

  • Node.js
  • Heroku CLI

winの人はターミナル(PowerShell)orコマンドプロンプトから
winget経由のインストールが楽です。

winget install --id OpenJS.NodeJS.LTS
winget install --id Heroku.HerokuCLI

Herokuの準備

Herokuのアカウントを作ります。

Herokuの認証にMicrosoft Authenticator使うのでiPhoneかAndroidを用意してね

winの人はターミナル(PowerShell)orコマンドプロンプトからHeroku CLIにログイン

heroku login

このコマンド叩くとブラウザが立ち上がってここに行くのでログインします

image.png

Proxy用のProjectフォルダの準備

Proxy用のProjectフォルダを作って中に入ります

mkdir CorsProxy
cd CorsProxy

以下の内容でpackage.jsonファイルを作ります。

package.json
{
  "name": "cors-proxy",
  "version": "1.0.0",
  "description": "CORS Proxy server",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "cors-anywhere": "^0.4.4",
    "express": "^4.17.1"
  },
  "engines": {
    "node": "20.x"
  }
}

必要なパッケージをインストールします。
さっきのNode.jsのインストールしてればいけます

npm install

一旦node_modulesを作ると管理者権限で開いた
ターミナル(PowerShell)から消さなきゃいけません
以下のコマンドで消せます。

Remove-Item -Recurse -Force node_modules

拡張子とか付けないProcfileという名前のファイルを作ります。
アプリケーションの起動コマンドを指定するのに使うファイルです。
中身はこれだけ

Procfile
web: node server.js

server.jsファイルを作成します
originWhitelistの中は前回作ったGitHub PagesのURLが入ります。
このオリジンからの通知は通してねみたいな穴です。

server.js
const express = require('express');
const corsAnywhere = require('cors-anywhere');

const host = process.env.HOST || '0.0.0.0';
const port = process.env.PORT || 8080;

corsAnywhere.createServer({
  originWhitelist: ['ここには前回つくったGitHub PagesのURLを入れます'], 
  requireHeader: ['origin', 'x-requested-with'],
  removeHeaders: ['cookie', 'cookie2']
}).listen(port, host, function () {
  console.log('Running CORS Anywhere on ' + host + ':' + port);
});

この時点でCorsProxyフォルダの中はこんな感じになってます。
image.png

Herokuへのデプロイ

引き続きターミナル(PowerShell)orコマンドプロンプトは
cdでCorsProxyフォルダにフォーカスしておいてください。

my-cors-proxyって名前でHerokuアプリを作成

heroku create my-cors-proxy

Gitの初期化+Herokuリモートリポジトリを追加

git init
heroku git:remote -a my-cors-proxy

すべてのファイルをGitに追加してコミット

git add .
git commit -m "Initial commit"

Herokuにデプロイします。

git push heroku master

手元のブランチがmainでHerokuがmasterみたいな名前違う時は

git push heroku master:main

って書き方でもOK
デプロイが完了すると、Heroku CLIにアプリのURLが表示されます。
以下のコマンドでアプリ名(my-cors-proxy)から詳細を確認してもOK

heroku info -a my-cors-proxy
 »   Warning: heroku update available from 7.53.0 to 8.11.5.
=== my-cors-proxy
Auto Cert Mgmt: false
Dynos:          web: 1
Git URL:        https://git.heroku.com/my-cors-proxy.git
Owner:          Herokuに登録した時のメールアドレス
Region:         us
Repo Size:      510 MB
Slug Size:      45 MB
Stack:          heroku-22
Web URL:        このアプリのURL

このアプリのURLをブラウザにコピペして入れて
以下の文言が表示されたら成功です。

This API enables cross-origin requests to anywhere.

Usage:

/               Shows help
/iscorsneeded   This is the only resource on this host which is served without CORS headers.
/<url>          Create a request to <url>, and includes CORS headers in the response.

If the protocol is omitted, it defaults to http (https if port 443 is specified).

Cookies are disabled and stripped from requests.

Redirects are automatically followed. For debugging purposes, each followed redirect results
in the addition of a X-CORS-Redirect-n header, where n starts at 1. These headers are not
accessible by the XMLHttpRequest API.
After 5 redirects, redirects are not followed any more. The redirect response is sent back
to the browser, which can choose to follow the redirect (handled automatically by the browser).

The requested URL is available in the X-Request-URL response header.
The final URL, after following all redirects, is available in the X-Final-URL response header.


To prevent the use of the proxy for casual browsing, the API requires either the Origin
or the X-Requested-With header to be set. To avoid unnecessary preflight (OPTIONS) requests,
it's recommended to not manually set these headers in your code.


Demo          :   https://robwu.nl/cors-anywhere.html
Source code   :   https://github.com/Rob--W/cors-anywhere/
Documentation :   https://github.com/Rob--W/cors-anywhere/#documentation

失敗してるとこんな表示になります。
image.png

このアプリのURLはあとから使うのでメモっておいてください。

OpenAIのAPI keyを作成する

ここで重要なのが

  • ChatGPTとAPIの料金形態は別
  • APIKeyの作成は無料で可能
  • APIKeyの無料期間はOpenAIのなんらかのアカウント登録から3か月
    の三点になります。
    無料で三か月ってのがまさかChatGPTのアカウント登録の時期からと思ってなくて
    気づくのにえらい時間かかった...おとなしくAPI課金しました。
    ちなみに無料期間すぎてるのに無料でAPIにアクセスしようとすると
    上限超えてます的なこと言われるだけなのでマジで気づけない。

UnityWebGL上での呼び出し方

で、OpenAIのAPI KeyとHerokuアプリのURLを組み合わせて
以下のデモを作りました。

以下はOpenAIのDall-E3を使ったデモです。
適当なゲームオブジェクトに貼って使ってください。
UniTaskがちょっと挙動があやしかったのでコルーチン使ってます。
GameObjectとShaderは任意で追加してください

DalleAPIProxy.cs
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
using System.Collections.Generic;
using Newtonsoft.Json;

public class DalleAPIProxy : MonoBehaviour
{
    private string apiKey = "OpenAIのAPIキー";
    private string apiUrl = "https://api.openai.com/v1/images/generations";
    private string proxyUrl = "HerokuアプリのURL";

    //ここで参照したGameObjectにDall-E3からの画像を張り付ける。
    [SerializeField] private GameObject cube;

    public void GenerateImage()
    {
        //ねこちゃんが3匹戯れるのをプロンプトにしたもの。ここを変えれば別の画像生成になります。
        string prompt = "Three adorable kittens playing together with a ball of yarn, in a cozy living room with sunlight streaming in through the window.";
        StartCoroutine(SendPostRequest(prompt));
    }

    private IEnumerator SendPostRequest(string prompt)
    {
        var requestBody = new Dictionary<string, object>
        {
            { "prompt", prompt },
            { "n", 1 },
            { "size", "512x512" }
        };

        string jsonBody = JsonConvert.SerializeObject(requestBody);
        byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonBody);

        UnityWebRequest webRequest = new UnityWebRequest(proxyUrl + apiUrl, "POST");
        webRequest.uploadHandler = new UploadHandlerRaw(bodyRaw);
        webRequest.downloadHandler = new DownloadHandlerBuffer();
        webRequest.SetRequestHeader("Content-Type", "application/json");
        webRequest.SetRequestHeader("Authorization", "Bearer " + apiKey);

        yield return webRequest.SendWebRequest();

        if (webRequest.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError("Error: " + webRequest.error);
            Debug.LogError("Response: " + webRequest.downloadHandler.text);
        }
        else
        {
            var jsonResponse = webRequest.downloadHandler.text;
            DalleResponse response = JsonConvert.DeserializeObject<DalleResponse>(jsonResponse);
            string imageUrl = response.data[0].url;
            Debug.Log("Image URL: " + imageUrl);

            StartCoroutine(DownloadImage(imageUrl));
        }
    }

    private IEnumerator DownloadImage(string imageUrl)
    {
        UnityWebRequest webRequest = UnityWebRequestTexture.GetTexture(proxyUrl + imageUrl);

        yield return webRequest.SendWebRequest();

        if (webRequest.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError("Error downloading image: " + webRequest.error);
        }
        else
        {
            Texture2D texture = ((DownloadHandlerTexture)webRequest.downloadHandler).texture;
            ApplyTextureToCube(texture);
        }
    }

    private void ApplyTextureToCube(Texture2D texture)
    {
        Renderer renderer = cube.GetComponent<Renderer>();
        if (renderer != null)
        {
            Material material = new Material(Shader.Find("任意のShader名"));
            material.mainTexture = texture;
            material.mainTextureScale = new Vector2(4, 4);//タイル配置する時の分割数
            renderer.material = material;
        }
    }

    [System.Serializable]
    public class DalleResponse
    {
        public List<Data> data;
    }

    [System.Serializable]
    public class Data
    {
        public string url;
    }
}

といった感じでUnityWebGLでも全然API叩けるようにできる
という記事でした。

できたけどHerokuまだあんまよくわかってない。昨日よりはだいぶわかったけど。
結局ChatGPTに頼りきっちゃダメで何のこと言ってるか
自分で理解して推測してテストしないとダメですね。
使い手が理解してないとなんにもならない。

個人的にUnityで公式がモバイルWebGL対応するようになった
ことに可能性感じてるのでデモガンガン作りたい。

気になった人はレッツトライ!

しかし自分ひとりでやるの限界ありますね。
聞ける人がいない。
もしあなたの開発で出てくる問題もオレに声かけてくれたら一緒に考えられるかも。

あと引き続きWebGL系の仕事を募集しています。
仕事決まったら募集してること消します。

おまけ

アプリとEditor用のソース

DalleAPI.cs
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
using System.Collections.Generic;
using Newtonsoft.Json;

public class DalleAPI : MonoBehaviour
{
    private string apiKey = "OpenAIのAPIKey";
    private string apiUrl = "https://api.openai.com/v1/images/generations";

    //ここで参照したGameObjectにDall-E3からの画像を張り付ける。
    [SerializeField] private GameObject cube;

    public void GenerateImage()
    {
        string prompt = "Three adorable kittens playing together with a ball of yarn, in a cozy living room with sunlight streaming in through the window.";
        StartCoroutine(SendPostRequest(prompt));
    }

    private IEnumerator SendPostRequest(string prompt)
    {
        var requestBody = new Dictionary<string, object>
        {
            { "prompt", prompt },
            { "n", 1 },
            { "size", "512x512" }
        };

        string jsonBody = JsonConvert.SerializeObject(requestBody);
        byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonBody);

        UnityWebRequest webRequest = new UnityWebRequest(apiUrl, "POST");
        webRequest.uploadHandler = new UploadHandlerRaw(bodyRaw);
        webRequest.downloadHandler = new DownloadHandlerBuffer();
        webRequest.SetRequestHeader("Content-Type", "application/json");
        webRequest.SetRequestHeader("Authorization", "Bearer " + apiKey);

        yield return webRequest.SendWebRequest();

        if (webRequest.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError("Error: " + webRequest.error);
            Debug.LogError("Response: " + webRequest.downloadHandler.text);
        }
        else
        {
            var jsonResponse = webRequest.downloadHandler.text;
            DalleResponse response = JsonConvert.DeserializeObject<DalleResponse>(jsonResponse);
            string imageUrl = response.data[0].url;
            Debug.Log("Image URL: " + imageUrl);

            StartCoroutine(DownloadImage(imageUrl));
        }
    }

    private IEnumerator DownloadImage(string imageUrl)
    {
        UnityWebRequest webRequest = UnityWebRequestTexture.GetTexture(imageUrl);

        yield return webRequest.SendWebRequest();

        if (webRequest.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError("Error downloading image: " + webRequest.error);
        }
        else
        {
            Texture2D texture = ((DownloadHandlerTexture)webRequest.downloadHandler).texture;
            ApplyTextureToCube(texture);
        }
    }

    private void ApplyTextureToCube(Texture2D texture)
    {
        Renderer renderer = cube.GetComponent<Renderer>();
        if (renderer != null)
        {
            Material material = new Material(Shader.Find("任意のShader"));
            material.mainTexture = texture;
            material.mainTextureScale = new Vector2(4, 4);
            renderer.material = material;
        }
    }

    [System.Serializable]
    public class DalleResponse
    {
        public List<Data> data;
    }

    [System.Serializable]
    public class Data
    {
        public string url;
    }
}

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1