← 作品にもどる 解説「リヴァイア算」へ このノートを .md で落とす

リヴァイア算 技術ノート

この作品の海と体が、どの式・どの値で動いているか。
解説ページ「リヴァイア算」は高校生にも読めるように書いています。こちらはその裏側で、実際に動いているコードと数値をそのまま載せたものです。作った人:アトリエファクトリー(aTELiER FACTORY)。


0. 全体の作り

page/
  index.html   作品+解説(1 つの住所。中で画面を切り替える)
  sea.js       海・空・泡・しぶき・描画の中心(約 2,800 行)
  creature.js  体の形(節・太さ・とげ・ひれ)
  rig.js       書き出した体(glb)に骨を貼る
  route.js     体が通る道(見せ場の並び)
  swim.js      うねり・向き・足し込み
  muscle.js    節どうしのつながり(なめらかさ)
  glsl.js      ノイズと空の色(シェーダから読む)
web/
  lev_body.glb 体のメッシュ(約 20MB)

計算はすべてブラウザの中。1 コマごとに「道の上の位置 → 体の節 → 水面の変形 → 泡としぶき → 描画」の順で進みます。


1. 海の形

1-1. 波は 8 本の足し算

作品の海は、向き・高さ・波長の違う sin 波 8 本の重ね合わせです。値はこれだけです(sea.js の WAVES)。

#向き x向き z高さ A (m)波長 λ (m)とがり
11.000.101.14431.70.42
20.90−0.420.63816.70.46
31.000.520.3619.20.51
40.78−0.620.1945.10.56
51.000.200.1062.70.72
60.580.820.0551.50.74
71.00−0.300.0260.80.74
80.400.920.0120.40.75

高さも波長も、地球の嵐の海を 1/5 に縮めた値です(体が 300m なので、そのままだと波が何十メートルにもなってしまう)。重力は縮めず 9.81 m/s² のまま使っています。

1-2. 波の速さ

深い海の波の位相速度は波長だけで決まります。

ω = √(g k),  k = 2π / λ,  c = ω / k = √(g / k)

31.7m の波は約 7.0 m/秒、0.4m の波は約 0.8 m/秒。長い波ほど速いので、時間が経つと 8 本の山の位置関係が変わり続け、同じ形が戻ってきません。

1-3. とがった山:ガーストナー波

sin の足し算だけでは、山も谷も同じなだらかさになります。本物の海は山がとがり、谷がなだらかです。そこで水の粒を、上下だけでなく進む向きにも動かします(ガーストナー波/トロコイド波)。

// 1 本ぶん(Q はとがりの強さ、表の「とがり」)
float phase = k * dot(dir, xz) - omega * t;
y  += A * cos(phase);
xz += dir * (Q * A * sin(phase));   // ← これで山がとがる

1-4. 白波は「面積がつぶれた所」

ガーストナー波では、山の頂上で水面の面積がつぶれます。つぶれ具合はヤコビアン(座標を変えたときの面積の伸び縮み)で測れます。

J = 1 − Σ Qᵢ Aᵢ kᵢ cos(phaseᵢ)
J が 0 に近い = 面積がつぶれている = そこが白波

作品では J が小さいほど白い泡を濃く乗せています。白波の位置を手で描いてはいません。


2. 空・雨・かすみ

2-1. 雲

同じノイズを大きさ半分・濃さ 1/1.35 倍にして 8 回重ねます(glsl.js の fbm)。

value = Σ (1/1.35)ⁿ · noise(2ⁿ · p)

濃さの減り方 g と大きさの比 2 から、ハースト指数 H = log(1/g) / log 2 ≒ 0.43 になります。自然の雲で測られているフラクタル次元(Lovejoy 1982、約 1.35)に合わせた値です。

2-2. 雨

雨粒は毎秒 7m で落ち、風は毎秒 17m。足し算(ベクトルの合成)で斜めに降ります。

tanθ = 17 / 7  →  θ ≒ 68°(鉛直から)

2-3. かすみ(遠くほど暗い)

光は濁った水・空気の中を進むと、進んだ長さに対して指数関数的に弱まります(ランベルト・ベールの法則)。

T = exp( −(0.012 d + 0.0069 D) )
d:横に進んだ距離(m)   D:水の深さ(m)

水では約 100m ごとに半分になります(0.0069 × 100 ≒ 0.69 = ln 2)。


3. 体

3-1. 寸法

const SEG = 240;          // 節の数
const STEP = 1.25;        // 節の間隔 (m)
// 全長 = 240 × 1.25 = 300 m

太さは頭から尾へ、次の式で決めています(creature.js)。

const radiusAt = i => {
  const s = i / SEG;
  return 5.2 * Math.pow(Math.sin(Math.PI * Math.pow(Math.min(s + 0.030, 1), 0.42)), 0.62);
};

いちばん太い所で半径 5.2m(直径 10.4m)。0.42 乗にしてあるので太い所が前寄りになり、尾へ向かってゆっくり細くなります。

3-2. 体の向き(背中がどちらを向くか)

節は「前の節の向きを受け取って、道が曲がったぶんだけ回す」形で向きを決めます(平行移動の枠=Bishop frame に近い考え方)。角度は範囲のない数として持ち続け、毎回いちばん近い向きを選ぶので、360° をまたぐ瞬間に裏返りません。


4. 泳ぎ

4-1. うねりと速さの関係

体をうねらせて進む生き物(ウナギ型)の速さは、次の関係で決まります。

U = f · λ · slip
f:ひとふりの回数(1/秒)   λ:体を伝わる波の波長(m)   slip:すべり(0〜1)

解説の最後のページで泳いでいる個体は、λ = 195m、slip = 0.75、U = 24 m/秒 とし、そこからひとふり 6.1 秒を計算しています(数字を決め打ちしていません)。

const om = 2 * Math.PI * (U / SLIP) / LAM;   // 1.03 rad/s → 周期 6.1 秒
const off = AMP * Math.sin(2 * Math.PI * back / LAM - om * t);

振れ幅 AMP = 14m(左右に ±14m = 体長の約 9%)。本物のウナギ類の尾の振れ幅もおよそ体長の 1 割です。

4-2. 24 m/秒 という見積もりの根拠

体が大きくなるほど、ひとふりは遅くなります。出せる力は筋肉の体積(体長の 3 乗)に、水の抵抗で失う力は(断面積 × 速さの 3 乗)=(体長の 2 乗 × v³)に比例するので、

L³ ∝ L² v³   →   v ∝ L^(1/3)

体長 30m・全力 10 m/秒 のシロナガスクジラを基準にすると、300m の体では 10 × 10^(1/3) ≒ 21〜24 m/秒。作品の見せ場で出る最大 150 m/秒 は、この見積もりをはるかに超えた「演出」です(下の「9. 物理から外したところ」を参照)。


5. 水との作用

5-1. 浮く・沈む

F = ρ V g        (アルキメデスの原理)
a = ( s − 1 ) g   s:その節が水につかっている割合(0〜1)

体の密度は水と同じ(ρ体 = 1)としています。全部沈めば浮きも沈みもしない、水から出た分だけ重さが勝って落ちる、という形です。

5-2. 水の抵抗

速さを「体に沿う向き」と「横切る向き」に分け、それぞれ 2 乗で効かせます。横向きは 25 倍効きます。細長い体が水を横に押すのは、とても大変だからです。

v  = v∥ + v⊥
a∥ = −k v∥²
a⊥ = −25 k v⊥²      k ≒ 0.010(太さの半径+1m から決めている)

この差があるおかげで、体は「頭が通った跡」をなぞるように進みます。

5-3. 顔を水に戻す

道の山が水面を越えると頭が空中に出ます。本物の生き物なら首が疲れるので、ただ落ちるより速く下げます。

Δy = −½ A u²      A = 5g ≒ 49 m/s²     u:鼻先が水から出てからの秒数

戻すのは鼻先が水面下 10m に届くまで。そこから元の道へ 3 次曲線でなめらかにつなぎます。

5-4. 水面を割る

体が水に入るとき、穴を開けるのは先頭だけ。後ろの節は開いた穴を通ります。

