simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

React x Typescript で index.html を独自ドメインで世界公開

まえがき

Unity Asset Store に例の

f:id:simplestar_tech:20200209162015j:plain
例のアセット
を提出したんだけど、リジェクトされてしまった。

どうも初回投稿なら、技術力を見せるサイトを持ってないとダメな様子
仕方なく、作ることにします。
とりあえず index.html にアセットの画像がごろごろあればいいんかな

Node 環境を整える

Web サイトか…

今なら React x Typescript かな?

ということで node パッケージマネージャーを使えるようにしましょう。
Windows 環境はインストーラから入れるなど
nodejs.org

React x Typescript のサンプルをローカルで作る

こちらの技術書を読んで、サンプルを動かしながらコードを理解してみる
oukayuka.booth.pm

typescript で、見ただけでデータの動きがアニメで見えることが大事
以下のような単語で絵が動けばいい

分割代入
スプレッド演算子

読み切れば Redux + Saga の概念の理解と関数コンポーネントによる React の具体的実装がわかる

独自ドメインで世界に公開する

https://unityassets.elastprism.com/

具体的な手順はこちら
qiita.com

1.yarn build でできた build フォルダの中身を S3 にアップロード
2.Cloudfront でディストリビューションを作って、データオリジンに S3 を選ぶ
参考記事
simplestar-tech.hatenablog.com
3.バージニア北部にドメイン指定で証明書を作る
4.Route53で独自ドメインからCloudfrontへのAlias作る

以上の手順で世界からの独自ドメインリクエストするとCloudfrontへルートができて、各エッジサーバーからReact の SPA のアクセスが有効になる

肝となるのが、上記の Qiita の紹介にあるとおり Cloudfront の Custom Error Response で /index.html を 403 や 404 のときに返すようにする
これで、どの Route 先ページでも F5 でリロードしても S3 アクセス拒否という 403 エラーは返ってこない

そういえば昔ホームページの作り方を書いたことあったけど5年もすると Cloudfront で SPA を配信できるようになる(腕を上げたな)
simplestar-tech.hatenablog.com

ところでアセット紹介ページがまだぜんぜん形になってないのです。

追記:

単純に yarn build の成果物を公開すると .map ファイルが同梱されるためソース内容(ts, tsx)実装が同時に公開されてしまいます。
.env ファイルを直下に置いて
GENERATE_SOURCEMAP=false
を書き込んでから yarn build することで without sourcemap で build でき、それを公開した場合は実装がばれずに公開することができました。

情報元はこちら
qiita.com

CubeWalk: go言語cacheサーバーのリージョン選定

前書き

先月、こちらに登壇して PlayFab の CloudScript で同時実行を回避しつつ、不正させないゲームのためのアイディアを語ってみたのですが
jpfug.connpass.com
質問者から CloudScript が実行されている場所って US West オレゴンだから、日本にキャッシュサーバー置くと
結果的に情報路が長くなって、片道 1.3万キロメートルだから、理論上通信に最小 0.1 秒ほどの不要な遅れが足されて
あと日本でサーバー立てる方がランニングコストも通信費も高いですよね

とご指摘いただいたのです。

なるほど、go言語キャッシュサーバーを US West オレゴンに置いて、そこにアクセスする CloudScript も用意して
実際の通信速度に差が 0.1 秒ほど出るのか試してみますね!

やったこと

以前行った時のログをたよりにドキュメントを読み直し 東京リージョンと同じ構成でオレゴンリージョンにも同じ golang キャッシュサーバーを構築します。
simplestar-tech.hatenablog.com

まず、自宅(日本、東京)の pc からリクエストをそれぞれの golang サーバーに送ってヘルスチェックレスポンスが返ってくるまでの時間を計測

東京リージョンは 110~180 ms を要してました。
オレゴンリージョンは 610~680 ms を要してました。

この、それぞれのサーバーにヘルスチェックリクエストをする PlayFab CloudScript を javascript で実装し
自宅(日本、東京)の pc 上で実行する Unity クライアントから、CloudScript を呼び出してヘルスチェックレスポンスが返ってくるまでの時間を計測しました。

