Files
fs2open.github.com/code/camera/camera.cpp
T
Fabian WoltermannandClaude Opus 5 9ec157833b Add command-line option overrides to options system (#7561)
* Add command-line option overrides to options system

- Extend `OptionsManager` to support storing and displaying reasons for overrides.
- Update related code paths to prioritize command-line overrides, ensure correct option resolution, and display override details in the UI.
- Add unit tests for override behavior and round-trip validation.
- Remove unused legacy functions (`removeVSyncOption`, `removeWindowModeOption`) in favor of the new system.

* Add unit tests for `Graphics.Anisotropy` option validation with and without OpenGL

* Add support for associating command-line option names to `OptionsManager` overrides

- Update HDR, shadow rendering, and graphics API option overrides to include their respective command-line flag names.
- Refactor `Graphics.RenderAPI` to use the `GraphicsAPI` enum instead of raw integers for better type safety and clarity.
- Ensure Vulkan support logic and fallback behavior function correctly with the updated options system.
- Update unit tests to validate override functionality and enum-based API configuration.

* Remove unused `removeVSyncOption` function

* Apply the PR review feedback for the command-line option overrides

Delete the unit-test run artifacts that this branch committed by mistake, and
add .gitignore rules for the files that the unittests binary writes.

Give an override priority over an in-session edit again. The options UI now
shows a disabled control for an overridden option, and setConfigValue()
refuses an edit for an overridden key. Before this change, a save wrote the
edit to the config file and applied it to the bound global, but getValue()
kept returning the override. The game and the options menu then showed
different values for the rest of the session.

Add a setOverride() overload that takes a JSON value. Every command-line flag
now builds its override with json_integer, json_real, json_true, json_false
or json_pack, in place of a hand-formatted string.

Make Graphics.CockpitFOV and Graphics.CockpitFOVToggle independent of the
order in which the options manager loads them, and remove the forced override
on the player's toggle preference. The toggle listener also replaces an
identical bind_to() call that made the listener dead code.

Add 16 to the values of Graphics.MSAASamples. The -msaa flag accepts 16, so an
override with that value made printValues() report an invalid value.

Keep the render API that the caller gives to gr_init(). Only a Default value
now comes from the Graphics.RenderAPI option, so FRED and the unit tests keep
the backend that they ask for.

Make the default_func of Graphics.Shadows and Graphics.ShadowRenderMethod
return their global. A constant discarded the value that -enable_shadows and
the mod table set. Set Max_rt_shadow_local_lights to 4, because 3 is not one
of the values that the option offers.

Also: resolve -lang one time, share the render API name with version.cpp,
correct the stale comments, and add unit tests with a fixture that clears the
overrides after each test.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UGh4ZM2u4oUGSbrP122Uyr

* Fix the order-dependent crash in the MSAA option test

The test `MsaaOverrideAcceptsEverySampleCountTheFlagAllows` calls
`getValidValues()`. `Option<T>` makes a display string for each value, and the
display function of `Graphics.MSAASamples` calls `XSTR()`. `XSTR()` runs
`Int3()` if `lcl_xstr_init()` did not run before. Only `FSTestFixture` runs it,
and `Xstr_inited` stays set for the remainder of the process. Thus the shuffled
test order decided if the test passed or if it stopped the test binary with
SIGILL.

Move that one test to a new fixture that derives from `FSTestFixture`. The
fixture initializes localization, and it also clears the overrides after each
test. Refresh the comment that told that the test binary never runs
`lcl_init()`.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UGh4ZM2u4oUGSbrP122Uyr

---------

Co-authored-by: Claude Opus 5 <[email protected]>
2026-09-10 13:14:18 +02:00

1341 lines
35 KiB
C++

#include "camera/camera.h"
#include "globalincs/alphacolors.h"
#include "globalincs/linklist.h"
#include "globalincs/systemvars.h" //VM_FREECAMERA etc
#include "graphics/matrix.h"
#include "hud/hud.h" //hud_get_draw
#include "math/vecmat.h"
#include "mod_table/mod_table.h"
#include "model/model.h" //polymodel, model_get
#include "options/Option.h"
#include "parse/parselo.h"
#include "parse/sexp_container.h"
#include "playerman/player.h" //player_get_padlock_orient
#include "ship/ship.h" //compute_slew_matrix
//*************************IMPORTANT GLOBALS*************************
fov_t VIEWER_ZOOM_DEFAULT = DEFAULT_FOV;
fov_t COCKPIT_ZOOM_DEFAULT = DEFAULT_FOV;
float Sexp_fov = 0.0f;
warp_camera Warp_camera;
//*************************OTHER STUFF*************************
//Some global vars
SCP_vector<subtitle> Subtitles;
SCP_vector<camera*> Cameras;
//Preset cameras
camid Current_camera;
camid Main_camera;
asymmetric_fov operator* (const asymmetric_fov& left, const float& right) {
return asymmetric_fov{ left.left * right, left.right * right, left.up * right, left.down * right };
}
asymmetric_fov operator+ (const asymmetric_fov& left, const float& right) {
float ar = (left.up - left.down) / (left.right - left.left);
return asymmetric_fov{ left.left - right * 0.5f, left.right + right * 0.5f, left.up + right * 0.5f * ar, left.down - right * 0.5f * ar };
}
asymmetric_fov operator- (const asymmetric_fov& left, const float& right) {
float ar = (left.up - left.down) / (left.right - left.left);
return asymmetric_fov{ left.left + right * 0.5f, left.right - right * 0.5f, left.up - right * 0.5f * ar, left.down + right * 0.5f * ar };
}
#define APPLY_TO_FOV_T(op, name) \
struct apply_##name##_to_fov { \
float r; \
template<typename T> \
fov_t operator()(const T& l) { \
return fov_t(l op r); \
} \
}; \
fov_t operator op (const fov_t& zoom, const float& scale) { \
return std::visit(apply_##name##_to_fov{scale}, zoom); \
}
APPLY_TO_FOV_T(*, mul)
APPLY_TO_FOV_T(+, add)
APPLY_TO_FOV_T(-, sub)
// Used to set the default value for in-game options
static float fov_default = DEFAULT_FOV;
static float cockpit_fov_default = DEFAULT_FOV;
static bool cockpit_fov_toggle_default = false;
bool Use_cockpit_fov = false;
static SCP_string fov_display(float val)
{
auto degrees = fl_degrees(val);
SCP_string out;
sprintf(out, u8"%.1f\u00B0", degrees);
return out;
}
static void parse_fov_func()
{
float value;
stuff_float(&value);
CLAMP(value, 0.436332f, 1.5708f);
fov_default = value;
}
static void parse_cockpit_fov_func()
{
float value;
stuff_float(&value);
CLAMP(value, 0.436332f, 1.5708f);
cockpit_fov_default = value;
}
static void parse_cockpit_fov_toggle_func()
{
bool value;
stuff_boolean(&value);
cockpit_fov_toggle_default = value;
}
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
auto FovOption = options::OptionBuilder<float>("Graphics.FOV",
std::pair<const char*, int>{"Field Of View", 1703},
std::pair<const char*, int>{"The vertical field of view", 1704})
.category(std::make_pair("Graphics", 1825))
.range(0.436332f, 1.5708f)
.change_listener([](const float& val, bool) {
VIEWER_ZOOM_DEFAULT = val;
return true;
})
.display(fov_display)
.default_func([]() { return fov_default; })
.level(options::ExpertLevel::Advanced)
.importance(60)
.parser(parse_fov_func)
.finish();
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
auto CockpitFovOption = options::OptionBuilder<float>("Graphics.CockpitFOV",
std::pair<const char*, int>{"Cockpit Field Of View", 1840},
std::pair<const char*, int>{"The vertical field of view for cockpit rendering. Only works if cockpits are active and cockpit FOV toggle is turned on.", 1841})
.category(std::make_pair("Graphics", 1825))
.range(0.436332f, 1.5708f)
.change_listener([](const float& val, bool) {
// Graphics.CockpitFOVToggle has its own listener that re-applies this value, so the
// result is the same whichever of the two options the manager happens to load first.
COCKPIT_ZOOM_DEFAULT = Use_cockpit_fov ? fov_t(val) : VIEWER_ZOOM_DEFAULT;
return true;
})
.display(fov_display)
.default_func([]() { return cockpit_fov_default; })
.level(options::ExpertLevel::Advanced)
.importance(62)
.parser(parse_cockpit_fov_func)
.finish();
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
auto CockpitFOVToggleOption = options::OptionBuilder<bool>("Graphics.CockpitFOVToggle",
std::pair<const char*, int>{"Cockpit FOV Toggle", 1838},
std::pair<const char*, int>{"Whether or not to use a different FOV for cockpit rendering from normal rendering", 1839})
.category(std::make_pair("Graphics", 1825))
.default_func([]() {return cockpit_fov_toggle_default;})
// This does the work of bind_to(&Use_cockpit_fov) and then re-applies Graphics.CockpitFOV,
// because that option's listener needs Use_cockpit_fov to already hold its final value.
// Doing it here makes the pair of options independent of the order the manager loads them in.
.change_listener([](bool val, bool) {
Use_cockpit_fov = val;
COCKPIT_ZOOM_DEFAULT = val ? fov_t(CockpitFovOption->getValue()) : VIEWER_ZOOM_DEFAULT;
return true; // This option will always persist so we never return false
})
.level(options::ExpertLevel::Advanced)
.importance(61)
.parser(parse_cockpit_fov_toggle_func)
.finish();
//*************************CLASS: camera*************************
//This is where the camera class begins! :D
camera::camera(const char *in_name, int in_signature)
{
set_name(in_name);
sig = in_signature;
reset();
}
camera::~camera()
{
//Check if this is in use
if(Current_camera.getSignature() == this->sig)
{
Current_camera = camid();
}
}
void camera::clear()
{
strcpy_s(name, "");
sig = -1;
reset();
}
void camera::reset()
{
flags = CAM_DEFAULT_FLAGS;
object_host = object_target = object_h();
object_host_submodel = object_target_submodel = -1;
c_fov = VIEWER_ZOOM_DEFAULT;
c_pos = vmd_zero_vector;
c_ori = vmd_identity_matrix;
func_custom_position = NULL;
func_custom_orientation = NULL;
fov.clear();
fov.set(g3_get_hfov(VIEWER_ZOOM_DEFAULT));
pos_x.clear();
pos_y.clear();
pos_z.clear();
for(int i = 0; i < 9; i++)
{
ori[i].clear();
ori[i].set(vmd_identity_matrix.a1d[i]);
}
}
void camera::set_name(const char *in_name)
{
if(in_name != NULL)
strncpy(name, in_name, NAME_LENGTH-1);
}
void camera::set_fov(fov_t in_fov, float in_fov_time, float in_fov_acceleration_time, float in_fov_deceleration_time)
{
if(in_fov_time == 0.0f && in_fov_acceleration_time == 0.0f && in_fov_deceleration_time == 0.0f)
{
flags |= CAM_STATIONARY_FOV;
c_fov = in_fov;
return;
}
flags &= ~CAM_STATIONARY_FOV;
if(g3_get_hfov(in_fov) <= 0.0f)
{
fov.setVD(in_fov_time, in_fov_acceleration_time, 0.0f);
}
fov.setAVD(g3_get_hfov(in_fov), in_fov_time, in_fov_acceleration_time, in_fov_deceleration_time, 0.0f);
}
void camera::set_object_host(object *objp, int n_object_host_submodel)
{
object_host = object_h(objp);
object_host_submodel = n_object_host_submodel;
set_custom_position_function(NULL);
set_custom_orientation_function(NULL);
if (n_object_host_submodel >= 0 && objp != nullptr && objp->type == OBJ_SHIP)
{
ship_subsys* ssp = GET_FIRST(&Ships[objp->instance].subsys_list);
while (ssp != END_OF_LIST(&Ships[objp->instance].subsys_list))
{
if (ssp->system_info->subobj_num == n_object_host_submodel)
{
if (ssp->system_info->type == SUBSYSTEM_TURRET)
{
set_custom_position_function(get_turret_cam_pos);
set_custom_orientation_function(get_turret_cam_orient);
}
}
ssp = GET_NEXT(ssp);
}
}
else if (Use_model_eyepoint_for_set_camera_host && object_host.isValid())
{
const object* host = object_host.objp();
vec3d eye_pos;
matrix eye_orient;
object_get_eye(&eye_pos, &eye_orient, host, false, true, true);
set_position(&eye_pos);
set_rotation(&eye_orient);
}
}
void camera::set_object_target(object *objp, int n_object_target_submodel)
{
if(objp == NULL)
object_target = object_h();
object_target = object_h(objp);
object_target_submodel = n_object_target_submodel;
}
/**
* Custom function receives the already-modified current position value.
* It should be replaced or added to as the custom function modifier sees fit.
*/
void camera::set_custom_position_function(void (*n_func_custom_position)(camera* /*cam*/, vec3d* /*camera_pos*/))
{
func_custom_position = n_func_custom_position;
}
/**
* Custom function receives the already-modified current orientation value.
* It should be replaced or added to as the custom function modifier sees fit.
*/
void camera::set_custom_orientation_function(void (*n_func_custom_orientation)(camera* /*cam*/, matrix* /*camera_ori*/))
{
func_custom_orientation = n_func_custom_orientation;
}
void camera::set_position(vec3d *in_position, float in_translation_time, float in_translation_acceleration_time, float in_translation_deceleration_time, float in_end_velocity)
{
if(in_position != NULL && in_translation_time == 0.0f && in_translation_acceleration_time == 0.0f && in_translation_deceleration_time == 0.0f)
{
c_pos = *in_position;
pos_x.set(c_pos.xyz.x);
pos_y.set(c_pos.xyz.y);
pos_z.set(c_pos.xyz.z);
flags |= CAM_STATIONARY_POS;
return;
}
flags &= ~CAM_STATIONARY_POS;
if(in_position == NULL)
{
pos_x.setVD(in_translation_time, in_translation_acceleration_time, in_end_velocity);
pos_y.setVD(in_translation_time, in_translation_acceleration_time, in_end_velocity);
pos_z.setVD(in_translation_time, in_translation_acceleration_time, in_end_velocity);
return;
}
pos_x.setAVD(in_position->xyz.x, in_translation_time, in_translation_acceleration_time, in_translation_deceleration_time, in_end_velocity);
pos_y.setAVD(in_position->xyz.y, in_translation_time, in_translation_acceleration_time, in_translation_deceleration_time, in_end_velocity);
pos_z.setAVD(in_position->xyz.z, in_translation_time, in_translation_acceleration_time, in_translation_deceleration_time, in_end_velocity);
}
void camera::set_rotation(matrix *in_orientation, float in_rotation_time, float in_rotation_acceleration_time, float in_rotation_deceleration_time)
{
if(in_orientation != NULL && in_rotation_time == 0.0f && in_rotation_acceleration_time == 0.0f && in_rotation_deceleration_time == 0.0f)
{
c_ori = *in_orientation;
for(int i = 0; i < 9; i++)
ori[i].set(in_orientation->a1d[i]);
flags |= CAM_STATIONARY_ORI;
return;
}
flags &= ~CAM_STATIONARY_ORI;
if(in_orientation == NULL)
{
for(int i = 0; i < 9; i++)
ori[i].setVD(in_rotation_time, in_rotation_acceleration_time, 0.0f);
return;
}
for(int i = 0; i < 9; i++)
ori[i].setAVD(in_orientation->a1d[i], in_rotation_time, in_rotation_acceleration_time, in_rotation_deceleration_time, 0.0f);
}
void camera::set_rotation(angles *in_angles, float in_rotation_time, float in_rotation_acceleration_time, float in_rotation_deceleration_time)
{
matrix mtx = IDENTITY_MATRIX;
vm_rotate_matrix_by_angles(&mtx, in_angles);
this->set_rotation(&mtx, in_rotation_time, in_rotation_acceleration_time, in_rotation_deceleration_time);
}
void camera::set_rotation_facing(vec3d *in_target, float in_rotation_time, float in_rotation_acceleration_time, float in_rotation_deceleration_time)
{
matrix temp_matrix = IDENTITY_MATRIX;
if (in_target != nullptr)
{
vec3d position;
matrix orient_buf;
matrix *orient = nullptr;
if (Use_host_orientation_for_set_camera_facing)
{
// we want to retrieve the camera's orientation, so provide a buffer for it
orient = &orient_buf;
// reset the camera's orientation state because if we have no host object, that is what the camera will default to
c_ori = vmd_identity_matrix;
}
this->get_info(&position, orient, false);
if (vm_vec_same(in_target, &position))
{
Warning(LOCATION, "Camera tried to point to self");
return;
}
vec3d targetvec;
vm_vec_normalized_dir(&targetvec, in_target, &position);
if (Use_host_orientation_for_set_camera_facing)
{
// point along the target vector, but using the host orient's roll
vm_vector_2_matrix_norm(&temp_matrix, &targetvec, &orient->vec.uvec, nullptr);
// if we have a host, we need the difference between the camera's current orient and the orient we want
// if not, we will later set the absolute orientation, rather than the orientation relative to the host
if (object_host.isValid())
{
vm_transpose(orient);
temp_matrix = temp_matrix * *orient;
}
}
else
{
// point directly along the target vector
vm_vector_2_matrix_norm(&temp_matrix, &targetvec, nullptr, nullptr);
}
}
set_rotation(&temp_matrix, in_rotation_time, in_rotation_acceleration_time, in_rotation_deceleration_time);
}
void camera::do_frame(float /*in_frametime*/)
{
}
object *camera::get_object_host()
{
return object_host.objp_or_null();
}
int camera::get_object_host_submodel()
{
return object_host_submodel;
}
object *camera::get_object_target()
{
return object_target.objp_or_null();
}
int camera::get_object_target_submodel()
{
return object_target_submodel;
}
fov_t camera::get_fov()
{
if(!(flags & CAM_STATIONARY_FOV))
{
float fov_now;
fov.get(&fov_now, nullptr);
return fov_now;
//TODO enable moving asymmetric fov
}
return c_fov;
}
eye* get_submodel_eye(polymodel *pm, int submodel_num);
void camera::get_info(vec3d *position, matrix *orientation, bool apply_camera_orientation)
{
if(position == NULL && orientation == NULL)
return;
eye* eyep = NULL;
vec3d host_normal;
matrix host_orient = vmd_identity_matrix;
bool use_host_orient = false;
//POSITION
if(!(flags & CAM_STATIONARY_POS) || object_host.isValid())
{
c_pos = vmd_zero_vector;
vec3d pt;
pos_x.get(&pt.xyz.x, NULL);
pos_y.get(&pt.xyz.y, NULL);
pos_z.get(&pt.xyz.z, NULL);
if(object_host.isValid())
{
object *objp = object_host.objp();
polymodel *pm = object_get_model(objp);
polymodel_instance *pmi = object_get_model_instance(objp);
if(object_host_submodel < 0 || pm == NULL)
{
vm_vec_unrotate(&c_pos, &pt, &object_host.objp()->orient);
vm_vec_add2(&c_pos, &object_host.objp()->pos);
}
else
{
eyep = get_submodel_eye(pm, object_host_submodel);
if(eyep)
{
Assertion(objp->type == OBJ_SHIP, "This part of the code expects the object to be a ship");
vec3d c_pos_in;
model_instance_local_to_global_point_dir(&c_pos_in, &host_normal, &eyep->pnt, &eyep->norm, pm, pmi, eyep->parent);
vm_vec_unrotate(&c_pos, &c_pos_in, &objp->orient);
vm_vec_add2(&c_pos, &objp->pos);
}
else
{
if (pmi != nullptr)
{
model_instance_local_to_global_point_orient(&c_pos, &host_orient, &pt, &vmd_identity_matrix, pm, pmi, object_host_submodel, &objp->orient, &objp->pos);
use_host_orient = true;
}
else
model_local_to_global_point( &c_pos, &pt, pm, object_host_submodel, &objp->orient, &objp->pos );
}
}
}
else
{
c_pos = pt;
}
//Do custom position stuff, if needed
if(func_custom_position != NULL && !eyep)
{
func_custom_position(this, &c_pos);
}
}
if (position != nullptr)
*position = c_pos;
//ORIENTATION
if(orientation != NULL)
{
bool target_set = false;
if(!(flags & CAM_STATIONARY_ORI) || object_target.isValid() || object_host.isValid())
{
if(object_target.isValid())
{
object *target_objp = object_target.objp();
polymodel *target_pm = object_get_model(target_objp);
polymodel_instance *target_pmi = object_get_model_instance(target_objp);
vec3d target_pos = vmd_zero_vector;
//If we don't have a submodel or don't have the model use object pos
//Otherwise, find the submodel pos as it is rotated
if (object_target_submodel < 0 || target_pm == nullptr)
{
target_pos = target_objp->pos;
}
else
{
if (target_pmi != nullptr)
model_instance_local_to_global_point(&target_pos, &vmd_zero_vector, target_pm, target_pmi, object_target_submodel, &target_objp->orient, &target_objp->pos);
else
model_local_to_global_point( &target_pos, &vmd_zero_vector, target_pm, object_target_submodel, &target_objp->orient, &target_objp->pos );
}
vec3d targetvec;
vm_vec_normalized_dir(&targetvec, &target_pos, &c_pos);
vm_vector_2_matrix_norm(&c_ori, &targetvec, nullptr, nullptr);
target_set = true;
}
else if(object_host.isValid())
{
if(eyep)
{
vm_vector_2_matrix_norm(&c_ori, &host_normal, vm_vec_same(&host_normal, &object_host.objp()->orient.vec.uvec) ? nullptr : &object_host.objp()->orient.vec.uvec, nullptr);
target_set = true;
}
else if (use_host_orient)
{
c_ori = host_orient;
}
else
{
c_ori = object_host.objp()->orient;
}
}
else
{
c_ori = vmd_identity_matrix;
}
//Do custom orientation stuff, if needed
if (func_custom_orientation != nullptr && !target_set)
{
func_custom_orientation(this, &c_ori);
}
// only do this if we want to find where the camera is actually pointing;
// skip this if we just want the orientation of the host
if (apply_camera_orientation)
{
matrix mtxA = c_ori;
matrix mtxB = IDENTITY_MATRIX;
float pos = 0.0f;
for (int i = 0; i < 9; i++)
{
ori[i].get(&pos, nullptr);
mtxB.a1d[i] = pos;
}
vm_matrix_x_matrix(&c_ori, &mtxA, &mtxB);
vm_orthogonalize_matrix(&c_ori);
}
}
*orientation = c_ori;
}
}
//*************************warp_camera*************************
warp_camera::warp_camera()
{
this->reset();
}
warp_camera::warp_camera(object *objp)
{
this->reset();
vec3d object_pos = objp->pos;
matrix tmp;
object_get_eye(&object_pos, &tmp, objp);
vm_vec_scale_add2( &object_pos, &Player_obj->orient.vec.rvec, 0.0f );
vm_vec_scale_add2( &object_pos, &Player_obj->orient.vec.uvec, 0.952f );
vm_vec_scale_add2( &object_pos, &Player_obj->orient.vec.fvec, -1.782f );
vec3d tmp_vel = { { { 0.0f, 5.1919f, 14.7f } } };
this->set_position( &object_pos );
this->set_rotation( &objp->orient );
this->set_velocity( &tmp_vel, true);
}
void warp_camera::reset()
{
c_pos = c_vel = c_desired_vel = vmd_zero_vector;
c_ori = vmd_identity_matrix;
c_damping = 1.0f;
c_time = 0.0f;
}
void warp_camera::set_position( vec3d *in_pos )
{
c_pos = *in_pos;
}
void warp_camera::set_rotation( matrix *in_ori )
{
c_ori = *in_ori;
}
void warp_camera::set_velocity( vec3d *in_vel, bool instantaneous )
{
c_desired_vel.xyz.x = 0.0f;
c_desired_vel.xyz.y = 0.0f;
c_desired_vel.xyz.z = 0.0f;
vm_vec_scale_add2( &c_desired_vel, &c_ori.vec.rvec, in_vel->xyz.x );
vm_vec_scale_add2( &c_desired_vel, &c_ori.vec.uvec, in_vel->xyz.y );
vm_vec_scale_add2( &c_desired_vel, &c_ori.vec.fvec, in_vel->xyz.z );
if ( instantaneous ) {
c_vel = c_desired_vel;
}
}
void warp_camera::do_frame(float in_frametime)
{
vec3d new_vel, delta_pos;
apply_physics( c_damping, c_desired_vel.xyz.x, c_vel.xyz.x, in_frametime, &new_vel.xyz.x, &delta_pos.xyz.x );
apply_physics( c_damping, c_desired_vel.xyz.y, c_vel.xyz.y, in_frametime, &new_vel.xyz.y, &delta_pos.xyz.y );
apply_physics( c_damping, c_desired_vel.xyz.z, c_vel.xyz.z, in_frametime, &new_vel.xyz.z, &delta_pos.xyz.z );
c_vel = new_vel;
vm_vec_add2( &c_pos, &delta_pos );
float ot = c_time+0.0f;
c_time += in_frametime;
if ( (ot < 0.667f) && ( c_time >= 0.667f ) ) {
vec3d tmp;
tmp.xyz.z = 4.739f; // always go this fast forward.
// pick x and y velocities so they are always on a
// circle with a 25 m radius.
float tmp_angle = frand()*PI2;
tmp.xyz.x = 22.0f * sinf(tmp_angle);
tmp.xyz.y = -22.0f * cosf(tmp_angle);
this->set_velocity( &tmp, 0 );
}
if ( (ot < 3.0f ) && ( c_time >= 3.0f ) ) {
vec3d tmp = ZERO_VECTOR;
this->set_velocity( &tmp, 0 );
}
}
void warp_camera::get_info(vec3d *position, matrix *orientation)
{
if(position != NULL)
*position = c_pos;
if(orientation != NULL)
*orientation = c_ori;
}
//*************************subtitle*************************
#define MAX_SUBTITLE_LINES 64
subtitle::subtitle(int in_x_pos, int in_y_pos, const char* in_text, const char* in_imageanim, float in_display_time,
float in_fade_time, const color* in_text_color, int in_text_fontnum, bool center_x, bool center_y, int in_width,
int in_height, bool in_post_shaded, int in_line_height_modifier, bool in_adjust_wh)
: display_time(-1.0f), fade_time(-1.0f), text_fontnum(-1), line_height_modifier(0),
image_id(-1), time_displayed(-1.0f), time_displayed_end(-1.0f), post_shaded(false), do_screen_scaling(false)
{
if ((in_text == nullptr || *in_text == '\0') && (in_imageanim == nullptr || *in_imageanim == '\0'))
return;
auto screen_w = gr_screen.center_w;
auto screen_h = gr_screen.center_h;
// we might be doing screen scaling
if (Show_subtitle_screen_base_res[0] > 0 && Show_subtitle_screen_base_res[1] > 0)
{
// The way this works is we specify our subtitle coordinates relative to a base resolution, e.g. 1024x768.
// The base resolution is adjusted behind the scenes to match the aspect ratio of the current resolution
// (and the subtitle coordinates are adjusted as well). Then the adjusted subtitles are resized by the
// graphics code when they are drawn to the screen.
do_screen_scaling = true;
// we might need to adjust the coordinates for aspect ratio
if (in_adjust_wh)
{
if (Show_subtitle_screen_base_res[0] != Show_subtitle_screen_adjusted_res[0])
{
in_x_pos = in_x_pos * Show_subtitle_screen_adjusted_res[0] / Show_subtitle_screen_base_res[0];
in_width = in_width * Show_subtitle_screen_adjusted_res[0] / Show_subtitle_screen_base_res[0];
}
if (Show_subtitle_screen_base_res[1] != Show_subtitle_screen_adjusted_res[1])
{
in_y_pos = in_y_pos * Show_subtitle_screen_adjusted_res[1] / Show_subtitle_screen_base_res[1];
in_height = in_height * Show_subtitle_screen_adjusted_res[1] / Show_subtitle_screen_base_res[1];
}
}
// use these because we will be scaling the subtitle when it is displayed
screen_w = Show_subtitle_screen_adjusted_res[0];
screen_h = Show_subtitle_screen_adjusted_res[1];
}
// Initialize color
gr_init_color(&text_color, 0, 0, 0);
SCP_string text_buf;
// basic init, this always has to be done
memset( imageanim, 0, sizeof(imageanim) );
memset( &text_pos, 0, 2*sizeof(int) );
memset( &image_pos, 0, 4*sizeof(int) );
if (in_text != NULL && in_text[0] != '\0')
{
text_buf = in_text;
sexp_replace_variable_names_with_values(text_buf);
sexp_container_replace_refs_with_values(text_buf);
// (message_translate_tokens is called when the subtitle is queued, so does not need to be called here)
in_text = text_buf.c_str();
}
int num_text_lines = 0;
const char *text_line_ptrs[MAX_SUBTITLE_LINES];
int text_line_lens[MAX_SUBTITLE_LINES];
//Setup text
if ( (in_text != NULL) && (in_text[0] != '\0') ) {
int split_width = (in_width > 0) ? in_width : 200;
num_text_lines = split_str(in_text, split_width, text_line_lens, text_line_ptrs, MAX_SUBTITLE_LINES);
for(int i = 0; i < num_text_lines; i++)
{
text_buf.assign(text_line_ptrs[i], text_line_lens[i]);
text_lines.push_back(text_buf);
}
}
//Setup text color
if(in_text_color != NULL)
text_color = *in_text_color;
else
gr_init_alphacolor(&text_color, 255, 255, 255, 255);
text_fontnum = in_text_fontnum;
line_height_modifier = in_line_height_modifier;
//Setup display and fade time
display_time = fl_abs(in_display_time);
fade_time = fl_abs(in_fade_time);
//Setup image
if ( (in_imageanim != NULL) && (in_imageanim[0] != '\0') )
{
image_id = bm_load(in_imageanim);
if (image_id >= 0)
strncpy(imageanim, in_imageanim, sizeof(imageanim) - 1);
}
//Setup pos
int w=0, h=0, tw=0, th=0;
if(center_x || center_y)
{
// switch font because we need to measure it
int old_fontnum;
if (text_fontnum >= 0)
{
old_fontnum = font::get_current_fontnum();
font::set_font(text_fontnum);
}
else
{
old_fontnum = -1;
}
//Get text size
for(int i = 0; i < num_text_lines; i++)
{
gr_get_string_size(&w, &h, text_line_ptrs[i], 1.0f, static_cast<size_t>(text_line_lens[i]));
if(w > tw)
tw = w;
if (line_height_modifier != 0.0f)
{
if (Show_subtitle_uses_pixels)
h = line_height_modifier;
else
h = fl2i(h * line_height_modifier / 100.0f);
}
th += h;
}
// restore old font
if (old_fontnum >= 0)
{
font::set_font(old_fontnum);
}
//Get image size
if(image_id != -1)
{
bm_get_info(image_id, &w, &h);
if (in_width > 0)
w = in_width;
if (in_height > 0)
h = in_height;
tw += w;
if(h > th)
th = h;
}
//Center it?
if(center_x)
image_pos.x = (screen_w - tw)/2;
if(center_y)
image_pos.y = (screen_h - th)/2;
}
if(in_x_pos < 0 && !center_x)
image_pos.x += screen_w + in_x_pos;
else if(!center_x)
image_pos.x += in_x_pos;
if(in_y_pos < 0 && !center_y)
image_pos.y += screen_h + in_y_pos;
else if(!center_y)
image_pos.y += in_y_pos;
image_pos.w = in_width;
image_pos.h = in_height;
if(image_id != -1)
text_pos.x = image_pos.x + w; //Still set from bm_get_info call
else
text_pos.x = image_pos.x;
text_pos.y = image_pos.y;
time_displayed = 0.0f;
time_displayed_end = 2.0f*fade_time + display_time;
post_shaded = in_post_shaded;
}
void subtitle::do_frame(float frametime)
{
//Figure out how much alpha
if(time_displayed < fade_time)
{
text_color.alpha = (ubyte)fl2i(255.0f*(time_displayed/fade_time));
}
else if(time_displayed > time_displayed_end)
{
//We're finished
return;
}
else if((time_displayed - fade_time) > display_time)
{
text_color.alpha = (ubyte)fl2i(255.0f*(1-(time_displayed - fade_time - display_time)/fade_time));
}
else
{
text_color.alpha = 255;
}
gr_set_color_fast(&text_color);
// save old font and set new font
int old_fontnum;
if (text_fontnum >= 0)
{
old_fontnum = font::get_current_fontnum();
font::set_font(text_fontnum);
}
else
{
old_fontnum = -1;
}
int font_height = gr_get_font_height();
int x = text_pos.x;
int y = text_pos.y;
auto sizing_mode = do_screen_scaling ? GR_RESIZE_FULL : GR_RESIZE_NONE;
if (do_screen_scaling)
{
gr_set_screen_scale(Show_subtitle_screen_adjusted_res[0], Show_subtitle_screen_adjusted_res[1]);
gr_set_clip(0, 0, Show_subtitle_screen_adjusted_res[0], Show_subtitle_screen_adjusted_res[1]);
}
// do the actual drawing ---------------------
for(SCP_vector<SCP_string>::iterator line = text_lines.begin(); line != text_lines.end(); ++line)
{
gr_string(x, y, (char*)line->c_str(), sizing_mode);
if (line_height_modifier != 0.0f)
{
if (Show_subtitle_uses_pixels)
font_height = line_height_modifier;
else
font_height = fl2i(font_height * line_height_modifier / 100.0f);
}
y += font_height;
}
if(image_id >= 0)
{
gr_set_bitmap(image_id, GR_ALPHABLEND_FILTER, GR_BITBLT_MODE_NORMAL, text_color.alpha/255.0f);
// image scaling?
if (image_pos.w > 0 || image_pos.h > 0)
{
int orig_w, orig_h;
vec3d scale;
bm_get_info(image_id, &orig_w, &orig_h);
scale.xyz.x = (image_pos.w > 0) ? (image_pos.w / (float) orig_w) : 1.0f;
scale.xyz.y = (image_pos.h > 0) ? (image_pos.h / (float) orig_h) : 1.0f;
scale.xyz.z = 1.0f;
gr_push_scale_matrix(&scale);
gr_bitmap(fl2i(image_pos.x / scale.xyz.x), fl2i(image_pos.y / scale.xyz.y), sizing_mode);
gr_pop_scale_matrix();
}
// no scaling
else
{
gr_bitmap(image_pos.x, image_pos.y, sizing_mode);
}
}
// finished the actual drawing ---------------
if (do_screen_scaling)
{
gr_reset_screen_scale();
gr_reset_clip();
}
// restore old font
if (old_fontnum >= 0)
font::set_font(old_fontnum);
time_displayed += frametime;
}
subtitle::~subtitle()
{
if (image_id != -1) {
if ( bm_release(image_id) ) {
image_id = -1;
}
}
}
void subtitle::clone(const subtitle &sub)
{
text_lines = sub.text_lines;
text_fontnum = sub.text_fontnum;
line_height_modifier = sub.line_height_modifier;
// copy the structs
text_pos = sub.text_pos;
image_pos = sub.image_pos;
display_time = sub.display_time;
fade_time = sub.fade_time;
memcpy( &text_color, &sub.text_color, sizeof(color) );
if ( strlen(sub.imageanim) ) {
memcpy( imageanim, sub.imageanim, MAX_FILENAME_LEN );
// we bm_load() again here to kick the refcount up
image_id = bm_load(imageanim);
} else {
memset( imageanim, 0, MAX_FILENAME_LEN );
image_id = -1;
}
time_displayed = sub.time_displayed;
time_displayed_end = sub.time_displayed_end;
post_shaded = sub.post_shaded;
do_screen_scaling = sub.do_screen_scaling;
}
subtitle& subtitle::operator=(const subtitle &sub)
{
if (this != &sub) {
if (image_id != -1) {
if ( bm_release(image_id) ) {
image_id = -1;
}
}
clone(sub);
}
return *this;
}
//*************************camid*************************
camid::camid()
{
idx = 0;
sig = -1;
}
camid::camid(size_t n_idx, int n_sig)
{
idx = n_idx;
sig = n_sig;
}
camera* camid::getCamera()
{
if(!isValid())
return NULL;
return Cameras[idx];
}
size_t camid::getIndex()
{
return idx;
}
int camid::getSignature()
{
return sig;
}
bool camid::isValid() const
{
if(idx >= Cameras.size())
return false;
if(Cameras[idx] == NULL)
return false;
if(Cameras[idx]->get_signature() != this->sig)
return false;
return true;
}
//*************************camera functions*************************
void cam_init()
{
//Rest FOV override
Sexp_fov = 0.0;
}
void cam_close()
{
//Set Current_camera to nothing
Current_camera = camid();
for (auto ii = Cameras.begin(); ii != Cameras.end(); ++ii) {
delete * ii;
}
Cameras.clear();
}
int cam_get_next_sig()
{
static int next_sig = 0;
return next_sig++;
}
camid cam_create(const char *n_name, vec3d *n_pos, matrix *n_ori, object *n_object, int n_object_host_submodel)
{
camera *cam = NULL;
camid cid;
//Get signature
int sig = cam_get_next_sig();
//Get name
char buf[NAME_LENGTH] = {'\0'};
if(n_name == NULL)
sprintf(buf, "Camera %d", cid.getSignature());
else
strncpy(buf, n_name, NAME_LENGTH-1);
//Find a free slot
cam = new camera(buf, sig);
cid = camid(Cameras.size(), sig);
Cameras.push_back(cam);
//Set attributes
if(n_pos != NULL)
cam->set_position(n_pos);
if(n_ori != NULL)
cam->set_rotation(n_ori);
if(n_object != NULL)
cam->set_object_host(n_object, n_object_host_submodel);
return cid;
}
void cam_delete(camid cid)
{
if(cid.isValid())
{
cid.getCamera()->clear();
}
}
void cam_do_frame(float frametime)
{
size_t i,size=Cameras.size();
for(i = 0; i < size; i++)
{
if(Cameras[i] != NULL)
Cameras[i]->do_frame(frametime);
}
}
camid cam_get_camera(uint idx)
{
if(idx >= Cameras.size())
return camid();
return camid(idx, Cameras[idx]->get_signature());
}
camid cam_get_current()
{
return Current_camera;
}
size_t cam_get_num()
{
return Cameras.size();
}
/**
* Looks up camera by name, returns -1 on failure
*/
camid cam_lookup(const char* name)
{
size_t i, size=Cameras.size();
for(i = 0; i < size; i++)
{
if(Cameras[i] != NULL && !stricmp(Cameras[i]->get_name(), name))
return camid(i, Cameras[i]->get_signature());
}
return camid();
}
static bool Camera_hud_draw_saved = false;
static int Camera_hud_draw_value = 0;
bool cam_set_camera(camid cid)
{
if(!cid.isValid())
{
return false;
}
Viewer_mode |= VM_FREECAMERA;
Current_camera = cid;
if (!Cutscene_camera_displays_hud)
{
if(!Camera_hud_draw_saved)
{
Camera_hud_draw_value = hud_get_draw();
Camera_hud_draw_saved = true;
}
hud_set_draw(0);
}
return true;
}
void cam_reset_camera()
{
Viewer_mode &= ~VM_FREECAMERA;
if (!Cutscene_camera_displays_hud)
{
hud_set_draw(Camera_hud_draw_value);
Camera_hud_draw_saved = false;
}
}
void subtitles_close()
{
Subtitles.clear();
}
void subtitles_do_frame(float frametime)
{
SCP_vector<subtitle>::iterator sub;
for(sub = Subtitles.begin(); sub != Subtitles.end(); ++sub)
{
if ( !sub->is_post_shaded( ) )
sub->do_frame(frametime);
}
}
void subtitles_do_frame_post_shaded(float frametime)
{
SCP_vector<subtitle>::iterator sub;
for(sub = Subtitles.begin(); sub != Subtitles.end(); ++sub)
{
if ( sub->is_post_shaded( ) )
sub->do_frame(frametime);
}
}
static vec3d normal_cache;
void get_turret_cam_pos(camera *cam, vec3d *pos)
{
object_h obj(cam->get_object_host());
if(!obj.isValid())
return;
ship* shipp = &Ships[cam->get_object_host()->instance];
ship_subsys* ssp = GET_FIRST(&shipp->subsys_list);
while ( ssp != END_OF_LIST( &shipp->subsys_list ) )
{
if(ssp->system_info->subobj_num == cam->get_object_host_submodel())
{
ship_get_global_turret_gun_info(cam->get_object_host(), ssp, pos, true, &normal_cache, true, nullptr);
vec3d offset = vmd_zero_vector;
offset.xyz.x = 0.0001f;
vm_vec_add2(pos, &offset); // prevent beam turrets from crashing with a nullvec
break;
}
ssp = GET_NEXT( ssp );
}
}
void get_turret_cam_orient(camera *cam, matrix *ori)
{
object_h obj(cam->get_object_host());
if(!obj.isValid())
return;
vm_vector_2_matrix_norm(ori, &normal_cache, vm_vec_same(&normal_cache, &cam->get_object_host()->orient.vec.uvec) ? nullptr : &cam->get_object_host()->orient.vec.uvec, nullptr);
}
eye* get_submodel_eye(polymodel *pm, int submodel_num)
{
if(pm->n_view_positions > 0)
{
for(int i = 0; i < pm->n_view_positions; ++i)
{
if(pm->view_positions[i].parent == submodel_num)
{
return &pm->view_positions[i];
}
int sm = pm->submodel[submodel_num].first_child;
while(sm > 0) //look for eyepoints attached to children, important for turret arms
{
if(pm->view_positions[i].parent == sm)
{
return &pm->view_positions[i];
}
sm = pm->submodel[sm].next_sibling;
}
}
}
return NULL;
}
// maybe push the camera away from preferred dist if it would intersect the ship's bounding box
float cam_get_bbox_dist(const object* viewer_obj, float preferred_distance, const matrix* cam_orient) {
if (viewer_obj == nullptr)
return preferred_distance;
// radius is the maximal extent of the ship's bbox from the center, so if its at least that plus the padding its guaranteed to be good
// plus a little fudge factor at the end because shields count against the bounding box, but not the radius
if (preferred_distance > (viewer_obj->radius * EXTERN_CAM_BBOX_MULTIPLIER_PADDING + EXTERN_CAM_BBOX_CONSTANT_PADDING) * 1.5f)
return preferred_distance;
polymodel* pm = object_get_model(viewer_obj);
if (pm == nullptr)
return preferred_distance;
vec3d adjusted_bbox;
const vec3d* cam_vec = &cam_orient->vec.fvec;
// the bounding box is expanded by the padding and maybe use negative min instead of max, because they could differ significantly
// and we only care about the one thats on the side of the camera
adjusted_bbox.xyz.x = (cam_vec->xyz.x < 0 ? -pm->mins.xyz.x : pm->maxs.xyz.x) * EXTERN_CAM_BBOX_MULTIPLIER_PADDING + EXTERN_CAM_BBOX_CONSTANT_PADDING;
adjusted_bbox.xyz.y = (cam_vec->xyz.y < 0 ? -pm->mins.xyz.y : pm->maxs.xyz.y) * EXTERN_CAM_BBOX_MULTIPLIER_PADDING + EXTERN_CAM_BBOX_CONSTANT_PADDING;
adjusted_bbox.xyz.z = (cam_vec->xyz.z < 0 ? -pm->mins.xyz.z : pm->maxs.xyz.z) * EXTERN_CAM_BBOX_MULTIPLIER_PADDING + EXTERN_CAM_BBOX_CONSTANT_PADDING;
vec3d adjusted_dist;
// adjusted_dist is the vector (with an inverted magnitude) that contacts the surface of an
// ellipsoid which contacts the adjusted_bbox at each of its faces
adjusted_dist.xyz.x = cam_vec->xyz.x / adjusted_bbox.xyz.x;
adjusted_dist.xyz.y = cam_vec->xyz.y / adjusted_bbox.xyz.y;
adjusted_dist.xyz.z = cam_vec->xyz.z / adjusted_bbox.xyz.z;
float compensation = vm_vec_mag(&adjusted_dist);
return fmaxf(1 / compensation, preferred_distance);
}