circle-loader
by
103/ 1

漣という効果を作ってみた人が多いと思います。実装方法はいろいろありますが、ここではCustomノードを使い、アルゴリズムで法線を生成する方法を採用します。それでは、私のアイデアを共有しながら、最終的な効果を一緒に確認したいです。ちなみに、文末には材質(マテリアル)の球に関するBaiduクラウドリンクが付いてあります。

最終の効果

実装の原理について簡単に紹介します。まずはUVで擬似ランダムのメッシュを作り、一つ一つのメッシュは単独的なUVです。ただし、異なる階調値があります。そして、メッシュの中心でサイズが違う同心円を生成します。その後、スケーリングとエッジブレンディングを行います。

 

ランダムノイズの生成

まず、3次元ベクトルを定義し3層のサイズ異なる漣の計算を行います。なぜかというと、UVの取り得る値の範囲0-1であるから、0-1の間float3の値定義なければなりません。この値は係数で、実際のサイズではありません。

float3 ripple_scale3=float3(0.1,0.2,0.3)

次に、異なる階調値があるUVメッシュを生成する必要があります。

float3 p3 = frac(float3(p.xyx) * ripple_scale3);
p3 += dot(p3, p3.yzx +20);
return frac((p3.xy + p3.yz) * p3.zy);

pは2次元ベクトルですから、ripple_scaleと乗算できるため、その.XYXまたは.XYYを任意に取ることができます。p3は累積値で、最終的な戻り値はfloat2です。UVはfloat2であるため、2つの軸を取れば、上記の操作を実行するだけで良いです。次に、1次元ベクトルを定義し、次の操作をもう一度実行します。

float ripple_scale1 = 0.1;
float3 p3 = frac(float3(p.xyx) * ripple_scale1);
p3 += dot(p3, p3.yzx + 10);
return frac((p3.x + p3.y) * p3.z);

上の二つ計算は十分にランダム的な値を得ることが目的としています。そのゆえ、他のアゴラリズで替わっても大丈夫です。もしUVを10回まで tiling すると 、floor処理後上のコードのpとして三次元の計算を行い、それから下の一次元のと乗算したら、以下のような結果が出します。(以下のような結果が得られるアルゴリズムならオーケーです。

このアルゴリズムをfunctionとする必要があります。循環計算が必要のため、strcut構造体で指定した後にコールします。

float ripple_scale1 = 0.1;一次元ランダム数の種
float3 ripple_scale3 = float3(0.1, 0.11, 0.09);//三次元ランダム数の種
float max_radius = 1;
struct rain
{

    float ripple1(float2 p)
    {
        float3 p3 = frac(float3(p.xyx) * ripple_scale1);
        p3 += dot(p3, p3.yzx + 10);
        return frac((p3.x + p3.y) * p3.z);
    }

    float2 ripple2(float2 p)
    {
        float3 p3 = frac(float3(p.xyx) * ripple_scale3);
        p3 += dot(p3, p3.yzx +20);
        return frac((p3.xy + p3.yz) * p3.zy);

    }
};
rain ra;

漣形の生成

続いては、ランダム値によって漣を生成して動かさせます。このステップは循環サンプリングが必要のため、変数を指定しておいたほうが良いと思います。

float tiling = 10;//UVtiling回数
float2 uv = (UV) * tiling;//UV
float2 p0 = floor(uv);//floor後tiling個の長寛のUVメッシュが生成される
float i = 0;//x軸の循環回数
float j = 0;//y軸の循環回数、UVは2軸だから、二つの方向がある
float2 pi = 0;毎UVメッシュの異なる諧調値を記録する
float2 circles = 0;//円形
float2 p = 0;//初期位置

そして、循環体を準備しておいてください。piを循環体に入れて累加します。

for (j = (- max_radius);j <= max_radius; j++)
        for (i = - max_radius; i<= max_radius; i++)
        {
          pi = p0 +float2(i, j);
         }
     }

累加の結果が多すぎのため、それをtiling回数で除算して観察します。明らかに、一つ一つのメッシュは異なる諧調値を持つことになります。それはi、j値が累加しつつあるからです。

pi循環後の値

それから、piの下にpiの値を三次元のランダム値functionに代入して計算します。

for (j = (- max_radius);j <= max_radius; j++)
    {
        for (i = - max_radius; i<= max_radius; i++)
        {
          pi = p0 +float2(i, j);
          float2 hsh = ra.ripple2(pi);//1回目のランダム計算
         }
     }

hsh循環後の値(ランダム化)

一度あることは二度ある、hsh値を三次元のランダム値に代入し続け、pi値と加算したら、pi(UV位置情報を持つ乱数)を持っているよく言われたpが得られます。

for (j = (- max_radius);j <= max_radius; j++)
    {
        {
          pi = p0 +float2(i, j);
          float2 hsh = ra.ripple2(pi);//1回目のランダム計算
          p = pi + ra.ripple2(hsh);//位置情報を持つランダム値が得られる
         }
     }

p値で、同じく見やすくため、tilingで除算しました。