東京リージョンは 650~700 ms を要してました。
オレゴンリージョンは 220~300 ms を要してました。

結果

日本・東京の Unity クライアントから、PlayFab CloudScript を呼び出して golang のキャッシュサーバーのレスポンスを確認して Unity に戻ってくるまでの時間を計測しました。
サーバーの場所:
東京リージョンは 650~700 ms を要してました。
オレゴンリージョンは 220~300 ms を要してました。

PlayFab の PlayStream には、CloudScript 呼び出しの詳細が json で記録されますが
東京リージョンに置いた golang キャッシュサーバーにヘルスチェックを返してもらうのに "ExecutionTimeSeconds": 0.5307192
オレゴンリージョンに置いた golang キャッシュサーバーにヘルスチェックを返してもらうのに "ExecutionTimeSeconds": 0.0244125

PlayFab のスタジオのリージョンを日本にできればいいんだけどね…オレゴンにあるのは間違いない
今は東京リージョンにキャッシュサーバーを置くのは間違い確定なので、しばらくオレゴンリージョンに置いて作業を進めます。

Unity: SteamVR 2.0 の入力処理の入門記事

前書き

こちらの記事の和訳だと思ってください
medium.com

VR コントローラの入力について、入門記事
アクションという概念を取り入れ、アクションを定義するところから始まります。

アクション単体では作れない

アクションを束ねた、アクションセットを作成するところから始まります。
最初はいくつものアクションが定義された default アクションセットが選択されている

default のほかに platformer, buggy, mixedreality のアクションセットが確認できる

アクションセットを作る

Window > Steam VR Input でダイアログを表示し
既存の default, platformer, buggy, misedreality のボタンの右側に + ボタンあるので押す

NewSet で作成、何か一つ In Actions に NewAction を追加 boolean Type とする

Save and generate ボタンを押し、完了を待つ

BooleanAction を参照してイベントハンドラを登録

using UnityEngine;
using Valve.VR;
public class MyActionScript : MonoBehaviour
{
    // a reference to the action
    public SteamVR_Action_Boolean sphereOnOff;
    // a reference to the hand
    public SteamVR_Input_Sources handType;
    //reference to the sphere
    public GameObject sphere;

    void Start()
    {
        this.sphereOnOff.AddOnStateDownListener(this.TriggerDown, handType);
        this.sphereOnOff.AddOnStateUpListener(this.TriggerUp, handType);
    }

    private void TriggerDown(SteamVR_Action_Boolean fromAction, SteamVR_Input_Sources fromSource)
    {
        Debug.Log("Trigger is down");
        this.sphere.GetComponent<MeshRenderer>().enabled = true;
    }

    private void TriggerUp(SteamVR_Action_Boolean fromAction, SteamVR_Input_Sources fromSource)
    {
        Debug.Log("Trigger is up");
        this.sphere.GetComponent<MeshRenderer>().enabled = false;
    }
}

ということ

f:id:simplestar_tech:20200217230014p:plain
エディタから参照!

Activate Action Set しないといけない

自分で作ったアクションセットはゲーム開始時にアクティブではない
アクティブになるように明示

f:id:simplestar_tech:20200217230122p:plain
なんでだよ

先ほどの MyActionScript より前に実行される位置にシーンに配置しないといけない制約もある

Open Binding UI

Window > Steam VR Input でダイアログを表示し、Open Binding UI ボタンを押す
newset という、さっき作ったアクションセットのタブが見える。
アクションが割り当たっていないことを示す表示かな

f:id:simplestar_tech:20200217230217p:plain
アクションが割り当たってないぞ

タブを選択して

f:id:simplestar_tech:20200217230554p:plain
トリガーボタンに boolean アクションを割り当て

はじめて動作することになる

まとめると、以下の一つが欠けると、何も反応ない結果になる

  • アクションセットを作る
  • アクションを作る
  • アクションハンドラを作る
  • アクションハンドラを登録するコードを書く
  • アクションを参照
  • 順番を守ったアクションアクティベート
  • 入力管理でアクションセットのアクションをコントローラのいずれかの操作にバインディング

Unity:VRでも使えるMToon互換のURPシェーダを30分で作ろう

前書き

Unity でゲーム作っている人なら多くの方が知っている VRM - VR向け3Dアバターファイルフォーマット -
その VRM の標準シェーダーとして採用されている MToon を URP でも表示できるようにしたい!

作りましょう!
そのための情報集めから作業をこの記事で公開します。

そのあと、公開しました。
github.com

MToon のパラメータ一覧

新しく ShaderGraph - Unlit を作成して公開パラメータとして次のものを作成します。

f:id:simplestar_tech:20200216160442p:plain
MToon の入力

光源情報を Custom Function ノードで取得

github.com
ここにあるサブグラフ一式と .hlsl シェーダー実装をコピーしておきます。いつでも使えるように
内容はほんと、単に光源情報取ってきているだけです。

f:id:simplestar_tech:20200216160717p:plain
サブグラフ一式

Toon シェーディング for ShaderGraph

次の動画の説明欄にあるリンクから Toon 表現の肝となる Node を見つけてコピーします。

ほしいのはこの Node です。

f:id:simplestar_tech:20200216161217p:plain
Toon 表現

アウトライン Shader

そして MToon で外せないのが Outline です。
Shader Graph で実現する方法が紹介されている記事がこちら

Outline Shader

Normal が Alpha テストできない問題に最終的に当たるので、もろもろ削除して、結局 Depth を書き出すプロジェクトで次の CustomFunction を呼ぶだけにします。

最終的に Outline サブシェーダーを作りますが、そこに登場する Custom Function は三つ

下図のとおり、左上のオフセットが

Out = unity_StereoEyeIndex;

左下の係数が

Out = 1.0;
#if UNITY_SINGLE_PASS_STEREO
Out = 0.5;
#endif

右下の Custom Function がこちら

TEXTURE2D(_CameraDepthTexture);
SAMPLER(sampler_CameraDepthTexture);
float4 _CameraDepthTexture_TexelSize;


void OutlineObject_float(float2 UV, float OutlineThickness, float DepthSensitivity, float NormalsSensitivity, out float Out)
{
    float halfScaleFloor = floor(OutlineThickness * 0.5);
    float halfScaleCeil = ceil(OutlineThickness * 0.5);
    
    float2 uvSamples[4];
    float depthSamples[4];

    uvSamples[0] = UV - float2(_CameraDepthTexture_TexelSize.x, _CameraDepthTexture_TexelSize.y) * halfScaleFloor;
    uvSamples[1] = UV + float2(_CameraDepthTexture_TexelSize.x, _CameraDepthTexture_TexelSize.y) * halfScaleCeil;
    uvSamples[2] = UV + float2(_CameraDepthTexture_TexelSize.x * halfScaleCeil, -_CameraDepthTexture_TexelSize.y * halfScaleFloor);
    uvSamples[3] = UV + float2(-_CameraDepthTexture_TexelSize.x * halfScaleFloor, _CameraDepthTexture_TexelSize.y * halfScaleCeil);

    for(int i = 0; i < 4 ; i++)
    {
        depthSamples[i] = SAMPLE_TEXTURE2D(_CameraDepthTexture, sampler_CameraDepthTexture, uvSamples[i]).r;
    }

    // Depth
    float depthFiniteDifference0 = depthSamples[1] - depthSamples[0];
    float depthFiniteDifference1 = depthSamples[3] - depthSamples[2];
    float edgeDepth = sqrt(pow(depthFiniteDifference0, 2) + pow(depthFiniteDifference1, 2)) * 100;
    float depthThreshold = (1/DepthSensitivity) * depthSamples[0];
    Out = edgeDepth > depthThreshold ? 1 : 0;
}

f:id:simplestar_tech:20200216162116p:plain
Outline for URP VR

あとはつなぐだけ

