-
-
Save photex/1def61e37c8cc3249e0cdf9c5b0c43ce to your computer and use it in GitHub Desktop.
Tangent Space just in four bytes (RGBA10_SNORM)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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