入るとき:先頭 π r²、続く体 π ( r² − r穴² )
出るとき:持ち上がった水の膜がちぎれて  y = v₀ t − ½ g t²

5-5. 押しのけられた水(マリンスノーの流れ)

1m 進むごとに押しのける水は π r²。その水は体の横へ回り込みます。広がりの輪の半径は、

r(d) = √(g d) × (後ろへの距離) ÷ v

6. 泡(キャビテーション)

速く動く物のまわりでは圧力が下がり、常温のまま水が沸きます。起きるかどうかはキャビテーション数 σ で決まります。

const RHO = 1025, P_ATM = 101325, P_VAP = 2300;   // 海水の密度 / 大気圧 / 20℃の飽和蒸気圧
const cavSigma = (depth, v) => (P_ATM + RHO * 9.81 * depth - P_VAP) / Math.max(0.5 * RHO * v * v, 1);

σ がその場所ごとのしきい値 σᵢ より小さいと沸きます。作品では、首のすぐ後ろ(頭から 0〜12m)で σᵢ = 0.6、それより後ろで 0.3、とげの先で 1.0。とげの先がいちばん沸きやすい設定です。

泡は 3 つの入れ物に分けて持っています。

const CAV_P = 40000;    // 圧力で沸く泡(短命・毎コマ数千個)
const CAV_AIR = 20000;  // 水面から巻き込んだ空気(5〜15 秒残る)
const CAV_BRK = 30000;  // 砕けた波の泡

7. 光

7-1. 水面の反射(フレネル)

正確なフレネルの式ではなく、シュリック近似を使っています。

float fres = 0.022 + 0.978 * pow(1.0 - clamp(dot(N, V), 0.0, 1.0), 5.0);

0.022 は水の垂直入射での反射率(屈折率 1.33 から求まる値)。浅い角度ほど 1 に近づき、遠くの水面が空を映します。

7-2. 濡れた皮膚

水から出た所は、しばらく濡れて見えます。

戻る光 = 入った光 × 0.78         (濡れると暗くなる)
滴る水の反射 = 1 − (出てからの秒) ÷ 3     (3 秒で乾く)

8. 解説の 16 項目 ↔ 実装

#解説での言い方実際に使っている式場所
01波は足し算sin 8 本の重ね合わせ+ガーストナーsea.js WAVES
02遠くほど暗いT = exp(−(0.012d + 0.0069D))sea.js かすみ
03道も波の足し算周期の違う sin の合成で道を作るroute.js
04コークスクリュー進む向きの回りに回す(回転行列)route.js MOVE_HELIX
05高さに天井をつけるy′ = B + K(1 − e^(−(y−B)/K))route.js
06曲がりの急さR = h / (2 sin(θ/2))(正弦定理)swim.js
07顔を水に戻すΔy = −½ A u²、A = 5groute.js
08浮く・沈むa = (s − 1) gswim.js
09水の抵抗a∥ = −k v∥²、a⊥ = −25k v⊥²swim.js
10体の向きa = v²/R、cos τ = 2g ÷ 0.6aswim.js
11速さが水を沸かすσ = (p₀ + ρgh − pᵥ) ÷ ½ρv²sea.js cavSigma
12水面を割るπ r² と π(r² − r穴²)、放物運動sea.js emitAirSheet
13体が濡れる0.78 倍+3 秒で乾くrig.js
14押しのけられた水π r²、√(g d) の広がりsea.js updateSnow
15空の表現fbm(半分の大きさ・1/1.35 の濃さ)glsl.js NOISE
16大学の知識ヤコビアン・ポテンシャル流・フレネル・乱流解説ページ内

9. 物理から外したところ(正直な一覧)

