Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save photex/1def61e37c8cc3249e0cdf9c5b0c43ce to your computer and use it in GitHub Desktop.
Save photex/1def61e37c8cc3249e0cdf9c5b0c43ce to your computer and use it in GitHub Desktop.
Tangent Space just in four bytes (RGBA10_SNORM)
// Copyright 2024, Benjamin 'BeRo' Rosseaux - zlib licensed
////////////////////////////
// QTangent based variant //
////////////////////////////
// The qtangent based variant has a better precision than the octahedron/diamond based variant below.
// 10bit 10bit 9bit for the 3 smaller components of the quaternion and 1bit for the sign of the bitangent and 2bit for the
// largest component index for the reconstruction of the largest component of the quaternion.
// Since the three smallest components of a quaternion are between -1/sqrt(2) and 1/sqrt(2), we can rescale them to -1 .. 1
// while encoding, and then rescale them back to -1/sqrt(2) .. 1/sqrt(2) while decoding, for a better precision.
uint encodeQTangentUI32(mat3 m){
float r = (determinant(m) < 0.0) ? -1.0 : 1.0; // Reflection matrix handling
m[2] *= r;
#if 0
// When the input matrix is always a valid orthogonal tangent space matrix, we can simplify the quaternion calculation to just this:
vec4 q = vec4(m[1][2] - m[2][1], m[2][0] - m[0][2], m[0][1] - m[1][0], 1.0 + m[0][0] + m[1][1] + m[2][2]);
#else
// Otherwise we have to handle all other possible cases as well.
float t = m[0][0] + (m[1][1] + m[2][2]);
vec4 q;
if(t > 2.9999999){
q = vec4(0.0, 0.0, 0.0, 1.0);
}else if(t > 0.0000001){
float s = sqrt(1.0 + t) * 2.0;
q = vec4(vec3(m[1][2] - m[2][1], m[2][0] - m[0][2], m[0][1] - m[1][0]) / s, s * 0.25);
}else if((m[0][0] > m[1][1]) && (m[0][0] > m[2][2])){
float s = sqrt(1.0 + (m[0][0] - (m[1][1] + m[2][2]))) * 2.0;
q = vec4(s * 0.25, vec3(m[1][0] + m[0][1], m[2][0] + m[0][2], m[1][2] - m[2][1]) / s);
}else if(m[1][1] > m[2][2]){
float s = sqrt(1.0 + (m[1][1] - (m[0][0] + m[2][2]))) * 2.0;
q = vec4(vec3(m[1][0] + m[0][1], m[2][1] + m[1][2], m[2][0] - m[0][2]) / s, s * 0.25).xwyz;
}else{
float s = sqrt(1.0 + (m[2][2] - (m[0][0] + m[1][1]))) * 2.0;
q = vec4(vec3(m[2][0] + m[0][2], m[2][1] + m[1][2], m[0][1] - m[1][0]) / s, s * 0.25).xywz;
}
#endif
vec4 qAbs = abs(q = normalize(q));
int maxComponentIndex = (qAbs.x > qAbs.y) ? ((qAbs.x > qAbs.z) ? ((qAbs.x > qAbs.w) ? 0 : 3) : ((qAbs.z > qAbs.w) ? 2 : 3)) : ((qAbs.y > qAbs.z) ? ((qAbs.y > qAbs.w) ? 1 : 3) : ((qAbs.z > qAbs.w) ? 2 : 3));
q.xyz = vec3[4](q.yzw, q.xzw, q.xyw, q.xyz)[maxComponentIndex] * ((q[maxComponentIndex] < 0.0) ? -1.0 : 1.0) * 1.4142135623730951;
return ((uint(round(clamp(q.x * 511.0, -511.0, 511.0) + 512.0)) & 0x3ffu) << 0u) |
((uint(round(clamp(q.y * 511.0, -511.0, 511.0) + 512.0)) & 0x3ffu) << 10u) |
((uint(round(clamp(q.z * 255.0, -255.0, 255.0) + 256.0)) & 0x1ffu) << 20u) |
((uint(((dot(cross(m[0], m[2]), m[1]) * r) < 0.0) ? 1u : 0u) & 0x1u) << 29u) |
((uint(maxComponentIndex) & 0x3u) << 30u);
}
mat3 decodeQTangentUI32(uint v){
vec4 q = vec4(((vec3(ivec3(uvec3((uvec3(v) >> uvec3(0u, 10u, 20u)) & uvec2(0x3ffu, 0x1ffu).xxy)) - ivec2(512, 256).xxy)) / vec2(511.0, 255.0).xxy) * 0.7071067811865475, 0.0);
q.w = sqrt(1.0 - clamp(dot(q.xyz, q.xyz), 0.0, 1.0));
q = normalize(vec4[4](q.wxyz, q.xwyz, q.xywz, q.xyzw)[uint((v >> 30u) & 0x3u)]);
vec3 t2 = q.xyz * 2.0, tx = q.xxx * t2.xyz, ty = q.yyy * t2.xyz, tz = q.www * t2.xyz;
vec3 tangent = vec3(1.0 - (ty.y + (q.z * t2.z)), tx.y + tz.z, tx.z - tz.y);
vec3 normal = vec3(tx.z + tz.y, ty.z - tz.x, 1.0 - (tx.x + ty.y));
return mat3(tangent, cross(tangent, normal) * (((v & (1u << 29u)) != 0u) ? -1.0 : 1.0), normal);
}
// Decodes the UI32 encoded qtangent into a unpacked qtangent for further processing like vertex interpolation and so on
vec4 decodeQTangentUI32Raw(uint v){
vec4 q = vec4(((vec3(ivec3(uvec3((uvec3(v) >> uvec3(0u, 10u, 20u)) & uvec2(0x3ffu, 0x1ffu).xxy)) - ivec2(512, 256).xxy)) / vec2(511.0, 255.0).xxy) * 0.7071067811865475, 0.0);
q.w = sqrt(1.0 - clamp(dot(q.xyz, q.xyz), 0.0, 1.0));
return normalize(vec4[4](q.wxyz, q.xwyz, q.xywz, q.xyzw)[uint((v >> 30u) & 0x3u)]) * (((v & (1u << 29u)) != 0u) ? -1.0 : 1.0);
}
// Constructs a TBN matrix from a unpacked qtangent for example for after vertex interpolation in the fragment shader
mat3 constructTBNFromQTangent(vec4 q){
q = normalize(q); // Ensure that the quaternion is normalized in case it is not, for example after interpolation and so on
vec3 t2 = q.xyz * 2.0, tx = q.xxx * t2.xyz, ty = q.yyy * t2.xyz, tz = q.www * t2.xyz;
vec3 tangent = vec3(1.0 - (ty.y + (q.z * t2.z)), tx.y + tz.z, tx.z - tz.y);
vec3 normal = vec3(tx.z + tz.y, ty.z - tz.x, 1.0 - (tx.x + ty.y));
return mat3(tangent, cross(tangent, normal) * ((q.w < 0.0) ? -1.0 : 1.0), normal);
}
//////////////////////////////////////
// Octahedron/Diamond based variant //
//////////////////////////////////////
/*
** Encoding and decoding functions from tangent space vectors to a single 32-bit unsigned integer (four bytes) in
** RGB10A2_SNORM format and back.
**
** These functions are used to encode and decode tangent space vectors into a single 32-bit unsigned integer.
** The encoding is done using the RGB10A2 snorm format, which allows to store the tangent space in a single integer.
** The encoding is lossy, but the loss is very small and the precision is enough for most use cases.
**
** The encoding is done as follows:
** 1. The normal is projected onto the octahedron, which is a 2D shape that represents the normal in a more efficient way.
** 2. The tangent is projected onto the canonical diamond space, which is a 2D space that is aligned with the normal.
** 3. The tangent is projected onto the tangent diamond, which is a 1D space that represents the tangent in a more efficient way.
** 4. The bitangent sign is stored in signed 2 bits as -1.0 or 1.0.
** 5. The values are packed into a single 32-bit unsigned integer using the RGB10A2 snorm format.
**
** The decoding is done as follows:
** 1. The values are unpacked from the RGB10A2 snorm format.
** 2. The normal is decoded from the octahedron.
** 3. The canonical directions are found.
** 4. The tangent diamond is decoded.
** 5. The tangent is found using the canonical directions and the tangent diamond.
** 6. The bitangent is found using the normal, the tangent and the bitangent sign.
**
** Idea based on https://www.jeremyong.com/graphics/2023/01/09/tangent-spaces-and-diamond-encoding/ but with improvements for
** packing into RGB10A2 snorm to a 32-bit unsigned integer.
**
**/
uint encodeTangentSpaceAsRGB10A2SNorm(mat3 tbn){
// Normalize tangent space vectors, just for the sake of clarity and for to be sure
tbn[0] = normalize(tbn[0]);
tbn[1] = normalize(tbn[1]);
tbn[2] = normalize(tbn[2]);
// Get the octahedron normal
const vec3 normal = tbn[2];
vec2 octahedronalNormal = normal.xy / (abs(normal.x) + abs(normal.y) + abs(normal.z));
octahedronalNormal = (normal.z < 0.0) ? ((1.0 - abs(octahedronalNormal.yx)) * fma(step(vec2(0.0), octahedronalNormal.xy), vec2(2.0), vec2(-1.0))) : octahedronalNormal;
// Find the canonical directions
vec3 canonicalDirectionA = cross(normalize(normal.zxy - dot(normal.zxy, normal)), normal);
vec3 canonicalDirectionB = cross(normal, canonicalDirectionA);
// Project the tangent into the canonical space
const vec2 tangentInCanonicalSpace = vec2(dot(tbn[0], canonicalDirectionA), dot(tbn[0], canonicalDirectionB));
// Find the tangent diamond direction (a diamond is more or less the 2D equivalent of the 3D octahedron here in this case)
const float tangentDiamond = (1.0 - (tangentInCanonicalSpace.x / (abs(tangentInCanonicalSpace.x) + abs(tangentInCanonicalSpace.y)))) * ((tangentInCanonicalSpace.y < 0.0) ? -1.0 : 1.0) * 0.5;
// Find the bitangent sign
const float bittangentSign = (dot(cross(tbn[0], tbn[1]), tbn[2]) < 0.0) ? -1.0 : 1.0;
// Encode the tangent space as signed values
const ivec4 encodedTangentSpace = ivec4(
ivec2(clamp(octahedronalNormal, vec2(-1.0), vec2(1.0)) * 511.0), // 10 bits including sign
int(clamp(tangentDiamond, -1.0, 1.0) * 511.0), // 10 bits including sign
int(clamp(bittangentSign, -1.0, 1.0)) // 2 bits
);
// Pack the values into RGB10A2 snorm
return ((uint(encodedTangentSpace.x) & 0x3ffu) << 0u) |
((uint(encodedTangentSpace.y) & 0x3ffu) << 10u) |
((uint(encodedTangentSpace.z) & 0x3ffu) << 20u) |
((uint(encodedTangentSpace.w) & 0x3u) << 30u);
}
mat3 decodeTangentSpaceFromRGB10A2SNorm(const in uint encodedTangentSpace){
// Unpack the values from RGB10A2 snorm
const ivec4 encodedTangentSpaceUnpacked = ivec4(
int(uint(encodedTangentSpace << 22u)) >> 22,
int(uint(encodedTangentSpace << 12u)) >> 22,
int(uint(encodedTangentSpace << 2u)) >> 22,
int(uint(encodedTangentSpace << 0u)) >> 30
);
// Decode the tangent space
const vec2 octahedronalNormal = vec2(encodedTangentSpaceUnpacked.xy) / 511.0;
vec3 normal = vec3(octahedronalNormal, 1.0 - (abs(octahedronalNormal.x) + abs(octahedronalNormal.y)));
normal = normalize((normal.z < 0.0) ? vec3((1.0 - abs(normal.yx)) * fma(step(vec2(0.0), normal.xy), vec2(2.0), vec2(-1.0)), normal.z) : normal);
// Find the canonical directions
vec3 canonicalDirectionA = cross(normalize(normal.zxy - dot(normal.zxy, normal)), normal);
vec3 canonicalDirectionB = cross(normal, canonicalDirectionA);
// Decode the tangent diamond direction
const float tangentDiamond = float(encodedTangentSpaceUnpacked.z) / 511.0;
const float tangentDiamondSign = (tangentDiamond < 0.0) ? -1.0 : 1.0; // No sign() because for 0.0 in => 1.0 out
vec2 tangentInCanonicalSpace;
tangentInCanonicalSpace.x = 1.0 - (tangentDiamond * tangentDiamondSign * 2.0);
tangentInCanonicalSpace.y = tangentDiamondSign * (1.0 - abs(tangentInCanonicalSpace.x));
tangentInCanonicalSpace = normalize(tangentInCanonicalSpace);
// Decode the tangent
const vec3 tangent = normalize((tangentInCanonicalSpace.x * canonicalDirectionA) + (tangentInCanonicalSpace.y * canonicalDirectionB));
// Decode the bitangent
const vec3 bitangent = normalize(cross(normal, tangent) * float(encodedTangentSpaceUnpacked.w));
return mat3(tangent, bitangent, normal);
}
// Copyright 2024, Benjamin 'BeRo' Rosseaux - zlib licensed
////////////////////////////
// QTangent based variant //
////////////////////////////
// The qtangent based variant has a better precision than the octahedron/diamond based variant below.
// 10bit 10bit 9bit for the 3 smaller components of the quaternion and 1bit for the sign of the bitangent and 2bit for the
// largest component index for the reconstruction of the largest component of the quaternion.
// Since the three smallest components of a quaternion are between -1/sqrt(2) and 1/sqrt(2), we can rescale them to -1 .. 1
// while encoding, and then rescale them back to -1/sqrt(2) .. 1/sqrt(2) while decoding, for a better precision.
function EncodeQTangentUI32(const aTangent,aBitangent:TpvVector3;aNormal:TpvVector3):TpvUInt32;
var Scale,t,s:TpvScalar;
q:TpvVector4;
AbsQ:TpvVector4;
MaxComponentIndex:TpvInt32;
begin
if ((((((aTangent.x*aBitangent.y*aNormal.z)+
(aTangent.y*aBitangent.z*aNormal.x)
)+
(aTangent.z*aBitangent.x*aNormal.y)
)-
(aTangent.z*aBitangent.y*aNormal.x)
)-
(aTangent.y*aBitangent.x*aNormal.z)
)-
(aTangent.x*aBitangent.z*aNormal.y)
)<0.0 then begin
// Reflection matrix, so flip y axis in case the tangent frame encodes a reflection
Scale:=-1.0;
aNormal:=-aNormal;
end else begin
// Rotation matrix, so nothing is doing to do
Scale:=1.0;
end;
t:=aTangent.x+(aBitangent.y+aNormal.z);
if t>2.9999999 then begin
q:=TpvVector4.InlineableCreate(0.0,0.0,0.0,1.0);
end else if t>0.0000001 then begin
s:=sqrt(1.0+t)*2.0;
q:=TpvVector4.InlineableCreate(TpvVector3.InlineableCreate(aBitangent.z-aNormal.y,aNormal.x-aTangent.z,aTangent.y-aBitangent.x)/s,s*0.25).Normalize;
end else if (aTangent.x>aBitangent.y) and (aTangent.x>aNormal.z) then begin
s:=sqrt(1.0+(aTangent.x-(aBitangent.y+aNormal.z)))*2.0;
q:=TpvVector4.InlineableCreate(TpvVector3.InlineableCreate(aBitangent.x+aTangent.y,aNormal.x+aTangent.z,aBitangent.z-aNormal.y)/s,s*0.25).wxyz.Normalize;
end else if aBitangent.y>aNormal.z then begin
s:=sqrt(1.0+(aBitangent.y-(aTangent.x+aNormal.z)))*2.0;
q:=TpvVector4.InlineableCreate(TpvVector3.InlineableCreate(aBitangent.x+aTangent.y,aNormal.y+aBitangent.z,aNormal.x-aTangent.z)/s,s*0.25).xwyz.Normalize;
end else begin
s:=sqrt(1.0+(aNormal.z-(aTangent.x+aBitangent.y)))*2.0;
q:=TpvVector4.InlineableCreate(TpvVector3.InlineableCreate(aNormal.x+aTangent.z,aNormal.y+aBitangent.z,aTangent.y-aBitangent.x)/s,s*0.25).xywz.Normalize;
end;
AbsQ:=q.Abs;
if AbsQ.x>AbsQ.y then begin
if AbsQ.x>AbsQ.z then begin
if AbsQ.x>AbsQ.w then begin
MaxComponentIndex:=0;
end else begin
MaxComponentIndex:=3;
end;
end else begin
if AbsQ.z>AbsQ.w then begin
MaxComponentIndex:=2;
end else begin
MaxComponentIndex:=3;
end;
end;
end else begin
if AbsQ.y>AbsQ.z then begin
if AbsQ.y>AbsQ.w then begin
MaxComponentIndex:=1;
end else begin
MaxComponentIndex:=3;
end;
end else begin
if AbsQ.z>AbsQ.w then begin
MaxComponentIndex:=2;
end else begin
MaxComponentIndex:=3;
end;
end;
end;
case MaxComponentIndex of
0:begin
q:=q.yzwx;
end;
1:begin
q:=q.xzwy;
end;
2:begin
q:=q.xywz;
end;
else {3:}begin
q:=q.xyzw;
end;
end;
q.xyz:=q.xyz*1.4142135623730951;
if q.w<0.0 then begin
q:=-q;
end;
result:=((TpvUInt32(round(clamp(q.x*511.0,-511.0,511.0)+512.0)) and $3ff) shl 0) or
((TpvUInt32(round(clamp(q.y*511.0,-511.0,511.0)+512.0)) and $3ff) shl 10) or
((TpvUInt32(round(clamp(q.z*255.0,-255.0,255.0)+256.0)) and $1ff) shl 20) or
(TpvUInt32(Ord((aTangent.Cross(aNormal).Dot(aBitangent)*Scale)<0.0) and 1) shl 29) or
((TpvUInt32(MaxComponentIndex and 3) shl 30));
end;
function EncodeQTangentUI32(const aMatrix:TpvMatrix3x3):TpvUInt32;
begin
result:=EncodeQTangentUI32(aMatrix.Tangent,aMatrix.Bitangent,aMatrix.Normal);
end;
function EncodeQTangentUI32(const aMatrix:TpvMatrix4x4):TpvUInt32;
begin
result:=EncodeQTangentUI32(aMatrix.Tangent.xyz,aMatrix.Bitangent.xyz,aMatrix.Normal.xyz);
end;
procedure DecodeQTangentUI32Vectors(const aValue:TpvUInt32;out aTangent,aBitangent,aNormal:TpvVector3);
const DivVector3:TpvVector3=(x:511.0;y:511.0;z:255.0);
var q:TpvVector4;
t2,tx,ty,tz:TpvVector3;
begin
q:=TpvVector4.InlineableCreate((TpvVector3.InlineableCreate(
TpvInt32((aValue shr 0) and $3ff)-512,
TpvInt32((aValue shr 10) and $3ff)-512,
TpvInt32((aValue shr 20) and $1ff)-256
)/DivVector3)*0.7071067811865475,0.0);
q.w:=sqrt(1.0-Clamp(q.xyz.SquaredLength,0.0,1.0));
case (aValue shr 30) and 3 of
0:begin
q:=q.wxyz.Normalize;
end;
1:begin
q:=q.xwyz.Normalize;
end;
2:begin
q:=q.xywz.Normalize;
end;
else {3:}begin
q:=q.xyzw.Normalize;
end;
end;
t2:=q.xyz*2.0;
tx:=q.xxx*t2.xyz;
ty:=q.yyy*t2.xyz;
tz:=q.www*t2.xyz;
aTangent:=TpvVector3.InlineableCreate(1.0-(ty.y+(q.z*t2.z)),tx.y+tz.z,tx.z-tz.y).Normalize;
aNormal:=TpvVector3.InlineableCreate(tx.z+tz.y,ty.z-tz.x,1.0-(tx.x+ty.y)).Normalize;
aBitangent:=aTangent.Cross(aNormal)*TpvScalar(TpvInt32(1-((Ord((aValue and (TpvUInt32(1) shl 29))<>0) and 1) shl 1)));
end;
function DecodeQTangentUI32(const aValue:TpvUInt32):TpvMatrix3x3;
begin
DecodeQTangentUI32Vectors(aValue,result.Tangent,result.Bitangent,result.Normal);
end;
//////////////////////////////////////
// Octahedron/Diamond based variant //
//////////////////////////////////////
(*
** Encoding and decoding functions from tangent space vectors to a single 32-bit unsigned integer (four bytes) in
** RGB10A2_SNORM format and back.
**
** These functions are used to encode and decode tangent space vectors into a single 32-bit unsigned integer.
** The encoding is done using the RGB10A2 snorm format, which allows to store the tangent space in a single integer.
** The encoding is lossy, but the loss is very small and the precision is enough for most use cases.
**
** The encoding is done as follows:
** 1. The normal is projected onto the octahedron, which is a 2D shape that represents the normal in a more efficient way.
** 2. The tangent is projected onto the canonical diamond space, which is a 2D space that is aligned with the normal.
** 3. The tangent is projected onto the tangent diamond, which is a 1D space that represents the tangent in a more efficient way.
** 4. The bitangent sign is stored in signed 2 bits as -1.0 or 1.0.
** 5. The values are packed into a single 32-bit unsigned integer using the RGB10A2 snorm format.
**
** The decoding is done as follows:
** 1. The values are unpacked from the RGB10A2 snorm format.
** 2. The normal is decoded from the octahedron.
** 3. The canonical directions are found.
** 4. The tangent diamond is decoded.
** 5. The tangent is found using the canonical directions and the tangent diamond.
** 6. The bitangent is found using the normal, the tangent and the bitangent sign.
**
** Idea based on https://www.jeremyong.com/graphics/2023/01/09/tangent-spaces-and-diamond-encoding/ but with improvements for
** packing into RGB10A2 snorm to a 32-bit unsigned integer.
**
*)
function EncodeAsRGB10A2SNorm(const aVector:TpvVector4):TpvUInt32;
var r,g,b,a:TpvUInt32;
begin
r:=TpvUInt32(TpvInt32(round(Min(Max(aVector.r,-1.0),1.0)*511.0)));
g:=TpvUInt32(TpvInt32(round(Min(Max(aVector.g,-1.0),1.0)*511.0)));
b:=TpvUInt32(TpvInt32(round(Min(Max(aVector.b,-1.0),1.0)*511.0)));
a:=TpvUInt32(TpvInt32(round(Min(Max(aVector.a,-1.0),1.0)*1.0)));
result:=(r and $3ff) or ((g and $3ff) shl 10) or ((b and $3ff) shl 20) or ((a and 3) shl 30);
end;
function DecodeFromRGB10A2SNorm(const aValue:TpvUInt32):TpvVector4;
var r,g,b,a:TpvUInt32;
begin
{$if declared(SARLongint)}
// More efficient version
// Extract the red, green, blue and alpha components, together with sign extension
r:=TpvUInt32(TpvInt32(SARLongint(TpvInt32(TpvUInt32(aValue shl 22)),22)));
g:=TpvUInt32(TpvInt32(SARLongint(TpvInt32(TpvUInt32(aValue shl 12)),22)));
b:=TpvUInt32(TpvInt32(SARLongint(TpvInt32(TpvUInt32(aValue shl 2)),22)));
a:=TpvUInt32(TpvInt32(SARLongint(TpvInt32(TpvUInt32(aValue shl 0)),30)));
{$else}
// Fallback version when SARLongint is not available for artithmetic right shiftings, and it is the even more readable
// reference version at the same time.
// Extract the red, green, blue and alpha components
r:=(aValue shr 0) and $3ff;
g:=(aValue shr 10) and $3ff;
b:=(aValue shr 20) and $3ff;
a:=(aValue shr 30) and 3;
// Sign extend the red, green and blue components
if (r and $200)<>0 then begin
r:=r or $fffffc00;
end;
if (g and $200)<>0 then begin
g:=g or $fffffc00;
end;
if (b and $200)<>0 then begin
b:=b or $fffffc00;
end;
if (a and 2)<>0 then begin
a:=a or $fffffffc;
end;
{$ifend}
// Normalize the red, green, blue and alpha components
result.r:=TpvInt32(r)/511.0;
result.g:=TpvInt32(g)/511.0;
result.b:=TpvInt32(b)/511.0;
result.a:=TpvInt32(a){/1.0}; // No need to normalize the alpha component, because it is already normalized
end;
function OctahedralProjectionMappingSignedEncode(const aVector:TpvVector3):TpvVector2;
var Vector:TpvVector3;
begin
Vector:=aVector.Normalize;
result:=Vector.xy/(abs(Vector.x)+abs(Vector.y)+abs(Vector.z));
if Vector.z<0.0 then begin
result:=(TpvVector2.InlineableCreate(1.0,1.0)-result.yx.Abs)*
TpvVector2.InlineableCreate(SignNonZero(result.x),SignNonZero(result.y));
end;
end;
function OctahedralProjectionMappingSignedDecode(const aVector:TpvVector2):TpvVector3;
begin
result:=TpvVector3.InlineableCreate(aVector.xy,(1.0-abs(aVector.x))-abs(aVector.y));
if result.z<0 then begin
result.xy:=(TpvVector2.InlineableCreate(1.0,1.0)-result.yx.Abs)*TpvVector2.InlineableCreate(SignNonZero(result.x),SignNonZero(result.y));
end;
result:=result.Normalize;
end;
function EncodeDiamondSigned(const aVector:TpvVector2):TpvScalar;
begin
result:=(1.0-(aVector.x/(abs(aVector.x)+abs(aVector.y))))*SignNonZero(aVector.y)*0.5;
end;
function DecodeDiamondSigned(const aValue:TpvScalar):TpvVector2;
var SignPMinusHalf,x,y:TpvScalar;
begin
SignPMinusHalf:=SignNonZero(aValue);
x:=1.0-(aValue*SignPMinusHalf*2.0);
y:=SignPMinusHalf*(1.0-abs(x));
result:=TpvVector2.InlineableCreate(x,y).Normalize;
end;
function EncodeTangentSpaceAsRGB10A2SNorm(const aTangent,aBitangent,aNormal:TpvVector3):TpvUInt32;
var OctahedronNormal,TangentInCanonicalSpace:TpvVector2;
Normal,Tangent,CanonicalDirectionA,CanonicalDirectionB:TpvVector3;
TangentDiamond,BitangentSign:TpvScalar;
TemporaryVector4:TpvVector4;
begin
Normal:=aNormal.Normalize;
Tangent:=aTangent.Normalize;
// Encode the normal as octahedron normal
OctahedronNormal:=OctahedralProjectionMappingSignedEncode(Normal);
// Find the canonical directions
CanonicalDirectionA:=(Normal.zxy-(Normal.zxy.Dot(Normal))).Normalize.Cross(Normal);
CanonicalDirectionB:=Normal.Cross(CanonicalDirectionA);
TangentInCanonicalSpace:=TpvVector2.InlineableCreate(Tangent.Dot(CanonicalDirectionA),Tangent.Dot(CanonicalDirectionB));
TangentDiamond:=EncodeDiamondSigned(TangentInCanonicalSpace);
BitangentSign:=SignNonZero(Normal.Cross(Tangent).Dot(aBitangent));
TemporaryVector4:=TpvVector4.InlineableCreate(OctahedronNormal.x,OctahedronNormal.y,TangentDiamond,BitangentSign);
result:=EncodeAsRGB10A2SNorm(TemporaryVector4);
end;
function EncodeTangentSpaceAsRGB10A2SNorm(const aMatrix:TpvMatrix3x3):TpvUInt32;
begin
result:=EncodeTangentSpaceAsRGB10A2SNorm(aMatrix.Tangent,aMatrix.Bitangent,aMatrix.Normal);
end;
procedure DecodeTangentSpaceFromRGB10A2SNorm(const aValue:TpvUInt32;out aTangent,aBitangent,aNormal:TpvVector3);
var TemporaryVector4:TpvVector4;
OctahedronNormal,TangentInCanonicalSpace:TpvVector2;
Normal,Tangent,CanonicalDirectionA,CanonicalDirectionB:TpvVector3;
begin
TemporaryVector4:=DecodeFromRGB10A2SNorm(aValue);
OctahedronNormal:=TemporaryVector4.xy;
Normal:=OctahedralProjectionMappingSignedDecode(OctahedronNormal);
// Find the canonical directions
CanonicalDirectionA:=(Normal.zxy-(Normal.zxy.Dot(Normal))).Normalize.Cross(Normal);
CanonicalDirectionB:=Normal.Cross(CanonicalDirectionA);
TangentInCanonicalSpace:=DecodeDiamondSigned(TemporaryVector4.z);
Tangent:=((CanonicalDirectionA*TangentInCanonicalSpace.x)+(CanonicalDirectionB*TangentInCanonicalSpace.y)).Normalize;
aTangent:=Tangent;
aBitangent:=Normal.Cross(Tangent).Normalize*TemporaryVector4.w;
aNormal:=Normal;
end;
procedure DecodeTangentSpaceFromRGB10A2SNorm(const aValue:TpvUInt32;out aMatrix3x3:TpvMatrix3x3);
var Tangent,Bitangent,Normal:TpvVector3;
begin
DecodeTangentSpaceFromRGB10A2SNorm(aValue,Tangent,Bitangent,Normal);
aMatrix3x3.Tangent:=Tangent;
aMatrix3x3.Bitangent:=Bitangent;
aMatrix3x3.Normal:=Normal;
end;
Copyright 2024, Benjamin 'BeRo' Rosseaux - zlib licensed
# Encoding and decoding functions from tangent space vectors to a single 32-bit unsigned integer (four bytes) in RGB10A2_SNORM format and back.
These functions are used to encode and decode tangent space vectors into a single 32-bit unsigned integer.
The encoding is done using the RGB10A2 snorm format, which allows to store the tangent space in a single integer.
The encoding is lossy, but the loss is very small and the precision is enough for most use cases.
## The encoding is done as follows:
1. The normal is projected onto the octahedron, which is a 2D shape that represents the normal in a more efficient way.
2. The tangent is projected onto the canonical diamond space, which is a 2D space that is aligned with the normal.
3. The tangent is projected onto the tangent diamond, which is a 1D space that represents the tangent in a more efficient way.
4. The bitangent sign is stored in signed 2 bits as -1.0 or 1.0.
5. The values are packed into a single 32-bit unsigned integer using the RGB10A2 snorm format.
## The decoding is done as follows:
1. The values are unpacked from the RGB10A2 snorm format.
2. The normal is decoded from the octahedron.
3. The canonical directions are found.
4. The tangent diamond is decoded.
5. The tangent is found using the canonical directions and the tangent diamond.
6. The bitangent is found using the normal, the tangent and the bitangent sign.
## Additional information
Idea based on https://www.jeremyong.com/graphics/2023/01/09/tangent-spaces-and-diamond-encoding/ but with improvements for
packing into RGB10A2 snorm to a 32-bit unsigned integer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment