Entities nested using TransformComponent2D.Parent

Are there any examples of how do render sprites when using entities nested using TransformComponent2D.Parent?

eg: if I have a ship with an attached turret, I want the turret entity to move, scale, rotate along with the ship, as well as having it’s own local transforms. Like you’d find in Unity or Cocos2D. Should that be possible within the Monogame.Extended ECS?

Use the WorldPosition, WorldRotation, etc for rendering or physics. Position, Rotation, etc are all in the local space relative to the parent.

Yep, that’s what I have. Here’s my SpriteRenderSystem:

protected override void Process(GameTime gameTime, Entity entity)
{
    // reference components
    var sprite = entity.Get<Sprite>();
    if (!sprite.visible || sprite.alpha < 0.01f) { return; }

    var transform = entity.Get<Transform>();

    // render sprite
    spriteBatch.Draw(
        sprite.frame.Texture,
        transform.WorldPosition,
        sprite.frame.Bounds,
        sprite.tint,
        transform.WorldRotation,
        sprite.originOffset,
        transform.WorldScale,
        sprite.effects,
        sprite.depth
    );
}

sprite.originOffset is the centre point of the sprite texture, basically Vector2(_frame.Width, _frame.Height) * 0.5f.

This works fine for translation and scaling, but I can’t get children rotating around their parent properly.

Uploading…

White is the parent, rotated 45 degrees. Green is offset (32,0), Blue offset (0,32). Grid is 16x16. The child entities rotate around their local centres, not the parent centre. I guess I need to change the origin parameter in the draw call but the correct expression eludes me for now…

By the looks of what you described I think the rotation of the parent is inherited for the children. I think the problem here is with getting SpriteBatch to render them correctly. The origin parameter is the offset applied to the position where you want (0, 0) to be for the local space of the sprite. Essentially this means it’s the pivot point for rotation and affects the final render position. Using matrices to understand how SpriteBatch operates, it would be something like:

Matrix1: -OriginTranslation * Scale * Rotation * PositionTranslation

To have the origin translation NOT affect the position translation, it has to be undone like so:

Matrix2: -OriginTranslation * Scale * Rotation * OriginTranslation * PositionTranslation

Now SpriteBatch unfortunately doesn’t allow to pass a handmade matrix to transform the 4 vertices of the quad, but the following would allow orbiting a target (by switching rotation and position).

Matrix3: -OriginTranslation * Scale * PositionTranslation * Rotation

So to fix this to work for you with SpriteBatch we can add a “hack” using the idea of Matrix2. Note that this isn’t really tested; I just wrote it on the spot. It probably doesn’t work for nesting correctly, but I do think it is something in the right direction.

var parentPosition = transform.Parent?.WorldPosition ?? Vector2.Zero;
spriteBatch.Draw(
        sprite.frame.Texture,
        transform.WorldPosition + parentPosition, // account for how SpriteBatch applies origin translation
        sprite.frame.Bounds,
        sprite.tint,
        transform.WorldRotation,
        sprite.originOffset - parentPosition, // move origin to parent location
        transform.WorldScale,
        sprite.effects,
        sprite.depth
    );

Thanks, I was sort of heading in the same direction to persuade SpriteBatch to do what I want.

var pp = transform.Parent?.WorldPosition ?? transform.WorldPosition;

spriteBatch.Draw(
    sprite.frame.Texture,
    transform.WorldPosition + (pp - transform.WorldPosition),
    sprite.frame.Bounds,
    sprite.tint,
    transform.WorldRotation,
    sprite.originOffset + (pp - transform.WorldPosition),
    transform.WorldScale,
    sprite.effects,
    sprite.depth
);

It need testing and so on, but it gives the following when I rotate the root (white) entity (the stuttering is just my lazy gif creation :)):

[edit]

Doesn’t work if I attach a child to the green/blue nodes.

I notice now MonoGame.Extended.Graphics has a Batcher2D class that looks like it can take a Matrix for each draw call, maybe that’ll work. I’m not wedded to the supplied SpriteBatch.

public void DrawSprite(
    Texture2D texture, ref Matrix2D transformMatrix, ref Rectangle sourceRectangle,
    Color? color = null, FlipFlags flags = FlipFlags.None, float depth = 0
)

