Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
305 changes: 305 additions & 0 deletions include/nbl/builtin/hlsl/math/quaternions.hlsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
// Copyright (C) 2018-2025 - DevSH Graphics Programming Sp. z O.O.
// This file is part of the "Nabla Engine".
// For conditions of distribution and use, see copyright notice in nabla.h
#ifndef _NBL_BUILTIN_HLSL_MATH_QUATERNIONS_INCLUDED_
#define _NBL_BUILTIN_HLSL_MATH_QUATERNIONS_INCLUDED_

#include "nbl/builtin/hlsl/cpp_compat.hlsl"
#include "nbl/builtin/hlsl/tgmath.hlsl"

namespace nbl
{
namespace hlsl
{
namespace math
{

template<typename T>
struct truncated_quaternion
{
using this_t = truncated_quaternion<T>;
using scalar_type = T;
using data_type = vector<T, 3>;

static this_t create()
{
this_t q;
q.data = data_type(0.0, 0.0, 0.0);
return q;
}

data_type data;
};

template <typename T>
struct quaternion
{
using this_t = quaternion<T>;
using scalar_type = T;
using data_type = vector<T, 4>;
using vector3_type = vector<T, 3>;
using matrix_type = matrix<T, 3, 3>;

using AsUint = typename unsigned_integer_of_size<sizeof(scalar_type)>::type;

static this_t create()
{
this_t q;
q.data = data_type(0.0, 0.0, 0.0, 1.0);
return q;
}
Comment on lines +45 to +50

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think the create overloads need to have their argument types templated with NBL_FUNC_REQUIRES because DXC will screw us over with implicit conversions


static this_t create(scalar_type x, scalar_type y, scalar_type z, scalar_type w)
{
this_t q;
q.data = data_type(x, y, z, w);
return q;
}
Comment on lines +52 to +57

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imho this is more painful than just

math::quaternion q;
q.data = vector_of_xyzw;

delete this pseudo-constructor


static this_t create(NBL_CONST_REF_ARG(this_t) other)
{
return other;
}
Comment on lines +59 to +62

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need this


// angle: Rotation angle expressed in radians.
// axis: Rotation axis, must be normalized.
static this_t create(scalar_type angle, const vector3_type axis)
Comment on lines +64 to +66

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we have axis first, then angle ?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw you can also throw in a uniformScale = 1.f into the mix :P

{
this_t q;
const scalar_type sinTheta = hlsl::sin(angle * 0.5);
const scalar_type cosTheta = hlsl::cos(angle * 0.5);
q.data = data_type(axis * sinTheta, cosTheta);
return q;
}


static this_t create(scalar_type pitch, scalar_type yaw, scalar_type roll)
{
const scalar_type rollDiv2 = roll * scalar_type(0.5);
const scalar_type sr = hlsl::sin(rollDiv2);
const scalar_type cr = hlsl::cos(rollDiv2);

const scalar_type pitchDiv2 = pitch * scalar_type(0.5);
const scalar_type sp = hlsl::sin(pitchDiv2);
const scalar_type cp = hlsl::cos(pitchDiv2);

const scalar_type yawDiv2 = yaw * scalar_type(0.5);
const scalar_type sy = hlsl::sin(yawDiv2);
const scalar_type cy = hlsl::cos(yawDiv2);

this_t output;
output.data[0] = cr * sp * cy + sr * cp * sy; // x
output.data[1] = cr * cp * sy - sr * sp * cy; // y
output.data[2] = sr * cp * cy - cr * sp * sy; // z
output.data[3] = cr * cp * cy + sr * sp * sy; // w

return output;
}
Comment on lines +76 to +97

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd break this down into two functions

template<typename T=vector<scalar_type,2> > requires is_same_v<vector<scalar_type,2>,T>
static this_t create(const T halfPitchCosSin, const T halfYawCosSin, const T halfRollCosSin);

template<typename T=scalar_type> requires is_same_v<scalar_type,T>
static this_t create(const T pitch, const T yaw, const T roll);


static this_t create(NBL_CONST_REF_ARG(matrix_type) m)
{
const scalar_type m00 = m[0][0], m11 = m[1][1], m22 = m[2][2];
const scalar_type neg_m00 = bit_cast<scalar_type>(bit_cast<AsUint>(m00)^0x80000000u);
const scalar_type neg_m11 = bit_cast<scalar_type>(bit_cast<AsUint>(m11)^0x80000000u);
const scalar_type neg_m22 = bit_cast<scalar_type>(bit_cast<AsUint>(m22)^0x80000000u);
const data_type Qx = data_type(m00, m00, neg_m00, neg_m00);
const data_type Qy = data_type(m11, neg_m11, m11, neg_m11);
const data_type Qz = data_type(m22, neg_m22, neg_m22, m22);

const data_type tmp = hlsl::promote<data_type>(1.0) + Qx + Qy + Qz;
const data_type invscales = hlsl::promote<data_type>(0.5) / hlsl::sqrt(tmp);
const data_type scales = tmp * invscales * hlsl::promote<data_type>(0.5);

// TODO: speed this up
this_t retval;
if (tmp.x > scalar_type(0.0))
{
retval.data.x = (m[2][1] - m[1][2]) * invscales.x;
retval.data.y = (m[0][2] - m[2][0]) * invscales.x;
retval.data.z = (m[1][0] - m[0][1]) * invscales.x;
retval.data.w = scales.x;
}
else
{
if (tmp.y > scalar_type(0.0))
{
retval.data.x = scales.y;
retval.data.y = (m[0][1] + m[1][0]) * invscales.y;
retval.data.z = (m[2][0] + m[0][2]) * invscales.y;
retval.data.w = (m[2][1] - m[1][2]) * invscales.y;
}
else if (tmp.z > scalar_type(0.0))
{
retval.data.x = (m[0][1] + m[1][0]) * invscales.z;
retval.data.y = scales.z;
retval.data.z = (m[0][2] - m[2][0]) * invscales.z;
retval.data.w = (m[1][2] + m[2][1]) * invscales.z;
}
else
{
retval.data.x = (m[0][2] + m[2][0]) * invscales.w;
retval.data.y = (m[1][2] + m[2][1]) * invscales.w;
retval.data.z = scales.w;
retval.data.w = (m[1][0] - m[0][1]) * invscales.w;
}
}

retval.data = hlsl::normalize(retval.data);
return retval;
}
Comment on lines +99 to +149

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only an orthogonal (no skew) and uniform scale matrix can be converted to a quaternion.

so dot products between the rows and columns should be 0, and the dot product of each column with itself should have the same length

You can write the whole algorithm assuming this, so invscales would be a scalar.

You could assert the orthogonality (no skew) and uniform scaling property OR take an extra const bool dontAssertValidMatrix=false where instead of asserting you return a NaN filled quaternion if the matrix is not valid


static this_t create(NBL_CONST_REF_ARG(truncated_quaternion<T>) first3Components)
{
this_t retval;
retval.data.xyz = first3Components.data;
retval.data.w = hlsl::sqrt(scalar_type(1.0) - hlsl::dot(first3Components.data, first3Components.data));
return retval;
}
Comment on lines +151 to +157

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

put this wholly inside the static_cast from truncated to full quaternion


this_t operator*(scalar_type scalar)
{
this_t output;
output.data = data * scalar;
return output;
}

this_t operator*(NBL_CONST_REF_ARG(this_t) other)
{
return this_t::create(
data.w * other.data.w - data.x * other.x - data.y * other.data.y - data.z * other.data.z,
data.w * other.data.x + data.x * other.w + data.y * other.data.z - data.z * other.data.y,
data.w * other.data.y - data.x * other.z + data.y * other.data.w + data.z * other.data.x,
data.w * other.data.z + data.x * other.y - data.y * other.data.x + data.z * other.data.w
);
}

static this_t lerp(const this_t start, const this_t end, const scalar_type fraction, const scalar_type totalPseudoAngle)
{
const AsUint negationMask = hlsl::bit_cast<AsUint>(totalPseudoAngle) & AsUint(0x80000000u);
const data_type adjEnd = hlsl::bit_cast<scalar_type>(hlsl::bit_cast<AsUint>(end.data) ^ negationMask);

this_t retval;
retval.data = hlsl::mix(start.data, adjEnd, fraction);
Comment on lines +178 to +182

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please unit test/check it somehow (re-derive) I'm not sure that when totalPseudoAngle<0 the result is meant to be S+(-E-S)*fraction and not `S+(E-S)*(-fraction)

P.S. btw my recent learnings about GPU architectures have made me aware that there's more FP32 cores than INT32 on Nvidia so write a comment to benchmark the sign xor flip against just *sign(totalPseudoAngle)

return retval;
}

static this_t lerp(const this_t start, const this_t end, const scalar_type fraction)
{
return lerp(start, end, fraction, hlsl::dot(start.data, end.data));
}

static scalar_type __adj_interpolant(const scalar_type angle, const scalar_type fraction, const scalar_type interpolantPrecalcTerm2, const scalar_type interpolantPrecalcTerm3)
{
const scalar_type A = scalar_type(1.0904) + angle * (scalar_type(-3.2452) + angle * (scalar_type(3.55645) - angle * scalar_type(1.43519)));
const scalar_type B = scalar_type(0.848013) + angle * (scalar_type(-1.06021) + angle * scalar_type(0.215638));
const scalar_type k = A * interpolantPrecalcTerm2 + B;
return fraction + interpolantPrecalcTerm3 * k;
}

static this_t flerp(const this_t start, const this_t end, const scalar_type fraction)
{
const scalar_type pseudoAngle = hlsl::dot(start.data,end.data);
const scalar_type interpolantPrecalcTerm = fraction - scalar_type(0.5);
const scalar_type interpolantPrecalcTerm3 = fraction * interpolantPrecalcTerm * (fraction - scalar_type(1.0));
const scalar_type adjFrac = __adj_interpolant(hlsl::abs(pseudoAngle),fraction,interpolantPrecalcTerm*interpolantPrecalcTerm,interpolantPrecalcTerm3);

this_t retval = lerp(start,end,adjFrac,pseudoAngle);
retval.data = hlsl::normalize(retval.data);
return retval;
}
Comment on lines +186 to +209

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

write a comment that lerp does not resepect scales, can stick an assert in lerp that start and end are both unit length

also make behaviour consistent, either lerp and flerp both normalize or both don't normalize the result (in case you go with normalization, make unnormLerp and unnormFlerp)


vector3_type transformVector(const vector3_type v)
{
scalar_type scale = hlsl::length(data);
vector3_type direction = data.xyz;
return v * scale + hlsl::cross(direction, v * data.w + hlsl::cross(direction, v)) * scalar_type(2.0);
}
Comment on lines +211 to +216

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a const bool assumeNoScale=false argument


matrix_type constructMatrix()
{
matrix_type mat;
mat[0] = data.yzx * data.ywz + data.zxy * data.zyw * vector3_type( 1.0, 1.0,-1.0);
mat[1] = data.yzx * data.xzw + data.zxy * data.wxz * vector3_type(-1.0, 1.0, 1.0);
mat[2] = data.yzx * data.wyx + data.zxy * data.xwy * vector3_type( 1.0,-1.0, 1.0);
mat[0][0] = scalar_type(0.5) - mat[0][0];
mat[1][1] = scalar_type(0.5) - mat[1][1];
mat[2][2] = scalar_type(0.5) - mat[2][2];
mat *= scalar_type(2.0);
return hlsl::transpose(mat); // TODO: double check transpose?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes please double check the tranpose, best add some unit tests to example 22 where random vectors get rotated by random quaternions, and you check the result is same as making a mat3 out of the quaternion and mul(matrix,inputVector)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Przemog1 can advise/help

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, also example 22 doesn't compile on master currently, for now just comment out the code that doesn't work

}

static vector3_type slerp_delta(const vector3_type start, const vector3_type preScaledWaypoint, scalar_type cosAngleFromStart)
{
vector3_type planeNormal = hlsl::cross(start,preScaledWaypoint);

cosAngleFromStart *= scalar_type(0.5);
const scalar_type sinAngle = hlsl::sqrt(scalar_type(0.5) - cosAngleFromStart);
const scalar_type cosAngle = hlsl::sqrt(scalar_type(0.5) + cosAngleFromStart);

planeNormal *= sinAngle;
const vector3_type precompPart = hlsl::cross(planeNormal, start) * scalar_type(2.0);

return precompPart * cosAngle + hlsl::cross(planeNormal, precompPart);
}

this_t inverse()
{
this_t retval;
retval.data.x = bit_cast<scalar_type>(bit_cast<AsUint>(data.x)^0x80000000u);
retval.data.y = bit_cast<scalar_type>(bit_cast<AsUint>(data.y)^0x80000000u);
retval.data.z = bit_cast<scalar_type>(bit_cast<AsUint>(data.z)^0x80000000u);
retval.data.w = data.w;
Comment on lines +248 to +251

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

retval.data.xyz = -retval.data.xyz is more readable and compiler should get it

return retval;
}

static this_t normalize(NBL_CONST_REF_ARG(this_t) q)
{
this_t retval;
retval.data = hlsl::normalize(q.data);
return retval;
}
Comment on lines +255 to +260

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

specialize normalize_helper from intrinsics instead


data_type data;
};

}

namespace impl
{

template<typename T>
struct static_cast_helper<math::quaternion<T>, math::truncated_quaternion<T> >
{
static inline math::quaternion<T> cast(math::truncated_quaternion<T> q)
{
return math::quaternion<T>::create(q);
}
};

template<typename T>
struct static_cast_helper<math::truncated_quaternion<T>, math::quaternion<T> >
{
static inline math::truncated_quaternion<T> cast(math::quaternion<T> q)
{
math::truncated_quaternion<T> t;
t.data.x = t.data.x;
t.data.y = t.data.y;
t.data.z = t.data.z;
return t;
}
};

template<typename T>
struct static_cast_helper<matrix<T,3,3>, math::quaternion<T> >
{
static inline matrix<T,3,3> cast(math::quaternion<T> q)
{
return q.constructMatrix();
}
};
}

}
}

#endif
1 change: 1 addition & 0 deletions src/nbl/builtin/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ LIST_BUILTIN_RESOURCE(NBL_RESOURCES_TO_EMBED "hlsl/math/geometry.hlsl")
LIST_BUILTIN_RESOURCE(NBL_RESOURCES_TO_EMBED "hlsl/math/intutil.hlsl")
LIST_BUILTIN_RESOURCE(NBL_RESOURCES_TO_EMBED "hlsl/math/polar.hlsl")
LIST_BUILTIN_RESOURCE(NBL_RESOURCES_TO_EMBED "hlsl/math/angle_adding.hlsl")
LIST_BUILTIN_RESOURCE(NBL_RESOURCES_TO_EMBED "hlsl/math/quaternions.hlsl")
LIST_BUILTIN_RESOURCE(NBL_RESOURCES_TO_EMBED "hlsl/math/equations/quadratic.hlsl")
LIST_BUILTIN_RESOURCE(NBL_RESOURCES_TO_EMBED "hlsl/math/equations/cubic.hlsl")
LIST_BUILTIN_RESOURCE(NBL_RESOURCES_TO_EMBED "hlsl/math/equations/quartic.hlsl")
Expand Down
Loading