MRT: рендеринг в несколько текстур одновременно

MRT (Multiple Render Targets) — это техника, позволяющая рендерить сцену одновременно в несколько текстур за один проход. В контексте Three.js это особенно важно для реализации сложных графических эффектов, таких как deferred shading, постобработка, SSAO (Screen Space Ambient Occlusion) и кастомные визуальные фильтры. Вместо последовательного рендера сцены в несколько буферов, MRT сокращает количество draw calls и повышает производительность.

В Three.js MRT реализуется с помощью объектов WebGLMultipleRenderTargets, которые создают несколько текстур, связанных с одним фреймбуфером.

import * as THREE from 'three';

const width = 1024;
const height = 1024;
const count = 3; // количество текстур

const renderTargets = new THREE.WebGLMultipleRenderTargets(width, height, count);

renderTargets.texture[0].name = "Albedo";
renderTargets.texture[1].name = "Normal";
renderTargets.texture[2].name = "Position";

renderTargets.texture.forEach(tex => {
    tex.minFilter = THREE.NearestFilter;
    tex.magFilter = THREE.NearestFilter;
    tex.type = THREE.FloatType;
});
  • count определяет количество текстур, в которые будет рендер сцены.
  • WebGLMultipleRenderTargets автоматически создаёт фреймбуфер и прикрепляет к нему все текстуры.
  • Каждая текстура может хранить разный тип данных: цвет, нормали, позиции, глубину и т.д.

Настройка шейдеров для MRT

Для использования MRT необходимо модифицировать Fragment Shader, чтобы выводить данные сразу в несколько буферов. В GLSL для этого применяется массив gl_FragData[].

#version 300 es
precision highp float;

layout(location = 0) out vec4 outAlbedo;
layout(location = 1) out vec4 outNormal;
layout(location = 2) out vec4 outPosition;

uniform vec3 uColor;

in vec3 vNormal;
in vec3 vPosition;

void main() {
    outAlbedo = vec4(uColor, 1.0);
    outNormal = vec4(normalize(vNormal) * 0.5 + 0.5, 1.0); 
    outPosition = vec4(vPosition, 1.0);
}
  • layout(location = X) указывает номер текстуры в MRT.
  • outAlbedo, outNormal, outPosition записывают разные данные в разные текстуры за один проход.
  • Нормали преобразуются в диапазон [0,1], чтобы корректно сохраняться в текстуре.

Интеграция с Three.js через RawShaderMaterial

Three.js поддерживает MRT через RawShaderMaterial, так как стандартные материалы (MeshStandardMaterial, MeshBasicMaterial) напрямую не предоставляют возможность вывода в несколько текстур. Пример:

const material = new THREE.RawShaderMaterial({
    vertexShader: document.getElementById('vertexShader').textContent,
    fragmentShader: document.getElementById('fragmentShader').textContent,
    uniforms: {
        uColor: { value: new THREE.Color(0xff0000) }
    }
});

const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
  • Vertex Shader передает позиции и нормали во фрагментный шейдер через varying.
  • Fragment Shader использует layout(location = X) для записи в несколько текстур.
  • Результат рендера сохраняется в WebGLMultipleRenderTargets, который затем может использоваться в постобработке.

Использование MRT для Deferred Shading

Deferred shading — классический сценарий применения MRT. Сначала создаётся G-Buffer, который хранит:

  1. Albedo — базовый цвет объекта.
  2. Normal — нормали поверхности.
  3. Position / Depth — мировые координаты или глубина.

После создания G-Buffer можно производить освещение вне основного рендера сцены, обходя необходимость перерендеривать геометрию.

const gBuffer = new THREE.WebGLMultipleRenderTargets(width, height, 3);
gBuffer.texture.forEach(tex => tex.type = THREE.FloatType);

renderer.setRenderTarget(gBuffer);
renderer.render(scene, camera);
renderer.setRenderTarget(null);

// Постобработка освещения
lightingPass.uniforms.albedoMap.value = gBuffer.texture[0];
lightingPass.uniforms.normalMap.value = gBuffer.texture[1];
lightingPass.uniforms.positionMap.value = gBuffer.texture[2];
renderer.render(lightingScene, camera);
  • Первый рендер создаёт G-Buffer.
  • Второй проход использует данные текстур для расчёта освещения и теней.
  • Такой подход уменьшает количество draw calls и позволяет масштабировать сцену с большим количеством источников света.

Советы по производительности и точности

  1. Тип текстур Для хранения нормалей, позиции и других векторов лучше использовать THREE.FloatType или THREE.HalfFloatType, чтобы избежать потери точности.

  2. Фильтрация MRT чаще всего используют с NearestFilter, так как линейная фильтрация может искажать данные (особенно нормали и позиции).

  3. Размеры текстур Для больших сцен стоит подбирать разрешение G-Buffer с компромиссом между качеством и производительностью.

  4. Совместимость WebGL2 MRT требует WebGL2. В WebGL1 поддержка ограничена и требует расширений (WEBGL_draw_buffers). Three.js автоматически проверяет поддержку MRT через WebGLRenderer.


Расширенные сценарии использования MRT

  • SSAO (Screen Space Ambient Occlusion): Использует G-Buffer для расчёта затемнения на основе геометрии сцены.
  • Custom Postprocessing Effects: MRT позволяет сохранять промежуточные данные сцены, например глубину, нормали или отражения, для последующего комбинирования.
  • Volumetric Effects: Лучи света и туман могут рендериться в отдельные текстуры без повторного прохода сцены.

Использование MRT в Three.js открывает возможности для высокопроизводительных визуализаций и сложной графики, обеспечивая гибкость и масштабируемость современных 3D-приложений.