【Unity】広域マップの計算誤差対策

ある夜眠れなくてこの世の深淵について考えていた時に、ふと『オープンワールドゲームを作るとして、計算誤差対策はどうやるんだろう』と思いいたり、作ってみることにした。

なお、完全に思い付いただけの手法をコードに落としてるだけなので、実用性は保証しない。

そもそもUnityでオープンワールドを作ると何が問題なのか

3Dでキャラクターが移動するゲームを作るのであれば、座標管理は必須だ。画面上の描画も、当たり判定も、座標情報がなければ成しえない。

Unityにはデフォルトでこれをサポートする機能が入っていて、どんなオブジェクトでも、Unityではシーン上に配置されればTransformによってオブジェクトの配置座標や回転方向を管理してくれるようになる。これによって開発者は座標管理から解放され、特に面倒くさいことをせずとも、(そのほか様々なサポート機能のおかげもあり)キャラクターの移動処理や当たり判定・描画処理が使える。

さて、そんなうれしいTransformであるが、オープンワールドのような広大なマップを持つゲームを作ろうと思うと直面しそうな問題がある。

floatの計算誤差問題である。

Unityで使われるTransformのPosition、つまり座標管理に利用される変数の型はVector3といって、内部を (float x, float y, float z) の3つの float で構成される構造体である。

floatは日本語で 浮動小数点 と呼ばれる型で、int とは異なり小数点以下の数値が扱え、3Dの座標計算で普通使われる大変ありがたい型なのだが 浮動 という名の通り、精度が良くない。

良くないといっても普通に使う分には全く問題ないので使われているのだが、扱う数値が 1000 あたりを超えてくると少し怪しくなってくる。

その辺の解説には自信がないので詳細はこちら 浮動小数点数の限界を把握する - Qiita を見ていただくとして、つまり、『オープンワールドゲームを作ってると問題が起きそう』ということである。

具体的にはこういうことが起きる

これはVRChatにある、floatの計算誤差を楽しむためのワールドらしい。

どう解決するか

前述のような問題点は座標管理に使われているのが float 型であるのが原因なので、 double が使えるなら double を使えば良いのだが、(doubleについては前述 浮動小数点数の限界を把握する - Qiita を参照)UnityのTransformではpositionにdoubleを与えることができないので、これは使えない。

そこで安直ではあるが、原点を動かす手法を考えた。

原点を動かす?

float を使用すると誤算問題が発生することは紹介したが、この誤差問題が大きく問題になってくるのは小数点以上の値が1000辺りを超えて小数点以下の数値の期待できる精度が一定の桁数を下回ってきた場合である。

キャラクターの座標値に float を使っている場合、例えば はじまりの町 を 座標(0, 0, 0) に配置し、 魔王城 を 座標(10000, 0, 0) などに配置すると、ゲームをスタートした直後は問題なくとも、ゲームを進め、魔王城に到着するころには周囲のオブジェクトはガクガクと震え、キャラクターは輪郭を保つことすら難しくなっているだろう。

しかし、ゲームの座標という観点で見るのであれば、これはあくまで はじまりの町 を原点位置に置いてあるから起きている問題なのである。

どういうことかというと、例え はじまりの町 と 魔王城 が 10km 離れた位置に配置されていようと、 魔王城 が 座標(0, 0, 0) はじまりの町 が 座標(-10000,0,0) に配置されているのであれば 魔王城 にいる限りは特別問題は起きない。

もちろん、 魔王城 を原点に配置しただけで問題が解決するわけではない。これだけでは、今度は逆にはじまりの町に戻るとゲームが崩壊してしまう。

ではどうするか。

はじまりの町 の近くにいるときは はじまりの町 を原点に 魔王城 の近くにいるときは 魔王城 を原点に配置すれば良いのだ。

原点を動かす!

さて、簡単に構成を考える。 まずほしい情報は

  • 現在のPCの座標
  • 原点になりうるオブジェクトの位置
    • これはランドマークと呼ぶことにする

あたりだろうか。

これらの情報は Vector3 の形で欲しいが、 Vector3 そのままで持っても仕方がない(通常のTransformをそのまま動かしているのと変わらなくなる)ので、 float の代わりに double で座標を保持する Vector3 を用意する。

今回はここの実装は既に同様の機能を実現されているこちら [Unity] double 精度の Vector3 クラス · GitHub を使わせていただいた

そして実装は以下のようにする

using Imu;
using System.Collections.Generic;
using UnityEngine;

public class OpenWorldOriginManager : MonoBehaviour
{
    [SerializeField]
    List<Landmark> landmarks = new List<Landmark>();

    [SerializeField]
    Transform character = null;

    [SerializeField]
    CharacterMoveController characterMoveController = new CharacterMoveController();

    DVector3 realCharacterPosition = DVector3.zero;

    float lastTime = 0;
    void Update()
    {
        realCharacterPosition = characterMoveController.Move(realCharacterPosition, Time.time - lastTime);
        SimpleOriginSwitch();
        lastTime = Time.time;
    }

    // 最も近いランドマークを原点とし、キャラクターやそのほかのランドマークの座標を移動させる
    void SimpleOriginSwitch()
    {
        DVector3 nearest = DVector3.zero;

        for (int i = 0; i < landmarks.Count; i++)
        {
            nearest = Near(realCharacterPosition, nearest, landmarks[i].position);
        }

        SetOrigin(nearest);
    }

    void SetOrigin(DVector3 center)
    {
        for (int i = 0; i < landmarks.Count; i++)
        {
            landmarks[i].entity.position =
                new Vector3(
                    (float)(landmarks[i].position.x - center.x),
                    (float)(landmarks[i].position.y - center.y),
                    (float)(landmarks[i].position.z - center.z));
        }

        character.position =
            new Vector3(
                    (float)(realCharacterPosition.x - center.x),
                    (float)(realCharacterPosition.y - center.y),
                    (float)(realCharacterPosition.z - center.z));
    }

    DVector3 Near(DVector3 center, DVector3 a, DVector3 b)
    {
        return (Distance(center, a) < Distance(center, b)) ? a : b;
    }

    double Distance(DVector3 a, DVector3 b)
    {
        DVector3 ab = a - b;
        return System.Math.Sqrt(ab.x * ab.x + ab.y * ab.y + ab.z * ab.z);
    }
}
public class CharacterMoveController
{
    [SerializeField]
    float moveSpeedPerSec = 0.1f;

    // Inputを受け取ってnowPosに足し引きするだけの処理
    public DVector3 Move(DVector3 nowPos, float timeScale)
    {
        Vector3 moves = Vector3.zero;
        float moveSpeed = moveSpeedPerSec * timeScale;

        if(Input.GetKey(KeyCode.UpArrow))
        {
            moves.z += 1;
        }

        if(Input.GetKey(KeyCode.DownArrow))
        {
            moves.z -= 1;
        }

        if(Input.GetKey(KeyCode.RightArrow))
        {
            moves.x += 1;
        }

        if(Input.GetKey(KeyCode.LeftArrow))
        {
            moves.x -= 1;
        }

        moves = moves.normalized * moveSpeed;

        return nowPos + moves;
    }
}

実行結果はこんな感じ

いい感じ。座標は瞬間的に変わる
実行結果 - 1

画面右上を見てもらえればわかると思うが、座標が一定のラインでカチっと切り替わる。これがよいのか悪いのかわからないが、スムーズに座標切り替え処理する方法も思いついたのでやるだけやった。

コードは以下

[SerializeField, Min(0)]
float range = 10;

void LerpOriginSwitch()
{
    DVector3 nearest = DVector3.zero;
    DVector3 secondNear = DVector3.zero;

    for(int i = 0; i < landmarks.Count; i++)
    {
        DVector3 near = Near(realCharacterPosition, nearest, landmarks[i].position);
        if(!Equals(near, nearest))
        {
            secondNear = nearest;
            nearest = near;
        } else if(Equals(nearest, secondNear))
        {
            secondNear = landmarks[i].position;
        }
    }

    double distanceFromCharacterToNearestFromSecond = System.Math.Abs(Distance(nearest, realCharacterPosition) - Distance(secondNear, realCharacterPosition));

    DVector3 center;
    if (distanceFromCharacterToNearestFromSecond > range)
    {
        center = nearest;
    } else
    {
        double perInRange = distanceFromCharacterToNearestFromSecond / (range * 2f) + .5f;
        center = nearest * perInRange + secondNear * (1.0 - perInRange);
    }

    SetOrigin(center);
}

何をやっているのかというと、現在のキャラクター位置に最も近いランドマークと、2番目に近いランドマークの座標を取得し、その各ランドマークとの距離差が range よりも小さければ lerp 的処理をかけて徐々に原点を ランドマークA から B へと遷移させてる感じです。

実行結果は以下(ランドマークBの座標をx = 950, z = 800 に移動させてある)

キャラクター座標・各ランドマークの原点ともに徐々に変化していることがわかる
実行結果 - 2

画面右上に座標をいくつか載せてあるが、キャラクター座標は最初 x = 300 あたりから始まったのに、途中から減少しだし、最終的には x = -400辺りになっている。

また、ランドマークA, Bともに途中で座標が変わりだし、最終的に A は x=-950, z = -800 に B は x = 0, z = 0 に落ち着いている。

実際に運用した際どっちの方がマシかはわからないが、とりあえず徐々に座標を動かすことには成功した。

感想

実際にオープンワールドゲームなどでどうやっているのかが気になるが、とりあえずそれっぽいものが作れた。

これが実際にはもっと大規模のフィールドになるはずなので、各ランドマークやNPCの座標をDB化して一定範囲より遠いものは動かさないようにしたり、オンラインの場合は別ユーザーの座標を処理したり、揺れ物がある場合はキャラクターの移動量で揺れ量を判定したりしているだろうから、そのあたりの処理も必要そうだ。

この方法にはこういった問題がある・実際にはこういう処理をしているなどあったら教えてください。

では。

https://twitter.com/asagi_00a3af

ARFoundation Anchors覚え書き

テストアプリ作って動かしているうえで何となくこういう事かと思ったことの覚え書き。正確性は保証しない

  • Spatial anchorとかとは違うのか
    • Azure spatial anchorとかとはたぶん違う
    • リファレンス見て一通り検索した感じだとそもそも共有とかそういう概念がない
      • セーブ/ロードすらない
  • 何ができるのか
    • 空間にマーカーを追加できる的な概念
      • Anchorというから分かり辛くなるけど、指定された地点の周囲の情報を保存している感じだと思う
        • その情報のパッケージをAnchorと呼んでいるのでは
        • 何の情報を保存しているのかはわからないけどたぶんPoint cloudとか
  • 何がうれしいのか
    • 触ってる感じだと、Position trackingとかがズレたとき、Anchorを配置しない場合より早く・正確に復帰している気がする
      • Vuforiaとか使ってる時に特に悩むのがアプリがバックグラウンドに行ったときの処理だけど、ARFoundation、とくにAnchorを配置しているとこれに悩むことはなくなりそう。
        • 自動でちゃんと復帰してくれる。Anchorのある場所で復帰しなおすと特に早い。
        • 自分が使ってた限りでは、Vuforiaはアプリがバックグラウンドに行くとAR空間の復帰ができなかった。やり方が悪かった説はある。
  • マーカーの代わりとして使える?
    • 微妙。少なくともAR foundationで使った感じだと、見失ったとき(違う部屋に行ったりして完全に視界の外にやったとき)のイベントがないのと、配置したAnchorの再検出はイベント取れたけど一回見失ってないとイベント発行されないぽい感じだったので、少なくともマーカーの代わりのような運用は想定されてないと思う。

HoloLens(1)触ってた時もAnchor周りはよく理解できなくて適当に使ってたけど、こういう事だったのだろうか。 兎にも角にもAnchorの公式情報少なすぎ。

参考URL

AR anchor manager | AR Foundation | 4.0.2

Class ARAnchor | AR Foundation | 4.0.2

追記: 読んだことをすっかり忘れてたけど、ARCoreのAnchorについての記載もっとちゃんと読んでたらもっとすんなり入ってきてたかもしれない。

