この作品の海と体が、どの式・どの値で動いているか。
解説ページ「リヴァイア算」は高校生にも読めるように書いています。こちらはその裏側で、実際に動いているコードと数値をそのまま載せたものです。作った人:アトリエファクトリー(aTELiER FACTORY)。
/3d/creature/leviathan/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 コマごとに「道の上の位置 → 体の節 → 水面の変形 → 泡としぶき → 描画」の順で進みます。
作品の海は、向き・高さ・波長の違う sin 波 8 本の重ね合わせです。値はこれだけです(sea.js の WAVES)。
| # | 向き x | 向き z | 高さ A (m) | 波長 λ (m) | とがり |
|---|---|---|---|---|---|
| 1 | 1.00 | 0.10 | 1.144 | 31.7 | 0.42 |
| 2 | 0.90 | −0.42 | 0.638 | 16.7 | 0.46 |
| 3 | 1.00 | 0.52 | 0.361 | 9.2 | 0.51 |
| 4 | 0.78 | −0.62 | 0.194 | 5.1 | 0.56 |
| 5 | 1.00 | 0.20 | 0.106 | 2.7 | 0.72 |
| 6 | 0.58 | 0.82 | 0.055 | 1.5 | 0.74 |
| 7 | 1.00 | −0.30 | 0.026 | 0.8 | 0.74 |
| 8 | 0.40 | 0.92 | 0.012 | 0.4 | 0.75 |
高さも波長も、地球の嵐の海を 1/5 に縮めた値です(体が 300m なので、そのままだと波が何十メートルにもなってしまう)。重力は縮めず 9.81 m/s² のまま使っています。
深い海の波の位相速度は波長だけで決まります。
ω = √(g k), k = 2π / λ, c = ω / k = √(g / k)
31.7m の波は約 7.0 m/秒、0.4m の波は約 0.8 m/秒。長い波ほど速いので、時間が経つと 8 本の山の位置関係が変わり続け、同じ形が戻ってきません。
sin の足し算だけでは、山も谷も同じなだらかさになります。本物の海は山がとがり、谷がなだらかです。そこで水の粒を、上下だけでなく進む向きにも動かします(ガーストナー波/トロコイド波)。
// 1 本ぶん(Q はとがりの強さ、表の「とがり」)
float phase = k * dot(dir, xz) - omega * t;
y += A * cos(phase);
xz += dir * (Q * A * sin(phase)); // ← これで山がとがる
ガーストナー波では、山の頂上で水面の面積がつぶれます。つぶれ具合はヤコビアン(座標を変えたときの面積の伸び縮み)で測れます。
J = 1 − Σ Qᵢ Aᵢ kᵢ cos(phaseᵢ)
J が 0 に近い = 面積がつぶれている = そこが白波
作品では J が小さいほど白い泡を濃く乗せています。白波の位置を手で描いてはいません。
同じノイズを大きさ半分・濃さ 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)に合わせた値です。
雨粒は毎秒 7m で落ち、風は毎秒 17m。足し算(ベクトルの合成)で斜めに降ります。
tanθ = 17 / 7 → θ ≒ 68°(鉛直から)
光は濁った水・空気の中を進むと、進んだ長さに対して指数関数的に弱まります(ランベルト・ベールの法則)。
T = exp( −(0.012 d + 0.0069 D) )
d:横に進んだ距離(m) D:水の深さ(m)
水では約 100m ごとに半分になります(0.0069 × 100 ≒ 0.69 = ln 2)。
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 乗にしてあるので太い所が前寄りになり、尾へ向かってゆっくり細くなります。
節は「前の節の向きを受け取って、道が曲がったぶんだけ回す」形で向きを決めます(平行移動の枠=Bishop frame に近い考え方)。角度は範囲のない数として持ち続け、毎回いちばん近い向きを選ぶので、360° をまたぐ瞬間に裏返りません。
体をうねらせて進む生き物(ウナギ型)の速さは、次の関係で決まります。
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 割です。
体が大きくなるほど、ひとふりは遅くなります。出せる力は筋肉の体積(体長の 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. 物理から外したところ」を参照)。
F = ρ V g (アルキメデスの原理)
a = ( s − 1 ) g s:その節が水につかっている割合(0〜1)
体の密度は水と同じ(ρ体 = 1)としています。全部沈めば浮きも沈みもしない、水から出た分だけ重さが勝って落ちる、という形です。
速さを「体に沿う向き」と「横切る向き」に分け、それぞれ 2 乗で効かせます。横向きは 25 倍効きます。細長い体が水を横に押すのは、とても大変だからです。
v = v∥ + v⊥
a∥ = −k v∥²
a⊥ = −25 k v⊥² k ≒ 0.010(太さの半径+1m から決めている)
この差があるおかげで、体は「頭が通った跡」をなぞるように進みます。
道の山が水面を越えると頭が空中に出ます。本物の生き物なら首が疲れるので、ただ落ちるより速く下げます。
Δy = −½ A u² A = 5g ≒ 49 m/s² u:鼻先が水から出てからの秒数
戻すのは鼻先が水面下 10m に届くまで。そこから元の道へ 3 次曲線でなめらかにつなぎます。
体が水に入るとき、穴を開けるのは先頭だけ。後ろの節は開いた穴を通ります。
入るとき:先頭 π r²、続く体 π ( r² − r穴² )
出るとき:持ち上がった水の膜がちぎれて y = v₀ t − ½ g t²
1m 進むごとに押しのける水は π r²。その水は体の横へ回り込みます。広がりの輪の半径は、
r(d) = √(g d) × (後ろへの距離) ÷ v
速く動く物のまわりでは圧力が下がり、常温のまま水が沸きます。起きるかどうかはキャビテーション数 σ で決まります。
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; // 砕けた波の泡
正確なフレネルの式ではなく、シュリック近似を使っています。
float fres = 0.022 + 0.978 * pow(1.0 - clamp(dot(N, V), 0.0, 1.0), 5.0);
0.022 は水の垂直入射での反射率(屈折率 1.33 から求まる値)。浅い角度ほど 1 に近づき、遠くの水面が空を映します。
水から出た所は、しばらく濡れて見えます。
戻る光 = 入った光 × 0.78 (濡れると暗くなる)
滴る水の反射 = 1 − (出てからの秒) ÷ 3 (3 秒で乾く)
| # | 解説での言い方 | 実際に使っている式 | 場所 |
|---|---|---|---|
| 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 = 5g | route.js |
| 08 | 浮く・沈む | a = (s − 1) g | swim.js |
| 09 | 水の抵抗 | a∥ = −k v∥²、a⊥ = −25k v⊥² | swim.js |
| 10 | 体の向き | a = v²/R、cos τ = 2g ÷ 0.6a | swim.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 | 大学の知識 | ヤコビアン・ポテンシャル流・フレネル・乱流 | 解説ページ内 |
作品は「高校の式で動く」ことを大事にしていますが、そのままでは成り立たない所もあります。隠さずに書きます。
?knobs=1 で見られる「本物の物理だけで泳ぐ」は実験用)。抵抗・浮力・向きは、その道の上での見え方を決めるために使っています→ 動く見本を開く
解説の 01〜16 を全部入れた、1 つのファイルです。ブラウザで開けば動きます。コードはそのファイルの中に全部あり、【01】〜【16】 の番号で、どこがどの項目か分かるようにしてあります。
| 番号 | 見本の中でやっていること |
|---|---|
| 01 | 8 本のガーストナー波(値は 1-1 の表と同じ) |
| 02 | 深さで暗くなる(e^(−0.0069 D))と、遠くのかすみ |
| 03 | 道は作品のものを 1 本そのまま(page/route.js の見せ場)。深さ 68m の闇から上がって、窓の前を横切り、また潜る |
| 04 | 体を進む向きのまわりにねじる(つまみ「ねじり」) |
| 05 | 道が水面より上へ行かないように、指数で頭打ちにする |
| 06 | 3 点を通る円(外接円)から、曲がりの半径を出す |
| 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)、雨、マリンスノーの細かい作り。体はただの筒です。この見本は仕組みを読むためのものです。
この技術ノートの文章と、動く見本のコードは CC0 1.0(パブリックドメイン)です。
誰でも、何にでも、名前を書かずに使えます。商売に使っても構いません。連絡も要りません。
作品「リヴァイアサン」の形・見た目・書き出したデータ(.glb)・写真についても、権利を主張しません。使いたい人は使ってください。
ひとつだけ、はっきり書いておきます。この作品と、ここに書いた方法を、作った本人(アトリエファクトリー)が今後も自由に使えることを前提としています。誰かが同じ内容で権利を主張して、こちらの利用を止めることはできません。この文書は日付とともに公開されており、それ自体が「先に世に出ていた」という記録になります。
なお、見本が使っている three.js は MIT ライセンス(Copyright 2010–2024 Three.js Authors)です。three.js を一緒に配るときは、その表示を残してください。
制作:アトリエファクトリー(aTELiER FACTORY)