Home
published on: mar 7, 2024 • updated on: may 18, 2024 • 12 min read

Mengkonversi kuaternion menjadi matriks rotasi

Pada kesempatan kali ini saya akan memperlihatkan bagaimana saya menggunakan kuaternion untuk merotasi objek 3 dimensi, dan sebelumnya jika barangkali teman-teman belum familiar dengan kuaternion dan transformasi matriks, teman-teman bisa menyimak video berikut:

Transformasi matriks

Kuaternion

Nah, jadi disini saya akan mentransformasi objek 3 dimensi dan dengan menggunakan kuaternion yang saya konversi menjadi matriks.

quat1

Sebelumnya saya ingin memperlihatkan potongan kode dari salah satu repository keren yang ada di github:

// Copyright : https://github.com/rwengine/openrw/blob/main/rwengine/src

// Animation slots used for character animation blending
constexpr unsigned int AnimIndexMovement = 0;
constexpr unsigned int AnimIndexAction = 1;

struct CharacterState {
    float health = 100.f;
    float armour = 0.f;
    bool isDying = false;
    bool isDead = false;

    ..............
    ..............
    ..............

};

/**
 * @brief The CharacterObject struct
 * Implements Character object behaviours.
 */
class CharacterObject final : public GameObject {
private:
    CharacterState currentState{};

    VehicleObject* currentVehicle = nullptr;
    size_t currentSeat{0};

    void createActor(const glm::vec2& size = glm::vec2(0.45f, 1.2f));
    void destroyActor();

    glm::vec3 movement{};
    glm::vec2 m_look{0.f, glm::half_pi<float>()};

    bool running = false;
    bool jumped = false;
    AnimationPtr jumpAnimation = nullptr;
    float jumpSpeed = DefaultJumpSpeed;

    bool motionBlockedByActivity = false;

    glm::vec3 updateMovementAnimation(float dt);
    glm::vec3 currenteMovementStep{};

    AnimCycle cycle_ = AnimCycle::Idle;

    ......................
    ......................
    ......................  

};

void CharacterObject::changeCharacterModel(const std::string& name) {
    auto modelName = std::string(name);
    std::transform(modelName.begin(), modelName.end(), modelName.begin(),
                   ::tolower);

    /// @todo don't model leak here

    engine->data->loadTXD(modelName + ".txd");
    auto newmodel = engine->data->loadClump(modelName + ".dff");

    setModel(newmodel);

    animator = std::make_unique<Animator>(getClump());
}

void CharacterObject::updateCharacter(float dt) {
    if (physCharacter) {
        glm::vec3 walkDir = updateMovementAnimation(dt);

        if (canTurn()) {
            float yaw = m_look.x;
            // When strafing we need to detach look direction from movement
            if (!isStrafing()) {
                yaw += std::atan2(movement.z, movement.x);
            }
            rotation = glm::quat(glm::vec3(0.f, 0.f, yaw));
            getClump()->getFrame()->setRotation(glm::mat3_cast(rotation));
        }

        walkDir = rotation * walkDir;

        if (jumped) {
            if (!isOnGround()) {
                // Get some inertia from the previous animation
                walkDir /= 4.f;

                walkDir += rotation * glm::vec3(0.f, jumpSpeed * dt, 0.f);
            }
        }
        
        .................
        .................
        .................
        
        _lastHeight = getPosition().z;
    } else {
        updateMovementAnimation(dt);
    }
}

void Animator::tick(float dt) {
    if (model == nullptr || animations.empty()) {
        return;
    }

    struct BoneTransform {
        glm::vec3 translation{};
        glm::quat rotation{1.0f,0.0f,0.0f,0.0f};
    };

    for (AnimationState& state : animations) {
        
        .......................
        .......................
        .......................

        for (auto& [bonePtr, frame] : state.boneInstances) {
            if (bonePtr->frames.empty()) continue;
            auto kf = bonePtr->getInterpolatedKeyframe(animTime);

            BoneTransform xform;
            xform.rotation = kf.rotation;
            if (bonePtr->type != AnimationBone::R00) {
                xform.translation = kf.position;
            }
            frame->setTranslation(frame->getDefaultTranslation() +
                                     xform.translation);
            frame->setRotation(glm::mat3_cast(xform.rotation));
        }
    }
}

Tahukah teman-teman potongan kode apa di atas? Itu adalah bagian kode dari karakter di game GTA. Mungkin bagi sebagian orang itu sesuatu yang belum tentu menarik, tapi bagi saya itu adalah sesuatu yang sangat keren. Tapi kode di atas adalah versi “re-implementation”, sebenarnya ada versi “leaked” cuman saya yakin akan lebih baik pada tulisan ini jika saya membagikan yang versi “re-implementation” ketimbang yang versi “leaked”.

Pertama kali saya bermain GTA itu di PS2 sewaktu SD, dan pertama kali saya modding di GTA menggunakan komputer sewaktu SMP. Dan dari SD sampai sekarang saya belum pernah menamatkan game GTA, jadi saya cuman jalan-jalan di map dengan berbagai macam MOD yang saya pasang. Dari yang awalnya suka berwisata alam di GTA, kemudian modding, sampai akhirnya di titik saya menyukai kode-kode C++ yang berkaitan dengan game engine. Jadi bisa di bilang ketertarikan saya pada perkodingan berawal dari pecinta alam (alam nya GTA), itulah sebabnya menurut saya potongan kode di atas terlihat keren, karena berasa seperti mengetahui rahasia alam (alam nya GTA).

Nah, kali ini saya akan membuat objek karakter 3 dimensi, tapi yang akan saya perlihatkan lebih simpel dibandingkan karakter utama di game GTA, yaitu lebih ke pengenalan konsep movement dan animation nya saja. Tentunya dari class yang saya buat bisa teman-teman kembangkan lagi di rumah, ini dia class nya:

// Copyright (c) 2024 Wildan R Wijanarko (@wildan9)

// This software is provided "as-is", without any express or implied warranty. In no event 
// will the authors be held liable for any damages arising from the use of this software.

// Permission is granted to anyone to use this software for any purpose, including commercial 
// applications, and to alter it and redistribute it freely, subject to the following restrictions:

//   1. The origin of this software must not be misrepresented; you must not claim that you 
//   wrote the original software. If you use this software in a product, an acknowledgment 
//   in the product documentation would be appreciated but is not required.

//   2. Altered source versions must be plainly marked as such, and must not be misrepresented
//   as being the original software.

//   3. This notice may not be removed or altered from any source distribution.

#pragma once

#include "raylib.h"
#include "rlgl.h"
#include "MathLib.h"

#include <cassert>
#include <string>
#include <unordered_map>
#include <functional>

class Player;

typedef std::function<void(Player&)> AnimStateFunction;

class Player
{
public:
	Player();
	~Player();
	
	inline Vector3 GetPosition() const { return _position; }
	inline Vector3 GetDestination() const { return _destination; }

	void Update(const Camera& camera);
	void Draw() const;

private:
	void Idle(Player&);
	void Walk(Player&);
	void Run(Player&);
	void Backflip(Player&);
	void SetAnimState(const std::string& newState);

	Vector3 _scale{};
	Vector3 _position{};
	Vector3 _destination{};
	Quaternion _rotation{};
	Transform3D _transform{};

	float _movementSpeed = 0.0f;
	float _backflipDuration = 0.0f;

	int _animsCount = 0;
	unsigned int _animIndex = 0;
	unsigned int _animCurrentFrame = 0;
	
	Model _model{};
	std::string _animState{};
	std::unordered_map<std::string, 
	AnimStateFunction> _animStateActions{};
	ModelAnimation* _modelAnimations = nullptr;
};

Kemudian dimana bagian mengkoversi kuaternion nya? Jadi pada class di atas itu kan ada variabel _rotation yang dimana merupakan kuaternion, nah jadi sebelum kuaternion itu kita kalikan dengan matriks lainnya yang digunakan untuk mentransformasi matriks, kuaternion itu harus kita konversi ke dalam bentuk matriks. Udah, inti dari tulisan ini cuman begitu doang sebenernya, dan kode nya sepert ini:

Mengkonversi kuaternion menjadi matriks rotasi

// Copyright : https://github.com/raysan5/raylib/blob/master/src/raymath.h

// Get a matrix for a given quaternion
RMAPI Matrix QuaternionToMatrix(Quaternion q)
{
    Matrix result = { 1.0f, 0.0f, 0.0f, 0.0f,
                      0.0f, 1.0f, 0.0f, 0.0f,
                      0.0f, 0.0f, 1.0f, 0.0f,
                      0.0f, 0.0f, 0.0f, 1.0f }; // MatrixIdentity()

    float a2 = q.x*q.x;
    float b2 = q.y*q.y;
    float c2 = q.z*q.z;
    float ac = q.x*q.z;
    float ab = q.x*q.y;
    float bc = q.y*q.z;
    float ad = q.w*q.x;
    float bd = q.w*q.y;
    float cd = q.w*q.z;

    result.m0 = 1 - 2*(b2 + c2);
    result.m1 = 2*(ab + cd);
    result.m2 = 2*(ac - bd);

    result.m4 = 2*(ab - cd);
    result.m5 = 1 - 2*(a2 + c2);
    result.m6 = 2*(bc + ad);

    result.m8 = 2*(ac + bd);
    result.m9 = 2*(bc - ad);
    result.m10 = 1 - 2*(a2 + b2);

    return result;
}

Transformasi matriks

_transform.position = Vector3{ _position.x, _position.y + 1.0f, _position.z };
_transform.rotation = _rotation;
_transform.scale    = _scale;

.............
.............
.............

inline Matrix Transform3D::ToMatrix() const
{
    return MatrixTranslateV(position) * QuaternionToMatrix(rotation) * MatrixScaleV(scale);
}

.............
.............
.............

void Player::Draw() const
{
    rlPushMatrix();
    rlMultMatrixf(MatrixToFloat(_transform.ToMatrix()));
    DrawModel(_model, Vector3Zero(), 0.02f, WHITE);
    rlPopMatrix();
}

Dan hasil rotasi nya seperti ini:

gif3

Dari konsep dasar movement dan animation yang saya bagikan ini tentunya bisa teman-teman kembangkan lagi di rumah menjadi game utuh, full source code nya bisa teman-teman unduh di:

Contoh game yang di buat menggunakan library yang sama dan barangkali bisa menginspirasi teman-teman:

Referensi:

Post by: Wildan