How to have a flat sprite in a 3D environment?

Almost! But cannot use the billboard…

I managed to create the quad, to understand TriangleList and TriangleStrip, to display it, to texture it, to make it deal with alpha!

I see the sprite, I can move the camera, rotate the camera…

But if I apply a billboard, it disappears.

My quad :
`
quadVertices = new VertexPositionTexture[4];

        quadVertices[0].Position = new Vector3(-1, -1, 0);  // Lower left
        quadVertices[1].Position = new Vector3(-1, 1, 0);   // Upper left
        quadVertices[2].Position = new Vector3(1, -1, 0);   // Lower right
        quadVertices[3].Position = new Vector3(1, 1, 0);    // Upper right

        quadVertices[0].TextureCoordinate = new Vector2(0, 1);
        quadVertices[1].TextureCoordinate = new Vector2(0, 0);
        quadVertices[2].TextureCoordinate = new Vector2(1, 1);
        quadVertices[3].TextureCoordinate = new Vector2(1, 0);

`

How I display the quad:

        mySpriteEffect.View = pView;
        mySpriteEffect.Projection = pProjection;
        mySpriteEffect.World = pWorld;
        //mySpriteEffect.World = Matrix.CreateBillboard(Vector3.Zero, cameraPosition, pWorld.Up, pWorld.Forward);
        mySpriteEffect.Texture = pTexture;

        // No need for this, but it will work with advanced effect with multiple passes
        foreach (var pass in mySpriteEffect.CurrentTechnique.Passes)
        {
            pass.Apply();

            graphics.GraphicsDevice.DrawUserPrimitives(
                PrimitiveType.TriangleStrip,
                pQuad,
                0,
                2);
        }

My camera is at 0,0,10.

My world is : Matrix.Identity

My view is:

        view = Matrix.CreateLookAt(
            cameraPosition,
            targetPosition,
            Vector3.Up
            );
        view = view * Matrix.CreateRotationY(camRotation);

Target position is 0,0,0.

My projection is:

        projection = Matrix.CreatePerspectiveFieldOfView(
            MathHelper.ToRadians(45f),
            16f/9f,
            1f,
            100f
            );

Any idea?
If I compare Matrix.Identity with the result of CreateBillboard, all seems inverted…

You may want to set the CullMode to None while testing the create billboard function.

Your texture coordinates are also inverted unless your doing that on purpose.

quadVertices[0].TextureCoordinate = new Vector2(0, 0);
quadVertices[1].TextureCoordinate = new Vector2(0, 1);
quadVertices[2].TextureCoordinate = new Vector2(1, 0);
quadVertices[3].TextureCoordinate = new Vector2(1, 1);

One thing to note right away is you never do this.

The view is meant to be created by CreateLookAt from a world object and passed to the shader.
If you try to rotate it or translate it you basically will destroy its functionallity and get garbage output from everything you draw.

You can rotate your camera’s target forward normal if you want and then send that to create look at.
So this means you need to be aware of how CreateLookAt works if you want to directly pass your cameras objects matrix’s Forward you got from CreateWorld to CreateLookAt directly.

The parameter of CreateLookAt(…, Target, …) is a little obscure.
What this parameter expects is a actual target position to point the camera at within your world, but it doesn’t offset that position from the position parameter you also pass to the function. This means if you want to pass some vector that represents the idea of a direction not a position You must account for that distinction yourself. Say you are sending it a rotation to look at, then it can get a little tricky.

For example.

var m = Matrix.CreateRotationY(someKeyboardControlledAngle)
var forwardNormal = m.Forward;

The above value is actually a normalized directional forward vector now.
Its a vector that’s elements are composed of values between -1 to 1
you cant use that as a target position in create look at directly,
it will always look at the same place.

Also the target passed to create look at gets normalized.
So in this particular case of using a normal direction to look at…
you want to denormalize it * 10.

so this is how you pass it in.

view = Matrix.CreateLookAt(
    cameraPosition,
    cameraPosition + (forwardNormal * 10f),
    Vector3.Up
    );

Ironically CreateWorld takes the idea of a direction not a position for it’s forward parameter.

