How to achieve smooth camera movement from mouse input - c++

Alright, so I've been trying to figure this out for several years now (no joke, i'll work on it until I can't anymore and move onto another task telling myself i'll come back to it later and it's been a never ending loop), but I think I've finally collected enough information about what's going on that I can explain with enough detail that someone might be able to point me in the right direction.
The issue is that I cannot seem to achieve a smooth camera rotation based off mouse input in this 3d framework/engine I've been working on (hobby project) with c++/glfw/glad/opengl. I've gone through and made sure that within windows mouse acceleration is disabled, within glfw cursor is disabled and GLFW_RAW_MOUSE_MOTION is enabled. There is nothing (that i'm aware of) that should be skewing the mouse input values that are being obtained.
The below is debug output I generated which shows: frame_time|current_mouse_x_position|mouse_delta_for_tick
0.010011: 5803.000000: 9.000000
0.010001: 5810.000000: 7.000000
0.010011: 5819.000000: 9.000000
0.010002: 5836.000000: 17.000000
0.010011: 5845.000000: 9.000000
0.010001: 5854.000000: 9.000000
0.010001: 5861.000000: 7.000000
0.010015: 5876.000000: 15.000000
0.010002: 5884.000000: 8.000000
0.010002: 5894.000000: 10.000000
0.010264: 5902.000000: 8.000000
0.010009: 5919.000000: 17.000000
0.010010: 5928.000000: 9.000000
0.010011: 5935.000000: 7.000000
0.010006: 5943.000000: 8.000000
0.010010: 5958.000000: 15.000000
0.010090: 5965.000000: 7.000000
0.010005: 5971.000000: 6.000000
0.010013: 5979.000000: 8.000000
0.010042: 5994.000000: 15.000000
0.010012: 6001.000000: 7.000000
0.010004: 6009.000000: 8.000000
0.010009: 6016.000000: 7.000000
0.010007: 6033.000000: 17.000000
This data was collected while moving the mouse along the x-axis as consistently as my hand would allow, and you can see in the mouse_delta_for_tick column that every so often the values appear to be double what they should be.
My first thought was that this could be due to the amount of time between when the positions are sampled, but you can see in the first column that the frame_time stays relatively consistent. So I'm uncertain what could be causing these values to be so off.
You can imagine that when using these values to calculate the camera rotation, it becomes stuttery/clicky. Below is how I'm doing the update every tick:
void Client::updateMouseDelta()
{
// this->input updated elsewhere as fast as engine can update it, so in this function it is
// the current mouse position for this tick
this->currentMousePosition.x = this->input.mouseXPos;
this->currentMousePosition.y = this->input.mouseYPos;
if (this->firstMouseInput)
{
this->lastMousePosition = this->currentMousePosition;
this->firstMouseInput = false;
}
//TODO mousedelta calculation and framerate or something causing noisy deltas??????idk????
this->mouseDelta.x = this->currentMousePosition.x - this->lastMousePosition.x;
this->mouseDelta.y = this->lastMousePosition.y - this->currentMousePosition.y;
this->lastMousePosition = this->currentMousePosition;
}
void Client::updatePitchYaw()
{
this->yaw += this->mouseDelta.x * this->mouseSensitivity;
this->pitch += this->mouseDelta.y * this->mouseSensitivity;
// make sure that when pitch is out of bounds, screen doesn't get flipped
if (this->pitch > 89.0f)
this->pitch = 89.0f;
if (this->pitch < -89.0f)
this->pitch = -89.0f;
}
void Client::updateRotation(float frameTime, float renderLerpInterval)
{
this->updateMouseDelta();
this->updatePitchYaw();
this->lookDirection.x = cos(glm::radians(this->yaw)) * cos(glm::radians(this->pitch));
this->lookDirection.y = sin(glm::radians(this->pitch));
this->lookDirection.z = sin(glm::radians(this->yaw)) * cos(glm::radians(this->pitch));
this->lookDirection = glm::normalize(this->lookDirection);
auto cameraPosition = this->ccc->getCapsuleActor()->getInterpolatedTranslation(renderLerpInterval);
this->worldCamera->setPosition(cameraPosition);
this->worldCamera->setLookAt(cameraPosition + this->lookDirection);
}
I've done various things throughout the years to attempt to mask this stutter by smoothing the input, but it's never perfect and always introduces a noticeable latency in mouse movement (moving average filter, converting into a quaternion and using slerp between previous and current rotations, a low pass filter (although that might have been a different unrelated issue now that I think about it)).
Also just to double confirm that this wasn't unrelated to mouse input I implemented a simple rotation test using arrow keys as such (which is buttery smooth):
void Client::updatePitchYaw()
{
if (this->input.keyUp)
this->pitch += 0.75f;
if (this->input.keyDown)
this->pitch -= 0.75f;
if (this->input.keyRight)
this->yaw += 0.75f;
if (this->input.keyLeft)
this->yaw -= 0.75f;
// make sure that when pitch is out of bounds, screen doesn't get flipped
if (this->pitch > 89.0f)
this->pitch = 89.0f;
if (this->pitch < -89.0f)
this->pitch = -89.0f;
}
If you've made it this far THANK YOU, and I suppose my main question is: How can I achieve the sort of buttery smooth responsive camera rotation via mouse input that every other game I've ever played seems to be able to achieve (counter-strike, quake3, half-life, etc)? There MUST be something that I'm missing.
EDIT
Adding main gameloop code and mouse input update code (stripped down version of loop, but all relevant parts should be there) which should make it clear how I am getting the mouse position values:
/*
App::execute() invoked via main(), once invoked, this method controls the
application until it is terminated.
*/
void App::execute()
{
this->fixedLogicTime = 1 / this->config.LOGIC_TICK; // LOGIC_TICK = 120.0
this->currentTime = this->time();
while (true)
{
if (this->shouldClose || this->window->shouldClose())
break;
this->newTime = this->time();
this->frameTime = this->newTime - this->currentTime;
// UPDATE MOUSE INPUT HERE. THIS METHOD CALLS THE Window::setMouse() method
// which is included below this one
this->window->updateInputState();
if (this->frameTime >= (1 / this->config.MAX_RENDER_FPS)) // cap max fps
{
this->currentTime = this->newTime;
if (this->frameTime > 0.25)
this->frameTime = 0.25;
this->accumulator += this->frameTime;
while (this->accumulator >= this->fixedLogicTime)
{
this->activeScene->applyTransformations();
this->activeScene->processSensors();
this->activeScene->innerLoop((float)this->fixedLogicTime); // fixed logic updates
this->activeScene->updateAnimations(this->fixedLogicTime);
this->activeScene->stepPhysics((float)this->fixedLogicTime);
this->activeScene->postPhysics((float)this->fixedLogicTime);
this->accumulator -= this->fixedLogicTime;
}
float renderLerpInterval = (float)(this->accumulator / this->fixedLogicTime);
// THIS METHOD WOULD BE CALLING THE METHODS WHERE THE MOUSE INPUT VALUES ARE USED
// IE, MY PREVIOUSLY POSTED Client::updateRotation() METHOD
this->activeScene->outerLoop((float)this->frameTime, renderLerpInterval); // immediate logic updates
this->gpu->clearBuffers(0.0f, 0.0f, 0.0f, 1.0f);
this->activeScene->draw(renderLerpInterval);
}
}
}
void Window::setMouse()
{
double mXPos;
double mYPos;
glfwGetCursorPos(this->glfwWindow, &mXPos, &mYPos);
this->inputState.mouseXPos = (float)mXPos;
this->inputState.mouseYPos = (float)mYPos;
}

You get smooth rotation with key press, because you add (subtract) a fixed value from the previous/actual one and increase (decrease) the rotation by a fixed amount. Every frame shows you a rotation by a fixed angle (smooth).
If you want to achieve the same result as key presses, use the same method.
Divide the screen into cells (the cell size is determined by your scaling factor, e.g. how many key strokes it needs for 180 degrees, which is equal to the screen width or a mouse motion from left to right). Based on your mouse position or moved distance, calculate the cell your pointer resides within or how many cells it moved, which gives you in return the desired angle for the rotation (by a fixed amount, just like key strokes).
Therefore, small changes of the mouse pointer will have no effect, only if the moved distance is greater than a cell size, then a rotation will occur (moved cells multiplied by a scaling factor).

Well. I finally figured it out, and it is so soul crushingly simple I'm almost embarrassed to post this answer, BUT maybe it will help someone down the road...
It was the mouse :(
I have multiple machines that I work from. Recently I have been using my laptop with this mouse and this is where I started noticing the stutter again. However, before I jumped to the conclusion that it was something code related, I tested the exact same build on my standing desktop setup, which also uses an hp mouse similar to the one above but not exact, and the issue persisted. This acted as my confirmation that something wasn't right since it was happening on both systems.
This evening, thinking it was something performance related I jumped on my primary desktop (5900x, rtx3070, etc), which uses a razer lancehead mouse...and the rotation was perfectly smooth. I then grabbed both of the hp mice and tried each on this system, and wouldn't you know...it was a stuttery mess.
I'm willing to accept this as the problem to the issue, but I had used that hp mouse on my last primary desktop build for years and played countless games and never noticed any of this stutteriness, I mean it's a 1000dpi mouse which should be plenty sensitive enough. I'm wondering if there is something that changed with the windows 10 usb mouse drivers? That's really all I can think of as the razer mouse has it's own proprietary drivers.
This all makes me wonder how many people out there are using these cheap mice and experiencing this, but not knowing that it's an issue.

Your mouse input is incorrect.
Maybe you want to doublecheck that you get more serious values with another program like
<html>
<body>
<form name="Form1">
<textarea type="text" name="posx" />
</textarea>
</form>
<script type="text/javascript">
var start = Date.now();
document.onmousemove = function (event)
{
if (Date.now() - start<10) return;
start = Date.now();
document.Form1.posx.value += start + "\t"+ event.clientX +"\n" ;
}
</script>
</body>
</html>

Related

SDL/C++ movement: time and acceleration

I want to teach myself some basic physics programming by creating a simple 2d platformer with SDL 2. It seems I'm falling at the first hurdle though, because I can't get movement using both velocity and acceleration per time unit, rather than per frame, to work.
I start by calculating the time per frame in the usual way:
previous_time = current_time;
current_time = SDL_GetTicks();
delta_time = current_time - previous_time;
Then, after the movement flag is set to true by pressing a directional button, this is passed to a function to handle the movement.
//Pass the movement flag and the milliseconds per frame to the right movement function.
if ( player.get_x() <= 740 ) {
player.x_movement_right(delta_time, 1, moving_right);
}
The integer that's passed doesn't do anything yet. Anyway, the function then determines the acceleration based on if the movement flag is set to true, and what the current velocity is:
void Player::x_movement_right(float dt, int direction, bool moving_right) {
dt /= 1000;
if (moving_right == true && _x_velocity <= 200 ) {
_x_acceleration = 50;
}
else if ( moving_right == false && _x_velocity > 0 ) {
_x_acceleration = -50;
}
else {
_x_acceleration = 0;
}
_x_velocity += _x_acceleration * dt;
_x_position += _x_velocity * dt;
}
The same process occurs if the left movement flag is activated, with inverted values of course. Yet after compiling I hit the directional keys and nothing happens.
What I've already tried:
I removed the dt's at the bottom of the movement function. The player avatar moves with incredible speed, since the acceleration is now per frame, rather than per second.
Same thing when I don't divide dt at the beginning of the movement function, since it's now per millisecond rather than per second.
I tried rounding the velocity times dt at the bottom, since I suspected SDL might have trouble calculation positions with floating point numbers rather than integers. Still no movement.
Based on this I suspect it has something to do with the numbers being too small, but I can't quite wrap my head around what the problem is or how to solve it. So, does anyone know what undoubtedly obvious thing I'm missing? Thanks in advance!
There is no way to know with the information shown, but there are several points that may help you:
Are your _x_velocity and the like floating point types? In what units are you measuring distance? It may be that your increment has not enough resolution to be nonzero.
Have you printed the values of each variable or run the program in a debugger?
What do you mean by "SDL might have trouble calculation positions with floating point numbers"? If you are using SDL's basic 2D renderer, you just need to give it the type it needs in whatever units they ask. The conversions are up to you.
Overall, I'd recommend trying to code the simulation outside SDL or graphics in general. Getting acquainted with C++, debugging and floating-point is also a plus.

How can I map the controller analog stick to the mouse

Alright, so I am trying to use the analog stick on a gamepad to move the desktop mouse cursor around. The problem is that I need to be able to get the same smoothness as Attempt 2, but without using an absolute mouse position. The cursor needs to be moved relative to its current position. The reason for this is that many applications (mainly video games) also set the mouse to an absolute position. This causes the application and attempt 2 to fight one another for control of the mouse.
Attempt 1 (relative)
// keep updating the controller state and mouse position
while (true)
{
// getState returns a 2d float vector with normalized values from [-1, 1]
// The cursor is being set relative to its current position here.
SetCursorPosition(GetCursorPosition() + analogStick->getState());
}
This solution works, but suffers from a rounding issue because GetCursorPosition and SetCursorPosition are based on integers. As a result, small movements are not registered because smaller analog movements will always get truncated. Visually speaking, small movements on the analog stick will only move the mouse along the X or Y axis even if you are try to make a diagonal movement.
Attempt 2 (absolute)
vec2 mouseTargetPosition = GetCursorPosition(); // global cursor position
while (true)
{
mouseTargetPosition += leftStick->getState();
vec2 newPosition = lerp(GetCursorPos(), mouseTargetPosition, 0.8f);
SetCursorPos(round(newPosition.x), round(newPosition.y));
}
This solution works great, the mouse responds to the smallest of movements and moves very naturally as a result of interpolating the accumulated analog movements. But, it sets the mouse to an absolute position (mouseTargetPosition), making this solution a deal breaker.
This is an awfully specific question in the first place I suppose. After fooling around with several configurations this is the one that feels smoothest and works well. It's basically magic considering because it can add native feeling analog support for games and model viewers that don't have it :)
vec2 mouseTargetPos, mouseCurrentPos, change;
while (true)
{
// Their actual position doesn't matter so much as how the 'current' vector approaches
// the 'target vector'
mouseTargetPos += primary->state;
mouseCurrentPos = util::lerp(mouseCurrentPos, mouseTargetPos, 0.75f);
change = mouseTargetPos - mouseCurrentPos;
// movement was too small to be recognized, so we accumulate it
if (fabs(change.x) < 0.5f) accumulator.x += change.x;
if (fabs(change.y) < 0.5f) accumulator.y += change.y;
// If the value is too small to be recognized ( < 0.5 ) then the position will remain the same
SetCursorPos(GetCursorPos() + change);
SetCursorPos(GetCursorPos() + accumulator);
// once the accumulator has been used, reset it for the next accumulation.
if (fabs(accumulator.x) >= 0.5f) accumulator.x = 0;
if (fabs(accumulator.y) >= 0.5f) accumulator.y = 0;
}

recalculate QwtScaleDiv before rendering

I've implemented a QwtPlot which scrolls across the screen as data is added in real-time. Based on user input, an image of the plot is occasionally rendered to a file using QwtPlotRenderer. However, because the axis scrolls during normal operation, the QwtScaleDiv tick marks can look a little wonky at render time (they are right-aligned):
Is there some easy way in which I can recalculate the division prior to rendering so that the first label is on the far left and the last one is on the far right?
This isn't as difficult as it looked at first. Bascially, all you need to do is temporarily replace the axisScaleDiv.
auto divX = this->axisScaleDiv(xBottom);
double ub = divX.upperBound();
double lb = divX.lowerBound();
double numTicks = 11.0; // 10 even divisions
// you can create minor/medium ticks if you want to, I didn't.
QList<double> majorTicks;
for (int i = 0; i < numTicks; ++i)
{
majorTicks.push_back(lb + i * ((ub - lb) / (numTicks - 1)));
}
// set the scale to the newly created division
QwtScaleDiv renderDivX(divX.lowerBound(), divX.upperBound(),
QList<double>(), QList<double>(), majorTicks);
this->setAxisScaleDiv(xBottom, renderDivX);
// DO PLOT RENDERING
QwtPlotRender renderer;
renderer.renderDocument(...);
// RESOTRE PREVIOUS STATE
this->setAxisScaleDiv(xBottom, divX);
this->setAxisScaleDiv(yLeft, divY);
// update the axes
this->updateAxes();

Implementing Zoom on an orbit camera

I wish to zoom in on the target of an orbiting camera.
The camera is manipulated using a function like this:
moveCamera(x,y,z);
Depending on angle the values x,y,z should be different to get a correct zoom feature but I cant figure out an way to do it.
I use functions like getCameraposx, getTargetposy etc.. to get the coordinates for my target and camera.
Zoom kinda works now after PigBens help but I've run in to a problem. Zooming in is no problem but after zooming in too close zooming out stops working. And with too close I'm still quite far away.
Here is my zoom function.
void Camera::orbZoom(bool Zoo)
{
float x;
float y;
float z;
float xc;
float yc;
float zc;
float zoom;
x=getTargetposx();
y=getTargetposy();
z=getTargetposz();
xc=getCameraposx();
yc=getCameraposy();
zc=getCameraposz();
xc=xc-x;
yc=yc-y;
zc=zc-z;
if ( ivan==true){
zoom = 1.02;
if (xc<1){xc=+1.5;}
else if (yc<1){yc=+1.5;}
else if (zc<1){zc=+1.5;}
xc=xc*zoom;
yc=yc*zoom;
zc=zc*zoom;
}
if(ivan==false) {
zoom = 0.98;
xc=xc*zoom;
yc=yc*zoom;
zc=zc*zoom;
}
xc=xc+x;
yc=yc+y;
zc=zc+z;
camerapos.assign(xc,yc,zc);
}
Ok so the last thing didnt work as I wrote in the last comment. I'm thinking there is something else causing this behavior. The limit for when it stops working is just a bit closer to target than cameras starting position or at starting position, not really sure on that. But if I start with zooming out and dont get any closer than the cameras starting position it's working.
I think the bug is in this part of the code but I could be wrong so if anyone want to see some other part just ask. All my other camera behaviors are working correctly. Two modes, orbit and tumble. Pitch, yaw and roll works for both modes and strafing for tumble mode.
Here are for example two of those functions.
void Camera::strafeUp(float distance)
{
camerapos += upvect * distance;
targetpos += upvect * distance;
}
void Camera::tumbleYaw(float angle)
{
Quaternionf rotation((angle*PIdiv180), upvect);
rightvect = rotation.matrix() * rightvect;
forwardvect = rotation.matrix() * forwardvect;
forwardvect.normalize();
rightvect.normalize();
targetpos = camerapos + forwardvect * cameralength;
}
Subtract the target position from the camera position, then scale it, then add the target position in again.
camera_position -= target_position;
camera_position /= zoom_factor;
camera_position += target_position;
Regarding your second problem. My guess is that it's due to lack of precision in floats. When you get down to a certain point, multiplying by 1.02 is not enough to change the float's value to the next higher representable value, so it doesn't change at all. My tests indicate that this doesn't happen until the float is in the 10e-44 range, so you must be using some pretty gigantic units for this to be a problem. A few possible solutions.
Use doubles instead of floats. You will still have the same problem. But it won't come into play until a much much closer zoom.
Use smaller units. I usually just go with 1.0 = 1 meter and I've never run in to this problem.
Enforce a maximum zoom. You would actually do this in combination with the other 2 above.

Preserving rotations in OpenGL

I'm drawing an object (say, a cube) in OpenGL that a user can rotate by clicking / dragging the mouse across the window. The cube is drawn like so:
void CubeDrawingArea::redraw()
{
Glib::RefPtr gl_drawable = get_gl_drawable();
gl_drawable->gl_begin(get_gl_context());
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glPushMatrix();
{
glRotated(m_angle, m_rotAxis.x, m_rotAxis.y, m_rotAxis.z);
glCallList(m_cubeID);
}
glPopMatrix();
gl_drawable->swap_buffers();
gl_drawable->gl_end();
}
and rotated with this function:
bool CubeDrawingArea::on_motion_notify_event(GdkEventMotion* motion)
{
if (!m_leftButtonDown)
return true;
_3V cur_pos;
get_trackball_point((int) motion->x, (int) motion->y, cur_pos);
const double dx = cur_pos.x - m_lastTrackPoint.x;
const double dy = cur_pos.y - m_lastTrackPoint.y;
const double dz = cur_pos.z - m_lastTrackPoint.z;
if (dx || dy || dz)
{
// Update angle, axis of rotation, and redraw
m_angle = 90.0 * sqrt((dx * dx) + (dy * dy) + (dz * dz));
// Axis of rotation comes from cross product of last / cur vectors
m_rotAxis.x = (m_lastTrackPoint.y * cur_pos.z) - (m_lastTrackPoint.z * cur_pos.y);
m_rotAxis.y = (m_lastTrackPoint.z * cur_pos.x) - (m_lastTrackPoint.x * cur_pos.z);
m_rotAxis.z = (m_lastTrackPoint.x * cur_pos.y) - (m_lastTrackPoint.y * cur_pos.x);
redraw();
}
return true;
}
There is some GTK+ stuff in there, but it should be pretty obvious what it's for. The get_trackball_point() function projects the window coordinates X Y onto a hemisphere (the virtual "trackball") that is used as a reference point for rotating the object. Anyway, this more or less works, but after I'm done rotating, and I go to rotate again, the cube snaps back to the original position, obviously, since m_angle will be reset back to near 0 the next time I rotate. Is there anyway to avoid this and preserve the rotation?
Yeah, I ran into this problem too.
What you need to do is keep a rotation matrix around that "accumulates" the current state of rotation, and use it in addition to the rotation matrix that comes from the current dragging operation.
Say you have two matrices, lastRotMx and currRotMx. Make them members of CubeDrawingArea if you like.
You haven't shown us this, but I assume that m_lastTrackPoint is initialized whenever the mouse button goes down for dragging. When that happens, copy currRotMx into lastRotMx.
Then in on_motion_notify_event(), after you calculate m_rotAxis and m_angle, create a new rotation matrix draggingRotMx based on m_rotAxis and m_angle; then multiply lastRotMx by draggingRotMx and put the result in currRotMx.
Finally, in redraw(), instead of
glRotated(m_angle, m_rotAxis.x, m_rotAxis.y, m_rotAxis.z);
rotate by currRotMx.
Update: Or instead of all that... I haven't tested this, but I think it would work:
Make cur_pos a class member so it stays around, but it's initialized to zero, as is m_lastTrackPoint.
Then, whenever a new drag motion is started, before you initialize m_lastTrackPoint, let _3V dpos = cur_pos - m_lastTrackPoint (pseudocode).
Finally, when you do initialize m_lastTrackPoint based on the mouse event coords, subtract dpos from it.
That way, your cur_pos will already be offset from m_lastTrackPoint by an amount based on the accumulation of offsets from past arcball drags.
Probably error would accumulate as well, but it should be gradual enough so as not to be noticeable. But I'd want to test it to be sure... composed rotations are tricky enough that I don't trust them without seeing them.
P.S. your username is demotivating. Suggest picking another one.
P.P.S. For those who come later searching for answers to this question, the keywords to search on are "arcball rotation". An definitive article is Ken Shoemake's section in Graphical Gems IV. See also this arcball tutorial for JOGL.