LoginSignup
5
4

More than 5 years have passed since last update.

Unityで3DモデルライブラリPolyからランタイムインポート

Last updated at Posted at 2019-04-15

Polyはすごい

Poly
Poly Developpers

Polyはさまざまな3Dモデルや360°写真1をCCライセンスで提供しているオンラインライブラリです。
「無料ですぐ使えるモデル置いとくよ! みんなでAR/VRコンテンツを開発していこうね!」というGoogle様のお慈悲です。
詳しいことは公式とかその周辺記事を漁ってみてください。

Poly API を使ってみる
[Unity] GooglePolyを使ってローポリ3Dオブジェクトを検索/配置する
Poly toolkit for unity使った。

Poly Toolkit for Unity

Poly Toolkit for Unity

エディタでロードしている記事はわりとあったんですが、ランタイムロードしている日本語の記事が見つからなかったので使った感触を書きます。2019/04/14時点の情報を元に書いてます。

Unity QuickStart

Unity QuickStart
クイックスタートに従って進めれば問題ありません。スクリーンショットつきの丁寧な説明なのですんなり行けると思います。
Troubleshootingのところにobsolete APIsを自動アップデートしてね!とか書いてありますが、これに驚いているようではGoogleのProductを使う「資格」はない。2

API

Polyを使うすべてのシーンに置いておけと書いてあるPolyToolkitManagerPrefabですが、PolyApi.Init();Awakeで実行する以外何もしてない……。
いずれ機能が追加されるかもしれないですし、置いとけって書いてあるんだから置いておきましょう。

おそらく公式が想定している手順であろう以下をコードでやっていきます。ついでにCallbackでつらいのでTaskに変換します。ログイン周りはめんどくさいのでパス。

  1. モデルの基礎情報を取得する
  2. サムネイルを表示する
  3. モデルをGameObjectとしてロード
  4. 権利表記

モデルの基礎情報を取得する

検索についての共通

PolyRequestを継承した検索条件のstructを各検索APIに渡します。
検索結果のPolyListAssetsResult.nextPageTokenを渡せば次が検索できるので、検索条件はどこかの変数にとっておいて使いまわしましょう。

PolyApi.ListAssets

普通の検索です。普通。

ListAssets
public static Task<PolyListAssetsResult> SearchListAssetsAsync(PolyListAssetsRequest assetsRequest)
{
    var tcs = new TaskCompletionSource<PolyListAssetsResult>();
    PolyApi.ListAssets(assetsRequest, result =>
    {
        if (result.Ok)
        {
            tcs.SetResult(result.Value);
        }
        else
        {
            tcs.SetException(new Exception(result.Status.errorMessage));
        }
    });
    return tcs.Task;
}
PolyApi.ListUserAssets

ログインが前提。ユーザが投稿したモデル一覧が検索されます。

public static Task<PolyListAssetsResult> SearchUserListAssetsAsync(PolyListUserAssetsRequest assetsRequest)
{
    var tcs = new TaskCompletionSource<PolyListAssetsResult>();
    PolyApi.ListUserAssets(assetsRequest, result =>
    {
        if (result.Ok)
        {
            tcs.SetResult(result.Value);
        }
        else
        {
            tcs.SetException(new Exception(result.Status.errorMessage));
        }
    });
    return tcs.Task;
}
PolyApi.ListLikedAssets

ログイン中のユーザがお気に入りしたモデル一覧です。お気に入りはブラウザのサイトからできます。

public static Task<PolyListAssetsResult> SearchLikedListAssetsAsync(PolyListLikedAssetsRequest assetsRequest)
{
    var tcs = new TaskCompletionSource<PolyListAssetsResult>();
    PolyApi.ListLikedAssets(assetsRequest, result =>
    {
        if (result.Ok)
        {
            tcs.SetResult(result.Value);
        }
        else
        {
            tcs.SetException(new Exception(result.Status.errorMessage));
        }
    });
    return tcs.Task;
}
PolyApi.GetAsset

使うモデルがあらかじめわかっている場合はこちらを使います。ブラウザで見たときの末尾の文字列です。
サンプルやAPIから返ってくる識別子にはassets/がついていますが、別になくても動きます。それでいいのか。

public static Task<PolyAsset> GetAssetAsync(string name)
{
    var tcs = new TaskCompletionSource<PolyAsset>();

    PolyApi.GetAsset(name, result =>
    {
        if (result.Ok)
        {
            tcs.SetResult(result.Value);
        }
        else
        {
            tcs.SetException(new Exception(result.Status.errorMessage));
        }
    });

    return tcs.Task;
}

