0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIに論文とOSSを調べさせて、Three.jsの魚を“生き物”にした

0
Posted at

はじめに

こんにちは、わかとです。

最近、スマホを釣り竿にして遊べるWeb釣りゲーム 「技術釣り」 を作っています。

PC側に海を表示して、QRコードをスマホで読み取ると、そのスマホが釣り竿になります。

スマホを振ってキャストして、魚が掛かったら釣り上げる。

さらに普通の魚だけではなく、GoやDockerなどの技術の特徴を魚の見た目や動きに落とし込んで、

遊んでいたら、なんとなく技術の特徴まで分かる

みたいなゲームを作っています。

発表資料を作っているとき、こだわりポイントを

  • 魚の動き
  • 魚の見た目
  • 海のグラフィック

くらいにまとめようとしていました。

ただGitの履歴と実装を見返していたら、

いや、ここ3行で済ませていい量じゃなくね?

となりました。

特に魚については、

  • 泳ぎ方
  • 逃げ方
  • ヒレの動き
  • 水中での見え方
  • 水面越しの屈折
  • 発光の見え方
  • 糸とのつながり
  • 竿への力の伝わり方

までかなり触っています。

この記事では、その中でも

「魚が動いている」から「魚を釣っている」に変わるまで

にやったことを書きます。


先に:論文調査はほぼAIにやらせています

最初にここはちゃんと書いておきます。

僕自身が魚類学や水中光学の論文を大量に読み込んで、全部理解したうえで数式を再現した、という話ではありません。

研究の探索・要約・比較にはかなりAIを使っています。

今回の開発フローはだいたいこんな感じでした。

実際にゲームを動かす
        ↓
「なんか魚っぽくない」
        ↓
AIにコードと画面上の違和感を分析させる
        ↓
関連する論文・OSS・既存実装を調査させる
        ↓
研究結果と自分の実装を比較
        ↓
使えそうな考え方を実装
        ↓
実際のゲーム画面で確認
        ↓
また違和感を見つける
        ↓
もう一回調査

AIに、

「魚って実際どうやって曲がるん?」

「尾びれってずっと同じ振り方してるん?」

「水面越しの魚ってどう見えるん?」

「Three.jsで似た実装ない?」

みたいなことをかなり調べさせています。

この記事で紹介する研究は、研究結果そのものを忠実に再現したという意味ではありません。

AIに関連研究を調査・比較させ、その中からゲーム表現に使えそうな考え方を参考にしています。

実装内の振幅・減衰率・視認性補正などには、ゲーム向けに自分で調整した値も多く含まれます。

個人的には今回、

AIに完成品を作らせる

というより、

AIをリサーチャー兼レビュー役として使う

感じが結構ハマりました。


最初の魚、なんか「ゴミを釣ってる」

魚のモデルを作る。

海に入れる。

泳がせる。

針に掛ける。

リールで手前まで引っ張る。

機能だけ見るとちゃんと魚釣りです。

でも実際に遊ぶと、

なんか魚じゃない。

もっと言うと、

海に落ちてるゴミを糸で引っ張ってる感じがする。

最初は単純に、

「モデルのクオリティが低いからかな」

と思っていました。

ただ、AIにコードや動きを分析させていくと、原因はモデル一個ではありませんでした。

例えば、

  • 巻くと魚がプレイヤーの方を向く
  • 状態が変わる瞬間に急に方向転換する
  • 身体全体が同じ周期で揺れる
  • ヒレまで全部同じように動く
  • 水面越しだと胴体が潰れる
  • 暗い胴体より発光線だけが目立つ
  • 糸が魚の口につながっているように見えない

など。

一個一個だけなら小さい違和感です。

でもこれが重なると、

3Dモデルではあるけど、生き物ではない。

という見え方になりました。

ここから一個ずつ潰していきます。


魚はずっと同じ泳ぎ方をしていない

まずAIに調べさせたのが、実在魚の遊泳です。

Flammang & Lauderによるブルーギルの研究では、加速・制動・後退などによって、尾びれの形状や筋活動が変わることが測定されています。

