[Astro] #166 サイトマップ #02 — 星に光を足して、霧と粒子と検索を入れ、VR へつなぐところまで作り込み
はじめに
前回、サイトマップを作りました。カテゴリを大きな星、アプリを周りの星、記事を星屑にして、クリックでワープする 3D 版と、JavaScript なしで読める 2D 版です。
[Astro] #165 サイトマップ — 3D の星座と、JavaScript なしの一覧で、サイト全体を見渡せるページを作成 // PROTOCOL.LAIN
ツールと記事が増えすぎて、作者自身も全体を把握できなくなってきたので、サイトマップを作りました。
lain-lab.com動きはしたのですが、見た目は「丸と線だけ」でした。そこで、同じデータのまま、作り込みました。番号は前回と同じ #165 で、続きとして書きます。
今回の要点は次の 4 つです。
- 後光・霧・粒子で、「丸と線」を星空らしくする
- 検索を入れて、ヒットした星だけをライトアップする
- スマホと画面サイズ周りの細かい不具合を直す
- VR 化の方針を決めて、最初の段階が Quest で動いた
完成画面
作り込む前
作り込んだあと
検索でライトアップ
見た目を作り込む
きっかけは、「この 3D 表示も、単なる丸と線だけだけど、少しアレンジしたら豪華になる?」という、ふとした疑問でした。やってみて、ダメなら戻せばいい、という軽い気持ちで始めました。戻せるように、変更の前のファイルは毎回残しています。
後光
星の後ろに、光のにじみを置きました。
- 光の点は、中心が明るくて縁が透明な丸の画像(放射状のグラデーション)
- 描画は、加算合成の
Points - カテゴリ用(大)、項目用(小)、カテゴリの周りの薄いもや用(特大)の 3 枚
星そのものは、前と同じ球です。後ろに光を足しただけで、見え方がかなり変わりました。
線の色も変えました。カテゴリ側を明るく、項目側を暗くして、グラデーションにしています。
const f = s < NSI ? 0.28 : 1; // 根元(カテゴリ側)は明るく、先端(項目側)は暗く
colArr.set([tmp.r, tmp.g, tmp.b, tmp.r * f, tmp.g * f, tmp.b * f], s * 6);
線は 1 本の LineSegments のままで、頂点の色を 2 つ変えているだけです。
選んだ星に、白い大玉をやめてリングを付ける
前は、選んだ星を白くして、1.4 倍に膨らませていました。これが、隣の項目名の文字を隠していました。
今は、星の色はそのままにして、周りに細いリングを出します。リングは Sprite で、距離に合わせて大きさを変えて、画面上ではいつも同じ大きさに見えるようにしています。
背景の星は、やめた
最初は、背景に小さな星を 1400 個ばらまきました。奥行きは出ましたが、DUST(記事の星屑)と見分けがつかなくなりました。
そこで、星の代わりに、以前作った Procedural Solar System の背景の、紫の霧を持ってくることにしました。ノイズのシェーダーなので、重くなるかもしれないと思っていました。
Procedural Solar System
説明なし
lain-lab.com霧は、1 回だけ焼く
ノイズを毎フレーム計算すると重くなります。そこで、起動したときに 1 回だけ、GPU でノイズを計算して、テクスチャに焼きました。そのテクスチャを、巨大な球(天球)の内側に貼ります。毎フレームの仕事は、球を 1 つ描くだけです。
renderer.setRenderTarget(rt);
renderer.render(bakeScene, new THREE.Camera()); // ノイズを rt(2048×1024)に焼く
renderer.setRenderTarget(null);
霧のシェーダーは、次の形です。
- 3D の fbm ノイズ(5 段)を、さらに別の fbm でゆがめる(ドメインワープ)。これで霧の筋ができる
- 球面の方向から 3D ノイズを引くので、つなぎ目が出ない
- カテゴリのある方向には、そのカテゴリの色を少し混ぜる
カテゴリの色の混ぜ方は、原点から見た方向で決めています。そのため、星座の中心に立つと、星の配置と霧の色が一致します。
もう 1 つ、細かい話があります。最初は、ノイズをリニア空間の値のまま 8bit のテクスチャに焼こうとしました。暗い部分は、リニアのままだと 1 段の幅が粗くなって、段差(バンディング)が出ます。そこで、表示用の色のまま焼いて、天球は色変換なしで出力しています。
天球の半径は 5000 です。カメラの描画距離を 6000 から 12000 に伸ばさないと、球の反対側が欠けます。
濃さの調整
最初は、霧が濃すぎて、ラベルや線が読みにくくなりました。強さを 0.6 に落として、ちょうど良くなりました。強さは GLOW.nebula で変えられて、0 にすれば霧は消えます。
const GLOW = { halo: 0.55, haze: 0.14, nebula: 0.6, ring: true, pulse: true, flow: 1.6 };
見た目の飾りは、全部ここで調整できます。気に入らなければ、個別に切れます。
線の上を、光の粒が流れる
最後に、線の上を光の粒が流れるようにしました。線 1 本につき、光の粒を 1 つ。根元から先端へ、ゆっくり動きます。
- 約 100 個の
Pointsを、毎フレーム CPU で位置と色を更新するだけ - 端では消えるように、
sinで明るさを変える - ホバー中のカテゴリの線だけが明るくなる
prefers-reduced-motionのときは、無効にする
正直、「線の上を粒子が動くって、どうなんだろう」と思っていました。やってみると、予想以上に良くて、生命が宿ったように見えました。動きがあるだけで、静止した図が、動いている世界になりました。
検索を入れる
記事が約 830 本とアプリが約 95 本あるので、星を眺めるだけでは、探したいものが見つかりません。検索を足しました。
検索は、重くない
検索の対象は、タイトル・タグ・アプリの説明だけです。記事の本文は検索しません。合計で約 1000 件なので、入力のたびに全部を見ても、ほとんど負荷はかかりません。
- 全角・半角、大文字・小文字は同じ扱い(NFKC に直して小文字にする)
- スペースで区切ると AND
- 日本語入力の、変換を確定する Enter は無視する
結果の見せ方
入力すると、カテゴリ一覧が、結果の一覧に入れ替わります。左端の色がカテゴリの色で、2 行目に「APP / 記事」とカテゴリ、日付が出ます。クリックするか、Enter で先頭を選ぶか、上下キーで選ぶと、その星へワープして詳細カードが開きます。
結果の一覧は最大 40 件ですが、ライトアップは全部のヒットが対象です。
ヒットした星だけが光る
検索の結果を、3D の星にも反映しました。例えば「MSX」で検索すると、MSX に関係のあるアプリと記事の星だけが明るくなって、他は沈みます。
ヒットしたかどうかを、Uint8Array で持っています。
let hitItem = null, hitDust = null, hitCat = null; // null なら検索していない
const kk = hitItem ? (hitItem[k] ? 1 : 0.22) : /* ふだんの明るさ */ base;
星・線・後光・光の粒・記事の星屑の明るさを決める所が、全部この配列を見ます。入力のたびに、約 1000 要素を回して塗り直すだけなので、軽いです。
ヒットした記事の星は、そのままだと小さくて見えないので、光の点を重ねています。
沈んだ星が、黒い穴に見えた
検索中、ヒットしなかった星は暗く沈めていました。ところが、星の色が黒に近いと、霧の上に黒い円盤が浮かんで、穴が空いたように見えました。暗さの下限を上げて、背景に溶ける程度の暗さにしました。
霧のような背景があると、「暗くする」ことは、「黒くする」こととは違う、と分かりました。
記事の星屑を、最初からオンにする
DUST(記事の星屑)は、最初はボタンを押すまで出さない仕様でした。ですが、記事の星があった方が、断然映えます。
重さは、コードから見積もりました。星屑は 1 つの Points(約 830 点)と、線と、光る点だけです。毎回の処理は、色を塗り直す 830 回のループと、マウスが動いたときの 830 点の画面座標への投影だけです。データは最初からページに埋め込んであるので、オンにしても、転送量は増えません。
そこで、起動時に toggleDust() を 1 回呼ぶだけで、最初からオンにしました。ボタンで消すことは、今までどおりできます。
スマホと画面サイズの修正
スマホで、左上の一覧が邪魔
スマホにすると、左上のカテゴリ一覧が、星空の大部分を隠していました。
- 700px 以下では、一覧を隠す
- 左上に「☰ MENU」ボタンを出して、押すと開く
- カテゴリを選んだとき、または星空を触ったときに、自動で閉じる
もともと、スマホ用の CSS は書いてありました。ところが、一覧が #sm-list という入れ子の中にあるせいで、横スクロールの指定が効いていませんでした。しかも、700px 以下では、ALL / ROTATE / DUST のボタンが隠れていて、スマホでは DUST が押せませんでした。今回の MENU で、どちらも解消しました。
画面の下に、黒い帯が出る
3D の描画領域とフッターの間に、細い黒い帯が出ていました。
原因は、ステージの高さを 100vh - 100px と、決め打ちにしていたことです。実際のヘッダーは約 40px、フッターは約 37px で、合っていませんでした。
そこで、ステージを画面に固定して、ヘッダーとフッターの実際の高さを JavaScript で測って、上下に合わせるようにしました。
const fit = () => {
const hd = document.querySelector('.navi-os-header');
const ft = document.querySelector('.system-footer');
stage.style.setProperty('--sm-top', (hd ? hd.offsetHeight : 40) + 'px');
stage.style.setProperty('--sm-bot', (ft ? ft.offsetHeight : 37) + 'px');
};
リサイズのときも、測り直します。要素が見つからないときは、40px と 37px です。
VR 化へ
ここまで作り込んだ世界を、VR で見たくなりました。
前回の試作は、失敗した
前回の記事の最後に、「島を歩く VR」を構想として書きました。試作も作りました。が、完成したものは、駄作でした。島に作品が並んでいても、情報が伝わりません。VR では、HTML も iframe も埋め込めない(と理解しています)。島を歩くよりも、今の星座のほうが、見たいものが分かります。
今の星座を、そのまま VR へ
方針を、次のように決めました。
- 開始位置は、星座の中心。周りにカテゴリの星が見渡せる
- コントローラーのレーザーでカテゴリの星を指して、最初は瞬間移動する
- 移動のあとは、ワープ移動で星を渡り歩く
- 星への当たり判定は、距離ではなく、角度で取る(画面上で最も近い星を探す、2D の方法と同じ考え方)
- 到着したら、そのカテゴリのアプリを、湾曲した壁のようにパネルで並べる(マルチモニタ風)
- 記事は、本文を JSON に抽出して、Canvas に描画する、簡易的なブラウザにする
VR の中では Web が開けません。そのため、記事を読むには、ビルド時に記事から本文を抜き出した JSON を、自前で描画するしかありません。コードブロックや画像は、簡略化することになります。
一気に作らない
段階を 3 つに分けました。
- VR で世界が見える。中心から、星を指して移動できる
- 到着先で、パネルを並べる
- 記事リーダー
1 つずつ、Quest で確かめながら進めます。段階 1 が、先に動きました。
画像の CORS は、確認が要る
パネルに画像を出すには、画像を WebGL のテクスチャにします。これには、CORS ヘッダー(Access-Control-Allow-Origin)が必要です。
2D のページで画像が見えているのは、<img> が CORS を必要としないからで、CORS が通っている証拠にはなりません。そこで、次のコードで確かめました。
fetch('https://assets.lain-lab.com/images/uploads/astro-165-sitemap.webp', { mode: 'cors' })
.then(r => console.log('OK', r.status))
.catch(e => console.log('NG', e));
結果は OK 200 でした。画像は、そのままテクスチャにできます。動画は、パネルに出す段階になったら確かめます。
ハマりどころ
1. 背景の星が、記事の星と混ざる
背景に星を撒くと、奥行きは出ますが、DUST の星屑と見分けがつかなくなります。星の代わりに、霧にしたら解決しました。奥行きの手がかりを出すときは、情報を持つ要素と、見分けがつく形にします。
2. 暗くしたつもりが、黒い穴になる
霧の上で「暗くする」と、黒い円盤になります。背景の色に近い暗さにします。
3. 暗い部分のノイズが、段差になる
リニア空間の値を 8bit のテクスチャに焼くと、暗い所で段差が出ます。表示用の色のまま焼きます。
4. 描画距離が足りず、天球が欠ける
天球の半径が 5000 のとき、カメラが原点から離れると、球の反対側が描画距離(far)を越えて欠けます。far を 12000 に伸ばしました。
5. 固定値の高さは、ズレる
100vh - 100px のような決め打ちは、ヘッダーとフッターの高さが変わると、すぐズレます。実際の高さを測ります。
6. 見えていても、CORS が通っているとは限らない
<img> で見えていても、WebGL のテクスチャにはできないことがあります。必要になる前に、fetch で確かめます。
7. 沈んだ星の黒、DUST の初期状態、スマホの CSS
どれも、実際の画面を見て、初めて気付きました。ヘッドレスのブラウザで確かめても、「見え方」までは分かりません。手元のブラウザで見て、直します。
まだ入れていないこと
- 実機(Quest・スマホ)での、フレームレートの計測
- VR の段階 2・3(パネルと記事リーダー)
- 時間の回廊:記事を日付順に並べて、手前へ迫ってくるビュー
- 作品(GALLERY の 14 本)を、3D の星にすること
おわりに
最初の版で、妥協しなくて良かったと思いました。
最初の版は、データを読んで、並べて、動かしただけのものでした。そこに、後光、霧、粒子、検索を足していくと、同じデータなのに、全然違う世界になりました。やはり、ライティングは大事です。
作り込みは、一度に全部をやらずに、1 つ足しては見て、1 つ足しては見て、を繰り返しました。気に入らなければ戻せるように、前のファイルを残しておく。それだけで、安心して試せました。
VR は、まだ最初の一歩です。次は、星の足元にパネルを並べて、記事を読めるところまで、進めたいと思います。