[Next.js #28] MMD時計アプリに Wallpaper カスタマイズ機能を追加 — 背景画像・Tint・UI切替

はじめに

今回のアップデートでは、 MMD時計アプリの背景壁を画像対応にし、さらにWallpaper UI から切り替え可能な形まで実装しました。

これまでは単色ベースの背景でしたが、今回の変更で空間の印象がかなり変わり、 時計・キャラクター・UI を含めた全体の世界観を調整できる構成に近づきました。

前回は Rig パネルや Morph UI を追加して、キャラクター側の制御を強化しましたが、 今回はその続きとして、空間そのものの見た目をUIから触れるようにした回です。

前回の記事:

動画(YouTube):

動画(PC):

今回何を作ったか

今回追加したのは、背景まわりのカスタマイズ機能です。

具体的には、次のような内容です。

  • 背景壁を単色から画像対応へ変更
  • 壁紙プリセットを用意
  • Tint Color で全体色を調整
  • Wallpaper パネルから切り替え可能にした
  • リサイズ時も壁が画面に追従するようにした

単に画像を貼るだけでなく、 壁紙の有無・種類・色味を UI から切り替えられるようにしたのが今回のポイントです。

実際に動かしてみると、背景を変えるだけでアプリ全体の印象がかなり変わるので、 見た目の満足度が一段上がりました。

なぜ必要だったか

もともと単色背景でも、時計アプリとしての体裁は成立していました。 ただ、実際に見ていると、キャラ・時計・UI があるのに対して、背景だけがやや情報量不足で、 空間としては少し物足りない印象がありました。

今回この機能を入れた理由は、主に次の通りです。

  • 単色背景でも成立していたが、空間の情報量が少なかった
  • キャラ・時計・UI に対して、背景も世界観の一部にしたかった
  • 壁紙を変えるだけで雰囲気が大きく変わる
  • UI から切り替えられることで“アプリらしさ”が増す

特に大きかったのは、背景を変えるだけで作品の印象が大きく変わることでした。

時計アプリというより、 「キャラクターが存在する 3D 空間そのものを眺める」感覚に少し近づいてきたと思います。

実装のポイント

今回の実装では、Three.js 側の背景壁に対して画像テクスチャを適用しつつ、 これまで入れていた軽い shader エフェクトはそのまま残す構成にしました。

ポイントになった要素は以下です。

  • MeshStandardMaterial + map
  • onBeforeCompile で既存 shader エフェクトを維持
  • WALLPAPER_PRESETS
  • textureCache
  • backWallState
  • applyBackWallStyle()
  • resizeBackWall() で view size に追従

MeshStandardMaterial + map

背景壁は MeshStandardMaterial を使ったまま、map にテクスチャを設定する形にしました。

これで、単色の壁から画像付きの壁へ移行できます。

const wallMat = new THREE.MeshStandardMaterial({
  color: config.clockHex,
  map: null,
  roughness: 0.95,
  metalness: 0.0,
});

ここで color0xffffff にすれば画像そのままの見え方になりますし、 テーマカラーを入れれば画像に色味が乗るので、空間全体の統一感も出せます。

実際に試してみると、画像そのままの表示も悪くないのですが、 config.clockHex のようなテーマ色を乗せた方が、時計や UI と馴染みやすく、 アプリ全体としてまとまって見えました。

onBeforeCompile で既存 shader エフェクトを維持

背景を画像対応にしても、もともと入れていたノイズベースの揺らぎ表現は残したかったので、 onBeforeCompile を使って fragment shader に後から処理を差し込む方式を継続しました。

これにより、

  • 明るさの微揺れ
  • ごく弱い粒状感
  • 弱い周辺減光

のような演出を、画像背景でも維持できます。

背景画像を単に貼るだけだと“静止画を貼っただけ”になりやすいですが、 わずかでも動きが入ることで、空間に少し空気感が出ます。

今回のシェーダーは強く動かす方向ではなく、 背景にほんの少しだけ生っぽさを足すくらいの弱い表現にしています。 時計やキャラより背景が主張しすぎるとバランスが崩れるので、ここは控えめにしました。

WALLPAPER_PRESETS

壁紙は固定1枚ではなく、プリセットとして管理する形にしました。

const WALLPAPER_PRESETS = {
  none: { label: "None", url: null },
  blue_lain: { label: "Blue Lain", url: "/textures/wallpaper-blue-lain.jpg" },
  orange_lain: { label: "Orange Lain", url: "/textures/wallpaper-orange-lain.jpg" },
  check_girl: { label: "Check Girl", url: "/textures/check-girl.jpg" },
};

こうしておくと、UI 側でリスト表示しやすく、 あとから壁紙を追加するのもかなり楽になります。

最初は1枚直書きでもよかったのですが、 UI から切り替える前提にすると、やはりプリセット化しておいた方が整理しやすかったです。

textureCache

テクスチャ切り替え時に毎回ロードし直さないように、簡易キャッシュも入れました。

const textureCache = new Map();

function getTexture(url) {
  if (!url) return null;
  if (textureCache.has(url)) return textureCache.get(url);

  const tex = texLoader.load(url);
  tex.colorSpace = THREE.SRGBColorSpace;
  tex.wrapS = THREE.ClampToEdgeWrapping;
  tex.wrapT = THREE.ClampToEdgeWrapping;

  textureCache.set(url, tex);
  return tex;
}

壁紙プリセットを切り替えて遊ぶ機能なので、ここは素直にキャッシュした方が気持ちよく動きます。

特に UI から何度も切り替えることを考えると、 毎回ロードし直すより、最初に読み込んだテクスチャを再利用した方が扱いやすいです。

backWallState

背景壁の状態は backWallState に寄せました。

const backWallState = {
  color: config.clockHex,
  wallpaperKey: "check_girl",
  textureEnabled: true,
};

これで

  • 現在の色
  • 現在の壁紙
  • テクスチャの有効 / 無効

をまとめて持てるようになり、 UI 側から状態を読むのも変更するのも分かりやすくなりました。

ここを state 化しておいたことで、 壁紙をただ表示するだけでなく、UI から制御するための土台ができたのが大きいです。

applyBackWallStyle()

背景の見た目更新は applyBackWallStyle() に集約しました。

function applyBackWallStyle() {
  wallMat.color.set(backWallState.color);

  const preset = WALLPAPER_PRESETS[backWallState.wallpaperKey];
  wallMat.map = backWallState.textureEnabled && preset?.url ? getTexture(preset.url) : null;

  wallMat.needsUpdate = true;
}

背景色や壁紙の変更処理をここにまとめておくことで、 UI から setter を呼ぶだけで反映できるようになります。

こういう「見た目の更新処理」を1か所にまとめておくと、 あとで項目が増えたときにも崩れにくいです。

resizeBackWall() で view size に追従

最初は背景壁の PlaneGeometry を固定サイズにしていましたが、 画面サイズやアスペクト比によって見え方が変わるため、最終的には カメラから見た可視範囲に合わせて壁をスケールする方式に変えました。

export function resizeBackWall() {
  if (!wall) return;
  const view = getViewSizeAtWorldZ(config.camera, wall.position.z);
  wall.scale.set(view.width * 1.05, view.height * 1.05, 1);
}

これで、横長画面でも縦長画面でも背景壁が破綻しにくくなりました。

実際、最初は「画像が大きすぎる」と感じて PlaneGeometry のサイズを直接いじっていたのですが、 最終的には固定ジオメトリではなく、画面に追従してサイズを合わせる方が自然でした。

この変更で、リサイズ時の安定感がかなり増しました。

UI 実装

背景機能をただ入れるだけでなく、 今回は UI から切り替えられる形まで進めました。

追加したのは以下です。

  • ツールバーに Wallpaper ボタン追加
  • tplWallpaperPanel
  • Enable Texture
  • Preset
  • Tint Color
  • ui.js から backwall.js の setter を呼ぶ構成

ツールバーに Wallpaper ボタン追加

右上のツールバーに Wallpaper ボタンを追加し、 既存の Model / Info / Rig / Debug と同じ流れでパネルを開けるようにしました。

このあたりは既存 UI の仕組みをそのまま流用できたので、 追加実装としては比較的スムーズでした。

tplWallpaperPanel

パネル本体は template 化しています。

<template id="tplWallpaperPanel">
  <div class="panel__section">
    <div class="section__title">Wallpaper</div>
    <div class="grid">
      <label class="check">
        <input type="checkbox" data-bind="wallpaperEnabled" />
        Enable Texture
      </label>
    </div>
  </div>

  <div class="panel__section">
    <div class="section__title">Preset</div>
    <select class="input" data-bind="wallpaperPreset"></select>
  </div>

  <div class="panel__section">
    <div class="section__title">Tint Color</div>
    <input class="input" type="color" data-bind="wallpaperColor" />
  </div>
</template>

これで UI 構造も既存パネルと統一できました。

必要な項目も最小限で、まずは十分です。

ui.js から backwall.js の setter を呼ぶ構成

UI 側では state を取得して初期表示し、 ユーザー操作に応じて setter を呼ぶだけのシンプルな構成です。

setBackWallTextureEnabled(enabledEl.checked);
setBackWallWallpaper(presetEl.value);
setBackWallColor(colorEl.value);

この形にしておくと、 UI と Three.js 側の責務が混ざりにくく、あとで拡張しやすいです。

たとえば今後、

  • 壁紙ごとの初期 Tint
  • エフェクト強度
  • 背景明るさ
  • repeat / offset

のような項目を増やしたくなっても、同じ方針で追加できます。

実際に変わったこと

今回の変更で見た目はかなり変わりました。

  • 背景画像で一気に作品感が出た
  • Tint 付き壁紙も意外と馴染む
  • 単なる時計ではなく、カスタマイズ可能な3D空間に近づいた

特に、壁紙を差し替えたり Tint Color を変えるだけで、 アプリ全体の雰囲気が大きく変化するのは想像以上に面白かったです。

単色背景のときは「時計+キャラ」という印象が強かったのですが、 背景を UI から制御できるようになったことで、 空間そのものをカスタマイズするアプリ に少し近づいた感覚があります。

実際、ここまで来ると「時計アプリ」というより、 キャラクター付き 3D デスクトップ空間に近い方向へ伸びてきた感じがあります。

まとめ

今回は、MMD時計アプリの背景壁を単色から画像対応へ拡張し、 Wallpaper UI から壁紙・色味・有効 / 無効を切り替えられるようにしました。

実装としては、

  • 背景壁の画像対応
  • shader エフェクトの維持
  • state 管理
  • texture cache
  • UI 連携
  • 画面追従リサイズ

まで含めて一通り整理できたので、 単なる見た目変更ではなく、今後の拡張にもつながる土台になったと思います。

背景を変えるだけでここまで印象が変わるなら、 今後は壁紙だけでなく、時間帯や天気、キャラクターの状態と連動した演出まで広げられそうです。