Keep the idea of a view and projection matrix seperate as if these are stand alone items.
The projection matrix is used on all your world objects so is the view. the view however is created from just one world object you have already completely set up.
When that world object (the one you picked to be the camera), position or rotation changes,
you should rebuild the view matrix afterwards from its values.

To make that clear the view is a special transformation that must be applied to all world objects but it is not a world object itself nor is it’s built translation / (position), the same as the world object it is created from. As such you need to just remember that the view is created from a world object that you designate to be the camera in your world.

Thanks a lot!

I have implemented an FPS camera style according to your advice, and I calculate the direction of the camera to deal with the movements:

Example:

        if (Keyboard.GetState().IsKeyDown(Keys.Z))  // forward
        {
            Matrix forwardMovement = Matrix.CreateRotationY(camDirection);
            Vector3 v = new Vector3(0, 0, -speed);
            cameraPosition.Z += v.Z;
            cameraPosition.X += v.X;
        }

And my view is now:

        Matrix rotationMatrix = Matrix.CreateRotationY(camDirection);
        Vector3 forwardNormal = rotationMatrix.Forward;

        view = Matrix.CreateLookAt(
            cameraPosition,
            cameraPosition + (forwardNormal * 10f),
            Vector3.Up
            );

I have fixed vertices and texture coordinates:

        quadVertices[0].Position = new Vector3(-1, -1, 0);  // Lower left
        quadVertices[1].Position = new Vector3(-1, 1, 0);   // Upper left
        quadVertices[2].Position = new Vector3(1, -1, 0);   // Lower right
        quadVertices[3].Position = new Vector3(1, 1, 0);    // Upper right

        quadVertices[0].TextureCoordinate = new Vector2(0, 1);
        quadVertices[1].TextureCoordinate = new Vector2(0, 0);
        quadVertices[2].TextureCoordinate = new Vector2(1, 1);
        quadVertices[3].TextureCoordinate = new Vector2(1, 0);

Then I removed the culling:

        RasterizerState rasterizerState = new RasterizerState();
        rasterizerState.CullMode = CullMode.None;
        graphics.GraphicsDevice.RasterizerState = rasterizerState;

And my quad appears (with the billboard applied as world) !

And I can note that it is reversed (My picture is mirrored). It explains why it was not visible with the culling. Why?

If the word is Matrix.Identity, the quad is on the right side, but with the Billboard matrix, wrong side…

Here is my code for the billboard:

mySpriteEffect.World = Matrix.CreateBillboard(Vector3.Zero, cameraPosition, Vector3.Up, null);

I tried to tink the 4th parameter (cameraForwardVector) but it seems ignored.

This is the last step I believe. I’m wondering what I’m doing wrong.

Edit: I tried to use CreateBillboard on a 3D model, and same result, I then see the back of the model…

CreateBillboard interally negates the forward vector. Which means the entire world matrix for the billboarded world object is spun around towards the camera. Also this method really expects all the fields to be inputed, except in simple cases. It also manually normalizes the forward you pass it. So it takes a direction.
One way to handle this is to just use your inverted texture coordinates or put a bool on your create quad that lets you flip the uv to whichever way you want it.

            // raw usage.
            var billboardPos = new Vector3(0, 0, -5f);
            var cameraPos = Vector3.Zero;
            var up = Vector3.Up;
            var forward = billboardPos - cameraPos;
            // this forward one passed ends up being negated so it will be -forward or backward.
            // this means the quad will be fliped around towards you.
            // that can affect the culling direction of the quad.
            Matrix objsBillboardMatrix = Matrix.CreateBillboard(billboardPos, cameraPos, up, forward);

           // To demonstrate in a gif.
           // How i would use it practically to ensure that in all rotations it will work.
           // If that is indeed desired here i task the billboards up to be the cameras up.

            var billboardPos = wObjRandomthing.Position + new Vector3(1f,0f,0f);
            var cameraPos = wObjCamera.Position;
            var up = wObjCamera.World.Up;
            var forward = billboardPos - cameraPos;
            Matrix objsBillboardMatrix = Matrix.CreateBillboard(billboardPos, cameraPos, up, forward);
            // set this objects world matrix to be a billboard.
            wObjRandomthing.world = objsBillboardMatrix;

            Helpers.DrawQuad(GraphicsDevice, effect, viewSpaceCameraTransformer.GetWorldViewProjection(wObjRandomthing), textureOrient);

