In this tutorial, I will explain the basics of how to write a camera mode mod. This tutorial is not complete. I will update this tutorial as time permits. I will not attempt to teach Lua, geometry, or trigonometry. A basic understanding of at least Lua (or programming in general) and geometry are required to write a camera mode mod, as well as the meanings and uses for necessary and common variables. I will use examples from official camera modes. The thread "Custom camera modes - possible with 0.5.5" explains how to package camera mode mods. At the very least, a camera mode needs to define the camera's position, FoV (Field of View), and the direction the camera is facing. It will need to do this every time the camera is updated. The camera's position will need to be a point in a 3D Cartesian coordinate system (using variables x, y, and z). The FoV will need to be an integer. The camera's rotation will need to be a quaternion, which usually gets defined by comparing the camera's position with a target position. I personally haven't learned about quaternions, so I'm going to stick with using a target position in this tutorial. Here is an example of a simple and complete camera mode. It is the topDown camera mode that @tdev showed off in the thread "Custom camera modes - possible with 0.5.5." -- 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 --- This is an example of a custom camera mode local C = {} C.__index = C function C:init() --- when this is loaded, the config will be in self alread. I suggest to inspect it: -- dump(self) self.veloSmoother = newExponentialSmoothing(20, 1) self.lastDataPos = vec3() self.fov = self.fov or 20 --- this marks it as base mode. It needs to set position, rotation and fov correctly self.baseMode = true -- add it to the default camera modes? self.register = true end function C:update(data) -- get the data local ref = vec3(data.veh:getNodePosition(self.refNodes.ref)) local back = vec3(data.veh:getNodePosition(self.refNodes.back)) -- we need to manually smooth the velocity as its too spiky otherwise which results in bad camera movement local velo = self.veloSmoother:get((self.lastDataPos - data.pos):length()) self.lastDataPos = data.pos -- figure out the way the vehicle is oriented local dir = (ref - back):normalized() -- find out the target that we should look on local targetPos = data.pos + vec3(dir.x, dir.y, 0) * math.min(50, 70 * velo) -- and place the camera above it local camPos = targetPos + vec3(0, 0, (velo * 80) + 50) -- then look from camera position to target local qdir = quatFromDir((targetPos - camPos):normalized()) -- set the data, this needs to happen data.res.pos = camPos -- required, vec3() data.res.rot = qdir -- required, quat() data.res.fov = self.fov -- required data.res.targetPos = targetPos -- this is optional return true end -- DO NOT CHANGE CLASS IMPLEMENTATION BELOW return function(...) local o = ... or {} setmetatable(o, C) o:init() return o end To start off, a camera mode needs an initial function: function C:init() --- when this is loaded, the config will be in self alread. I suggest to inspect it: -- dump(self) self.veloSmoother = newExponentialSmoothing(20, 1) self.lastDataPos = vec3() self.fov = self.fov or 20 --- this marks it as base mode. It needs to set position, rotation and fov correctly self.baseMode = true -- add it to the default camera modes? self.register = true endIt is the place to define variables that will need to carry over across camera updates, such as the camera's last position, rotation, etc. It is run when the camera mode is loaded. Camera modes also need a function that is run on updates: function C:update(data) -- get the data local ref = vec3(data.veh:getNodePosition(self.refNodes.ref)) local back = vec3(data.veh:getNodePosition(self.refNodes.back)) -- we need to manually smooth the velocity as its too spiky otherwise which results in bad camera movement local velo = self.veloSmoother:get((self.lastDataPos - data.pos):length()) self.lastDataPos = data.pos -- figure out the way the vehicle is oriented local dir = (ref - back):normalized() -- find out the target that we should look on local targetPos = data.pos + vec3(dir.x, dir.y, 0) * math.min(50, 70 * velo) -- and place the camera above it local camPos = targetPos + vec3(0, 0, (velo * 80) + 50) -- then look from camera position to target local qdir = quatFromDir((targetPos - camPos):normalized()) -- set the data, this needs to happen data.res.pos = camPos -- required, vec3() data.res.rot = qdir -- required, quat() data.res.fov = self.fov -- required data.res.targetPos = targetPos -- this is optional return true endThis is where the camera's position, facing, and FoV are calculated, as well as anything else needed to calculate these. This example uses a target position (targetPos) to calculate the camera's facing. It also uses the vehicle's velocity (by subtracting vehicle's the last position (self.lastDataPos) by it's current position (data.pos) to move the target ahead of the vehicle, as well as to lift the camera as speed increases. The data that gets sent to the update function is as follows: data.veh - The current vehicle data.pos - The current position of the vehicle data.vid - The ID of the vehicle data.dt - a multiplier for camera movement controls Other useful functions include reset, reload, and lookback: (example from the default "orbit" camera) function C:reset() if self.cameraResetted ~= -1 then -- if a reload hasn't just happened self.camRot = vec3(self.defaultRotation) self.cameraResetted = 2 self.camDist = self.defaultDistance self.externalCamBase = nil -- for some reason this fixes things sometimes :| MoveManager.pitchUpSpeed = 0 MoveManager.pitchDownSpeed = 0 MoveManager.yawLeftSpeed = 0 MoveManager.yawRightSpeed = 0 else self.cameraResetted = 0 end end function C:reloaded() -- if a reset countdown is NOT already happening if self.cameraResetted <= 0 then -- signal to reset that a reload has happened self.cameraResetted = -1 end -- make sure this gets recalculated by invalidating it self.externalCamBase = nil -- global fov tuning self.fov = self.fov + settings.getValue('cameraFOVTune') or 0 self.fov = math.min(150, math.max(1, self.fov)) self.relaxation = settings.getValue('cameraOrbitRelaxation') or 3 end function C:lookback() if self.camRot.x > 0 then self.camRot.x = 0 else self.camRot.x = 180 end endThe reset function is run when the camera is reset using "5" on the numpad or clicking in the right stick on a Xinput controller. The reload function is run when the vehicle is reloaded. The lookback function is run when the lookback button is pressed (default: "1" on numpad).
I am sometimes available to give assistance if needed, and I'm sure staff will be willing to help as well. I personally will not give very much assistance with learning how to program or the mathematical principles involved. Updates to the tutorial itself will be accompanied by a summarized post to notify people watching the thread that the tutorial has been updated. I plan to explain manual camera movement with the next update to the tutorial. At some point, I will get around to writing documentation in the wiki, if a dev doesn't beat me too it.
Is it possible to get the camera to manually focus on a vehicle that has a broken beam for a few seconds, then to return back to the player car focus, I want a setup similar to, Burnout Takedown where the camera sets on the car you takedown then back to your car. Its what I outlined here: