#5 - fixed interpolation & rollback
This commit is contained in:
@@ -2,6 +2,9 @@
|
||||
|
||||
layout(location = 0) out vec4 outColor;
|
||||
|
||||
layout(location = 0) in vec3 inNormal;
|
||||
|
||||
void main() {
|
||||
float diffuse = max(0.1, dot(vec3(0.8, 1.5, -1.0), inNormal));
|
||||
outColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
layout(location = 0) in vec3 pos;
|
||||
layout(location = 1) in vec3 norm;
|
||||
layout(location = 0) out vec3 outNormal;
|
||||
|
||||
layout(set = 0, binding = 0) uniform Camera {
|
||||
mat4 proj;
|
||||
@@ -21,5 +22,6 @@ void main() {
|
||||
// outPos = pos.xyz * 0.05;
|
||||
// outNormal = norm.xyz;
|
||||
// outUV = uv;
|
||||
outNormal = norm;
|
||||
gl_Position = cam.proj * cam.view * PushConstants.transform * vec4(pos, 1.0);
|
||||
}
|
||||
|
||||
@@ -101,10 +101,54 @@ class tw::dbg::ComponentGui<tw::net::EntityPositionInterpolation> {
|
||||
private:
|
||||
using Clock = std::chrono::high_resolution_clock;
|
||||
|
||||
/** How far back the plot reaches. */
|
||||
static constexpr float WINDOW_IN_SECONDS = 10.0f;
|
||||
|
||||
/** Ten seconds of frames at sixty a second. */
|
||||
static constexpr size_t CAPACITY = 600;
|
||||
|
||||
inline static const net::EntityPositionInterpolation* m_subject = nullptr;
|
||||
|
||||
inline static int m_last_frame = -1;
|
||||
inline static size_t m_head = 0;
|
||||
inline static size_t m_count = 0;
|
||||
|
||||
inline static std::vector<Clock::time_point> m_read_times;
|
||||
inline static std::vector<glm::vec3> m_read_values;
|
||||
|
||||
static int64_t millis_since(Clock::time_point point) {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - point).count();
|
||||
}
|
||||
|
||||
void restart(const net::EntityPositionInterpolation* instance) {
|
||||
m_subject = instance;
|
||||
m_last_frame = -1;
|
||||
m_head = 0;
|
||||
m_count = 0;
|
||||
|
||||
m_read_times.resize(CAPACITY);
|
||||
m_read_values.resize(CAPACITY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps one reading per frame, so drawing twice does not double up. The
|
||||
* whole value is kept rather than the axis on show, so switching axis
|
||||
* still shows the trace that was already gathered.
|
||||
*/
|
||||
void sample(Clock::time_point time, glm::vec3 value) {
|
||||
const int frame = ImGui::GetFrameCount();
|
||||
if(m_last_frame == frame) {
|
||||
return;
|
||||
}
|
||||
m_last_frame = frame;
|
||||
|
||||
m_read_times[m_head] = time;
|
||||
m_read_values[m_head] = value;
|
||||
|
||||
m_head = (m_head + 1) % CAPACITY;
|
||||
m_count = std::min(m_count + 1, CAPACITY);
|
||||
}
|
||||
|
||||
public:
|
||||
void draw(net::EntityPositionInterpolation* instance) {
|
||||
ImGui::SeparatorText("Received Positions");
|
||||
@@ -122,5 +166,73 @@ public:
|
||||
instance->values()[0].y,
|
||||
instance->values()[0].z,
|
||||
millis_since(instance->times()[0]));
|
||||
|
||||
static int axis = 0;
|
||||
ImGui::Combo("Axis##received", &axis, "X\0Y\0Z\0");
|
||||
|
||||
/** How far behind the present the reader looks; matches the caller. */
|
||||
static int delay_in_millis = 1000;
|
||||
ImGui::SliderInt("Delay (ms)", &delay_in_millis, 0, 2000);
|
||||
|
||||
if(m_subject != instance) {
|
||||
restart(instance);
|
||||
}
|
||||
|
||||
// The buffer wraps, so the index says nothing about when a sample
|
||||
// arrived. Placing each one at its own age puts it where it belongs
|
||||
// without having to know where the ring currently starts. Slots
|
||||
// nothing was pushed into are left out.
|
||||
const auto now = Clock::now();
|
||||
const size_t count = instance->size();
|
||||
|
||||
std::vector<float> ages(count);
|
||||
std::vector<float> positions(count);
|
||||
|
||||
for(size_t i = 0; i < count; i++) {
|
||||
ages[i] = -std::chrono::duration<float>(now - instance->times()[i]).count();
|
||||
positions[i] = instance->values()[i][axis];
|
||||
}
|
||||
|
||||
if(ImPlot::BeginPlot("Buffer", ImVec2(-1.0f, 180.0f))) {
|
||||
ImPlot::SetupAxes("seconds ago", "position", ImPlotAxisFlags_None, ImPlotAxisFlags_AutoFit);
|
||||
ImPlot::SetupAxisLimits(ImAxis_X1,
|
||||
-WINDOW_IN_SECONDS, 0.0f, ImGuiCond_Always);
|
||||
|
||||
ImPlot::PlotScatter("Received", ages.data(), positions.data(), (int)count);
|
||||
|
||||
// Reading the same way the position is read for drawing shows
|
||||
// which two samples the delay lands between, and how far the
|
||||
// result sits from either of them.
|
||||
const auto point = now - std::chrono::milliseconds(delay_in_millis);
|
||||
const auto [from, to, alpha] = instance->get_values_around(point);
|
||||
|
||||
const float read_at = -std::chrono::duration<float>(now - point).count();
|
||||
const float read_out = glm::mix(from, to, alpha)[axis];
|
||||
|
||||
ImPlot::SetNextMarkerStyle(ImPlotMarker_Circle, 5.0f);
|
||||
ImPlot::PlotScatter("Interpolated", &read_at, &read_out, 1);
|
||||
|
||||
// Each reading is kept at the moment it was read for, so the trace
|
||||
// ends on the marker above and runs back along the same timeline
|
||||
// the received samples sit on.
|
||||
sample(point, glm::mix(from, to, alpha));
|
||||
|
||||
std::vector<float> trace_ages(m_count);
|
||||
std::vector<float> trace_positions(m_count);
|
||||
|
||||
const size_t oldest = m_count == CAPACITY ? m_head : 0;
|
||||
|
||||
for(size_t i = 0; i < m_count; i++) {
|
||||
const size_t index = (oldest + i) % CAPACITY;
|
||||
|
||||
trace_ages[i] = -std::chrono::duration<float>(now - m_read_times[index]).count();
|
||||
trace_positions[i] = m_read_values[index][axis];
|
||||
}
|
||||
|
||||
ImPlot::PlotLine("Interpolated Trace",
|
||||
trace_ages.data(), trace_positions.data(), (int)m_count);
|
||||
|
||||
ImPlot::EndPlot();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ void EntityPositionInterpolator::set_position(Clock::time_point time_point, entt
|
||||
return;
|
||||
}
|
||||
|
||||
interpolation->push(Clock::now(), position);
|
||||
interpolation->push(time_point, position);
|
||||
}
|
||||
|
||||
glm::vec3 EntityPositionInterpolator::get_position(Clock::time_point time_point, entt::entity entity) {
|
||||
@@ -35,9 +35,7 @@ glm::vec3 EntityPositionInterpolator::get_position(Clock::time_point time_point,
|
||||
return glm::vec3();
|
||||
}
|
||||
|
||||
auto now = Clock::now() - std::chrono::milliseconds(m_bufferingIntervalInMillis);
|
||||
|
||||
auto [from, to, value] = interpolation->get_values_around(now);
|
||||
auto [from, to, value] = interpolation->get_values_around(time_point);
|
||||
return glm::mix(from, to, value);
|
||||
}
|
||||
|
||||
@@ -45,6 +43,10 @@ void EntityPositionInterpolator::interpolate_smoothed_entities(Clock::time_point
|
||||
m_registry->view<EntityPositionInterpolation, Transform>()
|
||||
.each([&](const auto entity, const EntityPositionInterpolation& interpolation, Transform& ts) {
|
||||
auto [from, to, value] = interpolation.get_values_around(time_point);
|
||||
spdlog::info("From: {} {} {} - To: {} {} {} - By: {}",
|
||||
from.x, from.y, from.z,
|
||||
to.x, to.y, to.z,
|
||||
value);
|
||||
ts.set_position(glm::mix(from, to, value));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,15 +5,21 @@
|
||||
|
||||
#include "common.hpp"
|
||||
|
||||
template<typename T, int length = 3>
|
||||
template<typename T, int length = 10>
|
||||
class InterpolatedProperty {
|
||||
using Clock = std::chrono::high_resolution_clock;
|
||||
|
||||
std::array<T, length> m_buffer;
|
||||
std::array<Clock::time_point, length> m_time_buffer;
|
||||
std::vector<T> m_buffer;
|
||||
std::vector<Clock::time_point> m_time_buffer;
|
||||
|
||||
uint32_t m_head;
|
||||
uint32_t m_size;
|
||||
|
||||
public:
|
||||
InterpolatedProperty(T initial_value) {
|
||||
InterpolatedProperty(T initial_value) :
|
||||
m_buffer(length),
|
||||
m_time_buffer(length),
|
||||
m_size(0), m_head(0) {
|
||||
for(int i = 0; i < length; i++) {
|
||||
m_buffer[i] = initial_value;
|
||||
m_time_buffer[i] = Clock::now();
|
||||
@@ -23,7 +29,21 @@ public:
|
||||
GET_REF(m_buffer, values);
|
||||
GET_REF(m_time_buffer, times);
|
||||
|
||||
/** How many slots hold something that was pushed. */
|
||||
inline uint32_t size() const {
|
||||
return m_size;
|
||||
}
|
||||
|
||||
void push(Clock::time_point point, T value) {
|
||||
// m_buffer[m_head] = std::move(value);
|
||||
// m_time_buffer[m_head] = point;
|
||||
//
|
||||
// m_head = (m_head + 1) % m_buffer.size();
|
||||
//
|
||||
// if (m_size < m_buffer.size())
|
||||
// {
|
||||
// ++m_size;
|
||||
// }
|
||||
if(point < m_time_buffer.at(0)) {
|
||||
return;
|
||||
}
|
||||
@@ -64,6 +84,46 @@ public:
|
||||
}
|
||||
|
||||
inline std::tuple<T, T, float> get_values_around(Clock::time_point point) const {
|
||||
/* const std::size_t capacity = m_buffer.size();
|
||||
const std::size_t oldest = (m_head + capacity - m_size) % capacity;
|
||||
|
||||
if (m_size == 0)
|
||||
{
|
||||
return { T(), T(), 0.0f };
|
||||
}
|
||||
|
||||
// Asking for a time the buffer no longer reaches back to. Standing on
|
||||
// the oldest is wrong by however much was missed, but it is wrong in
|
||||
// the direction the values were heading, unlike the newest.
|
||||
if (point < m_time_buffer[oldest])
|
||||
{
|
||||
return { m_buffer[oldest], m_buffer[oldest], 0.0f };
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i + 1 < m_size; ++i)
|
||||
{
|
||||
const std::size_t idx0 = (oldest + i) % capacity;
|
||||
const std::size_t idx1 = (oldest + i + 1) % capacity;
|
||||
|
||||
const auto t0 = m_time_buffer[idx0];
|
||||
const auto t1 = m_time_buffer[idx1];
|
||||
|
||||
if (point >= t0 && point <= t1)
|
||||
{
|
||||
const auto total =
|
||||
std::chrono::duration<float>(t1 - t0).count();
|
||||
const auto elapsed =
|
||||
std::chrono::duration<float>(point - t0).count();
|
||||
|
||||
const float alpha = total > 0.0f ? elapsed / total : 0.0f;
|
||||
|
||||
return { m_buffer[idx0], m_buffer[idx1], alpha };
|
||||
}
|
||||
}
|
||||
|
||||
// Handle exact last sample (or clamp)
|
||||
const std::size_t last = (oldest + m_size - 1) % capacity;
|
||||
return { m_buffer[last], m_buffer[last], 0.0f }; */
|
||||
T prev = m_buffer.at(0);
|
||||
Clock::time_point prev_point = m_time_buffer.at(0);
|
||||
|
||||
|
||||
@@ -151,6 +151,10 @@ void ReplicatorClient::handle_snapshot_entity(
|
||||
// return;
|
||||
}
|
||||
|
||||
// Applies correction from record_frame_idx to current_frame_idx (if any) on entity. Compares previous position on
|
||||
// frame record_frame_idx with p
|
||||
m_rollback.apply_correction(record_frame_idx, p, entity.value(), &m_world->registry(), current_frame_idx);
|
||||
|
||||
//snap_player_to(current_frame_idx, entity.value(), p);
|
||||
|
||||
// bool reconcile_happened = m_reconciler.reconcile(record_frame_idx, p, entity.value(), &m_world->registry(), current_frame_idx);
|
||||
@@ -168,15 +172,29 @@ void ReplicatorClient::handle_snapshot_entity(
|
||||
// }
|
||||
// m_network_metrics->record_ack_lag(ack_lag);
|
||||
// }
|
||||
}
|
||||
|
||||
m_entity_interpolator->set_position(std::chrono::high_resolution_clock::now(), entity.value(), p);
|
||||
} else {
|
||||
m_entity_interpolator->set_position(std::chrono::high_resolution_clock::now(), entity.value(), p);
|
||||
}
|
||||
}
|
||||
|
||||
void ReplicatorClient::handle_snapshot(uint32_t current_frame_idx, serial::WorldStateReader& reader) {
|
||||
auto header = reader.read_header();
|
||||
measure_response_time(current_frame_idx, header.frame_idx);
|
||||
|
||||
// Snapshots are sent unreliably, so one can overtake another. Taking a
|
||||
// late one would put entities back where they have already been seen.
|
||||
if(header.tick_idx <= m_latest_tick_idx) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_latest_tick_idx = header.tick_idx;
|
||||
|
||||
// Arrival is the only time the client can be sure of, and it grows with
|
||||
// every snapshot, which is what reading the buffer back relies on. What it
|
||||
// costs is spacing: the gaps between samples carry the jitter of the way
|
||||
// here rather than the even gaps the server sent them on.
|
||||
const auto taken_at = std::chrono::high_resolution_clock::now();
|
||||
|
||||
while(reader.has_entity()) {
|
||||
auto entity_r = reader.read_entity();
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ private:
|
||||
net::PlayerRollback m_rollback;
|
||||
World *m_world;
|
||||
net::ServerConnection* m_server_connection;
|
||||
uint32_t m_latest_tick_idx = 0;
|
||||
|
||||
std::optional<entt::entity> m_player_entity;
|
||||
|
||||
|
||||
@@ -50,7 +50,12 @@ void ThirdPersonPlayerController::update(const tw::io::InputManager* input, doub
|
||||
m_camera_rotation = yaw * m_camera_rotation * pitch;
|
||||
|
||||
glm::vec3 offset = m_camera_rotation * glm::vec3(0, 0, m_camera_zoom);
|
||||
m_camera->view().look_at(get_target_position() + offset, get_target_position());
|
||||
|
||||
glm::vec3 from = m_camera->view().position();
|
||||
glm::vec3 to = get_target_position() + offset;
|
||||
glm::vec3 interpolated = glm::mix(from, to, 0.8f);
|
||||
|
||||
m_camera->view().look_at(interpolated, get_target_position());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@
|
||||
|
||||
namespace tw {
|
||||
|
||||
typedef HistoryBuffer<long, glm::vec3> EntityPositionHistory;
|
||||
|
||||
entt::entity
|
||||
ClientWorldController::create_entity(const std::string& name, glm::vec3 position) {
|
||||
const auto entity = m_world->registry().create();
|
||||
@@ -82,8 +80,8 @@ void ClientWorldController::spawn_entity(const std::string& name, uint32_t serve
|
||||
if(m_controlled_server_id.has_value() && m_controlled_server_id.value() == server_id) {
|
||||
try_bind_player_entity();
|
||||
} else {
|
||||
m_interpolator.register_entity(entity);
|
||||
}
|
||||
m_interpolator.register_entity(entity);
|
||||
}
|
||||
|
||||
ClientWorldController::ClientWorldController(
|
||||
@@ -165,9 +163,9 @@ void ClientWorldController::try_bind_player_entity() {
|
||||
position
|
||||
));
|
||||
|
||||
// if(m_world->registry().all_of<net::EntityPositionInterpolation>(entity)) {
|
||||
// m_world->registry().remove<net::EntityPositionInterpolation>(entity);
|
||||
// }
|
||||
if(m_world->registry().all_of<net::EntityPositionInterpolation>(entity)) {
|
||||
m_world->registry().remove<net::EntityPositionInterpolation>(entity);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -187,7 +185,6 @@ void ClientWorldController::update_network() {
|
||||
});
|
||||
|
||||
|
||||
m_replicator_client.record_prediction(m_frame_idx);
|
||||
|
||||
|
||||
// m_world->registry().view<Transform>()
|
||||
@@ -216,10 +213,11 @@ void ClientWorldController::update(double delta_time) {
|
||||
if(controller) {
|
||||
// controller->set_input(m_network_frame_idx, input);
|
||||
m_replicator_client.set_input(m_network_frame_idx, input);
|
||||
m_replicator_client.record_prediction(m_network_frame_idx);
|
||||
}
|
||||
}
|
||||
|
||||
// m_physics_world->step(m_network_frame_idx, JoltPhysicsWorld::FIXED_DELTA_TIME, true);
|
||||
m_physics_world->step(m_network_frame_idx, JoltPhysicsWorld::FIXED_DELTA_TIME, true);
|
||||
m_network_frame_idx++;
|
||||
}
|
||||
|
||||
@@ -230,7 +228,7 @@ void ClientWorldController::update(double delta_time) {
|
||||
// TODO: The third person controller could pull
|
||||
Transform* player_transform = m_world->registry().try_get<Transform>(m_player_entity.value());
|
||||
if(player_transform) {
|
||||
m_player_controller.set_target(player_transform->position());
|
||||
// m_player_controller.set_target(player_transform->position());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user