Add Quaternion quantization for network stream
Reduced send rate by 56% in average. Identity quat is just 8-bits (flags) Added tests with full-sphere rotation to ensure this works correctly.
This commit is contained in:
@@ -2,6 +2,94 @@
|
|||||||
|
|
||||||
#include "NetworkStream.h"
|
#include "NetworkStream.h"
|
||||||
#include "INetworkSerializable.h"
|
#include "INetworkSerializable.h"
|
||||||
|
#include "Engine/Core/Math/Quaternion.h"
|
||||||
|
|
||||||
|
// Quaternion quantized for optimized network data size.
|
||||||
|
struct NetworkQuaternion
|
||||||
|
{
|
||||||
|
enum Flag : uint8
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
HasX = 1,
|
||||||
|
HasY = 2,
|
||||||
|
HasZ = 4,
|
||||||
|
NegativeX = 8,
|
||||||
|
NegativeY = 16,
|
||||||
|
NegativeZ = 32,
|
||||||
|
NegativeW = 64,
|
||||||
|
};
|
||||||
|
|
||||||
|
FORCE_INLINE static void Read(NetworkStream* stream, Quaternion& data)
|
||||||
|
{
|
||||||
|
uint8 flags;
|
||||||
|
stream->Read(flags);
|
||||||
|
if (flags == None)
|
||||||
|
{
|
||||||
|
// Early out on default value
|
||||||
|
data = Quaternion::Identity;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Quaternion raw = Quaternion::Identity;
|
||||||
|
#define READ_COMPONENT(comp, hasFlag, negativeFlag) \
|
||||||
|
if (flags & hasFlag) \
|
||||||
|
{ \
|
||||||
|
uint16 packed; \
|
||||||
|
stream->Read(packed); \
|
||||||
|
const float norm = (float)packed / (float)MAX_uint16; \
|
||||||
|
raw.comp = norm; \
|
||||||
|
if (flags & negativeFlag) \
|
||||||
|
raw.comp = -raw.comp; \
|
||||||
|
}
|
||||||
|
READ_COMPONENT(X, HasX, NegativeX);
|
||||||
|
READ_COMPONENT(Y, HasY, NegativeY);
|
||||||
|
READ_COMPONENT(Z, HasZ, NegativeZ);
|
||||||
|
#define READ_COMPONENT
|
||||||
|
|
||||||
|
// Calculate W
|
||||||
|
raw.W = Math::Sqrt(Math::Max(1.0f - raw.X * raw.X - raw.Y * raw.Y - raw.Z * raw.Z, 0.0f));
|
||||||
|
if (flags & NegativeW)
|
||||||
|
raw.W = -raw.W;
|
||||||
|
|
||||||
|
raw.Normalize();
|
||||||
|
data = raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
FORCE_INLINE static void Write(NetworkStream* stream, const Quaternion& data)
|
||||||
|
{
|
||||||
|
// Assumes rotation is normalized so W can be recalculated
|
||||||
|
Quaternion raw = data;
|
||||||
|
raw.Normalize();
|
||||||
|
|
||||||
|
// Compose flags that describe the data
|
||||||
|
uint8 flags = HasX | HasY | HasZ;
|
||||||
|
#define QUANTIZE_COMPONENT(comp, hasFlag, negativeFlag) \
|
||||||
|
if (Math::IsZero(raw.comp)) \
|
||||||
|
flags &= ~hasFlag; \
|
||||||
|
else if (raw.comp < 0.0f) \
|
||||||
|
flags |= negativeFlag
|
||||||
|
QUANTIZE_COMPONENT(X, HasX, NegativeX);
|
||||||
|
QUANTIZE_COMPONENT(Y, HasY, NegativeY);
|
||||||
|
QUANTIZE_COMPONENT(Z, HasZ, NegativeZ);
|
||||||
|
if (raw.W < 0.0f)
|
||||||
|
flags |= NegativeW;
|
||||||
|
#undef QUANTIZE_COMPONENT
|
||||||
|
|
||||||
|
// Write data
|
||||||
|
stream->Write(flags);
|
||||||
|
#define WRITE_COMPONENT(comp, hasFlag) \
|
||||||
|
if (flags & hasFlag) \
|
||||||
|
{ \
|
||||||
|
const float norm = Math::Abs(raw.comp); \
|
||||||
|
const uint16 packed = (uint16)(norm * MAX_uint16); \
|
||||||
|
stream->Write(packed); \
|
||||||
|
}
|
||||||
|
WRITE_COMPONENT(X, HasX);
|
||||||
|
WRITE_COMPONENT(Y, HasY);
|
||||||
|
WRITE_COMPONENT(Z, HasZ);
|
||||||
|
#undef WRITE_COMPONENT
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
NetworkStream::NetworkStream(const SpawnParams& params)
|
NetworkStream::NetworkStream(const SpawnParams& params)
|
||||||
: ScriptingObject(params)
|
: ScriptingObject(params)
|
||||||
@@ -58,6 +146,11 @@ void NetworkStream::Read(INetworkSerializable* obj)
|
|||||||
obj->Deserialize(this);
|
obj->Deserialize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NetworkStream::Read(Quaternion& data)
|
||||||
|
{
|
||||||
|
NetworkQuaternion::Read(this, data);
|
||||||
|
}
|
||||||
|
|
||||||
void NetworkStream::Write(INetworkSerializable& obj)
|
void NetworkStream::Write(INetworkSerializable& obj)
|
||||||
{
|
{
|
||||||
obj.Serialize(this);
|
obj.Serialize(this);
|
||||||
@@ -68,6 +161,11 @@ void NetworkStream::Write(INetworkSerializable* obj)
|
|||||||
obj->Serialize(this);
|
obj->Serialize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NetworkStream::Write(const Quaternion& data)
|
||||||
|
{
|
||||||
|
NetworkQuaternion::Write(this, data);
|
||||||
|
}
|
||||||
|
|
||||||
void NetworkStream::Flush()
|
void NetworkStream::Flush()
|
||||||
{
|
{
|
||||||
// Nothing to do
|
// Nothing to do
|
||||||
|
|||||||
@@ -72,10 +72,12 @@ public:
|
|||||||
using ReadStream::Read;
|
using ReadStream::Read;
|
||||||
void Read(INetworkSerializable& obj);
|
void Read(INetworkSerializable& obj);
|
||||||
void Read(INetworkSerializable* obj);
|
void Read(INetworkSerializable* obj);
|
||||||
|
void Read(Quaternion& data);
|
||||||
|
|
||||||
using WriteStream::Write;
|
using WriteStream::Write;
|
||||||
void Write(INetworkSerializable& obj);
|
void Write(INetworkSerializable& obj);
|
||||||
void Write(INetworkSerializable* obj);
|
void Write(INetworkSerializable* obj);
|
||||||
|
void Write(const Quaternion& data);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// [Stream]
|
// [Stream]
|
||||||
|
|||||||
70
Source/Engine/Tests/TestNetworking.cpp
Normal file
70
Source/Engine/Tests/TestNetworking.cpp
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
|
||||||
|
|
||||||
|
#include "Engine/Core/Formatting.h"
|
||||||
|
#include "Engine/Core/Math/Quaternion.h"
|
||||||
|
#include "Engine/Networking/NetworkStream.h"
|
||||||
|
#include <ThirdParty/catch2/catch.hpp>
|
||||||
|
|
||||||
|
TEST_CASE("Networking")
|
||||||
|
{
|
||||||
|
SECTION("Test Network Stream")
|
||||||
|
{
|
||||||
|
auto writeStream = New<NetworkStream>();
|
||||||
|
auto readStream = New<NetworkStream>();
|
||||||
|
|
||||||
|
// Quaternions
|
||||||
|
{
|
||||||
|
#define TEST_RAW 0
|
||||||
|
writeStream->Initialize();
|
||||||
|
constexpr int32 QuatRes = 64;
|
||||||
|
constexpr float ResToDeg = 360.0f / (float)QuatRes;
|
||||||
|
for (int32 x = 0; x <= QuatRes; x++)
|
||||||
|
{
|
||||||
|
const float qx = (float)x * ResToDeg;
|
||||||
|
for (int32 y = 0; y <= QuatRes; y++)
|
||||||
|
{
|
||||||
|
const float qy = (float)y * ResToDeg;
|
||||||
|
for (int32 z = 0; z <= QuatRes; z++)
|
||||||
|
{
|
||||||
|
const float qz = (float)z * ResToDeg;
|
||||||
|
auto quat = Quaternion::Euler(qx, qy, qz);
|
||||||
|
quat.Normalize();
|
||||||
|
#if TEST_RAW
|
||||||
|
writeStream->WriteBytes(&quat, sizeof(quat));
|
||||||
|
#else
|
||||||
|
writeStream->Write(quat);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int dataSizeInkB = writeStream->GetPosition() / 1024; // 4291 -> 1869
|
||||||
|
readStream->Initialize(writeStream->GetBuffer(), writeStream->GetPosition());
|
||||||
|
for (int32 x = 0; x <= QuatRes; x++)
|
||||||
|
{
|
||||||
|
const float qx = (float)x * ResToDeg;
|
||||||
|
for (int32 y = 0; y <= QuatRes; y++)
|
||||||
|
{
|
||||||
|
const float qy = (float)y * ResToDeg;
|
||||||
|
for (int32 z = 0; z <= QuatRes; z++)
|
||||||
|
{
|
||||||
|
const float qz = (float)z * ResToDeg;
|
||||||
|
auto expected = Quaternion::Euler(qx, qy, qz);
|
||||||
|
expected.Normalize();
|
||||||
|
Quaternion quat;
|
||||||
|
#if TEST_RAW
|
||||||
|
readStream->ReadBytes(&quat, sizeof(quat));
|
||||||
|
#else
|
||||||
|
readStream->Read(quat);
|
||||||
|
#endif
|
||||||
|
const bool equal = Quaternion::Dot(expected, quat) > 0.9999f;
|
||||||
|
CHECK(equal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#undef TEST_RAW
|
||||||
|
}
|
||||||
|
|
||||||
|
Delete(readStream);
|
||||||
|
Delete(writeStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user