Working with anchors  |  ARCore  |  Google Developers

遷移する可能性のあるアニメーションのステートは遷移中である可能性を考慮しつつ保存しなければならない(当たり前だ)

アニメーションのポーズ機能を作りたくて、

stateInfo = animator.GetCurrentAnimatorStateInfo(targetLayer);

みたいな処理を書き、

animator.Play(stateInfo.fullPathHash, targetLayer, stateInfo.normalizedTime);

みたいにして使っていた。しかし、デバッグ中に、アニメーションが正常に再生されない問題が発生した。

原因を探ると、どうもTriggerで wait -> move みたいに遷移を作っていたのがうまく動いていないみたいだった。

今回はwaitからmoveへの繊維は2フレーム程度で終わるようにしていたのだが、利用者がボタンを連打したりすることで、waitからmoveへの遷移中にポーズ処理が発行される場合があることがわかった。

対策は以下

if(animator.IsInTransition(targetLayer))
    {
        stateInfo[targetLayer] = animator.GetNextAnimatorStateInfo(targetLayer);
    }
    else
    {
        stateInfo[targetLayer] = animator.GetCurrentAnimatorStateInfo(targetLayer);
    }

アニメーションが遷移中は遷移先のステートを保存するようにした。今の所これで同様の不具合は出ていないので大丈夫だと思う。

2019年やったこと・考えていたこととか

こんばんは。xRArchiが末端、建築にあまり明るくない勢、麻木浅葱です。

今年は割と仕事が忙しく、自身の実績として書けるようなことが殆どないので、仕事でやったことで一応名前が出ている奴をボカしつつ、内容について触れていきたいと思います。

某子ども向け科学雑誌の付録AR

こちらは昨年末から本年明けにかけてやった仕事で、某無人探査機が探査対象の小惑星に対し実行する作業の一部始終をARとして見せるというような、シンプルかつ教育的な内容です。

某アーティストMV用AR

こちらも昨年末あたりから本年春にかけてやっていた仕事です。主には調整が長引いた形です。

こちらはARKitの平面検出を利用し、街中にアーティスト本人が手書き(Tilt Brush)した歌詞を配置。それを音楽に合わせて追いかけるというような内容になっています。

とにかく現地での調整がひたすらに長く、つらかったです。次似たような案件があれば、最初に予定される進行ルートとその周辺をスキャニング・ないしはマッピングし、カーブやスロープ・階段・高低差などを3D化するでしょう。

このあたりはもしかしたらArchiにかかわってくる部分かもしれませんね。

また、屋外ならではの問題ですが、日が昇ってくるとアスファルトや舗装道・タイル張りの道なんかはトラッキングが外れやすくなり、長距離の移動や撮影といった用途では厳しくなってきます。こういうこともあってテストや撮影が早朝・夕方にしか行えなかったのも大変でした。

仕事でxR周りに携わっていて、今年の所感

今年は大きな変動があったというよりは、よりxR・特にARが盤石に求められるようになってきたなという印象です。 VRがいらなくなったとかというよりは、Vuforia, ARKit, ARCore, そして8th Wallに代表されるWebARの台頭によりARが再び脚光を浴びているように思います(その前はスノーとかの顔ARとかソニースマホとかにあったやつ)。

特にスマホARの熱はぐんぐんと増してきているように思います。 何せ、今先進国で一般に普及しているようなスマホなら半分以上はARKitやARCoreなんかは動くでしょうし、マーカーARに限ればほとんどのスマホで問題なく動くでしょう。結果として液晶を通さねば見れないという点で大きな制約性のあるものではありますが、AR眼鏡の開発・普及も伴って今後よりARは重要な産業になってくるのではと思っております。

とはいえ、まだまだスマホARは万能ではないです。 先ほど書いたように、液晶を通さないと見れない=VRほどの没入感・現実感がないというのもそうですし、端末によってできる表現の差があったり、トラッキング精度にまだ不安があったり…。

あと、これはARの問題ではないのですが、『ARをやりたい』が先行してしまって『ARで何をするか』が決まらないまま走り出してしまってグダグダ…という話も耳にするので、気を付けたいところですね。

