// ============================================= // 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 "aster/util/files.h" #include "aster/systems/commit_manager.h" #include "aster/systems/device.h" #include "asset_loader.h" #include "light_manager.h" #include "gui.h" #include "ibl_helpers.h" #include #include 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/"}, }}; 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(swapchainSize.m_Width) / static_cast(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(internalResolution.height), .width = static_cast(internalResolution.width), .height = -static_cast(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, }; eastl::fixed_vector, MAX_FRAMES_IN_FLIGHT> depthImages; eastl::fixed_vector, 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(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(extent.width) / static_cast(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(cameraController.m_AspectRatio * static_cast(inputResolution.height)); if (gui::Button("Change Resolution")) { if (inputResolution.width != internalResolution.width || inputResolution.height != internalResolution.height) { internalResolution = inputResolution; viewport.width = static_cast(internalResolution.width); viewport.height = -static_cast(internalResolution.height); viewport.y = static_cast(internalResolution.height); scissor.extent = internalResolution; } } } else { if (swapchainSize.m_Width != internalResolution.width || swapchainSize.m_Height != internalResolution.height) { internalResolution = swapchainSize; viewport.width = static_cast(internalResolution.width); viewport.height = -static_cast(internalResolution.height); viewport.y = static_cast(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(&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(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 ¤tFrame = device.GetNextFrame(); auto context = currentFrame.CreateGraphicsContext(); auto ¤tDepthImage = depthImages[currentFrame.m_FrameIdx]; auto ¤tAttachment = 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(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(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(swapchainSize.m_Width), static_cast(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; }