[JavaScript] Lain Clock アップデート — ZIP文字化け修正・床パターンシステム・モデル復元機能

[JavaScript] Lain Clock アップデート — ZIP文字化け修正・床パターンシステム・モデル復元機能

はじめに

昨日公開したMMD時計アプリ「Lain Clock」に、大幅な改善を入れたメモです。

[JavaScript] Lain Clock アップデート — ZIP文字化け修正・床パターンシステム・モデル復元機能

[JavaScript] Lain Clock アップデート — ZIP文字化け修正・床パターンシステム・モデル復元機能

1. ZIPファイル名のShift_JIS文字化け修正

問題

MMDモデルのZIPをドロップして読み込むと、ファイル名が □□□□□qロT のように文字化けする。IndexedDBの中身も壊れている。

原因

ZIPフォーマットは1989年に設計され、ファイル名のエンコーディング規定がなかった。日本のWindowsで作られたZIPはShift_JIS(CP932)でファイル名が格納されるが、JSZipはそれをUTF-8として解釈するため文字化けが発生する。

2006年にbit 11(Language Encoding Flag)が追加され、UTF-8であることを明示できるようになったが、MMD界隈のZIPは古いツールで作られたものが大半で、このフラグは立っていない。

解決策

JSZipにファイル名の解決を任せず、ZIPのCentral Directoryを自前でパースする方式を採用した。

function parseZipFilenames(buffer) {
  const view = new DataView(buffer);
  const sjis = new TextDecoder("shift_jis");
  const utf8 = new TextDecoder("utf-8");
  const names = [];

  // End of Central Directory を後方検索
  let eocd = -1;
  for (let i = buffer.byteLength - 22; i >= Math.max(0, buffer.byteLength - 65557); i--) {
    if (view.getUint32(i, true) === 0x06054b50) { eocd = i; break; }
  }
  if (eocd === -1) return names;

  const count = view.getUint16(eocd + 10, true);
  let offset = view.getUint32(eocd + 16, true);

  for (let i = 0; i < count; i++) {
    if (offset + 46 > buffer.byteLength) break;
    if (view.getUint32(offset, true) !== 0x02014b50) break;

    const flags   = view.getUint16(offset + 8, true);
    const nameLen = view.getUint16(offset + 28, true);
    const extraLen  = view.getUint16(offset + 30, true);
    const commentLen = view.getUint16(offset + 32, true);

    const nameBytes = new Uint8Array(buffer, offset + 46, nameLen);
    const isUtf8 = (flags & 0x800) !== 0;
    names.push(isUtf8 ? utf8.decode(nameBytes) : sjis.decode(nameBytes));

    offset += 46 + nameLen + extraLen + commentLen;
  }
  return names;
}

ポイントは以下の通り。

  • Central Directoryの各エントリからbit 11を確認
  • フラグが立っていればUTF-8、なければShift_JISでデコード
  • JSZipはデータ展開だけに使い、ファイル名の解決は自前で行う
  • ブラウザの TextDecoder('shift_jis') は標準サポートなので外部ライブラリ不要

この修正はLain Clockだけでなく、同じパイプラインを使っていたMMD to GLBツールにも適用した。

2. 床パターンシステム

設計

床のシェーダーをプラグイン構造に変更した。

src/floor-patterns/
  index.js       ← レジストリ
  brick.js       ← ブリック(既存)
  paw.js         ← 肉球 + チェッカー + グロー
  hexagon.js     ← 六角タイル
  stars.js       ← 流れる星
  wave.js        ← 波
  diamond.js     ← ひし形タイル
  neon-grid.js   ← ネオングリッド
  circuit.js     ← 基板パターン
  solid.js       ← 単色

各パターンは統一フォーマットで定義する。

export default {
  name: "brick",
  label: "Brick",
  uniforms: {
    uScale: { value: 10.0 },
    uMortar: { value: 0.01 },
    // ...
  },
  vertexShader: `...`,
  fragmentShader: `...`,
}

mmd-floor.js はパターン定義から ShaderMaterial を自動生成し、uTimeuAlpha といった共通uniformを注入する。選択されていないパターンはGPUに一切触らないため、パターンをいくら増やしてもノーコスト。

透明度の自動注入

全パターンに透明度を対応させるため、各シェーダーを個別に書き換えるのではなく、マテリアル生成時にフラグメントシェーダーへ自動注入する方式を採った。

// 先頭に uniform 宣言を追加
frag = 'uniform float uAlpha;\n' + frag;

// main() の最後の } の直前に alpha 乗算を挿入
const lastBrace = frag.lastIndexOf('}');
frag = frag.slice(0, lastBrace) + '  gl_FragColor.a *= uAlpha;\n' + frag.slice(lastBrace);

この方法ならシェーダーの内部構造に一切依存しない。どんなパターンを追加しても透明度が自動的に使える。

パフォーマンス

全パターンがプロシージャルシェーダー(GPU内の計算のみ)で完結しており、テクスチャを使わない。PBRマテリアル(MeshStandardMaterial)より軽い。

3. モデル状態の保存・復元

変更前

起動時に必ずハードコードされたデフォルトモデル(lain_v1)が読み込まれ、前回の状態は失われていた。

変更後

  • settings.activeModels に表示中のモデルキーを保存
  • 起動時にIndexedDBから復元
  • デフォルトモデルもモデル一覧に表示し、他のモデルと同じ操作で切り替え可能
  • モデル無し(時計のみ)の表示にも対応

デフォルトモデルの読み込みもZIPパイプラインに統一した。/public/clock/models/lain_v1.zip からfetchし、IndexedDBのモデルと全く同じ loadActorFromRecord で読み込む。コードパスが一本化され、Shift_JISデコードも自動的に適用される。

4. 設定のリアルタイム更新

変更前

設定パネルの値を変更しても、Saveボタンを押すまで反映されなかった。

変更後

input イベントリスナーを追加し、スライダーやカラーピッカーの操作が即座に画面に反映されるようにした。Saveボタンは localStorage への永続化のみの役割になった。

settingsBody.addEventListener("input", (ev) => {
  const t = ev.target;
  if (!t.dataset?.bind) return;

  if (t.matches('[data-bind="floorAlpha"]')) {
    setFloorAlpha(toNum(t.value, 1));
    return;
  }

  collectCurrentSettings();
  applySettingsToRuntime(currentSettings);
});

まとめ

一日で入れた変更の一覧。

  • ZIPファイル名のShift_JISデコーダー(Lain Clock + MMD to GLB)
  • 床パターンシステム(9種類のプロシージャルシェーダー)
  • 床の透明度対応(uAlpha自動注入)
  • モデル状態の保存・復元
  • デフォルトモデルのZIPパイプライン統一
  • デフォルトモデルのモデル一覧表示
  • 設定パネルのリアルタイム更新
  • 設定パネルにパターン選択ドロップダウン追加
  • 設定パネルに床透明度スライダー追加

バグ修正から始まった一日が、結果的にアプリ全体のアーキテクチャ改善になった。ZIPの文字化けという35年前の設計判断に殴られたおかげで、コードが綺麗になったのは皮肉な話だ。