来年に向けて、そのほか雑感

これ書くにあたって 今年何もやってねえな!!!!!!!!!!!! ってめっちゃ思ったので今年残した1ケ月と来年は、もうちょっと何か作りたいですね。

とか毎年言ってる気もしますが…。みんなどうやって時間作っとるんや…。

さて、ほかのxRArchiメンバーに比べて大分お粗末な出来になってしまいましたが今の私にはこれが限界です。 来年こそはみんなをあっと言わせる記事が書けるようになりたいですね。 ではさよ~なら~。良いお年を。

【Unity】Vuforiaを使うとCanvasの 'Screen Space - Camera' が使えなくなるので対処法を考えた

今作っているアプリで、画面の左下に、Cameraにレンダリングされる UI(アプリロゴ)を表示する必要があった(Render textureとして取得したかった)。

当然、普通ならCanvasのRender ModeをScreen Space - Cameraに設定しておけば良い。 しかし、何度やってもうまく描画されず、なんでやねんと調べてみると、どうやら Vuforia Behaviour を適用している状態のカメラをRender Cameraに設定すると、WidthとかRightとか、そういった値が壊れてしまうことが分かった。

当初はARCameraの階下に別のCameraを配置して、レンダリングはそっちにやらせれば…などと思っていたのだが、処理が重くなりそうなのと、肝心なAR周りの描画がちょっと不安定になったりしたのでやめた。

完成図

手法

結果としては、今回は、カメラの near clipping plane にあたる位置を取得し、それをもとにQuadを配置する手法をとった。

当初はポストエフェクト的にロゴを表示する方法なども考えたが、ポストエフェクトではCameraがレンダリングした画像としては取得できないことに気づいてこちらもやめた。

実装

まず作ったのは、near clip planeの取得処理。

これはそのものを取得することは出来ないので、 Camera.ScreenToWorldPoint を使って取得する。 具体的には以下

AsagiHandyScriptSingle/GetScreenWorldPoint.cs at master · M-T-Asagi/AsagiHandyScriptSingle · GitHub

using UnityEngine;

namespace AsagiHandyScripts
{
    public static class GetScreenWorldPoint
    {
        public struct ScreenWorldPoints
        {
            public Vector3 topLeft;
            public Vector3 topRight;
            public Vector3 bottomLeft;
            public Vector3 bottomRight;

            public ScreenWorldPoints(Vector3 tl, Vector3 tr, Vector3 bl, Vector3 br)
            {
                topLeft = tl;
                topRight = tr;
                bottomLeft = bl;
                bottomRight = br;
            }
        }

        public static ScreenWorldPoints GetPoints(Camera eye)
        {
            Vector3 UPSIDE_DOWN;
            UPSIDE_DOWN.x = 1;
            UPSIDE_DOWN.y = -1;
            UPSIDE_DOWN.z = 1;

            ScreenWorldPoints points;
            points.bottomLeft = eye.ScreenToWorldPoint(new Vector3(0, 0, eye.nearClipPlane));
            points.topLeft = eye.ScreenToWorldPoint(new Vector3(0, Screen.height, eye.nearClipPlane));
            points.topRight = eye.ScreenToWorldPoint(new Vector3(Screen.width, Screen.height, eye.nearClipPlane));
            points.bottomRight = eye.ScreenToWorldPoint(new Vector3(Screen.width, 0, eye.nearClipPlane));
            return points;
        }
    }
}

のように書いた。 画面のそれぞれ端点をNearClipの距離に合わせて取得する。

次に書いたのはQuadの配置とスケーリング処理で、

以下のように書いた

private void Update()
{
    GetScreenWorldPoint.ScreenWorldPoints points = GetScreenWorldPoint.GetPoints(camera);

    Vector3 newScale;
    newScale.x = Vector3.Distance(points.topLeft, points.topRight);
    newScale.y = newScale.x;
    newScale.z = 1;
    quad.localScale = newScale;

    Vector3 newPos = camera.transform.position;
    newPos += camera.transform.forward * (camera.nearClipPlane - 1f);
    newPos += -(points.topLeft - points.bottomLeft) * 0.5f + camera.transform.up * newScale.y * 0.5f;
    quad.position = newPos;
}