そして、時間tを定義する必要があり、同じくランダム化する必要があるが、一次元のランダム化関数を用います(もし三次元のままだとしたら、tilingを生成する。下図を参照(下図のtiling値は20である))。その後、fracして0-1循環を行います。

for (j = (- max_radius);j <= max_radius; j++)
    {
        for (i = - max_radius; i<= max_radius; i++)
        {
            pi = p0 +float2(i, j);
            float2 hsh = ra.ripple2(pi);//1回目のランダム計算
            p = pi + ra.ripple2(hsh);//位置情報を持つランダム値が得られる
            float t = frac(0.3 * iTime + ra.ripple1(hsh));//ランダム時間に生成し、0.3は速度を表し、外部で調整できる
        }
    }

一種類のランダム化を採用したt

二種類のランダム化を採用したt

そして、実際の位置を出します。vで表示すると、UV値を引ければ良いです。UV値と対応のためです。引かないと、最後は法線が得られなくなります(平)。

for (j = (- max_radius);j <= max_radius; j++)
    {
        for (i = - max_radius; i<= max_radius; i++)
        {
            pi = p0 +float2(i, j);
            float2 hsh = ra.ripple2(pi);//1回目のランダム計算
            p = pi + ra.ripple2(hsh);//位置情報を持つランダム値が得られる
            float t = frac(0.3 * iTime + ra.ripple1(hsh));//ランダム時間に生成し、0.3は速度を表し、外で調整できる
            float2 v = p - uv;//実の位置情報
        }
    }

それからは、円を計算することです。円の計算式はlength(position)-Rです。その中には、positionは円心がUVでの位置を表し、Rは円の半径です。こちらはmax_radius+1で、1を加算しないと、全ての値は前の値より小さくなり、法線の強度が弱くなります。それからは得られたtと計算すれば拡散後の円になります。

for (j = (- max_radius);j <= max_radius; j++)
    {
        for (i = - max_radius; i<= max_radius; i++)
        {
            pi = p0 +float2(i, j);
            float2 hsh = ra.ripple2(pi);//1回目のランダム計算
            p = pi + ra.ripple2(hsh);//位置情報を持つランダム値
            float t = frac(0.3 * iTime + ra.ripple1(hsh));//ランダム時間に生成し、0.3は速度を表し、外で調整できる。
            float2 v = p - uv;//実の位置情報
            float d = length(v) - (max_radius + 1) * t;//円を計算する
        }
    }

計算された拡散円

でもこれはほんの一部にすぎません。円形の累加は後で行いますから、先に漣形を作ります。 得られた d をsine関数演算して漣を作ります。ある値を d に掛けると, 異なる円数の漣を得ることができます. そして Smoothstep を使ってエッジの虚実効果を制御します。ここでは 2 つのレイヤーを実行し、補間の 2 つのレイヤーを使用して漸次変化をシミュレートし、h を使用して漣のオフセット値を制御します。

for (j = (- max_radius);j <= max_radius; j++)
    {
        for (i = - max_radius; i<= max_radius; i++)
        {
            pi = p0 +float2(i, j);
            float2 hsh = ra.ripple2(pi);//1回目のランダム計算
            p = pi + ra.ripple2(hsh);//位置情報を持つランダム値が得られる
            float t = frac(0.3 * iTime + ra.ripple1(hsh));//ランダム時間に生成し、0.3は速度を表し、外で調整できる
            float2 v = p - uv;//実の位置情報
            float d = length(v) - (max_radius + 1) * t;//円を計算する
            float h = 1e-3;//0.001である
            float d1 = d - h;
            float d2 = d + h;
            float p1 = sin(31. * d1) * smoothstep(-0.6, -0.3, d1) * smoothstep(0., -0.3, d1);
            float p2 = sin(31. * d2) * smoothstep(-0.6, -0.3, d2) * smoothstep(0., -0.3, d2);
        }
    }

d*30

d*60

p1の効果

漣の漸進的変化の効果

動けるようになった後、最大値に達してからフェードアウトする(次第に消える)効果が必要となります。両者の差に時間の逆数を掛けることで、つまり 1-0 でエッジの漸次変化効果シミュレートします。二回のtをかけるのはコントラストを上げるためです。

for (j = (- max_radius);j <= max_radius; j++)
    {
        for (i = - max_radius; i<= max_radius; i++)
        {
            pi = p0 +float2(i, j);
            float2 hsh = ra.ripple2(pi);//1回目のランダム計算
            p = pi + ra.ripple2(hsh);//位置情報を持つランダム値が得られる
            float t = frac(0.3 * iTime + ra.ripple1(hsh));//ンダム時間に生成し、0.3は速度を表し、外で調整できる
            float2 v = p - uv;//実の位置情報
            float d = length(v) - (max_radius + 1) * t;//円を計算する
            float h = 1e-3;//0.001になる
            float d1 = d - h;
            float d2 = d + h;
            float p1 = sin(31. * d1) * smoothstep(-0.6, -0.3, d1) * smoothstep(0., -0.3, d1);
            float p2 = sin(31. * d2) * smoothstep(-0.6, -0.3, d2) * smoothstep(0., -0.3, d2);
            circles = (p2 - p1) / (2. * h) * (1. - t) * (1. - t);//フェードアウト効果
        }
    }

