struct UniformBufferObject {
glm::mat4 model;
glm::mat4 view;
glm::mat4 proj;
};
Before we dive into the integration, let’s briefly introduce the key libraries we’ll be using:
GLFW (Graphics Library Framework): A lightweight, multi-platform library for creating windows, contexts, and surfaces, handling input, and events. We use it for window management and input handling. [https://www.glfw.org/]
GLM (OpenGL Mathematics): A mathematics library for graphics programming that provides vector and matrix operations similar to GLSL. We use it for all our 3D math operations. [https://github.com/g-truc/glm]
Now that we have a camera system and understand transformation matrices, let’s integrate them with our Vulkan application. We’ll focus on how to set up uniform buffers for our matrices and update them each frame based on camera movement.
To keep the integration digestible, think of it in five small steps:
Define the UBO layout (model/view/proj) and create per-frame buffers
Create a descriptor set layout and allocate descriptor sets for the UBO
Write descriptor sets and persistently map the buffers for fast updates
Update the UBO each frame from the camera (view/proj) and model transform
Bind the descriptor set and draw using the updated matrices
|
Note
|
See Transformation matrices and Camera implementation for a refresher on matrix math. |
First, we need to define our uniform buffer structure:
struct UniformBufferObject {
glm::mat4 model;
glm::mat4 view;
glm::mat4 proj;
};
Next, we’ll create the uniform buffer and its descriptor set:
|
Note
|
Uniform buffers should be allocated per frame-in-flight (maxConcurrentFrames), not per swapchain image. This matches how you submit work and synchronize frames, avoids unnecessary allocations, and simplifies your logic. |
// Use a fixed number of frames-in-flight, not the number of swapchain images
constexpr uint32_t maxConcurrentFrames = 2; // Adjust to your renderer
struct UniformBufferObject {
glm::mat4 model;
glm::mat4 view;
glm::mat4 proj;
};
// Keep the mapped pointer alongside the buffer for clarity and safety
struct UboBuffer {
vk::raii::Buffer buffer{nullptr};
vk::raii::DeviceMemory memory{nullptr};
void* mapped = nullptr;
};
std::array<UboBuffer, maxConcurrentFrames> uniformBuffers;
void createUniformBuffers() {
vk::DeviceSize bufferSize = sizeof(UniformBufferObject);
// Create the buffer
vk::BufferCreateInfo bufferInfo{
.size = bufferSize,
.usage = vk::BufferUsageFlagBits::eUniformBuffer,
.sharingMode = vk::SharingMode::eExclusive
};
for (size_t i = 0; i < maxConcurrentFrames; i++) {
uniformBuffers[i].buffer = vk::raii::Buffer(device, bufferInfo);
// Allocate and bind memory
vk::MemoryRequirements memRequirements = uniformBuffers[i].buffer.getMemoryRequirements();
vk::MemoryAllocateInfo allocInfo{
.allocationSize = memRequirements.size,
.memoryTypeIndex = findMemoryType(
memRequirements.memoryTypeBits,
vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent
)
};
uniformBuffers[i].memory = vk::raii::DeviceMemory(device, allocInfo);
uniformBuffers[i].buffer.bindMemory(*uniformBuffers[i].memory, 0);
// Persistently map the buffer memory
uniformBuffers[i].mapped = uniformBuffers[i].memory.mapMemory(0, bufferSize);
}
}
We need to create a descriptor set layout that describes our uniform buffer:
void createDescriptorSetLayout() {
vk::DescriptorSetLayoutBinding uboLayoutBinding{
.binding = 0,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.descriptorCount = 1,
.stageFlags = vk::ShaderStageFlagBits::eVertex,
.pImmutableSamplers = nullptr
};
vk::DescriptorSetLayoutCreateInfo layoutInfo{
.bindingCount = 1,
.pBindings = &uboLayoutBinding
};
descriptorSetLayout = device.createDescriptorSetLayout(layoutInfo);
}
Now we’ll create descriptor sets that point to our uniform buffers:
void createDescriptorSets() {
std::array<vk::DescriptorSetLayout, maxConcurrentFrames> layouts{};
layouts.fill(*descriptorSetLayout);
vk::DescriptorSetAllocateInfo allocInfo{
.descriptorPool = *descriptorPool,
.descriptorSetCount = maxConcurrentFrames,
.pSetLayouts = layouts.data()
};
descriptorSets = device.allocateDescriptorSets(allocInfo);
vk::DescriptorBufferInfo bufferInfo{
.offset = 0,
.range = sizeof(UniformBufferObject)
};
for (size_t i = 0; i < maxConcurrentFrames; i++) {
bufferInfo.buffer = *uniformBuffers[i].buffer;
vk::WriteDescriptorSet descriptorWrite{
.dstSet = descriptorSets[i],
.dstBinding = 0,
.dstArrayElement = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.pBufferInfo = &bufferInfo
};
device.updateDescriptorSets(1, &descriptorWrite, 0, nullptr);
}
}
In our main loop, we’ll update the uniform buffer with the latest camera data:
void updateUniformBuffer(uint32_t currentFrame) {
static auto startTime = std::chrono::high_resolution_clock::now();
auto currentTime = std::chrono::high_resolution_clock::now();
float time = std::chrono::duration<float, std::chrono::seconds::period>(currentTime - startTime).count();
UniformBufferObject ubo{};
// Model matrix: rotate the model around the Y axis
ubo.model = glm::rotate(glm::mat4(1.0f), time * glm::radians(45.0f), glm::vec3(0.0f, 1.0f, 0.0f));
// View matrix: get from our camera
ubo.view = camera.getViewMatrix();
// Projection matrix: get from our camera
ubo.proj = camera.getProjectionMatrix(swapChainExtent.width / (float)swapChainExtent.height);
// Vulkan's Y coordinate is inverted compared to OpenGL
ubo.proj[1][1] *= -1;
// Copy the data to the uniform buffer for the current frame-in-flight
memcpy(uniformBuffers[currentFrame].mapped, &ubo, sizeof(ubo));
}
We need to handle user input to control the camera:
void processInput() {
// Calculate delta time
static float lastFrame = 0.0f;
float currentFrame = glfwGetTime();
float deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
// Process keyboard input for camera movement
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
camera.processKeyboard(CameraMovement::FORWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
camera.processKeyboard(CameraMovement::BACKWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
camera.processKeyboard(CameraMovement::LEFT, deltaTime);
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
camera.processKeyboard(CameraMovement::RIGHT, deltaTime);
if (glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS)
camera.processKeyboard(CameraMovement::UP, deltaTime);
if (glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS)
camera.processKeyboard(CameraMovement::DOWN, deltaTime);
}
We’ll also need to handle mouse movement for camera rotation:
// Global variables for mouse handling
float lastX = 0.0f, lastY = 0.0f;
bool firstMouse = true;
void mouseCallback(GLFWwindow* window, double xpos, double ypos) {
if (firstMouse) {
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // Reversed: y ranges bottom to top
lastX = xpos;
lastY = ypos;
camera.processMouseMovement(xoffset, yoffset);
}
void scrollCallback(GLFWwindow* window, double xoffset, double yoffset) {
camera.processMouseScroll(yoffset);
}
In our initialization code, we need to set up the input callbacks:
void initWindow() {
// ... existing GLFW initialization code ...
// Set up input callbacks
glfwSetCursorPosCallback(window, mouseCallback);
glfwSetScrollCallback(window, scrollCallback);
// Capture the cursor for camera control
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
}
Finally, we integrate everything in our main loop:
void mainLoop() {
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
processInput();
// Update uniform buffer with latest camera data
updateUniformBuffer(currentFrame);
// Draw frame
drawFrame();
}
}
With these components in place, we now have a fully functional camera system integrated with our Vulkan application. Users can navigate the 3D scene using keyboard and mouse controls, and the view will update accordingly.
In the next section, we’ll wrap up with a conclusion and discuss potential improvements to our camera system.
Vulkan 是 Khronos Group Inc. 的注册商标
教程内容版权归原作者,遵循 CC BY-SA 4.0;本站独立代码及设计除外。