別の研究でも、泳ぐ速度によって尾びれ内部の筋活動が変化しています。

つまり、

魚 = 同じsin波を永遠に流せばいい

ではなさそうです。

そこで技術釣りでは、魚の泳ぎに複数の状態を作りました。

src/fish.ts
const GAIT_PROFILES = {
  cruise: {
    acceleration: 1.8,
    drag: 0.7,
    amplitude: 0.08,
    frequency: 1.6,
    wavelength: 0.8,
  },

  turn: {
    acceleration: 2.8,
    drag: 0.9,
    amplitude: 0.16,
    frequency: 2.6,
    wavelength: 0.72,
  },

  burst: {
    acceleration: 5.4,
    drag: 0.35,
    amplitude: 0.22,
    frequency: 4.6,
    wavelength: 0.64,
  },

  coast: {
    acceleration: 0.7,
    drag: 0.28,
    amplitude: 0.045,
    frequency: 0.8,
    wavelength: 0.9,
  },

  hooked_burst: {
    acceleration: 7.2,
    drag: 0.3,
    amplitude: 0.3,
    frequency: 5.8,
    wavelength: 0.58,
  },

  exhausted: {
    acceleration: 1,
    drag: 1.25,
    amplitude: 0.025,
    frequency: 0.6,
    wavelength: 1,
  },
};

ざっくり、

状態 役割
cruise 普通に泳ぐ
turn 方向転換
burst 急加速
coast 惰性
hooked_burst 針に掛かった直後
exhausted 疲労

という感じです。

cruiseでは振幅0.08 / 周波数1.6なのに対して、針に掛かった直後のhooked_burstでは0.30 / 5.8まで上げています。

逆に疲労状態では0.025 / 0.6まで落とします。

ここで使っている具体的な数値は論文の値ではありません。

研究から拾ったのは、

運動状態によって身体の使い方は変わる

という考え方です。

数値自体はゲーム画面を見ながら調整しています。


針に掛かった瞬間だけ、一気に逃げる

フッキングした瞬間も普通のcruiseのままだと、かなり味気ないです。

そこで、針に掛かった直後だけ専用の急加速を入れました。

src/fish.ts
triggerCStart(escapeDirection: Vec3): void {
  const direction = normalise(escapeDirection);

  this.cStartRemaining = 0.22;
  this.gait = "hooked_burst";
  this.heading = direction;

  this.velocity = add(
    this.velocity,
    scale(direction, 1.6)
  );
}

魚には、刺激を受けた瞬間に身体を大きく曲げて逃走するC-startと呼ばれる行動があります。

技術釣りではその生体運動を正確に再現しているわけではありません。

ただ、

「掛かった瞬間に一気に逃げる」

という演出の参考にしました。

フッキングすると0.22秒だけhooked_burstへ移行します。

これだけでも、

「ヒットした瞬間に魚が抵抗した」

感じがかなり増えました。


一番効いた修正:「移動方向」と「泳ぎたい方向」を分ける

ここが今回一番面白かったところです。

最初の実装では、

魚が実際に移動している方向

をそのまま魚の向きに使っていました。

普通に泳いでいる魚なら、これでも問題ありません。

右へ進む魚は右を向く。

左へ進む魚は左を向く。

ただ、釣りでは壊れます。

プレイヤーがリールを巻くと、

魚が糸で手前へ引かれる
↓
魚の座標が手前へ移動する
↓
速度ベクトルもプレイヤー側を向く
↓
魚の頭もプレイヤー側を向く

となります。

つまり、

必死に逃げているはずの魚が、自分からこっちに泳いでくる。

これがめちゃくちゃ変でした。

そこで、

正味の移動

と

魚自身の自力遊泳

を分けました。

src/fish.ts
export interface FishMotionSnapshot {
  position: Vec3;
  velocity: Vec3;
  heading: Vec3;
  speed: number;

  // ...

  /**
   * Self-propulsion,
   * separate from translation imposed by a fishing line.
   */
  swim?: {
    velocity: Vec3;
    effort: number;
    turn: number;
  };
}