In the below image you can see that while both arrows in the world are undergoing severe rotations and position changes in all axis in relation to the camera, the one arrow texture stays billboarded to the camera.
It’s not billboarded to some terrain’s up, ( and camera to object positions, forward) (which in your case you would probably prefer to do) you would use the terrains up, to make it keep that upwards orientation, in the case of the camera rolling.

Thanks!

I’m still confused, I still do not understand why it’s not “facing” the camera :(.
I don’t understand when you write “Which means the entire world matrix for the billboarded world object is spun around towards the camera”. Does it mean it’s normal that the result show the back of the quad? Does not sound like the behavior expected by the function. Is my quad wrong? But I got the same result with a 3D model…

I tried your code for calculating the forward and the result is the same (forward = 0,0,-10), I see the back of my quad. If I pass the inverted value of forward (forward = 0,0,10)… no change. It seems the forward parameter do not change the orientation.

The only way I found is:

        mySpriteEffect.World = Matrix.CreateBillboard(Vector3.Zero, cameraPosition, Vector3.Up, forward);
        mySpriteEffect.World *= Matrix.CreateRotationY(MathHelper.Pi);

EDIT : Ignore this last workaround :slight_smile: it’s working only if the quad is at 0,0,0 of course…

But I feel like I workaround something I do wrong on the first place.

Ok sorry i was thinking you could invert the forward to fix that well.

Does it mean it’s normal that the result show the back of the quad?

Normally id say no in fact im not sure why the function is negating the forward direction honestly i would do this myself with create world. Either way it’s sort of trivial for a billboard since you want the quads rotation locked to the cameras backwards then again the CreateLookAt swaps the forward as well.

I wouldn’t worry too much about the texture being off though just flip the u or v or both.
Just keep a seperate quad for this specifically.
I build a quad just for billboarding and then adjust the uv’s (u,v) as needed.

If you want to flip a texure horizontally or vertically…
just flip the whole columns x or y from 0 to 1 or 1 to 0.
for example. To flip this around so its upside down.

change this (u,v)

  quadVertices[0].TextureCoordinate = new Vector2(0, 1);
  quadVertices[1].TextureCoordinate = new Vector2(0, 0);
  quadVertices[2].TextureCoordinate = new Vector2(1, 1);
  quadVertices[3].TextureCoordinate = new Vector2(1, 0);

to this

    quadVertices[0].TextureCoordinate = new Vector2(0, 0);
    quadVertices[1].TextureCoordinate = new Vector2(0, 1);
    quadVertices[2].TextureCoordinate = new Vector2(1, 0);
    quadVertices[3].TextureCoordinate = new Vector2(1, 1);

if you had instead wanted to flip it so its left was its right.
change this (u,v)

  quadVertices[0].TextureCoordinate = new Vector2(0, 1);
  quadVertices[1].TextureCoordinate = new Vector2(0, 0);
  quadVertices[2].TextureCoordinate = new Vector2(1, 1);
  quadVertices[3].TextureCoordinate = new Vector2(1, 0);

to this.

  quadVertices[0].TextureCoordinate = new Vector2(1, 1);
  quadVertices[1].TextureCoordinate = new Vector2(1, 0);
  quadVertices[2].TextureCoordinate = new Vector2(0, 1);
  quadVertices[3].TextureCoordinate = new Vector2(0, 0);

The Matrix.CreateBillboard is like CreateWorld in that it gives you a world matrix but like CreateLookAt.
When the camera rotates or moves around in your world, you need to update these billboards.

It just takes a bit of practice.

Thank you :slight_smile:
Inverting the texture means I need to keep the culling off. And I guess it’s not performance friendly right?

Perhaps I should do the math by myself, calculate the right angle and rotate the quad accordingly.

Inverting the texture means I need to keep the culling off. And I guess it’s not performance friendly right?

You have plenty of ways to deal with culling.
You can switch the culling direction order on the graphics device before you draw.
(not performance freindly to switch state, but there is nothing to cull its a billboard)
You can change the actual vertice order passed i.e. the order of the vertices you make the quad from.
(zero penaltys)
You can use a vertex position normal texture structure and assign the normal direction yourself per vertice to point in either direction forward or backwards. (only penalty is its 12 bytes bigger)

The way culling works is like so you have a triangle like this.
A
|_\B
C

If you make the triangle vertice position A B C for vertice [0] [1] [2]
That is a clockwise triangle it has a clockwise winding order.

If you send in the vertices in the opposite order C B A for vertice [0] [[1] [2]
Then you get a counter clockwise triangle.

The culling you set on the graphics device has the options for both clockwise and counterclockwise as well as none.

The way these windings work is that they relate to the direction of a surface normal extending out from a triangle.
A cross product creates a normal from the found from 3 vertices, the order that you send them in to be crossed creates either a forward or backward surface normal to the perpendicular of the plane of the that the triangle lies upon, (the plane that all three vertices form in space).

Perhaps I should do the math by myself, calculate the right angle and rotate the quad accordingly.

Once you fully understand how the matrix parameters are interacting with the quad.
You will start to more clearly see how to set a matrix yourself without using createbillboard at all.

Ouaw… OK. Lot of concepts to digest when you deal with 3D, I need to improve myself. I’m impressed by your knowledge.

My need is to display creatures in a dungeon, and be sure that the quad is facing the camera when the player encounter one of them.

I try to have a code very simple.

I tested Matrix.CreateLookAt considering the quad is the camera, and the camera is the target, but cannot get something from that, the rotation was inverted…

Here is my try:

I just rotate the quad according to the cam rotation, then I move the quad to its position.

        mySpriteEffect.World = Matrix.Identity;
        mySpriteEffect.World *= Matrix.CreateFromAxisAngle(Vector3.UnitY, camDirection);
        mySpriteEffect.World *= Matrix.CreateTranslation(quadPosition);

I did a test and it seems to work. What do you think?

Im going to make a little image and post it so you can visualize it.

Your getting warmer but createfromaxis angle would need to use the terrains up. For the first term the second term in that function is just the radians of spin around that first angle so it may work but its not very flexible and is easily broken.

Hopefully i wrote that out right im pretty tired.

Amazing.
By the way, I looked at CreateBillboard code and the forward vector is used only when the distance is very closed and a round error can occurs:

        float num = vector.LengthSquared();
        if (num < 0.0001f)
        {
            vector = cameraForwardVector.HasValue ? -cameraForwardVector.Value : Vector3.Forward;
        }
        else
        {
            Vector3.Multiply(ref vector, (float)(1f / ((float)Math.Sqrt((double)num))), out vector);
        }

If I am not mistaken, CreateBillboard() assumes a quad with a normal that’s facing forward.
So you might want to test it with a camera positioned forward at (0,0,-10) that looks back at (0,0,0). Then swap your vertices left-right or rotate it 180.

That’s an interesting trail!

I assume that Monogame is using a right hand coordinates system:

http://i.imgur.com/AXMtUaI.gif

So my camera seems correct right?

I thought about the quad being facing the wrong way, but if I do not apply the billboard, my quad is oriented to the camera and the texture is correct. I don’t understand why I need to invert/rotate something being at the right place :frowning:

I found this:

https://social.msdn.microsoft.com/Forums/sqlserver/en-US/5de82565-3f54-4d96-9d0d-f0b4fc4a9b87/matrixcreatebillboard?forum=xnaframework

The user explained how he compared manual calculation with CreateBillboard and noticed that the Right.Z is negated. I’ll make a try as soon as I’m on my computer (I’m on my phone and literally obsessed about this :)).

EDIT:
It works! Inverting the Right vector fix the front/behind issue. I can now have the culling on.

Here is my code:

        Matrix billMatrix = Matrix.CreateBillboard(quadPosition, cameraPosition, Vector3.Up, pForward);
        billMatrix.Right = -billMatrix.Right;
        mySpriteEffect.World = billMatrix;
        mySpriteEffect.Texture = pTexture;

Should I do something with the normals of my quad instead of this? Like in this sample : https://github.com/CartBlanche/MonoGame-Samples/blob/master/TexturedQuad/Quad.cs ?

You had your camera facing origin from [0,0,10] (= [0,0,10Backward] or [0,0,-10Forward]) and your quad normal pointing backward.

if you rotate everything by 180 you will still be able to see it with you culling ON.
This time the camera facing origin from [0,0,-10] (= [0,0,-10Backward] or [0,0,10Forward]) and your quad normal pointing Forward.

Either way will work.
you can either (1) rotate 180 your quad with a matrix (rotQuad180Matrix * billMatrix ) which is cleaner code but slower,
or (2) negate billMatrix.Right which will have the same result as the rotQuad180Matrix,
or (3) hardcode your vertices to face forward instead of backward.

personally I prefer (3).
I think that’s also the standard with 3D models, you have to place the camera in-front of a model looking back at [0,0,0], that is if you want to see it face on. If you put the camera behind you will see the back of the model.

Well not to rain on the parade but since he’s having fun with this.
I should point out though i sort of hinted at the fact but didn’t directly elaborate on it, that you will soon run into another problem if you encounter a special case scenario.

Now that you have all this in your mind fresh its probably good to bring it up, there are two types of billboarding.

The one i showed in the gif is like completely billboarded no matter what that quad is billboarded but this isn’t desirable most of the time.
The one i described in the illustration is not reliant on the camera up.

Here’s a scenario to imagine.

Lets say you take your camera with you on a plane and fly over your virtual world. Imagine you are directly above a tree looking down.

If that tree is billboarded to the cameras up and not the terrains up what will you see?
2)
If it is billboarded to the terrains up what will you see?

