隨機不只是 Math.random —— 前端噪聲應用

騰訊IVWEB團隊發表於2019-07-12

作者:周志創
閱讀時間:10~15min

在瞭解噪聲之前,我對隨機的認識,僅僅停留在 Math.random 。它很有用,比如 H5 裡的簡單抽獎程式,或者隨機選取一張卡片... 而最近工作中需要實現一些的隨機影像效果,讓我發現這個函式能做的事十分有限。之後我偶然瞭解到噪聲這一種隨機形式,它很完美的解決了我的問題。於是我想寫這一篇文章,希望可以讓一些前端同學,特別是工作上涉及較多效果還原的前端同學瞭解噪聲,或許在這之後,你會對設計師設計稿上這些隨意元素,有更多的想法。

設計師經常會在他們的設計中新增一些隨意元素,這是一種很棒的設計技巧。但是對於前端實現來說,這類設計元素的還原大多數情況會讓我們感到無能為力,因為基礎 Api 提供的都是規律的幾何形狀(圓,矩形等...),常常最後,這些效果都只好妥協使用切圖還原,我們不能做更多優化,更不能通過增加動畫表現更多的設計想法,這一直是大多數同學前端還原的空白領域。

設計稿中的隨意元素

上面這些圖片都是從一些真實設計稿中擷取的,其中很容易找到我所說的隨機元素,它們給設計增添神祕感和科技感,但是它們好像很難用傳統的前端設計還原技巧製作。而這些,恰恰非常適合使用噪聲來實現。

噪聲演算法經過多年發展已經非常成熟,而且有很多種分類 Perlin噪聲, Simplex噪聲,Wavelet噪聲,本篇文章裡邊出現的噪聲預設指的是 Perlin 噪聲,不會深入介紹具體噪聲演算法,主要目的是向沒有接觸過這個領域的前端同學作一個啟發式的介紹。

隨機的線

先來解決一個簡單的問題:畫出一根隨意的線。這似乎很簡單,我們用 Math.random 生成一系列的點,然後把他們連線起來

隨機線1

for (let i = 0; i < POINTCOUNT; i++) {
  const y = HEIGHT / 2 + (Math.random() - 0.5) * 2 * MAXOFFSET;
  const point = {
    x: i * STEP,
    y
  };
  POINTS.push(point);
}
...
ctx.moveTo(POINTS[0].x, POINTS[0].y);

for (let i = 1; i < POINTS.length; i++) {
  const point = POINTS[i];
  ctx.lineTo(point.x, point.y);
}
ctx.stroke();
複製程式碼

但是這沒有什麼特別的,像是刻意畫出來的折線圖。直線讓我們的影像很生硬,下面我們做點小改良,我們在兩個隨機點間找兩個控制點,用貝塞爾曲線再次將隨機點連起來:

隨機線2

for (let i = 0; i < POINTS.length - 1; i++) {
  const point = POINTS[i];
  const nextPoint = POINTS[i + 1];

  ctx.bezierCurveTo(
    controlPoints[i][1].x, 
    controlPoints[i][1].y, 
    controlPoints[i + 1][0].x, 
    controlPoints[i + 1][0].y,
    nextPoint.x,
    nextPoint.y
  );
}
ctx.stroke();
複製程式碼

這次自然多了,我們這一步做的事情很關鍵,雖然這不是噪聲演算法的本身,但是其中的思想是一樣的。上面例子的隨機點,我們可以理解為隨機特徵,單個隨機特徵是沒有意義的。之後我們用直線連線起來,它們整體看起來依然沒有聯絡,但是我們將他們平滑地串聯起來之後,它們互相之前形成了一個完整的圖案,得到了一種自然的隨機效果。這也是我學習完噪聲的主觀理解——噪聲主要解決的問題,就是讓互不相關的隨機特徵,產生某種平滑地過渡或聯絡。

好,到這裡我們停下來,想想大自然,放鬆一下。想想雲、河流、山脈... 這些、都是大自然裡各種隨機事件,日積月累創造出來的。這裡的關鍵,就是“日積月累”,也就是說不會平白無故地出現一片雲、不會突然就出現一條河,他們都是一點一點演變而來的,所以在這些隨機事件中是“平滑”變換的。

柏林噪聲

在現代遊戲和影視作品中,已經有能力逼真地還原這些自然元素了。它們並不是也不可能使用大量建模軟體製作,而是用程式碼生成的,這裡用到的關鍵技術,就是噪聲演算法。這種演算法重要的部分,就是在隨機特徵中的插值演算法,在隨機特徵中間的值能平滑過度,以模擬大自然中這種演變的結果。