これで、

実際の移動
→ プレイヤー側

魚自身が泳いでいる方向
→ 沖側

を同時に持てます。

結果、

糸の力で手前へ引かれているのに、魚自身は沖へ逃げ続ける

という表現ができるようになりました。

これはかなり効きました。

モデルそのものはほぼ変わっていないのに、

「オブジェクトが運ばれている」

から

「生き物が抵抗している」

に見え方が変わります。


状態切り替えで魚が瞬間移動っぽくなる

次は旋回です。

ファイト中は魚が左右に逃げます。

最初はモードごとにsin波を使って左右移動させていました。

ただ、

rest
↓
surge

のように状態が変わった瞬間、位相や目標方向まで切り替えると、

ガクッ

と魚が逆方向に動きます。

生き物というよりNPCです。

そこで、

  • 横方向の位相を連続的に積分
  • 横方向の加速度に上限
  • 旋回速度にも上限

を持たせました。

状態としては

「右へ逃げたい」

に一瞬で変わっても、

身体はそこから徐々に右へ曲がる。

この

意思は瞬間的に変わっても、身体は瞬間的には変われない

感じが結構重要でした。


GitHubの魚実装もAIに探させた

論文だけではなく、GitHub上にある魚や生物のProcedural AnimationもAIに調べさせています。

設計時に参考にしたのは、例えば次のような実装です。

  • animal-proc-anim
  • Google WebGL Aquarium
  • Three.js GPGPU Birds
  • PolyFish

ここで参照したのは設計パターンや考え方です。

魚のメッシュ・テクスチャ・第三者コードをそのままコピーしたわけではありません。

animal-proc-anim

この実装では、魚を一本の背骨チェーンとして扱っています。

背骨の各位置に身体幅を持たせて、

中心線が曲がる → 身体とヒレがそこについてくる

という考え方です。

技術釣りでは2Dチェーン自体は使わず、

3Dメッシュ + 頂点シェーダー

として作り直しました。

WebGL Aquarium

WebGL Aquariumでは、魚体の後方ほど変形量を大きくして、

頭を比較的安定させ、尾へ行くほど強く振る

方式が使われています。

この考え方自体は技術釣りでも使っています。

ただし、一般的な水槽と釣りゲームでは一個大きな違いがあります。

水槽なら、

進行方向 ≒ 魚が泳いでいる方向

でほぼ問題ありません。

釣りだと、

進行方向 ≠ 魚自身が泳いでいる方向

になります。

既存パターンをそのまま使うのではなく、

釣りという外力がある状況に合わせて組み直す必要がありました。

ここは結構面白かったです。


魚の3DモデルもThree.jsで自作する

Go魚の3Dモデルも自作しています。

GLBなどの完成した魚モデルを読み込んでいるわけではありません。

まず、魚の身体の断面を定義します。

src/rendering/go-fish.ts
const profile = [
  [-1.86, .018, .016, -.012],
  [-1.67, .19,  .12,  .016],
  [-1.38, .34,  .22,  .041],
  [-.94,  .475, .305, .045],
  [-.37,  .51,  .32,  .034],
  [.23,   .43,  .282, .018],
  [.79,   .28,  .192, .005],
  [1.28,  .125, .097, 0],
  [1.72,  .061, .052, 0],
  [1.9,   .052, .041, 0],
];

これをCatmullRomCurve3でつないで、

各位置に円周状の頂点を生成し、BufferGeometryとして魚体を作っています。

ほかにも、

  • 背びれ
  • 胸びれ
  • 腹びれ
  • 尻びれ
  • 二股の尾
  • 目
  • 口
  • 鰓
  • 発光ノード

などを作っています。

モデル設計では、

頭は尖らせる

胴体はちゃんと厚くする

尾柄は絞る

発光させすぎない

なども決めています。

ポリゴン数を増やすより、

魚として読めるシルエット

を優先しました。


モデル全体を左右に振るだけでは魚に見えない

モデルをそのまま左右へ回転させても、一応泳いでは見えます。

