How can I calculate the translation used for rotating an object with a quaternion in Unity, to use for OpenGL?

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)

[

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);

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:

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 };

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 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)

rlCheckRenderBatchLimit(4);

rlSetTexture(texture.id);

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?

• "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. Jan 7 at 17:23
• 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!) Jan 7 at 18:13

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:

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.

    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 bottomRight = rightScaled;
Vector3 bottomLeft = {0, 0, 0};

// Translate points to the pivot point (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.