WebGL進階——走進圖形噪聲

blank

WebGL進階——走進圖形噪聲

導語:大自然蘊含著各式各樣的紋理,小到細胞菌落分佈,大到宇宙星球表面。運用圖形噪聲,我們可以在3d場景中模擬它們,本文就帶大家一起走進萬能的圖形噪聲。

概述

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

blank
Noise構造地形、體積雲

通過不同的噪聲算法,作用在物體紋理和材質細節,我們可以模擬不同類型的材質。

blank
不同Noise生成的材質

基礎噪聲算法

一個基礎的噪聲函數的入參通常是一個點坐標(這個點坐標可以是二維的、三維的,甚至N維),返回值是一個浮點數值: noise(vec2(x,y))
我們將這個浮點值轉成灰度顏色,形成噪聲圖,具體可以通過編寫片元著色器程序來繪製。

blank
噪聲函數灰度圖

上圖是各類噪聲函數在片元著色器中的運行效果,代碼如下:

// noise fragment shadervaryingvec2uv;floatnoise(vec2p){// TODO}voidmain(){floatn=noise(uv);// 通过噪声函数计算片元坐标对应噪声值gl_FragColor=vec4(n,n,n,1.0);}

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

梯度噪聲(Gradient Noise)

梯度噪聲產生的紋理具有連續性,所以經常用來模擬山脈、雲朵等具有連續性的物質,該類噪聲的典型代表是Perlin Noise。

Perlin Noise為Perlin提出的噪聲算法

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

算法步驟

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

blank
Perlin Noise隨機向量代表梯度(圖片摘自網絡)

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

#define SCALE 4. // 将平面分为 4 × 4 个正方形网格floatnoise(vec2p){p*=SCALE;// TODO}

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

生成隨機向量(圖片摘自網絡)

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

// 输入网格顶点位置,输出随机向量vec2random(vec2p){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到四個頂點的距離向量,然後和對應的梯度向量進行點積。

blank
梯度貢獻值計算

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

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

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

細胞噪聲(Celluar Noise)

blank
細胞噪聲生成水紋

Celluar Noise生成的噪聲圖由很多個“晶胞”組成,每個晶胞向外擴張,晶胞之間相互抑制。這類噪聲可以模擬細胞形態、皮革紋理等。

blank
worley noise

算法步驟

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

特徵點距離場

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

// 输入网格索引,输出网格特征点坐标vec2random(vec2st){returnfract(sin(vec2(dot(st,vec2(127.1,311.7)),dot(st,vec2(269.5,183.3))))*43758.5453);}

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

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

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

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

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

噪聲算法組合

前面介紹了兩種主流的基礎噪聲算法,我們可以通過對多個不同頻率的同類噪聲進行運算,產生更為自然的效果,下圖是經過分形操作後的噪聲紋理。

blank
基礎噪聲/ 分形/ 湍流

分形布朗運動(Fractal Brownian Motion)

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

blank

  • 公式: fbm = noise(st) + 0.5 * noise(2*st) + 0.25 * noise(4*st)
// fragment shader片元着色器#define OCTAVE_NUM 5// 叠加5次的分形噪声floatfbm_noise(vec2p){floatf=0.0;p=p*4.0;floata=1.;for(inti=0;i<OCTAVE_NUM;i++){f+=a*noise(p);p=4.0*p;a/=4.;}returnf;}

湍流(Turbulence)

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

  • 公式: fbm = |noise(st)| + 0.5 * |noise(2*st)| + 0.25 * |noise(4*st)|
// 湍流分形噪声floatfbm_abs_noise(vec2p){...for(inti=0;i<OCTAVE_NUM;i++){f+=a*abs(noise(p));// 对噪声函数取绝对值...}returnf;}

現在結合上文提到的梯度噪聲和細胞噪聲分別進行fbm,可以實現以下效果:

Perlin Noise與Worley Noise的2D分形

翹曲域(Domain Wrapping)

blank

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

  • 公式: f(p) = fbm( p + fbm( p + fbm( p ) ) )
floatdomain_wraping(vec2p){vec2q=vec2(fbm(p),fbm(p));vec2r=vec2(fbm(p+q),fbm(p+q));returnfbm(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.varyingvec2uv;uniformfloatu_time;floatnoise(vec3p){// TODO}voidmain(){floatn=noise(uv,u_time*SPEED);// 传入片元坐标与时间gl_FragColor=vec4(n,n,n,1.0);}

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

blank
Perlin Noise製作火焰

噪聲貼圖應用

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

Color Mapping

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

blank
噪聲應用於Color Mapping

Height Mapping

另一種是作為Height Mapping高度貼圖,生成地形高度。高度貼圖的每個像素映射到平麵點的高度值,通過圖形噪聲生成的Height Map可模擬連綿起伏的山脈。

blank
Fbm Perlin Noise→heightmap→山脈

Normal Mapping

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

blank
Worley Noise→Normalmap→地表細節

這裡的噪聲值被映射為法線貼圖的color值。

噪聲貼圖實踐

在WebGL中使用噪聲貼圖通常有兩種方法:

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

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

這裡將通過實現如上圖球體的紋理貼圖效果,為了簡化代碼,我使用Three.js來實現。
demo預覽: yonechen.github.io/webg

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

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

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

constColorMapShader={uniforms:{"scale":{value:newTHREE.Vector2(1,1)},"offset":{value:newTHREE.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 Interpolationvec3 u = smoothstep(0.,1.,s);//根據八個頂點進行插值return mix(mix(mix( a, b, ux),mix( c, e, ux), uy),mix(mix( d, f, ux),mix( g, h, ux), uy), uz);}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的載體。constplane=newTHREE.PlaneBufferGeometry(window.innerWidth,window.innerHeight);constcolorMapMaterial=newTHREE.ShaderMaterial({...ColorMapShader,//將噪聲著色器代碼傳入ShaderMaterialuniforms:{...ColorMapShader.uniforms,scale:{value:newTHREE.Vector2(1,1)}},lights:false});constnoise=newTHREE.Mesh(plane,colorMapMaterial);scene.add(noise);//創建噪聲紋理的渲染對象framebuffer。constcolorMap=newTHREE.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=newTHREE.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構造的鵝卵石地表: yonechen.github.io/webg

blank
Worley Noise構造地形

最後

本文相關的代碼地址: github.com/YoneChen/web
我是前端Yone,喜歡分享Web 3D相關技術,歡迎大家關注

參考資料

What do you think?

Written by marketer

blank

Angular 6+依賴注入使用指南:providedIn與providers對比

blank

一種讓小程序支持JSX語法的新思路