Работа с UV, нормалями и тангентами в шейдере

Основы работы с UV-координатами

UV-координаты представляют собой способ отображения текстур на поверхности 3D-модели. В Babylon.js каждая вершина меша может иметь набор UV-координат, который определяет, какая часть текстуры будет отображена на этой вершине. UV-координаты обычно находятся в диапазоне от 0 до 1 по осям U (горизонталь) и V (вертикаль).

Для доступа к UV-координатам вершин используется массив mesh.getVerticesData(BABYLON.VertexBuffer.UVKind). Этот массив содержит пары чисел [u0, v0, u1, v1, ...], соответствующие каждой вершине меша. Изменение этих данных позволяет создавать эффекты смещения текстуры, анимации текстурных координат и процедурную генерацию UV.

Пример изменения UV-координат:

const mesh = BABYLON.MeshBuilder.CreatePlane("plane", { size: 2 }, scene);
const uvs = mesh.getVerticesData(BABYLON.VertexBuffer.UVKind);

for (let i = 0; i < uvs.length; i += 2) {
    uvs[i] += 0.1; // сдвиг по U
    uvs[i + 1] += 0.1; // сдвиг по V
}

mesh.updateVerticesData(BABYLON.VertexBuffer.UVKind, uvs);

Нормали и их значение для шейдера

Нормали — это векторы, перпендикулярные поверхности модели в каждой вершине. Они необходимы для корректного освещения и расчета отражений. В Babylon.js нормали хранятся в массиве mesh.getVerticesData(BABYLON.VertexBuffer.NormalKind). Нормали обычно нормализованы, то есть имеют длину 1.

Основные применения нормалей:

  • Расчет диффузного освещения через скалярное произведение с направлением света.
  • Генерация нормальных карт для создания детализации поверхности.
  • Определение направления отраженного света в шейдерах.

Пример вычисления нормали для пользовательского меша:

const positions = mesh.getVerticesData(BABYLON.VertexBuffer.PositionKind);
const indices = mesh.getIndices();
const normals = [];

BABYLON.VertexData.ComputeNormals(positions, indices, normals);
mesh.updateVerticesData(BABYLON.VertexBuffer.NormalKind, normals);

Тангенты и бамп-маппинг

Тангенты — это векторы, лежащие в плоскости поверхности и направленные по оси U текстуры. Тангенты необходимы для корректного применения нормальных карт, чтобы преобразовать данные нормалей из текстурного пространства в мировое или локальное пространство шейдера.

Babylon.js автоматически генерирует тангенты через BABYLON.VertexData.ComputeTangents, если меш содержит UV-координаты и нормали:

const positions = mesh.getVerticesData(BABYLON.VertexBuffer.PositionKind);
const indices = mesh.getIndices();
const normals = mesh.getVerticesData(BABYLON.VertexBuffer.NormalKind);
const uvs = mesh.getVerticesData(BABYLON.VertexBuffer.UVKind);
const tangents = [];

BABYLON.VertexData.ComputeTangents(positions, indices, uvs, normals, tangents);
mesh.updateVerticesData(BABYLON.VertexBuffer.TangentKind, tangents);

Тангенты передаются в шейдер как vTangent для вершинного или фрагментного шейдера и используются для построения TBN-матрицы (Tangent-Bitangent-Normal), которая позволяет корректно трансформировать нормали из текстурного пространства:

TBN = [Tangent, Bitangent, Normal]

Где Bitangent вычисляется как перекрестное произведение нормали и тангента с учётом направления w-компоненты тангента.

Прямое использование в шейдерах

В Babylon.js создание шейдера с поддержкой нормалей, тангентов и UV осуществляется через ShaderMaterial. Пример передачи данных в шейдер:

const shaderMaterial = new BABYLON.ShaderMaterial("shader", scene, {
    vertex: "custom",
    fragment: "custom",
}, {
    attributes: ["position", "normal", "uv", "tangent"],
    uniforms: ["worldViewProjection", "world"]
});

mesh.material = shaderMaterial;

В вершинном шейдере:

attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;
attribute vec4 tangent;

varying vec2 vUV;
varying vec3 vNormal;
varying vec3 vTangent;
varying vec3 vBitangent;

void main() {
    vUV = uv;
    vNormal = normalize(normalMatrix * normal);
    vTangent = normalize(normalMatrix * tangent.xyz);
    vBitangent = cross(vNormal, vTangent) * tangent.w;
    gl_Position = worldViewProjection * vec4(position, 1.0);
}

Фрагментный шейдер может использовать TBN-матрицу для применения нормальных карт:

varying vec2 vUV;
varying vec3 vNormal;
varying vec3 vTangent;
varying vec3 vBitangent;

uniform sampler2D normalMap;

void main() {
    vec3 N = texture2D(normalMap, vUV).rgb;
    N = N * 2.0 - 1.0;
    mat3 TBN = mat3(vTangent, vBitangent, vNormal);
    vec3 finalNormal = normalize(TBN * N);
    // расчет освещения с finalNormal
}

Практические советы

  • Всегда проверять, что меш содержит нормали и UV-координаты, иначе тангенты не будут корректно вычислены.
  • При изменении UV или деформации меша необходимо пересчитывать тангенты.
  • Для анимации нормалей через текстуры лучше использовать ShaderMaterial или NodeMaterial, так как стандартные материалы могут иметь ограниченную гибкость.

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