Урок · Середній рівень · ~50 хв
Three.js · WebGLRenderTarget · GLSL · Постобробка

RenderTarget та постобробка у Three.js

Постобробка рендерить сцену в текстуру, а потім застосовує повноекранні проходи шейдерів, щоб додати bloom, глибину різкості, віньєтку чи будь-який інший ефект. Цей урок будує конвеєр з нуля за допомогою WebGLRenderTarget і повноекранного квада — жодних зовнішніх бібліотек постобробки не потрібно.

1Рендеримо сцену в текстуру

import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.160/build/three.module.js'; function makeRT(w, h) { return new THREE.WebGLRenderTarget(w, h, { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBAFormat, type: THREE.HalfFloatType, // HDR-діапазон для bloom }); } let rt = makeRT(innerWidth, innerHeight); // У циклі рендерингу: renderer.setRenderTarget(rt); // рендеримо В текстуру renderer.render(scene, camera); renderer.setRenderTarget(null); // повертаємось до екранного фреймбуфера // rt.texture тепер містить сцену як семпльоване зображення
Використовуйте рендертаргети HalfFloatType для HDR-bloom — значення понад 1.0 кодують надто яскраві ділянки. Звичайний UnsignedByteType обрізає все до [0,1] і втрачає сигнал bloom.

2Налаштування повноекранного квада

// Площина, що покриває весь куб NDC const quadGeo = new THREE.PlaneGeometry(2, 2); const quadCam = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); const quadScene = new THREE.Scene(); // Матеріал постобробки читає текстуру сцени const postMat = new THREE.ShaderMaterial({ uniforms: { u_tex: { value: null }, u_res: { value: new THREE.Vector2(innerWidth, innerHeight) }, u_time: { value: 0 }, }, vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4(position, 1.0); } `, fragmentShader: ` uniform sampler2D u_tex; varying vec2 vUv; void main() { gl_FragColor = texture2D(u_tex, vUv); } `, }); const quad = new THREE.Mesh(quadGeo, postMat); quadScene.add(quad);

3Bloom: поріг + гаусове розмиття

// Прохід 1: виділяємо яскраві ділянки понад порогом const brightFrag = /* glsl */ ` uniform sampler2D u_tex; uniform float u_threshold; varying vec2 vUv; void main() { vec3 col = texture2D(u_tex, vUv).rgb; float lum = dot(col, vec3(0.2126, 0.7152, 0.0722)); gl_FragColor = vec4(lum > u_threshold ? col : vec3(0.0), 1.0); } `; // Прохід 2: гаусове розмиття на 9 семплів (горизонтальне) const blurHFrag = /* glsl */ ` uniform sampler2D u_tex; uniform vec2 u_res; varying vec2 vUv; const float W[5] = float[](0.2270, 0.1945, 0.1216, 0.0540, 0.0162); void main() { vec2 px = vec2(1.0 / u_res.x, 0.0); vec3 col = texture2D(u_tex, vUv).rgb * W[0]; for (int i = 1; i < 5; i++) { col += texture2D(u_tex, vUv + float(i) * px).rgb * W[i]; col += texture2D(u_tex, vUv - float(i) * px).rgb * W[i]; } gl_FragColor = vec4(col, 1.0); } `; // Вертикальне розмиття ідентичне з py = vec2(0.0, 1.0/u_res.y) // Прохід 3: композиція: оригінал + bloom const compositeFrag = /* glsl */ ` uniform sampler2D u_scene; uniform sampler2D u_bloom; uniform float u_strength; varying vec2 vUv; void main() { vec3 scene = texture2D(u_scene, vUv).rgb; vec3 bloom = texture2D(u_bloom, vUv).rgb; gl_FragColor = vec4(scene + bloom * u_strength, 1.0); } `;

4Хроматична аберація

const chromaFrag = /* glsl */ ` uniform sampler2D u_tex; uniform float u_amount; // напр. 0.003 varying vec2 vUv; void main() { vec2 dir = vUv - 0.5; // напрямок від центру float dist = length(dir); // Семплюємо канали R, G, B у трохи різних позиціях float r = texture2D(u_tex, vUv + dir * u_amount * dist).r; float g = texture2D(u_tex, vUv).g; float b = texture2D(u_tex, vUv - dir * u_amount * dist).b; gl_FragColor = vec4(r, g, b, 1.0); } `;

5Віньєтка та скан-лінії

const vigFrag = /* glsl */ ` uniform sampler2D u_tex; uniform float u_time; varying vec2 vUv; void main() { vec3 col = texture2D(u_tex, vUv).rgb; // Віньєтка vec2 uv2 = vUv * (1.0 - vUv.yx); float vig = uv2.x * uv2.y * 15.0; vig = clamp(pow(vig, 0.25), 0.0, 1.0); // Скан-лінії ЕПТ float scan = sin(vUv.y * 800.0) * 0.04; // Зернистість плівки float noise = fract(sin(dot(vUv, vec2(12.9898, 78.233)) + u_time) * 43758.5453); noise = (noise - 0.5) * 0.04; col = col * vig - scan + noise; gl_FragColor = vec4(col, 1.0); } `;

6Багатопрохідний конвеєр

// Допоміжна функція для проходу шейдера: текстура-джерело → RenderTarget-приймач (або екран) function pass(shader, uniforms, srcTex, dst) { postMat.fragmentShader = shader; postMat.uniforms = { u_tex: { value: srcTex }, ...uniforms }; postMat.needsUpdate = true; renderer.setRenderTarget(dst ?? null); renderer.render(quadScene, quadCam); } const brightRT = makeRT(innerWidth / 2, innerHeight / 2); // половинна роздільність для bloom const blurH = makeRT(innerWidth / 2, innerHeight / 2); const blurV = makeRT(innerWidth / 2, innerHeight / 2); renderer.setAnimationLoop(t => { // 1. Рендеримо сцену в HDR RT renderer.setRenderTarget(rt); renderer.render(scene, camera); // 2. Виділяємо яскраві ділянки (половинна роздільність) pass(brightFrag, { u_threshold: { value: 0.85 } }, rt.texture, brightRT); // 3. Горизонтальне розмиття pass(blurHFrag, { u_res: { value: new THREE.Vector2(brightRT.width, brightRT.height) } }, brightRT.texture, blurH); // 4. Вертикальне розмиття pass(blurVFrag, { u_res: { value: new THREE.Vector2(blurH.width, blurH.height) } }, blurH.texture, blurV); // 5. Композиція + тонова корекція на екран postMat.fragmentShader = compositeFrag; postMat.uniforms = { u_scene: { value: rt.texture }, u_bloom: { value: blurV.texture }, u_strength: { value: 1.5 }, }; postMat.needsUpdate = true; renderer.setRenderTarget(null); renderer.render(quadScene, quadCam); });
Прохід bloom виконується у половинній роздільності, щоб заощадити пропускну здатність. Це просте зменшення 4:1 — для вищої якості використовуйте розмиття Kawase або dual Kawase, яке дає гаусовий результат за меншу кількість семплів.