Toon 表現と Outline がそろっているので、接続して MToon の説明をたよりに編みます
MToon の心臓となる処理は Main と Shade のテクスチャを陰影で利用すること。
なので、Toon 表現で白黒を出力して、それを Lerp する処理が書けたらほぼ完成。自分はさらにライト色をもらうように Multiplyしてます。

f:id:simplestar_tech:20200216162748p:plain
心臓部 Lerp

MToon の Add を少々

最後に塩コショウのように Sphere Add と Emission を足すと

f:id:simplestar_tech:20200216163046p:plain
Add するだけ

完成!

f:id:simplestar_tech:20200216163358p:plain
MToon互換 URP VR 結果

VR の Single Pass Stereo で覗いてもアウトラインが引けていました。
そこがポイント

色々と動的読み込みしても、今のところ困った絵にはならない様子
みんなも試してみてね!

f:id:simplestar_tech:20200216163948p:plain
オニャンコポンなつのすがた
f:id:simplestar_tech:20200216164103p:plain
童田明治ファンアートモデル

Imagemagick:画像のエッジを引き延ばして格子状に再配置

前書き

キューブの見た目を更新する目的で動いています。

前回は Shader ができたところまで
simplestar-tech.hatenablog.com

今回はテクスチャを連番で結合する処理を作ります。
大分前に python の pillow で画像処理を書きましたが…
github.com
処理が遅すぎるのと
画像のエッジの引き延ばしを行っていなかったので、キューブ群を遠目で見ると結合部分が光ったり黒くなったり…とにかく見た目が良くない
その部分を今回は imagemagick を利用して解決していきます。

参考までに、昔のキューブの見た目は頑張ってこんなの
simplestar-tech.hatenablog.com

もう2年以上、技術選定を続けているという…今年は完成させるぞ!
simplestar-tech.hatenablog.com

Imagemagick の基本を習得

まずは Windows 環境で使えるように portable 版をダウンロードして展開し PATH を通しておきます。
imagemagick.org

以降、Windows 環境だと convert コマンドを magick にしなければバッティングする旨を imagemagick が教えてくれるので
magick コマンドを利用します。
そうしたことを念頭に次のマニュアルを完走して、imagemagick を身体になじませました。

imagemagick.biz

Imagemagick での画像のエッジ引き延ばし

情報ソースはこちら
www.imagemagick.org

細かいことは置いとくと、次の倍率を指定するだけで余白を引き延ばすことができます。
(残念ながら distort は基本操作には出てこない)

magick in.png -set option:distort:viewport %[fx:w*(1+0.05)]x%[fx:h*(1+0.05)] -virtual-pixel Edge -distort SRT "0,0 1,1 0 %[fx:w/(2/0.05)],%[fx:h/(2/0.05)]" out.png

ちょっとだけ解説すると
distort:vieport が left, top 引き延ばし幅に関与&全体の幅高さ倍率指定
virtual^pixel Edge が right, bottom の引き延ばし幅に関与していて、要するに 1 引いた倍率を 1/2 倍で指定というながれ

画像を変換してタイル状に並べる

変換した画像を利用するという小技について、まずはおさらい。
こちらの画像を入力に(昔、自分が描いた絵です)

f:id:simplestar_tech:20200210204717j:plain
in.jpg

こちらの処理を実行すると

magick in.jpg -resize 200x200 -size 300x300 xc:blue +swap -gravity center -compose over -composite -mattecolor red -frame 10x10 exit.jpg

意味は、200x200 にアスペクト比を保ったまま縮小して、blue 一色で塗りつぶした 300x300 の画像を新規作成、並び順を入れ替えて、conposite により青を背景に重ね合わせて、10pixel 幅の赤いフレームを追加
という文(読めました?)

次の結果が得られます。

f:id:simplestar_tech:20200210204915j:plain
exit.jpg

PowerShell で実行する複数ファイル作成とタイリングモンタージュ

以下の powershellWindows 環境で imagemagick に PATH が通っていれば動きます。

テクスチャはこちらの有料アセット(single entity license)を購入したものを利用します。
Yughues PBR Nature Materials
assetstore.unity.com

