Files
astromaddie--HaloCE-VR/port/linux/game/render_interpolation.c
T
Madison 01b962ad67 vr.diag_motion: follow a moving object and count blended draws
Each logged frame now also has how many objects were drawn blended
between ticks and how many fell back to the latest tick, and the blended
root of the first object seen moving; the blend is read from the game
clock (render_interpolation_fraction is 1 outside the drawing).

On the device: a10's opening cutscene and a30's play step their blends by
exactly a third a frame at 90 Hz (0.224, 0.557, 0.891), the cutscene
camera's steps stay within about 1% of their neighbours with no pattern
by the tick, and in a30 175 objects a frame are blended, none not; a
followed object's steps are even except where a frame was dropped.
2026-10-03 14:01:01 +09:00

888 lines
29 KiB
C

/*
RENDER_INTERPOLATION.C
Frames between the game's 30 Hz ticks, for the native ports (port/linux,
port/android, port/windows; see port/linux/README.md, "Frame rate").
The game simulates in 30 Hz ticks and originally drew one frame per tick.
The ports draw at the display's refresh rate instead, and every frame shows
the world between the last two ticks: after each tick the camera, every
object's node matrices and the first-person weapon's pose are kept, and a
frame blends the previous and the latest by how far the game clock has run
into the next tick. That puts what is drawn one tick (33 ms) behind the
simulation, the usual price of interpolation. (A Catmull-Rom spline would
also need the tick after the pair it spans: two ticks behind.)
Rotations are blended as quaternions (normalised lerp, taking the shorter
way round), positions and scales linearly. Anything that moves further than
a tick of motion plausibly allows (teleports, respawns, camera cuts) snaps
instead of sweeping across the world.
Particles, contrails and other effects already move every frame
(game_frame), so they need nothing here.
An object the distributed netcode moves to where the host has it
(port/linux/game/network_objects.c) is drawn gliding there over a few ticks
rather than jumping: its snapshots move with it, and the difference is drawn
on top of it, whole until the next tick and fading from then on (no more than
a few world units of it: further is a jump). A local player's view glides so
with their unit, and with what it rides.
*/
#include "cseries.h"
#include "math/real_math.h"
#include "objects/objects.h"
#include "camera/director.h"
#include "camera/observer.h"
#include "cutscene/cinematics.h"
#include "game/players.h"
#include "render/render_cameras.h"
#include "units/units.h"
/* port/linux/src/port_config.c */
int config_boolean(const char *name);
#include <math.h>
#include <stdlib.h>
#include <string.h>
/* ---------- constants */
/* (by absolute index: the object array's all) */
#define MAXIMUM_INTERPOLATED_OBJECTS MAXIMUM_OBJECTS_PER_MAP
#define MAXIMUM_INTERPOLATED_NODES 64
/* world units (10 feet each) a node may move in one tick before it snaps:
well beyond any vehicle, short of any teleport */
#define OBJECT_SNAP_DISTANCE 10.0f
/* a correction's difference left drawn after each tick (of 1) */
#define CORRECTION_DECAY 0.6f
/* ... and small enough to be none */
#define CORRECTION_NEGLIGIBLE 0.001f
/* ... or so large that it is no glide but a jump (corrections come one on
another: their sum is drawn): past the largest a client's own unit or
vehicle is corrected by (3 and 4 world units), short of a snap */
#define CORRECTION_MAXIMUM 8.0f
/* a camera cut: a jump or turn no player or scripted camera makes in 33 ms */
#define CAMERA_CUT_DISTANCE 3.0f
#define CAMERA_CUT_COSINE 0.5f
/* ---------- structures */
struct interpolation_quaternion
{
real i, j, k, w;
};
/* a node's rotation: whether its basis is one a quaternion can hold, and
that quaternion */
struct interpolation_rotation
{
struct interpolation_quaternion quaternion;
boolean is_rotation;
};
struct interpolated_object
{
long object_index; /* NONE when unused */
long tick; /* the tick of the latest snapshot */
short node_count;
short node_capacity;
boolean has_previous;
byte latest; /* which snapshot is the latest */
long blended_frame;
/* where it is drawn from where it is: a correction fading, and those
since the last tick, drawn whole until the next begins to fade them */
real_vector3d correction;
real_vector3d correction_pending;
/* [0] and [1]: the two snapshots, [2]: the blend drawn this frame */
real_matrix4x3 *nodes;
/* ... the two snapshots' rotations, found when first blended */
struct interpolation_rotation *rotations;
boolean rotations_valid[2];
};
struct interpolated_camera
{
long tick;
boolean valid;
boolean has_previous;
struct observer_result previous;
struct observer_result latest;
struct observer_result blended;
/* its player's unit (or what it rides) corrected: as an object's */
real_vector3d correction;
real_vector3d correction_pending;
};
struct interpolated_first_person
{
long tick;
short node_count;
boolean has_previous;
real_matrix4x3 previous[MAXIMUM_INTERPOLATED_NODES];
real_matrix4x3 latest[MAXIMUM_INTERPOLATED_NODES];
};
/* ---------- globals */
static struct interpolated_object *interpolated_objects;
static struct interpolated_camera interpolated_cameras[MAXIMUM_LOCAL_PLAYERS];
static struct interpolated_first_person interpolated_first_person[MAXIMUM_LOCAL_PLAYERS];
static long interpolation_tick;
static long interpolation_frame;
static boolean interpolation_rendering;
static real interpolation_fraction = 1.0f;
/* ---------- blending */
static real lerp(real a, real b, real t)
{
return a + (b - a) * t;
}
static void point_lerp(real_point3d const *a, real_point3d const *b, real t, real_point3d *result)
{
result->x = lerp(a->x, b->x, t);
result->y = lerp(a->y, b->y, t);
result->z = lerp(a->z, b->z, t);
}
static real vector_length(real_vector3d const *v)
{
return (real)sqrt(v->i * v->i + v->j * v->j + v->k * v->k);
}
static void vector_nlerp(real_vector3d const *a, real_vector3d const *b, real t, real_vector3d *result)
{
real length;
result->i = lerp(a->i, b->i, t);
result->j = lerp(a->j, b->j, t);
result->k = lerp(a->k, b->k, t);
length = vector_length(result);
if (length > 1e-6f)
{
result->i /= length;
result->j /= length;
result->k /= length;
}
else
{
*result = *b;
}
}
/* an orthonormal right-handed basis, which a quaternion can represent */
static boolean basis_is_rotation(real_matrix4x3 const *matrix)
{
real_vector3d cross;
real determinant;
if (fabs(vector_length(&matrix->forward) - 1.0f) > 1e-2f ||
fabs(vector_length(&matrix->left) - 1.0f) > 1e-2f ||
fabs(vector_length(&matrix->up) - 1.0f) > 1e-2f)
{
return FALSE;
}
cross.i = matrix->forward.j * matrix->left.k - matrix->forward.k * matrix->left.j;
cross.j = matrix->forward.k * matrix->left.i - matrix->forward.i * matrix->left.k;
cross.k = matrix->forward.i * matrix->left.j - matrix->forward.j * matrix->left.i;
determinant = cross.i * matrix->up.i + cross.j * matrix->up.j + cross.k * matrix->up.k;
return determinant > 0.5f;
}
/* the basis vectors are the matrix's columns (x forward, y left, z up) */
static void quaternion_from_basis(real_matrix4x3 const *matrix, struct interpolation_quaternion *q)
{
real m00 = matrix->forward.i, m10 = matrix->forward.j, m20 = matrix->forward.k;
real m01 = matrix->left.i, m11 = matrix->left.j, m21 = matrix->left.k;
real m02 = matrix->up.i, m12 = matrix->up.j, m22 = matrix->up.k;
real trace = m00 + m11 + m22;
real s;
if (trace > 0.0f)
{
s = 0.5f / (real)sqrt(trace + 1.0f);
q->w = 0.25f / s;
q->i = (m21 - m12) * s;
q->j = (m02 - m20) * s;
q->k = (m10 - m01) * s;
}
else if (m00 > m11 && m00 > m22)
{
s = 2.0f * (real)sqrt(1.0f + m00 - m11 - m22);
q->w = (m21 - m12) / s;
q->i = 0.25f * s;
q->j = (m01 + m10) / s;
q->k = (m02 + m20) / s;
}
else if (m11 > m22)
{
s = 2.0f * (real)sqrt(1.0f + m11 - m00 - m22);
q->w = (m02 - m20) / s;
q->i = (m01 + m10) / s;
q->j = 0.25f * s;
q->k = (m12 + m21) / s;
}
else
{
s = 2.0f * (real)sqrt(1.0f + m22 - m00 - m11);
q->w = (m10 - m01) / s;
q->i = (m02 + m20) / s;
q->j = (m12 + m21) / s;
q->k = 0.25f * s;
}
}
static void basis_from_quaternion(struct interpolation_quaternion const *q, real_matrix4x3 *matrix)
{
real ii = q->i * q->i, jj = q->j * q->j, kk = q->k * q->k;
real ij = q->i * q->j, ik = q->i * q->k, jk = q->j * q->k;
real wi = q->w * q->i, wj = q->w * q->j, wk = q->w * q->k;
matrix->forward.i = 1.0f - 2.0f * (jj + kk);
matrix->forward.j = 2.0f * (ij + wk);
matrix->forward.k = 2.0f * (ik - wj);
matrix->left.i = 2.0f * (ij - wk);
matrix->left.j = 1.0f - 2.0f * (ii + kk);
matrix->left.k = 2.0f * (jk + wi);
matrix->up.i = 2.0f * (ik + wj);
matrix->up.j = 2.0f * (jk - wi);
matrix->up.k = 1.0f - 2.0f * (ii + jj);
}
static void rotation_from_matrix(real_matrix4x3 const *matrix, struct interpolation_rotation *rotation)
{
rotation->is_rotation = basis_is_rotation(matrix);
if (rotation->is_rotation)
quaternion_from_basis(matrix, &rotation->quaternion);
}
/* a matrix a fraction t of the way from a to b, their rotations found */
static void matrix_blend_rotations(
real_matrix4x3 const *a,
real_matrix4x3 const *b,
struct interpolation_rotation const *rotation_a,
struct interpolation_rotation const *rotation_b,
real t,
real_matrix4x3 *result)
{
result->scale = lerp(a->scale, b->scale, t);
point_lerp(&a->position, &b->position, t, &result->position);
if (rotation_a->is_rotation && rotation_b->is_rotation)
{
struct interpolation_quaternion qa = rotation_a->quaternion, qb = rotation_b->quaternion, q;
real length;
/* q and -q are the same rotation: take the shorter way round */
if (qa.i * qb.i + qa.j * qb.j + qa.k * qb.k + qa.w * qb.w < 0.0f)
{
qb.i = -qb.i;
qb.j = -qb.j;
qb.k = -qb.k;
qb.w = -qb.w;
}
q.i = lerp(qa.i, qb.i, t);
q.j = lerp(qa.j, qb.j, t);
q.k = lerp(qa.k, qb.k, t);
q.w = lerp(qa.w, qb.w, t);
length = (real)sqrt(q.i * q.i + q.j * q.j + q.k * q.k + q.w * q.w);
if (length > 1e-6f)
{
q.i /= length;
q.j /= length;
q.k /= length;
q.w /= length;
basis_from_quaternion(&q, result);
return;
}
}
/* a basis a quaternion cannot hold (scaled or mirrored): blend it as is */
result->forward.i = lerp(a->forward.i, b->forward.i, t);
result->forward.j = lerp(a->forward.j, b->forward.j, t);
result->forward.k = lerp(a->forward.k, b->forward.k, t);
result->left.i = lerp(a->left.i, b->left.i, t);
result->left.j = lerp(a->left.j, b->left.j, t);
result->left.k = lerp(a->left.k, b->left.k, t);
result->up.i = lerp(a->up.i, b->up.i, t);
result->up.j = lerp(a->up.j, b->up.j, t);
result->up.k = lerp(a->up.k, b->up.k, t);
}
/* a matrix a fraction t of the way from a to b */
static void matrix_blend(real_matrix4x3 const *a, real_matrix4x3 const *b, real t, real_matrix4x3 *result)
{
struct interpolation_rotation rotation_a, rotation_b;
rotation_from_matrix(a, &rotation_a);
rotation_from_matrix(b, &rotation_b);
matrix_blend_rotations(a, b, &rotation_a, &rotation_b, t, result);
}
/* the vector's parts' sum, large enough to be a correction (so written that
one not a number is none) */
static boolean correction_significant(real_vector3d const *correction)
{
return fabs(correction->i) + fabs(correction->j) + fabs(correction->k) >= CORRECTION_NEGLIGIBLE;
}
/* a correction a tick on: what was drawn fades, what came since is drawn
whole from now on, fading from the next */
static void correction_advance(real_vector3d *correction, real_vector3d *pending)
{
correction->i = correction->i * CORRECTION_DECAY + pending->i;
correction->j = correction->j * CORRECTION_DECAY + pending->j;
correction->k = correction->k * CORRECTION_DECAY + pending->k;
*pending = *global_zero_vector3d;
if (!correction_significant(correction))
*correction = *global_zero_vector3d;
}
/* a correction as drawn this frame: fading through the tick as it does tick
to tick, and those since the tick whole */
static void correction_drawn(real_vector3d const *correction, real_vector3d const *pending, real_vector3d *drawn)
{
real fade = lerp(1.0f, CORRECTION_DECAY, interpolation_fraction);
drawn->i = correction->i * fade + pending->i;
drawn->j = correction->j * fade + pending->j;
drawn->k = correction->k * fade + pending->k;
}
/* a correction added (offset): all of it dropped when the sum is too large
to glide (or not a number) */
static void correction_add(real_vector3d *correction, real_vector3d *pending, real_vector3d const *offset)
{
real i, j, k;
pending->i += offset->i;
pending->j += offset->j;
pending->k += offset->k;
i = correction->i + pending->i;
j = correction->j + pending->j;
k = correction->k + pending->k;
if (!(i * i + j * j + k * k <= CORRECTION_MAXIMUM * CORRECTION_MAXIMUM))
{
*correction = *global_zero_vector3d;
*pending = *global_zero_vector3d;
}
}
static real distance_squared(real_point3d const *a, real_point3d const *b)
{
real x = a->x - b->x, y = a->y - b->y, z = a->z - b->z;
return x * x + y * y + z * z;
}
/* ---------- ticks */
void render_interpolation_tick(void)
{
struct object_iterator iterator;
struct object_datum *object;
long previous_tick = interpolation_tick++;
if (!halo_interpolation_enabled())
return;
/* the cameras' corrections a tick on, as the objects' (below) */
{
short local_player_index;
for (local_player_index = 0; local_player_index < MAXIMUM_LOCAL_PLAYERS; local_player_index++)
{
struct interpolated_camera *camera = &interpolated_cameras[local_player_index];
if (camera->valid)
correction_advance(&camera->correction, &camera->correction_pending);
}
}
if (!interpolated_objects)
{
long index;
interpolated_objects = calloc(MAXIMUM_INTERPOLATED_OBJECTS, sizeof(*interpolated_objects));
if (!interpolated_objects)
return;
for (index = 0; index < MAXIMUM_INTERPOLATED_OBJECTS; index++)
interpolated_objects[index].object_index = NONE;
}
object_iterator_new(&iterator, _object_mask_all, 0);
while ((object = (struct object_datum *)object_iterator_next(&iterator)) != NULL)
{
long absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(iterator.index);
struct interpolated_object *record;
short node_count = (short)(object->object.node_matrices.size / (short)sizeof(real_matrix4x3));
boolean continuing;
if (absolute_index >= MAXIMUM_INTERPOLATED_OBJECTS)
continue;
record = &interpolated_objects[absolute_index];
if (node_count <= 0 || node_count > MAXIMUM_INTERPOLATED_NODES)
{
record->object_index = NONE;
continue;
}
if (record->node_capacity < node_count)
{
real_matrix4x3 *nodes = realloc(record->nodes, 3 * node_count * sizeof(real_matrix4x3));
struct interpolation_rotation *rotations;
if (!nodes)
{
record->object_index = NONE;
continue;
}
record->nodes = nodes;
rotations = realloc(record->rotations, 2 * node_count * sizeof(struct interpolation_rotation));
if (!rotations)
{
record->object_index = NONE;
continue;
}
record->rotations = rotations;
record->node_capacity = node_count;
record->object_index = NONE; /* the old snapshots moved */
}
continuing = record->object_index == iterator.index &&
record->node_count == node_count &&
record->tick == previous_tick;
if (continuing)
{
record->latest ^= 1;
correction_advance(&record->correction, &record->correction_pending);
}
else
{
record->correction = *global_zero_vector3d;
record->correction_pending = *global_zero_vector3d;
}
memcpy(
record->nodes + record->latest * record->node_capacity,
object_get_node_matrices(iterator.index),
node_count * sizeof(real_matrix4x3));
record->rotations_valid[record->latest] = FALSE;
record->object_index = iterator.index;
record->node_count = node_count;
record->tick = interpolation_tick;
record->has_previous = continuing;
record->blended_frame = NONE;
}
}
/* a new map (game.c): its objects take the indices of the last one's, and
nothing of theirs is drawn from */
void render_interpolation_reset(void)
{
long index;
if (interpolated_objects)
{
for (index = 0; index < MAXIMUM_INTERPOLATED_OBJECTS; index++)
interpolated_objects[index].object_index = NONE;
}
memset(interpolated_cameras, 0, sizeof(interpolated_cameras));
for (index = 0; index < MAXIMUM_LOCAL_PLAYERS; index++)
{
interpolated_first_person[index].node_count = 0;
interpolated_first_person[index].has_previous = FALSE;
}
}
/* ---------- frames */
void render_interpolation_frame_begin(void)
{
interpolation_rendering = halo_interpolation_enabled();
interpolation_frame++;
interpolation_fraction = game_time_get_tick_fraction();
}
void render_interpolation_frame_end(void)
{
interpolation_rendering = FALSE;
}
real render_interpolation_fraction(void)
{
return interpolation_rendering ? interpolation_fraction : 1.0f;
}
/* the headset's vr.diag_motion: objects drawn blended and not this frame,
and the first object seen moving, followed (its blended root) */
static struct
{
boolean enabled;
long blended, unblended;
long object_index;
real_point3d position;
} interpolation_diag = { FALSE, 0, 0, NONE };
void render_interpolation_diag(int enable, long *blended, long *unblended, long *object_index, float position[3])
{
interpolation_diag.enabled = enable != 0;
*blended = interpolation_diag.blended;
*unblended = interpolation_diag.unblended;
*object_index = interpolation_diag.object_index;
position[0] = interpolation_diag.position.x;
position[1] = interpolation_diag.position.y;
position[2] = interpolation_diag.position.z;
interpolation_diag.blended = interpolation_diag.unblended = 0;
}
real_matrix4x3 *render_interpolation_object_node_matrices(long object_index)
{
struct interpolated_object *record;
long absolute_index;
if (!interpolation_rendering || !interpolated_objects || object_index == NONE)
return NULL;
absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(object_index);
if (absolute_index >= MAXIMUM_INTERPOLATED_OBJECTS)
return NULL;
record = &interpolated_objects[absolute_index];
if (record->object_index != object_index || record->tick != interpolation_tick || !record->has_previous)
{
interpolation_diag.unblended++;
return NULL;
}
if (record->blended_frame != interpolation_frame)
{
real_matrix4x3 const *previous = record->nodes + (record->latest ^ 1) * record->node_capacity;
real_matrix4x3 const *latest = record->nodes + record->latest * record->node_capacity;
real_matrix4x3 *blended = record->nodes + 2 * record->node_capacity;
short node_index;
/* (so written that a position not a number snaps) */
if (!(distance_squared(&previous[0].position, &latest[0].position) <=
OBJECT_SNAP_DISTANCE * OBJECT_SNAP_DISTANCE))
{
memcpy(blended, latest, record->node_count * sizeof(real_matrix4x3));
}
else
{
struct interpolation_rotation *previous_rotations =
record->rotations + (record->latest ^ 1) * record->node_capacity;
struct interpolation_rotation *latest_rotations =
record->rotations + record->latest * record->node_capacity;
short snapshot;
/* (each snapshot's rotations found once, not every frame: its
nodes' positions may move with a correction, their rotations
never) */
for (snapshot = 0; snapshot < 2; snapshot++)
{
if (!record->rotations_valid[snapshot])
{
real_matrix4x3 const *nodes = record->nodes + snapshot * record->node_capacity;
struct interpolation_rotation *rotations = record->rotations + snapshot * record->node_capacity;
for (node_index = 0; node_index < record->node_count; node_index++)
rotation_from_matrix(&nodes[node_index], &rotations[node_index]);
record->rotations_valid[snapshot] = TRUE;
}
}
for (node_index = 0; node_index < record->node_count; node_index++)
{
matrix_blend_rotations(&previous[node_index], &latest[node_index], &previous_rotations[node_index],
&latest_rotations[node_index], interpolation_fraction, &blended[node_index]);
}
}
if (correction_significant(&record->correction) || correction_significant(&record->correction_pending))
{
real_vector3d drawn;
correction_drawn(&record->correction, &record->correction_pending, &drawn);
for (node_index = 0; node_index < record->node_count; node_index++)
{
blended[node_index].position.x += drawn.i;
blended[node_index].position.y += drawn.j;
blended[node_index].position.z += drawn.k;
}
}
record->blended_frame = interpolation_frame;
if (interpolation_diag.enabled)
{
real_matrix4x3 const *previous = record->nodes + (record->latest ^ 1) * record->node_capacity;
real_matrix4x3 const *latest = record->nodes + record->latest * record->node_capacity;
real moved = distance_squared(&previous[0].position, &latest[0].position);
interpolation_diag.blended++;
if (interpolation_diag.object_index == NONE && moved > 0.005f * 0.005f && moved < 1.0f)
interpolation_diag.object_index = object_index;
if (interpolation_diag.object_index == object_index)
interpolation_diag.position = blended[0].position;
}
}
return record->nodes + 2 * record->node_capacity;
}
/* ---------- corrections */
/* the object (and what it carries) moved by the netcode from where it was,
offset from where it is now: drawn from there, gliding */
void render_interpolation_correct_object(long object_index, real_vector3d const *offset)
{
struct object_datum *object;
long child_index;
long absolute_index;
/* (so written that an offset not a number is none) */
if (!interpolated_objects || object_index == NONE ||
!(offset->i * offset->i + offset->j * offset->j + offset->k * offset->k <= OBJECT_SNAP_DISTANCE * OBJECT_SNAP_DISTANCE))
{
return;
}
absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(object_index);
if (absolute_index < MAXIMUM_INTERPOLATED_OBJECTS &&
interpolated_objects[absolute_index].object_index == object_index)
{
struct interpolated_object *record = &interpolated_objects[absolute_index];
short snapshot;
short node_index;
/* the snapshots where it would have been, the difference drawn */
for (snapshot = 0; snapshot < 2; snapshot++)
{
real_matrix4x3 *nodes = record->nodes + snapshot * record->node_capacity;
for (node_index = 0; node_index < record->node_count; node_index++)
{
nodes[node_index].position.x -= offset->i;
nodes[node_index].position.y -= offset->j;
nodes[node_index].position.z -= offset->k;
}
}
correction_add(&record->correction, &record->correction_pending, offset);
record->blended_frame = NONE;
}
/* a local player's unit (by itself, or with what it rides): its
first-person view, which the observer poses from it, glides with it */
{
short local_player_index;
for (local_player_index = 0; local_player_index < MAXIMUM_LOCAL_PLAYERS; local_player_index++)
{
struct interpolated_camera *camera = &interpolated_cameras[local_player_index];
if (!camera->valid || player_control_get_unit_index(local_player_index) != object_index)
continue;
camera->previous.position.x -= offset->i;
camera->previous.position.y -= offset->j;
camera->previous.position.z -= offset->k;
camera->latest.position.x -= offset->i;
camera->latest.position.y -= offset->j;
camera->latest.position.z -= offset->k;
correction_add(&camera->correction, &camera->correction_pending, offset);
}
}
object = object_get(object_index);
for (child_index = object->object.first_child_object_index; child_index != NONE;
child_index = object_get(child_index)->object.next_object_index)
{
render_interpolation_correct_object(child_index, offset);
}
}
/* ---------- camera */
static struct observer_result direct_cameras[MAXIMUM_LOCAL_PLAYERS];
static struct observer_result const *render_interpolation_blended_camera(
short local_player_index,
struct observer_result const *observer);
/* A first-person view is posed from the player's facing, which the input
turns every frame (player_control.c), but the observer keeps it as of the
last tick and the blend above draws it a tick later still. On foot, the view
points where the player aims now (display.direct_camera). In a vehicle's
seat or a cinematic the view is the seat's or the script's: left as it is.
Desktop only: display.direct_camera is not an Android setting, and its
default would otherwise apply there. */
static struct observer_result const *render_interpolation_direct_camera(
short local_player_index,
struct observer_result const *observer)
{
#ifdef HALO_ANDROID
(void)local_player_index;
return observer;
#else
static int enabled = -1;
struct observer_result *direct;
long unit_index;
if (enabled < 0)
enabled = config_boolean("display.direct_camera");
if (!enabled || !observer ||
director_get_perspective(local_player_index) != _director_perspective_first_person ||
director_inhibited_facing(local_player_index) ||
cinematic_in_progress())
{
return observer;
}
unit_index = player_control_get_unit_index(local_player_index);
if (unit_index == NONE || object_get(unit_index)->object.parent_object_index != NONE)
return observer;
direct = &direct_cameras[local_player_index];
*direct = *observer;
player_control_get_facing_direction(local_player_index, &direct->forward);
observer_up_from_forward(&direct->forward, &direct->up);
return direct;
#endif
}
struct observer_result const *render_interpolation_camera(
short local_player_index,
struct observer_result const *observer)
{
if (local_player_index < 0 || local_player_index >= MAXIMUM_LOCAL_PLAYERS)
return observer;
return render_interpolation_direct_camera(local_player_index,
render_interpolation_blended_camera(local_player_index, observer));
}
static struct observer_result const *render_interpolation_blended_camera(
short local_player_index,
struct observer_result const *observer)
{
struct interpolated_camera *camera;
real t = interpolation_fraction;
if (!interpolation_rendering || !observer)
{
return observer;
}
camera = &interpolated_cameras[local_player_index];
/* the observer as it stood after each tick (the first frame drawn
after the tick) */
if (!camera->valid || camera->tick != interpolation_tick)
{
/* (its correction taken on each tick, render_interpolation_tick) */
if (!camera->valid)
{
camera->correction = *global_zero_vector3d;
camera->correction_pending = *global_zero_vector3d;
}
camera->has_previous = camera->valid;
camera->previous = camera->latest;
camera->latest = *observer;
camera->tick = interpolation_tick;
camera->valid = TRUE;
}
/* (so written that a position or direction not a number cuts) */
if (!camera->has_previous ||
!(distance_squared(&camera->previous.position, &camera->latest.position) <=
CAMERA_CUT_DISTANCE * CAMERA_CUT_DISTANCE) ||
!(camera->previous.forward.i * camera->latest.forward.i +
camera->previous.forward.j * camera->latest.forward.j +
camera->previous.forward.k * camera->latest.forward.k >= CAMERA_CUT_COSINE))
{
return observer;
}
camera->blended = camera->latest;
point_lerp(&camera->previous.position, &camera->latest.position, t, &camera->blended.position);
if (correction_significant(&camera->correction) || correction_significant(&camera->correction_pending))
{
real_vector3d drawn;
correction_drawn(&camera->correction, &camera->correction_pending, &drawn);
camera->blended.position.x += drawn.i;
camera->blended.position.y += drawn.j;
camera->blended.position.z += drawn.k;
}
vector_nlerp(&camera->previous.forward, &camera->latest.forward, t, &camera->blended.forward);
vector_nlerp(&camera->previous.up, &camera->latest.up, t, &camera->blended.up);
{
/* keep up perpendicular to forward */
real_vector3d *forward = &camera->blended.forward;
real_vector3d *up = &camera->blended.up;
real along = up->i * forward->i + up->j * forward->j + up->k * forward->k;
real length;
up->i -= forward->i * along;
up->j -= forward->j * along;
up->k -= forward->k * along;
length = vector_length(up);
if (length > 1e-6f)
{
up->i /= length;
up->j /= length;
up->k /= length;
}
else
{
*up = camera->latest.up;
}
}
camera->blended.field_of_view = lerp(camera->previous.field_of_view, camera->latest.field_of_view, t);
return &camera->blended;
}
/* ---------- first-person weapon */
/* The first-person weapon and hands are posed in world space from the drawn
camera each frame, from animation state that changes once a tick: blend the
pose relative to the camera. */
void render_interpolation_first_person(
short local_player_index,
real_matrix4x3 *node_matrices,
short node_count,
struct render_camera const *camera)
{
struct interpolated_first_person *first_person;
real_matrix4x3 camera_matrix;
real_matrix4x3 inverse_camera;
short node_index;
if (!interpolation_rendering ||
local_player_index < 0 || local_player_index >= MAXIMUM_LOCAL_PLAYERS ||
node_count <= 0 || node_count > MAXIMUM_INTERPOLATED_NODES)
{
return;
}
first_person = &interpolated_first_person[local_player_index];
matrix4x3_from_point_and_vectors(&camera_matrix, &camera->position, &camera->forward, &camera->up);
matrix4x3_inverse(&camera_matrix, &inverse_camera);
if (first_person->tick != interpolation_tick)
{
/* the pose drawn last, at the end of the previous tick */
first_person->has_previous = first_person->node_count == node_count;
memcpy(first_person->previous, first_person->latest, sizeof(first_person->previous));
first_person->tick = interpolation_tick;
}
for (node_index = 0; node_index < node_count; node_index++)
matrix4x3_multiply(&inverse_camera, &node_matrices[node_index], &first_person->latest[node_index]);
first_person->node_count = node_count;
if (!first_person->has_previous)
return;
for (node_index = 0; node_index < node_count; node_index++)
{
real_matrix4x3 blended;
matrix_blend(
&first_person->previous[node_index],
&first_person->latest[node_index],
interpolation_fraction,
&blended);
matrix4x3_multiply(&camera_matrix, &blended, &node_matrices[node_index]);
}
}
/* ---------- time */
/* game time for animated shaders, continuous between ticks: the time of
the frame drawn (a tick behind the simulation, like the objects) */
real render_interpolation_game_time_sec(long ticks)
{
real time;
if (!interpolation_rendering)
return (real)ticks * (1.0f / TICKS_PER_SECOND);
time = ((real)ticks - 1.0f + interpolation_fraction) * (1.0f / TICKS_PER_SECOND);
return time > 0.0f ? time : 0.0f;
}