diff --git a/Source/engine/displacement.hpp b/Source/engine/displacement.hpp index 7aac6dbda..b7da81dae 100644 --- a/Source/engine/displacement.hpp +++ b/Source/engine/displacement.hpp @@ -78,6 +78,20 @@ struct Displacement { return *this; } + constexpr Displacement &operator/=(const int factor) + { + deltaX /= factor; + deltaY /= factor; + return *this; + } + + constexpr Displacement &operator/=(const float factor) + { + deltaX = static_cast(deltaX / factor); + deltaY = static_cast(deltaY / factor); + return *this; + } + constexpr friend Displacement operator+(Displacement a, Displacement b) { a += b; @@ -102,16 +116,43 @@ struct Displacement { return a; } + constexpr friend Displacement operator/(Displacement a, const int factor) + { + a /= factor; + return a; + } + + constexpr friend Displacement operator/(Displacement a, const float factor) + { + a /= factor; + return a; + } + constexpr friend Displacement operator-(const Displacement &a) { return { -a.deltaX, -a.deltaY }; } + constexpr friend Displacement operator<<(Displacement a, unsigned factor) + { + return { a.deltaX << factor, a.deltaY << factor }; + } + + constexpr friend Displacement operator>>(Displacement a, unsigned factor) + { + return { a.deltaX >> factor, a.deltaY >> factor }; + } + constexpr friend Displacement abs(Displacement a) { return { abs(a.deltaX), abs(a.deltaY) }; } + float magnitude() const + { + return static_cast(hypot(deltaX, deltaY)); + } + /** * @brief Returns a new Displacement object in screen coordinates. * @@ -154,9 +195,34 @@ struct Displacement { return { (2 * deltaY + deltaX) / 8, (2 * deltaY - deltaX) / 8 }; } - constexpr Displacement operator>>(size_t factor) + /** + * @brief Returns a 16 bit fixed point normalised displacement in isometric projection + * + * This will return a displacement of the form (-1.0 to 1.0, -0.5 to 0.5), to get a full tile offset you can multiply by 16 + */ + Displacement worldToNormalScreen() const + { + // Most transformations between world and screen space take shortcuts when scaling to simplify the math. This + // routine is typically used with missiles where we want a normal vector that can be multiplied with a target + // velocity (given in pixels). We could normalize the vector first but then we'd need to scale it during + // rotation from world to screen space. To save performing unnecessary divisions we rotate first without + // correcting the scaling. This gives a vector in elevation projection aligned with screen space. + Displacement rotated { (deltaY - deltaX), -(deltaY + deltaX) }; + // then normalize this vector + Displacement rotatedAndNormalized = rotated.normalized(); + // and finally scale the y axis to bring it to isometric projection + return { rotatedAndNormalized.deltaX, rotatedAndNormalized.deltaY / 2 }; + } + + /** + * @brief Calculates a 16 bit fixed point normalized displacement (having magnitude of ~1.0) from the current Displacement + */ + Displacement normalized() const { - return Displacement(deltaX >> factor, deltaY >> factor); + float magnitude = this->magnitude(); + Displacement normalDisplacement = *this << 16; + normalDisplacement /= magnitude; + return normalDisplacement; } constexpr Displacement Rotate(int quadrants) diff --git a/Source/missiles.cpp b/Source/missiles.cpp index 586efef85..c2892b9d6 100644 --- a/Source/missiles.cpp +++ b/Source/missiles.cpp @@ -100,22 +100,18 @@ constexpr Direction16 Direction16Flip(Direction16 x, Direction16 pivot) return static_cast(ret); } -void UpdateMissileVelocity(Missile &missile, Point destination, int v) +void UpdateMissileVelocity(Missile &missile, Point destination, int velocityInPixels) { missile.position.velocity = { 0, 0 }; if (missile.position.tile == destination) return; - // rotate so that +x leads to the right of screen and +y leads to the bottom (screen space but without the scaling factor) - double dxp = (destination.x + missile.position.tile.y - missile.position.tile.x - destination.y); - double dyp = (destination.y + destination.x - missile.position.tile.x - missile.position.tile.y); - double dr = sqrt(dxp * dxp + dyp * dyp); - // velocity is stored in screen coordinates so apply the scaling factor to the y axis while normalizing. - double normalizedX = dxp / dr; - double normalizedY = dyp / dr / 2; - missile.position.velocity.deltaX = static_cast(normalizedX * (v << 16)); - missile.position.velocity.deltaY = static_cast(normalizedY * (v << 16)); + // Get the normalized vector in isometric projection + Displacement fixed16NormalVector = (missile.position.tile - destination).worldToNormalScreen(); + + // Multiplying by the target velocity gives us a scaled velocity vector. + missile.position.velocity = fixed16NormalVector * velocityInPixels; } /** diff --git a/test/math_test.cpp b/test/math_test.cpp index 8c914de72..b2f96cd9e 100644 --- a/test/math_test.cpp +++ b/test/math_test.cpp @@ -6,7 +6,7 @@ namespace devilution { TEST(MathTest, WorldScreenTransformation) { - Displacement offset = { 5, 2 }; + Displacement offset { 5, 2 }; // Diablo renders tiles with the world origin translated to the top left of the screen, while the normal convention // has the screen origin at the bottom left. This means that we end up with negative offsets in screen space for // tiles in world space where x > y @@ -28,4 +28,35 @@ TEST(MathTest, WorldScreenTransformation) EXPECT_EQ(cursorPosition.screenToWorld().worldToScreen(), Displacement(320, -160)); } +TEST(MathTest, NormalizeDisplacement) +{ + // Normalizing displacements transforms the value into 16 bit fixed point representations + Displacement vector { 5, 0 }; + EXPECT_FLOAT_EQ(vector.magnitude(), 5); + EXPECT_EQ(vector.normalized(), Displacement(1 << 16, 0)); // (1.0, 0.0) + + vector = { 3, 4 }; + EXPECT_FLOAT_EQ(vector.magnitude(), 5); + EXPECT_EQ(vector.normalized(), Displacement(39321, 52428)); // ~(0.6, 0.8) + + vector = { -5, 2 }; + EXPECT_FLOAT_EQ(vector.magnitude(), 5.3851647f); + EXPECT_EQ(vector.normalized(), Displacement(-60848, 24339)); // ~(-0.92, 0.37) +} + +TEST(MathTest, MissileTransformation) +{ + // starting with a Displacement 2 world units West results in a vector pointing left of screen + EXPECT_EQ(Displacement(2, -2).worldToNormalScreen(), Displacement(-65536, 0)); + + // if it's not normalizing the vector then it's a problem :D + EXPECT_EQ(Displacement(4, -4).worldToNormalScreen(), Displacement(-65536, 0)); + + // Because of the isometric projection the y axis gets squashed + EXPECT_EQ(Displacement(8, 1).worldToNormalScreen(), Displacement(-40235, -25865)); // ~(0.6, 0.8/2) + + // in elevation projection this would be a vector with x == y, isometric projection means y == x/2 + EXPECT_EQ(Displacement(8, 0).worldToNormalScreen(), Displacement(-46340, -23170)); // ~(0.7, 0.7/2) +} + } // namespace devilution