二つのシェーダーで水をつくる — 水たまり・小川・滝

島に水面シミュレーションも反射もありません。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 一組で、静的な島は生きて見えます。

← 記事一覧に戻る

コメント

…