Scale部分の処理

Vector3 newScale;
newScale.x = Vector3.Distance(points.topLeft, points.topRight);
newScale.y = newScale.x;
newScale.z = 1;
quad.localScale = newScale;

今回、の目的は「ロゴを左下に表示したい」と「画面の大きさに合わせてスケールさせたい」の二点だったので、上記のような処理になった。

表示する画像はただのロゴ画像ではなく、背景透過された正方形の画像の左下にロゴが配置されている、といったもので、たとえば画面が正方形であればこの処理だけで画面の左下にロゴが配置できるものになった。

もし、そういった画像が用意できない、あるいはスクリプトだけで処理を完結させたい場合は、これを

newScale.x = Vector3.Distance(points.topLeft, points.topRight) * 0.25f;
newScale.y = (newScale.x / originX) * originY;

のように書く必要がある。

Position部分の処理

こちらは、

Vector3 newPos = camera.transform.position; でQuadの位置をカメラにそろえ

newPos += camera.transform.forward * (baseSettings.Eye.nearClipPlane - 1f); でnearClipPlaneの位置にQuadをそろえ

newPos += -(points.topLeft - points.bottomLeft) * 0.5f + camera.transform.up * newScale.y * 0.5f; でQuadを画面下部に固定している。

気を付けたいのはnear clip planeの位置に合わせている部分で、最後に -1f などしているように、少しばかりカメラ側に寄せないとVuforiaのAR(カメラ映像)とぶつかる。

おわり

UnityでMissingになっているScriptを特定する

作業をしようと思ったら Missing の文字が。多分リファクタリングの過程で削除したスクリプトの残滓だと思うので放置しても良いが、重要な何かだったら困るので確認をする。

guidを特定する

対称のGameObjectが配置されているシーンファイルをVisualStudioなどで開き、ファイル内検索などを使って対称のGameObjectの項へ移動する。

GameObjectの名前はシーンファイルに記述されているので、GameObjectの名前で検索すれば開けるはず。

Generalという名前のオブジェクトはGeneralで検索すると出る
こんな感じ

GameObjectの情報が見れたら、その下は次のGameObjectが出てくるまで対象のGameObjectに関する情報になっているので、 MonoBehaviour: という記述を追っていく。

GameObjectにスクリプトをいくつかアタッチしている場合は、同数のMonoBehaviourに関する項目があるので、それぞれの

m_Script: {fileID: ******, guid: ***************, type: *}

という記述を見る。

これで、アタッチされているすべてのスクリプトのguidが追える

ファイルの存在確認

この時点で、GameObjectに3つのスクリプトをアタッチしていたのであれば、3つのguidが取得できていることになる。

これを、プロジェクトに追加されているすべてのスクリプトのmetaファイルに記述されている、guidと比較していく。

方法はなんでもよくて、VSの機能を使うなり、エクスプローラーの機能を使うなり、Linux, UNIX系なら find ./Assets/Scripts -type f -name "*.meta" | xargs grep "guid" などすれば全スクリプトのguid一覧が見れるので検索するなりすればよい。

ファイルがある場合

上記までの手順でguidの一致が確認できた場合は、アタッチされているはずのスクリプトが何らかの理由でMissingになってしまっているということなので、原因を探して解消すると良い。

ファイルがない場合

ファイルがない場合考えられる大きな可能性は2つで、

1つはファイルを削除してしまっている場合。

もう1つはファイルをリネームするなどしてguidが変わっている場合。

どちらも、Gitを使っていれば git log --diff-filter=D --summarygit log --diff-filter=R --summary などで終えるので、直近の変更を確認してみると良い。

変更をさかのぼるなどしてguidの一致を確認できるはずだ。

.NET Core 3.0が正式リリースされたのでBlazor Server触ってみた②

せっかく触ってみたので、ページの追加くらいしてみようと思っていじってみた。

最初はプロジェクトの Pages ディレクトリ下に適当な Test のようなディレクトリを作成し、VSの右クリックメニューから 追加 > Razorページ などしてページを作成したのだが、何か違う気がしたのでやり直した。

