← Назад к вопросам

Для чего нужны нормали в 3D графике? Как трансформировать нормали между системами координат?

2.2 Middle🔥 192 комментариев
#Рендеринг и графика

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI7 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Роль нормалей в 3D-графике

Нормали — это векторы единичной длины, перпендикулярные поверхности объекта. Их основное назначение — определение ориентации поверхности относительно источников света и камеры. В Unity и 3D-графике в целом они критически важны для:

  • Расчёта освещения: Большинство моделей освещения (например, Фонга, Ламберта) используют скалярное произведение вектора нормали и вектора направления света для определения яркости поверхности (dot(normal, lightDir)). Без нормалей все объекты выглядели бы плоскими.
  • Определения карт нормалей (Normal Mapping): Техника, позволяющая симулировать мелкую детализацию поверхности (например, неровности кирпича или поры кожи) без увеличения геометрической сложности модели. Нормаль из текстуры подменяет геометрическую нормаль.
  • Расчёта отражений: Например, в моделях бликового освещения (specular) или для построения кубических карт отражений (Cubemap Reflection).
  • Коллизий и физики: Определение направления отскока при столкновении или расчёт силы трения.
  • Селекции граней (Face Culling): Отбраковка невидимых (задних) полигонов на основе направления их нормали относительно камеры.

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

// Упрощённый фрагмент шейдера с освещением по Ламберту
Shader "Custom/NormalExample" {
    SubShader {
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct appdata {
                float4 vertex : POSITION;
                float3 normal : NORMAL; // Нормаль из модели
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
            };

            v2f vert (appdata v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                // Преобразование нормали в мировое пространство (см. ниже)
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target {
                // Направление света (предположим, направленный источник)
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                // Расчёт диффузного освещения: dot(N, L)
                float diff = max(0, dot(normalize(i.worldNormal), lightDir));
                return fixed4(diff, diff, diff, 1);
            }
            ENDCG
        }
    }
}

Трансформация нормалей между системами координат

Нормали — это не точки и не положения, а направления. Поэтому их преобразование между пространствами координат (например, из локального пространства объекта в мировое) отличается от преобразования вершин.

Ключевое правило

Если вершины трансформируются матрицей M (состоящей из поворота R, масштаба S и перемещения T), то нормали должны трансформироваться обратно-транспонированной матрицей обратной матрицы M, чтобы остаться перпендикулярными поверхности после деформации объекта. На практике для матрицы модели (Model Matrix), состоящей из поворота и масштаба (перемещение для нормалей не важно), это сводится к:

Нормали преобразуются обратно-транспонированной матрицей поворота-масштаба модели. Формула:
worldNormal = transpose(inverse(M)) * objectNormal

Где M — это подматрица 3x3 матрицы модели (без компоненты перемещения).

Почему так сложно?

  • Масштаб (Scale): Если объект масштабируется неравномерно (например, (2, 1, 1)), нормали, преобразованные той же матрицей, что и вершины, перестанут быть перпендикулярными поверхности. Обратно-транспонированная матрица это компенсирует.
  • Поворот (Rotation): Для чистого поворота (ортонормированная матрица) обратно-транспонированная матрица эквивалентна исходной, поэтому можно использовать просто M.

Практика в Unity

В Unity эти преобразования уже инкапсулированы во вспомогательные функции и стандартные шейдеры:

// В C# скрипте для преобразования нормали из локального в мировое пространство
Vector3 worldNormal = transform.TransformDirection(objectNormal); // Без учёта масштаба, только поворот
// ИЛИ более точный способ через матрицу:
Matrix4x4 modelMatrix = transform.localToWorldMatrix;
Matrix4x4 inverseTranspose = modelMatrix.inverse.transpose;
Vector3 worldNormal = inverseTranspose.MultiplyVector(objectNormal).normalized;
// В шейдере (CG/HLSL) используйте встроенные функции Unity:
float3 worldNormal = UnityObjectToWorldNormal(v.normal); // Рекомендованный способ
// Что эквивалентно:
// float3 worldNormal = normalize(mul((float3x3)unity_WorldToObject, v.normal));
// ИЛИ для перехода в пространство камеры (касательное):
float3 viewNormal = mul((float3x3)UNITY_MATRIX_V, worldNormal);

Краткий алгоритм преобразования

  1. Определите исходное и целевое пространство (Object, World, View, Tangent).
  2. Используйте правильную матрицу преобразования:
    *   Для **вершин**: матрица `Model * View * Projection`.
    *   Для **нормалей**: обратно-транспонированная часть `Model` (или `ModelView`) матрицы, или специальные функции вроде `UnityObjectToWorldNormal`.
  1. Всегда нормализуйте итоговый вектор нормали после преобразования, если в цепочке преобразований присутствовал неравномерный масштаб, так как его длина может перестать быть равной единице.

Понимание трансформации нормалей — ключ к написанию корректных кастомных шейдеров и работе с освещением в условиях сложных иерархий объектов и анимаций.