サムネイルを表示する

PolyApi.FetchThumbnail

サムネイルを取得します。これをしないとPolyAsset.thumbnailTextureはnullのままです。
中ではPolyAsset.thumbnail.urlを読み込んでるだけです。とはいえPolyFetchThumbnailOptionsによるサイズ調整とかダウンロード処理のキューイングとかしてくれてるので使っておきましょう。

public static Task<PolyAsset> FetchThumbnailAsync(PolyAsset asset, PolyFetchThumbnailOptions options)
{
    var tcs = new TaskCompletionSource<PolyAsset>();
    PolyApi.FetchThumbnail(asset, options, (polyAsset, status) =>
    {
        if (status.ok)
        {
            tcs.SetResult(polyAsset);
        }
        else
        {
            tcs.SetException(new Exception(status.errorMessage));
        }
    });

    return tcs.Task;
}

モデルをGameObjectとしてロード

PolyApi.Import

ようやくモデルの読み込みです。PolyImportOptionsの各パラメータの意味はエディタ上でインポートするときの表示と突き合わせると理解しやすいです。Scaleをちゃんと調整しておかないとめっちゃでかいやつが出てきます。

public static Task<(GameObject obj, PolyAsset polyAsset)> ImportAsync(PolyAsset asset, PolyImportOptions? options = null)
{
    var tcs = new TaskCompletionSource<(GameObject obj, PolyAsset polyAsset)>();

    PolyApi.Import(asset, options ?? PolyImportOptions.Default(), (polyAsset, result) =>
    {
        if (result.Ok)
        {
            tcs.SetResult((result.Value.gameObject, polyAsset));
        }
        else
        {
            tcs.SetException(new Exception(result.Status.errorMessage));
        }
    });
    return tcs.Task;
}

権利表記

PolyApi.GenerateAttributionsはEditor上でインポートしておいたモデルの権利表記に加えてランタイムロードしたPolyAssetListを渡せば全部まとめて権利表記文を書き出してくれるイカしたAPIです。
引数次第でどちらかだけ出す、なんてこともできます。
ただし第二引数List<PolyAsset> runtimeAssetsのデフォルトはnullですが中でnullチェックしないで触ってます。第二引数なしで蹴ると必ずぬるぽになるので適当にnewしたリストでも入れてあげましょう。ははは。3

ちなみにひとつだけ知りたい、という場合はPolyAsset.AttributionInfoで取れます。

実践

「人気順にAnimalカテゴリからdogを検索して一番目のモデルをロードして権利表記も表示する」
メソッドをStartで実行してみます。

private async void SearchAndShowExample()
{
    var searchQuery = PolyListAssetsRequest.Featured();
    // カテゴリ:動物
    searchQuery.category = PolyCategory.ANIMALS;
    // 犬を検索        
    searchQuery.keywords = "dog";
    searchQuery.pageSize = 5;

    var search = await PolyTasker.SearchListAssetsAsync(searchQuery);

    var (obj, asset) = await PolyTasker.ImportAsync(search.assets[0], PolyImportOptions.Default());
    _text.text = asset.AttributionInfo;
}

スクリーンショット 2019-04-16 5.11.50.png

出ました! 狐が。4
権利表記もちゃんと読み込めています。

Task化

いままでのtask化したものをまとめてwrapper classにしました。使ってください。
gistはこちら

using System;
using System.Threading.Tasks;
using PolyToolkit;
using UnityEngine;

namespace Daimao.Nekomimi.Poly
{
    public static class PolyTasker
    {
        public static Task<PolyListAssetsResult> SearchListAssetsAsync(PolyListAssetsRequest assetsRequest)
        {
            var tcs = new TaskCompletionSource<PolyListAssetsResult>();
            PolyApi.ListAssets(assetsRequest, result =>
            {
                if (result.Ok)
                {
                    tcs.SetResult(result.Value);
                }
                else
                {
                    tcs.SetException(new Exception(result.Status.errorMessage));
                }
            });
            return tcs.Task;
        }

        public static Task<PolyListAssetsResult> SearchUserListAssetsAsync(PolyListUserAssetsRequest assetsRequest)
        {
            var tcs = new TaskCompletionSource<PolyListAssetsResult>();
            PolyApi.ListUserAssets(assetsRequest, result =>
            {
                if (result.Ok)
                {
                    tcs.SetResult(result.Value);
                }
                else
                {
                    tcs.SetException(new Exception(result.Status.errorMessage));
                }
            });
            return tcs.Task;
        }