In both cases you will see a messed up tree in the first it will look like the tree fell over on the ground. In the second case it will appear as thin line as you fly directly over it and look down from your plane.

What is normally done is this second case billboard most of the time, but for each tree object you have two billboards. One is for that special case which is the other type of billboarding. Or its another second case billboard were its forward is the terrains up its up is the terrains forward. That only starts to become visible as your cameras forward and the tree’s up when Dot(c.Forward, t.Up) start to return a result near 1.0f.

1 Like

This thread is fun, and I hope it is helpful for the community too, as a lot of information regarding billboarding is in there :slight_smile:

I’m in!

Here is my try (but not working):

        quadVertices = new VertexPositionNormalTexture[4];

        quadVertices[0].Position = new Vector3(-1, -1, 0);  // Lower left
        quadVertices[1].Position = new Vector3(-1, 1, 0);   // Upper left
        quadVertices[2].Position = new Vector3(1, -1, 0);   // Lower right
        quadVertices[3].Position = new Vector3(1, 1, 0);    // Upper right
        for (int i = 0; i < 4; i++)
        {
            quadVertices[i].Normal = Vector3.Forward;
        }

        quadVertices[0].TextureCoordinate = new Vector2(0, 1);
        quadVertices[1].TextureCoordinate = new Vector2(0, 0);
        quadVertices[2].TextureCoordinate = new Vector2(1, 1);
        quadVertices[3].TextureCoordinate = new Vector2(1, 0);

        Matrix billMatrix = Matrix.CreateBillboard(quadPosition, cameraPosition, Vector3.Up, pForward);
        //billMatrix.Right = -billMatrix.Right;

        mySpriteEffect.World = billMatrix;
        mySpriteEffect.Texture = pTexture;

        // No need for this, but it will work with advanced effect with multiple passes
        foreach (var pass in mySpriteEffect.CurrentTechnique.Passes)
        {
            pass.Apply();

            graphics.GraphicsDevice.DrawUserPrimitives<VertexPositionNormalTexture>(
                PrimitiveType.TriangleStrip,
                pQuad,
                0,
                2);
        }
    }

