導語:大天然蘊含着各式各樣的紋理,小到細胞菌落分佈,大到宇宙星球表面。運用圖形噪聲,咱們能夠在3d場景中模擬它們,本文就帶你們一塊兒走進萬能的圖形噪聲。javascript
圖形噪聲,是計算機圖形學中一類隨機算法,常常用來模擬天然界中的各類紋理材質,以下圖的雲、山脈等,都是經過噪聲算法模擬出來的。html
經過不一樣的噪聲算法,做用在物體紋理和材質細節,咱們能夠模擬不一樣類型的材質。一個基礎的噪聲函數的入參一般是一個點座標(這個點座標能夠是二維的、三維的,甚至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
梯度噪聲產生的紋理具備連續性,因此常常用來模擬山脈、雲朵等具備連續性的物質,該類噪聲的典型表明是Perlin Noise。web
其它梯度噪聲還有Simplex Noise和Wavelet Noise,它們也是由Perlin Noise演變而來。算法
梯度噪聲是經過多個隨機梯度相互影響計算獲得,經過梯度向量的方向與片元的位置計算噪聲值。這裏以2d舉例,主要分爲四步:1. 網格生成;2. 網格隨機梯度生成;3. 梯度貢獻值計算;4. 平滑插值api
第一步,咱們將2d平面分紅m×n個大小相同的網格,具體數值取決於咱們須要生成的紋理密度(下面以4×4做爲例子);bash
#define SCALE 4. // 將平面分爲 4 × 4 個正方形網格 float noise(vec2 p) { p *= SCALE; // TODO } 複製代碼
第二步,梯度向量生成,這一步是根據第一步生成的網格的頂點來產生隨機向量,四個頂點就有四個梯度向量;markdown
咱們須要將每一個網格對應的隨機向量記錄下來,確保不一樣片元在相同網格中獲取的隨機向量是一致的。
// 輸入網格頂點位置,輸出隨機向量 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生成的噪聲圖由不少個「晶胞」組成,每一個晶胞向外擴張,晶胞之間相互抑制。這類噪聲能夠模擬細胞形態、皮革紋理等。
細胞噪聲算法主要經過距離場的形式實現的,以單個特徵點爲中心的徑向漸變,多個特徵點共同做用而成。主要分爲三步: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映射爲當前像素點的顏色值,能夠是gl_FragColor = vec4(vec3(pow(noise(uv), 2.)), 1.0);
。 不只如此,咱們還能夠取特徵點v到點p第二近的距離F2,經過F2 - F1,獲得相似泰森多變形的紋理,如上圖最右側。
前面介紹了兩種主流的基礎噪聲算法,咱們能夠經過對多個不一樣頻率的同類噪聲進行運算,產生更爲天然的效果,下圖是通過分形操做後的噪聲紋理。
分形布朗運動,簡稱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; } 複製代碼
另一種變種是在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,能夠實現如下效果:
翹曲域噪聲用來模擬捲曲、螺旋狀的紋理,好比煙霧、大理石等,實現公式以下:
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的文章:www.iquilezles.org/www/article…
前面講的都是基於2d平面的靜態噪聲,咱們還能夠在2d基礎上加上時間t維度,造成動態的噪聲。
以下爲實現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); } 複製代碼
利用時間,咱們能夠生成實現動態紋理,模擬如火焰、雲朵的變換。
利用噪聲算法,咱們能夠構造物體表面的紋理顏色和材質細節,在3d開發中,通常採用貼圖方式應用在3D Object上的Material材質上。
彩色貼圖是最經常使用的是方式,即直接將噪聲值映射爲片元顏色值,做爲材質的Texture圖案。
另外一種是做爲Height Mapping高度貼圖,生成地形高度。高度貼圖的每一個像素映射到平面點的高度值,經過圖形噪聲生成的Height Map可模擬綿亙不絕的山脈。
除了經過heightMap生成地形,還能夠經過法線貼圖改變光照效果,實現材質表面的凹凸細節。
這裏的噪聲值被映射爲法線貼圖的color值。
在WebGL中使用噪聲貼圖一般有兩種方法:
這裏將經過實現如上圖球體的紋理貼圖效果,爲了簡化代碼,我使用Three.js來實現。 demo預覽:yonechen.github.io/webgl-noise…
首先,按往常同樣建立場景、相機、渲染器,在初始化階段建立一個球體,咱們將把噪聲紋理應用在這顆球體上:
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構造的鵝卵石地表:yonechen.github.io/webgl-noise…