結局チュートリアルを参考にMovieモデルを追加する形に落ち着いた。

チュートリアル: 

ASP.NET Core での Razor ページ アプリへのモデルの追加 | Microsoft Docs

おおよそはチュートリアル通りにやればよいのだが、プロジェクトの作成に際して何か設定を間違えたのか、作成したMovies以下のページにスタイルが適用されていない。

レンダリングされたページのHTMLを見てると、とか<HEAD>とかといった必須そうな項目が何1つない。 どうやらページのベースになるレイアウトっぽいものが読み込まれていないようだ。

色々見てみる

とにかく、BlazorとかRazorとか触るのが初めてなので構造がわからない。 もしかしたらそのうち開設されるかもとチュートリアルを一通り流して見るが、特に記述なし。

.cs とか .cshtml とか .razor とか書かれているファイルを片っ端から覗いていく。

関係のありそうなファイル

一通り眺めて関係ありそうだなと思ったのが _Host.cshtmlStartup.cs の2つ。

_Host.cshtml は

@page "/"
@namespace BlazorTest001.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>BlazorTest001</title>
    <base href="~/" />
    <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
    <link href="css/site.css" rel="stylesheet" />
</head>
<body>
    <app>
        @(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
    </app>

    <script src="_framework/blazor.server.js"></script>
</body>
</html>

といった様子で、まさに求めていたベースっぽいものが書かれている。ほかにこれっぽい記述のあるファイルはなかったので、デフォルトで出てくるようなHello world!ページは恐らくこれを読み込んでいるのだろう。

Startup.csチュートリアルなんかでも軽く触れられていたが、実行時の初期化処理を担っているようで、 endpoints.MapFallbackToPage("/_Host"); といった記述があったので、関係ありそうだと踏んだ。

テンプレートの適用されているファイル

実験的に Pages > Movies 以下に、`index.razor をコピーしておいてみる。

indextest.razor のように改名し、内部の @page を修正して実行すると、こちらはきれいに表示される。 どうやら、Moviesページがうまく出ないのは .cshtml.razor とは違う流れでレンダリングされているのが原因らしい。

_ViewStart.cshtml

ほとほと困り果てていろいろ触っていると、スキャフォールディングファイルの作成をするダイアログの中に レイアウトページを使用する > Razor _viewstart ファイルで設定されている場合は空のままにしてください といった記述を見つけた。

察するに新規にビューを作成する際はテンプレートとなるレイアウトを選ぶか、 viewstart なるファイルの設定に従うかを選べるようだ。 しかし、プロジェクト内を検索してみても、 viewstart ファイルは見つからない。どうやら存在しないようだ。

そこで見つけたのがこちらのページ asp.net mvc - _ViewStart.cshtml not found to render embedded cshtml - Stack Overflow

ないなら作ればいいじゃないということのようで、Pages下に作成し、以下のように記述した

@{ 
    Layout = "~/Shared/_Layout.cshtml";
}

同様に _Layout.cshtml ファイルも Shared ディレクトリ内に作成し、 _Host.cshtml の内容をコピーした。

表示できた

改めてページを表示してみると、見事レンダリングは成功していた。

レンダリング成功
画像は残る問題解決後のもの
スタイルシートの設定が違うのか、若干見本と違う気がするが、些細な差だろう。

新たな問題

今度は、 Movies/Create にアクセスすると InvalidOperationException: The following sections have been defined but have not been rendered by the page at '/Shared/_Layout.cshtml': 'Scripts'. To ignore an unrendered section call IgnoreSection("sectionName"). というようなエラーが出るようになった。

要約すると Scriptsセクションが定義されているのにレンダリングされていません ということらしい。

こちらも検索すると @RenderSection("Scripts", false); のように書けばよいことがわかるので、そのようにする。


さしあたって本日はここまで。

とりあえずこれでプロジェクトの作成→モデル定義→ビューの確認までできるようになったことになる。 実はまだ私の環境だとCreateの操作が反映されないなどの問題があるので、追って挑戦したい。