Ainul Ξ Creative Dev

Ainul Ξ Creative Dev

Product-focused software engineer.
twitter
tg_channel

地形線画

2021 年の初めから中頃にかけて、このアートスタイルを至る所で見かけました。特にブランディングで。あまりにも多く見かけたので、実際にそれにそっくりなデスクマットを持っています。これが私が話していることです:

Topographic

基本設定#

私が最初にしたことはリサーチです。デザイナーたちはこれをどうやって作っているのか?手作業で作るにはあまりにも複雑に見えましたが、ランダムにしてはあまりにも整然としていました。おそらく、手続き的に生成されているに違いありませんよね?私はこのチュートリアルを見つけました。他にもいくつかありました。一般的なアイデアは、いくつかのパーリンノイズから始め、それをぼかし、コントラストを高めるためにレベルを調整し、エッジを検出するというものでした。私たちはそれを完全には行いませんが、似たようなルートを進みます。

私たちはthree.jsを使ってこれを構築します。基本的なプレイグラウンドを設定する方法はたくさんありますが、私は基本に従います。ホットリロードサポートを得るために、素早く Vite プロジェクトを立ち上げますが、実際には HTML ページだけでも構いません:

<html>
  <div id="topo"></div>
  <script type="module" src="./src/index.ts"></script>
</html>

私たちの JavaScript ファイルでは、基本的な ThreeJS シーンを設定します。これに不慣れな方は、three のドキュメントに基本的なチュートリアルがあります。そうは言っても、進む前にグラフィックス用語に慣れておくことをお勧めします。そうしないと、ほとんどのことが意味不明に聞こえるでしょう。私たちは、興味深い部分に到達するために、ThreeJS 特有のことを急いで進めます。すべてのコーディングをシェーダー内で行うため、Three から必要なのは、カメラの視界を完全に覆う平面を持つシンプルなシーンだけです。私はOrthographicCameraと私の平面をこのように設定します:

const width = 600;
const height = 400;

const scene = new THREE.Scene();

// カメラを作成し、FOVが正確にビューポートにマッピングされるように位置を設定します
const camera = new THREE.OrthographicCamera(0, width, 0, height, 1, 3);
camera.position.z = 2;

const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);

// 平面だけで十分です
const geometry = new THREE.PlaneGeometry(width, height);
geometry.translate(width / 2, height / 2, 0);

// 白色のマテリアルで始めます
const material = new THREE.MeshBasicMaterial({
  color: 0xffffff,
  side: THREE.DoubleSide, // 平面の両面がレンダリングされるようにします。これにより、法線に関連する問題を回避します。
});
scene.add(new THREE.Mesh(geometry, material));

document.getElementById("topo").appendChild(renderer.domElement);
function frame() {
  requestAnimationFrame(frame);
  renderer.render(scene, camera);
}
frame();

すべてがうまくいけば、白いcanvasが表示されるはずです。いいですね。

MeshBasicMaterialShaderMaterialに置き換え、基本的な頂点シェーダーとフラグメントシェーダーを書いて、機能することを確認しましょう:

// ..snip

const vs = `
  void main() {
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
`;
const fs = `
  void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0)
  }
`;
const material = new THREE.MeshBasicMaterial({
  vertexShader: vs,
  fragmentShader: fs,
  side: THREE.DoubleSide,
});

// ..snip

(さて、聞いてください、シェーダーを文字列に詰め込むよりも良い方法がありますが、みんなそれぞれのやり方があるので、自分の好きな方法でやってください。この投稿では、シンプルに保つために文字列のままにします。)

あなたの白いcanvasは今、赤くなるはずです。これで全てが完了したので、楽しい部分に行きましょう。

シェーダー#

パーリンノイズを作成しましょう。これを JS 側(CPU)で行うこともできますが、ノイズをアニメーションさせる予定なので、これを行うとすぐに高コストになります。なぜなら、少なくとも 1 秒間に 60 回は行わなければならないからです。代わりに、GPU でこれを行います。GPU はこの種のことに非常に優れています。GLSL でパーリンノイズ関数を書くのはこの投稿の範囲を超えているので、私はこちらを見つけました。それをフラグメントシェーダーにコピーし、main関数の上に置きます。次に、snoise関数の戻り値を使用してフラグメントの色を生成します:

// `noise3D.glsl`の内容をここに貼り付けます

void main() {
  float noise = snoise(vec3(gl_FragCoord.xy, 1.0));
  gl_FragColor = vec4(vec3(noise), 1.0);
}

まだあまり見えないでしょう。なぜなら、ノイズがあまりにも小さいからです。gl_FragCoord.xy0.005を掛けて少し「ズームイン」しましょう:

noise

ただし、黒が白よりも多いことに気づくかもしれません。これは、snoise関数が-11の間の値を返すためです。フラグメントシェーダーの出力は01の間にクリップされるため、「正規化」する必要があります。シェーダーの文脈での正規化は、ある範囲の値を [0-1] にスケーリングすることを指します。

void main() {
  float noise = snoise(vec3(gl_FragCoord.xy * 0.005, 1.0)); // ノイズを取得
  noise = (noise + 1.0) / 2.0; // 正規化します
  gl_FragColor = vec4(vec3(noise), 1.0);
}

normalized

