tbrosman / hxmath

A math library for Haxe.
MIT License
92 stars 10 forks source link

Fewer allocations in Frame classes #44

Open tbrosman opened 8 years ago

tbrosman commented 8 years ago

I'm weighing the following possible approaches. Design A is what is implemented now. Tangentially related to issue #23 :)

Design A: Complex objects instantiated when getting translation/rotation

interface IFrame2
{
    public var offset(get, set):Vector2;
    public var angleDegrees(get, set):Float;
}
interface IFrame2
{
    public var offsetX(get, set):Float;
    public var offsetY(get, set):Float;
    public var angleDegrees(get, set):Float;
}
class Frame2
{
    public var offsetX:Float;
    public var offsetY:Float;
    public var angleDegrees:Float;
}
player-03 commented 2 years ago

I'm not sure I follow the part about not being able to inline getter calls. Other than the fact that you're using interfaces, which are obviously going to prevent it. Do you think users will need to switch between Frame2Default and FlxSpriteFrame2 on the fly? Because I'm pretty sure if you go the Frame2Type route, you could inline everything just fine.

I've never been one to try to tie mathematical constructs to a game engine's objects. I'd rather keep them in sync by hand. Though part of that might be that I don't use Flixel or similar. I write all the code to move, rotate, and so on, then once per frame send it to the engine for rendering. If I was relying on the engine to handle more of the logic, I might actually care about integration.


Anyway, how about a couple more design options?

tbrosman commented 2 years ago

It has been a while since I thought about this. Coming back to it, I think I was trying to do too many things with a single type. Frames were supposed to accomplish two things:

6+ years later, I think these two concerns should be decoupled. The "Frame" types should only solve representation, and some static adapter class should solve the "sync with existing engines" problem. @player-03 I agree that storing the offset as a Vector2 makes the most sense. All the convenience methods live there already. I also agree that storing it as an explicit member rather than a getter (with slightly weird semantics: see cons of design A) is more straightforward. The performance tradeoff is probably moot in Haxe 4.

As for the rotation part, 2x2 matrices don't convert nicely into angles for all cases. FlxSpriteFrame2 is a (admittedly heavy-handed) attempt to have both and keep them in sync with the downside that you can never set the matrix directly. Exposing the matrix means non-uniform scales, reflections, shears, etc. may be specified. Extracting the angle by using atan2 on one of the basis vectors would give a useless result. One of the principles behind hxmath was to design the APIs so that the client code author doesn't need to know about the edge cases to get the math right.

There's another option for rotations: rotors, which are generalized quaternions. In 2D they look like one of the basis vectors from the rotation matrix. See http://geocalc.clas.asu.edu/GA_Primer/GA_Primer/introduction-to-geometric/rotors-and-rotations-in-the.html for a short derivation. There are fewer degrees of freedom to deal with, though it is still possible to build a rotor that isn't normalized. Building a rotation matrix of a rotor is trivial as well. The downside is that most engines don't work with rotors, so it might just result in more allocations than it is worth. Summary of options:

Option 1: Matrices for concat

Option 2: Motors for concat

player-03 commented 2 years ago

Wow, that was a rabbit hole. Geometric algebra certainly opens up options.

I'd recommend linking to this introductory article as documentation, in addition to or instead of the one you linked above.


(with slightly weird semantics: see cons of design A)

But those weird semantics resulted from using a getter, right? If the vector is a normal variable (or (default, null), or a final variable), then you aren't incurring extra allocations, and frame.offset.x += 30 works just fine.

Exposing the matrix means non-uniform scales, reflections, shears, etc. may be specified.

That's why I suggested storing multiple Vector2s instead of one matrix. Then the user can call orthoNormalize() after making changes. Similar to how you'd normalize a Quaternion if you performed an operation that modified the length.

I definitely see where you're coming from - don't give users the option to shoot themselves in the foot if you can avoid it.

Building a rotation matrix of a rotor is trivial as well.

More so in 2D than in 3D, but yeah.

The downside is that most engines don't work with rotors

From what I've read so far, it sounds like you can use the same underlying type for Rotor3 and Quaternion. You're right for 2D rotors, though.

Changing the angle is very slow (requires use of atan2, sin, and cos).

I don't think that's true.


As for the big question, I think we should consider use cases. Why go to the trouble of constructing a frame at all? Maybe it's just to store an offset plus rotation and retrieve them later. But more likely, it's for the transformation functionality: you want to convert to/from local coordinates. This functionality either requires a matrix or something equivalent to it (a rotor, a vector's rotate() function, it's all pretty much the same).

  1. If you change the angle several times per coordinate conversion, it's faster to store the angle as a float, and calculate the matrix/rotor only when needed.
  2. If you alternate between changing the angle and doing a coordinate conversion, the approaches are equal.
  3. If you do several coordinate conversions per angle change, it's faster to store the angle as a matrix or rotor, and calculate the float rotation only when needed.

All of the games I've made thus far fall under use case 3. And even if I made a game where the angle was constantly changing, it'd actually change exactly once per frame, and there would always be at least one coordinate conversion in between, resulting in use case 2. I can't think of an example of use case 1 at the moment (and even if I could, it's easy to store your own float and work around the problem).

Conclusion: Option 2C gets my vote.