0
\$\begingroup\$

In Unity, whenever I rotate an object, it also performs a translation. If I simply try to draw a quad with the quaternion used in Unity, it appears off because of that missing translation unity applies. I can 'replicate' it by performing each step in openGL in the same order that Unity does:

  • scale first,

  • (translation offset)

  • rotate Z

  • (translation offset)

  • rotate X

  • (translation offset)

  • rotate Y

  • (translation offset)

  • and final translation

My question is, given a quaternion that will be applied to a quad to rotate it, how can I also calculate this final translation that needs to be applied to the quad to mimic the behavior exhibited in Unity? Apologies if my methodology, terminology isn't the best. I welcome any advice and filling of knowledge gaps I might be displaying.

Examples:

In Unity, I simply change the position / scale / rotation in editor. Values for rotation displayed are simply Euler angles like how unity represents them in the editor.

(Notice the Y rotation before and after applying a y 290 degree Euler on it)

[Unrotated sprite1

Rotated sprite

This raylib (wrapping OpenGL calls) replicates the behavior perfectly

void Draw3DBillboardRecUnity(Camera camera, Texture2D texture, Rectangle source, Vector3 position, Vector2 size, Vector3 eulerAngles, Vector3 scale, Color tint) {
    rlPushMatrix();

       Vector3 camPos = camera.position;
       Vector3 camTar = camera.target;
       Vector3 camUp = camera.up;
       // get the camera view matrix
       Matrix mat = MatrixInvert(MatrixLookAt(camPos, camTar, camUp));
       // peel off just the rotation
       Quaternion quat = QuaternionFromMatrix(mat);
       mat = QuaternionToMatrix(quat);
       
       // apply just the rotation
       rlMultMatrixf(MatrixToFloat(mat));
       
       // draw the billboard
       float width = size.x / 2;
       float height = size.y / 2;
       // initial spot
       rlScalef(1, 1, 1);
       rlTranslatef(width, height, 0);
       
       // null out position experiment
   //    position = Vector3Zero();
       
       float positionOffsetX = position.x - width * 0.5;
       float positionOffsetY = position.y - height * 0.5;
       Vector3 positionOffsetVector;
       positionOffsetVector.x = positionOffsetX;
       positionOffsetVector.y = positionOffsetY;
       positionOffsetVector.z = position.z;
       Vector3 positionOffsetVectorInverse = Vector3Negate(positionOffsetVector);
       
       // apply scaling first
       if (vectorAvailable(scale)) {
           rlScalef(scale.x, scale.y, scale.z);
           if (vectorAvailable(position))
               rlTranslatef(positionOffsetVector.x, positionOffsetVector.y, positionOffsetVector.z);
           else
               rlTranslatef(-width, -height, 0);
       }
       
       // apply rotation
       if (rotVectorAvailable(eulerAngles)) {
           
           // prepare yourself
           rlTranslatef(positionOffsetVector.x, positionOffsetVector.y, positionOffsetVector.z);
          
           if (eulerAngles.z > 180) {
               // eh questionable
               rlTranslatef(positionOffsetVector.x, positionOffsetVector.y, positionOffsetVector.z);
               rlRotatef(180 - (eulerAngles.z - 180), 0, 0, 1);
               rlRotatef(180 - (eulerAngles.z - 180), 0, 0, 1);
               rlTranslatef(positionOffsetVectorInverse.x, positionOffsetVectorInverse.y, positionOffsetVectorInverse.z);
           } else {
               rlTranslatef(positionOffsetVector.x, positionOffsetVector.y, positionOffsetVector.z);
               rlRotatef(eulerAngles.z, 0, 0, 1);
               rlTranslatef(positionOffsetVectorInverse.x, positionOffsetVectorInverse.y, positionOffsetVectorInverse.z);
           }
           
           rlTranslatef(positionOffsetVector.x, positionOffsetVector.y, positionOffsetVector.z);
           rlRotatef(eulerAngles.x, -1, 0, 0);
           rlTranslatef(positionOffsetVectorInverse.x, positionOffsetVectorInverse.y, positionOffsetVectorInverse.z);
           
           if (eulerAngles.y > 180) {
               // greater than 180
               rlTranslatef(positionOffsetVector.x, positionOffsetVector.y, positionOffsetVector.z);
               rlRotatef(eulerAngles.y, 0, -1, 0);
               rlTranslatef(positionOffsetVectorInverse.x, positionOffsetVectorInverse.y, positionOffsetVectorInverse.z);
           } else {
               // less than 180
               rlTranslatef(positionOffsetVectorInverse.x, positionOffsetVectorInverse.y, positionOffsetVectorInverse.z);
               rlRotatef(eulerAngles.y, 0, -1, 0);
               rlTranslatef(positionOffsetVector.x, positionOffsetVector.y, positionOffsetVector.z);
           }
           
           // inverse
           rlTranslatef(positionOffsetVectorInverse.x, positionOffsetVectorInverse.y, positionOffsetVectorInverse.z);
       }
       
       // apply transformation last
       if (vectorAvailable(position)) {
           rlTranslatef(positionOffsetVector.x, positionOffsetVector.y, positionOffsetVector.z);
       }
       
       Color color = tint;

       rlCheckRenderBatchLimit(6);

       rlSetTexture(texture.id);

       // draw quad
       rlBegin(RL_QUADS);
       rlColor4ub(tint.r, tint.g, tint.b, tint.a);

       rlTexCoord2f((float)source.x / texture.width, (float)(source.y + source.height) / texture.height);
       rlVertex3f(-width, -height, 0);  // Bottom Left Of The Texture and Quad
       
       rlTexCoord2f((float)(source.x + source.width) / texture.width, (float)(source.y + source.height) / texture.height);
       rlVertex3f(+width, -height, 0);  // Bottom Right Of The Texture and Quad
      
       rlTexCoord2f((float)(source.x + source.width) / texture.width, (float)source.y / texture.height);
       rlVertex3f(+width, +height, 0);  // Top Right Of The Texture and Quad

       rlTexCoord2f((float)source.x / texture.width, (float)source.y / texture.height);
       rlVertex3f(-width, +height, 0);  // Top Left Of The Texture and Quad

       rlEnd();
       rlSetTexture(0);
       rlPopMatrix();
}

With this result:

Rendered result

However this code that simply applies the quaternion, does not.

void Draw3DBillboardRecAlmost(Camera camera, Texture2D texture, Rectangle source, Vector3 position, Vector2 size, Vector3 eulerAngles, Vector3 scale, Color tint) {
    Vector2 sizeRatio = { size.x, size.y };
    
    Quaternion tQuat = QuaternionFromEuler(eulerAngles.x*DEG2RAD, eulerAngles.y*-DEG2RAD, eulerAngles.z*-DEG2RAD);

    Matrix matView = MatrixLookAt(camera.position, camera.target, camera.up);

    Vector3 right = { matView.m0, matView.m4, matView.m8 };
    Vector3 up = { matView.m1, matView.m5, matView.m9 };
    
    // rotate both up + right vectors to achieve same effect like unity
    up = Vector3RotateByQuaternion(up, tQuat);
    right = Vector3RotateByQuaternion(right, tQuat);
    
    Vector3 rightScaled = Vector3Scale(right, sizeRatio.x/2);
    Vector3 upScaled = Vector3Scale(up, sizeRatio.y/2);

    Vector3 p1 = Vector3Add(rightScaled, upScaled);
    Vector3 p2 = Vector3Subtract(rightScaled, upScaled);

    Vector3 topLeft = Vector3Scale(p2, -1);
    Vector3 topRight = p1;
    Vector3 bottomRight = p2;
    Vector3 bottomLeft = Vector3Scale(p1, -1);

    // Translate points to the draw center (position)
    topLeft = Vector3Add(topLeft, position);
    topRight = Vector3Add(topRight, position);
    bottomRight = Vector3Add(bottomRight, position);
    bottomLeft = Vector3Add(bottomLeft, position);

    rlCheckRenderBatchLimit(4);

    rlSetTexture(texture.id);

    rlBegin(RL_QUADS);
        rlColor4ub(tint.r, tint.g, tint.b, tint.a);

        // Bottom-left corner for texture and quad
        rlTexCoord2f((float)source.x/texture.width, (float)source.y/texture.height);
        rlVertex3f(topLeft.x, topLeft.y, topLeft.z);

        // Top-left corner for texture and quad
        rlTexCoord2f((float)source.x/texture.width, (float)(source.y + source.height)/texture.height);
        rlVertex3f(bottomLeft.x, bottomLeft.y, bottomLeft.z);

        // Top-right corner for texture and quad
        rlTexCoord2f((float)(source.x + source.width)/texture.width, (float)(source.y + source.height)/texture.height);
        rlVertex3f(bottomRight.x, bottomRight.y, bottomRight.z);

        // Bottom-right corner for texture and quad
        rlTexCoord2f((float)(source.x + source.width)/texture.width, (float)source.y/texture.height);
        rlVertex3f(topRight.x, topRight.y, topRight.z);
    rlEnd();

    rlSetTexture(0);
}

With this result (notice it missing the translation)

How can I get the below code to work like the above test case?

Unwanted Result

\$\endgroup\$
2
  • \$\begingroup\$ "In Unity, whenever I rotate an object, it also performs a translation" I understand that you have a test case that leads you to believe this is true, however, it is not the case. Rotating a Unity quad by a quaternion rotates it about its local origin point, so that origin is unchanged before and after (ie. transform.position gives the same value before and after the rotation). You can get a translation if you're rotating the parent of an object, where that child object is placed somewhere other than the parent's origin in the parent coordinate system. \$\endgroup\$
    – DMGregory
    Commented Jan 7, 2023 at 17:23
  • \$\begingroup\$ Anytime you're tempted to believe "Wow, Unity is doing some ridiculous non-sensical thing like adding translation every time I rotate", it helps a great deal to check your assumptions. Here you assumed the object's pivot was its visual center, which led to a lot of unnecessary confusion. (There are non-sensical things engines do, but faulty assumptions are much, MUCH more common in gamedev!) \$\endgroup\$
    – DMGregory
    Commented Jan 7, 2023 at 18:13

1 Answer 1

0
\$\begingroup\$

You are not drawing a built-in Unity Quad (a Mesh drawn with a MeshRenderer, whose pivot is at its center). You are drawing a Sprite with a SpriteRenderer. This terminology is important - you've waited this long for an answer because you used words that describe a completely different problem.

That Sprite has its pivot placed somewhere other than the center of the shape - you can inspect this in the Sprite Editor to verify this.

This is being masked because in you've configured your Scene View to show the hot point in the center of the shape, rather than at the true location of the pivot. (This would have been easier to spot if you hadn't cropped that setting out of your screenshot) We can't see where the pivot of the sprite actually is in these screenshots, so we have to infer it for now. You can change this setting in the top-left of the Scene View tab:

Center / Pivot setting

The pivot determines the object's local origin point - the point whose local coordinates are (0, 0, 0), and whose world coordinates are transform.position. That's the point the object rotates around when you change its Euler angles. Here that origin is at -2.42 on the x, somewhere around the left edge of the visible frame. So when we rotate around that point, it swings left like a door opening on a hinge at the left side of the screen, instead of pirouetting in the middle of the screen.

There's still no translation being applied. The sprite's local origin, its pivot, is staying where it always was.

Your raylib code does not replicate this off-center pivot. You construct your quad symmetrically: topLeft is -1 * bottomRight and similarly for topRight and bottomLeft. So you've placed the pivot, the local origin, in the center of the quad. It's that mismatch that causes the difference in behaviour you're observing.

You can instead construct your quad in Raylib something like this:

    up = Vector3RotateByQuaternion(up, tQuat);
    right = Vector3RotateByQuaternion(right, tQuat);
    
    // No division by 2 - we'll use these vectors at their full width/height size.
    Vector3 rightScaled = Vector3Scale(right, sizeRatio.x);
    Vector3 upScaled = Vector3Scale(up, sizeRatio.y);

    Vector3 topLeft = upScaled;
    Vector3 topRight = Vector3Add(rightScaled, upScaled);
    Vector3 bottomRight = rightScaled;
    Vector3 bottomLeft = {0, 0, 0};

    // Translate points to the pivot point (position)
    topLeft = Vector3Add(topLeft, position);
    topRight = Vector3Add(topRight, position);
    bottomRight = Vector3Add(bottomRight, position);
    bottomLeft = Vector3Add(bottomLeft, position);

