Camera: front-camera transform and focus changes

Compute a more accurate front (selfie) camera transform and adjust front-camera params (FOV, distance, height). Replace apply_rear_camera_view with a unified apply_camera_view that selects the correct view_mode for ped/vehicle. Add block-look support to SkyPhoneFocus (Resolve now returns block_look) and make ApplyGameInputControls accept a block_look flag; main.lua tracks phone_block_look and passes it when disabling controls. Update related tests to reflect new camera math and input behavior.
This commit is contained in:
Leon.Schmidt
2026-08-16 17:16:01 +02:00
parent 91bf310778
commit 0a43a96744
6 changed files with 111 additions and 64 deletions
@@ -71,12 +71,9 @@ describe('Camera app controls', () => {
expect(cameraClient).toContain(
'SetFollowPedCamViewMode(first_person_view_mode)',
)
expect(cameraClient).toContain(
'SetFollowVehicleCamViewMode(first_person_view_mode)',
)
expect(cameraClient).toMatch(
/while camera_state\.active do[\s\S]*else\s+apply_rear_camera_view\(\)/,
)
expect(cameraClient).toContain('SetFollowVehicleCamViewMode(view_mode)')
expect(cameraClient).toContain('SetFollowPedCamViewMode(view_mode)')
expect(cameraClient).toMatch(/while camera_state\.active do[\s\S]*apply_camera_view\(\)/)
expect(cameraClient).not.toContain('next_view_apply')
expect(cameraClient).not.toContain('ultrawide_camera_handle')
expect(cameraClient).not.toContain('ensure_ultrawide_camera')
@@ -89,9 +86,16 @@ describe('Camera app controls', () => {
expect(cameraClient).toContain(
'if camera_state.locked or camera_state.front_camera then',
)
expect(cameraClient).toContain('capture_front_camera_transform')
expect(cameraClient).toContain('forward_vector * front_camera_distance')
expect(cameraClient).toContain('front_camera_target_offset')
expect(cameraClient).toContain('get_front_camera_transform')
expect(cameraClient).toContain('local front_camera_view_mode = 0')
expect(cameraClient).toContain('local front_camera_fov = 32.0')
expect(cameraClient).toContain('local front_camera_distance = 1.05')
expect(cameraClient).toContain('local head_position = GetPedBoneCoords')
expect(cameraClient).toContain('local dot = (to_camera.x * forward_vector.x)')
expect(cameraClient).toContain('front_camera_target_height')
expect(cameraClient).toContain(
'return { block_game = false, block_look = false, cursor = false, focused = true, game_input = true, keep_input = true }',
)
expect(cameraClient).toContain('SetCamCoord(')
expect(cameraClient).toContain('PointCamAtCoord(')
expect(cameraClient).not.toContain('SetCamRot(')
+26 -35
View File
@@ -2,11 +2,11 @@ local minimum_zoom = 0.5
local maximum_zoom = 3.0
local mouse_wheel_zoom_step = 0.08
local first_person_view_mode = 4
local front_camera_fov = 48.0
local front_camera_distance = 1.45
local front_camera_side_offset = 0.0
local front_camera_height = 0.06
local front_camera_target_height = -0.1
local front_camera_view_mode = 0
local front_camera_fov = 32.0
local front_camera_distance = 1.05
local front_camera_height = 0.05
local front_camera_target_height = 0.03
local blocked_camera_controls = {
0, -- INPUT_NEXT_CAMERA
22, -- INPUT_JUMP
@@ -43,8 +43,6 @@ local camera_state = {
focus_watcher = false,
front_camera = false,
front_camera_handle = nil,
front_camera_offset = nil,
front_camera_target_offset = nil,
game_input = false,
landscape = false,
locked = false,
@@ -85,34 +83,32 @@ local function set_flash_enabled(enabled)
end)
end
local function capture_front_camera_transform(ped)
local ped_position = GetEntityCoords(ped)
local function get_front_camera_transform(ped)
local head_position = GetPedBoneCoords(ped, 31086, 0.0, 0.0, 0.0)
local head_height = head_position.z - ped_position.z
local forward = GetEntityForwardVector(ped)
local forward_vector = vector3(forward.x, forward.y, 0.0)
local right_vector = vector3(forward_vector.y, -forward_vector.x, 0.0)
local camera_offset = (forward_vector * front_camera_distance)
+ (right_vector * front_camera_side_offset)
+ vector3(0.0, 0.0, head_height + front_camera_height)
local target_offset = (right_vector * (front_camera_side_offset * 0.25))
+ vector3(0.0, 0.0, head_height + front_camera_target_height)
return camera_offset, target_offset
local forward_vector = vector3(forward.x, forward.y, forward.z)
local camera_offset = forward_vector * front_camera_distance
local camera_position = head_position + camera_offset + vector3(0.0, 0.0, front_camera_height)
local to_camera = camera_position - head_position
local dot = (to_camera.x * forward_vector.x)
+ (to_camera.y * forward_vector.y)
+ (to_camera.z * forward_vector.z)
if dot < 0.0 then
camera_position = head_position - camera_offset + vector3(0.0, 0.0, front_camera_height)
end
local target_position = head_position + vector3(0.0, 0.0, front_camera_target_height)
return camera_position, target_position
end
local function apply_front_camera(ped)
if not camera_state.front_camera_handle or not DoesCamExist(camera_state.front_camera_handle) then
camera_state.front_camera_offset, camera_state.front_camera_target_offset =
capture_front_camera_transform(ped)
camera_state.front_camera_handle = CreateCam("DEFAULT_SCRIPTED_CAMERA", true)
SetCamFov(camera_state.front_camera_handle, front_camera_fov)
SetCamActive(camera_state.front_camera_handle, true)
RenderScriptCams(true, false, 0, true, true)
end
local ped_position = GetEntityCoords(ped)
local camera_position = ped_position + camera_state.front_camera_offset
local target_position = ped_position + camera_state.front_camera_target_offset
local camera_position, target_position = get_front_camera_transform(ped)
SetCamCoord(
camera_state.front_camera_handle,
camera_position.x,
@@ -133,20 +129,16 @@ local function clear_front_camera()
DestroyCam(camera_state.front_camera_handle, false)
end
camera_state.front_camera_handle = nil
camera_state.front_camera_offset = nil
camera_state.front_camera_target_offset = nil
end
local function apply_rear_camera_view()
if camera_state.front_camera then
return
end
local function apply_camera_view()
local ped = PlayerPedId()
local view_mode = camera_state.front_camera and front_camera_view_mode or first_person_view_mode
if IsPedInAnyVehicle(ped, false) then
SetFollowVehicleCamViewMode(first_person_view_mode)
SetFollowVehicleCamViewMode(view_mode)
return
end
SetFollowPedCamViewMode(first_person_view_mode)
SetFollowPedCamViewMode(view_mode)
end
local function restore_camera_view()
@@ -269,7 +261,7 @@ local function set_camera_active(active)
camera_state.previous_vehicle_view = GetFollowVehicleCamViewMode()
DisplayRadar(false)
set_camera_focus(true)
apply_rear_camera_view()
apply_camera_view()
TriggerEvent("sky_phone:animation:camera", {
active = true,
front = camera_state.front_camera,
@@ -284,9 +276,8 @@ local function set_camera_active(active)
HideHudAndRadarThisFrame()
if camera_state.front_camera then
apply_front_camera(PlayerPedId())
else
apply_rear_camera_view()
end
apply_camera_view()
Wait(0)
end
camera_state.enforcing = false
@@ -321,8 +312,8 @@ local function set_front_camera(active)
apply_front_camera(PlayerPedId())
else
clear_front_camera()
apply_rear_camera_view()
end
apply_camera_view()
TriggerEvent("sky_phone:animation:camera", {
active = true,
front = camera_state.front_camera,
+19 -5
View File
@@ -1,23 +1,34 @@
SkyPhoneFocus = {}
local blocked_phone_controls = { 19, 24, 140, 141, 142, 257, 263, 264 }
local blocked_phone_look_controls = { 1, 2, 3, 4, 5, 6 }
function SkyPhoneFocus.ApplyGameInputControls()
function SkyPhoneFocus.ApplyFocusedControls()
DisableAllControlActions(0)
DisablePlayerFiring(PlayerId(), true)
end
function SkyPhoneFocus.ApplyGameInputControls(block_look)
for _, control in ipairs(blocked_phone_controls) do
DisableControlAction(0, control, true)
end
if block_look then
for _, control in ipairs(blocked_phone_look_controls) do
DisableControlAction(0, control, true)
end
end
DisablePlayerFiring(PlayerId(), true)
end
function SkyPhoneFocus.Resolve(state)
if state.activity_suspended then
return { cursor = false, focused = false, game_input = false, keep_input = false }
return { block_game = false, cursor = false, focused = false, game_input = false, keep_input = false }
end
if state.call_focus then
return { cursor = true, focused = true, game_input = false, keep_input = false }
return { block_game = true, cursor = true, focused = true, game_input = false, keep_input = false }
end
if state.camera_active and not state.camera_nui_focused then
return { cursor = false, focused = true, game_input = false, keep_input = true }
return { block_game = false, block_look = false, cursor = false, focused = true, game_input = true, keep_input = true }
end
local game_input = state.is_open and state.allow_movement and not state.camera_active
local focused = state.is_open
@@ -25,8 +36,11 @@ function SkyPhoneFocus.Resolve(state)
or state.payphone_focus
or state.sim_picker_open
or (state.camera_active and state.camera_nui_focused)
local cursor = focused and not (game_input and state.cursor_disabled)
return {
cursor = focused and not (game_input and state.cursor_disabled),
block_game = cursor,
block_look = game_input and not state.cursor_disabled,
cursor = cursor,
focused = focused,
game_input = game_input,
keep_input = game_input,
+11 -3
View File
@@ -12,6 +12,8 @@ local active_call_payload = nil
local call_channel = 0
local nui_generation = 0
local activity_suspended = false
local phone_block_game = false
local phone_block_look = false
local phone_game_input = false
local phone_cursor_disabled = false
@@ -306,8 +308,10 @@ local function update_nui_focus()
})
SetNuiFocus(focus.focused, focus.cursor)
SetNuiFocusKeepInput(focus.keep_input)
phone_block_game = focus.block_game == true
phone_block_look = focus.block_look == true
phone_game_input = focus.game_input
if not phone_game_input then
if not phone_game_input and not phone_block_game then
phone_cursor_disabled = false
end
TriggerEvent("sky_phone:client:cameraFocusApplied", {
@@ -320,8 +324,12 @@ end
CreateThread(function()
while true do
if phone_game_input then
SkyPhoneFocus.ApplyGameInputControls()
if phone_game_input or phone_block_game then
if phone_block_game then
SkyPhoneFocus.ApplyFocusedControls()
else
SkyPhoneFocus.ApplyGameInputControls(phone_block_look)
end
if IsDisabledControlJustPressed(0, 19) then
phone_cursor_disabled = not phone_cursor_disabled
update_nui_focus()
+3 -3
View File
@@ -113,11 +113,11 @@ assert(response_from("camera:setFacing", { front = true }).success)
assert(camera_created and scripted_camera_rendering, "selfie mode must render a scripted camera")
assert(camera_coord and camera_target, "selfie mode must position and aim the camera")
assert(close_enough(camera_coord.x, 10.0))
assert(close_enough(camera_coord.y, 21.45))
assert(close_enough(camera_coord.z, 2.76))
assert(close_enough(camera_coord.y, 21.05))
assert(close_enough(camera_coord.z, 2.75))
assert(close_enough(camera_target.x, 10.0))
assert(close_enough(camera_target.y, 20.0))
assert(close_enough(camera_target.z, 2.6))
assert(close_enough(camera_target.z, 2.73))
assert(response_from("camera:setFacing", { front = false }).success)
assert(camera_destroyed and not scripted_camera_rendering, "rear mode must release the selfie camera")
+39 -9
View File
@@ -1,4 +1,5 @@
local disabled_controls = {}
local all_controls_disabled = false
local firing_disabled = false
function DisableControlAction(group, control, disabled)
@@ -6,6 +7,11 @@ function DisableControlAction(group, control, disabled)
disabled_controls[control] = true
end
function DisableAllControlActions(group)
assert(group == 0, "focused phone input must block the primary input group")
all_controls_disabled = true
end
function PlayerId()
return 7
end
@@ -53,7 +59,10 @@ assert(
local stationary_phone = resolve({ is_open = true })
assert(
stationary_phone.cursor and stationary_phone.focused and not stationary_phone.keep_input,
stationary_phone.cursor
and stationary_phone.focused
and stationary_phone.block_game
and not stationary_phone.keep_input,
"an open phone must block game input when movement is disabled"
)
@@ -62,8 +71,9 @@ assert(
movable_phone.cursor
and movable_phone.focused
and movable_phone.keep_input
and movable_phone.game_input,
"an open phone must keep GTA input when movement is enabled"
and movable_phone.game_input
and movable_phone.block_game,
"an open phone with the cursor active must block GTA hotkeys while keeping NUI input"
)
local cursor_disabled_phone = resolve({
@@ -74,19 +84,35 @@ local cursor_disabled_phone = resolve({
assert(
not cursor_disabled_phone.cursor
and cursor_disabled_phone.focused
and cursor_disabled_phone.keep_input,
and cursor_disabled_phone.keep_input
and cursor_disabled_phone.game_input
and not cursor_disabled_phone.block_game,
"toggling Alt must release the NUI cursor while preserving phone and GTA input"
)
SkyPhoneFocus.ApplyGameInputControls()
SkyPhoneFocus.ApplyFocusedControls()
assert(all_controls_disabled, "focused phone cursor must block every GTA control while typing")
assert(firing_disabled, "focused phone cursor must block attacks while typing")
all_controls_disabled = false
firing_disabled = false
SkyPhoneFocus.ApplyGameInputControls(true)
for _, control in ipairs({ 19, 24, 140, 141, 142, 257, 263, 264 }) do
assert(disabled_controls[control], ("phone control %d must remain disabled"):format(control))
end
assert(not disabled_controls[1] and not disabled_controls[2], "look controls must stay enabled")
for _, control in ipairs({ 1, 2, 3, 4, 5, 6 }) do
assert(disabled_controls[control], ("look control %d must be disabled while the phone cursor is active"):format(control))
end
assert(not disabled_controls[21] and not disabled_controls[22], "sprint and jump must stay enabled")
assert(not disabled_controls[30] and not disabled_controls[31], "movement axes must stay enabled")
assert(firing_disabled, "player attacks must remain disabled while the phone is open")
disabled_controls = {}
firing_disabled = false
SkyPhoneFocus.ApplyGameInputControls(false)
assert(not disabled_controls[1] and not disabled_controls[2], "Alt cursor toggle must restore camera look")
assert(firing_disabled, "player attacks must remain disabled after the cursor is toggled off")
local movable_notification = resolve({ allow_movement = true, notification_focus = true })
assert(
movable_notification.focused and not movable_notification.keep_input,
@@ -102,8 +128,9 @@ assert(
not camera_game_input.cursor
and camera_game_input.focused
and camera_game_input.keep_input
and not camera_game_input.game_input,
"camera movement must keep keyboard focus without retaining the NUI cursor"
and camera_game_input.game_input
and not camera_game_input.block_look,
"camera movement must keep keyboard focus and GTA movement without retaining the NUI cursor"
)
local focused_camera = resolve({
@@ -113,7 +140,10 @@ local focused_camera = resolve({
is_open = true,
})
assert(
focused_camera.cursor and focused_camera.focused and not focused_camera.keep_input,
focused_camera.cursor
and focused_camera.focused
and not focused_camera.keep_input
and not focused_camera.game_input,
"focused camera must override movement configuration until Space enables passthrough"
)