aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md28
-rw-r--r--src/core/application.cpp4
-rw-r--r--src/platform/vulkan/vulkan_renderer.cpp266
3 files changed, 256 insertions, 42 deletions
diff --git a/README.md b/README.md
index 4a1cd9d..db4b448 100644
--- a/README.md
+++ b/README.md
@@ -1,36 +1,14 @@
-![Logo](branding/Logo-Black.jpg)
+![Logo](branding/Logo-Monochrome-White.jpg)
A real-time black hole ray tracer.
-## Table of Contents
-
-1. [Overview](#overview)
-2. [Mathematical Theory](docs/mathematical-theory.md)
- - [Einstein's Field Equations](docs/mathematical-theory.md#einsteins-field-equations)
- - [Schwarzschild Metric](docs/mathematical-theory.md#schwarzschild-metric)
- - [Geodesics: Paths of Free-Falling Particles and Light](docs/mathematical-theory.md#geodesics-paths-of-free-falling-particles-and-light)
- - [Conserved Quantities](docs/mathematical-theory.md#conserved-quantities)
-3. [Ray Tracing Implementation](docs/ray-tracing-implementation.md)
- - [Ray Initialization](docs/ray-tracing-implementation.md#1-ray-initialization)
- - [Geodesic Integration](docs/ray-tracing-implementation.md#2-geodesic-integration)
- - [Intersection Testing](docs/ray-tracing-implementation.md#3-intersection-testing)
- - [Rendering Pipeline](docs/ray-tracing-implementation.md#4-rendering)
-4. [Numerical Methods](docs/numerical-methods.md)
- - [Runge-Kutta 4 Integration](docs/numerical-methods.md#runge-kutta-4-rk4-integration--explained)
- - [Adaptive Step Size](docs/numerical-methods.md#adaptive-step-size)
- - [Performance Optimizations](docs/numerical-methods.md#performance-optimizations)
-5. [Configuration](docs/configuration.md)
- - [Configuration Files](docs/configuration.md#configuration-files)
- - [Simulation Parameters](docs/configuration.md#simulation-parameters)
- - [Performance Settings](docs/configuration.md#performance-settings)
-
## Overview
Donut is a real-time black hole ray tracer that simulates the visual effects of gravitational lensing around Sagittarius A* (Sgr A*), the supermassive black hole at the center of our galaxy. The application renders images of how light would bend and distort as it passes near the black hole's intense gravitational field.
## Screenshots
-![Black Hole with Accretion Disk](Branding/Screenshot1.png)
-![Black hole with HDRI](Branding/Screenshot2.png)
+![Black Hole with Accretion Disk](branding/Screenshot1.png)
+![Black hole with HDRI](branding/Screenshot2.png)
*Real-time rendering of Sagittarius A* with volumetric accretion disk, featuring:*
diff --git a/src/core/application.cpp b/src/core/application.cpp
index 8708417..3b1df16 100644
--- a/src/core/application.cpp
+++ b/src/core/application.cpp
@@ -97,8 +97,8 @@ namespace Donut
if (scene)
{
- ImGui::TextDisabled("World-builder scene (grid). Drag to orbit, scroll to zoom.");
- ImGui::TextDisabled("Lit sphere + skybox coming next.");
+ ImGui::TextDisabled("Lit sphere on the reference grid. Drag to orbit, scroll to zoom.");
+ ImGui::TextDisabled("Skybox coming next.");
}
else
{
diff --git a/src/platform/vulkan/vulkan_renderer.cpp b/src/platform/vulkan/vulkan_renderer.cpp
index cad0a44..3e22ccd 100644
--- a/src/platform/vulkan/vulkan_renderer.cpp
+++ b/src/platform/vulkan/vulkan_renderer.cpp
@@ -21,13 +21,13 @@
namespace Donut
{
- #define VK_CHECK(expr) \
- do { \
- VkResult _r = (expr); \
- if (_r != VK_SUCCESS) { \
- DONUT_ERROR("Vulkan: {} failed ({})", #expr, (int)_r); \
- return false; \
- } \
+ #define VK_CHECK(expr) \
+ do { \
+ VkResult _r = (expr); \
+ if (_r != VK_SUCCESS) { \
+ DONUT_ERROR("Vulkan: {} failed ({})", #expr, (int)_r); \
+ return false; \
+ } \
} while (0)
static constexpr int MAX_FRAMES_IN_FLIGHT = 2;
@@ -135,6 +135,26 @@ namespace Donut
VkPipelineLayout grid_pipeline_layout = VK_NULL_HANDLE;
VkPipeline grid_pipeline = VK_NULL_HANDLE;
+ // Scene color+depth pass into the swapchain (the grid is coplanar, but the
+ // sphere needs real depth). Depth image + framebuffers track the swapchain.
+ VkImage scene_depth_image = VK_NULL_HANDLE; VkDeviceMemory scene_depth_mem = VK_NULL_HANDLE;
+ VkImageView scene_depth_view = VK_NULL_HANDLE;
+ VkRenderPass scene_render_pass = VK_NULL_HANDLE;
+ std::vector<VkFramebuffer> scene_framebuffers;
+
+ // Lit sphere (Sphere.slang): pos+normal indexed mesh; samples the HDRI
+ // cubemap for ambient at binding 1 (shares the geodesic cube).
+ VkBuffer sphere_vb = VK_NULL_HANDLE; VkDeviceMemory sphere_vb_mem = VK_NULL_HANDLE;
+ VkBuffer sphere_ib = VK_NULL_HANDLE; VkDeviceMemory sphere_ib_mem = VK_NULL_HANDLE;
+ uint32_t sphere_index_count = 0;
+ VkBuffer sphere_ubo = VK_NULL_HANDLE; VkDeviceMemory sphere_ubo_mem = VK_NULL_HANDLE;
+ void* sphere_ubo_mapped = nullptr;
+ VkDescriptorSetLayout sphere_set_layout = VK_NULL_HANDLE;
+ VkDescriptorPool sphere_pool = VK_NULL_HANDLE;
+ VkDescriptorSet sphere_set = VK_NULL_HANDLE;
+ VkPipelineLayout sphere_pipeline_layout = VK_NULL_HANDLE;
+ VkPipeline sphere_pipeline = VK_NULL_HANDLE;
+
auto find_memory_type(uint32_t type_filter, VkMemoryPropertyFlags flags) const -> uint32_t;
auto create_buffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags props, VkBuffer& buf, VkDeviceMemory& mem) const -> bool;
static auto load_spirv(const std::string& path) -> std::vector<uint32_t>;
@@ -153,6 +173,8 @@ namespace Donut
auto rebuild_hdri_cubemap(const char* path) -> void;
auto create_present_resources() -> bool;
auto create_scene_resources() -> bool;
+ auto create_scene_targets() -> bool;
+ auto destroy_scene_targets() -> void;
auto process_input() -> void;
auto update_geodesic_uniforms() -> void;
auto update_scene_uniforms() -> void;
@@ -400,6 +422,74 @@ namespace Donut
return true;
}
+ // Depth image + a color+depth render pass + per-image framebuffers for the
+ // scene view. Swapchain-sized, so recreated alongside the swapchain.
+ auto VulkanRenderer::Impl::create_scene_targets() -> bool
+ {
+ const VkFormat depth_fmt = VK_FORMAT_D32_SFLOAT;
+
+ VkImageCreateInfo dici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
+ dici.imageType = VK_IMAGE_TYPE_2D; dici.format = depth_fmt;
+ dici.extent = { swapchain_extent.width, swapchain_extent.height, 1 };
+ dici.mipLevels = 1; dici.arrayLayers = 1; dici.samples = VK_SAMPLE_COUNT_1_BIT;
+ dici.tiling = VK_IMAGE_TILING_OPTIMAL; dici.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
+ VK_CHECK(vkCreateImage(device, &dici, nullptr, &scene_depth_image));
+ VkMemoryRequirements dreq{}; vkGetImageMemoryRequirements(device, scene_depth_image, &dreq);
+ VkMemoryAllocateInfo dai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
+ dai.allocationSize = dreq.size; dai.memoryTypeIndex = find_memory_type(dreq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
+ VK_CHECK(vkAllocateMemory(device, &dai, nullptr, &scene_depth_mem));
+ VK_CHECK(vkBindImageMemory(device, scene_depth_image, scene_depth_mem, 0));
+ VkImageViewCreateInfo dvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
+ dvci.image = scene_depth_image; dvci.viewType = VK_IMAGE_VIEW_TYPE_2D; dvci.format = depth_fmt;
+ dvci.subresourceRange = { VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1 };
+ VK_CHECK(vkCreateImageView(device, &dvci, nullptr, &scene_depth_view));
+
+ VkAttachmentDescription atts[2]{};
+ atts[0].format = swapchain_format; atts[0].samples = VK_SAMPLE_COUNT_1_BIT;
+ atts[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; atts[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
+ atts[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; atts[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
+ atts[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; atts[0].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
+ atts[1].format = depth_fmt; atts[1].samples = VK_SAMPLE_COUNT_1_BIT;
+ atts[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; atts[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
+ atts[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; atts[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
+ atts[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; atts[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
+ VkAttachmentReference color_ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
+ VkAttachmentReference depth_ref{ 1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL };
+ VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
+ subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &color_ref; subpass.pDepthStencilAttachment = &depth_ref;
+ VkSubpassDependency dep{};
+ dep.srcSubpass = VK_SUBPASS_EXTERNAL; dep.dstSubpass = 0;
+ dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
+ dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
+ dep.srcAccessMask = 0;
+ dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
+ VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO };
+ rpci.attachmentCount = 2; rpci.pAttachments = atts; rpci.subpassCount = 1; rpci.pSubpasses = &subpass;
+ rpci.dependencyCount = 1; rpci.pDependencies = &dep;
+ VK_CHECK(vkCreateRenderPass(device, &rpci, nullptr, &scene_render_pass));
+
+ scene_framebuffers.resize(image_views.size());
+ for (size_t i = 0; i < image_views.size(); ++i)
+ {
+ VkImageView att[2] = { image_views[i], scene_depth_view };
+ VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO };
+ fbci.renderPass = scene_render_pass; fbci.attachmentCount = 2; fbci.pAttachments = att;
+ fbci.width = swapchain_extent.width; fbci.height = swapchain_extent.height; fbci.layers = 1;
+ VK_CHECK(vkCreateFramebuffer(device, &fbci, nullptr, &scene_framebuffers[i]));
+ }
+ return true;
+ }
+
+ auto VulkanRenderer::Impl::destroy_scene_targets() -> void
+ {
+ for (auto fb : scene_framebuffers) vkDestroyFramebuffer(device, fb, nullptr);
+ scene_framebuffers.clear();
+ if (scene_render_pass) { vkDestroyRenderPass(device, scene_render_pass, nullptr); scene_render_pass = VK_NULL_HANDLE; }
+ if (scene_depth_view) { vkDestroyImageView(device, scene_depth_view, nullptr); scene_depth_view = VK_NULL_HANDLE; }
+ if (scene_depth_image) { vkDestroyImage(device, scene_depth_image, nullptr); scene_depth_image = VK_NULL_HANDLE; }
+ if (scene_depth_mem) { vkFreeMemory(device, scene_depth_mem, nullptr); scene_depth_mem = VK_NULL_HANDLE; }
+ }
+
auto VulkanRenderer::Impl::create_command_buffers() -> bool
{
VkCommandPoolCreateInfo pci{ VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO };
@@ -887,6 +977,15 @@ namespace Donut
write.dstSet = geo_set; write.dstBinding = 4; write.descriptorCount = 1;
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; write.pImageInfo = &cube_info;
vkUpdateDescriptorSets(device, 1, &write, 0, nullptr);
+
+ // The scene sphere samples the same cube (binding 1) — repoint it too.
+ if (sphere_set)
+ {
+ VkWriteDescriptorSet sw{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET };
+ sw.dstSet = sphere_set; sw.dstBinding = 1; sw.descriptorCount = 1;
+ sw.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; sw.pImageInfo = &cube_info;
+ vkUpdateDescriptorSets(device, 1, &sw, 0, nullptr);
+ }
}
auto VulkanRenderer::Impl::create_present_resources() -> bool
@@ -1013,6 +1112,8 @@ namespace Donut
VkPipelineDynamicStateCreateInfo dsci{ VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO }; dsci.dynamicStateCount = 2; dsci.pDynamicStates = dyn;
VkPipelineRasterizationStateCreateInfo rs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = VK_CULL_MODE_NONE; rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f;
VkPipelineMultisampleStateCreateInfo ms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
+ VkPipelineDepthStencilStateCreateInfo ds{ VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO };
+ ds.depthTestEnable = VK_TRUE; ds.depthWriteEnable = VK_FALSE; ds.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL; // transparent lines: test but don't write
VkPipelineColorBlendAttachmentState cba{};
cba.blendEnable = VK_TRUE;
cba.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; cba.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; cba.colorBlendOp = VK_BLEND_OP_ADD;
@@ -1023,12 +1124,98 @@ namespace Donut
gpci.stageCount = 2; gpci.pStages = stages;
gpci.pVertexInputState = &vin; gpci.pInputAssemblyState = &ia; gpci.pViewportState = &vps;
gpci.pDynamicState = &dsci;
- gpci.pRasterizationState = &rs; gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb;
- gpci.layout = grid_pipeline_layout; gpci.renderPass = render_pass; gpci.subpass = 0;
+ gpci.pRasterizationState = &rs; gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb; gpci.pDepthStencilState = &ds;
+ gpci.layout = grid_pipeline_layout; gpci.renderPass = scene_render_pass; gpci.subpass = 0;
VkResult pr = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpci, nullptr, &grid_pipeline);
vkDestroyShaderModule(device, vmod, nullptr); vkDestroyShaderModule(device, fmod, nullptr);
if (pr != VK_SUCCESS) { DONUT_ERROR("Vulkan: grid pipeline creation failed ({})", (int)pr); return false; }
- DONUT_INFO("Vulkan: scene resources ready ({} grid verts)", grid_vertex_count);
+
+ // Lit sphere: unit UV-sphere mesh (pos+normal, stride 24), placed/scaled by
+ // u_Transform; samples the geodesic HDRI cubemap for ambient (binding 1).
+ {
+ std::vector<float> sv; std::vector<uint32_t> si;
+ const int RINGS = 24, SECTORS = 48;
+ for (int r = 0; r <= RINGS; ++r)
+ {
+ float phi = (float)std::numbers::pi * r / RINGS;
+ for (int s = 0; s <= SECTORS; ++s)
+ {
+ float theta = 2.0f * (float)std::numbers::pi * s / SECTORS;
+ float x = sinf(phi) * cosf(theta), y = cosf(phi), z = sinf(phi) * sinf(theta);
+ sv.push_back(x); sv.push_back(y); sv.push_back(z); // position (unit)
+ sv.push_back(x); sv.push_back(y); sv.push_back(z); // normal == position
+ }
+ }
+ for (int r = 0; r < RINGS; ++r)
+ for (int s = 0; s < SECTORS; ++s)
+ {
+ uint32_t a = r * (SECTORS + 1) + s, b = a + SECTORS + 1;
+ si.push_back(a); si.push_back(b); si.push_back(a + 1);
+ si.push_back(b); si.push_back(b + 1); si.push_back(a + 1);
+ }
+ sphere_index_count = (uint32_t)si.size();
+ VkDeviceSize svsz = sv.size() * sizeof(float), sisz = si.size() * sizeof(uint32_t);
+ if (!create_buffer(svsz, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, sphere_vb, sphere_vb_mem)) return false;
+ if (!create_buffer(sisz, VK_BUFFER_USAGE_INDEX_BUFFER_BIT, host_vis, sphere_ib, sphere_ib_mem)) return false;
+ void* sp = nullptr;
+ vkMapMemory(device, sphere_vb_mem, 0, svsz, 0, &sp); memcpy(sp, sv.data(), svsz); vkUnmapMemory(device, sphere_vb_mem);
+ vkMapMemory(device, sphere_ib_mem, 0, sisz, 0, &sp); memcpy(sp, si.data(), sisz); vkUnmapMemory(device, sphere_ib_mem);
+
+ if (!create_buffer(208, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, sphere_ubo, sphere_ubo_mem)) return false;
+ vkMapMemory(device, sphere_ubo_mem, 0, 208, 0, &sphere_ubo_mapped);
+
+ VkDescriptorSetLayoutBinding sb[2]{};
+ sb[0].binding = 0; sb[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; sb[0].descriptorCount = 1; sb[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
+ sb[1].binding = 1; sb[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; sb[1].descriptorCount = 1; sb[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
+ VkDescriptorSetLayoutCreateInfo sdslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; sdslci.bindingCount = 2; sdslci.pBindings = sb;
+ VK_CHECK(vkCreateDescriptorSetLayout(device, &sdslci, nullptr, &sphere_set_layout));
+ VkDescriptorPoolSize sps[2] = { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1 } };
+ VkDescriptorPoolCreateInfo sdpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; sdpci.maxSets = 1; sdpci.poolSizeCount = 2; sdpci.pPoolSizes = sps;
+ VK_CHECK(vkCreateDescriptorPool(device, &sdpci, nullptr, &sphere_pool));
+ VkDescriptorSetAllocateInfo sdsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; sdsai.descriptorPool = sphere_pool; sdsai.descriptorSetCount = 1; sdsai.pSetLayouts = &sphere_set_layout;
+ VK_CHECK(vkAllocateDescriptorSets(device, &sdsai, &sphere_set));
+ VkDescriptorBufferInfo sbi{ sphere_ubo, 0, VK_WHOLE_SIZE };
+ VkDescriptorImageInfo sii{ cube_sampler, cube_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL };
+ VkWriteDescriptorSet sw[2]{};
+ sw[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; sw[0].dstSet = sphere_set; sw[0].dstBinding = 0; sw[0].descriptorCount = 1; sw[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; sw[0].pBufferInfo = &sbi;
+ sw[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; sw[1].dstSet = sphere_set; sw[1].dstBinding = 1; sw[1].descriptorCount = 1; sw[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; sw[1].pImageInfo = &sii;
+ vkUpdateDescriptorSets(device, 2, sw, 0, nullptr);
+
+ VkShaderModule svmod, sfmod;
+ if (!create_shader_module("assets/shaders/generated/Sphere.vertexMain.spv", svmod)) return false;
+ if (!create_shader_module("assets/shaders/generated/Sphere.fragmentMain.spv", sfmod)) return false;
+ VkPipelineLayoutCreateInfo splci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; splci.setLayoutCount = 1; splci.pSetLayouts = &sphere_set_layout;
+ VK_CHECK(vkCreatePipelineLayout(device, &splci, nullptr, &sphere_pipeline_layout));
+ VkPipelineShaderStageCreateInfo sstages[2]{};
+ sstages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; sstages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; sstages[0].module = svmod; sstages[0].pName = "main";
+ sstages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; sstages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; sstages[1].module = sfmod; sstages[1].pName = "main";
+ VkVertexInputBindingDescription svib{ 0, 6 * (uint32_t)sizeof(float), VK_VERTEX_INPUT_RATE_VERTEX };
+ VkVertexInputAttributeDescription svia[2] = { { 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 }, { 1, 0, VK_FORMAT_R32G32B32_SFLOAT, 3 * (uint32_t)sizeof(float) } };
+ VkPipelineVertexInputStateCreateInfo svin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO };
+ svin.vertexBindingDescriptionCount = 1; svin.pVertexBindingDescriptions = &svib;
+ svin.vertexAttributeDescriptionCount = 2; svin.pVertexAttributeDescriptions = svia;
+ VkPipelineInputAssemblyStateCreateInfo sia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; sia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
+ VkPipelineViewportStateCreateInfo svps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; svps.viewportCount = 1; svps.scissorCount = 1;
+ VkDynamicState sdyn[2] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR };
+ VkPipelineDynamicStateCreateInfo sdsci{ VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO }; sdsci.dynamicStateCount = 2; sdsci.pDynamicStates = sdyn;
+ VkPipelineRasterizationStateCreateInfo srs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; srs.polygonMode = VK_POLYGON_MODE_FILL; srs.cullMode = VK_CULL_MODE_NONE; srs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; srs.lineWidth = 1.0f;
+ VkPipelineMultisampleStateCreateInfo sms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; sms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
+ VkPipelineDepthStencilStateCreateInfo sds{ VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO };
+ sds.depthTestEnable = VK_TRUE; sds.depthWriteEnable = VK_TRUE; sds.depthCompareOp = VK_COMPARE_OP_LESS;
+ VkPipelineColorBlendAttachmentState scba{}; scba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
+ VkPipelineColorBlendStateCreateInfo scb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; scb.attachmentCount = 1; scb.pAttachments = &scba;
+ VkGraphicsPipelineCreateInfo sgpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO };
+ sgpci.stageCount = 2; sgpci.pStages = sstages;
+ sgpci.pVertexInputState = &svin; sgpci.pInputAssemblyState = &sia; sgpci.pViewportState = &svps;
+ sgpci.pDynamicState = &sdsci;
+ sgpci.pRasterizationState = &srs; sgpci.pMultisampleState = &sms; sgpci.pColorBlendState = &scb; sgpci.pDepthStencilState = &sds;
+ sgpci.layout = sphere_pipeline_layout; sgpci.renderPass = scene_render_pass; sgpci.subpass = 0;
+ VkResult spr = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &sgpci, nullptr, &sphere_pipeline);
+ vkDestroyShaderModule(device, svmod, nullptr); vkDestroyShaderModule(device, sfmod, nullptr);
+ if (spr != VK_SUCCESS) { DONUT_ERROR("Vulkan: sphere pipeline creation failed ({})", (int)spr); return false; }
+ }
+
+ DONUT_INFO("Vulkan: scene resources ready ({} grid verts, {} sphere indices)", grid_vertex_count, sphere_index_count);
return true;
}
@@ -1059,6 +1246,28 @@ namespace Donut
g.grid_alpha = 0.75f;
g.camera_pos = scene_camera.get_orbital_position();
if (grid_ubo_mapped) memcpy(grid_ubo_mapped, &g, sizeof(g));
+
+ struct SphereUBO {
+ glm::mat4 view_projection; // 0
+ glm::mat4 transform; // 64
+ glm::vec3 color; float specular; // 128, 140
+ float emission; float p0[3]; // 144
+ glm::vec3 light_pos; float p1; // 160, 172
+ glm::vec3 camera_pos; int is_selected; // 176, 188
+ glm::vec3 outline_color; float outline_width; // 192, 204
+ } s{};
+ static_assert(sizeof(SphereUBO) == 208, "SphereUBO std140 layout mismatch");
+ s.view_projection = vp; // same no-transpose rule as the grid
+ s.transform = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 2.0f, 0.0f)) * glm::scale(glm::mat4(1.0f), glm::vec3(2.0f));
+ s.color = glm::vec3(0.85f, 0.35f, 0.2f);
+ s.specular = 0.6f;
+ s.emission = 0.0f;
+ s.light_pos = glm::vec3(10.0f, 20.0f, 10.0f);
+ s.camera_pos = scene_camera.get_orbital_position();
+ s.is_selected = 0;
+ s.outline_color = glm::vec3(1.0f, 1.0f, 0.0f);
+ s.outline_width = 0.1f;
+ if (sphere_ubo_mapped) memcpy(sphere_ubo_mapped, &s, sizeof(s));
}
auto VulkanRenderer::Impl::update_geodesic_uniforms() -> void
@@ -1169,6 +1378,18 @@ namespace Donut
if (grid_vb) vkDestroyBuffer(device, grid_vb, nullptr);
if (grid_vb_mem) vkFreeMemory(device, grid_vb_mem, nullptr);
+ if (sphere_pipeline) vkDestroyPipeline(device, sphere_pipeline, nullptr);
+ if (sphere_pipeline_layout) vkDestroyPipelineLayout(device, sphere_pipeline_layout, nullptr);
+ if (sphere_pool) vkDestroyDescriptorPool(device, sphere_pool, nullptr);
+ if (sphere_set_layout) vkDestroyDescriptorSetLayout(device, sphere_set_layout, nullptr);
+ if (sphere_ubo_mapped) { vkUnmapMemory(device, sphere_ubo_mem); sphere_ubo_mapped = nullptr; }
+ if (sphere_ubo) vkDestroyBuffer(device, sphere_ubo, nullptr);
+ if (sphere_ubo_mem) vkFreeMemory(device, sphere_ubo_mem, nullptr);
+ if (sphere_ib) vkDestroyBuffer(device, sphere_ib, nullptr);
+ if (sphere_ib_mem) vkFreeMemory(device, sphere_ib_mem, nullptr);
+ if (sphere_vb) vkDestroyBuffer(device, sphere_vb, nullptr);
+ if (sphere_vb_mem) vkFreeMemory(device, sphere_vb_mem, nullptr);
+
if (present_pipeline) vkDestroyPipeline(device, present_pipeline, nullptr);
if (present_pipeline_layout) vkDestroyPipelineLayout(device, present_pipeline_layout, nullptr);
if (present_pool) vkDestroyDescriptorPool(device, present_pool, nullptr);
@@ -1204,22 +1425,34 @@ namespace Donut
if (scene_mode)
{
- // Scene view: grid lines drawn straight into the swapchain, then ImGui.
- VkClearValue cv{}; cv.color = { { clear.r, clear.g, clear.b, clear.a } };
+ // Scene view: opaque sphere (writes depth) then the transparent grid on
+ // top, into a color+depth swapchain pass, then ImGui.
+ VkClearValue cvs[2]{};
+ cvs[0].color = { { clear.r, clear.g, clear.b, clear.a } };
+ cvs[1].depthStencil = { 1.0f, 0 };
VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO };
- rpbi.renderPass = render_pass; rpbi.framebuffer = framebuffers[image_index];
+ rpbi.renderPass = scene_render_pass; rpbi.framebuffer = scene_framebuffers[image_index];
rpbi.renderArea = { { 0, 0 }, swapchain_extent };
- rpbi.clearValueCount = 1; rpbi.pClearValues = &cv;
+ rpbi.clearValueCount = 2; rpbi.pClearValues = cvs;
vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE);
- vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, grid_pipeline);
+
// Negative-height viewport flips Y so the scene reads like the GL path.
VkViewport gvp{ 0, (float)swapchain_extent.height, (float)swapchain_extent.width, -(float)swapchain_extent.height, 0, 1 };
VkRect2D gsc{ { 0, 0 }, swapchain_extent };
vkCmdSetViewport(cmd, 0, 1, &gvp);
vkCmdSetScissor(cmd, 0, 1, &gsc);
+
+ vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, sphere_pipeline);
+ vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, sphere_pipeline_layout, 0, 1, &sphere_set, 0, nullptr);
+ VkDeviceSize soff = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &sphere_vb, &soff);
+ vkCmdBindIndexBuffer(cmd, sphere_ib, 0, VK_INDEX_TYPE_UINT32);
+ vkCmdDrawIndexed(cmd, sphere_index_count, 1, 0, 0, 0);
+
+ vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, grid_pipeline);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, grid_pipeline_layout, 0, 1, &grid_set, 0, nullptr);
VkDeviceSize goff = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &grid_vb, &goff);
vkCmdDraw(cmd, grid_vertex_count, 1, 0, 0);
+
if (draw_data)
ImGui_ImplVulkan_RenderDrawData(draw_data, cmd);
vkCmdEndRenderPass(cmd);
@@ -1269,6 +1502,7 @@ namespace Donut
auto VulkanRenderer::Impl::cleanup_swapchain() -> void
{
+ destroy_scene_targets();
for (auto fb : framebuffers) vkDestroyFramebuffer(device, fb, nullptr);
framebuffers.clear();
for (auto iv : image_views) vkDestroyImageView(device, iv, nullptr);
@@ -1296,6 +1530,7 @@ namespace Donut
if (!create_image_views()) return false;
if (!create_render_pass()) return false;
if (!create_framebuffers())return false;
+ if (!create_scene_targets())return false;
images_in_flight.assign(images.size(), VK_NULL_HANDLE);
return true;
}
@@ -1315,6 +1550,7 @@ namespace Donut
if (!v.create_image_views()) return false;
if (!v.create_render_pass()) return false;
if (!v.create_framebuffers()) return false;
+ if (!v.create_scene_targets()) return false;
if (!v.create_command_buffers()) return false;
if (!v.create_sync_objects()) return false;
if (!v.create_geodesic_resources()) return false;