В классическом графическом пайплайне WebGL результат работы фрагментного шейдера направляется непосредственно в экранный кадровый буфер (Default Framebuffer), и пиксели отображаются на дисплее пользователя. Однако для достижения кинематографического качества картинки требуется этап вторичной обработки — пост-процессинг (Post-processing).
Оригинальная архитектура Three.js опиралась на модуль EffectComposer, где каждый эффект (размытие, цветокоррекция, свечение) представлял собой отдельный полноэкранный проход (Pass). При цепочке из пяти эффектов сцена выполняла 5 последовательных переключений кадровых буферов и 5 отрисовок полноэкранных четырехугольников (Full-screen Quads). Это вызывало колоссальную нагрузку на пропускную способность шины видеопамяти (Fillrate & Memory Bandwidth), делая пост-процессинг непригодным для мобильных устройств.
Современный стек @react-three/postprocessing базируется на высокопроизводительном движке postprocessing (разработка vanruesc). Его фундаментальное архитектурное преимущество — Shader Fusion (слияние шейдеров).
┌─────────────────────────────────────────────────────────────────────────────┐
│ КЛАССИЧЕСКИЙ THREE.JS (Multi-Pass Overhead) │
│ [Scene] ──► FBO 1 ──► [Bloom Pass] ──► FBO 2 ──► [Vignette] ──► Screen │
│ (Draw 1) (Draw 2) (Draw 3) │
│ 3x Fullscreen Quad Writes = Катастрофический Fillrate │
├─────────────────────────────────────────────────────────────────────────────┤
│ СОВРЕМЕННЫЙ R3F PIPELINE (Shader Fusion) │
│ [Scene] ──► Offscreen Render Target (Color + Depth + Normal) │
│ │ │
│ ▼ (Единая скомпилированная шейдерная программа) │
│ [Combined Shader: Bloom + N8AO + DoF + Vignette + ToneMapping] │
│ │ │
│ ▼ (Один Draw Call) │
│ Screen Buffer (Zero redundant FBO switches) │
└─────────────────────────────────────────────────────────────────────────────┘Использование кадровых буферов вне экрана (FBO / Render Target) дает доступ к буферу глубины (Depth Buffer) и буферу нормалей (Normal Buffer). Это делает возможным реализацию пространственных экранных алгоритмов: затенения полостей (SSAO/N8AO) и физической расфокусировки линзы (Depth of Field).
Ментальная модель @react-three/postprocessing представляет пост-процессинг как декларативный контейнер эффектов, перехватывающий управление главным рендер-лупом Three.js:
┌─────────────────────────────────────────────────────────────┐
│ <Canvas> │
│ <SceneContent /> │
│ │
│ <EffectComposer multisampling={0} disableNormalPass> │
│ <Selection> │
│ <Bloom luminanceThreshold={0.8} mipmapBlur /> │
│ </Selection> │
│ <N8AO aoRadius={0.5} intensity={1.5} /> │
│ <DepthOfField focusDistance={0.02} bokehScale={3} /> │
│ <Vignette eskil={false} offset={0.1} darkness={1.1} /> │
│ </EffectComposer> │
│ </Canvas> │
└─────────────────────────────────────────────────────────────┘<EffectComposer>: Отключает автоматический вызов gl.render(scene, camera) в R3F. Создает первичный WebGLRenderTarget с форматом цвета RGBA и буфером глубины.Selection / Select): Изолирует высокоинтенсивные световые ореолы только на определенных объектах сцены с помощью маскирования трафаретного буфера (Stencil Buffer) или отдельных цветовых слоев.DepthOfField): Шейдер считывает нелинейную глубину из depthTexture, преобразует ее в линейное метрическое расстояние до камеры и размывает фрагменты, чье расстояние отклоняется от плоскости фокусировки.N8AO): Алгоритм имитирует диффузное самозатенение поверхностей в щелях и углах за счет анализа окрестностей пикселя в экранном пространстве.Отрисовка всех непрозрачных и прозрачных объектов во внеэкранный буфер цвета и текстуру глубины (Depth Texture).
Формирование буфера нормалей (Normal Pass) и маски свечения (Luminance Pass) для алгоритмов AO и Bloom.
Многократное понижение разрешения (Downsampling) и размытие ярких участков для эффекта физического рассеяния линзы.
Выполнение объединенного шейдера эффектов за один проход с наложением виньетирования, DoF и тональной компрессии.
В чем заключается главное архитектурное отличие библиотеки postprocessing (vanruesc) от оригинального Three.js EffectComposer при обработке цепочки из 6 эффектов?
В конфигураторах премиальных брендов (Porsche, Audi) требуется подчеркнуть хромированные элементы и оптику фар. Классический Bloom привел бы к нежелательному размытию белых элементов кузова. Используется Selective Bloom: светятся исключительно стеклянные линзы фар и неоновые полосы интерфейса с помощью обертки <Select>. Алгоритм N8AO создает реалистичные мягкие тени под днищем автомобиля и в воздухозаборниках без тяжелых shadow maps.
В интернет-магазинах электроники при клике на определенную деталь устройства (микрочип, объектив смартфона) фокусное расстояние камеры плавно подстраивается под координаты выбранного объекта. Raycaster в реальном времени считывает глубину поверхности под курсором, а DepthOfField интерполирует параметр focusDistance, создавая размытие переднего и заднего планов (Cinematic Bokeh).
При рендеринге сложных поверхностей (компьютерная томография, рельефная местность) плоское освещение не позволяет оценить перепады высот. Экранное фоновое затенение (SSAO) вычисляет локальную окклюзию окружающей среды, визуально подчеркивая микрорельеф костей, тканей и впадин ландшафта в реальном времени на мобильных планшетах врачей.
Для симуляции винтажных ЭЛТ-мониторов или эффекта аналоговой видеокамеры создается кастомный шейдерный проход. Он комбинирует хроматическую аберрацию (раздельное смещение каналов R и B от оптической оси), радиальное затемнение (Vignette), динамический шум зернистости пленки (Noise) и искажение развертки (Scanlines).
Рассмотрим реализацию оптимизированного конвейера пост-процессинга с селективным свечением, современным ambient occlusion и автофокусировкой глубины резкости.
import React, { useRef, useState } from 'react';
import { Canvas, useFrame, useThree } from '@react-three/fiber';
import {
EffectComposer,
Bloom,
N8AO,
DepthOfField,
Vignette,
Selection,
Select,
} from '@react-three/postprocessing';
import * as THREE from 'three';
// 1. Компонент с динамическим автофокусом Depth of Field
function AutoFocusDepthOfField() {
const dofRef = useRef<any>(null);
const { camera, pointer, raycaster, scene } = useThree();
useFrame(() => {
if (!dofRef.current) return;
// Трассировка луча от курсора для нахождения дистанции до объекта
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length > 0) {
// Дистанция пересечения в мировых единицах
const hitDistance = intersects[0].distance;
// Преобразование в нормализованное фокусное расстояние [0..1]
const targetFocus = hitDistance / camera.far;
// Плавная интерполяция фокуса (LERP) для устранения рывков
dofRef.current.target = THREE.MathUtils.lerp(
dofRef.current.target || targetFocus,
targetFocus,
0.1
);
}
});
return (
<DepthOfField
ref={dofRef}
focusDistance={0.02}
focalLength={0.05}
bokehScale={4.0}
height={480} // Ограничение разрешения прохода размытия для GPU
/>
);
}
// 2. Основная сцена со светящимися объектами
export function PostProcessingScene() {
return (
<div className="w-full h-[600px] bg-slate-950 rounded-2xl overflow-hidden relative">
<Canvas
camera={{ position: [0, 2, 6], fov: 45, far: 50 }}
dpr={[1, 1.5]}
// Отключаем дефолтный антиалиасинг Canvas при использовании пост-процессинга
gl={{ antialias: false, powerPreference: 'high-performance' }}
>
<ambientLight intensity={0.4} />
<directionalLight position={[5, 10, 5]} intensity={1.2} />
{/* Обертка Selection для изоляции объектов селективного свечения */}
<Selection>
<EffectComposer
multisampling={0} // 0 для мобильных GPU; сглаживание через SMAA
disableNormalPass={false}
>
{/* Selective Bloom: ореол только вокруг компонентов внутри <Select> */}
<Bloom
luminanceThreshold={0.6}
luminanceSmoothing={0.4}
intensity={2.0}
mipmapBlur // Высококачественное сглаженное размытие
/>
{/* Высокопроизводительный SSAO нового поколения (N8AO) */}
<N8AO
aoRadius={1.2}
intensity={2.5}
distanceFalloff={0.8}
quality="medium"
/>
{/* Динамическая глубина резкости */}
<AutoFocusDepthOfField />
{/* Финальное виньетирование краев кадра */}
<Vignette offset={0.2} darkness={0.9} />
</EffectComposer>
{/* Обычный объект без свечения: принимает затенение N8AO */}
<mesh position={[-1.5, 0, 0]}>
<boxGeometry args={[1.5, 1.5, 1.5]} />
<meshStandardMaterial color="#64748b" roughness={0.3} />
</mesh>
{/* Селективно светящийся объект */}
<Select enabled>
<mesh position={[1.5, 0, 0]}>
<sphereGeometry args={[1, 32, 32]} />
{/* Эмиссионный цвет превышает threshold (0.6), создавая свечение */}
<meshStandardMaterial
color="#06b6d4"
emissive="#06b6d4"
emissiveIntensity={3.0}
toneMapped={false} // toneMapped=false гарантирует пробитие HDR порога
/>
</mesh>
</Select>
{/* Пол для визуализации теней и окклюзии N8AO */}
<mesh position={[0, -1, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[20, 20]} />
<meshStandardMaterial color="#1e293b" roughness={0.8} />
</mesh>
</Selection>
</Canvas>
</div>
);
}Если встроенных эффектов недостаточно, можно создать собственный шейдерный проход. Для этого наследуется базовый класс Effect из библиотеки postprocessing и оборачивается для React через wrapEffect.
import { Effect, EffectAttribute } from 'postprocessing';
import { Uniform } from 'three';
import { wrapEffect } from '@react-three/postprocessing';
const fragmentShader = /* glsl */ `
uniform float uDistortion;
uniform float uTime;
void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {
// Вычисление радиального смещения цветовых каналов
vec2 dir = uv - vec2(0.5);
float dist = length(dir);
// Высокочастотная модуляция смещения от времени
float offset = sin(uTime * 8.0 + uv.y * 20.0) * uDistortion * dist;
// Выборка цветовых компонент с расхождением координат (Chromatic Aberration)
float r = texture2D(inputBuffer, uv + dir * offset).r;
float g = texture2D(inputBuffer, uv).g;
float b = texture2D(inputBuffer, uv - dir * offset).b;
outputColor = vec4(r, g, b, inputColor.a);
}
`;
// 1. Класс эффекта, интегрируемый в пайплайн Shader Fusion
export class GlitchEffectImpl extends Effect {
constructor({ distortion = 0.02 }: { distortion?: number } = {}) {
super('GlitchEffect', fragmentShader, {
attributes: EffectAttribute.CONVOLUTION,
uniforms: new Map<string, Uniform>([
['uDistortion', new Uniform(distortion)],
['uTime', new Uniform(0)],
]),
});
}
// Обновление uniforms перед каждым графическим кадром
update(renderer: any, inputBuffer: any, deltaTime: number) {
const timeUniform = this.uniforms.get('uTime');
if (timeUniform) {
timeUniform.value += deltaTime;
}
}
}
// 2. Декларативная обертка для R3F
export const CustomGlitch = wrapEffect(GlitchEffectImpl);Рендеринг геометрии сцены в текстуру цвета с включенной записью глубины gl.DEPTH_COMPONENT24.
Исходный рендеринг геометрии без антиалиасинга MSAA.
Многократное деление разрешения (1/2, 1/4, 1/8) для быстрого и широкого рассеяния ярких участков Bloom.
Снижает число операций текстурной выборки в 16 раз.
Восстановление позиций в видовом пространстве (View Space) по буферу глубины для расчета затенения и кружков нерезкости (CoC).
Использует инвертированную проекционную матрицу камеры.
Исполнение финального слитного пиксельного шейдера с записью итоговых RGB-значений в системный холст браузера.
Окончательный вывод за 1 проход отрисовки.
Команда включила EffectComposer и заметила, что на экранах iPhone частота кадров упала со 120 FPS до дерганых 20 FPS. В конфигурации холста было указано: <Canvas dpr={[1, 3]} gl={{ antialias: true }}>, а в композере: multisampling={8}. В чем ключевая архитектурная ошибка?
Установка <Canvas gl={{ antialias: true }}> при подключенном EffectComposer приводит к напрасной трате аппаратных ресурсов. Стандартный MSAA сглаживает только экранный кадровый буфер, в то время как сцена рендерится во внеэкранный RenderTarget. В результате anti-aliasing не работает, но отнимает память.
Решение: Отключайте gl={{ antialias: false }} на Canvas. Для сглаживания сцены с пост-процессингом подключайте <SMAA /> внутри <EffectComposer>.
Если использовать общий <Bloom luminanceThreshold={0.5} />, любые яркие белые поверхности (текст, карточки меню, светлые текстуры) начинают светиться, превращая интерфейс в нечитаемую кашу.
Решение: Используйте селективное свечение через обертку <Selection> и <Select enabled>, либо настраивайте toneMapped={false} и emissiveIntensity > 2.0 только на целевых лампах и светодиодах при высоком пороге luminanceThreshold={1.0}.
При рендеринге в FBO и последующем выводе на экран цвета сцены могут выглядеть выцветшими или, наоборот, неестественно контрастными. Это происходит из-за повторного применения гамма-коррекции или отсутствия тональной компрессии.
Решение: Используйте <ToneMapping mode={ToneMappingMode.ACES_FILMIC} /> внутри EffectComposer и следите, чтобы на холсте был установлен корректный gl={{ outputColorSpace: THREE.SRGBColorSpace }}.
Рендеринг эффектов DepthOfField или SSAO в нативном разрешении экрана (особенно на 2K/4K экранах) вызывает фатальную просадку частоты кадров из-за миллиардов обращений к текстурам выборки в секунду.
Решение: Ограничивайте высоту и ширину проходов эффектов через пропсы: <DepthOfField height={480} /> и активируйте проп mipmapBlur для эффекта <Bloom>.
postprocessing, которая использует технологию Shader Fusion для объединения нескольких эффектов в один проход рендеринга.depthTexture), позволяя вычислять пространственные эффекты SSAO/N8AO и Depth of Field.<Selection> и <Select>, предотвращая засветку нейтральных UI-компонентов.NormalPass.gl.antialias, ограничивать dpr={[1, 1.5]} и использовать SMAA внутри <EffectComposer>.