Ainul Ξ Creative Dev

Ainul Ξ Creative Dev

Product-focused software engineer.
twitter
tg_channel

地形線條藝術

在 2021 年初至中期,我到處都看到這種藝術風格,特別是在品牌設計中。甚至我還擁有一個看起來完全一樣的桌墊。這就是我所說的:

Topographic

基本設置#

我做的第一件事是研究;設計師是如何做到這一點的?這似乎太複雜,無法手動構建,但又太有組織,無法隨機生成。這一定是程序生成的,對吧?我找到了一個這個教程,還有其他的。一般的想法似乎是從一些 Perlin 噪聲開始,模糊它,調整級別以增加對比度,然後檢測邊緣。我們不完全這樣做,但我們會走一條相似的路。

我們將使用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。很好。

讓我們將MeshBasicMaterial替換為ShaderMaterial,並編寫一個基本的頂點和片段著色器,以確保它們也能正常工作:

// ..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現在應該變成紅色。完成這一切後,讓我們進入有趣的部分。

著色器#

讓我們創建一些 Perlin 噪聲。我們可以在 JS 端(在 CPU 上)做到這一點,但由於我計劃對噪聲進行動畫處理,這樣做會很快變得昂貴,因為我們必須至少每秒做 60 次。相反,我們將在 GPU 上進行這一操作,GPU 在這方面非常擅長。在 GLSL 中編寫 Perlin 噪聲函數遠遠超出了這篇文章的範疇,所以我找到了一個。我將其複製到我的片段著色器中,放在main函數上方。然後我們可以使用snoise函數的返回值來生成片段顏色:

// 在這裡粘貼`noise3D.glsl`的內容

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

你可能還看不到太多,因為噪聲太小了。讓我們將gl_FragCoord.xy乘以0.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之間的所有值都轉換為112之間的所有值轉換為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

嘿,現在我們有進展了!不過,我們想要的不是顏色帶,而是這些顏色帶的_邊緣_。我們不需要實現一個花哨的邊緣檢測算法,我們可以利用在海報化過程中,我們只是將 Perlin 噪聲的實際值向下取整。如果你想想,實際值和四捨五入值之間的差異小於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。這個值實際上應該是一個 “時間” 值,並且是 Perlin 噪聲算法的一部分。如果你將其設置為1.0以外的值,你將看到著色器中的不同模式。不過,令人驚訝的是,這個 “時間” 值的相似值會產生 “相似” 的 Perlin 噪聲模式。我的意思是,1.1的 “時間” 值會創建一個與我們所看到的模式只有輕微不同的模式。把它想像成一個 “相位” 參數,而不是 “時間”。我們可以利用這一點,將其設置為 ThreeJS 時鐘的值。因為它不斷增加,我們_應該_也能看到我們的 Perlin 噪聲模式隨著時間的推移緩慢變化。讓我們創建一個Clock,並將其用作我們ShaderMaterial中的一個 uniform:

// ..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,
});

當然,我們還必須告訴我們的片段著色器這個 uniform,以便我們可以將其傳遞給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);
}

你可能會注意到你的模式發生了劇烈變化,但沒有動畫🤔。這是因為我們只設置了一次 uniform,但從未更新它!我們必須確保在渲染循環中更新我們的time uniform:

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

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

哇!你應該會看到一個迷幻的動畫!當然,這太快了,所以我們可以通過將傳遞給snoise函數的time uniform 乘以一個小數字,例如0.01來減慢它。

這就是所有內容!你還可以創建一個color uniform,以便你可以控制在片段著色器中繪製的顏色,但我將這留給你。這裡有幾個我認為效果不錯的顏色組合,作為結尾:

topo1

topo2

topo3

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。