WebGL進階——走進圖形噪聲

導語:大天然蘊含着各式各樣的紋理,小到細胞菌落分佈,大到宇宙星球表面。運用圖形噪聲,咱們能夠在3d場景中模擬它們,本文就帶你們一塊兒走進萬能的圖形噪聲。javascript

概述

圖形噪聲,是計算機圖形學中一類隨機算法,常常用來模擬天然界中的各類紋理材質,以下圖的雲、山脈等,都是經過噪聲算法模擬出來的​。html

Noise構造地形、體積雲
經過不一樣的噪聲算法,做用在物體紋理和材質細節,咱們能夠模擬不一樣類型的材質。前端

不一樣Noise生成的材質

基礎噪聲算法

一個基礎的噪聲函數的入參一般是一個點座標(這個點座標能夠是二維的、三維的,甚至N維),返回值是一個浮點數值:noise(vec2(x,y))
咱們將這個浮點值轉成灰度顏色,造成噪聲圖,具體能夠經過編寫片元着色器程序來繪製。java

噪聲函數灰度圖

上圖是各種噪聲函數在片元着色器中的運行效果,代碼以下:git

// noise fragment shader
varying vec2 uv;
float noise(vec2 p) {
  // TODO
}
void main() {
    float n = noise(uv);  // 經過噪聲函數計算片元座標對應噪聲值
    gl_FragColor = vec4(n, n, n, 1.0);
}

其中noise(st)的入參st是片元座標,返回的噪聲值映射在片元的顏色上。
目前基礎噪聲算法比較主流的有兩類:1. 梯度噪聲;2. 細胞噪聲;github

梯度噪聲 (Gradient Noise)

梯度噪聲產生的紋理具備連續性,因此常常用來模擬山脈、雲朵等具備連續性的物質,該類噪聲的典型表明是Perlin Noise。web

Perlin Noise爲Perlin提出的噪聲算法

其它梯度噪聲還有Simplex Noise和Wavelet Noise,它們也是由Perlin Noise演變而來。算法

算法步驟

梯度噪聲是經過多個隨機梯度相互影響計算獲得,經過梯度向量的方向與片元的位置計算噪聲值。這裏以2d舉例,主要分爲四步:1. 網格生成;2. 網格隨機梯度生成;3. 梯度貢獻值計算;4. 平滑插值api

Perlin Noise隨機向量表明梯度

第一步,咱們將2d平面分紅m×n個大小相同的網格,具體數值取決於咱們須要生成的紋理密度(下面以4×4做爲例子);app

#define SCALE 4. // 將平面分爲 4 × 4 個正方形網格
float noise(vec2 p) {
  p *= SCALE;
  // TODO
}

第二步,梯度向量生成,這一步是根據第一步生成的網格的頂點來產生隨機向量,四個頂點就有四個梯度向量;

生成隨機向量

咱們須要將每一個網格對應的隨機向量記錄下來,確保不一樣片元在相同網格中獲取的隨機向量是一致的。

// 輸入網格頂點位置,輸出隨機向量
vec2 random(vec2 p){
    return  -1.0 + 2.0 * fract(
        sin(
            vec2(
                dot(p, vec2(127.1,311.7)),
                dot(p, vec2(269.5,183.3))
            )
        ) * 43758.5453
    );
}

如上,借用三角函數sin(θ)的來生成隨機值,入參是網格頂點的座標,返回值是隨機向量。

第三步,梯度貢獻計算,這一步是經過計算四個梯度向量對當前片元點P的影響,主要先求出點P到四個頂點的距離向量,而後和對應的梯度向量進行點積。

梯度貢獻值計算

如圖,網格內的片元點P的四個頂點距離向量爲a1, a2, a3, a4,此時將距離向量與梯度向量g1, g2, g3, g4進行點積運算:c[i] = a[i] · g[i];

第四步,平滑插值,這一步咱們對四個貢獻值進行線性疊加,使用smoothstep()方法,平滑網格邊界,最終獲得當前片元的噪聲值。具體代碼以下:

float noise_perlin (vec2 p) {
    vec2 i = floor(p); // 獲取當前網格索引i
    vec2 f = fract(p); // 獲取當前片元在網格內的相對位置
    // 計算梯度貢獻值
    float a = dot(random(i),f); // 梯度向量與距離向量點積運算
    float b = dot(random(i + vec2(1., 0.)),f - vec2(1., 0.));
    float c = dot(random(i + vec2(0., 1.)),f - vec2(0., 1.));
    float d = dot(random(i + vec2(1., 1.)),f - vec2(1., 1.));
    // 平滑插值
    vec2 u = smoothstep(0.,1.,f);
    // 疊加四個梯度貢獻值
    return mix(mix(a,b,u.x),mix(c,d,u.x),u.y);
}