        public static Task<PolyListAssetsResult> SearchLikedListAssetsAsync(PolyListLikedAssetsRequest assetsRequest)
        {
            var tcs = new TaskCompletionSource<PolyListAssetsResult>();
            PolyApi.ListLikedAssets(assetsRequest, result =>
            {
                if (result.Ok)
                {
                    tcs.SetResult(result.Value);
                }
                else
                {
                    tcs.SetException(new Exception(result.Status.errorMessage));
                }
            });
            return tcs.Task;
        }

        public static Task<PolyAsset> FetchThumbnailAsync(PolyAsset asset, PolyFetchThumbnailOptions options)
        {
            var tcs = new TaskCompletionSource<PolyAsset>();
            PolyApi.FetchThumbnail(asset, options, (polyAsset, status) =>
            {
                if (status.ok)
                {
                    tcs.SetResult(polyAsset);
                }
                else
                {
                    tcs.SetException(new Exception(status.errorMessage));
                }
            });

            return tcs.Task;
        }

        public static Task<PolyAsset> GetAssetAsync(string name)
        {
            var tcs = new TaskCompletionSource<PolyAsset>();
            PolyApi.GetAsset(name, result =>
            {
                if (result.Ok)
                {
                    tcs.SetResult(result.Value);
                }
                else
                {
                    tcs.SetException(new Exception(result.Status.errorMessage));
                }
            });

            return tcs.Task;
        }


        public static Task<(GameObject obj, PolyAsset polyAsset)> ImportAsync(PolyAsset asset, PolyImportOptions? options = null)
        {
            var tcs = new TaskCompletionSource<(GameObject obj, PolyAsset polyAsset)>();
            PolyApi.Import(asset, options ?? PolyImportOptions.Default(), (polyAsset, result) =>
            {
                if (result.Ok)
                {
                    tcs.SetResult((result.Value.gameObject, polyAsset));
                }
                else
                {
                    tcs.SetException(new Exception(result.Status.errorMessage));
                }
            });
            return tcs.Task;
        }
    }
}

気になる点

  • Android, iOSではcacheできない
    https://github.com/googlevr/poly-toolkit-unity/issues/9
    できなくはないらしい。
    ていうかGoogle様!!!!!!これ主にARCoreとかARKitで使ってほしいやつですよね!!!?キャッシュはモバイルにこそ必要!!!!!なんでそこサポートしてないの!!?!?!?!!?!?!

  • delegate使ってる
    delegateをどうやったらUniRxで扱えるかわからなかったのと、なんかdelegateは古くてダサいらしいからActionとか使ったほうが……と思ったんですがおそろしく深いところまでcallback渡しに行ってるのでそう簡単に手を入れられない。

  • API制限
    PolyもGCPの一部なので呼び出し回数に制限があります。おそらくPoly Toolkit for UnityPoly APIをラップしただけのもののはずなので3000 queries-per-minute (QPM)が適応されるはず。でも「もっと!」っておねだりはできるっぽい。

まとめ

3Dモデル関連はぜんぜん詳しくないのですが、競合サービスと比べてランタイムロードできるのはけっこうな強みのような気がします。でもあんま更新されてなくてGoogle様これ飽きたのでは……?
わりと普通に勝てるサービスだと思うのでもっと頑張ってほしいな!

おしまい。

おまけ

見てて面白かったモデル載せます。見てる感じえろいのは流石にフィルタリングしてるっぽいですが、後は著作権とかない無法の沃野と化してます。5
公式動画ですら1:00あたりにやべーやつが大地に立ってるしな!
私的にこういうLowPolyなモデル、特にGoogle様公式のモデルがほんと好きです。大好き。6
iframeの埋め込みが動かないっぽいのでリンクから見てください。

Doggo DayDream
かわいい。

Godzilla
手でコントローラ持ってるのがチャームポイント。

Roadtrip
かわいい。アニメーションしてる。

Ninja Droid WIP
ニンジャナンデ? カワイイヤッター!

Corgi-saurus Rex
見えなくはない。

Turkey Attack!
Attack!


  1. 360写真の方に家族写真投稿してるやついてめっちゃおもしろい。 

  2. いつものことです。 

  3. 殺意すら感じる。 

  4. ナンデ? 

  5. 往時のPlayStoreを思い出します。今もわりと無法ですが。 

  6. Donut Countyとか大好き。 

5
4
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
5
4