-- --------------------------------------------------------------------------- -- Scripted cameras for the pre-spawn screens. -- -- One camera object is reused throughout. Everything is driven from a single -- render thread that eases the live values towards target values, so a change -- requested by the UI never snaps. -- --------------------------------------------------------------------------- Cam = {} local cam = nil local mode = 'off' -- 'off' | 'shots' | 'orbit' | 'fly' local renderThread = false -- live and target orbit values local orbit = { angle = 180.0, targetAngle = 180.0, radius = 1.6, targetRadius = 1.6, height = 0.65, targetHeight = 0.65, focus = nil, -- entity to orbit pitch = 0.0, targetPitch = 0.0, } local shots = { list = {}, index = 0, startedAt = 0, duration = 22000 } local fly = { from = nil, to = nil, lookFrom = nil, lookTo = nil, startedAt = 0, duration = 3000, done = nil } local function ensureCam() if cam and DoesCamExist(cam) then return cam end cam = CreateCam('DEFAULT_SCRIPTED_CAMERA', true) SetCamActive(cam, true) RenderScriptCams(true, false, 0, true, true) return cam end local function lerp(a, b, t) return a + (b - a) * t end local function vlerp(a, b, t) return vector3(lerp(a.x, b.x, t), lerp(a.y, b.y, t), lerp(a.z, b.z, t)) end -- smoothstep, so fly-throughs start and end at rest local function ease(t) t = math.max(0.0, math.min(1.0, t)) return t * t * (3.0 - 2.0 * t) end local function startRenderThread() if renderThread then return end renderThread = true CreateThread(function() while mode ~= 'off' do local c = ensureCam() if mode == 'shots' then local shot = shots.list[shots.index] if shot then local t = (GetGameTimer() - shots.startedAt) / shots.duration if t >= 1.0 then shots.index = (shots.index % #shots.list) + 1 shots.startedAt = GetGameTimer() shot = shots.list[shots.index] t = 0.0 end -- a slow linear dolly reads as a helicopter, not a spline local pos = vlerp(shot.from, shot.to, t) SetCamCoord(c, pos.x, pos.y, pos.z) PointCamAtCoord(c, shot.look.x, shot.look.y, shot.look.z) end elseif mode == 'orbit' then orbit.angle = lerp(orbit.angle, orbit.targetAngle, 0.14) orbit.radius = lerp(orbit.radius, orbit.targetRadius, 0.10) orbit.height = lerp(orbit.height, orbit.targetHeight, 0.10) orbit.pitch = lerp(orbit.pitch, orbit.targetPitch, 0.10) local ent = orbit.focus if ent and DoesEntityExist(ent) then local base = GetEntityCoords(ent) local rad = math.rad(orbit.angle) local px = base.x + math.sin(rad) * orbit.radius local py = base.y + math.cos(rad) * orbit.radius local pz = base.z + orbit.height SetCamCoord(c, px, py, pz) PointCamAtCoord(c, base.x, base.y, base.z + orbit.height - orbit.pitch) end elseif mode == 'fly' then local t = ease((GetGameTimer() - fly.startedAt) / fly.duration) local pos = vlerp(fly.from, fly.to, t) local look = vlerp(fly.lookFrom, fly.lookTo, t) SetCamCoord(c, pos.x, pos.y, pos.z) PointCamAtCoord(c, look.x, look.y, look.z) if t >= 1.0 then mode = 'idle' if fly.done then local fn = fly.done fly.done = nil fn() end end end Wait(0) end renderThread = false end) end --- Slow establishing shots over the city, cycled forever. function Cam.Shots(list) shots.list = list shots.index = 1 shots.startedAt = GetGameTimer() mode = 'shots' ensureCam() SetCamFov(cam, 42.0) startRenderThread() end --- Orbit an entity. `snap` places the camera immediately instead of easing in. function Cam.Orbit(entity, radius, height, angle, snap) orbit.focus = entity orbit.targetRadius = radius or orbit.targetRadius orbit.targetHeight = height or orbit.targetHeight if angle then orbit.targetAngle = angle end if snap then orbit.radius = orbit.targetRadius orbit.height = orbit.targetHeight orbit.angle = orbit.targetAngle end mode = 'orbit' ensureCam() SetCamFov(cam, 34.0) startRenderThread() end --- Framing presets used by the creator's section tabs. function Cam.OrbitFrame(radius, height, pitch) orbit.targetRadius = radius orbit.targetHeight = height orbit.targetPitch = pitch or 0.0 end function Cam.Nudge(deltaDegrees) orbit.targetAngle = orbit.targetAngle + deltaDegrees end function Cam.SetAngle(deg) orbit.targetAngle = deg end function Cam.Angle() return orbit.targetAngle end --- Fly from wherever we are to a point looking at a target, then call `done`. function Cam.FlyTo(fromPos, fromLook, toPos, toLook, duration, done) fly.from = fromPos fly.to = toPos fly.lookFrom = fromLook fly.lookTo = toLook fly.duration = duration or 3000 fly.startedAt = GetGameTimer() fly.done = done mode = 'fly' ensureCam() startRenderThread() end function Cam.SetFov(fov) if cam and DoesCamExist(cam) then SetCamFov(cam, fov + 0.0) end end function Cam.Position() if cam and DoesCamExist(cam) then return GetCamCoord(cam) end return GetEntityCoords(PlayerPedId()) end --- Hand control back to the gameplay camera. With a duration the engine --- interpolates between the two, which hides the cut entirely. function Cam.Release(duration) mode = 'off' duration = duration or 0 RenderScriptCams(false, duration > 0, duration, true, true) if cam and DoesCamExist(cam) then DestroyCam(cam, true) end cam = nil end