細胞噪聲 (Celluar Noise)

細胞噪聲生成水紋

Celluar Noise生成的噪聲圖由不少個「晶胞」組成,每一個晶胞向外擴張,晶胞之間相互抑制。這類噪聲能夠模擬細胞形態、皮革紋理等。

worley noise

算法步驟

細胞噪聲算法主要經過距離場的形式實現的,以單個特徵點爲中心的徑向漸變,多個特徵點共同做用而成。主要分爲三步:1. 網格生成;2. 特徵點生成;3. 最近特徵點計算

特徵點距離場

第一步,網格生成:將平面劃分爲m×n個網格,這一步和梯度噪聲的第一步同樣;
第二步,特徵點生成:爲每一個網格分配一個特徵點v[i,j],這個特徵點的位置在網格內隨機。

// 輸入網格索引,輸出網格特徵點座標
vec2 random(vec2 st){
    return  fract(
        sin(
            vec2(
                dot(st, vec2(127.1,311.7)),
                dot(st, vec2(269.5,183.3))
            )
        ) * 43758.5453
    );
}

第三步,針對當前像素點p,計算出距離點p最近的特徵點v,將點p到點v的距離記爲F1;

float noise(vec2 p) {
    vec2 i = floor(p); // 獲取當前網格索引i
    vec2 f = fract(p); // 獲取當前片元在網格內的相對位置
    float F1 = 1.;
    // 遍歷當前像素點相鄰的9個網格特徵點
    for (int j = -1; j <= 1; j++) {
        for (int k = -1; k <= 1; k++) {
            vec2 neighbor = vec2(float(j), float(k));
            vec2 point = random(i + neighbor);
            float d = length(point + neighbor - f);
            F1 = min(F1,d);
        }
    }
    return F1;
}

求解F1,咱們能夠遍歷全部特徵點v,計算每一個特徵點v到點p的距離,再取出最小的距離F1;但實際上,咱們只需遍歷離點p最近的網格特徵點便可。在2d中,則最多遍歷包括自身相連的9個網格,如圖:

求解F1:點P的最近特徵點距離

最後一步,將F1映射爲當前像素點的顏色值,能夠是gl_FragColor = vec4(vec3(pow(noise(uv), 2.)), 1.0);
不只如此,咱們還能夠取特徵點v到點p第二近的距離F2,經過F2 - F1,獲得相似泰森多變形的紋理,如上圖最右側。

噪聲算法組合

前面介紹了兩種主流的基礎噪聲算法,咱們能夠經過對多個不一樣頻率的同類噪聲進行運算,產生更爲天然的效果,下圖是通過分形操做後的噪聲紋理。
基礎噪聲 / 分形 / 湍流

分形布朗運動(Fractal Brownian Motion)

分形布朗運動,簡稱fbm,是經過將不一樣頻率和振幅的噪聲函數進行操做,最經常使用的方法是:將頻率乘2的倍數,振幅除2的倍數,線性相加。

  • 公式:fbm = noise(st) + 0.5 * noise(2*st) + 0.25 * noise(4*st)
// fragment shader片元着色器
#define OCTAVE_NUM 5
// 疊加5次的分形噪聲
float fbm_noise(vec2 p)
{
    float f = 0.0;
    p = p * 4.0;
    float a = 1.;
    for (int i = 0; i < OCTAVE_NUM; i++)
    {
        f += a * noise(p);
        p = 4.0 * p;
        a /= 4.;
    }
    return f;
}

湍流(Turbulence)

另一種變種是在fbm中對噪聲函數取絕對值,使噪聲值等於0處發生突變,產生湍流紋理:

  • 公式:fbm = |noise(st)| + 0.5 * |noise(2*st)| + 0.25 * |noise(4*st)|
// 湍流分形噪聲
float fbm_abs_noise(vec2 p)
{
    ...
    for (int i = 0; i < OCTAVE_NUM; i++)
    {
        f += a * abs(noise(p)); // 對噪聲函數取絕對值
        ...
    }
    return f;
}

如今結合上文提到的梯度噪聲和細胞噪聲分別進行fbm,能夠實現如下效果:

Perlin Noise與Worley Noise的2D分形

翹曲域(Domain Wrapping)

翹曲域噪聲用來模擬捲曲、螺旋狀的紋理,好比煙霧、大理石等,實現公式以下:

  • 公式:f(p) = fbm( p + fbm( p + fbm( p ) ) )
float domain_wraping( vec2 p )
{
    vec2 q = vec2( fbm(p), fbm(p) );

    vec2 r = vec2( fbm(p + q), fbm(p + q) );

    return fbm( st + r );
}

具體實現可參考Inigo Quiles的文章:https://www.iquilezles.org/www/articles/warp/warp.htm

動態紋理

前面講的都是基於2d平面的靜態噪聲,咱們還能夠在2d基礎上加上時間t維度,造成動態的噪聲。

2D + Time 動態噪聲

以下爲實現3d noise的代碼結構:

// noise fragment shader
#define SPEED 20.
varying vec2 uv;
uniform float u_time;
float noise(vec3 p) {
  // TODO
}
void main() {
    float n = noise(uv, u_time *  SPEED);  // 傳入片元座標與時間
    gl_FragColor = vec4(n, n, n, 1.0);
}

利用時間,咱們能夠生成實現動態紋理,模擬如火焰、雲朵的變換。

Perlin Noise製做火焰

噪聲貼圖應用

利用噪聲算法,咱們能夠構造物體表面的紋理顏色和材質細節,在3d開發中,通常採用貼圖方式應用在3D Object上的Material材質上。

Color Mapping

彩色貼圖是最經常使用的是方式,即直接將噪聲值映射爲片元顏色值,做爲材質的Texture圖案。

噪聲應用於Color Mapping

Height Mapping

另外一種是做爲Height Mapping高度貼圖,生成地形高度。高度貼圖的每一個像素映射到平面點的高度值,經過圖形噪聲生成的Height Map可模擬綿亙不絕的山脈。

Fbm Perlin Noise→heightmap→山脈

Normal Mapping

除了經過heightMap生成地形,還能夠經過法線貼圖改變光照效果,實現材質表面的凹凸細節。

Worley Noise→Normalmap→地表細節

這裏的噪聲值被映射爲法線貼圖的color值。

噪聲貼圖實踐

在WebGL中使用噪聲貼圖一般有兩種方法:

  1. 讀取一張靜態noise圖片的噪聲值;
  2. 加載noise程序,切換着色器中運行它

前者沒必要多說,適用於靜態紋理材質,後者適用於動態紋理,這裏主要介紹後者的實現。

這裏將經過實現如上圖球體的紋理貼圖效果,爲了簡化代碼,我使用Three.js來實現。
demo預覽:https://yonechen.github.io/webgl-noise-examples/web/index.html

首先,按往常同樣建立場景、相機、渲染器,在初始化階段建立一個球體,咱們將把噪聲紋理應用在這顆球體上:

class Web3d {
    constructor() { ... } // 建立場景、相機、渲染器
    // 渲染前初始化鉤子
    start() {
        this.addLight(); // 添加燈光
        this.addBall(); // 添加一個球體
    }
    addBall() {
        const { scene } = this;
        this.initNoise();
        const geometry = new THREE.SphereBufferGeometry(50, 32, 32); // 建立一個半徑爲50的球體
        // 建立材質
        const material = new THREE.MeshPhongMaterial( {
            shininess: 5,
            map: this.colorMap.texture // 將噪聲紋理做爲球體材質的colorMap
        } );
        const ball = new THREE.Mesh( geometry, material );
        ball.rotation.set(0,-Math.PI,0);
        scene.add(ball);
    }
    // 動態渲染更新鉤子
    update() { }
}

接着,編寫Noise shader程序,咱們把前面的梯度噪聲shader搬過來稍微封裝下:

const ColorMapShader = {
    uniforms: {
        "scale": { value: new THREE.Vector2( 1, 1 ) },
        "offset": { value: new THREE.Vector2( 0, 0 ) },
        "time": { value: 1.0 },
    },
    vertexShader: `
        varying vec2 vUv;
        uniform vec2 scale;
        uniform vec2 offset;

        void main( void ) {
            vUv = uv * scale + offset;
            gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
        }
    `,
    fragmentShader: `
        varying vec2 vUv;
        uniform float time;
        vec3 random_perlin( vec3 p ) {
            p = vec3(
                    dot(p,vec3(127.1,311.7,69.5)),
                    dot(p,vec3(269.5,183.3,132.7)), 
                    dot(p,vec3(247.3,108.5,96.5)) 
                    );
            return -1.0 + 2.0*fract(sin(p)*43758.5453123);
        }
        float noise_perlin (vec3 p) {
            vec3 i = floor(p);
            vec3 s = fract(p);

            // 3D網格有8個頂點
            float a = dot(random_perlin(i),s);
            float b = dot(random_perlin(i + vec3(1, 0, 0)),s - vec3(1, 0, 0));
            float c = dot(random_perlin(i + vec3(0, 1, 0)),s - vec3(0, 1, 0));
            float d = dot(random_perlin(i + vec3(0, 0, 1)),s - vec3(0, 0, 1));
            float e = dot(random_perlin(i + vec3(1, 1, 0)),s - vec3(1, 1, 0));
            float f = dot(random_perlin(i + vec3(1, 0, 1)),s - vec3(1, 0, 1));
            float g = dot(random_perlin(i + vec3(0, 1, 1)),s - vec3(0, 1, 1));
            float h = dot(random_perlin(i + vec3(1, 1, 1)),s - vec3(1, 1, 1));

            // Smooth Interpolation
            vec3 u = smoothstep(0.,1.,s);

            // 根據八個頂點進行插值
            return mix(mix(mix( a, b, u.x),
                        mix( c, e, u.x), u.y),
                    mix(mix( d, f, u.x),
                        mix( g, h, u.x), u.y), u.z);
        }
        float noise_turbulence(vec3 p)
        {
            float f = 0.0;
            float a = 1.;
            p = 4.0 * p;
            for (int i = 0; i < 5; i++) {
                f += a * abs(noise_perlin(p));
                p = 2.0 * p;
                a /= 2.;
            }
            return f;
        }
        void main( void ) {
            float c1 = noise_turbulence(vec3(vUv, time/10.0));
            vec3 color = vec3(1.5*c1, 1.5*c1*c1*c1, c1*c1*c1*c1*c1*c1);
            gl_FragColor = vec4( color, 1.0 );
        }
    `
};

OK,如今讓WebGL去加載這段程序,並告訴它這段代碼是要做爲球體的紋理貼圖的:

initNoise() {
        const { scene, renderer } = this;
        // 建立一個噪聲平面,做爲運行噪聲shader的載體。
        const plane = new THREE.PlaneBufferGeometry( window.innerWidth, window.innerHeight );
        const colorMapMaterial = new THREE.ShaderMaterial( {
            ...ColorMapShader, // 將噪聲着色器代碼傳入ShaderMaterial
            uniforms: {
                ...ColorMapShader.uniforms,
                scale: { value: new THREE.Vector2( 1, 1 ) }
            },
            lights: false
        } );
        const noise = new THREE.Mesh( plane, colorMapMaterial );
        scene.add( noise );
        // 建立噪聲紋理的渲染對象framebuffer。
        const colorMap = new THREE.WebGLRenderTarget( 512, 512 );
        colorMap.texture.generateMipmaps = false;
        colorMap.texture.wrapS = colorMap.texture.wrapT = THREE.RepeatWrapping;
        this.noise = noise;
        this.colorMap = colorMap;
        this.uniformsNoise = colorMapMaterial.uniforms;
        // 建立一個正交相機,對準噪聲平面。
        this.cameraOrtho = new THREE.OrthographicCamera( window.innerWidth / - 2, window.innerWidth / 2, window.innerHeight / 2, window.innerHeight / - 2, - 10000, 10000 );
        this._renderNoise();
    }

第四步,讓renderer動態運行噪聲shader,更新噪聲變量,能夠是時間、顏色、偏移量等。

_renderNoise() {
        const { scene, noise, colorMap, renderer, cameraOrtho } = this;
        noise.visible = true;
        renderer.setRenderTarget( colorMap );
        renderer.clear();
        renderer.render( scene, cameraOrtho );
        noise.visible = false;
    }
    update(delta) {
        this.uniformsNoise[ 'time' ].value += delta; // 更新noise的時間,生成動態紋理
        this._renderNoise();
    }

經過一樣的方法,咱們能夠試着用在將高度貼圖上,好比用Worley Noise構造的鵝卵石地表:https://yonechen.github.io/webgl-noise-examples/web/heightmap.html
Worley Noise構造地形

最後

我是Yone,鵝廠前端一枚,歡迎和我一塊兒交流WebGL相關知識。

參考資料

相關文章
相關標籤/搜索