Cameraz - #995
Conversation
…s - we were never be able to do ourMatrixT = glmMatrixT and its important since a lot of utils create glmMatrixT actually
…atrix xD remove the = operator glm -> to ours
…H (ambiguity dependent type issues), reference #760
…ng/Nabla/pull/760/files#r1816728485 for #760 PR, update examples_tests submodule
… were missing an equivalent of E_KEY_CODE. Update examples_tests submodule
| virtual void updateKeyboardMapping(const std::function<void(keyboard_to_virtual_events_t&)>& mapKeys) = 0; | ||
| virtual void updateMouseMapping(const std::function<void(mouse_to_virtual_events_t&)>& mapKeys) = 0; | ||
| virtual void updateImguizmoMapping(const std::function<void(imguizmo_to_virtual_events_t&)>& mapKeys) = 0; |
There was a problem hiding this comment.
I don't get the std::functions, this is virtual, but the implementation just calls the function?!
and calling the function is simply a lambda. this is super unnecessary, the members are public.
why would you want to modify them through a std::function? just access the member, assign it, clear it.
I'd get rid of the whole updateXXXMapping through function and modify the data directly
There was a problem hiding this comment.
actually we could drop callbacks
i started with private maps and used callbacks to edit them
| /// @brief Virtual event emitted by this binding. | ||
| gimbal_event_t event = {}; | ||
| /// @brief Per-binding gain applied when raw input is converted into one virtual-event magnitude. | ||
| double magnitudeScale = DefaultMagnitudeScale; |
There was a problem hiding this comment.
float may be enough, more precision alone is not a reason to keep double
There was a problem hiding this comment.
ok, what's a good reason to keep a multiplier double here?
I usually don't care about camera classes being larger btw, there are very few of them. but since this is going into map of all possible inputs, i care a bit
| /// @brief Per-binding gain applied when raw input is converted into one virtual-event magnitude. | ||
| double magnitudeScale = DefaultMagnitudeScale; | ||
| /// @brief Runtime latch used by held keyboard and mouse-button bindings. | ||
| bool active = false; |
There was a problem hiding this comment.
CHashInfo is two unrelated things in one struct. same for magnitude inside the event
- get rid of sanitize
- get rid of useless std::functions accessors that is equivalent to returning map&
then I would look for a solution like this:
struct SBinding { // pure config — what lives in every stored layout
uint8_t eventIndex; // index into CVirtualGimbalEvent::VirtualEventsTypeTable
float magnitudeScale; // sensitivity; float is ample for a multiplier
}; // = 8 bytes
Binding layout would just become a struct holding three maps and that's it ;)
State of active could be:
std::bitset<EKC_COUNT> m_heldKeys; // 16 B
std::bitset<EMC_COUNT> m_heldButtons; // 8 B
std::array<double, CVirtualGimbalEvent::EventsCount> m_magnitude; // 144 B
// = 168 B, fixed
also active+magnitude should not be something that "InputBinding" should care. that's why I suggest moving it up the hierarchy when used in IGimbalInputProcessor
this will not only be a net reduction in memory usage, but cleans things up a lot better. and you have data with different frequncies living seperately and in a better place
There was a problem hiding this comment.
we can separate config and state and improve memory usage
| /// Relative mouse movement, mouse scroll, and ImGuizmo deltas emit | ||
| /// `abs(rawDelta) * magnitudeScale` per bound axis. The result is written into | ||
| /// `CVirtualGimbalEvent::magnitude`. | ||
| class IGimbalInputProcessor : public CGimbalBindingLayoutStorage |
There was a problem hiding this comment.
InputProcessor needs to store map of the bindings from input to events.
and it makes 0 sense that a "processor" inherits from a "BindingLayout"
There was a problem hiding this comment.
you already needed it somewhere else and decided to store it as member function.
but for some reason this inherits?
// in PlanarProjection
ui::CGimbalBindingLayoutStorage m_inputBinding
based on https://github.com/Devsh-Graphics-Programming/Nabla/pull/995/changes#r3933703955 this inheritance should be changed to just storage of the static "layout" (the maps) + dynamic "states" (the active status + magnitude)
also interface inheriting from impl was a bit sketchy
There was a problem hiding this comment.
inheritance lets the processor be used as IGimbalBindingLayout by the same binding editor.
composition works too, but callers would need access to its layout member
| using inv_concatenated_matrix_t = std::optional<hlsl::float64_t4x4>; | ||
|
|
||
| /// @brief One concrete linear projection matrix together with cached inverse metadata. | ||
| struct CProjection : public IProjection |
There was a problem hiding this comment.
I don't get the choice to have ILinearProjection::CProjection inherit from IProjection and not the ILinearProjection itself
I would assume the convention IXXXProjection inherits from IProjection.
There was a problem hiding this comment.
the wrapper has multiple projections, while CProjection represents one.
we can change the names, but making the wrapper implement IProjection still needs a choice of which projection project(...) uses
| /// `MinDistance` prevents zero-distance target-relative states. | ||
| /// `DefaultMaxDistance` is unbounded. Individual cameras and tools may apply | ||
| /// their own finite limits on top of it. | ||
| struct SCameraTargetRelativeTraits final |
There was a problem hiding this comment.
"Traits" is usually something that get specialized via template on different classes
this header looks more like a "limit" to me.
naming convention aside.
- why do we need to hardcode clamp distance to target by 0.1f?
- why are values here float and the ones in
SCameraToolingThresholdsbelow in double? - I think this
MinDistancevalue should be the same as whatever the projection paired with the camera uses as zNear plane. - but again, I don't get the reason of existance for
MinDistanceand why we need to clamp some distance between a costexpr Min and Max that's baked into the library
There was a problem hiding this comment.
one camera can have multiple projections with independent clipping parameters, so which zNear would it use?
distance to the target is a different setting
There was a problem hiding this comment.
my problem is not what it should be equal to. my problem is why is that a thing that is hardcoded with 0.1f.
it seems mostly like a UX thing, but breaks when user wants to work in small ranges.
it seems to be mostly for preventing the distances to targets becoming 0 and introducing div by 0 issues.
also I don't see any reason to clamp against infinity.
What i suggest is that if you want to have min/max distance, let the user provide it to you when building the camera classes, don't decide that for them
| /// the minimum representable `float` value. | ||
| static inline constexpr float MinDistance = 1e-1f; | ||
| /// @brief Default upper bound for target-relative distance when no camera-specific cap is requested. | ||
| static inline constexpr float DefaultMaxDistance = std::numeric_limits<float>::infinity(); |
There was a problem hiding this comment.
again, this is float, and from what I've seen you're clamping/comparing doubles with this.
float infinity is not double infinity
There was a problem hiding this comment.
this is not true
float infinity stays infinity when converted to double. see C++ [conv.fpprom]/1.
A prvalue of type float can be converted to a prvalue of type double. The value is unchanged.
There was a problem hiding this comment.
interesting. this i learned today.
but again. let's be consistent with our types and limits
| }; | ||
|
|
||
| /// @brief Comparison thresholds used by helper layers outside the runtime camera interface. | ||
| struct SCameraToolingThresholds final |
There was a problem hiding this comment.
the limits in these files need to clarify:
- Numerical guards — "will this divide by zero?"
- Policy floors — "should we let the user do this?" A UX decision
- e.g. MinDistance, which I think is useless why would we need to clamp a positive number to 0.1 and +inf? it's not guarding a div and it seems to be soley UX related.
- Comparison tolerances — "are these two states the same?" and the epsilons relate to the floating point representation used. and their values need to be justified, their usages need to be justified as well. what are we protecting against?
There was a problem hiding this comment.
this is not true
computeDollyFov divides by max(distance, MinDistance), so it does guard a division
using 0.1 as the minimum is a separate choice
There was a problem hiding this comment.
ok but why 0.1 as hardcoded choice? MinDistance is being used multiple places for multiple reasons.
here is used for 0 guard div but it's used somewhere else for something else.
| // This file is part of the "Nabla Engine". | ||
| // For conditions of distribution and use, see copyright notice in nabla.h | ||
|
|
||
| #ifndef _C_CAMERA_TRAITS_HPP_ |
There was a problem hiding this comment.
CCameraMathUtils has similar limits structs, maybe they should live in the same place.
There was a problem hiding this comment.
we can put them together, but keep the different kinds of limits clear
| } | ||
|
|
||
| template<typename Vec, typename E = double> | ||
| static inline bool isOrthoBase(const Vec& x, const Vec& y, const Vec& z, const E epsilon = 1e-6) |
There was a problem hiding this comment.
the problem here is that's it's hardcoded for fp32 but templated on float type. you could get away with 1e-13 for doubles here
also, instead of "length" which has a sqrt involved. you could just abs(dot(V,V)-1.0) < eps
There was a problem hiding this comment.
casting the input axes to double does not restore precision already lost in float32.
using the same epsilon for the squared length check also changes which vectors pass
| hlsl::matrix<precision_t, 3, 3> m_orthonormal; | ||
|
|
||
| /// @brief Counter that increments for each performed manipulation, resets with each begin() call | ||
| size_t m_counter = {}; |
There was a problem hiding this comment.
why reset every frame?
Isn't the point of the counter to avoid doing things everyframe and the manipulation to persist across frames? it's stored as uint64_t/size_t which means every frame there can be 2^64-1 manipulations?! even uint16 seems enough to me (~65K) for every frame.
Oh no! you're just doing &=bool(m_gimbal.getManipulationCounter()); everywhere, it is literally just 1 bit/bool needed but somehow we have counter and isManipulating, both are useless and replacable by a bool at this stage.
I'm going to go an step further, remove the begin/end and counter reset everyframe and the weird bool below. store a 64-bit counter and increment on every edit. higher level code could store a gimbal manipulation counter value next to their computed view matrix and recalc when counters mismatch.
also some functions appear to be missing manipulation_counter++ like setScale
There was a problem hiding this comment.
view updates are already conditional, the counter tells us whether something changed while isManipulating tells us whether begin/end is open.
a counter that never resets can work too, but scale does not change the rigid view matrix
| size_t m_counter = {}; | ||
|
|
||
| /// @brief Tracks whether gimbal is currently in manipulation mode | ||
| bool m_isManipulating = false; |
There was a problem hiding this comment.
this is just for assert(m_isManipulating) it seems. need to remove
There was a problem hiding this comment.
it is also returned by isManipulating(), not just used in asserts.
after begin() the flag is true and the counter is zero, so they tell us different things
There was a problem hiding this comment.
begin/end and isManipulating need to be replaced with global uint64 counter
| if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleXInc) | ||
| if (event.type == CVirtualGimbalEvent::ScaleXInc) | ||
| impulse.dVirtualScale.x *= static_cast<precision_t>(event.magnitude); | ||
|
|
||
| if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleXDec) | ||
| if (event.type == CVirtualGimbalEvent::ScaleXDec) | ||
| impulse.dVirtualScale.x *= static_cast<precision_t>(event.magnitude); |
There was a problem hiding this comment.
- you don't need scale for camera gimbal
- this class is not supposed to be general model matrix construction that handles scale, this class is all about cameras and virtual events affecting cameras.
- also I looked and there is literally 0 difference between
CVirtualGimbalEvent::ScaleXIncandCVirtualGimbalEvent::ScaleXDec. you're treating them as the same thing everywhere - also no camera allows scaling gimball event as it makes 0 sense
we need to nuke the scale event completely and remove m_scale from here.
There was a problem hiding this comment.
other objects and scale were explicitly part of the design, so this is not limited to cameras.
the input distinguishes factors below and above 1, although both events use multiplication in IGimbal
There was a problem hiding this comment.
ok got it, you wrote it specifically for all types of objects. then you need to stop using hlsl::CCameraMathUtilities and hlsl::camera_vector_t and anything with camera in it's name.
also void transform function doesn't even take impulse.dVirtualScale into account. we may want to fix that as well (not sure if intentional?)
| template<typename T> | ||
| static inline constexpr camera_vector_t<T, 3> getCameraWorldRight() | ||
| { | ||
| return camera_vector_t<T, 3>(T(1), T(0), T(0)); | ||
| } | ||
|
|
||
| template<typename T> | ||
| static inline constexpr camera_vector_t<T, 3> getCameraWorldUp() | ||
| { | ||
| return camera_vector_t<T, 3>(T(0), T(1), T(0)); | ||
| } | ||
|
|
||
| template<typename T> | ||
| static inline constexpr camera_vector_t<T, 3> getCameraWorldForward() | ||
| { | ||
| return camera_vector_t<T, 3>(T(0), T(0), T(1)); | ||
| } |
There was a problem hiding this comment.
very bad names and types.
In linear algebra it's just simply standard basis X Y Z, has nothing to do with the camera.
you transform these vectors using quaternions to put into matrix columns and construct a matrix that transforms these standard basis to the new ones.
need to rename to something that makes more sense or get rid of the functions alltogether. writing it directly makes more sense and is more readable
There was a problem hiding this comment.
"Has nothing to do with the camera" is not true
we model the camera with the gimbal's orthonormal frame and these helpers define its canonical right, up and forward axes.
being standard basis vectors does not make them unrelated to cameras
There was a problem hiding this comment.
doesn't matter that much. I'd rename them to CameraStandardBasisX.
when I see World in the name hints at the units being in worldspace. while these are just the standard basis of a camera in camera space. the camera's up vector is always 0,1,0. that's why it's local and not world
| /// The class exists mainly as a convenient instantiable type when no additional | ||
| /// camera-specific state or manipulation policy is required on top of `IGimbal`. | ||
| template<typename T = hlsl::float64_t> | ||
| class CGeneralPurposeGimbal : public IGimbal<T> |
There was a problem hiding this comment.
unused class and adds 0 value to parent class
There was a problem hiding this comment.
not being used inside Nabla alone does not make it unnecessary, but this wrapper is only a convenience type
There was a problem hiding this comment.
ok not used, but what value do they add to IGimball? they don't even specialize a template param. they are identical at this point. did you have any plans to add any feature to this class?
| } | ||
|
|
||
| /// @brief Apply a prebuilt rigid reference transform and an accumulated impulse in one step. | ||
| inline void transform(const CReferenceTransform& reference, const VirtualImpulse& impulse) |
There was a problem hiding this comment.
this function is not used anywhere. just keeping notes for future
There was a problem hiding this comment.
the ICamera::CGimbal::transform wrapper calls it.
it applies translation and rotation relative to a reference frame, lack of a current caller alone does not make that operation unnecessary
There was a problem hiding this comment.
mostly hinting that a function is untested. and needs to be reviewed with more care later on or with unit tests
There was a problem hiding this comment.
btw, any reason this does not take scale of the "impulse" into account?
| if constexpr (std::is_same_v<T, float>) | ||
| return makeQuaternionFromBasisImpl(canonicalRight, canonicalUp, canonicalForward); | ||
| else | ||
| return makeQuaternionFromBasisImpl(canonicalRight, canonicalUp, canonicalForward); |
There was a problem hiding this comment.
these are the exact same, no need to specialize with if constexpr
There was a problem hiding this comment.
yeah my bad, no need for the if constexpr
| { | ||
| setOrientation(reference.orientation * hlsl::CCameraMathUtilities::makeQuaternionFromEulerRadiansYXZ(impulse.dVirtualRotation)); | ||
| setPosition( | ||
| hlsl::float64_t3(reference.frame[3]) + |
There was a problem hiding this comment.
templated on T but using float64_t3 directly here?
There was a problem hiding this comment.
doesn't matter, this will all change to float64 soon
There was a problem hiding this comment.
this uses float64 despite being templated on T, we should make that consistent
| if(dRadians) | ||
| m_counter++; | ||
|
|
||
| const auto dRotation = hlsl::CCameraMathUtilities::makeQuaternionFromAxisAngle(axis, static_cast<precision_t>(dRadians)); |
There was a problem hiding this comment.
float for radians/angle is enough precision. so this is fine.
although that function needs to go into actual hlsl lib
There was a problem hiding this comment.
we can move the math helper to hlsl
|
|
||
| /// @brief Accumulates one frame of virtual events into a translation/rotation/scale impulse. | ||
| template <uint32_t AllowedEvents> | ||
| VirtualImpulse accumulate(std::span<const CVirtualGimbalEvent> virtualEvents, const vector_t<3u>& gRightOverride, const vector_t<3u>& gUpOverride, const vector_t<3u>& gForwardOverride) |
There was a problem hiding this comment.
3 params are not used at all.
There was a problem hiding this comment.
yeah these three params are not used, could be another leftover
| m_viewMatrix[0u] = hlsl::float64_t4(gRight, -hlsl::dot(gRight, position)); | ||
| m_viewMatrix[1u] = hlsl::float64_t4(gUp, -hlsl::dot(gUp, position)); | ||
| m_viewMatrix[2u] = hlsl::float64_t4(gForward, -hlsl::dot(gForward, position)); |
There was a problem hiding this comment.
make sure to mention that the inverse of an orthonormal matrix is it's transpose, so we caan put columns into rows.
also it's really confusing because you're treating m_orthonormal (where those getX/Y/ZAxis come from) as a column-major order matrix treating mat[0] as the first column but here you're doing it properly with nabla conventions (hlsl::matrix is row matrix)
and the conventions aren't clear, the when I look at some functions, some treat it as column-major some as row-major
There was a problem hiding this comment.
here the basis stores the rotated axes as rows, which is already the transpose of the orientation used with column vectors. updateView uses those rows and their negative dot products with position, so this view construction is correct.
i think we can document that convention
There was a problem hiding this comment.
I think we need to fix up the code and anywhere in the ext that uses matrix first.
either store 3 seperate orthonormal column vectors if you want to strore columns. otherwise you'll risk bugs and somebody touching up the code later and assuming the matrices are safe to mul with, not knowing sometimes it's transposed and sometimes not.
for example we cannot move something like getQuaternionBasisMatrix to the hlsl library because it clashes with nabla's convention of row major matrices.
| template<typename T> | ||
| static inline camera_vector_t<T, 3> projectWorldVectorToLocalBasis( | ||
| const camera_vector_t<T, 3>& worldVector, | ||
| const camera_vector_t<T, 3>& right, | ||
| const camera_vector_t<T, 3>& up, | ||
| const camera_vector_t<T, 3>& forward) | ||
| { | ||
| const camera_matrix_t<T, 3, 3> basis { right, up, forward }; | ||
| return hlsl::mul(hlsl::transpose(basis), worldVector); | ||
| } | ||
|
|
||
| template<typename T> | ||
| static inline camera_vector_t<T, 3> transformLocalVectorToWorldBasis( | ||
| const camera_vector_t<T, 3>& localVector, | ||
| const camera_vector_t<T, 3>& right, | ||
| const camera_vector_t<T, 3>& up, | ||
| const camera_vector_t<T, 3>& forward) | ||
| { | ||
| const camera_matrix_t<T, 3, 3> basis { right, up, forward }; | ||
| return hlsl::mul(basis, localVector); | ||
| } |
There was a problem hiding this comment.
this is wrong and swapped because hlsl::matrix and mul treats matrix as row major matrix.
I'm 90% sure this is causing a bug with the CSphericalTargetCamera::applyPlanarTargetTranslation using this function
There was a problem hiding this comment.
yeah these two are swapped, local to world needs the transpose when the axes are stored as rows
this also affects planar target movement
There was a problem hiding this comment.
btw instead of mul(transpose(m), v) you can do mul(v, m)
| const TRS operator()() const | ||
| { |
There was a problem hiding this comment.
- this function is unused and is a bit funny, cause it's the same function but when you pass
hlsl::matrix<T, 4u, 4u>it treats as row-major but otherwise column major and the function is never used btw.
|
the convention in your camera ext is super unclear:
|
| vector_t<3u> m_scale = { 1.f, 1.f , 1.f }; | ||
|
|
||
| /// @brief Orthonormal basis reconstructed from the current orientation. | ||
| hlsl::matrix<precision_t, 3, 3> m_orthonormal; |
There was a problem hiding this comment.
if you want to keep this. then you can do it as seperate column vectors, because it looks like this matrix is only used for storage and not any matrix operations. and you're storing your columns in this matrx's rows and any mul using this matrix would break immediately.
| } | ||
|
|
||
| template<typename T> | ||
| inline math::quaternion<T> matrix_to_quaternion_cast(NBL_CONST_REF_ARG(matrix<T,3,3>) m) |
There was a problem hiding this comment.
why are we considering the trasnpose matrices for matrix_to_quaternion cast? why are we doing this 4 times 4 different ways and scoring them?
| static inline camera_matrix_t<T, 3, 3> getQuaternionBasisMatrix(const camera_quaternion_t<T>& orientation) | ||
| { | ||
| const auto normalizedOrientation = normalizeQuaternion(orientation); | ||
| return camera_matrix_t<T, 3, 3>( | ||
| normalizedOrientation.transformVector(getCameraWorldRight<T>(), true), | ||
| normalizedOrientation.transformVector(getCameraWorldUp<T>(), true), | ||
| normalizedOrientation.transformVector(getCameraWorldForward<T>(), true)); | ||
| } |
There was a problem hiding this comment.
why not use __constructMatrix? already available in hlsl.
There was a problem hiding this comment.
probably because first this PR got started and THEN the quaternions got cleaned up by Przemog
| } | ||
|
|
||
| template<typename VecA, typename VecB, typename T> | ||
| static inline bool nearlyEqualVec3(const VecA& a, const VecB& b, const T epsilon) |
There was a problem hiding this comment.
we have nbl::hlsl::RelativeApproxCompareHelper that should even work for matrices. this is redundant
| } | ||
|
|
||
| template<typename T> | ||
| static inline bool nearlyEqualScalar(const T a, const T b, const T epsilon) |
There was a problem hiding this comment.
we have nbl::hlsl::RelativeApproxCompareHelper that should even work for matrices. this is redundant
|
not included in this PR but needs to be fixed: compare with 0.0 or any other subnormal even with a generous tolerance with return false. that's causing some quaternion stuff to be nan when they shouldn't e.g here: |
Introduces the new camera stack as Nabla::ext::Cameras.