tga ファイルは magick で変換すると上下逆転しますし、Specular は png に tga から変換すると背景色が真っ白に変わるので(本当は黒)
一度 jpg に中間ファイルを出して、上下反転しつつもなんとか
あと、ファイルによっては正方形ではないので正方形に直します。
加えて、左右に 8 pixel の余白を設けるように 1008 x 1008 に圧縮してタイリングしたいと思います。

を実現する Powershell が以下の通り

# [AO, Diffuse, Height, Normal, Specular]
$target = "Specular"
New-Item $target -ItemType Directory -Force
$dir = "~\Assets\Yughues PBS Nature Materials"
$imgs = Get-ChildItem -Recurse -Path "$dir\*$target.tga"
foreach ($img in $imgs) {
    $fullName = $img.FullName
    $jpg = "$target\" + [io.path]::ChangeExtension($img.Name, "jpg")
    $id = identify $fullName
    $edge = 0.0158730
    if ($id -like "*512x1024*") {
        magick $fullName -resize 504x1008 -flip -write mpr:i +delete mpr:i mpr:i +append -set option:distort:viewport "%[fx:w*(1+$edge)]x%[fx:h*(1+$edge)]" -virtual-pixel Edge -distort SRT "0,0 1,1 0 %[fx:w/(2/$edge)],%[fx:h/(2/$edge)]" -quality 100 $jpg
    }
    else {
        magick $fullName -resize 1008x1008 -flip -set option:distort:viewport "%[fx:w*(1+$edge)]x%[fx:h*(1+$edge)]" -virtual-pixel Edge -distort SRT "0,0 1,1 0 %[fx:w/(2/$edge)],%[fx:h/(2/$edge)]" -quality 100 $jpg
    }
    echo $jpg
}
montage "$target\*$target.jpg" -tile 8x8 -geometry 1024x1024 "0_$target.png"

出力されるテクスチャを使って、Height map を利用しつつ絵を作ることができました。

f:id:simplestar_tech:20200211123626p:plain
8x8 の 64 分割テクスチャによる 1 マテリアル描画で全キューブ描画

ステップ2はクリアです。
残すはゲーム内の UV 操作のみ

追記

余白をエッジ引き延ばしするのもよかった
だけど parallax で使う場合はかなり余白を利用することになるし、そこがエッジ引き延ばしだとまた不自然な見た目になったので
リピートラッピングする方法を考案しました。

次の命令は画像の上下左右ななめ方向すべてにタイリングする例です

magick in.jpg -resize 960x960 -flip -write mpr:i +delete mpr:i mpr:i mpr:i +append -write mpr:j +delete mpr:j mpr:j mpr:j -append -gravity center -crop 1024x1024+0+0 -quality 100 out.jpg

これをスクリプトにつなげると

# [AO, Diffuse, Height, Normal, Specular]
$target = "Diffuse"
New-Item $target -ItemType Directory -Force
$dir = "D:\github\Unity\UniversalParallaxOffset\Assets\Yughues PBS Nature Materials"
$imgs = Get-ChildItem -Recurse -Path "$dir\*$target.tga"
foreach ($img in $imgs) {
    $fullName = $img.FullName
    $jpg = "$target\" + [io.path]::ChangeExtension($img.Name, "jpg")
    $id = identify $fullName
    if ($id -like "*512x1024*") {
        magick $fullName -resize 480x960 -flip -write mpr:i +delete mpr:i mpr:i +append -write mpr:j +delete mpr:j mpr:j mpr:j +append -write mpr:k +delete mpr:k mpr:k mpr:k -append -gravity center -crop 1024x1024+0+0 -quality 100 $jpg
    }
    else {
        magick $fullName -resize 960x960 -flip -write mpr:j +delete mpr:j mpr:j mpr:j +append -write mpr:k +delete mpr:k mpr:k mpr:k -append -gravity center -crop 1024x1024+0+0 -quality 100 $jpg
    }
    echo $jpg
}
montage "$target\*$target.jpg" -tile 8x8 -geometry 1024x1024 "0_$target.png"

期待通り