I tried with backward and forward, but no change.

Shouldn’t this be lower-left → upper-left → upper-right → bottom-right since you’re using a triangle-strip? LL, UL, LR, UR would spit out a bow-tie I’d think since the last edge is LL and LR, making the next triangle LR, LL, UR.

Not 100% certain since I don’t touch those old pit functions. Pretty sure Intel’s still convert those to indexed triangle lists in software on the driver.

Edit: Nope, my bad, I’m wrong. BC edge not CA edge for the next triangle edge.

Removed using wrong account : - D

I’ll jump in to this interesting thread : - D My Billboard implementation which for my 2.5D games ^_^y
To get the final entity billboard WORLD you should multiply the SCALE * ROTATION * BILLMATRIX and compute the billboard forward by using the camera final world matrix, camera.World.Forward

MY RULES :

  1. Billboard cannot resize on Z Axis
  2. Billboard can only rotate at Z Axis
  3. You pass the final camera.World.Forward when creating the Billboard
  4. Compute only when necessary __IsTo* and multiply by using Matrix with ref
  5. For a 2.5D games limit the camera when panning, but otherwise use willmote implementation

THE VERTICES:

            #region VERTICES
            //!!
            //!

            __VerticesPNT = new Z3D.Core.VertexPNT[4];
            //
            __VerticesPNT[0].Position.X  =  1.0f;
            __VerticesPNT[0].Position.Y  =  1.0f;
            __VerticesPNT[0].Position.Z  = -1.0f;
            __VerticesPNT[0].TextureUV.X =  0.0f;
            __VerticesPNT[0].TextureUV.Y =  0.0f;
            //
            __VerticesPNT[1].Position.X  = -1.0f;
            __VerticesPNT[1].Position.Y  =  1.0f;
            __VerticesPNT[1].Position.Z  = -1.0f;
            __VerticesPNT[1].TextureUV.X =  1.0f;
            __VerticesPNT[1].TextureUV.Y =  0.0f;
            //
            __VerticesPNT[2].Position.X  =  1.0f;
            __VerticesPNT[2].Position.Y  = -1.0f;
            __VerticesPNT[2].Position.Z  = -1.0f;
            __VerticesPNT[2].TextureUV.X =  0.0f;
            __VerticesPNT[2].TextureUV.Y =  1.0f;
            //
            __VerticesPNT[3].Position.X  = -1.0f;
            __VerticesPNT[3].Position.Y  = -1.0f;
            __VerticesPNT[3].Position.Z  = -1.0f;
            __VerticesPNT[3].TextureUV.X =  1.0f;
            __VerticesPNT[3].TextureUV.Y =  1.0f;

            //--> Normal
            __VerticesPNT[0].Normal = Vector3.Cross((__VerticesPNT[1].Position - __VerticesPNT[0].Position), (__VerticesPNT[2].Position - __VerticesPNT[0].Position));
            __VerticesPNT[0].Normal.Normalize();
            //
            __VerticesPNT[1].Normal = __VerticesPNT[0].Normal;
            __VerticesPNT[2].Normal = __VerticesPNT[0].Normal;
            __VerticesPNT[3].Normal = __VerticesPNT[0].Normal;

            //!
            //!!
            #endregion VERTICES    