でもかなり

おもちゃ感

があります。

そこで頂点シェーダー側で、魚体全体に連続した波を流しています。

src/rendering/go-fish.ts
float bendZ(float x) {
  float s = clamp(
    (x + 1.45) /
    mix(4.8, 3.8, uNaturalSwim),
    0.0,
    1.0
  );

  float amplitude = mix(
    0.18 + uSwimPower * 0.43,
    uSwimPower * .62,
    uNaturalSwim
  );

  return sin(
    s * uSwimWavelength - swimPhase()
  ) * s * s * amplitude
  + uTurn * s * s * .45 * uNaturalSwim;
}

ポイントは* s * sです。

頭側では変形を小さくして、

後ろへ行くほど振幅を大きくしています。

さらに単純な時間だけではなく、

  • uTurn
  • uEffort
  • uTetherLoad
  • uSwimPower
  • uNaturalSwim

などをshaderへ渡しています。

つまり、

同じ魚モデルに固定の泳ぎアニメーションを再生しているわけではありません。

その瞬間の魚の状態によって身体の動きが変わります。


ヒレまで全部同じように揺らすと、袋になる

これも結構ハマりました。

最初はヒレも身体の波にそのまま乗せていました。

すると、

魚というより、水の中で袋を引っ張ってる感じ

になります。

なので現在は、身体とヒレの反応を少し分けています。

例えば強く泳いでいるときは正中鰭を畳み、尾の形状を変えます。

逆に低速時や旋回時には、胸びれなどの対鰭へ小さな動きを加えています。

src/rendering/go-fish.ts
if (p.x > 1.6) {
  // tail
  p.y *= 1.0 - response * uEffort * .22;
}
else if (abs(p.z) < .12) {
  // median fins
  p.y *= 1.0 - response * (.24 + uEffort * .2);
}
else {
  // paired fins
  float side = sign(p.z);

  p.z *= 1.0 -
    response * (uEffort * .36 - uTetherLoad * .22);

  p.x += response *
    (1.0 - uEffort * .7) *
    (
      .10 * sin(swimPhase() * .65 + side * .55)
      + side * uTurn * .12
    );
}

ここも、

論文の魚をそのまま再現

しているわけではありません。

ただ実魚の研究を見ても、

全部のヒレが同じ周期でただ揺れているわけではない

ことは分かります。

それをゲーム表現に落としました。


「魚は尾だけで泳ぐ」でもなかった

調べている途中で面白かったのが、

魚って尾だけで進んでるわけじゃない

という話です。

Lucasらによる研究では、魚体の前方部分も圧力差によって推力へ寄与できることが示されています。

もちろん技術釣りでは流体力学を計算していません。

なので、

「この論文の物理を再現しています」

ではありません。

ただ、

魚らしさを作るなら尾だけ振って終わりではない

という判断の参考にはなりました。


次の問題:水に入れたら魚が壊れた

魚の泳ぎがかなり改善して、

「結構いいやん」

となりました。

そして実際の海へ入れます。

すると次の問題。

水面越しだとなんか変。

具体的には、

  • 魚体が潰れる
  • ヒレや尾が布みたいに歪む
  • 暗い身体が見えなくなる
  • 発光線だけが浮いて見える
  • 遠くなると魚そのものが認識できない

など。

動きだけ直しても終わりではありませんでした。

ここからAIに、

水中で物体がどう見えるのか

を調査させました。


「青くしてopacity下げたら水中」はやめる

参考にした研究の一つが、Akkaynak & Treibitzによる

A Revised Underwater Image Formation Model

です。

この研究では、水中画像における

  • 物体から直接届く光
  • 水中で散乱して届く背景光

を単純に同じものとして扱えないことが示されています。

ここから参考にしたのは、

水中だから魚全体を青くして透明にする

みたいな一括処理はやめよう、という考え方です。

ただし、この研究は主に水中カメラを対象としています。

技術釣りは、

空気中のカメラ
↓
波のある水面
↓
水中の魚

という構成です。

そのため論文のモデルをそのまま実装しているわけではありません。

