ゴマちゃんフロンティア

アザラシが大好きな山梨県産SEの開発日記です

【Unity】AWS Lambdaを使用してWebGLからS3に画像をアップロードしたお話

time 2019/11/12

今回はUnityの「WebGLからS3に画像をアップロードしたい!」なお話です。
例えばTwitter投稿機能を実装している場合、そのツイートに画像を載せたりしたいですよね。そんな思いが私にもあり、なるべく外部の画像系サービスを使わずに実現したかった次第です。

しかしUnity向けのAWS公式SDKがWebGL未対応(2019年11月現在)なので、ブラウザ上から直接S3にアップロードするのは難しいです。
そこでAWSの「Lambda」と「API Gateway」を使い、「WebGLからAPI Gatewayへリクエスト→LambdaからS3に画像アップロード」という流れで実現してみました。

Lambda関数の作成

「AWS Toolkit for Visual Studio」のインストールとプロジェクトの作成

まずは「AWS Toolkit for Visual Studio」をインストールします。ダウンロードはAWS公式から。
https://aws.amazon.com/jp/visualstudio/

VisualStudioを起動し、Lambda用にUnityとは別で新規にプロジェクトを作成します。
「新しいプロジェクトの作成」に「AWS Lambda Project」が増えているので、こちらを選択します。

テンプレートはなにから作っても構いませんが、初めてであればS3のものを使用するのが楽かと思います。

Nugetパッケージの追加

ソリューションエクスプローラーからプロジェクトを右クリックし、「NuGetパッケージの管理」を開きます。

デフォルトで入っているS3とLambdaに加えて、「Amazon.Lambda.APIGatewayEvents」が必要になります。

コードの実装

APIへは画像をbase64エンコードしたものをパラメータとして送信します。

using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using Amazon.S3;
using Amazon.S3.Model;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;

[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]

namespace UnityTwitterImageCommand_Upload
{
    public class Function
    {
        IAmazonS3 S3Client { get; set; }

        protected const string S3_BUCKET_NAME = "unity-screen-shot-test";

        public Function()
        {
            S3Client = new AmazonS3Client(Amazon.RegionEndpoint.APNortheast1);
        }

        public Function(IAmazonS3 s3Client)
        {
            this.S3Client = s3Client;
        }

        public async Task<APIGatewayProxyResponse> FunctionHandler(LambdaRequest lambdaRequest, ILambdaContext context)
        {
            var request = JsonConvert.DeserializeObject<UnityTwitterImageRequest>(lambdaRequest.Body);

            try {
                // アップロード画像のStream用意
                string encodedImage = request.encodedImage;
                encodedImage = encodedImage.Replace('-', '+').Replace('_', '/').PadRight(4 * ((encodedImage.Length + 3) / 4), '=');
                byte[] imageByte = Convert.FromBase64String(encodedImage);
                var stream = new MemoryStream(imageByte);
                stream.Write(imageByte, 0, imageByte.Length);

                // 画像アップロード
                var imageObjectRequest = new PutObjectRequest()
                {
                    BucketName = S3_BUCKET_NAME,
                    Key = "test.png",
                    ContentType = "image/png",
                    InputStream = stream,
                    CannedACL = S3CannedACL.PublicRead
                };

                await S3Client.PutObjectAsync(imageObjectRequest);

                var response = new APIGatewayProxyResponse
                {
                    StatusCode = (int)HttpStatusCode.OK,
                    Body = JsonConvert.SerializeObject(new UnityTwitterImageResponse()
                    {
                        result = "Upload Executed"
                    }),
                    Headers = new Dictionary<string, string>() { { "Access-Control-Allow-Origin", "*" } }
                };

                return response;
            } catch (Exception e) {
                context.Logger.LogLine(e.Message);
                context.Logger.LogLine(e.StackTrace);
                throw;
            }
        }
    }

    public class LambdaRequest
    {
        [JsonProperty(PropertyName = "body")]
        public string Body { get; set; }
    }

    public class UnityTwitterImageRequest
    {
        public string encodedImage;
    }

    public class UnityTwitterImageResponse
    {
        public string result;
    }
}

ポイントは「エンコードされた画像のデコード」で、文字列をそのままFromBase64String()しても、以下のようなエラーが発生してしまいます。

The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters

このあたりの解決方法はstackoverflowに載っていました。回答通りに文字列を加工してからデコードします。
https://stackoverflow.com/questions/50524665/convert-frombase64string-throws-invalid-base-64-string-error

ビルドとアップロード準備

まずはLambdaのコマンドをインストールするため、コマンドプロンプトで以下のコマンドを実行します。これは初回だけでOKです。

dotnet tool install -g Amazon.Lambda.Tools

次にコマンドプロンプト上でプロジェクトフォルダの直下に移動し、以下のコマンドを実行。

`dotnet lambda package --configuration Release --framework netcoreapp2.1`

するとプロジェクトフォルダの¥bin¥Reselase¥の下にzipファイルができあがります。これを後ほどLambdaのマネジメントコンソールからアップロードします。

AWS側の作業

S3のバケット作成

AWSマネジメントコンソールからS3のバケットを作成します。バケット名はお好みでどうぞ。
何も指定せずに作るとパブリックアクセスがブロックされてしまうので、バケットのアクセス権限タブからブロックを解除します。

その他の設定はとりあえず弄らなくてもいけるはず。

LambdaでS3アップロード用の関数の作成

サービス一覧から「Lambda」を開いて新しく関数を作成します。
ランタイムは「.NET Core 2.1 (C#/PowerShell)」を指定しましょう。

S3にアップロードするので、S3に対する権限が必要です。
「既存ロール」プルダウン下のロールをクリックするとIAMのページに遷移するので、S3のフルアクセス権限を付与します。
(記事作成の関係で、画像だとロール名と関数名が合っていません。ご了承ください。)

次に「ハンドラ」欄で最初に実行されるメソッドを指定します。私の場合、先ほど作ったLambda用プロジェクトに合わせて「UnityImageUploadTest::UnityImageUploadTest.Function::FunctionHandler」と入力しました。

何を入力すればいいか分からない場合、Lambda関数用プロジェクトの「aws-lambda-tools-defaults.json」を見ると、「function-handler」のところに書かれています。

最後にソースコードのアップロードです。ビルドして作成したzipファイルをアップロードします。

ここまで設定し終わったら、画面右上の「保存」ボタンで保存することを忘れずに。

API Gatewayでリクエスト先の作成

続いて「API Gateway」を開き、新しいAPIを作成します。
こちらのAPI名やメソッド名は適当に。後々自分が見て分かるものであれば問題ないかと思います。

適当にリソースを作り、その中にPOSTメソッドを作成します。作成したPOSTをクリックし、「統合リクエスト」で先ほど作成したLambdaの関数を指定します。
また「Lambda統合プロキシの使用」にチェックを入れましょう。

unityroomさんなど、外部のサービスでゲームを公開する場合はCORSに引っ掛からないように設定する必要があります。作成したリソースをクリックし、「アクション→CORSの有効化」を開きます。

設定はそのままでも良いかと思います。公開するドメインが限定されている場合はAccess-Control-Allow-Originを書き換えるとなお良いかと
設定し終わったら一度デプロイしてしまいましょう。ステージ名はこれまたお好みで。

Unity側の実装

リクエスト用メソッドの作成

UnityからHTTPリクエストを送信する仕組みは以下の記事の「Unity側の実装」をご参照ください。

【Unity】ランキング機能を自作してみたお話

しかしUnityWebRequestはデフォルトでapplication/x-www-form-urlencodedとして送信してしまうので、HttpServiceに新しくJSONで投げるメソッドを作成しました。

public static IEnumerator PostJson<T>(string url, string json)
{
    var request = new UnityWebRequest(url, "POST");
    byte[] bodyRaw = Encoding.UTF8.GetBytes(json);
    request.uploadHandler = new UploadHandlerRaw(bodyRaw);
    request.downloadHandler = new DownloadHandlerBuffer();
    request.SetRequestHeader("Content-Type", "application/json");

    // リクエスト送信
    yield return request.SendWebRequest();

    // エラー判定
    if (request.isHttpError || request.isNetworkError) {
        throw new Exception("通信に失敗しました。(" + request.error + ")");
    }

    yield return JsonUtility.FromJson<T>(request.downloadHandler.text);
}

API Gatewayへのリクエスト処理の実装

WebGL上で画面キャプチャを取得する方法は、以下の記事をご参照ください。

【Unity】WebGLでゲーム画面のキャプチャ(スクリーンショット)を取得する方法

以下のコードでは例として、Start()内で画面キャプチャを取得し、そのままAPI Gatewayへリクエストを投げています。

using System;
using System.Collections;
using System.Runtime.InteropServices;
using UnityEngine;

public class UploadImageTest : MonoBehaviour
{
    [DllImport("__Internal")]
    private static extern void OpenTweetWindow(string text, string hashtags, string url);

    protected Texture2D currentScreenShotTexture;

    void Start()
    {
        // スクリーンショット用のTexture2D用意
        currentScreenShotTexture = new Texture2D(Screen.width, Screen.height);

        StartCoroutine(UpdateCurrentScreenShot());
    }

    protected IEnumerator UpdateCurrentScreenShot()
    {
        // これがないとReadPixels()でエラーになる
        yield return new WaitForEndOfFrame();

        currentScreenShotTexture.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);
        currentScreenShotTexture.Apply();

        StartCoroutine(UploadTweetImage());
    }

    public IEnumerator UploadTweetImage()
    {
        byte[] imageBytes = currentScreenShotTexture.EncodeToPNG();

        var requestUploadTweetImage = new RequestUploadTweetImage()
        {
            encodedImage = Convert.ToBase64String(imageBytes)
        };

        // サーバへ登録リクエスト送信
        var httpPost = HttpService.PostJson<ResponseUploadTweetImage>(
            "API GatewayのURL",
            JsonUtility.ToJson(requestUploadTweetImage)
        );

        yield return StartCoroutine(httpPost);
        var responseUploadTweetImage = httpPost.Current as ResponseUploadTweetImage;
        Debug.Log(responseUploadTweetImage.result);
    }
}

[Serializable]
public class RequestUploadTweetImage
{
    public string encodedImage;
}

public class ResponseUploadTweetImage
{
    public string result;
}

上手くいくとS3にファイルが追加されているはずです。

オブジェクトURLからファイルにアクセスできることを確認してみましょう。
慣れてきたらRequestUploadTweetImageを拡張し、ファイル名やアップロード先バケットも変えられるようにすると、様々な場面で使いやすくなるのでおすすめです。

あとがき

そんなわけで、WebGLからS3に画像をアップロードする方法を考えてみました!
WebGL単体ではできないことも、他の仕組みを組み合わせることで擬似的に実現できる可能性があります。webの楽しさってこういうところかもしれないですね。

down

コメントする



ツイッター

このブログを書いている人

りべるん

りべるん

アザラシが大好きな山梨県産Webエンジニア。2019年3月よりフリーランスSEとして東京で活動中。自称「Web業界のアザラシ」 [詳細]