作品は「高校の式で動く」ことを大事にしていますが、そのままでは成り立たない所もあります。隠さずに書きます。

  1. 海の大きさを 1/5 に縮めた。 重力は 9.81 のまま。300m の体に本物の嵐の波を合わせると、波高が数十メートルになり、地球では起こりえない海になるため
  2. 体は力学で解いていない。 先に道を決め、その上に体を乗せています(?knobs=1 で見られる「本物の物理だけで泳ぐ」は実験用)。抵抗・浮力・向きは、その道の上での見え方を決めるために使っています
  3. 抗力はレイノルズ数を使っていない。 係数 k と「横は 25 倍」で近似しています
  4. 向きは力のつり合いではなく角度で決めている(cos τ = 2g ÷ 0.6a)。重力を強め、遠心力を弱めた値です
  5. 泡の数と寿命は見た目で決めた。 沸く条件(σ)は物理ですが、1 コマに出す個数や消えるまでの秒数は、見え方から選んだ値です
  6. 光は近似。 フレネルはシュリック近似、体の中の散乱(SSS)も簡単な近似です
  7. 最大 秒速 150m は演出。 体長からの見積もり(4-2)は 21〜24 m/秒。水中の乗り物で最速の部類(泡に包まれて進む魚雷)でも約 100 m/秒です

10. 動く見本(1 ファイルで動きます)

→ 動く見本を開く

解説の 01〜16 を全部入れた、1 つのファイルです。ブラウザで開けば動きます。コードはそのファイルの中に全部あり、【01】〜【16】 の番号で、どこがどの項目か分かるようにしてあります。

番号見本の中でやっていること
018 本のガーストナー波(値は 1-1 の表と同じ)
02深さで暗くなる(e^(−0.0069 D))と、遠くのかすみ
03道は作品のものを 1 本そのまま(page/route.js の見せ場)。深さ 68m の闇から上がって、窓の前を横切り、また潜る
04体を進む向きのまわりにねじる(つまみ「ねじり」)
05道が水面より上へ行かないように、指数で頭打ちにする
063 点を通る円(外接円)から、曲がりの半径を出す
07頭が水から出たら 5g で引き戻す
08節ごとの沈み具合 s から a = (s − 1) g
09抵抗 −k v²(上下は 25 倍)。進む速さも抵抗とつり合う所に落ち着く
10曲がりの半径から傾きを出して、体を倒す
11キャビテーション数 σ から泡を出す
12水面をまたいだ所から、しぶきが放物運動で飛ぶ
13水から出た所は 0.78 倍に暗くなり、3 秒で戻る
14体のまわりの粒が、押しのけられて外へ流れる
15空の雲(半分の大きさ・1/1.35 の濃さで 6 回重ねる)
16ヤコビアンがつぶれた所を白波にする

動かし方:ドラッグで回す/ホイールで寄る/R でもとの追いかけに戻す。つまみは 4 つ(進む速さ・波の高さ・うねりの振れ幅・ねじり)。
画面の左上に、いま計算している数字(ひとふりの秒数・沈み具合・σ・曲がりの半径・傾き)が出ます。

自分のパソコンで動かすとき:ファイルを保存して、先頭にある読み込み先の 1 行をこれに替えてください。

<script type="importmap">
{ "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
               "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/" } }
</script>

入っていないもの:体の見た目(うろこ・とげ・目・口の中)、書き出したメッシュ(約 20MB)、雨、マリンスノーの細かい作り。体はただの筒です。この見本は仕組みを読むためのものです。

11. 使ってよい条件

この技術ノートの文章と、動く見本のコードは CC0 1.0(パブリックドメイン)です。
誰でも、何にでも、名前を書かずに使えます。商売に使っても構いません。連絡も要りません。

作品「リヴァイアサン」の形・見た目・書き出したデータ(.glb)・写真についても、権利を主張しません。使いたい人は使ってください。

ひとつだけ、はっきり書いておきます。この作品と、ここに書いた方法を、作った本人(アトリエファクトリー)が今後も自由に使えることを前提としています。誰かが同じ内容で権利を主張して、こちらの利用を止めることはできません。この文書は日付とともに公開されており、それ自体が「先に世に出ていた」という記録になります。

なお、見本が使っている three.js は MIT ライセンス(Copyright 2010–2024 Three.js Authors)です。three.js を一緒に配るときは、その表示を残してください。


12. 出どころ


制作:アトリエファクトリー(aTELiER FACTORY)

日本語EN