onBeforeCompile: патчинг встроенных материалов

В библиотеке Three.js большинство визуальных объектов создаются с использованием встроенных материалов, таких как MeshStandardMaterial, MeshPhongMaterial, MeshBasicMaterial и других. Эти материалы обеспечивают удобные настройки освещения, текстур и отражений, однако их функциональность может быть ограничена при необходимости внедрения нестандартных эффектов шейдинга. Для гибкой модификации шейдеров используется метод onBeforeCompile.


Принцип работы onBeforeCompile

Каждый материал в Three.js при компиляции превращается в шейдер на языке GLSL, состоящий из вершинного и фрагментного шейдера. Метод onBeforeCompile позволяет вмешиваться в этот процесс перед компиляцией, изменяя исходный код шейдера или добавляя собственные uniform-переменные и функции.

Сигнатура метода:

material.onBeforeComp ile = function(shader) {
    // shader.vertexShader
    // shader.fragmentShader
    // shader.uniforms
};
  • shader.vertexShader — строка с кодом вершинного шейдера.
  • shader.fragmentShader — строка с кодом фрагментного шейдера.
  • shader.uniforms — объект с uniform-переменными, доступными в шейдерах.

После изменения этих свойств шейдер компилируется с учётом внесённых изменений, что позволяет создавать уникальные визуальные эффекты, не прибегая к написанию полностью кастомного ShaderMaterial.


Добавление пользовательских uniform-переменных

Uniform-переменные — это способ передавать данные из JavaScript в GLSL. Пример добавления анимации с изменяющейся интенсивностью света:

const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });

material.onBeforeComp ile = (shader) => {
    shader.uniforms.time = { value: 0.0 };

    shader.fragmentShader = `
        uniform float time;
    ` + shader.fragmentShader;

    shader.fragmentShader = shader.fragmentShader.replace(
        '#include <dithering_fragment>',
        `
        float pulse = abs(sin(time));
        gl_FragColor.rgb *= pulse;
        #include <dithering_fragment>
        `
    );
};

const mesh = new THREE.Mesh(geometry, material);

function animate(time) {
    requestAnimationFrame(animate);
    material.userData.shader && (material.userData.shader.uniforms.time.value = time * 0.001);
    renderer.render(scene, camera);
}

Здесь происходит:

  1. Добавление нового uniform time.
  2. Вставка кода GLSL в фрагментный шейдер для создания пульсации цвета.
  3. Подмена встроенной части шейдера с помощью replace.

Патчинг встроенных блоков шейдера

Three.js использует блоки шейдера (#include <...>), которые содержат стандартные вычисления: освещение, карты нормалей, тени. Метод onBeforeCompile позволяет:

  • Подменять стандартные блоки.
  • Добавлять код до или после них.
  • Интегрировать собственные функции GLSL.

Пример замены освещения на кастомный эффект:

material.onBeforeComp ile = (shader) => {
    shader.fragmentShader = shader.fragmentShader.replace(
        '#include <lights_fragment_begin>',
        `
        vec3 customLight = vec3(1.0, 0.5, 0.2);
        vec3 outgoingLight = diffuseColor.rgb * customLight;
        `
    );
};

В этом случае стандартная подсветка объекта отключается, и цвет рассчитывается по собственной формуле.


Динамическое добавление функций GLSL

Можно внедрять целые функции в шейдер:

material.onBeforeComp ile = (shader) => {
    const myFunc = `
        float noise(vec2 uv) {
            return fract(sin(dot(uv.xy ,vec2(12.9898,78.233))) * 43758.5453);
        }
    `;
    shader.fragmentShader = myFunc + shader.fragmentShader;

    shader.fragmentShader = shader.fragmentShader.replace(
        'gl_FragColor = vec4(outgoingLight, diffuseColor.a);',
        `
        float n = noise(vUv * 10.0);
        gl_FragColor = vec4(outgoingLight * n, diffuseColor.a);
        `
    );
};

Реализация позволяет добавлять перлин-шум, анимацию, постэффекты без создания отдельного ShaderMaterial.


Сохранение состояния и доступ к шейдеру

После компиляции материала объект shader не сохраняется автоматически в material. Для доступа к uniform-переменным из анимации можно сохранять ссылку:

material.onBeforeComp ile = (shader) => {
    material.userData.shader = shader;
};

Теперь uniform-переменные можно обновлять в функции анимации:

material.userData.shader.uniforms.time.value = performance.now() * 0.001;

Ограничения и особенности

  • Сложность отладки: GLSL-шейдеры при ошибках дают сообщения в консоли, но привязка к исходным материалам может быть непрозрачной.
  • Перезапись материалов: При изменении шейдера через onBeforeCompile повторная компиляция перезапишет изменения, если не сохранять код.
  • Совместимость с post-processing: кастомные патчи могут потребовать корректировки при использовании эффектов, таких как EffectComposer.

Примеры применения

  • Пульсирующие или анимированные материалы.
  • Добавление процедурных текстур (шум, полосы, градиенты).
  • Встраивание динамического освещения или специальных отражений.
  • Визуальные эффекты, такие как «glitch», искажения или сканирующие линии.
  • Интеграция с внешними данными для создания реактивных материалов.

Метод onBeforeCompile открывает доступ к глубокой кастомизации встроенных материалов в Three.js, сохраняя при этом простоту работы с высокоуровневыми объектами. Он позволяет сочетать удобство стандартных шейдеров с гибкостью собственного GLSL-кода, создавая уникальные визуальные эффекты без полной замены материала на ShaderMaterial.