Chunки Three.js: переиспользование кода шейдеров

В библиотеке Three.js механизм чанков (shader chunks) позволяет переиспользовать и комбинировать фрагменты GLSL-кода между различными материалами и шейдерами. Чанк — это по сути шаблон GLSL, который может быть вставлен в стандартный шейдер через директивы #include. Такой подход значительно упрощает поддержку сложных визуальных эффектов, снижает дублирование кода и обеспечивает согласованность между материалами.

Three.js содержит обширную коллекцию встроенных чанков, которые охватывают:

  • Функции работы с освещением (lights_physical_fragment, lights_pars_begin)
  • Работа с текстурами (map_fragment, envmap_fragment)
  • Обработка нормалей (normal_fragment, normal_pars_vertex)
  • Трансформации вершин (morphtarget_vertex, skinning_vertex)
  • Постобработку и глобальные параметры (fog_fragment, shadowmap_pars_fragment)

Каждый чанк хранится в виде строки GLSL и подключается через синтаксис:

#include <имя_чанка>

Например:

void main() {
    #include <color_fragment>
    gl_FragColor = vec4( diffuseColor, opacity );
}

Структура встроенных чанков

Все встроенные чанки находятся в объекте THREE.ShaderChunk. Например:

console.log(THREE.ShaderChunk);

Результатом будет объект, содержащий десятки строковых шаблонов, таких как:

  • envmap_pars_fragment
  • lights_physical_pars_fragment
  • shadowmap_pars_vertex
  • fog_fragment

Каждый чанк обычно делится на две части:

  1. Pars (*_pars_*) — объявления переменных и функций, которые нужны шейдеру, например:
uniform sampler2D map;
varying vec2 vUv;
  1. Основной фрагмент — непосредственно вставка кода в тело main() шейдера:
vec4 texelColor = texture2D( map, vUv );
diffuseColor *= texelColor;

Такое разделение позволяет подключать функционал либо к объявлению переменных, либо к основной логике.

Создание собственного чанка

Three.js позволяет создавать кастомные чанки, чтобы их потом использовать как стандартные. Это делается через добавление строкового шаблона в объект THREE.ShaderChunk:

THREE.ShaderChunk.myCustomChunk = `
vec3 modifyColor(vec3 color) {
    return color * vec3(0.8, 0.9, 1.0);
}
`;

И подключение в шейдере:

#include <myCustomChunk>

void main() {
    vec3 baseColor = vec3(1.0, 0.5, 0.2);
    vec3 finalColor = modifyColor(baseColor);
    gl_FragColor = vec4(finalColor, 1.0);
}

Использование чанков в ShaderMaterial

Для интеграции чанков с кастомными шейдерами используют ShaderMaterial. Важно правильно комбинировать vertexShader и fragmentShader, используя директивы #include для переиспользования кода. Пример:

const material = new THREE.ShaderMaterial({
    uniforms: {
        time: { value: 0 }
    },
    vertexShader: `
        #include <common>
        #include <morphtarget_pars_vertex>
        void main() {
            #include <begin_vertex>
            #include <project_vertex>
        }
    `,
    fragmentShader: `
        #include <common>
        #include <color_pars_fragment>
        uniform float time;
        void main() {
            vec3 dynamicColor = vec3(sin(time), cos(time), 0.5);
            gl_FragColor = vec4(dynamicColor, 1.0);
        }
    `
});

Здесь используются как встроенные чанки (common, color_pars_fragment), так и кастомная логика через GLSL.

Переопределение стандартных чанков

Three.js позволяет переопределять встроенные чанки для изменения стандартного поведения материалов без копирования всего шейдера. Например, можно заменить обработку освещения:

THREE.ShaderChunk.lights_fragment_begin = `
vec3 totalLight = vec3(0.0);
// кастомная логика освещения
`;

Все материалы, использующие lights_fragment_begin, автоматически получат новую реализацию. Это удобно для глобальных изменений стиля освещения или визуальных эффектов.

Рекомендации по организации кода

  • Разделять чанки на небольшие, переиспользуемые модули.
  • Использовать понятные имена (*_pars_* для объявлений, *_fragment/*_vertex для логики).
  • Не дублировать код, если функционал уже реализован в стандартных чанках.
  • Проверять зависимости чанков, так как некоторые чанки требуют других (common, uv_pars, lights_pars_begin).

Динамическая генерация и условные чанки

Для более сложных материалов возможна динамическая генерация шейдеров, когда чанки подключаются в зависимости от условий:

let fragmentShader = `
#include <common>
`;

if (useFog) fragmentShader += `#include <fog_fragment>`;
if (useTexture) fragmentShader += `#include <map_fragment>`;

fragmentShader += `
void main() {
    gl_FragColor = vec4(1.0);
}
`;

Такой подход позволяет строить адаптивные шейдеры без дублирования базовой логики.

Применение чанков в расширенных материалах

Чанки активно используются в:

  • MeshStandardMaterial и MeshPhysicalMaterial для освещения и PBR.
  • ShaderMaterial для кастомной визуализации.
  • Постобработке через EffectComposer и шейдерные пассы.

Это делает код более модульным, поддерживаемым и расширяемым, особенно при работе с большими сценами и сложными эффектами.

Заключение по функциональности

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