Here we've put the local origin - the pivot - on the bottom-left corner. So as you rotate, the left edge won't move, serving as the hinge. You can accomplish intermediate effects by sliding all four points by some multiple of right.

\$\endgroup\$
3
  • \$\begingroup\$ I will attempt to apply this shortly but you're definitely right. I am humbled at the shortcomings of my approach at discussing the problem, beyond words and cannot express how grateful I am for this comprehensive answer. \$\endgroup\$
    – hexley
    Commented Jan 7, 2023 at 18:21
  • \$\begingroup\$ I can't thank you enough @DMGregory. My complex scenes seem to be rendering well. The only example from unity I'm noticing that isn't working in raylib is when X is rotated to 185 degrees. On raylib I see nothing but unity it appears to be reflected over the Y axis close to the bottom of the screen. I guess it's a quirk I'll look at later unless you have any ideas. Thank you so much for your help! \$\endgroup\$
    – hexley
    Commented Jan 7, 2023 at 21:36
  • \$\begingroup\$ If it's rotated more than 180 degrees, then you're looking at the polygons from the back sides. If back face culling is enabled, the GPU will skip rendering those triangles, assuming they're going to be occluded behind the front of the object. Unity disables back face culling for the default sprite material, so you can see 2D content like sprites from both sides. But this is not the usual default for 3D content, so you'll need to set that up in your raylib code manually. \$\endgroup\$
    – DMGregory
    Commented Jan 7, 2023 at 22:34

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .