Skip to content

Instantly share code, notes, and snippets.

@ikt32
Last active April 16, 2023 20:13
Show Gist options
  • Save ikt32/d5995d4fb6c4ffce0c1996f1eff2e716 to your computer and use it in GitHub Desktop.
Save ikt32/d5995d4fb6c4ffce0c1996f1eff2e716 to your computer and use it in GitHub Desktop.
BeamNG Dynamic horizon lock
-- This Source Code Form is subject to the terms of the bCDDL, v. 1.1.
-- If a copy of the bCDDL was not distributed with this
-- file, You can obtain one at http://beamng.com/bCDDL-1.1.txt
-- Modified for pitch damping by ikt
-- https://gist.github.com/E66666666/d5995d4fb6c4ffce0c1996f1eff2e716
-- Changelog:
-- 2023-01-31: Initial version for 0.27
-- 2023-04-15: Updated for 0.28
-- 2023-04-16: Fix pitch not reset on camera change
local vecY = vec3(0,1,0)
local vecZ = vec3(0,0,1)
local min, max, abs = math.min, math.max, math.abs
local pitchAvg = 0
local function lerp(a, b, f)
return a + f * (b - a)
end
local function rotateEuler(x, y, z, q)
q = q or quat()
q = quatFromEuler(0, z, 0) * q
q = quatFromEuler(0, 0, x) * q
q = quatFromEuler(y, 0, 0) * q
return q
end
local manualzoom = require('core/cameraModes/manualzoom')
local C = {}
C.__index = C
function C:init()
self.saveTimeout = nil
self.camLastRot = vec3()
self.rockPos = vec3()
self.cameraResetted = 3
self.camRot = vec3(0, 0, 0)
self.relativeYaw = 0
self.relativePitch = 0
self.fwdVel = 0
self.manualzoom = manualzoom()
self:onVehicleCameraConfigChanged()
self:onSettingsChanged()
self.vehicleIsMoving = false
pitchAvg = 0
end
function C:onVehicleCameraConfigChanged()
--trigger reloading of new vehicle from settings
self.seatPosition = nil
self.seatRotation = 0
--trigger gathering of new initial node position
self.camPosInitialLocal = nil
self.cameraResetted = 3
pitchAvg = 0
end
function C:onSettingsChanged()
self.physicsFactor = settings.getValue('cameraDriverPhysics') / 100 -- 0..1 multiplier
self.autocenter = settings.getValue('cameraDriverAutocenter')
self.allowSeatAdjustments = settings.getValue('cameraDriverAllowSeatAdjustments')
self.stableHorizonFactor = settings.getValue('cameraDriverStableHorizon') / 100 -- 0..1 multiplier
self.lookAheadAngle = settings.getValue("cameraDriverLookAheadAngle") / 100
self.lookAheadSmoothness = settings.getValue("cameraDriverLookAheadSmoothness") / 100
self.manualzoom:init(settings.getValue('cameraDriverFov'), nil, nil, "ui.camera.fovDriver")
end
function C:resetSeat()
self.rockPos = vec3()
self.seatPosition = vec3()
self.seatRotation = 0
self.saveTimeout = 0 -- trigger save instantaneously
end
function C:resetSeatAll()
self.rockPos = vec3()
self.seatPosition = vec3()
self.seatRotation = 0
self.saveTimeout = nil -- disable any ongoing auto-save
settings.setValue('cameraDriverVehicleConfigs', "{}")
end
function C:reset()
self.relativeYaw = 0
self.relativePitch = 0
self.rockPos = vec3()
pitchAvg = 0
end
local dxSmoother = newTemporalSmoothing(3,1)
local dySmoother = newTemporalSmoothing(3,1)
local dzSmoother = newTemporalSmoothing(3,1)
local currentCarPos, prevCarPos = vec3(), vec3()
local rot = vec3()
local left, ref, back = vec3(), vec3(), vec3()
local carLeft, carFwd, carUp, carDir = vec3(), vec3(), vec3(), vec3()
local nodePos = vec3()
local camUp = vec3()
local camPosLocal, combinedPos, rotationOffset = vec3(), vec3(), vec3()
local intermediateCamPos = vec3()
function C:update(data)
local carPos = data.pos
-- retrieve camera node (except when resetting, because data is not reliable then)
self.cameraResetted = max(self.cameraResetted - 1, 0)
if self.cameraResetted > 0 then
data.res.pos = carPos
data.res.rot = quatFromDir(vecY, vecZ)
return
end
local camNodeID, rightHandDrive = core_camera.getDriverData(data.veh)
-- read seat adjustment settings
if self.seatPosition == nil then
local vehicleName = data.veh:getJBeamFilename()
local vehConfigs = settings.getValue('cameraDriverVehicleConfigs')
if type(vehConfigs) ~= "string" then vehConfigs = "{}" end
vehConfigs = vehConfigs:gsub("'",'"') -- fix INI values that passed through javascript (e.g. when opening Options menu)
vehConfigs = jsonDecode(vehConfigs) -- and then deserialize, so we can follow the user settings
local vehConfig = vehConfigs[vehicleName] or {0,0,0}
self.seatPosition = vec3(0, vehConfig[2], vehConfig[3])
self.seatRotation = vehConfig[1]
end
-- process mouse rotation input
self.relativeYaw = clamp(self.relativeYaw + 0.1*MoveManager.yawRelative , -1, 1)
self.relativePitch = clamp(self.relativePitch - 0.3*MoveManager.pitchRelative, -1, 1)
-- process kbd/pad rotation input
local absYaw = 0
local absPitch = 0
local filter = core_camera.getLastFilter()
if self.autocenter and data.veh then
currentCarPos:set(data.veh:getPositionXYZ())
if prevCarPos then
local newValue = (prevCarPos:distance(currentCarPos) / data.dt) > 0.3
if newValue and newValue ~= self.vehicleIsMoving then
-- send back to center
self.relativeYaw = 0
self.relativePitch = 0
end
self.vehicleIsMoving = newValue
end
prevCarPos:set(currentCarPos)
end
if self.autocenter and self.vehicleIsMoving then
-- camera will go back to center as soon as the controller is released
absPitch = MoveManager.pitchDown - MoveManager.pitchUp
absYaw = MoveManager.yawRight - MoveManager.yawLeft
if filter == FILTER_KBD or filter == FILTER_KBD2 then
-- keyboard look-to-rear key combo (press both left+right to look back)
absYaw = 0.5*(MoveManager.yawRight - MoveManager.yawLeft)
if MoveManager.yawLeft > 0 and MoveManager.yawRight > 0 then
absYaw = absYaw + sign(self.camRot.x)
end
end
else
-- camera will stay where it is when the controller is released
self.relativeYaw = self.relativeYaw + (MoveManager.yawRight - MoveManager.yawLeft) * 0.01
self.relativePitch = self.relativePitch + (MoveManager.pitchDown - MoveManager.pitchUp) * 0.04
end
local sideInput = self.relativeYaw + absYaw
local vertInput = self.relativePitch + absPitch
-- convert input into angles
local maxAngle = 160 -- max degrees the head will be looking back
self.camRot.x = sideInput * maxAngle
if data.lookBack then self.camRot.x = rightHandDrive and -maxAngle or maxAngle end
self.camRot.y = vertInput * 20
if vertInput > 0 then self.camRot.y = self.camRot.y * 2 end
-- orientation
rot:set(math.rad(self.camRot.x), math.rad(self.camRot.y), math.rad(self.camRot.z))
local ratiox = 1 / (data.dt * 50)
local ratioy = 1 / (data.dt * 10)
if not self.autocenter then ratioy = 1 / (data.dt * 50) end
rot.x = 1 / (ratiox + 1) * rot.x + (ratiox / (ratiox + 1)) * self.camLastRot.x
rot.y = 1 / (ratioy + 1) * rot.y + (ratioy / (ratioy + 1)) * self.camLastRot.y
self.camLastRot:set(rot)
self.camRot:set(math.deg(rot.x), math.deg(rot.y) - self.seatRotation, math.deg(rot.z))
left:set(data.veh:getNodePositionXYZ(self.refNodes.left))
ref:set(data.veh:getNodePositionXYZ(self.refNodes.ref))
back:set(data.veh:getNodePositionXYZ(self.refNodes.back))
carLeft:setSub2(left, ref); carLeft:normalize()
carFwd:setSub2(back, ref); carFwd:normalize()
carUp:setCross(carLeft, back); carUp:normalize()
-- Smooth velocity using rock on a string algorithm
self.rockPos = self.rockPos - data.vel * data.dt
local projectedRockPos = self.rockPos:projectToOriginPlane(carUp):resized(min(self.rockPos:length(), self.lookAheadSmoothness))
-- When vehicle flips, left and right sides of it flip aswell. To prevent this from happening
-- We tempereraly stop projecting the rock position
if self.rockPos:distance(projectedRockPos) < 0.1 then
self.rockPos = projectedRockPos
else
self.rockPos = self.rockPos:resized(min(self.rockPos:length(), self.lookAheadSmoothness))
end
-- Stable horizon
carDir = quatFromDir(carFwd, carUp)
nodePos:set(data.veh:getNodePositionXYZ(camNodeID or 0))
carUp:set(-(push3(carFwd):cross(carLeft))); carUp:normalize()
-- Average the carDirs pitch
pitchAvg = lerp(pitchAvg, carFwd.z, 2.5 * data.dt)
local carFwdMod = vec3(1*carFwd.x, 1*carFwd.y, pitchAvg)
--local camDir = quatFromDir(-push3(carFwd))
local camDir = quatFromDir(-push3(carFwdMod))
camUp:setRotate(camDir, vecZ)
local carRoll = math.atan2(push3(camUp):dot(-push3(carLeft)), camUp:dot(carUp))
local carRollFactor = 1 - self.stableHorizonFactor * smootheststep(clamp(1.42*carUp.z, 0, 1))
local camRoll = carRoll * carRollFactor
-- Look-ahead angle
self.fwdVel = lerp(self.fwdVel, -data.vel:length() * data.vel:normalized():dot(carFwd), data.dt * ( 1.5 - self.lookAheadSmoothness))
local nRockPos = (carFwd * (1 - self.rockPos:length() / self.lookAheadSmoothness) + self.rockPos):normalized()
local lookAheadAngle = math.atan2(nRockPos.x * carFwd.y - nRockPos.y * carFwd.x, nRockPos.x * carFwd.x + nRockPos.y * carFwd.y)
self.rockPos = self.rockPos * ((1 - data.dt * 0.1) * clamp(self.fwdVel / 20, 0, 1))
lookAheadAngle = clamp(lookAheadAngle, -1.1, 1.1) * self.lookAheadAngle * clamp(self.fwdVel / 15, 0, 1)
-- Pitch smoothing
--local roll, pitch, yaw = data.veh:getRollPitchYawAngularVelocity()
local pitch = 0
camDir = rotateEuler(math.rad(self.camRot.x) + lookAheadAngle, math.rad(self.camRot.y) - pitch, camRoll, camDir) -- stable hood line
local notifiedFov = self.manualzoom:update(data)
if notifiedFov then
self.saveTimeout = 1
end
-- physics-based position
camPosLocal:setRotate(carDir:inversed(), nodePos)
-- static position
if self.camPosInitialLocal == nil then ---- FIXME this can happen at any point, e.g. when vehicle is damaged
self.camPosInitialLocal = vec3(camPosLocal)
end
-- physics+static position combination
combinedPos:set(push3(camPosLocal)*(0+self.physicsFactor) + push3(self.camPosInitialLocal)*(1-self.physicsFactor))
-- left/right head sticking out position
local minAngle = 70 -- starting angle when driver will start looking back
local headOut = clamp(abs(self.camRot.x) - minAngle, 0, maxAngle) / (maxAngle - minAngle) -- how much the head is looking back, from 0 to 1
local lateralFactor = headOut
local forwardFactor = headOut
local verticalFactor = headOut
local lateralOffset = 0.26
local forwardOffset = -0.075
local verticalOffset = -0.02
local lookingThroughWindow = rightHandDrive == (self.camRot.x > 0)
if lookingThroughWindow then
local origSpawnAABB = data.veh:getSpawnLocalAABB()
forwardFactor = clamp(forwardFactor * 1.75, 0, 1)
verticalFactor = clamp(verticalFactor * 1.00, 0, 1)
forwardOffset = -0.3
lateralOffset = 0.5
local minExt = origSpawnAABB.minExtents
local maxExt = origSpawnAABB.maxExtents
local margin = (maxExt.x - minExt.x)/2 - abs(self.camPosInitialLocal.x-(maxExt.x + minExt.x)/2) -- distance to boundingbox lateral
lateralOffset = min(0.6, margin)
verticalOffset = -0.1
end
rotationOffset:set(
lateralOffset * lateralFactor * sign(-self.camRot.x), -- stick head out (or towards center)
forwardOffset * forwardFactor, -- dodge the B-pillar (or bucket seat/head rest)
verticalOffset * verticalFactor -- dodge the roof
)
-- up/down head bobbing, to more easily discover occluded switches in the cockpit
local headWiggleZ = clamp(self.camRot.y / 20, -1, 1)
local maxWiggleZ = 0.05
local wiggleZ = headWiggleZ * maxWiggleZ
rotationOffset.z = rotationOffset.z + wiggleZ
-- left/right head bobbing, to more easily discover occluded switches in the cockpit
local headWiggleX = clamp(self.camRot.x / 40, -1, 1)
local maxWiggleX = 0.15
local wiggleX = headWiggleX * maxWiggleX
rotationOffset.x = sign(-self.camRot.x) * max(abs(rotationOffset.x), abs(wiggleX))
-- apply seat adjustment
local dr, dy, dz = 0, 0 ,0
if self.allowSeatAdjustments then
dr = dxSmoother:getCapped(MoveManager.left - MoveManager.right , data.dt)
dy = dySmoother:getCapped(MoveManager.backward - MoveManager.forward, data.dt)
dz = dzSmoother:getCapped(MoveManager.up - MoveManager.down , data.dt)
local adjustedSpeed = data.fastSpeedModifier and data.speed * 3 or data.speed
local pdr = dr * data.dt * adjustedSpeed * 2
local pdy = dy * data.dt * adjustedSpeed / 50
local pdz = dz * data.dt * adjustedSpeed / 50
local posLimit = 0.4
self.seatRotation = clamp(self.seatRotation + pdr, -30, 20)
self.seatPosition.y = clamp(self.seatPosition.y + pdy, -posLimit, posLimit)
self.seatPosition.z = clamp(self.seatPosition.z + pdz, -posLimit, posLimit)
end
if self.saveTimeout ~= nil then
self.saveTimeout = self.saveTimeout - data.dt
end
if dr ~= 0 then
ui_message({txt='ui.camera.driverTiltAdjusted', context={vehicleName = data.veh:getJBeamFilename(), angle=self.seatRotation}}, 2, 'cameramode')
self.saveTimeout = 1
end
if dy ~= 0 or dz ~= 0 then
ui_message({txt='ui.camera.driverPositionAdjusted', context={vehicleName = data.veh:getJBeamFilename(), y=self.seatPosition.y, z=self.seatPosition.z}}, 2, 'cameramode')
self.saveTimeout = 1
end
-- application
intermediateCamPos:set(push3(combinedPos) + self.seatPosition + rotationOffset)
data.res.pos:setRotate(carDir, intermediateCamPos)
data.res.pos:setAdd(carPos)
data.res.rot = camDir
-- save fov/seat settings on timeout
if self.saveTimeout and self.saveTimeout <= 0 then
local vehConfig = { self.seatRotation, self.seatPosition.y, self.seatPosition.z }
if vehConfig[1] == 0 and vehConfig[2] == 0 and vehConfig[3] == 0 then vehConfig = nil end
local vehicleName = data.veh:getJBeamFilename()
local vehConfigs = settings.getValue('cameraDriverVehicleConfigs')
if type(vehConfigs) ~= "string" then vehConfigs = "{}" end
vehConfigs = vehConfigs:gsub("'",'"') -- fix INI values that passed through javascript (e.g. when opening Options menu)
vehConfigs = jsonDecode(vehConfigs) -- and then deserialize, so we can follow the user settings
vehConfigs[vehicleName] = vehConfig
settings.setValue('cameraDriverVehicleConfigs', jsonEncode(vehConfigs))
settings.setValue('cameraDriverFov', data.res.fov)
self.saveTimeout = nil
end
end
function C:setRefNodes(centerNodeID, leftNodeID, backNodeID)
self.refNodes = self.refNodes or {}
self.refNodes.ref = centerNodeID
self.refNodes.left = leftNodeID
self.refNodes.back = backNodeID
end
function C:mouseLocked(locked)
if locked then return end
if self.autocenter and self.vehicleIsMoving then
self.relativeYaw = 0
self.relativePitch = 0
end
end
-- DO NOT CHANGE CLASS IMPLEMENTATION BELOW
return function(...)
local o = ... or {}
setmetatable(o, C)
o:init()
return o
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment