|
64 | 64 |
|
65 | 65 | -- PRIVATE |
66 | 66 |
|
67 | | -local function freecamFrame () |
| 67 | +local function freecamFrame (deltaTime) |
| 68 | + freecamMouseApply(deltaTime) |
68 | 69 | -- work out an angle in radians based on the number of pixels the cursor has moved (ever) |
69 | 70 | local cameraAngleX = rotX |
70 | 71 | local cameraAngleY = rotY |
@@ -217,48 +218,103 @@ local function freecamFrame () |
217 | 218 | setCameraMatrix ( camPosX, camPosY, camPosZ, camTargetX, camTargetY, camTargetZ, 0, options.fov ) |
218 | 219 | end |
219 | 220 |
|
220 | | -local function freecamMouse (cX,cY,aX,aY) |
221 | | - --ignore mouse movement if the cursor or MTA window is on |
222 | | - --and do not resume it until at least 5 frames after it is toggled off |
223 | | - --(prevents cursor mousemove data from reaching this handler) |
224 | | - if isCursorShowing() or isMTAWindowActive() or (not isMTAWindowFocused()) then |
225 | | - mouseFrameDelay = 5 |
226 | | - return |
227 | | - elseif mouseFrameDelay > 0 then |
228 | | - mouseFrameDelay = mouseFrameDelay - 1 |
229 | | - return |
| 221 | +-- Internal state (module-level) |
| 222 | +local mouseFrameDelay = 0 -- frames to ignore after cursor/window toggles |
| 223 | +local accumDX, accumDY = 0, 0 -- accumulated raw mouse deltas (pixels) |
| 224 | +local lastEventTick = 0 |
| 225 | +local PI, RAD = math.pi, math.pi / 180 |
| 226 | + |
| 227 | +-- Tunables (adjust to taste) |
| 228 | +local RESUME_FRAMES = 5 -- frames to wait after cursor/window active |
| 229 | +local DEADZONE_PX = 0.15 -- ignore tiny jitters |
| 230 | +local MAX_EVENT_DELTA = 200 -- clamp a single event spike (px) |
| 231 | +local APPLY_K_60FPS = 0.42 -- how quickly to drain accumulator at 60fps (0..1) |
| 232 | + |
| 233 | +-- Normalize angle to [-PI, PI] |
| 234 | +local function normPI(a) |
| 235 | + a = a % (2 * PI) |
| 236 | + if a > PI then |
| 237 | + a = a - 2 * PI |
230 | 238 | end |
| 239 | + return a |
| 240 | +end |
231 | 241 |
|
232 | | - -- how far have we moved the mouse from the screen center? |
233 | | - aX = aX - width / 2 |
234 | | - aY = aY - height / 2 |
| 242 | +function freecamMouse(cX, cY, aX, aY) |
| 243 | + -- Gate input when the UI is up / focus lost |
| 244 | + if isCursorShowing() or isMTAWindowActive() or (not isMTAWindowFocused()) then |
| 245 | + mouseFrameDelay = RESUME_FRAMES |
| 246 | + accumDX, accumDY = 0, 0 |
| 247 | + return |
| 248 | + elseif mouseFrameDelay > 0 then |
| 249 | + mouseFrameDelay = mouseFrameDelay - 1 |
| 250 | + accumDX, accumDY = 0, 0 |
| 251 | + return |
| 252 | + end |
235 | 253 |
|
236 | | - --invert the mouse look if specified |
237 | | - if options.invertMouseLook then |
238 | | - aY = -aY |
239 | | - end |
| 254 | + -- How far from screen center? |
| 255 | + local dx = (aX - width * 0.5) |
| 256 | + local dy = (aY - height * 0.5) |
240 | 257 |
|
241 | | - rotX = rotX + aX * options.mouseSensitivity * 0.01745 |
242 | | - rotY = rotY - aY * options.mouseSensitivity * 0.01745 |
| 258 | + -- Optional invert |
| 259 | + if options.invertMouseLook then |
| 260 | + dy = -dy |
| 261 | + end |
243 | 262 |
|
244 | | - local PI = math.pi |
245 | | - if rotX > PI then |
246 | | - rotX = rotX - 2 * PI |
247 | | - elseif rotX < -PI then |
248 | | - rotX = rotX + 2 * PI |
| 263 | + -- Deadzone + spike clamp |
| 264 | + if math.abs(dx) < DEADZONE_PX then |
| 265 | + dx = 0 |
249 | 266 | end |
250 | | - |
251 | | - if rotY > PI then |
252 | | - rotY = rotY - 2 * PI |
253 | | - elseif rotY < -PI then |
254 | | - rotY = rotY + 2 * PI |
| 267 | + if math.abs(dy) < DEADZONE_PX then |
| 268 | + dy = 0 |
| 269 | + end |
| 270 | + if dx > MAX_EVENT_DELTA then |
| 271 | + dx = MAX_EVENT_DELTA |
| 272 | + elseif dx < -MAX_EVENT_DELTA then |
| 273 | + dx = -MAX_EVENT_DELTA |
255 | 274 | end |
256 | | - -- limit the camera to stop it going too far up or down - PI/2 is the limit, but we can't let it quite reach that or it will lock up |
257 | | - -- and strafeing will break entirely as the camera loses any concept of what is 'up' |
258 | | - if rotY < -PI / 2.05 then |
259 | | - rotY = -PI / 2.05 |
260 | | - elseif rotY > PI / 2.05 then |
261 | | - rotY = PI / 2.05 |
| 275 | + if dy > MAX_EVENT_DELTA then |
| 276 | + dy = MAX_EVENT_DELTA |
| 277 | + elseif dy < -MAX_EVENT_DELTA then |
| 278 | + dy = -MAX_EVENT_DELTA |
| 279 | + end |
| 280 | + |
| 281 | + -- Accumulate; application to rot happens in freecamMouseApply() every frame |
| 282 | + accumDX = accumDX + dx |
| 283 | + accumDY = accumDY + dy |
| 284 | + lastEventTick = getTickCount() |
| 285 | +end |
| 286 | + |
| 287 | +function freecamMouseApply(deltaTime) |
| 288 | + -- If UI pops up mid-frame, bail early |
| 289 | + if isCursorShowing() or isMTAWindowActive() or (not isMTAWindowFocused()) then |
| 290 | + mouseFrameDelay = RESUME_FRAMES |
| 291 | + accumDX, accumDY = 0, 0 |
| 292 | + return |
| 293 | + end |
| 294 | + |
| 295 | + -- Frame-rate–independent smoothing: convert APPLY_K_60FPS to current deltaTime |
| 296 | + -- factor = 1 - (1 - k)^(deltaTime * 60ms^-1) |
| 297 | + local factor = 1 - ((1 - APPLY_K_60FPS) ^ math.max(deltaTime / 16.666, 0.001)) |
| 298 | + |
| 299 | + -- Take a smooth chunk out of the accumulator |
| 300 | + local useDX = accumDX * factor |
| 301 | + local useDY = accumDY * factor |
| 302 | + accumDX = accumDX - useDX |
| 303 | + accumDY = accumDY - useDY |
| 304 | + |
| 305 | + -- Convert pixels -> radians (sensitivity is in degrees/pixel) |
| 306 | + local rpp = (options.mouseSensitivity or 1) * RAD |
| 307 | + |
| 308 | + -- Apply to camera (note Y is typically "pitch" and inverted vs screen Y) |
| 309 | + rotX = normPI(rotX + useDX * rpp) |
| 310 | + rotY = rotY - useDY * rpp |
| 311 | + |
| 312 | + -- Clamp pitch to avoid gimbal lock / upside-down strafing |
| 313 | + local limit = PI / 2.05 |
| 314 | + if rotY < -limit then |
| 315 | + rotY = -limit |
| 316 | + elseif rotY > limit then |
| 317 | + rotY = limit |
262 | 318 | end |
263 | 319 | end |
264 | 320 |
|
|
0 commit comments