あくまで、

水中の光をどう分けて考えるか

の参考にしています。


屈折率1.333を参考に、水面越しの位置をずらす

技術釣りでは、水の屈折率として1.333を参考にしています。

src/rendering/fish-water.ts
const cosWater = Math.sqrt(
  1 -
  (1 - cosAir * cosAir) /
  (1.333 * 1.333)
);

魚が水中に入った場合、そのままの位置を投影せず、

水面越しに見たときの見かけの深さを近似しています。

さらにshader側ではFresnelも計算しています。

src/rendering/fish-water.ts
float rs =
  (cosAir - 1.333 * cosWater) /
  (cosAir + 1.333 * cosWater);

float rp =
  (1.333 * cosAir - cosWater) /
  (1.333 * cosAir + cosWater);

float transmission =
  1.0 - .5 * (rs * rs + rp * rp);

水面に対して浅い角度から見るほど反射が強くなり、

水中から届く光が減ります。

RGBについても同じ割合ではなく、

vec3 extinction =
  exp(-vec3(.58, .22, .17) * path);

として別々の減衰を入れています。


でもこれは「物理的に正しい水」ではない

ここはかなり重要です。

コードにも、

Art-directed coherent refraction,
not a full refracted-ray renderer.

とコメントしています。

つまり、

完全な屈折光線追跡ではありません。

最初は波の高さを魚体各頂点の見かけ位置に強く反映していました。

すると、

魚がグニャグニャになる。

ヒレも尾も布みたいになります。

そりゃそうです。

水面の波によって見え方が変わっているはずなのに、

画面上では魚そのものが変形しているように見える。

そこで魚一匹に共通する光学的な中心を使い、

魚体自体の形はできるだけ保つ方向へ変更しました。

さらに、

Math.max(.55, ...)

のような下限や、

(world.y - center.y) * .72

といった補正値も入れています。

この0.55や0.72は物理定数ではありません。

魚の形を壊さず、水面越しに見えるための調整値です。


そして、物理っぽくしたら魚が消えた

ここが一番おもろかったところです。

屈折を入れる。

Fresnelを入れる。

距離と深度で減衰させる。

RGBも別々に減衰させる。

水面背景とちゃんと合成する。

「よし、結構水中っぽくなった」

と思って実ゲームを起動しました。

魚見えへん。

近距離で横から見るテストでは確かに魚がいます。

でも実際の釣りカメラから見ると、

魚がほぼ認識できない。

ここで、

「描画されている」

と

「プレイヤーから見える」

は全く別だと気付きました。


物理とは別に「魚の存在感」を作る

そこで最終的には、

光学的な減衰とは別に

最低限どれくらい魚体が背景を遮るか

という値を持たせました。

src/rendering/fish-water.ts
const presence = {
  floor: .30,
  surface: .32,

  clearDepth: 2.4,
  falloff: .35,

  near: 34,
  far: 72,
  rangeLoss: .82,

  fin: .85,
  light: .30,
  detail: .18,
};

コード中ではこれを、

artistic readability budget

として扱っています。

つまり、

読めるための予算。

胴体は比較的残す。

ヒレは少し薄くする。

細かい発光部分はさらに弱くする。

ただし深くなったり遠くなったらちゃんと消す。

こうすることで、

水中っぽい

と

魚がいる

を両立させています。

ここでかなり考え方が変わりました。

リアルなゲームグラフィックスって、

物理的に正しい値を並べれば完成

ではない。

むしろ、

人間がその画面をどう読むか

まで含めてリアルなんだと思います。


Go魚を全部光らせたら、光る糸くずになった

Go魚にはシアンの発光表現があります。

最初は、

「技術っぽい魚ならいっぱい光らせた方がかっこいいやろ」

くらいでした。

でも水中に入れると、

暗い魚体より発光線だけが目立って、

光る糸くず

みたいになります。

そこでAIに発光魚についても調べさせました。

Martinらによるハダカイワシ類の発光器研究では、

発光部分は身体全体ではなく、発光組織・色素層・反射構造などを持つ局所的な器官として存在しています。