次に、ノイズを滑らかではなくしたいと思います。今のように滑らかではなく、色の「バンド」を持ちたいのです。これがまさにポスタリゼーションです。こちらはポスタリゼーションについて学ぶための素晴らしいリソースです(他にもたくさんのことがあります)。要するに、01の間の連続した値(つまり、私たちのノイズ)を離散的なステップに変換しようとしています。値を切り上げ / 切り下げすることを考えてみてください。01の間のすべてが1に変換され、12の間のすべてが2に変換される、という具合です。

ただし、切り上げるのと正確には異なります。なぜなら、私たちのノイズ値はすでに01の間にあるからです。したがって、ノイズ値を10倍して [0-10] にスケーリングし、その後「切り上げ」操作を行います:

void main() {
  float noise = snoise(vec3(gl_FragCoord.xy * 0.005, 0.0));
  noise = 10.0 * (noise + 1.0) / 2.0;

  float rounded = ceil(noise);
  float color = rounded / 10.0; // 切り上げた値を[0-1]に戻す必要があります。これを有効な色として使用できます。
  gl_FragColor = vec4(color, color, color, 1.0);
}

posterized

さて、これで進展がありました!ただし、私たちが欲しいのは色のバンドではなく、これらの色のバンドの「エッジ」です。洗練されたエッジ検出アルゴリズムを実装する代わりに、ポスタリゼーション中に実際のパーリンノイズの値を切り上げたことを利用できます。実際の値と切り上げた値が0.1未満(または任意の小さな閾値)で異なる場所は、(大まかに)「エッジ」です!これらの場所に白いピクセルを描画し、それ以外は黒いピクセルを描画しましょう。試してみましょう:

void main() {
  float noise = snoise(vec3(gl_FragCoord.xy * 0.005, 0.0));
  noise = (noise + 1.0) / 2.0; // 正規化します

  float rounded = ceil(noise);
  float rounding_error = rounded - noise;

  if (rounding_error < 0.1) {
    gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
  } else {
    gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
  }
}

edges

これで完了です!ここで終わることもできますが、いくつかの明らかな改善点があります。最も明らかなのは、ギザギザの線を改善することです。これは、ThreeJS にデバイスのピクセル比を使用するように指示するだけで簡単です。これを ThreeJS コードに追加できます:

renderer.setPixelRatio(window.devicePixelRatio);

これにより、すべてが小さくなりますので、gl_FragCoord.xyに掛ける係数を変更してサイズを再度大きくします。私は0.003に設定しますが、あなたが良いと思う値を使用してください!

次に、フラグメントを黒で塗りつぶす代わりに、単に破棄することができます。つまり、WebGL にそれを全く塗りつぶさないように指示します。これにより、最終的なマテリアルが透明になり、ページの背景とより良くブレンドされるという利点もあります。WebGLRendererのコンストラクタ呼び出しでalphaパラメータも設定してください!

void main() {
  float noise = snoise(vec3(gl_FragCoord.xy * 0.003, 1.0));
  noise = 10.0 * (noise + 1.0) / 2.0;

  float rounded = ceil(noise);
  float rounding_error = rounded - noise;

  if (rounding_error > 0.1)
    discard;

  gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}

アニメーション#

snoise関数には、これまで1に設定していた 2 番目のパラメータがあることに気づいたかもしれません。この値は実際には「時間」値であり、パーリンノイズアルゴリズムの一部です。これを1.0以外の値に設定すると、シェーダーから異なるパターンが表示されます。ただし、非常にクールなのは、この時間値の類似の値が「類似の」パーリンノイズパターンを生成することです。つまり、時間値が1.1の場合、私たちが見ているものとはわずかに異なるパターンが生成されます。これを「時間」ではなく「位相」パラメータとして考えてみてください。これを利用して、ThreeJS のクロックの値を設定します。クロックは連続的に増加するため、私たちのパーリンノイズパターンも時間とともにゆっくりと変化するはずです。Clockを作成し、ShaderMaterialのユニフォームとして使用します:

// ..snip
const clock = new THREE.Clock();
// ..snip
const material = new THREE.ShaderMaterial({
  uniforms: {
    time: {
      value: clock.startTime,
    },
  },
  vertexShader: vs,
  fragmentShader: snoise + fs,
  side: THREE.BackSide,
});

もちろん、フラグメントシェーダーにもこのユニフォームについて教えて、snoise関数に渡す必要があります:

uniform float time;

void main() {
  float noise = snoise(vec3(gl_FragCoord.xy * 0.003, time));
  noise = 10.0 * (noise + 1.0) / 2.0;

  float rounded = ceil(noise);
  float rounding_error = rounded - noise;

  if (rounding_error > 0.1)
    discard;

  gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}

あなたのパターンが大きく変わったことに気づくかもしれませんが、アニメーションはありません 🤔 それは、ユニフォームを一度設定しただけで、更新していないからです!レンダーループ内でtimeユニフォームを更新する必要があります:

// ..snip
function frame() {
  requestAnimationFrame(frame);

  material.uniforms.time.value = clock.startTime + clock.getElapsedTime();
  renderer.render(scene, camera);
}
frame();

わお!トリッピーなアニメーションが表示されるはずです!もちろん、速すぎるので、snoise関数に渡すtimeユニフォームを小さな数(例えば0.01)で掛けることで遅くすることができます。

これで全てが完了です!もう一つできることは、フラグメントシェーダーで塗る色を制御できるcolorユニフォームを作成することですが、それはあなたに任せます。最後に、私が良いと思ういくつかの色の組み合わせを紹介します:

topo1

topo2

topo3

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。