用一段 Shader 打开噪声世界:基于 Three.js 的二维 Value Noise 动效实践
用 Three.js 和 GLSL 实现一个动态噪声的视觉特效,我们先来看下效果:
实现思路
- 初始化阶段: 通过init函数设置场景、相机和着色器材质,并添加事件监听器。
- 动画循环: animate函数创建动画循环,每帧调用render函数更新画面。
- 着色器执行:
- main函数是着色器的入口点,负责坐标变换和选择使用简单或复杂模式。
- 根据u_complex变量选择使用pattern或pattern2函数生成图案。
- pattern2函数比pattern函数多了几层fbm1调用,产生更复杂的视觉效果。
- 最后计算并输出最终颜色。
- 辅助函数:
- random2函数:生成随机向量。
- noise函数:使用梯度噪声算法创建平滑的噪声。
- fbm1函数:通过多次叠加不同频率和振幅的噪声,创建分形布朗运动效果。
- 交互: 鼠标移动影响动画速度,鼠标点击切换简单/复杂模式。
实现架构图
大家可以配合下面的图来理解思路
代码实现
创建基础的渲染环境
我们采用经典的 Shader 渲染方式:在一个全屏的平面上绘制 Fragment Shader 生成的内容。首先引入 Three.js,并在页面中预留容器:
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/88/three.min.js"></script>
<div id="container"></div>
然后使用 JavaScript 初始化 Three.js 的核心部分:相机、场景、渲染器和一个满屏平面。
let container;
let camera, scene, renderer;
let uniforms;
function init() {
container = document.getElementById('container');
camera = new THREE.Camera();
camera.position.z = 1;
scene = new THREE.Scene();
const geometry = new THREE.PlaneBufferGeometry(2, 2);
uniforms = {
u_time: { type: 'f', value: 0.0 },
u_resolution: { type: 'v2', value: new THREE.Vector2() },
u_mouse: { type: 'v2', value: new THREE.Vector2() },
u_complex: { type: 'b', value: false }
};
const material = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: document.getElementById('vertexShader').textContent,
fragmentShader: document.getElementById('fragmentShader').textContent
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
renderer = new THREE.WebGLRenderer();
container.appendChild(renderer.domElement);
onWindowResize();
window.addEventListener('resize', onWindowResize, false);
document.addEventListener('mouseup', onMouseUp, false);
document.onmousemove = function (e) {
uniforms.u_mouse.value.x = e.pageX;
uniforms.u_mouse.value.y = e.pageY;
};
}
function onWindowResize() {
renderer.setSize(window.innerWidth, window.innerHeight);
uniforms.u_resolution.value.x = renderer.domElement.width;
uniforms.u_resolution.value.y = renderer.domElement.height;
}
function onMouseUp() {
uniforms.u_complex.value = !uniforms.u_complex.value;
}
注意,这里我们注册了一个 mouseup
事件,用于在点击时切换一种更复杂的图案模式。
构建顶点与片元着色器
我们使用最简形式的顶点着色器,将二维平面上的顶点传入片元着色器:
<script id="vertexShader" type="x-shader/x-vertex">
void main() {
gl_Position = vec4(position, 1.0);
}
</script>
重点的工作全部在 Fragment Shader 中进行,主要由以下几个部分组成:
引入噪声与 FBM 函数
我们使用 Inigo Quilez 提出的经典 Value Noise 实现。为了构造更有层次的变化,引入了多重噪声合成(fbm):
float noise(vec2 st, float seed) {
vec2 i = floor(st);
vec2 f = fract(st);
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(mix(dot(random2(i, seed), f - vec2(0.0, 0.0)),
dot(random2(i + vec2(1.0, 0.0), seed), f - vec2(1.0, 0.0)), u.x),
mix(dot(random2(i + vec2(0.0, 1.0), seed), f - vec2(0.0, 1.0)),
dot(random2(i + vec2(1.0, 1.0), seed), f - vec2(1.0, 1.0)), u.x), u.y);
}
再通过一个 fbm1
函数合成多重噪声图案:
float fbm1(in vec2 _st, float seed) {
float v = 0.0;
float a = 0.5;
vec2 shift = vec2(100.0);
mat2 rot = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.5));
for (int i = 0; i < 6; ++i) {
v += a * noise(_st, seed);
_st = rot * _st * 2.0 + shift;
a *= 0.4;
}
return v;
}
构建可切换的图案函数
我们提供了 pattern
和 pattern2
两种函数,分别控制不同复杂度的噪声叠加方式。通过鼠标点击 u_complex
变量切换不同的绘制模式。
两种模式的核心差异是叠加层数和每层噪声对空间的扰动程度。
简化模式:
float pattern(vec2 uv, float seed, float time, inout vec2 q, inout vec2 r) {
q = vec2(fbm1(uv * .1, seed), fbm1(uv + vec2(5.2, 1.3), seed));
r = vec2(fbm1(uv * .1 + 4.0 * q + vec2(1.7 - time / 2., 9.2), seed),
fbm1(uv + 4.0 * q + vec2(8.3 - time / 2., 2.8), seed));
return fbm1(uv * .05 + 4.0 * r, seed);
}
复杂模式:
float pattern2(vec2 uv, float seed, float time, inout vec2 q, inout vec2 r) {
q = vec2(fbm1(uv, seed), fbm1(uv + vec2(5.2, 1.3), seed));
r = vec2(fbm1(uv + 4.0 * q + vec2(1.7 - time / 2., 9.2), seed),
fbm1(uv + 4.0 * q + vec2(8.3 - time / 2., 2.8), seed));
vec2 s = vec2(fbm1(uv + 5.0 * r + vec2(21.7 - time / 2., 90.2), seed),
fbm1(uv + 5.0 * r + vec2(80.3 - time / 2., 20.8), seed));
vec2 t = vec2(fbm1(uv + 4.0 * s + vec2(121.7, 190.2), seed),
fbm1(uv + 4.0 * s + vec2(180.3, 120.8), seed));
vec2 u = vec2(fbm1(uv + 3.0 * t + vec2(221.7, 290.2), seed),
fbm1(uv + 3.0 * t + vec2(280.3, 220.8), seed));
vec2 v = vec2(fbm1(uv + 2.0 * u + vec2(221.7, 290.2), seed),
fbm1(uv + 2.0 * u + vec2(280.3, 220.8), seed));
return fbm1(uv + 4.0 * v, seed);
}
最终片元着色器实现
在 main
函数中,我们将片元坐标映射为标准化空间(居中 [-1, 1]),加入旋转和缩放变化后,调用 pattern 函数获取当前颜色值:
void main() {
vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / u_resolution.y;
float time = u_time / 10.0;
mat2 rot = mat2(cos(time / 10.), sin(time / 10.), -sin(time / 10.), cos(time / 10.));
uv = rot * uv;
uv *= 0.9 * sin(u_time / 20.0) + 3.0;
uv.x -= time / 5.0;
vec2 q = vec2(0.0);
vec2 r = vec2(0.0);
float _pattern = u_complex ? pattern2(uv, seed, time, q, r) : pattern(uv, seed, time, q, r);
vec3 colour = vec3(_pattern) * 2.0;
colour.r -= dot(q, r) * 15.0;
colour = mix(colour, vec3(pattern(r, seed2, time, q, r), dot(q, r) * 15.0, -0.1), 0.5);
colour -= q.y * 1.5;
colour = mix(colour, vec3(0.2), clamp(q.x, -1.0, 0.0) * 3.0);
gl_FragColor = vec4(-colour + abs(colour) * 0.5, 1.0);
}
通过这些计算和色彩混合,你将获得一种具有梦幻感的动态图案,点击切换模式时呈现不同的图层深度。
启动渲染循环
最后,通过 requestAnimationFrame 实现帧更新,推动 u_time 和交互变化。
function animate() {
requestAnimationFrame(animate);
uniforms.u_time.value += 0.05 * (1 + uniforms.u_mouse.value.x / 200.0);
renderer.render(scene, camera);
}
init();
animate();
代码
https://codepen.io/shubniggurath/pen/NXGbBo (opens in a new tab)