Урок · Середній рівень · ~50 хв
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, яке дає гаусовий
результат за меншу кількість семплів.