The Batcher2D is experimental. It is more generic than SpriteBatch in that the implementation allows for drawing geometric shapes such as convex polygons, arcs and circles (the code for those are in my branch still). It also designed to match the APIs as close as possible to SpriteBatch allowing it exploit everyone’s prior knowledge and allow to be used as drop in replacement if desired. It really does need more testing to be sure, but I think SpriteBatch is so commonly used and lots of people have optimized it that I think there is no way that Batcher2D will be able to compete on the same performance level.

I really do think that a solution can be found using SpriteBatch though. Just moving the origin to the parent then applying a position translation to correct for final render position should work, just needs some thought of how with nested children.

EDIT:
Try:

spriteBatch.Draw(
    sprite.frame.Texture,
    transform.WorldPosition - parentPosition,
    sprite.frame.Bounds,
    sprite.tint,
    transform.WorldRotation,
    parentPosition + sprite.originOffset,
    transform.WorldScale,
    sprite.effects,
    sprite.depth
);

That didn’t work, sorry.

Any solution to this that I’ve seen has used a matrix-using batcher. In MonoGame land it looks like this means modifying SpriteBatch at the framework level, or throwing it out and using a custom batcher.

However, this page seems to have a non-matrix solution for combining an arbitrary level of nesting based on (I think, I haven’t studied it in much detail) applying successive transforms down from the root each time for each child. Perhaps this could be used to calculate a working set of parameters for SpriteBatch.

[montage: googling]

Seems modifying SpriteBatch this way is a long-time open issue #3156.

Based on various bits of other code I’ve seen to implement this, in TransformComponent2D I changed line 170 from:
Matrix2D.Multiply(ref matrix, ref localMatrix, out matrix);
to
Matrix2D.Multiply(ref localMatrix, ref matrix, out matrix);

My render method is:

var sprite = entity.Get<Sprite>();
if (!sprite.visible || sprite.alpha < 0.01f) { return; }

var transform = entity.Get<Transform>();

spriteBatch.Draw(
    sprite.frame.Texture,
    transform.WorldPosition,
    sprite.frame.Bounds,
    sprite.tint,
    -transform.WorldRotation,
    sprite.originOffset,
    transform.WorldScale,
    sprite.effects,
    sprite.depth
);

The result is:

Red is a child of Green with local position 8,0. Only White is rotating.

Oh, well there is a bug then? Can’t believe that was overlooked lol

@nivrig Nicely spotted :slight_smile:

We should really have a demo of these features to reduce the chance of bugs like these getting missed so easily.

Oh, btw… I just noticed this line appears 3 times in our code base:

Transform.cs(290):            Matrix2D.Multiply(ref matrix, ref localMatrix, out matrix);
TransformComponent.cs(172):   Matrix2D.Multiply(ref matrix, ref localMatrix, out matrix);
TransformComponent2D.cs(170): Matrix2D.Multiply(ref matrix, ref localMatrix, out matrix);

One of them is in the currently broken platformer demo, but still, code duplication is never good. In this case, it appears to be difficult because TransformComponent is already inheriting from EntityComponent. Sometimes duplication like this is difficult to avoid but there’s many tools in the toolbox to deal with this kind of thing. It might be worth a refactor at some point.

Anyway, duplicate code means duplicate bugs. I guess I’ll change them all and deal with the duplication later.

Wait a second. Something’s not right here.

I just ran the demos after making those changes and the Scene Graphs demo is messed up. I don’t know if it’s the demo or the changes but in any case I guess this needs a little more thought before steaming ahead.

I’ll revert the changes.

Heh I didn’t mean that as a PR :slight_smile: I didn’t get a chance to test it much further, and actually the deeply nested children (red in the example) don’t behave when they are rotating themselves.

More digging.

Referring to this debug SpriteRenderSystem:

Based on code here, this system recalculates each TransformComponent2D’s world matrix from scratch every frame by walking up and down the hierarchy. Obviously, this is bad :), but the results are interesting when compared to the built-in TransformComponent2D.WorldMatrix (specifically WorldPosition) value.

[GIF]

This shows a system of entities nested 3 deep and each individually rotating, rendered twice, once in white using the recalculated matrix and again in red using TransformComponent2D.WorldMatrix.

