二つのシェーダーで水をつくる — 水たまり・小川・滝
島に水面シミュレーションも反射もありません。ShaderMaterial は二つだけで、寝ている水と落ちる水を担当し、time と night を共有します。
水は作り込みすぎやすい題材です。島の水は二つのマテリアルだけ。反射も屈折も流体計算もありません。
寝ている水 — 一つのマテリアルで三箇所
水たまりと小川は同じマテリアルです。形はまるで違います(片方は円、片方はベジェ曲線で描いた川床)が、フラグメントシェーダーが見るのは位置と時間だけです。
float w = sin(vPosition.x * 9. + vPosition.y * 12. + time * 1.6)
* sin(vPosition.y * 7. - time);
float ring = pow(max(0., sin(length(vPosition.xy) * 19. - time * 1.6)), 22.);
vec3 c = mix(vec3(.10, .43, .43), vec3(.29, .72, .67), w * .25 + .5);
c += ring * .1;
二つの正弦の積が不規則な波紋を、高い冪がときどき光る輪を作ります。UV ではなく位置を見ているので、同じマテリアルがどんな形にも載ります。川床専用の UV は不要です。
落ちる水 — 頂点変位と縞
滝は 0.94 × 4.9 の平面を 10 × 32 に分割したものです。頂点シェーダーが面自体をうねらせます。
p.z += sin(uv.y * 12. + time * 2. + uv.x * 5.) * .055;
フラグメント側は高周波の縞とゆっくりした脈動、両端は smoothstep で消します。
float stripe = pow(.5 + .5 * sin(vUv.x * 70. + sin(vUv.y * 8. - time * 3.)), 7.);
float edge = smoothstep(0., .1, vUv.x) * (1. - smoothstep(.83, 1., vUv.x));
gl_FragColor = vec4(col, edge * fade * .85);
マテリアルは transparent と depthWrite: false を設定します。でないと滝が背後の霧を隠してしまいます。
uniform は一組を二つのマテリアルで共有
time と night は同じオブジェクトです。
const waterUniforms = { time: { value: 0 }, night: { value: 0 } };
アニメーションループで一度書けば両方が同時に更新されます。節約できるのは行数だけではありません。夜に切り替えたとき、二箇所の水が同じ瞬間に暗くなることを保証します。
必ず入れる二つの include
独自のフラグメントシェーダーは既定でトーンマッピングと色空間変換を飛ばします。そのままだと水だけ一段明るく、光るプラスチックに見えます。
#include <tonemapping_fragment>
#include <colorspace_fragment>
three のシェーダーチャンクで、色を計算した後に書きます。忘れると環境光の設定を疑い始めることになります。
まとめ
水の説得力は物理的な正しさではなく、動きとハイライトから来ます。正弦二つ、冪一つ、共有 uniform 一組で、静的な島は生きて見えます。

コメント
…