最大値に達した後のフェードアウト効果

漣形の累加

続いては、前のnormalize後のposition(つまりv)を掛けると、今の正しい法線の効果が得られます。最後は毎回の循環の結果を累加すれば欲しい漣の効果が得られます。何かの数値とかけると、法線の強度を制御することができるようになります。

for (j = (- max_radius);j <= max_radius; j++)
    {
        for (i = - max_radius; i<= max_radius; i++)
        {
            pi = p0 +float2(i, j);
            float2 hsh = ra.ripple2(pi);//1回目のランダム計算
            p = pi + ra.ripple2(hsh);//位置情報を持つランダム値が得られる
            float t = frac(0.3 * iTime + ra.ripple1(hsh));//ランダム時間に生成し、0.3は速度を表し、外で調整できる
            float2 v = p - uv;//実の位置情報
            float d = length(v) - (max_radius + 1) * t;//円を計算する
            float h = 1e-3;//0.001になる
            float d1 = d - h;
            float d2 = d + h;
            float p1 = sin(31. * d1) * smoothstep(-0.6, -0.3, d1) * smoothstep(0., -0.3, d1);
            float p2 = sin(31. * d2) * smoothstep(-0.6, -0.3, d2) * smoothstep(0., -0.3, d2);
            circles = (p2 - p1) / (2. * h) * (1. - t) * (1. - t);//フェードアウト効果
            circles = 0.5 * normalize(v) * ((p2 - p1) / (2. * h) * (1. - t) * (1. - t));//正しい法線の方向が得られる
            circles=circles+circles;//効果の累加
        }
    }

circles法線の累加効果

これにより、法線は適切な形状になりましたが、効果はきれいに見えません。これは累積的であるため、循環の終了を循環の総数で割った後、正しい効果を得ることができます。

circles /= float(max_radius*2+1)*(max_radius*2+1);

 

色補正後の効果

法線を生成する

最後は法線Bチャンネルを求める方法(開平する)で、Bチャンネルの出力を求めれば良いです。なぜ内積で二乗するかはみんな詳しいと思います。

float3 n = float3(circles, sqrt(1. - dot(circles, circles)));

法線効果

下記は完全なコードです。

float3 ripple_scale= float3(0.1,0.2,0.3);
float max_radius = 2;
struct rain
{

    float2 ripple(float2 p)
    {
        float3 p3 = frac(float3(p.xyx) * ripple_scale);
        p3 +=p3;
        return frac((p3.xy + p3.yz) * p3.zy);
    }
};
rain ra;

float tiling = 10;
float2 uv = (UV) * tiling;
float2 p0 = floor(uv);
float j = 0;
float i = 0;
float2 pi = 0;
float2 circles = 0;
float2 p = 0;
for (j = (- max_radius);j <= max_radius; j++)
    {
        for (i = - max_radius; i<= max_radius; i++)
        {
            pi = p0 +float2(j, i);
            p = pi+ra.ripple(pi);
            float t = frac(iTime + ra.ripple(pi));
            float2 v = p - uv;
            float d = length(v) - (max_radius + 1) * t;
            float h = 0.01;
            float d1 = d - h;
            float d2 = d + h;
            float p1 = sin(30 * d1) * smoothstep(-0.6, -0.3, d1) * smoothstep(0., -0.3, d1);
            float p2 = sin(30. * d2) * smoothstep(-0.6, -0.3, d2) * smoothstep(0., -0.3, d2);
            circles += 0.5 * normalize(v)* ((p2-p1 )/(2. * h) * (1. - t) * (1. - t)) ;
        }
    }
circles /= pow((max_radius*2+1),2);
float3 n = float3(circles, sqrt(1. - dot(circles, circles)));
return n;

水の底の石を生成する

石の部分はParallaxのまま使えますが、この前の記事を読んでいただければ、先ほど変更したアルゴリズムをそのまま使えます。

https://zhuanlan.zhihu.com/p/347445204(中国語注意)

POM石の高度値

反射

水面の反射は相変わらずReflection Vectorを使います。最後の法線はこのnormalインターフェイスに入力されます。別の法線と上記のrippleでminを作成して、円を変更させました。

Reflection Vectorに入力された結果

屈折

屈折については、上の法線の混合結果を石の色のUVに直接追加してからシミュレーションできます。もちろんですが、強度はそんなに高いではあります。

屈折と反射

透明度

最後に、フレネルを使用して深度と透明度の変化を作れば済みます。

フレネル作成の深度と透明度

完全なノート

リンク:https://pan.baidu.com/s/1xXvxxsYBjBVVnb42wOWFrA

パスワード:fp37


UWA公式サイト:https://jp.uwa4d.com

UWA公式ブログ:https://blog.jp.uwa4d.com

UWA公式Q&Aコミュニティ(中国語注意)https://answer.uwa4d.com