OpenGL.Shader:9-光法線マップの学習(TBNマトリックスの計算)
この記事では、通常のマップについて学習します。通常のマップは、ゲーム開発やGISシステム開発で特に広く使用されており、表現力が特に強く、描画効果が非常に現実に近いものです。さらに重要な点は、非常に経済的なモデルを非常に少ないコストで作成できることです。これは、ゲームの傑作では特に重要です。
上記の2つのキューブテクスチャの効果を見てみましょう。左側が通常のテクスチャマップ、右側が通常のマップで、視覚効果に大きな違いがあります。これが通常のマップの魅力です。
ショーコードの前に、通常のマップの関連知識について簡単に説明します。
1.通常のマッピング
照明効果をシミュレートするとき、照明される表面が完全に平らな表面になる理由は何ですか?答えは表面の法線ベクトルです。レンガを例にとると、照明アルゴリズムの観点から、レンガの表面には1つの法線ベクトルしかなく、表面はこの法線ベクトルに基づいて一貫した方法で照明されるため、詳細効果の実現は比較的簡単です。各フラグメントに独自の異なる法線がある場合はどうなりますか?このようにして、表面の微妙な詳細に応じて法線ベクトルを変更できます。これにより、表面上ではるかに複雑に見える視覚効果が得られます。
各フラグメントは独自の法線を使用するため、多くの小さな(異なる法線ベクトル)平面で構成されるサーフェスを作成でき、そのようなオブジェクトのサーフェスの詳細が大幅に改善されます。サーフェス上のすべてのフラグメントに同じ法線を使用する代わりに、フラグメントピクセルごとに独自の法線を使用するこの手法は、法線マッピングと呼ばれます。
通常のベクトルは3要素の幾何学的モデルであるため、2Dテクスチャは色と照明のデータを保存できるだけでなく、通常のベクトルも保存できます。考えてみてください。テクスチャの色成分r、g、bは、vec3の数学モデルで表されます。同じvec3の通常のベクトルx、y、およびz要素を、色r、g、およびb要素の代わりにテクスチャに格納して、通常のテクスチャを形成することもできます。このようにして、位置データのセットに基づいて、通常のテクスチャから対応する位置の通常のベクトルを同時にサンプリングできます。このようにして、通常のマップはテクスチャマップのように機能します。
2.タンジェントスペース
法線ベクトル(x、y、z)をテクスチャの(r、g、b)コンポーネントにマッピングするので、最初の直感は、各フラグメントの法線ベクトルがテクスチャに垂直でなければならないということです。平面(つまり、UV座標で構成される平面)、3つのコンポーネントの比率はすべてz(b)コンポーネント上にあるため、通常のテクスチャはほとんど青みがかった視覚的外観を示します。(以下に示すように)しかし、これには深刻な問題があります。この例では、立方体の6つの面があり、上面の法線ベクトルのみが(0、0、1)ですが、他の方向の面はこの方法を使用できませんか?線の質感?
考えてみてください。モデルの頂点/テクスチャ座標をワールド座標に変換するにはどうすればよいですか?通常のベクトルは、モデルの変更操作にどのように同期されますか?すべて座標系の行列演算によるものであり、ここでは接線空間座標系を紹介します。通常の2Dテクスチャ座標にはUとVが含まれます。U座標が増加する方向は接線空間(接線軸)の接線方向であり、V座標が増加する方向は接線空間の2次接線方向(双接軸)モデルです。の異なる面にはすべて対応する接線空間があり、接線軸と双接軸はそれぞれ描画面に配置され、対応する法線方向と組み合わされます。接線軸(T)、双接軸(B)、法線軸(N )座標系は接線空間(TBN)で構成されていますか(以下を参照)
TBN接線空間座標系では、法線テクスチャから抽出された法線ベクトルはTBNの値に基づいており、数学的な行列演算を実行し、TBN変換行列を掛けて、モデルに必要な正しい方向に変換します。通常のベクトル。
(TBNマトリックスの計算とより深い数学的変換の原理については、次のリンクを参照してください)
https://blog.csdn.net/jxw167/article/details/58671953
https://blog.csdn.net/yuchenwuhen/article/詳細/ 71055602
3.コードの使用
class CubeTBN {
struct V3N3UV2 {
float x, y, z; //位置坐标
float nx, ny, nz; //法向量
float u,v; //纹理坐标
};
struct V3N3UV2TB6
{
float x, y, z;
float nx, ny, nz;
float u, v;
float tx,ty,tz;
float bx,by,bz;
};
// ...
};
まず、2つの構造を定義します。V3N3UV2は、これまで使用してきた標準のデータ構造です(位置頂点vec3、法線ベクトルvec3、テクスチャ座標vec2)。V3N3UV2に2つのvec3を追加します。これらは、接線方向(接線軸)と2次接線方向(双接軸)です。メソッドconvertTBNを介して特定の値を見つけます
public:
V3N3UV2TB6 _data[36];
void init(const CELL::float3 &halfSize)
{
// 之前的标准数据,通过传入size确定大小。
V3N3UV2 verts[] =
{
// 前
{-halfSize.x, +halfSize.y, +halfSize.z, 0.0f, 0.0f, +1.0f, 0.0f,0.0f},
{-halfSize.x, -halfSize.y, +halfSize.z, 0.0f, 0.0f, +1.0f, 1.0f,0.0f},
{+halfSize.x, +halfSize.y, +halfSize.z, 0.0f, 0.0f, +1.0f, 0.0f,1.0f},
{-halfSize.x, -halfSize.y, +halfSize.z, 0.0f, 0.0f, +1.0f, 1.0f,0.0f},
{+halfSize.x, -halfSize.y, +halfSize.z, 0.0f, 0.0f, +1.0f, 1.0f,1.0f},
{+halfSize.x, +halfSize.y, +halfSize.z, 0.0f, 0.0f, +1.0f, 0.0f,1.0f},
// 后
{+halfSize.x, -halfSize.y, -halfSize.z, 0.0f, 0.0f, -1.0f, 1.0f,0.0f},
{-halfSize.x, -halfSize.y, -halfSize.z, 0.0f, 0.0f, -1.0f, 1.0f,1.0f},
{+halfSize.x, +halfSize.y, -halfSize.z, 0.0f, 0.0f, -1.0f, 0.0f,0.0f},
{-halfSize.x, +halfSize.y, -halfSize.z, 0.0f, 0.0f, -1.0f, 1.0f,0.0f},
{+halfSize.x, +halfSize.y, -halfSize.z, 0.0f, 0.0f, -1.0f, 0.0f,0.0f},
{-halfSize.x, -halfSize.y, -halfSize.z, 0.0f, 0.0f, -1.0f, 1.0f,1.0f},
// 左
{-halfSize.x, +halfSize.y, +halfSize.z, -1.0f, 0.0f, 0.0f, 0.0f,0.0f},
{-halfSize.x, -halfSize.y, -halfSize.z, -1.0f, 0.0f, 0.0f, 1.0f,1.0f},
{-halfSize.x, -halfSize.y, +halfSize.z, -1.0f, 0.0f, 0.0f, 1.0f,0.0f},
{-halfSize.x, +halfSize.y, -halfSize.z, -1.0f, 0.0f, 0.0f, 0.0f,1.0f},
{-halfSize.x, -halfSize.y, -halfSize.z, -1.0f, 0.0f, 0.0f, 1.0f,1.0f},
{-halfSize.x, +halfSize.y, +halfSize.z, -1.0f, 0.0f, 0.0f, 0.0f,0.0f},
// 右
{+halfSize.x, +halfSize.y, -halfSize.z, +1.0f, 0.0f, 0.0f, 0.0f,0.0f},
{+halfSize.x, +halfSize.y, +halfSize.z, +1.0f, 0.0f, 0.0f, 0.0f,1.0f},
{+halfSize.x, -halfSize.y, +halfSize.z, +1.0f, 0.0f, 0.0f, 1.0f,1.0f},
{+halfSize.x, -halfSize.y, -halfSize.z, +1.0f, 0.0f, 0.0f, 1.0f,0.0f},
{+halfSize.x, +halfSize.y, -halfSize.z, +1.0f, 0.0f, 0.0f, 0.0f,0.0f},
{+halfSize.x, -halfSize.y, +halfSize.z, +1.0f, 0.0f, 0.0f, 1.0f,1.0f},
// 上
{-halfSize.x, +halfSize.y, +halfSize.z, 0.0f, +1.0f, 0.0f, 0.0f,1.0f},
{+halfSize.x, +halfSize.y, +halfSize.z, 0.0f, +1.0f, 0.0f, 1.0f,1.0f},
{+halfSize.x, +halfSize.y, -halfSize.z, 0.0f, +1.0f, 0.0f, 1.0f,0.0f},
{-halfSize.x, +halfSize.y, -halfSize.z, 0.0f, +1.0f, 0.0f, 0.0f,0.0f},
{-halfSize.x, +halfSize.y, +halfSize.z, 0.0f, +1.0f, 0.0f, 0.0f,1.0f},
{+halfSize.x, +halfSize.y, -halfSize.z, 0.0f, +1.0f, 0.0f, 1.0f,0.0f},
// 下
{+halfSize.x, -halfSize.y, -halfSize.z, 0.0f, -1.0f, 0.0f, 1.0f,1.0f},
{+halfSize.x, -halfSize.y, +halfSize.z, 0.0f, -1.0f, 0.0f, 1.0f,0.0f},
{-halfSize.x, -halfSize.y, -halfSize.z, 0.0f, -1.0f, 0.0f, 0.0f,1.0f},
{+halfSize.x, -halfSize.y, +halfSize.z, 0.0f, -1.0f, 0.0f, 1.0f,0.0f},
{-halfSize.x, -halfSize.y, +halfSize.z, 0.0f, -1.0f, 0.0f, 0.0f,0.0f},
{-halfSize.x, -halfSize.y, -halfSize.z, 0.0f, -1.0f, 0.0f, 0.0f,1.0f}
};
// 根据位置/纹理 -> TBN
convertTBN(verts, _data);
}
void convertTBN(V3N3UV2* vertices,V3N3UV2TB6* nmVerts)
{
for (size_t i = 0; i <36; i += 3) // 一次操作一个三角形的三个点
{
// copy xyz normal uv
nmVerts[i + 0].x = vertices[i + 0].x;
nmVerts[i + 0].y = vertices[i + 0].y;
nmVerts[i + 0].z = vertices[i + 0].z;
nmVerts[i + 0].nx = vertices[i + 0].nx;
nmVerts[i + 0].ny = vertices[i + 0].ny;
nmVerts[i + 0].nz = vertices[i + 0].nz;
nmVerts[i + 0].u = vertices[i + 0].u;
nmVerts[i + 0].v = vertices[i + 0].v;
nmVerts[i + 1].x = vertices[i + 1].x;
nmVerts[i + 1].y = vertices[i + 1].y;
nmVerts[i + 1].z = vertices[i + 1].z;
nmVerts[i + 1].nx = vertices[i + 1].nx;
nmVerts[i + 1].ny = vertices[i + 1].ny;
nmVerts[i + 1].nz = vertices[i + 1].nz;
nmVerts[i + 1].u = vertices[i + 1].u;
nmVerts[i + 1].v = vertices[i + 1].v;
nmVerts[i + 2].x = vertices[i + 2].x;
nmVerts[i + 2].y = vertices[i + 2].y;
nmVerts[i + 2].z = vertices[i + 2].z;
nmVerts[i + 2].nx = vertices[i + 2].nx;
nmVerts[i + 2].ny = vertices[i + 2].ny;
nmVerts[i + 2].nz = vertices[i + 2].nz;
nmVerts[i + 2].u = vertices[i + 2].u;
nmVerts[i + 2].v = vertices[i + 2].v;
// Shortcuts for vertices
CELL::float3 v0 = CELL::float3(vertices[i + 0].x,vertices[i + 0].y,vertices[i + 0].z);
CELL::float3 v1 = CELL::float3(vertices[i + 1].x,vertices[i + 1].y,vertices[i + 1].z);
CELL::float3 v2 = CELL::float3(vertices[i + 2].x,vertices[i + 2].y,vertices[i + 2].z);
CELL::float2 uv0 = CELL::float2(vertices[i + 0].u, vertices[i + 0].v);
CELL::float2 uv1 = CELL::float2(vertices[i + 1].u, vertices[i + 1].v);
CELL::float2 uv2 = CELL::float2(vertices[i + 2].u, vertices[i + 2].v);
// 构建triangle平面的方向向量 (position delta, δ)
CELL::float3 deltaPos1 = v1 - v0;
CELL::float3 deltaPos2 = v2 - v0;
// 构建UV平面的两个方向的向量 (uv delta, δ)
CELL::float2 deltaUV1 = uv1 - uv0;
CELL::float2 deltaUV2 = uv2 - uv0;
float r = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV1.y * deltaUV2.x); // uv叉积作底
CELL::float3 tangent = (deltaPos1 * deltaUV2.y - deltaPos2 * deltaUV1.y)*r; // 得出切线
CELL::float3 binormal = (deltaPos2 * deltaUV1.x - deltaPos1 * deltaUV2.x)*r; // 得出副切线
// 赋值到t b
nmVerts[i + 0].tx = tangent.x; nmVerts[i + 0].bx = binormal.x;
nmVerts[i + 0].ty = tangent.y; nmVerts[i + 0].by = binormal.y;
nmVerts[i + 0].tz = tangent.z; nmVerts[i + 0].bz = binormal.z;
nmVerts[i + 1].tx = tangent.x; nmVerts[i + 1].bx = binormal.x;
nmVerts[i + 1].ty = tangent.y; nmVerts[i + 1].by = binormal.y;
nmVerts[i + 1].tz = tangent.z; nmVerts[i + 1].bz = binormal.z;
nmVerts[i + 2].tx = tangent.x; nmVerts[i + 2].bx = binormal.x;
nmVerts[i + 2].ty = tangent.y; nmVerts[i + 2].by = binormal.y;
nmVerts[i + 2].tz = tangent.z; nmVerts[i + 2].bz = binormal.z;
}
}
これまで、各基準点のTBNマトリックスが計算されてきました。データを取得したら、シェーダープログラムの作成方法を学び始めることができます。
まず、頂点シェーダーの部分を見てみましょう。
#version 320 es
in vec3 _position; // vec3_normalの外部入力
;
vec2 _uv;
in vec3 _tagent;
in vec3_biTagent;
ユニフォームmat4_mvp; // mvpマトリックス
ユニフォームmat3_normalMatrix; //ノーマルマトリックス
ユニフォームmat4_matModel; / /モデル変換マトリックス
outvec2 _outUV;
out vec3 _outPos;
out mat3 _TBN;
void main()
{ _ outUV = _uv;テクスチャマップと通常のマップを抽出するためにテクスチャ座標をフラグメントシェーダーに出力します vec4pos = _matModel * vec4( _position、1); _outPos = pos.xyz;モデルの頂点位置を出力して、各フラグメントの光の方向が洗練されていることを確認します vec3 normal = normalize(_normalMatrix * _normal); //通常の行列を掛けて、モデルの変換を確認します操作後の一貫性。
vec3 tagent = normalize(_normalMatrix * _tagent);
vec3 biTagent = normalize(_normalMatrix * _biTagent);
_TBN = mat3x3(tagent、biTagent、normal); // TBNマトリックスを作成し、フラグメントシェーダーに出力します
gl_Position = _mvp * vec4(_position、 1.0); //最終的に描画された頂点座標を出力します
}
em ...コメントが追加されました。 モデリング操作後の(ワールド座標系の)頂点をフラグメントシェーダーに出力する必要があるのはなぜですか?光の強さの主な属性である照明の方向を計算します。以前は頂点シェーダーで直接計算していました。これは、以前は通常のマップの主要なコンテンツをマスターしておらず、各フラグメントの通常のベクトルを調整できなかったためです。の中で。頂点位置データが頂点シェーダーからフラグメントシェーダーに出力された後、フラグメントシェーダーは補間操作を実行します。補間後、各フラグメントには補間された頂点位置データがあるため、より良い計算結果を得るには、通常のマップと一致するように、より詳細な光の方向を再計算する必要があります。
ES 320 #Version
精密フロートをmediump;
; VEC2 _outUVに
、Vec3 _outPosに
MAT3 _TBNで、
制服Vec3 _lightColor;
制服Vec3 _lightDiffuse;
制服sampler2Dの_texture;
制服sampler2D _texNormal;
制服Vec3 _lightPos;
制服Vec3 _cameraPos;
OUT Vec3 _fragColor、
無効メイン()
{ vec3 lightDir = normalize(_lightPos-_outPos); //各フラグメントの光の方向を計算します vec3normal = normalize(_TBN *(texture(_texNormal、_outUV).xyz * 2.0-1.0)); //正規マップから正規ベクトルを抽出して[-1,1]間隔を正規化し、TBNマトリックスを介して最終的な正規ベクトルに変換します
vec4 materialColor = texture(_texture、_outUV);
float lightStrength = max(dot(normal、lightDir)、0.0); //光強度を計算
vec4lightColor = vec4(_lightColor * lightStrength + _lightDiffuse、1); //光強度*色光+拡散光
_fragColor.rgb = materialColor.rgb * 0.2 + 0.8 * lightColor.rgb; //混合入力効果。
}
_uvテクスチャ座標は、テクスチャマップのカラー値を抽出し、通常のマップの通常のベクトル値を抽出できます。texture(_texNormal、_outUV).xyzの範囲値は[0,255]であり、正規化は[0,1]であることに注意してください。しかし、通常のベクトルに必要なのは[-1、1]であり、自分で変換する必要があります。最終的な混合出力効果は固定されていません。必要に応じて効果を調整するだけです。
最後に、CubeTBN.renderメソッドを追加します。
void render(Camera3D& camera)
{
_program.begin();
static float angle = 0;
angle += 0.1f;
CELL::matrix4 matRot;
matRot.rotateYXZ(angle, 0.0f, 0.0f);
CELL::matrix4 model = _modelMatrix * matRot;
glUniformMatrix4fv(_program._matModel, 1, GL_FALSE, model.data());
CELL::matrix4 vp = camera.getProject() * camera.getView();
CELL::matrix4 mvp = (vp * model);
glUniformMatrix4fv(_program._mvp, 1, GL_FALSE, mvp.data());
CELL::matrix3 matNormal = mat4_to_mat3(model)._inverse()._transpose();
glUniformMatrix3fv(_program._normalMatrix, 1, GL_FALSE, matNormal.data());
glUniform3f(_program._lightDiffuse, 0.1f, 0.1f, 0.1f); // 漫反射 环境光
glUniform3f(_program._lightColor, 1.0f, 1.0f, 1.0f); // 定向光源的颜色
glUniform3f(_program._lightPos, camera._eye.x, camera._eye.y, camera._eye.z);//光源位置
glActiveTexture(GL_TEXTURE0);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, _texMaterial);
glUniform1i(_program._texture, 0); // 加载纹理贴图
glActiveTexture(GL_TEXTURE1);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, _texNormal);
glUniform1i(_program._texNormal, 1); // 加载法线贴图
glVertexAttribPointer(_program._position, 3, GL_FLOAT, GL_FALSE, sizeof(V3N3UV2TB6), _data);
glVertexAttribPointer(_program._normal, 3, GL_FLOAT, GL_FALSE, sizeof(V3N3UV2TB6), &_data[0].nx);
glVertexAttribPointer(_program._uv, 2, GL_FLOAT, GL_FALSE, sizeof(V3N3UV2TB6), &_data[0].u);
glVertexAttribPointer(_program._tagent, 3, GL_FLOAT, GL_FALSE, sizeof(V3N3UV2TB6), &_data[0].tx);
glVertexAttribPointer(_program._biTagent, 3, GL_FLOAT, GL_FALSE, sizeof(V3N3UV2TB6), &_data[0].bx);
glDrawArrays(GL_TRIANGLES, 0, 36);
_program.end();
}
プロジェクトデモソースコード:参照ファイルCubeTBN.hpp CubeTbnProgram.hpp