Search code examples
c#openglxnamonogame

How can I transform billboarded sprites into y-axis aligned billboarded sprites?


I'm looking at Shawn Hargreave's old xna tutorials about billboarding 2D sprites, and in the last example he shows how its possible to use spritebatch to draw all of the billboarded sprites within a single SpriteBatch.Begin and End.

However, the method he presents causes billboarded sprites to face the camera regardless of where the camera is. For example, if the camera is directly above a sprite, the sprite will look like its lying on its back. This is also an issue with lesser angles and can cause the sprite to clip into 3D objects.

Here is the relevant code from the blog:

`Matrix invertY = Matrix.CreateScale(1, -1, 1);

basicEffect.World = invertY;
basicEffect.View = Matrix.Identity;
basicEffect.Projection = projection;

spriteBatch.Begin(0, null, null, DepthStencilState.DepthRead, RasterizerState.CullNone, basicEffect);

for each billboard sprite
{
    Vector3 textPosition = new Vector3(0, 45, 0);

    Vector3 viewSpaceTextPosition = Vector3.Transform(textPosition, view * invertY);

    const string message = "hello, world!";
    Vector2 textOrigin = spriteFont.MeasureString(message) / 2;
    const float textSize = 0.25f;

    spriteBatch.DrawString(spriteFont, message, new Vector2(viewSpaceTextPosition.X, viewSpaceTextPosition.Y), Color.White, 0, textOrigin, textSize, 0, viewSpaceTextPosition.Z);
}

spriteBatch.End(); `

Its mildly psuedo code but I think that's probably just a typo on his part. Notice how he does not need to change the basic effect more than once.

I'm not an expert with matrix math and am hoping there's some sort of simple solution to force the sprites to only rotate around their Y (up) axis (Remove Pitch).

I'm able to get the desired effect using custom shaders, but this rules out spritebatch and the nice sorting system it has baked in.


Solution

  • If you control your camera (View) angles as Euler angles (e.g. XRot around X-axis and YRot around Y-axis or Z-axis when Z is your up), e.g. a dome-like camera), you can do something like this (assuming your XRot and YRot angles are in degrees):

    ...
    var invertY = Matrix.CreateScale(1, -1, 1);
    var invertXAngle = Matrix.CreateFromAxisAngle(new(1, 0, 0), (-XRot + 90) * MathF.PI / 180f);
    var world = invertY * invertXAngle;
    basicEffect.World = world;
    basicEffect.View = Matrix.Identity;
    basicEffect.Projection = Projection;
    ...
    

    This:

    ...
    Vector3 viewSpaceTextPosition = Vector3.Transform(textPosition, View * invertY)
    ...
    

    ...then becomes:

    ...
    Vector3 viewSpaceTextPosition = Vector3.Transform(textPosition, View * world);
    ...
    

    Here, 'View' must be the correct view matrix of the camera. This 'removes' the X rotation from the cameras view matrix on a sprite local basis. Depending on your interpretation of the camera rotation, (-XRot + 90) can also be (-XRot - 90).

    To even improve its speed, you can also cache 'View * world' as it does not change for each sprite. Let me know if this helped. If needed, I can also add code for the view matrix calculation of a dome-like camera.