もちろんGo魚は架空魚です。

ハダカイワシの発光構造を忠実に再現したわけでもありません。

参考にしたのは、

不透明な身体 + 局所的な発光

という構成です。

そこで、

  • 暗い胴体はちゃんと残す
  • 発光は局所的にする
  • 水中では細い発光線を抑える

という方向にしました。


海もThree.jsのWaterを置いて終わりではない

魚だけではなく、海自体もかなり触っています。

Three.jsのWebGLRenderer上で、

自作のShaderMaterialを使って海を描画しています。

中では、

  • proceduralな空
  • 複数周波数の波
  • 空の反射
  • Fresnel
  • 太陽のglint
  • 着水波紋
  • 遠景haze
  • vignette

などをまとめています。

src/rendering/ocean-scene.ts
float fresnel =
  0.035 +
  0.965 *
  pow(
    1.0 - max(dot(-rd, n), 0.0),
    4.5
  );

READMEには最初から、

「魚や説明がなくても眺めていたい海」

と書いています。

なので、

魚がいない状態でも成立する景色

を結構意識しています。


最後に「魚・糸・竿・水面」を全部つなげる

魚だけがどれだけ自然に動いても、

魚だけ勝手に暴れていたら釣りには見えません。

最終的には、

魚
↓
糸
↓
竿
↓
プレイヤー

まで同じ力が流れているように見せる必要があります。

糸についても、

竿先
↓
水面への進入点
↓
魚の口

までちゃんとつなげています。

以前は糸が水面上の見えない点へ接続されていて、

「結局何を引っ張ってるん?」

という見え方になっていました。

魚の口へ接続して、初めて

魚を引いている

と読めます。

竿も一本の黒い線ではなく、

  • 先細りするブランク
  • ガイド
  • リール
  • しなり
  • 材質の反射

までThree.jsで作っています。

さらに、魚・糸・竿の細かい演出には同じ泳ぎの位相を使う部分もあります。

ただし、

魚・流体・糸・竿を連成した物理シミュレーション

ではありません。

あくまで、

画面上で「力がつながっている」と読めるようにする演出

です。

でもこれが入ったことで、

自分の中では初めて

「魚が動いている」

から

「魚を釣っている」

になりました。


コミット履歴を見ると、ずっと「違和感」を潰していた

あとからコミット履歴を見返すと、かなり分かりやすいです。

最初に海と魚を作って終わりではなく、

海と魚を作る
↓
魚の移動をサーバーと同期
↓
スマホの振りをキャスト距離へ反映
↓
魚の接近と逃走を同期
↓
魚影と逃走カメラを調整
↓
魚種と図鑑を統合
↓
アタリのUIを調整
↓
Dockerクジラを統合

みたいに、後半のコミットほど

「動くか」より「どう感じるか」

へ寄っています。

特に魚については、

実装した → 違和感を見つけた → 原因を調べた → 修正した

を何回も繰り返しています。

今回の記事自体も、完成時のコードだけを見るより、

コミットの変遷を見る方が何にこだわったか分かりやすかった

です。


AIに「コードを書かせる」以外の使い方がかなり良かった

今回、個人的に一番学びがあったのはここかもしれません。

生成AIを使うと、

コードを生成させる

ところに目が行きがちです。

もちろん今回も実装にはかなりAIを使っています。

でも一番役立ったのはむしろ、

「何がおかしいのか」を一緒に考えさせること

でした。

例えば、

魚がゴミっぽい

という感想だけだと、そのままでは実装に落とせません。

そこからAIに、

向きがおかしい?
身体変形がおかしい?
ヒレがおかしい?
光学表現がおかしい?
糸との接続がおかしい?

と分解させる。

それぞれについて関連研究を探させる。

OSSで他の人がどう解いているか調べる。

自分のコードと比較する。

その上で、

どれを採用するかは実際の画面を見て決める。

このループがかなり強かったです。


AIが出したものをそのまま信じない

一方で、今回かなり大事だったのがこれです。

最後は実画面で見る。