BILLBOARD ENTITY WORLD FORMULATION :

internal void _M_Update( BaseCamera pCamera )
{


        //*>> Translate entity position
        //
        if ( __IsToTranslate  )
        {
            XF.Matrix.CreateTranslation(ref __Position, out __WPosition);                
        }

        //*>> S C A L I N G |  S I Z E
        //
        if ( __IsToScale )
        {
                __Scale.Z = 0.0f; 
                XF.Matrix.CreateScale(ref __Scale, out __WScale);   
        }

        //*>> R O T A T I O N
        //
        if ( __IsToRotate )
        {
                __Rotation.Y = 0.0f;
                __Rotation.X = 0.0f;              
                //
                XF.Matrix.CreateFromYawPitchRoll( 0.0f, 0.0f, XF.MathHelper.ToRadians(__Rotation.Z), out __WRotation);
         }

        //*>> W O R L D  M A T R I X
        //
        if ( __IsToTranslate || __IsToScale || __IsToRotate )
        {
        
                XF.Matrix m_MatrixToFaceCam = XF.Matrix.Identity;

                XF.Matrix.CreateBillboard
                (
                   ref __Position, 

                   ref pCamera.__Position, 
                   ref pCamera.__UpVector, 
                   pCamera.World.Forward, 

                   out m_MatrixToFaceCam
                );
                    
                //--> Final entity WORLD to pass on effect
                //
                ZH.Math.MatMul3( out __World, ref __WScale, ref __WRotation, ref m_MatrixToFaceCam );
    
        }   

}

Cheers ^_^y

1 Like