噪音的目的是为了增加随机性,使事物看起来更真实。但是简单的使用随机函数 并不 自然,因为随机函数产生的值与值之间并不连续。
要构造一个连续的2d噪音,简单的想法可以是: 在一个平面上所有整数点位置赋一个随机的值, 而整数网格中的点 则采用插值的方式得到值。
这里有两个问题, 一是整数格子上的值如何给, 二是插值的方式。
perlin噪声的解决方式是:
整数格子上的计算一个叫做gradient的东西,在2d空间这个gradient是2维度向量, 它的分布具有各向同性的特点,也就是每个整数格子点都有相同的概率得到某个方向的gradient。 实现的时候, gradient可以从 8个方向的向量里面获取, 具体取哪一个是需要一个随机的过程的。。
[1, 0], [1, 1],
[0, 1], [-1, 1],
[-1, 0], [-1, -1],
[0, -1], [1, -1],
获取一个点周围4个整数定点的 gradient之后, 将gradient和 4个点到该点的 向量 的内积 作为4个点的值,
接着需要对这4个值进行插值。
简单的2维 线性插值的方法是: 首先 沿 x方向 将 n00 n10 x插值, 再将 n01 n11 x插值, 最后将结果 沿y方向插值。
def mix(u, v, a):
return u*(1-a)+v*a
但是这样插值结果不光滑, 可以调整插值的系数, 用函数 3*a*a - 2*a 重新映射,得到新的插值系数,用这个值插值就比较光滑, 且在端点两侧更加紧凑。
float smooth(float u) {
return 3.0*u*u-2.0*u;
}
接着是如何为每个定点选择gradient呢? 这里有采用一个周期循环的方式。 例如对256个数字0-255 生成一个排列permutation, 这样平面上每个点
通过permutation[(X+permutation[Y%256])%256] 可以得到一个0-255 之间的某个值 接着将这个值 %8 就得到 其对应的gradient编号。
这样做,虽然会导致平面上的一个循环的问题,但是噪声这种东西,本来就是局部的,在大的范围上看,噪声都是趋于0的, 因此问题不大。
而如何生成一个 0-255的排列呢? 有一种叫做 permutation polynomial 排列多项式的东西, 例如 (2*x*x+x)%256 这样一个函数,
可以生成一个0-255的排列 只需要把 0-255的值代入就可以了。
实施上可以生成任意 2^k 的排列。
这样整个流程就是:
一个 gradient数组
一个permutation
计算一个点周围4个整数点的 gradient
计算gradient和 4个整数点 到目标点的 向量的内积
光滑插值积分的值。
在Three.js 中我们声明一个平面 -1 1 -1 1. 使用平面的纹理坐标作为计算噪音的坐标点。
var plane = new THREE.Mesh(new THREE.PlaneGeometry(2, 2, 1, 1),
但是有个问题,纹理坐标是从 0-1的, 因此平面上的点的临近的4个整数点都是一样的, 因此我们需要一个系数用来乘以纹理坐标,将平面的坐标空间扩大, 因此shader里面需要一个uniform float coff 作为系数。
同时shader里面也需要一个uniform vec2 gradien[8]; 的2维度向量数组。
但是如何访问这个数组呢? shader里面数组的访问只能使用常数索引的方式, 当然我们可以把gradient放到1维纹理里面, 这样就可以通过texture得到对应的向量,这种方式似乎更好。
因此需要一个 8*1的数据纹理, 注意纹理需要设置 needsUpdate 来将数据存入到显卡中。
var texture = new THREE.DataTexture(data, 8, 1, THREE.RGBFormat);
texture.needsUpdate = true;
其中data是一个 8*3 的 UintArray
var v = [
[1, 0], [1, 1],
[0, 1], [-1, 1],
[-1, 0], [-1, -1],
[0, -1], [1, -1],
];
var data = new Uint8Array(3*8);
for(var i in v) {
data[i*3] = ~~((v[i][0]+1)/2*255);//R
data[i*3+1] = ~~((v[i][1]+1)/2*255);//G
data[i*3+2] = 0;//B
}
因此平面的material是:
var plane = new THREE.Mesh(new THREE.PlaneGeometry(2, 2, 1, 1),
new THREE.ShaderMaterial({
uniforms:{
coff:{type:'f', value:512},
gradient:{type:'t', value:0, texture:texture},
},
attributes:{
},
vertexShader: document.getElementById("vert").textContent,
fragmentShader: document.getElementById("frag").textContent,
}));
而shader主要是fragment, vertex主要将纹理坐标传入到fragment中。
uniform float coff;
//uniform vec2 gradient[8];
uniform sampler2D gradient;
varying vec2 vUv;
//计算随机的排列序列号 接着获取一个gradient
float permu(float x){
x = mod(x, 256.0);
return mod((2.0*x*x+x), 256.0);
}
float smooth(float u) {
return 3.0*u*u-2.0*u;
}
float myDot(vec2 p, float dx, float dy){
return p.x*dx+p.y*dy;
}
vec2 getGradient(float v)
{
v = mod(v, 8.0);
v = v/8.0;
vec4 c = texture2D(gradient, vec2(v, 0));
return vec2(c.r*2.0-1.0, c.g*2.0-1.0);
}
float noise(float x, float y) {
float X = floor(x);
float Y = floor(y);
//偏移坐标
float difx = x-X;
float dify = y-Y;
//约束到0-255
X = mod(X, 255.0);
Y = mod(Y, 255.0);
//0 - 255
float g00 = permu(x+permu(y));
float g01 = permu(x+permu(y+1.0));
float g11 = permu(x+1.0+permu(y+1.0));
float g10 = permu(x+1.0+permu(y));
//0-8 gradient * 偏移坐标
float n00 = myDot(getGradient(g00), difx, dify);
float n01 = myDot(getGradient(g01), difx, dify-1.0);
float n11 = myDot(getGradient(g11), difx-1.0, dify-1.0);
float n10 = myDot(getGradient(g10), difx-1.0, dify);
//插值
float u = smooth(difx);
float v = smooth(dify);
float nx0 = mix(n00, n10, u);
float nx1 = mix(n01, n11, u);
float nxy = mix(nx0, nx1, v);
return nxy;
}
void main( void ) {
float n = noise(vUv.x*coff, vUv.y*coff);
gl_FragColor = vec4(n, n, n, 1);
}