AIが、

「この処理で自然になります」

と言っていても、

ブラウザを開くと普通に

魚見えへんやん

みたいなことが起きます。

論文に近い処理を入れたとしても、

実際のカメラ・魚の大きさ・背景・画面サイズでは成立しないことがあります。

なので今回は、

AIの分析
↓
実装
↓
自動テスト
↓
ブラウザで確認
↓
ダメなら戻る

をかなり繰り返しました。

この

「AIの答え」より「動いている画面」を優先する

のは、グラフィックス系だと特に大事だと思いました。


結局、「リアル」って何なんだろう

今回一番感じたのは、

リアルさは、一個の高精度なシミュレーションから生まれるわけではない

ということです。

魚モデルがめちゃくちゃ綺麗でも、

プレイヤーの方を向いて逃げていたら変。

物理っぽい屈折でも、

魚が見えなければゲームとして変。

綺麗な尾アニメーションでも、

糸も竿も無反応なら釣りには見えない。

逆に、

  • 魚自身は沖へ逃げる
  • 糸の力で実位置は手前へ来る
  • 状態によって泳ぎ方が変わる
  • 身体とヒレがそれぞれ反応する
  • 水中ではちゃんと減衰する
  • でも魚体は認識できる
  • 糸が口につながる
  • 竿がその力に反応する
  • 水面にも波紋が出る

みたいな小さい手掛かりが矛盾せずにつながると、

急に

「魚がそこにいる」

感じが出てきます。

今回やっていたのは結局、

リアルな魚をシミュレーションすること

というより、

「魚を釣っている」と感じるための矛盾を一個ずつ消すこと

だったのかもしれません。


まとめ

技術釣りでは、

魚の3DモデルをThree.jsで自作しました。

魚の泳ぎもProcedural Animationで作りました。

移動と自力遊泳を分離しました。

水面越しの屈折やFresnelも入れました。

物理っぽくした結果、魚が見えなくなったので視認性も調整しました。

魚・糸・竿・水面までつなげました。

そしてその過程ではAIに、

魚の遊泳研究

水中光学

発光魚の研究

GitHub上の先行実装

を横断して調べさせています。

研究やOSSをそのまま再現するのではなく、

研究・先行実装
↓
使えそうな考え方を抽出
↓
自分のゲームへ実装
↓
実際に遊ぶ
↓
違和感を潰す

という使い方でした。

最初は、

「スマホを釣り竿にしたらおもろくね?」

くらいから始めたゲームです。

気付いたらAIに魚の筋活動と水中光学の論文を読ませていました。

釣りゲーム、業が深い。


参考にした研究・実装

今回の調査で特に参考にしたものです。

遊泳

  • Flammang & Lauder, Speed-dependent intrinsic caudal fin muscle recruitment during steady swimming in bluegill sunfish, 2008
  • Flammang & Lauder, Caudal fin shape modulation and control during acceleration, braking and backing maneuvers in bluegill sunfish, 2009
  • Lucas, Lauder & Tytell, Airfoil-like mechanics generate thrust on the anterior body of swimming fishes, 2020

水中光学

  • Akkaynak & Treibitz, A Revised Underwater Image Formation Model, CVPR 2018

発光魚

  • Martin et al., Variation in lanternfish photophore structure: A comprehensive comparative analysis, 2024

公開実装

  • argonautcode / animal-proc-anim
  • Google WebGL Aquarium
  • Three.js GPGPU Birds
  • PolyFish

各研究については「このゲームが研究内容を正確に再現している」という意味ではなく、実装方針を考えるための参考として利用しています。

特に水中表現には視認性を優先した独自の補正値が含まれています。

おわり

発表ではここまで全部話す時間がないので、

「魚がちゃんと生き物っぽく動く」

くらいにまとめました。

でも中身を見ると、思っていた以上にいろいろやっていました。

実装を掘り返して記事を書くの、普通におもろいですね。

次は、

スマホをどうやって釣り竿にしたか

とか、

WebSocketでPCとスマホをどう同期しているか

あたりも書けたらと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?