WorldScale and WorldRotation look to be correct, however the logs show TransformComponent2D.WorldPosition drifting away from the recalculated matrix’s position IF the transform has a parent, whereas the recalculated matrix seems correct for any nested depth/scale/rotation that I tested.

  1. swapped the matrix order in RecalculateWorldMatrix as earlier in this thread
  2. removed Parent references from RecalculateLocalMatrix
    https://gist.github.com/johngirvin/a60ae7f164d3490ff79f902cf2759eab

The draw call is now (note the negation of rotation angle - seems to be a different between Matrix and Matrix2D):

spriteBatch.Draw(
    sprite.frame.Texture,
    transform.WorldPosition,
    sprite.frame.Bounds,
    sprite.tint,
    -transform.WorldRotation,
    sprite.originOffset,
    transform.WorldScale,
    sprite.effects,
    sprite.depth
);

This works for my multiply nested examples (red and gold are the TransformComponent2D ones):
[GIF]

A similar change to Transform2D works for the SceneGraphDemo:
[GIF]

Awesome. If this works in all cases then I would be happy to have these changes applied in develop.

As for the negative rotation, the rectangular coordinate system used in mathematics and the coordinate system for SpriteBatch are different; the y-axis is flipped. (The flip could be adjusted in the projection matrix where the Z-axis is also flipped to correct for how right-handed coordinate systems work in graphics.) In the right-handed rectangular coordinate system from mathematics the positive rotation about the Z-axis is counter-clockwise, but with the SpriteBatch coordinate system, the same angle is clockwise. Unfortunately, everyone is use to the flipped y-axis that SpriteBatch uses. That’s why you see the negative rotation applied in the matrices. You didn’t include the negative rotation in your changes so that’s why you have to do it when drawing the sprite. Personally I would prefer the coordinate system to match what is taught in school and textbooks but that would break out-of-the-box compatibility with many SpriteBatch tutorials found online. Also it is possible that new people coming to game development don’t have the education in the linear algebra to understand how coordinate systems work, specifically the world, view, and projection matrices and how they work in computer graphics. I can be argued wether people really need to know that stuff for 2D game development though. (For anyone reading this and want to learn more check out immersive math).

As for the code duplication, the ECS takes control of instantiating the components so they can be pooled, a necessary technique where components and entities have the potential be destroyed/created many times as game time goes on. Unfortunately this means that Transform2D can’t be inherited since EntityComponent is inherited. I rather not have people implement the IPoolable by hand if it can be avoided as it easy to get wrong / mess-up. Maybe that is not a great reason and a new interface IEntityComponent can be introduced to create specialized custom components.

I found that if I put the -ve rotation in to the RecalculateLocalMatrix calculation, things went weird.

private void RecalculateLocalMatrix(out Matrix2D matrix)
{
    matrix = Matrix2D.CreateScale(_scale) *
             Matrix2D.CreateRotationZ(-_rotation) *
             Matrix2D.CreateTranslation(_position);
}

spriteBatch.Draw(
    ....
    transform.WorldRotation,
    ....
);

Only white is rotating here:

RE: DRY - maybe make TransformComponent2D a facade over an embedded Transform2D<?> ?

Yes you are right. The following works correctly for all cases:

        protected internal override void RecalculateWorldMatrix(ref Matrix2D localMatrix, out Matrix2D matrix)
        {
            if (Parent != null)
            {
                Parent.GetWorldMatrix(out matrix);
                Matrix2D.Multiply(ref localMatrix, ref matrix, out matrix);
            }
            else
            {
                matrix = localMatrix;
            }
        }

        protected internal override void RecalculateLocalMatrix(out Matrix2D matrix)
        {
            matrix = Matrix2D.CreateScale(_scale) *
                     Matrix2D.CreateRotationZ(_rotation) *
                     Matrix2D.CreateTranslation(_position);
        }
                    var texture = ...;
                    var sourceRectangle = ...;
                    var color = ...;
                    var position = WorldPosition;
                    var rotation = -WorldRotation;
                    var scale = WorldScale;
                    var origin = ....;

                    spriteBatch.Draw(texture, position, sourceRectangle, color, rotation, origin,
                        scale);

The scene graphs demo is doing extra work when rendering which is not needed.

Sent a PR for review.