縫い目が浮いた。夜のHUD(画面上に表示される情報)が台無しになった。焦ったけど対処。
環境マップから逃げられず再構築
HDR(高ダイナミックレンジ、明るさの範囲が広い画像形式)をそのまま貼ると左右に白い帯が走る。CanvasTexture(Canvas要素から作成されるテクスチャ)を作り直して左右8%をフェードさせ、PMREMGenerator(環境マップを生成するツール)に渡すところからやり直した。TextureLoaderのロード完了後にCanvasTextureへ描き直し、minFilterとmagFilterをLinearFilterに固定して粒度を抑える。
一度つまずいた。フェードなしのままでは輝度差が18%も残った。フェードを入れて差が2%に落ち、ようやく夜景が繋がった。
// webgl.js(抜粋)
const fadeWidth = canvas.width * 0.08;
const left = ctx.createLinearGradient(0, 0, fadeWidth, 0);
left.addColorStop(0, 'rgba(0,0,0,1)');
left.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = left;
ctx.fillRect(0, 0, fadeWidth, canvas.height);
const right = ctx.createLinearGradient(canvas.width - fadeWidth, 0, canvas.width, 0);
right.addColorStop(0, 'rgba(0,0,0,0)');
right.addColorStop(1, 'rgba(0,0,0,1)');
ctx.fillStyle = right;
ctx.fillRect(canvas.width - fadeWidth, 0, fadeWidth, canvas.height);
デフォルト背景はrequestAnimationFrame(アニメーション用のAPI)で100msごとにneedsUpdateを立てる。setIntervalを捨てただけでjankが消えた。意外でした。
ビジュアルに納得できたので、フェードあり/なしの差も残した。
前提知識
- Three.js(3Dグラフィックスライブラリ)0.180: PMREMGenerator(環境マップを生成するツール)とOrbitControls(カメラ操作のコントロール)をESM(ES Modules、モジュールシステム)経由で読み込んでいます
- CanvasTexture(Canvas要素から作成されるテクスチャ): 更新フラグを立てることでループと同期するテクスチャ更新の仕組み
- JavaScript requestAnimationFrame(アニメーション用のAPI): deltaからmsを積算してイベントを打つ基本形
多層HUDを支える粒子と光の編成
HUDレイヤーはCanvas 2D(2D描画用の要素)、奥はWebGL(ブラウザで3D描画を行う技術)。ParticleクラスにnormalizedDeltaを入れてFPS(フレームレート、1秒あたりのフレーム数)差を吸収するまで、60fpsと120fpsで軌跡がズレ続けた。これは課題。
// index.html 内 script(抜粋)
particle.update = function update(delta) {
const normalized = delta / 16.67;
if (this.type === 'fireworks') this.vy += 0.015 * normalized;
this.x += this.vx * normalized;
this.y += this.vy * normalized;
this.life -= this.decay * normalized;
this.vx *= Math.pow(this.speedDecay, normalized);
this.vy *= Math.pow(this.speedDecay, normalized);
return this.life > 0;
};
auroraからpulseまでの5モードを数字で見直し、spawn数は18→8→6と段階的に削減。Chromeのメモリ使用量は9.3MBから4.9MBまで落ち着いた。ヨシ。
processAutoEmission()は80〜1800msのautoTimerで再生産する。タイムライン化してdeltaの積算を視覚化した。
三次元側は四灯構成。Hemisphereでベース色、Directionalでシャドウ、Pointで中心の暖色、Spotでリングを拾う。DirectionalLight.shadow.mapSizeを2048に上げた分は受け入れた。
デバイスとUIの呼吸を合わせる
setupDeviceOptimization()でFOV(視野角)とpixelRatio(ピクセル比)を端末別に切り替える。モバイルではFOV 75°→85°、pixelRatio 1.5→1.2へ絞り、GPU(画像処理専用のプロセッサ)時間を30%節約。
トップバーは5秒後にsmallクラスへ縮む。クリック時にタイマーを張り直すように書き直し、setTimeoutの多重起動を避けた。収入がない身としてはUIで失敗する方が不安が大きい。
前面Canvasと背面Three.jsの二層構造を7ステップに整理し、pointer-eventsの切り替えを見える化した。
実際に試したスナップ
- オーロラフォントをVRAM(ビデオメモリ)4GBのノートPCで表示。フェード未適用時は縦線が出たが、8%ガード適用でほぼ消えた
- Pixel 7(Googleのスマートフォン)でstreamモードを連続再生。autoTimer 140msでも60fps(FPS、フレームレート)を維持し、particleCountは平均520
使ってみて
実装したフェード処理と粒子制御をそのまま確認できます。
フェード幅を0〜20%で試し、HUDの縁でどのくらいバンディングが消えるかを確認できます。
まとめ
- CanvasTexture(Canvas要素から作成されるテクスチャ)に描き直し、左右8%フェードで縫い目を2%まで縮小
- requestAnimationFrame(アニメーション用のAPI)とdelta正規化で粒子と背景を同期
- FOV(視野角)とpixelRatio(ピクセル比)を端末別に切り替え、GPU(画像処理専用のプロセッサ)時間を30%削減
さらに読みたくなったら
最後まで読んでくださり、ありがとうございました。