Ken Perlin 早在1983年正在參與制作迪士尼的動畫電影《TRON》,就是為了實現這種自然的紋理效果,提出了 Perlin 噪聲,Perlin 噪聲很成功,他也因此獲得了奧斯卡科技成果獎。

我們上面例子為了在隨機特徵間平滑過度使用了貝塞爾曲線,而柏林噪聲則使用了更加科學複雜的插值方法:

f(t)=t*t*t*(t*(t*6-15)+10)
複製程式碼

有興趣可以自行查閱其中實現細節。柏林噪聲經過發展演變出了很多變形實現,包括後來柏林噪聲的優化版本 simplex 噪聲,本文側重於介紹噪聲的使用,下文其中一些實現,預設使用就是柏林噪聲。

噪聲函式的使用

噪聲函式的使用方法都是接受一個點,然後返回一個 -1 到 1 的結果,隨機特徵長度單位為 1,所以在使用噪聲函式之前,需要注意單位的轉換。 比如,你有一個 1000 x 1000 的畫布,想要使用噪聲對應每個畫素的隨機值,那就首先就要定義想應用多少個隨機方格,如果應用 n x n 個方格,那在獲取隨機值之前,就需要對座標進行轉換

scale = 1000 / n;

nx = x / scale;
ny = y / scale;

pointRandom = noise2D(nx, ny);
複製程式碼

js 實現的噪聲工具: github.com/josephg/noi… github.com/jwagner/sim…

隨機集

噪聲接受相同的引數總是能返回相同的結果,所以通常要預設一個隨機集,目的就是為了生成固定的隨機特徵點,然後平滑的隨機結果將在這些隨機特徵點中插值產生。當中的實現不做討論,有些噪聲工具預設已經設定好了一個隨機集,也可以自定義不同的種子來生成不同的隨機效果,上一個噪聲工具 noisejs 可以這樣簡單定義即可:

noise.seed(Math.random());
複製程式碼

一維噪聲

一維噪聲可以這樣簡單理解,將隨機集分配到 x 軸上整數位上,通過平滑插值函式計算連續變換的隨機值。這個說法可能和具體 Perlin 噪聲不完全一樣,但是基本思路可以這樣簡單理解

一維噪聲

簡單從結果上來講就是給定一個座標,你將得到一個隨機值,如果給定連續的座標,你將可以得到連續的隨機值,而不是像 Math.random() 那樣得到互不相關的錯落的值。

一維噪聲示例

下面的例子是使用一維噪聲製作的一個雞蛋的效果,先在圓上取固定的等分的點,它們分別通過 noise 獲取到一個隨機偏移,然後再結合時間,讓偏移隨著時間平滑地改變:

codepen.io/chiunhauyou…

egg

比如下面這個火的效果,火焰尾部搖曳的效果的偏移,就是一維噪聲結合時間偏移實現的

codepen.io/gnauhca/pen…

fire

一維噪聲這種隨時間偏移的效果,還可以通過把時間作為二維噪聲函式的 y 引數實現。

二維噪聲

二維噪聲通過定義一個二維的方格,上一步定義的固定的隨機集會被分佈在這些方格頂點上,而每一個頂點的隨機特徵是一個梯度向量,然後,計算方格內的點周圍四個頂點到方格內一個點(也就是需要求噪聲值的點,下圖中黃色的點)的向量,用他們與梯度向量點乘,最後使用插值函式進行插值得到該點對應的隨機噪聲值。

二維噪聲1

下面影像是使用 webGL 應用 2D 噪聲函式生成的,白色代表 1,黑色代表 -1,這是 5 * 5 方格的效果。這裡不用關心 webgl,我們只要關心如何定義適用自己問題的方格,還有獲取對應的連續的隨機值。

二維噪聲2

這裡側重的是介紹噪聲使用,具體實現方法可以自己點選下面參考連結瞭解。使用方法很簡單:

// 這裡除 100 是為了以一百個畫素為單位應用一個噪聲方格
let randomValue = noise.simplex2(x / 100, y / 100); 
複製程式碼

二維噪聲示例

噪聲函式獲取的隨機值就很自由了,它可以用來定義各種你需要的變數,比如下面這個例子,平滑地隨機值被用作定義粒子的速度向量從而實現既隨機又連續的運動效果:

function calculateField() {
  for(let x = 0; x < columns; x++) {
    for(let y = 0; y < rows; y++) {
      let angle = noise.simplex3(x/20, y/20, noiseZ) * Math.PI * 2;
      let length = noise.simplex3(x/40 + 40000, y/40 + 40000, noiseZ) * 0.5;
      field[x][y].setLength(length);
      field[x][y].setAngle(angle);
    }
  }
}
複製程式碼

生成向量一個隨機角度,和向量長度,這裡使用 simplex3 三維噪聲實際上第三個引數是被用作二維的偏移量

codepen.io/DonKarlsson…

二維噪聲示例1

如果保留運動痕跡,則可以得到更炫酷的效果

// 使用半透明清楚畫布
function drawBackground(alpha) {
  ctx.fillStyle = `rgba(0, 0, 0, ${alpha || 0.07})`;
  ctx.fillRect(0, 0, w, h);
}
複製程式碼

codepen.io/DonKarlsson…

二維噪聲示例2

二維噪聲示例(svg)

通過把幾個不同頻率的 Perlin 噪聲相疊加,這項技術叫做分形噪聲,可以實現更多紋理,比如水流,山川。 SVG 中有一個這種噪聲的濾鏡應用,feTurbulence,對比普通我們熟知的 css 濾鏡,它很容易被人忽略,但是它卻能實現很多意想不到的效果:

<feTurbulence type="turbulence" baseFrequency="0.01 .1" numOctaves="1" result="turbulence" seed="53" />
複製程式碼
  • numOctaves 噪聲疊加數
  • seed 隨機種子
  • type 噪聲型別

feTurbulence 實際上是在每個畫素上應用得到的噪聲值,從而得到顏色值,然後可以結合其他濾鏡,將這些隨機顏色轉換成其他隨機表現,如畫素偏移,可以使用 feTurbulence 濾鏡實現一些自然紋理,還有倒影效果。

feTurbulence 濾鏡輸出

codepen.io/yoksel/pen/…

二維噪聲示例(svg)

wow.techbrood.com/fiddle/3165…

二維噪聲示例(svg)2

這裡的噪聲疊加在頻率和振幅上有一定的約束,它們是自相似的,參考thebookofshaders.com/13

三維噪聲

三維噪聲實際上就只是在二維噪聲基礎上又增加一個緯度,定義一個三維的隨機頂點集,在三維方格當中的三維座標,將會對應得到一個噪聲值,同樣也是連續的。

三維噪聲獲取的隨機值可以轉換成一個三維點在其法向向量方向的偏移,從而實現一種隨機的起伏變形。在 webGL 當中,這一步這通常在頂點著色器當中完成,頂點著色器會對所有定義的頂點執行:

float addLength = maxLength * cn(normalize(position) * 2.9 + time * 0.9); // 計算隨機值
vec3 newPosition = position + normal * addLength; // 轉換為法向向量方向上的偏移值
複製程式碼

codepen.io/gnauhca/pen…

三維噪聲1

在片元著色器中,則可以應用使用噪聲製作紋理:

codepen.io/timseverien…

三維噪聲2

就像之前提到的噪聲被設計出來的一開始的目的,就是應用在影視/遊戲作品當中模擬自然效果的,所以,使用 webGL 製作海洋,山川這種效果,也自然不在話下: codepen.io/matikbird/p…

三維噪聲3
三維噪聲4

當然這不僅需要熟練使用噪聲,還需要掌握 webGL,雖然一開始我們介紹了噪聲在 canvas/svg 中的使用方法,但是,現在 webGL 已經被電腦端手機端瀏覽器廣泛支援,結合 webGL ,噪聲能發揮它更大價值。

結語

噪聲的應用十分廣泛,是圖形學領域的重要知識,在三維程式扮演重要的角色。但是噪聲不是三維圖形領域的專屬,學習使用噪聲,在 canvas svg css 這些基礎的前端技術上應用,也能實現一些意想不到的效果,當某一天設計師又輸出了一個類似這種隨機特徵的效果圖,不妨直接找到設計師溝通。說不定這些效果,就是用某個影像處理軟體使用噪聲生成的。如果獲取到生成的引數,在前端也是有可能實現的,通過結合時間偏移,說不定就能實現一個很棒的動畫。

參考資料

blog.csdn.net/qq_34302921…

codepen.io/DonKarlsson…

thebookofshaders.com/13/?lan=ch


隨機不只是 Math.random —— 前端噪聲應用

關注【IVWEB社群】公眾號獲取每週最新文章,通往人生之巔!

相關文章