Реалізація WebGL-анімацій та 3D-ефектів на сайті
WebGL — це не просто «додати красивість». Це програмований графічний конвейєр прямо в браузері з доступом до GPU. Коли потрібно рендерити тисячи частинок, деформувати геометрію за аудіосигналом або будувати інтерактивні 3D-сцени без плагінів — це єдиний інструмент, який впорається без компромісів.
Використовується WebGL 2.0 (поддержка 95%+ браузерів на 2025 рік), як правило через Three.js або напрямку через WebGL API для нестандартних завдань.
Стек та підходи
Three.js — стандарт де-факто для більшості веб-проектів. Абстрагує шейдери та буфери, надає сцену, камеру, освітлення. Версія r169+ підтримує WebGPU як альтернативний рендерер.
Raw WebGL застосовується коли потрібна повна контрольованість: кастомні геометричні примітиви, нестандартні режими змішування, мінімальний розмір bundle без зайвого коду.
GLSL-шейдери пишуться вручну під кожен ефект — універсальних рішень тут не існує.
// Вертексний шейдер — деформація площини за шумом
uniform float uTime;
uniform float uAmplitude;
varying vec2 vUv;
// Simplex noise (вбудовується як функція)
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
void main() {
vUv = uv;
vec3 pos = position;
float noise = snoise(vec2(pos.x * 0.5 + uTime * 0.3, pos.y * 0.5));
pos.z += noise * uAmplitude;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
// Three.js — ініціалізація сцени з постпроцесингом
import * as THREE from 'three'
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'
const renderer = new THREE.WebGLRenderer({
canvas: document.querySelector('#webgl'),
antialias: true,
alpha: true,
})
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.toneMapping = THREE.ACESFilmicToneMapping
const composer = new EffectComposer(renderer)
composer.addPass(new RenderPass(scene, camera))
composer.addPass(new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
0.8, // strength
0.4, // radius
0.85 // threshold
))
Типові ефекти та їх реалізація
Шейдерний фон з шумом
Один з найбільш затребуваних ефектів: анімований градієнтний фон, який реагує на мишу. Реалізується через PlaneGeometry, яка покриває весь viewport, з фрагментним шейдером на базі FBM (fractional Brownian motion).
// Фрагментний шейдер — колірний шум
uniform float uTime;
uniform vec2 uMouse;
uniform vec2 uResolution;
varying vec2 vUv;
void main() {
vec2 uv = vUv;
vec2 mouse = uMouse / uResolution;
// Зміщення UV за позицією миші
uv += (mouse - 0.5) * 0.05;
float noise = fbm(uv * 3.0 + uTime * 0.15);
vec3 colorA = vec3(0.1, 0.0, 0.4);
vec3 colorB = vec3(0.0, 0.3, 0.8);
vec3 colorC = vec3(0.8, 0.1, 0.3);
vec3 color = mix(colorA, colorB, noise);
color = mix(color, colorC, smoothstep(0.4, 0.7, noise));
gl_FragColor = vec4(color, 1.0);
}
Система частинок
Для 100k+ частинок використовується BufferGeometry з атрибутами в Float32Array. Анімація йде цілком у вертексному шейдері — CPU не задіяний при runtime.
const COUNT = 150000
const positions = new Float32Array(COUNT * 3)
const randoms = new Float32Array(COUNT)
for (let i = 0; i < COUNT; i++) {
positions[i * 3 + 0] = (Math.random() - 0.5) * 10
positions[i * 3 + 1] = (Math.random() - 0.5) * 10
positions[i * 3 + 2] = (Math.random() - 0.5) * 10
randoms[i] = Math.random()
}
const geometry = new THREE.BufferGeometry()
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
geometry.setAttribute('aRandom', new THREE.BufferAttribute(randoms, 1))
const material = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uSize: { value: 3.0 * renderer.getPixelRatio() },
},
vertexShader: particleVertexShader,
fragmentShader: particleFragmentShader,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
})
Image distortion при наведенні
Текстура зображення завантажується як THREE.Texture, деформується через displacement map за позицією курсора. Ефект «рідкого» наведення.
// Uniforms для передачі в шейдер
const uniforms = {
uTexture: { value: texture },
uDisplacement: { value: displacementTexture },
uMouse: { value: new THREE.Vector2(0, 0) },
uVelo: { value: 0 },
}
// Відстеження швидкості руху миші
let lastMouse = new THREE.Vector2()
let currentVelo = 0
window.addEventListener('mousemove', (e) => {
const current = new THREE.Vector2(
e.clientX / window.innerWidth,
1.0 - e.clientY / window.innerHeight
)
const delta = current.distanceTo(lastMouse)
currentVelo = Math.min(delta * 10, 1.0)
lastMouse.copy(current)
uniforms.uMouse.value.copy(current)
})
Інтеграція з React
Через @react-three/fiber (R3F) Three.js вбудовується у React-компонент декларативно. @react-three/drei дає готові хелпери: useGLTF, MeshTransmissionMaterial, Float, Environment.
import { Canvas, useFrame } from '@react-three/fiber'
import { useRef } from 'react'
import * as THREE from 'three'
function AnimatedMesh() {
const meshRef = useRef<THREE.Mesh>(null)
useFrame(({ clock, pointer }) => {
if (!meshRef.current) return
meshRef.current.rotation.y = clock.getElapsedTime() * 0.3
meshRef.current.position.x = THREE.MathUtils.lerp(
meshRef.current.position.x,
pointer.x * 2,
0.05
)
})
return (
<mesh ref={meshRef}>
<icosahedronGeometry args={[1.5, 4]} />
<meshStandardMaterial
color="#5500ff"
wireframe={false}
roughness={0.1}
metalness={0.8}
/>
</mesh>
)
}
export function Scene() {
return (
<Canvas
camera={{ position: [0, 0, 5], fov: 45 }}
gl={{ antialias: true, alpha: true }}
dpr={[1, 2]}
>
<ambientLight intensity={0.5} />
<pointLight position={[10, 10, 10]} intensity={1} />
<AnimatedMesh />
</Canvas>
)
}
Продуктивність
Target framerate — 60fps на десктопі, 30fps на мобілях з автоматичним зниженням якості. Визначається через navigator.hardwareConcurrency та бенчмарк при першому рендері.
Ключові правила:
- Один drawcall замість тисячі:
InstancedMeshдля повторюваної геометрії -
renderer.setPixelRatio(Math.min(devicePixelRatio, 2))— не рендерити на 3x на Retina без потреби - Dispose при unmount:
geometry.dispose(),material.dispose(),texture.dispose() -
requestAnimationFrameчерез Three.js renderer, не свій loop - Постпроцесинг тільки при
prefersReducedMotion === false
// Перевірка перед ініціалізацією важких ефектів
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
const isMobile = /Mobi|Android/i.test(navigator.userAgent)
const config = {
bloomEnabled: !prefersReduced && !isMobile,
particleCount: isMobile ? 10000 : 150000,
pixelRatio: isMobile ? 1 : Math.min(devicePixelRatio, 2),
}
Завантаження ассетів
3D-моделі — формат .glb (GLB = бінарний GLTF). Стиснення через Draco (geom) + KTX2 (текстури). Завантаження через GLTFLoader + DRACOLoader.
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js'
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('/draco/') // wasm в public/
const ktx2Loader = new KTX2Loader()
ktx2Loader.setTranscoderPath('/basis/')
ktx2Loader.detectSupport(renderer)
const loader = new GLTFLoader()
loader.setDRACOLoader(dracoLoader)
loader.setKTX2Loader(ktx2Loader)
loader.load('/models/scene.glb', (gltf) => {
scene.add(gltf.scene)
}, (progress) => {
const pct = (progress.loaded / progress.total * 100).toFixed(0)
onProgress(pct)
})
Строки та етапи
Прототип з основним ефектом — 3–5 днів. Повна інтеграція в сайт з адаптивністю, fallback для слабких пристроїв, оптимізація bundle — 10–20 днів залежно від складності сцени. Анімований hero з шейдерним фоном та реакцією на мишу — ближче до нижної межи. Інтерактивна 3D-модель продукту з конфігуратором матеріалів — до верхньої.







