Реализация WebGL-анимаций и 3D-эффектов на сайте
WebGL — это не «добавить красивость». Это программируемый графический конвейер прямо в браузере, с доступом к GPU. Когда нужно рендерить тысячи частиц, деформировать геометрию по аудиосигналу или строить интерактивные 3D-сцены без плагинов — это единственный инструмент, который справится без компромиссов.
Используется WebGL 2.0 (поддержка 95%+ браузеров на 2025 год), как правило через Three.js или напрямую через WebGL API для нестандартных задач.
Стек и подходы
Three.js — стандарт де-факто для большинства веб-проектов. Абстрагирует шейдеры и буферы, предоставляет сцену, камеру, освещение. Версия r169+ поддерживает WebGPU как альтернативный рендерер.
Raw WebGL применяется когда нужна полная управляемость: кастомные геометрические примитивы, нестандартные режимы бленда, минимальный bundle size без лишнего кода.
GLSL-шейдеры пишутся вручную под каждый эффект — universальных решений здесь не существует.
// Вертексный шейдер — деформация плоскости по шуму
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 при hover
Текстура изображения загружается как 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>
)
}
Производительность
Framerate target — 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-модель продукта с конфигуратором материалов — к верхней.







