OpenGL NCKU HW3

作業環境

vscode / glfw version 3.3.6 / cmake version 3.31.4 / GNU Make 4.3 / g++ (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3. / Linux 6.11.0-19-generic #19~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Mon Feb 17 11:51:52 UTC 2 x86_64 x86_64 x86_64 GNU/Linux

方法說明

動作呈現是以 interpolation 與 frame 方式進行。

其 interpolation:

glm::vec3 interpolatedOffset = glm::mix(prevFrameData->offset, FrameData->offset, progress);
glm::vec3 interpolatedRotation = glm::mix(prevFrameData->rotation, FrameData->rotation, progress);
glm::vec3 interpolatedSize = glm::mix(prevFrameData->size, FrameData->size, progress);

而每個 frame 為 hard-code ,並以位移、旋轉、縮放,對該物件進行操作。

transformFrames_[TransformFrame::FRAME1] = 
{
    {"torso", std::make_shared<TransformFrameData>(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(40.0f, 0.0f, 0.0f), glm::vec3(1.0f, 1.5f, 0.5f))},
    {"abdomen", std::make_shared<TransformFrameData>(glm::vec3(0.0f, -0.1f, 0.5f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.5f, 1.0f, 0.4f))},
    {"abdomenRight", std::make_shared<TransformFrameData>(glm::vec3(-0.6f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.5f, 1.0f, 0.4f))},
    {"abdomenLeft", std::make_shared<TransformFrameData>(glm::vec3(0.6f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.5f, 1.0f, 0.4f))},
    {"tail", std::make_shared<TransformFrameData>(glm::vec3(0.0f, -1.5f, -0.3f), glm::vec3(40.0f, 0.0f, 0.0f), glm::vec3(0.1f, 0.5f, 0.1f))},
    {"head", std::make_shared<TransformFrameData>(glm::vec3(0.0f, 2.2f, 0.0f), glm::vec3(-35.0f, 0.0f, 0.0f), glm::vec3(0.75f, 0.75f, 0.75f))},
    {"nose", std::make_shared<TransformFrameData>(glm::vec3(0.0f, -0.3f, 0.75f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.4f, 0.4f, 0.2f))},
    {"leftShoulder", std::make_shared<TransformFrameData>(glm::vec3(-1.2f, 1.0f, 0.0f), glm::vec3(-65.0f, 0.0f, 0.0f), glm::vec3(0.25f, 0.4f, 0.25f))},
    {"leftArm", std::make_shared<TransformFrameData>(glm::vec3(0.0f, -0.8f, 0.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.25f, 0.4f, 0.25f))},
    {"leftHand", std::make_shared<TransformFrameData>(glm::vec3(0.0f, -0.7f, 0.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.3f, 0.3f, 0.3f))},
    {"rightShoulder", std::make_shared<TransformFrameData>(glm::vec3(1.2f, 1.0f, 0.0f), glm::vec3(-65.0f, 0.0f, 0.0f), glm::vec3(0.25f, 0.4f, 0.25f))},
    {"rightArm", std::make_shared<TransformFrameData>(glm::vec3(0.0f, -0.8f, 0.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.25f, 0.4f, 0.25f))},
    {"rightHand", std::make_shared<TransformFrameData>(glm::vec3(0.0f, -0.7f, 0.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.3f, 0.3f, 0.3f))},
    {"leftHip", std::make_shared<TransformFrameData>(glm::vec3(-0.4f, -2.0f, 0.3f), glm::vec3(-30.0f, 0.0f, 0.0f), glm::vec3(0.25f, 0.5f, 0.25f))},
    {"leftLeg", std::make_shared<TransformFrameData>(glm::vec3(0.0f, -1.0f, 0.0f), glm::vec3(15.0f, 0.0f, 0.0f), glm::vec3(0.25f, 0.5f, 0.25f))},
    {"leftFoot", std::make_shared<TransformFrameData>(glm::vec3(0.0f, -0.7f, 0.1f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.3f, 0.2f, 0.5f))},
    {"rightHip", std::make_shared<TransformFrameData>(glm::vec3(0.4f, -1.0f, 0.7f), glm::vec3(-90.f, 0.0f, 0.0f), glm::vec3(0.25f, 0.5f, 0.25f))},
    {"rightLeg", std::make_shared<TransformFrameData>(glm::vec3(0.0f, -1.0f, 0.0f), glm::vec3(15.0f, 0.0f, 0.0f), glm::vec3(0.25f, 0.5f, 0.25f))},
    {"rightFoot", std::make_shared<TransformFrameData>(glm::vec3(0.0f, -0.7f, 0.1f), glm::vec3(25.0f, 0.0f, 0.0f), glm::vec3(0.3f, 0.2f, 0.5f))}
};

骨架為先創建枝幹,接著按照親代關係建立結構。

std::shared_ptr<Joint> Animal::createBoneHierarchy() {
    // Create individual models for each body part with different sizes and textures
    
    // Create torso (root)
    auto torsoModel = createBodyPartModel("torso", glm::vec3(1.0f, 1.5f, 0.5f), texturePath);
    auto torso = std::make_unique<Joint>("torso", glm::vec3(0.0f, 0.0f, 0.0f), torsoModel);
    
    auto abdomenModel = createBodyPartModel("abdomen", glm::vec3(0.5f, 0.5f, 0.2f), texturePath);
    auto abdomen = std::make_unique<Joint>("abdomen", glm::vec3(0.0f, -0.5f, 0.5f), abdomenModel);

    ...
    
    // Build hierarchy (bottom-up)
    leftArm->addChild(std::move(leftHand));
    leftShoulder->addChild(std::move(leftArm));
    
    rightArm->addChild(std::move(rightHand));
    rightShoulder->addChild(std::move(rightArm));
    
    leftLeg->addChild(std::move(leftFoot));
    leftHip->addChild(std::move(leftLeg));
    
    rightLeg->addChild(std::move(rightFoot));
    rightHip->addChild(std::move(rightLeg));
    
    head->addChild(std::move(nose));

    // Add all to torso
    torso->addChild(std::move(abdomen));
    torso->addChild(std::move(abdomenLeft));
    torso->addChild(std::move(abdomenRight));
    torso->addChild(std::move(tail));
    torso->addChild(std::move(head));
    torso->addChild(std::move(leftShoulder));
    torso->addChild(std::move(rightShoulder));
    torso->addChild(std::move(leftHip));
    torso->addChild(std::move(rightHip));
    
    return torso;
}

Bonus

  • Using texture.
  • Camera look at and translation.
  • Free Camera: WASD and mouse control.
  • Model control.
  • Animation control.

使用 texture

利用

glBufferData(static_cast<GLenum>(type_), size, data,
             static_cast<GLenum>(usagePattern_));

glVertexAttribPointer(index, size, type, normalized, stride,
                      reinterpret_cast<void *>(offset));

將 texture Coordinates 的資料給儲存並設定性質,並在渲染時 glActiveTextureglBindTexture

Camera 移動與關注物體

根據文檔設定相機位置與關注目標物體。

GLM_FUNC_DECL mat<4, 4, T, Q> glm::lookAt(vec< 3, T, Q > const &eye,
                                          vec< 3, T, Q > const &center,
                                          vec< 3, T, Q > const &up)	

Free camera WASD移動

bool wasFreeCamera_ = true;
bool isFirstFreeCamera_ = true;
bool isFreeCamera_ = false;
glm::vec3 lookAt_;
glm::vec3 cameraPosition_ = glm::vec3(0.0f, 0.0f, 3.0f);
glm::vec3 cameraFront_ = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 cameraUp_ = glm::vec3(0.0f, 1.0f, 0.0f);
float cameraSpeedFactor_ = 2.5f;

float deltaTime_ = 0.0f; // time between current frame and last frame
float lastFrame_ = 0.0f;

static bool mouseCaptured_;
float mouse_lastX_ = 400, mouse_lastY_ = 300;
float mouse_yaw_ = -90.0f;
float mouse_pitch_ = 0.0f;
float mouse_fov_ = 45.0f;
float sensitivity_ = 1.0f;

紀錄相機座標、速度、視角目標與紀錄時間變化讓移動運算合理可行,使用 glfwGetKey 接受按鍵事件,並針對其控制相機位置

Free camera 滑鼠控制視角

增加滑鼠位置之 callback 函式,使用 mouseCaptured 與按鍵 c 確保滑鼠指標能夠隨時使用與拖離控制視角滑鼠鎖定 GLFW_CURSOR_DISABLED 並確定事件觸發為一組按壓與施放操作,以免在按下後取消與進入控制視角模式不斷切換。

if (glfwGetKey(window_, GLFW_KEY_C) == GLFW_PRESS)
{
    if (!mouseCaptured)
    {
        mouseCaptured = true;

        if (glfwGetInputMode(window_, GLFW_CURSOR) == GLFW_CURSOR_DISABLED)
        {
            glfwSetInputMode(window_, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
            glfwSetCursorPosCallback(window_, nullptr);
            wasFreeCamera_ = true;
        }
        else
        {
            glfwSetInputMode(window_, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
            glfwSetCursorPosCallback(window_, Detail::mouseCallback);
        }
    }
}
else
{
    mouseCaptured = false;
}

firstEnterMouseCallback(window_, mouse_lastX_, mouse_lastY_,
                        isFirstFreeCamera_);

cameraDirectionLoop(window_, mouse_lastX_, mouse_lastY_);
}

firstEnterMouseCallback 確保相機視角不會因為第一次進入自由相機模式或者第一次移動滑鼠而造成相機大幅度改變視角

使用 cameraDirectionLoop 進行滑鼠移動運算。

void OpenGLWindow::cameraDirectionLoop(GLFWwindow *window, double xpos,
                                       double ypos)
{
    float xoffset = xpos - mouse_lastX_;
    float yoffset = mouse_lastY_ - ypos;
    mouse_lastX_ = xpos;
    mouse_lastY_ = ypos;

    xoffset *= sensitivity_;
    yoffset *= sensitivity_;

    mouse_yaw_ += xoffset;
    mouse_pitch_ += yoffset;

    // Add constraints to prevent camera flipping
    if (mouse_pitch_ > 89.0f)
        mouse_pitch_ = 89.0f;
    if (mouse_pitch_ < -89.0f)
        mouse_pitch_ = -89.0f;

    glm::vec3 front;
    front.x = cos(glm::radians(mouse_pitch_)) * cos(glm::radians(mouse_yaw_));
    front.y = sin(glm::radians(mouse_pitch_));
    front.z = cos(glm::radians(mouse_pitch_)) * sin(glm::radians(mouse_yaw_));
    cameraFront_ = glm::normalize(front);

    lookAt_ = cameraPosition_ + cameraFront_;
}

模型控制

能夠自由的更改每個階層模型的位置、大小與旋轉。
位置及旋轉為 local 操作且子代會受到影響,而大小只跟自己有關,調整大小並不會影響其他模型的大小,方便物件移動觀察。

額外建立一個 Joint 物件,並將 scale 的計算分開,以達成大小與其他物件無關。

class Joint 
{
    ...
        
    glm::mat4 getLocalTransform() const;
    void draw(glm::mat4 parentTransform, glm::mat4 &view, glm::mat4 &projection);

    ...
};
void Joint::draw(glm::mat4 parentTransform, glm::mat4 &view, glm::mat4 &projection) {
    glm::mat4 localTransform = getLocalTransform();
    glm::mat4 worldTransformNoScale = parentTransform * localTransform;
    glm::mat4 worldTransform = glm::scale(worldTransformNoScale, size_);

    if (model_) {
        model_->setModelMatrix(worldTransform);
        model_->draw(view, projection);
    }
    
    for (auto& child : children_) {
        child->draw(worldTransformNoScale, view, projection);
    }
}

在 GUI 中利用迭代與遞迴展開骨架

void OpenGLWindow::windowImguiModelSetting() 
{
    ...

    auto animal_root = animal_->getRootJoint();
    _windowImguiModelSetting(animal_root);
}
void OpenGLWindow::_windowImguiModelSetting(std::shared_ptr<Joint> &joint)
{
    ImGui::Text("%s", joint->getName().c_str());
    ImGui::SliderFloat3(("Position##" + joint->getName()).c_str(), glm::value_ptr(joint->getOffset()), -10.0f, 10.0f);
    ImGui::SliderFloat3(("Rotation##" + joint->getName()).c_str(), glm::value_ptr(joint->getRotation()), -180.0f, 180.0f);
    ImGui::SliderFloat3(("Scale##" + joint->getName()).c_str(), joint.get(), &Joint::getSize, &Joint::setSize, 0.1f, 5.0f);
    ImGui::Separator();
     
    for(auto& child : joint->getChildren()) {
        _windowImguiModelSetting(child);
    }
}

動畫控制

由於我們動畫是一幀幀組成,只要將幀數們紀錄在變數中並由 GUI 呈現,即可監控動畫。至於連續循環動畫則是透過餘數運算進行頭對尾的動畫連接。

void Animal::TransformToFrame(TransformFrame frame)
{
    if(currentFrame == frame) {
        return;
    }
    currentFrame = frame;
    transformationProgress_ = 0.0001f;
}

GUI

ImGui::RadioButton("Transform 1", (int *) &animal_->getCurrentFrame(), 0); ImGui::SameLine();
ImGui::RadioButton("Transform 2", (int *) &animal_->getCurrentFrame(), 1); ImGui::SameLine();
ImGui::RadioButton("Transform 3", (int *) &animal_->getCurrentFrame(), 2);
animal_->TransformToFrame(static_cast<Animal::TransformFrame>(animal_->getCurrentFrame()));

更新動畫的算法,當正在運行動畫且該影格運行結束,則播放下一個影格。

if(isAnimating_ && transformationProgress_ >= 1.0f) {
    currentFrame = static_cast<TransformFrame>((currentFrame + 1) % transformFrameSize);
    transformationProgress_ = 0.0001f;
}

程式如何執行

$ cd "out/build/GCC 13.3.0 x86_64-linux-gnu/bin/Debug"
$ ./Homework03 

程式一開始會先建立 Animal 物件,Animal 物件會創建 shader 並連結到一個 shaerprogram ,接著建立 cube 物件們組成人體,cube 物件會設定 texture 相關訊息, VAO 、 VBO 也設置好後,即可透過 Window 將貼好 texture 的 model 顯示出來。

呈現 model 後,若要產生動作,則利用旋轉與縮放達成。