导语:大天然蕴含着各式各样的纹理,小到细胞菌落分布,大到宇宙星球表面。运用图形噪声,咱们能够在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…