project-aster/samples/03_model_render/model_render.cpp

648 lines
24 KiB
C++

// =============================================
// Aster: model_render.cpp
// Copyright (c) 2020-2025 Anish Bhobe
// =============================================
#include "aster/aster.h"
#include "aster/core/buffer.h"
#include "aster/core/constants.h"
#include "aster/core/device.h"
#include "aster/core/image.h"
#include "aster/core/instance.h"
#include "aster/core/physical_device.h"
#include "aster/core/pipeline.h"
#include "aster/core/swapchain.h"
#include "aster/core/window.h"
#include "asset_loader.h"
#include "frame.h"
#include "helpers.h"
#include "light_manager.h"
#include "aster/util/files.h"
#include "gui.h"
#include "ibl_helpers.h"
#include "pipeline_utils.h"
#include <EASTL/array.h>
#include <aster/systems/commit_manager.h>
#include <aster/systems/device.h>
#include <tiny_gltf.h>
#include <filesystem>
constexpr auto PIPELINE_CACHE_FILE = "PipelineCacheData.bin";
constexpr auto MODEL_FILE = "model/DamagedHelmet.glb";
constexpr auto BACKDROP_FILE = "image/photo_studio_loft_hall_4k.hdr";
constexpr auto MODEL_SHADER_FILE = "model";
constexpr auto BACKGROUND_SHADER_FILE = "background";
constexpr u32 INIT_WIDTH = 1280;
constexpr u32 INIT_HEIGHT = 720;
struct Camera
{
mat4 m_View;
mat4 m_Perspective;
mat4 m_InverseView;
mat4 m_InversePerspective;
vec3 m_Position;
f32 m_PositionHomogenousPad_ = 1.0f;
void
CalculateInverses()
{
m_InverseView = inverse(m_View);
m_InversePerspective = inverse(m_Perspective);
}
};
struct CameraController
{
constexpr static auto UP = vec3(0.0f, 1.0f, 0.0f);
f32 m_Fov;
f32 m_Pitch;
f32 m_Yaw;
f32 m_AspectRatio;
Camera m_Camera;
CameraController(const vec3 &position, const vec3 &target, const f32 vFov, const f32 aspectRatio)
: m_Fov(vFov)
, m_Pitch{0.0f}
, m_Yaw{0.0f}
, m_AspectRatio{aspectRatio}
, m_Camera{
.m_View = lookAt(position, target, UP),
.m_Perspective = glm::perspective(vFov, aspectRatio, 0.1f, 100.0f),
.m_Position = position,
}
{
const vec3 dir = normalize(target - vec3(position));
m_Pitch = asin(dir.y);
m_Yaw = acos(-dir.z / sqrt(1.0f - dir.y * dir.y));
m_Camera.CalculateInverses();
}
void
SetAspectRatio(const f32 aspectRatio)
{
m_AspectRatio = aspectRatio;
m_Camera.m_Perspective = glm::perspective(m_Fov, aspectRatio, 0.1f, 100.0f);
m_Camera.CalculateInverses();
}
void
SetPosition(const vec3 &position)
{
m_Camera.m_Position = vec4(position, 1.0f);
f32 cosPitch = cos(m_Pitch);
const auto target = vec3(sin(m_Yaw) * cosPitch, sin(m_Pitch), -cos(m_Yaw) * cosPitch);
m_Camera.m_View = lookAt(position, position + target, UP);
m_Camera.CalculateInverses();
}
void
SetPitchYaw(f32 pitch, f32 yaw)
{
m_Pitch = pitch;
m_Yaw = yaw;
f32 cosPitch = cos(m_Pitch);
const auto target = vec3(sin(m_Yaw) * cosPitch, sin(m_Pitch), -cos(m_Yaw) * cosPitch);
const vec3 position = m_Camera.m_Position;
m_Camera.m_View = lookAt(position, position + target, UP);
m_Camera.CalculateInverses();
}
void
SetLookAt(const vec3 &target)
{
const vec3 dir = normalize(target - m_Camera.m_Position);
m_Pitch = acos(dir.y);
m_Yaw = acos(dir.z / sqrt(1.0f - dir.y * dir.y));
m_Camera.m_View = lookAt(m_Camera.m_Position, m_Camera.m_Position + target, UP);
m_Camera.CalculateInverses();
}
};
int
main(int, char **)
{
MIN_LOG_LEVEL(Logger::LogType::eInfo);
Window window = {"ModelRender (Aster)", {INIT_WIDTH, INIT_HEIGHT}};
Features enabledDeviceFeatures = {
.m_Vulkan10Features = {.samplerAnisotropy = true, .shaderInt16 = true},
.m_Vulkan12Features =
{
.descriptorIndexing = true,
.shaderSampledImageArrayNonUniformIndexing = true,
.shaderStorageBufferArrayNonUniformIndexing = true,
.shaderStorageImageArrayNonUniformIndexing = true,
.descriptorBindingUniformBufferUpdateAfterBind = true, // Not related to Bindless
.descriptorBindingSampledImageUpdateAfterBind = true,
.descriptorBindingStorageImageUpdateAfterBind = true,
.descriptorBindingStorageBufferUpdateAfterBind = true,
.descriptorBindingPartiallyBound = true,
.runtimeDescriptorArray = true,
.timelineSemaphore = true,
.bufferDeviceAddress = true,
.bufferDeviceAddressCaptureReplay = true,
},
.m_Vulkan13Features =
{
.synchronization2 = true,
.dynamicRendering = true,
},
};
auto pipelineCacheData = ReadFileBytes(PIPELINE_CACHE_FILE, false);
systems::Device device{{
.m_Window = window,
.m_Features = enabledDeviceFeatures,
.m_AppName = "ModelRender",
.m_AppVersion = VERSION,
.m_PipelineCacheData = pipelineCacheData,
.m_ShaderSearchPaths = {"shader/slang/"},
}};
AssetLoader assetLoader{device};
LightManager lightManager{device};
Model model = assetLoader.LoadModelToGpu(MODEL_FILE);
auto environmentHdri = assetLoader.LoadHdrImage(BACKDROP_FILE);
auto envHdriHandle = device.m_CommitManager->CommitTexture(environmentHdri);
auto environment = CreateCubeFromHdrEnv(assetLoader, 512, envHdriHandle);
auto attachmentFormat = device.m_Swapchain.m_Format;
Pipeline pipeline;
if (auto result = device.CreatePipeline(pipeline, {
.m_Shaders = {{
.m_ShaderFile = MODEL_SHADER_FILE,
.m_EntryPoints = {"vsmain", "fsmain"},
}},
.m_Name = "Primary",
}))
{
ERROR("Could not create model pipeline. Cause: {}", result.What()) THEN_ABORT(result.Value());
}
Pipeline backgroundPipeline;
if (auto result = device.CreatePipeline(
backgroundPipeline, {
.m_Shaders = {{
.m_ShaderFile = BACKGROUND_SHADER_FILE,
.m_EntryPoints = {"vsmain", "fsmain"},
}},
.m_DepthTest = systems::GraphicsPipelineCreateInfo::DepthTest::eReadOnly,
.m_DepthOp = systems::GraphicsPipelineCreateInfo::CompareOp::eLessThanOrEqualTo,
.m_Name = "Background",
}))
{
ERROR("Could not create background pipeline. Cause: {}", result.What()) THEN_ABORT(result.Value());
}
lightManager.AddPoint(vec3{-5.0f, -5.0f, 5.0f}, vec3{1.0f}, 30.0f, 16.0f);
lightManager.AddPoint(vec3{5.0f, -5.0f, 5.0f}, vec3{1.0f}, 30.0f, 16.0f);
lightManager.AddPoint(vec3{-5.0f, 5.0f, 5.0f}, vec3{1.0f}, 30.0f, 16.0f);
lightManager.AddPoint(vec3{5.0f, 5.0f, 5.0f}, vec3{1.0f}, 30.0f, 16.0f);
lightManager.Update();
vk::Extent2D internalResolution = {1920, 1080};
auto swapchainSize = device.GetSwapchainSize();
CameraController cameraController = {vec3{0.0f, 0.0f, 2.0f}, vec3{0.0f}, 70_deg,
static_cast<f32>(swapchainSize.m_Width) /
static_cast<f32>(swapchainSize.m_Height)};
auto cameraBuffer = device.CreateStorageBuffer(sizeof cameraController.m_Camera, "Camera Info");
auto cameraBufferId = device.m_CommitManager->CommitBuffer(cameraBuffer);
auto lightManagerBuffer =
device.CreateStorageBuffer(sizeof environment + sizeof lightManager.m_MetaInfo, "Light Info");
auto lightsBufferId = device.m_CommitManager->CommitBuffer(lightManagerBuffer);
lightManagerBuffer->Write(0, sizeof environment, &environment);
lightManagerBuffer->Write(sizeof environment, sizeof lightManager.m_MetaInfo, &lightManager.m_MetaInfo);
// Persistent variables
vk::Viewport viewport = {
.x = 0,
.y = static_cast<f32>(internalResolution.height),
.width = static_cast<f32>(internalResolution.width),
.height = -static_cast<f32>(internalResolution.height),
.minDepth = 0.0,
.maxDepth = 1.0,
};
vk::Rect2D scissor = {
.offset = {0, 0},
.extent = internalResolution,
};
vk::ImageSubresourceRange subresourceRange = {
.aspectMask = vk::ImageAspectFlagBits::eColor,
.baseMipLevel = 0,
.levelCount = 1,
.baseArrayLayer = 0,
.layerCount = 1,
};
vk::ImageMemoryBarrier2 attachmentPreRenderBarrier = {
.srcStageMask = vk::PipelineStageFlagBits2::eTransfer,
.srcAccessMask = vk::AccessFlagBits2::eTransferRead,
.dstStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput,
.dstAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite,
.oldLayout = vk::ImageLayout::eUndefined,
.newLayout = vk::ImageLayout::eColorAttachmentOptimal,
.srcQueueFamilyIndex = vk::QueueFamilyIgnored,
.dstQueueFamilyIndex = vk::QueueFamilyIgnored,
.subresourceRange = subresourceRange,
};
vk::ImageMemoryBarrier2 attachmentRenderToBlitBarrier = {
.srcStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput,
.srcAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite,
.dstStageMask = vk::PipelineStageFlagBits2::eBlit,
.dstAccessMask = vk::AccessFlagBits2::eTransferRead,
.oldLayout = vk::ImageLayout::eColorAttachmentOptimal,
.newLayout = vk::ImageLayout::eTransferSrcOptimal,
.srcQueueFamilyIndex = vk::QueueFamilyIgnored,
.dstQueueFamilyIndex = vk::QueueFamilyIgnored,
.subresourceRange = subresourceRange,
};
// Dependency from Acquire to Blit
vk::ImageMemoryBarrier2 swapchainPreBlitBarrier = {
.srcStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput,
.srcAccessMask = vk::AccessFlagBits2::eNone,
.dstStageMask = vk::PipelineStageFlagBits2::eBlit,
.dstAccessMask = vk::AccessFlagBits2::eTransferRead | vk::AccessFlagBits2::eTransferWrite,
.oldLayout = vk::ImageLayout::eUndefined,
.newLayout = vk::ImageLayout::eTransferDstOptimal,
.srcQueueFamilyIndex = vk::QueueFamilyIgnored,
.dstQueueFamilyIndex = vk::QueueFamilyIgnored,
.subresourceRange = subresourceRange,
};
// Execution dependency between blit and GUI render.
vk::ImageMemoryBarrier2 swapchainBlitToGuiBarrier = {
.srcStageMask = vk::PipelineStageFlagBits2::eTransfer,
.srcAccessMask = vk::AccessFlagBits2::eTransferWrite,
.dstStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput,
.dstAccessMask = vk::AccessFlagBits2::eColorAttachmentRead,
.oldLayout = vk::ImageLayout::eTransferDstOptimal,
.newLayout = vk::ImageLayout::eColorAttachmentOptimal,
.srcQueueFamilyIndex = vk::QueueFamilyIgnored,
.dstQueueFamilyIndex = vk::QueueFamilyIgnored,
.subresourceRange = subresourceRange,
};
vk::ImageMemoryBarrier2 swapchainPrePresentBarrier = {
.srcStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput,
.srcAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite,
.dstStageMask = vk::PipelineStageFlagBits2::eBottomOfPipe,
.dstAccessMask = vk::AccessFlagBits2::eNone,
.oldLayout = vk::ImageLayout::eColorAttachmentOptimal,
.newLayout = vk::ImageLayout::ePresentSrcKHR,
.srcQueueFamilyIndex = vk::QueueFamilyIgnored,
.dstQueueFamilyIndex = vk::QueueFamilyIgnored,
.subresourceRange = subresourceRange,
};
auto primeBarriers = [](const eastl::span<vk::ImageMemoryBarrier2> &barriers, const vk::Image image) {
for (auto &bar : barriers)
{
bar.image = image;
}
};
eastl::fixed_vector<Ref<ImageView>, MAX_FRAMES_IN_FLIGHT> depthImages;
eastl::fixed_vector<Ref<ImageView>, MAX_FRAMES_IN_FLIGHT> attachmentImages;
for (u32 index = 0; index < MAX_FRAMES_IN_FLIGHT; ++index)
{
auto name = fmt::format("Depth Frame{}", index);
depthImages.emplace_back(device.CreateDepthStencilImageWithView({
.m_Extent = internalResolution,
.m_Name = name.c_str(),
}));
name = fmt::format("Attachment0 Frame{}", index);
attachmentImages.emplace_back(device.CreateAttachmentWithView({
.m_Format = attachmentFormat,
.m_Extent = internalResolution,
.m_Name = name.c_str(),
}));
}
gui::Init(device, window);
bool rotating = false;
bool lockToScreen = true;
bool showDiffuse = false;
bool useDiffuse = true;
bool showPrefilter = false;
bool useSpecular = true;
constexpr static u32 USE_DIFFUSE_BIT = 1;
constexpr static u32 USE_SPECULAR_BIT = 1 << 1;
constexpr static u32 SHOW_DIFFUSE_BIT = 1 << 2;
constexpr static u32 SHOW_PREFILTER_BIT = 1 << 3;
i32 height = static_cast<i32>(internalResolution.height);
f32 camPitch = glm::degrees(cameraController.m_Pitch);
f32 camYaw = glm::degrees(cameraController.m_Yaw);
vec3 camPosition = cameraController.m_Camera.m_Position;
vk::Extent2D inputResolution = internalResolution;
device.RegisterResizeCallback([&cameraController](vk::Extent2D extent) {
cameraController.SetAspectRatio(static_cast<f32>(extent.width) / static_cast<f32>(extent.height));
});
Time::Init();
INFO("Starting loop");
while (window.Poll())
{
Time::Update();
gui::StartBuild();
gui::Begin("Settings");
gui::Text("Window Resolution: %ux%u", swapchainSize.m_Width, swapchainSize.m_Height);
gui::Text("Render Resolution: %ux%u", internalResolution.width, internalResolution.height);
gui::Checkbox("Lock Resolution to Window", &lockToScreen);
if (!lockToScreen)
{
if (gui::InputInt("FrameBuffer Height", &height, 1, 10))
{
height = eastl::clamp(height, 64, 4320);
}
inputResolution.height = height;
inputResolution.width =
static_cast<i32>(cameraController.m_AspectRatio * static_cast<f32>(inputResolution.height));
if (gui::Button("Change Resolution"))
{
if (inputResolution.width != internalResolution.width ||
inputResolution.height != internalResolution.height)
{
internalResolution = inputResolution;
viewport.width = static_cast<f32>(internalResolution.width);
viewport.height = -static_cast<f32>(internalResolution.height);
viewport.y = static_cast<f32>(internalResolution.height);
scissor.extent = internalResolution;
}
}
}
else
{
if (swapchainSize.m_Width != internalResolution.width ||
swapchainSize.m_Height != internalResolution.height)
{
internalResolution = swapchainSize;
viewport.width = static_cast<f32>(internalResolution.width);
viewport.height = -static_cast<f32>(internalResolution.height);
viewport.y = static_cast<f32>(internalResolution.height);
scissor.extent = internalResolution;
}
}
gui::Separator();
gui::Text("Delta: %0.6f ms", 1000.0f * Time::m_Delta);
gui::Text("FPS: %0.6f", 1.0f / Time::m_Delta);
gui::Separator();
gui::PushItemWidth(100);
bool yawChange = gui::DragFloat("Camera Yaw", &camYaw);
bool pitchChange = gui::DragFloat("Camera Pitch", &camPitch, 1, -89.0f, 89.0f);
if (yawChange || pitchChange)
{
camYaw = camYaw - floor((camYaw + 180.0f) / 360.0f) * 360.0f;
cameraController.SetPitchYaw(glm::radians(camPitch), glm::radians(camYaw));
}
if (gui::InputFloat3("Camera Position", reinterpret_cast<f32 *>(&camPosition)))
{
cameraController.SetPosition(camPosition);
}
gui::Separator();
gui::Text("IBL");
gui::Checkbox("Show DiffIrr", &showDiffuse);
gui::Checkbox("Use DiffIrr", &useDiffuse);
gui::Checkbox("Show Prefilter", &showPrefilter);
gui::Checkbox("Use Specular", &useSpecular);
gui::Separator();
gui::Checkbox("Rotate", &rotating);
gui::PopItemWidth();
if (gui::Button("Exit"))
{
window.RequestExit();
}
gui::End();
gui::EndBuild();
if (rotating)
{
model.SetModelTransform(
rotate(model.GetModelTransform(), static_cast<f32>(45.0_deg * Time::m_Delta), vec3(0.0f, 1.0f, 0.0f)));
}
model.Update();
cameraController.m_Camera.CalculateInverses();
cameraBuffer->Write(0, sizeof cameraController.m_Camera, &cameraController.m_Camera);
auto &currentFrame = device.GetNextFrame();
auto context = currentFrame.CreateGraphicsContext();
auto &currentDepthImage = depthImages[currentFrame.m_FrameIdx];
auto &currentAttachment = attachmentImages[currentFrame.m_FrameIdx];
if (currentAttachment->m_Extent.width != internalResolution.width ||
currentAttachment->m_Extent.height != internalResolution.height)
{
auto name = fmt::format("Depth Frame{}", currentFrame.m_FrameIdx);
currentDepthImage = device.CreateDepthStencilImageWithView({
.m_Extent = internalResolution,
.m_Name = name.c_str(),
});
name = fmt::format("Attachment0 Frame{}", currentFrame.m_FrameIdx);
currentAttachment = device.CreateAttachmentWithView({
.m_Format = attachmentFormat,
.m_Extent = internalResolution,
.m_Name = name.c_str(),
});
}
vk::Image currentSwapchainImage = currentFrame.m_SwapchainImage;
vk::ImageView currentDepthImageView = currentDepthImage->m_View;
vk::Image currentImage = currentAttachment->m_Image->m_Image;
vk::ImageView currentImageView = currentAttachment->m_View;
attachmentPreRenderBarrier.image = currentImage;
attachmentRenderToBlitBarrier.image = currentImage;
swapchainPreBlitBarrier.image = currentSwapchainImage;
swapchainBlitToGuiBarrier.image = currentSwapchainImage;
swapchainPrePresentBarrier.image = currentSwapchainImage;
context.Begin();
eastl::array firstBarriers = {attachmentPreRenderBarrier, swapchainPreBlitBarrier};
context.Dependency({
.imageMemoryBarrierCount = static_cast<u32>(firstBarriers.size()),
.pImageMemoryBarriers = firstBarriers.data(),
});
// Render
eastl::array attachmentInfos = {
vk::RenderingAttachmentInfo{
.imageView = currentImageView,
.imageLayout = vk::ImageLayout::eColorAttachmentOptimal,
.resolveMode = vk::ResolveModeFlagBits::eNone,
.loadOp = vk::AttachmentLoadOp::eClear,
.storeOp = vk::AttachmentStoreOp::eStore,
.clearValue = vk::ClearColorValue{0.0f, 0.0f, 0.0f, 1.0f},
},
};
vk::RenderingAttachmentInfo depthAttachment = {
.imageView = currentDepthImageView,
.imageLayout = vk::ImageLayout::eDepthAttachmentOptimal,
.resolveMode = vk::ResolveModeFlagBits::eNone,
.loadOp = vk::AttachmentLoadOp::eClear,
.storeOp = vk::AttachmentStoreOp::eDontCare,
.clearValue = vk::ClearDepthStencilValue{.depth = 1.0f, .stencil = 0},
};
vk::RenderingInfo renderingInfo = {
.renderArea = {.extent = ToExtent2D(currentAttachment->m_Extent)},
.layerCount = 1,
.colorAttachmentCount = static_cast<u32>(attachmentInfos.size()),
.pColorAttachments = attachmentInfos.data(),
.pDepthAttachment = &depthAttachment,
};
context.BeginRendering(renderingInfo);
context.SetViewport(viewport);
context.BindIndexBuffer(model.m_IndexBuffer);
context.BindPipeline(pipeline);
u32 flags = 0;
if (useSpecular)
{
flags |= USE_SPECULAR_BIT;
}
if (useDiffuse)
{
flags |= USE_DIFFUSE_BIT;
}
if (showPrefilter)
{
flags |= SHOW_PREFILTER_BIT;
}
if (showDiffuse)
{
flags |= SHOW_DIFFUSE_BIT;
}
u32 pcbOffset = 0;
context.PushConstantBlock(model.m_Handles);
pcbOffset += sizeof model.m_Handles;
context.PushConstantBlock(pcbOffset, cameraBufferId);
pcbOffset += sizeof cameraBufferId;
context.PushConstantBlock(pcbOffset, lightsBufferId);
pcbOffset += sizeof lightsBufferId;
context.PushConstantBlock(pcbOffset, flags);
pcbOffset += sizeof flags;
for (auto &prim : model.m_MeshPrimitives)
{
u32 innerPcbOffset = pcbOffset;
context.PushConstantBlock(innerPcbOffset, prim.m_MaterialIdx);
innerPcbOffset += sizeof prim.m_MaterialIdx;
context.PushConstantBlock(innerPcbOffset, prim.m_TransformIdx);
innerPcbOffset += sizeof prim.m_TransformIdx;
context.DrawIndexed(prim.m_IndexCount, prim.m_FirstIndex, prim.m_VertexOffset);
}
context.BindPipeline(backgroundPipeline);
context.Draw(3);
context.EndRendering();
context.Dependency({.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &attachmentRenderToBlitBarrier});
vk::ImageBlit2 blitRegion = {
.srcSubresource =
{
.aspectMask = vk::ImageAspectFlagBits::eColor,
.mipLevel = 0,
.baseArrayLayer = 0,
.layerCount = 1,
},
.srcOffsets =
std::array{
vk::Offset3D{0, 0, 0},
ToOffset3D(currentAttachment->m_Extent),
},
.dstSubresource =
{
.aspectMask = vk::ImageAspectFlagBits::eColor,
.mipLevel = 0,
.baseArrayLayer = 0,
.layerCount = 1,
},
.dstOffsets =
std::array{
vk::Offset3D{0, 0, 0},
vk::Offset3D{static_cast<i32>(swapchainSize.m_Width), static_cast<i32>(swapchainSize.m_Height), 1},
},
};
vk::BlitImageInfo2 blit = {
.srcImage = currentImage,
.srcImageLayout = vk::ImageLayout::eTransferSrcOptimal,
.dstImage = currentSwapchainImage,
.dstImageLayout = vk::ImageLayout::eTransferDstOptimal,
.regionCount = 1,
.pRegions = &blitRegion,
.filter = vk::Filter::eLinear,
};
context.Blit(blit);
context.Dependency({.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &swapchainBlitToGuiBarrier});
gui::Draw(currentFrame, context);
context.Dependency({.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &swapchainPrePresentBarrier});
context.End();
device.Present(currentFrame, context);
}
device.WaitIdle();
pipelineCacheData = device.DumpPipelineCache();
ERROR_IF(!WriteFileBytes(PIPELINE_CACHE_FILE, pipelineCacheData), "Pipeline Cache incorrectly written");
gui::Destroy(device);
return 0;
}