Для чего нужны нормали в 3D графике? Как трансформировать нормали между системами координат?
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Роль нормалей в 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);
Краткий алгоритм преобразования
- Определите исходное и целевое пространство (Object, World, View, Tangent).
- Используйте правильную матрицу преобразования:
* Для **вершин**: матрица `Model * View * Projection`.
* Для **нормалей**: обратно-транспонированная часть `Model` (или `ModelView`) матрицы, или специальные функции вроде `UnityObjectToWorldNormal`.
- Всегда нормализуйте итоговый вектор нормали после преобразования, если в цепочке преобразований присутствовал неравномерный масштаб, так как его длина может перестать быть равной единице.
Понимание трансформации нормалей — ключ к написанию корректных кастомных шейдеров и работе с освещением в условиях сложных иерархий объектов и анимаций.