diff --git a/Source/Engine/Networking/NetworkStream.cpp b/Source/Engine/Networking/NetworkStream.cpp index a5de34232..8c197602c 100644 --- a/Source/Engine/Networking/NetworkStream.cpp +++ b/Source/Engine/Networking/NetworkStream.cpp @@ -2,6 +2,94 @@ #include "NetworkStream.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) : ScriptingObject(params) @@ -58,6 +146,11 @@ void NetworkStream::Read(INetworkSerializable* obj) obj->Deserialize(this); } +void NetworkStream::Read(Quaternion& data) +{ + NetworkQuaternion::Read(this, data); +} + void NetworkStream::Write(INetworkSerializable& obj) { obj.Serialize(this); @@ -68,6 +161,11 @@ void NetworkStream::Write(INetworkSerializable* obj) obj->Serialize(this); } +void NetworkStream::Write(const Quaternion& data) +{ + NetworkQuaternion::Write(this, data); +} + void NetworkStream::Flush() { // Nothing to do diff --git a/Source/Engine/Networking/NetworkStream.h b/Source/Engine/Networking/NetworkStream.h index 37b8c6f17..a79a6ec6b 100644 --- a/Source/Engine/Networking/NetworkStream.h +++ b/Source/Engine/Networking/NetworkStream.h @@ -72,10 +72,12 @@ public: using ReadStream::Read; void Read(INetworkSerializable& obj); void Read(INetworkSerializable* obj); + void Read(Quaternion& data); using WriteStream::Write; void Write(INetworkSerializable& obj); void Write(INetworkSerializable* obj); + void Write(const Quaternion& data); public: // [Stream] diff --git a/Source/Engine/Tests/TestNetworking.cpp b/Source/Engine/Tests/TestNetworking.cpp new file mode 100644 index 000000000..c249d7fc8 --- /dev/null +++ b/Source/Engine/Tests/TestNetworking.cpp @@ -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 + +TEST_CASE("Networking") +{ + SECTION("Test Network Stream") + { + auto writeStream = New(); + auto readStream = New(); + + // 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); + } +}