畜史が成りしものを、思考学が曲がった判断を、肥料学がなぜ死んだかを録すなら、技術学は実装の層で何を使い、何を使わないかを録す。
目的はただ一つ。軽量な頭(小さなAIモデル・記憶を持たない次のこっこ)でも、この書を読めば島ぐらしの水面や影がどう描かれているかをゼロから調べ直さずに再現できること。
畜学七相の七「一器に納む」=HTML1ファイル自己完結、という掟の具体的な実装形は次の三層に収束している。
| 層 | 使うもの | 使わないもの |
|---|---|---|
| 描画 | 生WebGL1(自前シェーダ)/Canvas2D/SVG | Three.js・Babylon.js等の外部3Dエンジン |
| 音 | WebAudio(矩形波/三角波/ノイズ合成、自前インパルス応答リバーブ) | 外部音声ファイル(mp3/wav)の同梱・CDN読込 |
| 保存・共有 | localStorage(進行状況)/Firebase(世界ランキングrankings/*のみ) | それ以外の外部API・バックエンド |
外部3Dエンジン禁止は比喩ではなく実務の制約。 島ぐらし2旧版(mori2/)はThree.js依存+シェーダprecision不統一(頂点highp・フラグメントmediump)でiOS起動不能となり、公開から3日で取り下げられた(詳細は肥料学 巻二)。後継のshima2/は素のWebGL1・全シェーダprecision highp float統一で建て直し、現在mori/にも同じ設計が継承されている。
WebGLを使うのは mori・shima2・shinshima・galaxy・pulse・rom・splat3d の7作のみ。いずれも外部ライブラリを使わず、createShader→compileShader→createProgram→linkProgram を自前ヘルパーで書く共通スタイル。
シェーダ12種:depth/meshDepth(影)・terrain・water・mesh・grass・sky・sprite・noise・bright/blur/post(ブルーム)。シャドウマップ・fbmノイズ地形・水面フレネル・大気フォグ・ACES風トーンマップ・GPUハイトマップ生成を実装。
品質自動フォールバック:HQシェーダのコンパイルをtry/catchし、失敗時は簡易版へ自動降格(低性能端末対応)。
moriの技術的祖先。シェーダ約10種、構成はmoriと対応(空・水面グリッド・GPUハイトフィールド・シャドウマップ・風になびく草)。
全フラグメントシェーダ共通のprecision highp floatプレフィックスを先頭に付与する設計。mori2の死因(precision不統一)を踏まえた直接の対策がここにある。
シェーダ3種(星雲・瞬く星・インスタンシング火花)のみの軽量構成。WebGL2優先取得、WebGL完全非対応環境向けの2D星空フォールバックあり(6作中唯一)。メインゲームプレイはCanvas2D、WebGLは背景レイヤー。
シェーダ2種(スプライト・トンネル)。z除算による疑似3D遠近、頂点シェーダの羽ばたきアニメーション。ANGLE_instanced_arrays必須(非対応環境は例外throw=救わない設計)。
シェーダ1種のみ。SDF(符号付き距離関数)で六角形クリスタルを描画、フレネル風リムライト・コア発光。6作中唯一alpha:trueでCanvas2D背景に透過合成。地形もシャドウも要らないなら、この規模で十分という実例。
シェーダ2種(地面・ペイント面/スプライト)。プレイヤーの塗り跡をCanvas2Dで描画→WebGLテクスチャへ部分転送しワールド合成するインクペイント表現が核。地形は平面的で、mori/shima2にあるシャドウ・水面フレネル・ブルームは無い。
mori/shima2の直系(precision highp統一・tryProg・パックデプス影)。シェーダ13種。本物の平面リフレクション:y=0を鏡面にする行列を掛けた鏡像カメラで空・地形・プロップを半解像度FBOへ描き直し、水シェーダが波法線でゆがめてサンプル(鏡像パスは巻き順反転+uClipYで水面下をdiscard)。
インスタンシング草原:ANGLE_instanced_arraysで葉1枚×約2.6万インスタンスを1ドローコール、風の帯状波+プレイヤーの足元で草が割れる。非対応端末は同じシェーダのまま展開VBOへ自動フォールバック(pulseの「切り捨て」と対極、moriの「救う」を継ぐ)。雲は2層fbm+太陽側の縁が銀色に光るシルバーライニング。影は2048px・PCF9点。
シン島ぐらしの水中表現を既存作へ移植した実例。別世界は月ステージ方式の再生成(島シード^定数でハイトマップを作り直し、全高度を沈めて深い盆地に)。コースティクスは既存の地形シェーダへ uniform 2つ(uUnder/uT2)を足すだけで、tryProgのフォールバック側は無改造=非対応端末では自動的に消える。水中の色は毎フレームの光源計算をG.inSeaで上書き。
絵文字に潜水艦は存在しないため、アトラスの空きセルへCanvas 2Dで手描き(CELL_BARと同じ手法)。深度連動の「かいていわざ」はそらわざのHUD・ボタン・Fキーをそのまま再利用し、発射関数だけ分岐=UIを増やさず動詞を増やす。深海滞在中はセーブしない設計で島のセーブデータを守る。
窓の顔は焼きこまず、実描画時にセル0(公式svg)を重ねる方式に変更(初版は128px角セルに34px相当で焼きこんでいて粗かった)。この修正中に見つけた別バグ:スプライト一括描画がdepthMaskを明示しておらず直前の状態を引き継ぎ、同じdrawArrays内で重なる複数スプライトが自分自身の深度に負けて消えるという不具合があった。影バッチと同じく描画の前後を`depthMask(false)`/`true`で挟んで解消(乗り物を本体に重ねる等、他の重ね描き箇所にも薄く効いていた可能性がある)。
雲の上ステージ=「雪の高度帯リマップ」:深海(heights×0.85−24=盆地)の姉妹技。heights[i]=12.7+heights[i]×0.15 で全地形を雪の高度帯へ持ち上げると、既存シェーダの雪分岐がそのまま効いて一面の白い雲海になる。シェーダ無改修・水面は地形の下へ沈んで自動的に消える。同じハイトマップ一枚が、係数と平行移動だけで島・深海・雲の上の三世界になる。もう一つの教訓:ミサイル発射音に別ゲームのSound.dash()を写して発射のたび例外を出した——兄弟の木でも音器官のメニューは木ごとに違う。移植コードの関数はその木に実在するか確かめてから。
ヘリコプターも「顔を焼きこまず窓に重ねる」方式へ:初版は実在する絵文字🚁をそのまま採用したが、写実的なOS標準レンダリングがフラットな絵柄と衝突し「乗り物の中にいる」感が出なかった。潜水艦と同じ設計(機体は手描き・窓の顔は実描画時にセル0=公式svgを重ねる)に統一。実在する絵文字だからと安易に採用せず、既存の乗り物と絵柄の系統が揃うかを先に見る。
移植したのは①平面リフレクション(半解像度FBO・鏡像行列・水面下discard・海/雲の上ステージ中は自動オフ)②インスタンシング草原(最大2.6万本、非対応環境は展開VBOへフォールバック)③銀縁シルバーライニング雲+シャドウ2048px・PCF9点。当たり判定・経済・セーブ形式などの「設定」は一切変更していない。
検証で「移植した高品質シェーダが実は一度もコンパイルに成功していない」ことを発見。検証フックに現在の品質層を返すgfx()を新設して調べると、水面の高品質パスは常にfalse、草はインスタンス本数も旧来の頂点本数も常に0=一切描かれていないことが判明した。
原因はシェーダの精度不一致によるANGLE(多くの実ブラウザが使う厳格なWebGL検証系)のリンクエラー:水面は頂点シェーダがuWLをmediump明示・フラグメントは無指定でhighpになり不一致、草は頂点シェーダのuCamが既定でhighp・フラグメントはprecision mediump float既定でmediumpになり不一致。草にはフォールバック先が無く、この不一致は移植より前から存在していた実機バグ(草は元から一枚も生えていなかった可能性が高い)。フラグメント側の精度宣言をVSに合わせて明示するだけで解消し、リフレクションON/OFFの見た目差・2.6万本の草原描画・地上/深海/雲の上を行き来してもpageerrorゼロを確認した。
教訓:失敗を握りつぶして自動フォールバックする設計は端末互換性には有効だが、フォールバック自体が常態化すると気づく手段が無くなる。検証フックに「今どの品質層で動いているか」を常時返す関数を用意し、実際に高品質側と簡易版で見た目が変わるかをスクリーンショット比較まで行って初めて、配管の生死でなく「実際に選ばれているか」を確認できる。
補二は見た目(リフレクション・草・雲)だけで「空・海の動き」自体はまだだった。太陽の半日ぶん反対側を通る月(ディスク+月あかりのにじみ、夜の陰影方向も月へ差し替え)と、水面の実ジオメトリ波(1枚クアッド→72×72格子メッシュを頂点シェーダで実際にY変位、有限差分で法線算出)を追加移植した。当たり判定・満潮位・地形生成・経済・セーブの「設定」は無変更。
水面の頂点シェーダに新設した経過時間の共有uniformは、補二で見つけたのと全く同じ危険な形(VS/FS間の精度不一致でANGLEがリンク拒否)を新たに持ち込むところだった。今回は不具合が起きてから検証で発見するのでなく、実装した瞬間に「補二と同じパターンだ」と気づいて先に両側をmediumpへ明示的に統一し、精度低下による長時間プレイ時のジッターを避けるため時刻値を周期的に折り返した。
教訓:肥料学の「同じ死因を持つ他の実装が無いか」という横展開の作法は、技術学(同じ技術的パターンを持つ新しいコードを書くとき)にも同じ形で効く。一度見つけたバグの「形」を覚えておけば、次に同じ形の変更をするとき発生する前に気づいて防げる。gfx()のような品質層の可視化を検証フックに常設しておくと、この種の再発をすぐ検知できる担保にもなる。
島ぐらしには既にシン島ぐらしの海(反射・ジオメトリ波・月・深海コースティクス)が入っていたので、「しおさい2の綺麗なグラフィックを移植」の依頼に対し、しおさい2=シン島ぐらし+4新カテゴリと捉え、島ぐらしに無いものだけに絞った:①🦠夜光虫②本物のゲルストナー波③波がしらの泡④🌙月の道。しおさい2の濡れ砂・潮位連動コースティクス・月の応答は潮というゲーム機構に密結合なので、潮の無い島ぐらしには持ち込まない。「綺麗なグラフィックを移植」は総量コピーでなく、移植先に欠けていて世界観に合う新カテゴリだけを選ぶこと。
夜光虫はANGLE_instanced_arraysで約6000粒を1ドローコール(非対応端末は展開VBO)。水面下の海域だけに配置し、夜(1−dayF)かつ島の通常ステージ(深海・雲の上以外)でのみ加算合成で描く。ゲルストナー波は島ぐらしの水面が当たり判定に使わない純粋な見た目なので、単純3正弦波を方向・波数・角速度のゲルストナーへ置換し波頭をとがらせた。精度の作法は補二・補三の直接の継承:新しい共有uniformは作らず既存のuT(VS/FSともmediump)を使い、月の道のuMoonDir/uMoonStrはフラグメント専用にしてVS/FS精度不一致を構造的に起こさない形にした。検証はgfx()に夜光虫の品質層(plankProg/plankInstanced/plankN)を足し、6000粒がインスタンシングで生きていることを実測、夜のスクリーンショットで夜光虫・波・泡・反射を目視、昼で消えることを確認。既存作へ新カテゴリだけを足す移植では、gfx()にその新カテゴリの生死を返す一行を足すのが補二の「動いていなかった」を二度と踏まない最小の保険。
シン島ぐらし直系の土台に「潮」を中心とした4カテゴリを新設。動的潮位+3成分ゲルストナー実ジオメトリ波:水面の高さを「潮位+波」の一つの式にし、JSの判定(水没・泳ぎ・錨の沈み・敵の浄化)とGPUの頂点変位が同じ式を共有する。平面リフレクションの鏡面も潮位に追従(鏡像行列の並進項とuClipYを毎フレーム更新)。
濡れ砂の記憶:splat3dのCanvas2D→WebGLテクスチャ転送の配管を「潮が触れた場所」の記録に意味だけ差し替え、引き潮のあとに約8秒かけて乾く濡れた浜を残す。夜光虫:ANGLE_instanced_arraysで約6000粒を1ドローコール(非対応端末は展開VBOへ自動フォールバック)。波がしらとこっこの航跡で発光が強まり、加算合成でブルームに乗る。潮位連動コースティクス:moriの深海uUnder/uT2方式を移植し、現在潮位より下の地形にだけ月光の網を出す=潮が満ちると浜が光の網に包まれ、引くと消える。月の応答:入力ホールドに合わせ月ディスクの指数・月あかり・雲の透過を変調(立ち上がり0.35秒・戻り2秒)。
教訓(TDZ):GLSL共通チャンク(const文字列)を新しいシェーダから参照するときは宣言順に注意。宣言より前で使うとCannot access before initializationでスクリプト全体が起動時に死ぬ。node --checkは構文しか見ないためこれを捕まえられず、「ページを開いて検証フックが生えているか」までが起動確認。
WebGLを使わない大多数(Canvas2D・SVG)はこの型に収束している。新作の既定はこちらから始めるのが最も再現しやすい。
requestAnimationFrameによる固定/可変タイムステップのゲームループ。
devicePixelRatioでcanvas実ピクセルを合わせ、CSSサイズは論理ピクセルのまま拡大鈍化を防ぐ。
単一2Dコンテキストへの直接描画(レイヤー分離なし)。パーティクルは配列管理+毎フレーム全走査で十分な規模。
pointer/touchイベントを共通化し、PC/スマホ両対応。
WebAudioで矩形波/三角波/ノイズを合成(romの合成音はCanvas2D側ゲームプレイと共存する好例)。SVGの2作(jusshiki/story)は静的な図表が中心で、アニメーションはCSS transitionまたは軽量なJSでの属性書き換え。
韓国語IMEは合成中に直前の字が変わる(요+ㄱ→욕→요가)。合成中に入力欄の値を消すとIMEが壊れるため、値をゲーム中は消さないストリーム方式を採る:文が終わったら「現在文の開始位置base」を進めるだけにし、判定はvalue.slice(base)と目標文の先頭一致長、加点はその単調増加分だけ。パッチム合体の揺れは自己修復し、二重加点も起きない。ミスは「間違った字を通り過ぎて次を打ったとき」だけ数える(末尾1字は合成中とみなす)。実inputは透明(font-size:16px=iOS自動ズーム防止)で置き表示はspan着色が担う。プレイ中はpointerdownをpreventDefaultしてキーボードを閉じさせず、visualViewportで入力ドックをキーボードの真上へ。ゴースト録画は高度を0.2秒刻みで録ってlocalStorageへ保存し、次走で線形補間して公式SVGをglobalAlpha=0.3で描くだけ──リプレイに必要なのは入力の再現でなく結果(高度軌跡)だけ、という割り切りが軽さの芯。追補(実機スマホの教訓):スマホの韓国語キーボードは入力が素直でない──文末スペース確定の癖・候補タップの自動スペース・候補確定でのスペース抜け・iOSダブルスペースの「. 」挿入。厳密一致だと一度ズレた瞬間から永久に全部不一致になる。対策は判定の寛容化(スペースは両側とも任意・半角/NBSP/全角同一視・句読点ゴミ無視)と、手が止まった隙に確定済みの古い文を入力欄の前方から削るアイドルトリム(合成中はcompositionイベントで避け、合成が長引くAndroidは900msで強制。値の内容は変えず前方を削るだけなので安全)。タイピング判定は「入力を正とする」のでなく「ノイズ込みの入力から意図を読む」のがモバイルIME対応の芯。英語版(/sorageri-en/)への横展開では、言語差で判定を3点だけ足した(大文字小文字を区別しない・アポストロフィ/句読点は打っても省いてもよい・スペース任意)。英語は語の途中で折り返すと読めないので、文字は単語ごとにまとめて折り返す描画にした。保存キーとランキングノードは別名前空間(sorageriEn)にして同一端末で韓国語版と混ざらないようにした=同じ遊びの多言語展開は「判定の寛容化+別名前空間」の二点セットで量産できる。追補(計算生成スカイ):空蹴りの魂は「景色を楽しむ」=景色が終わりが商品。mori のフル3Dは過剰なので持ち込まず、galaxy/pulse型の二層で最小適用した──背景に#skyのWebGLキャンバスを1枚足し、フルスクリーン単一フラグメントシェーダで空を高度から計算生成(2層fbmの雲・高空の星・オーロラ)、遊び本体は前面の既存Canvas2Dがそのまま担う。シェーダ内の高度写像を Canvas2D のsy()と同じ式にして二層の視差を一致させ、下地はJS側skyColors()の実色をそのまま継ぐので既存の色は不変(計算生成分は純粋加算)。空は低周波なので実解像度0.7×で塗り面積を削り、precision highp float統一。WebGL非対応なら初期化がnullを返し従来のグラデ空にフォールバック=純粋な上乗せ。上昇でも落下でも同じ空を通るので景色が高度で連続的に流れ、落下が「終わりの余韻」として成立する。
Canvas2D単体。群れ最大48羽・黒こっこ最大6体を毎フレーム全走査(巻三の素朴な型で足りる規模)。長さは全て u=min(W,H)/720 倍で解像度非依存に。輪をつくる力は、指の点からの距離が目標半径になるよう符号つきの誤差で引き/押す(外なら引き、内なら押す)=これで「殻」はできる。押している間その半径を縮め、離すと広げる=一つの入力の時間軸に深さが出る。落とし穴:殻はできても輪にならない(彗星の尾)──通常のboidsの分離は距離でしか押し合わないため、全羽が同じ方向から追いつく状況では角度が偏ったまま安定する。実測では黒こっこが指の真上にいて周囲に9〜18羽いるのに、8方位のうち2〜3セクタしか埋まらなかった。解=角の散らし:各羽について「指を中心とした角度」を取り、角度差がセクタ幅の約1.2倍以内の相手から接線方向へ押し合う(指に近すぎる羽で発散しないよう頭打ちにする)。これだけで8/8セクタが埋まる。"輪"が要る群れでは、距離の分離だけでなく角度の分離を必ず持つ。 包囲の判定と可視化:対象を中心に半径R内の羽を8セクタへ振り分け、1羽以上入ったセクタ数で包囲率を出す(密度でなく被覆で測るので羽の偏りに強い)。空きセクタの連続runの中心角=すきまの向きで、対象はそこへ逃げる。UIは対象の足もとに8本の円弧を描き、埋まったセクタだけ光らせる=どこに穴があるかが一目で分かる。見た目の教訓:真上から見た盤面で「夜明け」を描くとき、空のグラデーションの上に地面レイヤーを全面に敷くと夜明けの色が全部隠れる(覆いのαを夜明けの進みで下げて透かす)。終端で群れを画面外へ飛ばすと「去った」に見えるので、薄く・小さくしながらゆっくり昇らせると真上視点でも「昇天」に読める。
mori/shima2の構成を土台にする。ただし技術的複雑度は格ではない──必要な複雑さだけを持ち込む(romのように単一シェーダで足りるなら、それで足りる)。
Canvas2Dのみで作る(巻三の型に従う)。畜物語の大多数はこちら。WebGLを使う理由が無いなら使わない。
メインの遊びがCanvas2Dで背景演出だけWebGL(galaxy/pulse型)か、逆にメインをWebGLにしてHUD/UI/生成物だけCanvas2D(mori/rom/splat3d型)かを先に決める。中途半端に混在させない。
mori2の死因を繰り返さない。フラグメントシェーダ先頭に共通プレフィックスを用意し、全プログラムに付与する(shima2方式)のが最も事故りにくい。
moriの「品質自動フォールバック」とpulseの「非対応なら例外throw」は対極の判断。畜物語のターゲット(親子)を考えれば、原則は救う方。