├── .gitignore ├── CMakeLists.txt ├── FindVulkan.cmake ├── LICENSE ├── README.md ├── data ├── Arcade.lights ├── Bistro_inside.lights ├── Bistro_outside.lights ├── LinBiolinum_Rah.ttf ├── attic.lights ├── cornell_box.lights ├── cornell_box.vks ├── cornell_box_textures │ ├── _emission_BaseColor.vkt │ ├── _emission_Normal.vkt │ ├── _emission_Specular.vkt │ ├── green_BaseColor.vkt │ ├── green_Normal.vkt │ ├── green_Specular.vkt │ ├── red_BaseColor.vkt │ ├── red_Normal.vkt │ ├── red_Specular.vkt │ ├── white_BaseColor.vkt │ ├── white_Normal.vkt │ └── white_Specular.vkt ├── living_room_day.lights ├── living_room_night.lights └── saves │ ├── Arcade │ └── default.rt_save │ ├── attic │ └── default.rt_save │ ├── bistro │ ├── inside.rt_save │ ├── mirror.rt_save │ ├── outside.rt_save │ ├── path_overview.rt_save │ ├── pretty.rt_save │ ├── pretty_day.rt_save │ ├── shading_point_dark.rt_save │ └── shading_point_day.rt_save │ ├── cornell_box │ └── default.rt_save │ └── living_room │ ├── day.rt_save │ ├── night.rt_save │ ├── path_overview_day.rt_save │ ├── shading_point_ceiling_day.rt_save │ └── shading_point_day.rt_save ├── ext └── nuklear.h ├── src ├── CMakeLists.txt ├── camera.c ├── camera.h ├── main.c ├── main.h ├── math_utilities.c ├── math_utilities.h ├── nuklear.c ├── nuklear.h ├── scene.c ├── scene.h ├── shaders │ ├── brdfs.glsl │ ├── camera_utilities.glsl │ ├── constants.glsl │ ├── gui.frag.glsl │ ├── gui.vert.glsl │ ├── mesh_quantization.glsl │ ├── pathtrace.frag.glsl │ ├── pathtrace.vert.glsl │ ├── shading_data.glsl │ ├── srgb_utility.glsl │ ├── tonemap.frag.glsl │ └── tonemap.vert.glsl ├── slides.c ├── stb_image_write.c ├── stb_image_write.h ├── string_utilities.c ├── string_utilities.h ├── textures.c ├── textures.h ├── timer.c ├── timer.h ├── vulkan_basics.c ├── vulkan_basics.h ├── vulkan_formats.c └── vulkan_formats.h └── tools ├── COPYING.txt ├── io_export_spherical_lights_blender40.py ├── io_export_vulkan_blender28.py ├── material_conversion.py ├── texture_conversion ├── CMakeLists.txt ├── float_to_half.h ├── main.c ├── stb_dxt.h └── stb_image.h ├── vk.xml └── vulkan_formats.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | .vscode 3 | .idea 4 | *.kate-swp 5 | *.spv 6 | *.blend* 7 | *.lyx~ 8 | build 9 | data/attribution.txt 10 | data/Bistro_textures 11 | data/Bistro_inside.vks 12 | data/Bistro_outside.vks 13 | data/quicksave.save 14 | data/brain.blob 15 | data/M24_06.edf 16 | data/screenshot.* 17 | data/results 18 | data/Arcade_textures 19 | data/Arcade.vks 20 | data/attic.vks 21 | data/attic_textures 22 | data/living_room_day.vks 23 | data/living_room_night.vks 24 | data/living_room_textures 25 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 3.11) 2 | 3 | # We build a single executable 4 | project(path_tracer) 5 | add_executable(path_tracer) 6 | target_compile_definitions(path_tracer 7 | # Keep OpenGL from being included 8 | PUBLIC GLFW_INCLUDE_NONE 9 | # Silence MS build warnings 10 | PUBLIC _CRT_SECURE_NO_WARNINGS 11 | ) 12 | 13 | # We use C99 14 | set_target_properties(path_tracer PROPERTIES C_STANDARD 99) 15 | set_target_properties(path_tracer PROPERTIES CMAKE_C_STANDARD_REQUIRED True) 16 | 17 | # All source code (except dependencies) is in src 18 | add_subdirectory(src) 19 | 20 | # Add Vulkan and GLFW as dependencies 21 | find_package(glfw3 3.3) 22 | if(NOT ${glfw3_FOUND}) 23 | set(GLFW_BUILD_DOCS False) 24 | set(GLFW_BUILD_EXAMPLES False) 25 | set(GLFW_BUILD_TESTS False) 26 | set(GLFW_VULKAN_STATIC True) 27 | add_subdirectory(ext/glfw) 28 | endif() 29 | find_package(Vulkan REQUIRED) 30 | target_link_libraries(path_tracer PRIVATE Vulkan::Vulkan glfw) 31 | 32 | # math.h is being used 33 | if (UNIX) 34 | find_library(MATH_LIBRARY m) 35 | if(MATH_LIBRARY) 36 | target_link_libraries(path_tracer PUBLIC ${MATH_LIBRARY}) 37 | # MSVC does not support variable-length arrays or void* pointer arithmetic, 38 | # so we always treat these things as errors in Linux 39 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Werror=vla -Werror=pointer-arith") 40 | endif() 41 | endif() 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, Christoph Peters, TU Delft 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repository contains a Vulkan rendering framework with an implementation of a path tracer written for educational purposes. 2 | The core code of the path tracer can be found in src/shaders/pathtrace.frag.glsl. 3 | The underlying rendering algorithms are described in lectures available here: 4 | https://www.tudelft.nl/ewi/over-de-faculteit/afdelingen/intelligent-systems/computer-graphics-and-visualization/education/path-tracing-lecture 5 | 6 | 7 | # Build instructions 8 | 9 | Dependencies of this project are GLFW 3.4 or newer and Vulkan 1.3.295 or newer and those have to be installed separately. 10 | For Windows the Vulkan SDK installer can be downloaded here: 11 | https://vulkan.lunarg.com/sdk/home 12 | And GLFW is available here: 13 | https://www.glfw.org/download.html 14 | Download the GLFW source package, unzip it into ext and rename it such that the path ext/glfw/CMakeLists.txt is valid. 15 | 16 | For Linux, it is probably easier to obtain Vulkan and GLFW via a package manager. 17 | For pacman, the relevant packages are: vulkan-headers vulkan-validation-layers glfw 18 | 19 | Once all dependencies are available, use [CMake](https://cmake.org/) to build the project using your preferred IDE or the following commands (using the directory containing this README.md as working directory): 20 | $ cmake . 21 | $ make 22 | 23 | 24 | # Run instructions 25 | 26 | If you want to see a scene other than the Cornell box, you must first make sure that you have all data files upon which the renderer depends. 27 | Several large files are not part of this repository and should instead be downloaded at the following link and added to the data directory. 28 | https://www.tudelft.nl/ewi/over-de-faculteit/afdelingen/intelligent-systems/computer-graphics-and-visualization/education/path-tracing-lecture 29 | 30 | Then launch the path_tracer binary. 31 | **Important:** The current working directory has to be the directory containing this README.md. 32 | If anything goes wrong, there should be an informative error message on stdout. 33 | 34 | Camera controls use WASD for horizontal movement, QE to move down/up, right mouse button to rotate the camera and control/shift to adjust the speed. F1 toggles the UI, F2 toggles v-sync, F3/F4 are quicksave/quickload, F5 reloads shaders, holding F6 disables sample accumulation and F10/F11/F12 store screenshots to the data folder using hdr/png/jpg as format. 35 | -------------------------------------------------------------------------------- /data/Arcade.lights: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/Arcade.lights -------------------------------------------------------------------------------- /data/Bistro_inside.lights: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/Bistro_inside.lights -------------------------------------------------------------------------------- /data/Bistro_outside.lights: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/Bistro_outside.lights -------------------------------------------------------------------------------- /data/LinBiolinum_Rah.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/LinBiolinum_Rah.ttf -------------------------------------------------------------------------------- /data/attic.lights: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/attic.lights -------------------------------------------------------------------------------- /data/cornell_box.lights: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/cornell_box.lights -------------------------------------------------------------------------------- /data/cornell_box.vks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/cornell_box.vks -------------------------------------------------------------------------------- /data/cornell_box_textures/_emission_BaseColor.vkt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/cornell_box_textures/_emission_BaseColor.vkt -------------------------------------------------------------------------------- /data/cornell_box_textures/_emission_Normal.vkt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/cornell_box_textures/_emission_Normal.vkt -------------------------------------------------------------------------------- /data/cornell_box_textures/_emission_Specular.vkt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/cornell_box_textures/_emission_Specular.vkt -------------------------------------------------------------------------------- /data/cornell_box_textures/green_BaseColor.vkt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/cornell_box_textures/green_BaseColor.vkt -------------------------------------------------------------------------------- /data/cornell_box_textures/green_Normal.vkt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/cornell_box_textures/green_Normal.vkt -------------------------------------------------------------------------------- /data/cornell_box_textures/green_Specular.vkt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/cornell_box_textures/green_Specular.vkt -------------------------------------------------------------------------------- /data/cornell_box_textures/red_BaseColor.vkt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/cornell_box_textures/red_BaseColor.vkt -------------------------------------------------------------------------------- /data/cornell_box_textures/red_Normal.vkt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/cornell_box_textures/red_Normal.vkt -------------------------------------------------------------------------------- /data/cornell_box_textures/red_Specular.vkt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/cornell_box_textures/red_Specular.vkt -------------------------------------------------------------------------------- /data/cornell_box_textures/white_BaseColor.vkt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/cornell_box_textures/white_BaseColor.vkt -------------------------------------------------------------------------------- /data/cornell_box_textures/white_Normal.vkt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/cornell_box_textures/white_Normal.vkt -------------------------------------------------------------------------------- /data/cornell_box_textures/white_Specular.vkt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/cornell_box_textures/white_Specular.vkt -------------------------------------------------------------------------------- /data/living_room_day.lights: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/living_room_day.lights -------------------------------------------------------------------------------- /data/living_room_night.lights: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/living_room_night.lights -------------------------------------------------------------------------------- /data/saves/Arcade/default.rt_save: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/saves/Arcade/default.rt_save -------------------------------------------------------------------------------- /data/saves/attic/default.rt_save: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/saves/attic/default.rt_save -------------------------------------------------------------------------------- /data/saves/bistro/inside.rt_save: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/saves/bistro/inside.rt_save -------------------------------------------------------------------------------- /data/saves/bistro/mirror.rt_save: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/saves/bistro/mirror.rt_save -------------------------------------------------------------------------------- /data/saves/bistro/outside.rt_save: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/saves/bistro/outside.rt_save -------------------------------------------------------------------------------- /data/saves/bistro/path_overview.rt_save: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/saves/bistro/path_overview.rt_save -------------------------------------------------------------------------------- /data/saves/bistro/pretty.rt_save: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/saves/bistro/pretty.rt_save -------------------------------------------------------------------------------- /data/saves/bistro/pretty_day.rt_save: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/saves/bistro/pretty_day.rt_save -------------------------------------------------------------------------------- /data/saves/bistro/shading_point_dark.rt_save: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/saves/bistro/shading_point_dark.rt_save -------------------------------------------------------------------------------- /data/saves/bistro/shading_point_day.rt_save: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/saves/bistro/shading_point_day.rt_save -------------------------------------------------------------------------------- /data/saves/cornell_box/default.rt_save: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/saves/cornell_box/default.rt_save -------------------------------------------------------------------------------- /data/saves/living_room/day.rt_save: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/saves/living_room/day.rt_save -------------------------------------------------------------------------------- /data/saves/living_room/night.rt_save: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/saves/living_room/night.rt_save -------------------------------------------------------------------------------- /data/saves/living_room/path_overview_day.rt_save: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/saves/living_room/path_overview_day.rt_save -------------------------------------------------------------------------------- /data/saves/living_room/shading_point_ceiling_day.rt_save: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/saves/living_room/shading_point_ceiling_day.rt_save -------------------------------------------------------------------------------- /data/saves/living_room/shading_point_day.rt_save: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MomentsInGraphics/path_tracer/978e77c02aac5c368f7b65a2bb3055d370744aca/data/saves/living_room/shading_point_day.rt_save -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_policy(SET CMP0076 NEW) 2 | target_sources(path_tracer PRIVATE 3 | camera.c 4 | camera.h 5 | main.c 6 | main.h 7 | math_utilities.c 8 | math_utilities.h 9 | nuklear.c 10 | nuklear.h 11 | scene.c 12 | scene.h 13 | slides.c 14 | stb_image_write.c 15 | string_utilities.h 16 | string_utilities.c 17 | textures.c 18 | textures.h 19 | timer.c 20 | timer.h 21 | vulkan_basics.h 22 | vulkan_basics.c 23 | vulkan_formats.h 24 | vulkan_formats.c 25 | shaders/camera_utilities.glsl 26 | shaders/constants.glsl 27 | shaders/gui.frag.glsl 28 | shaders/gui.vert.glsl 29 | shaders/mesh_quantization.glsl 30 | shaders/pathtrace.frag.glsl 31 | shaders/pathtrace.vert.glsl 32 | shaders/srgb_utility.glsl 33 | shaders/tonemap.frag.glsl 34 | shaders/tonemap.vert.glsl 35 | ) 36 | -------------------------------------------------------------------------------- /src/camera.c: -------------------------------------------------------------------------------- 1 | #include "camera.h" 2 | #include "math_utilities.h" 3 | #include "timer.h" 4 | #include 5 | #include 6 | #define GLFW_INCLUDE_VULKAN 7 | #include 8 | 9 | 10 | void control_rotation(controllable_rotation_t* rotation, GLFWwindow* window) { 11 | // Defines which mouse axis controls which rotation angle and the rotation 12 | // speed (in radians per pixel) 13 | const float rot_speed = M_PI / 2000.0f; 14 | const float mouse_offset_to_rotation[3 * 2] = { 15 | 0.0f, -rot_speed, 16 | 0.0f, 0.0f, 17 | rot_speed, 0.0f, 18 | }; 19 | // Query the mouse cursor position 20 | double mouse_pos_d[2] = { 0.0, 0.0 }; 21 | glfwGetCursorPos(window, &mouse_pos_d[0], &mouse_pos_d[1]); 22 | float mouse_pos[2] = { (float) mouse_pos_d[0], (float) mouse_pos_d[1] }; 23 | // Activate/deactivate mouse rotation 24 | int rmb_state = glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_2); 25 | if (!rotation->mouse_active && rmb_state == GLFW_PRESS) { 26 | rotation->mouse_active = true; 27 | float angles_offset[3]; 28 | mat_vec_mul(angles_offset, mouse_offset_to_rotation, mouse_pos, 3, 2); 29 | for (uint32_t i = 0; i != 3; ++ i) 30 | rotation->origin_angles[i] = rotation->angles[i] - angles_offset[i]; 31 | } 32 | else if (rotation->mouse_active && rmb_state == GLFW_RELEASE) 33 | rotation->mouse_active = false; 34 | // Update the rotation 35 | if (rotation->mouse_active) { 36 | float angles_offset[3]; 37 | mat_vec_mul(angles_offset, mouse_offset_to_rotation, mouse_pos, 3, 2); 38 | for (uint32_t i = 0; i != 3; ++ i) 39 | rotation->angles[i] = rotation->origin_angles[i] + angles_offset[i]; 40 | } 41 | // Limit rotation along the x-axis 42 | if (rotation->angles[0] < 0.0f) 43 | rotation->angles[0] = 0.0f; 44 | if (rotation->angles[0] > M_PI) 45 | rotation->angles[0] = M_PI; 46 | } 47 | 48 | 49 | void control_camera(camera_t* camera, GLFWwindow* window) { 50 | control_rotation(&camera->rotation, window); 51 | // Movement happens along three axes using WASD and QE 52 | float x = 0.0f, y = 0.0f, z = 0.0f; 53 | if (glfwGetKey(window, GLFW_KEY_W)) y += 1.0f; 54 | if (glfwGetKey(window, GLFW_KEY_A)) x -= 1.0f; 55 | if (glfwGetKey(window, GLFW_KEY_S)) y -= 1.0f; 56 | if (glfwGetKey(window, GLFW_KEY_D)) x += 1.0f; 57 | if (glfwGetKey(window, GLFW_KEY_Q)) z -= 1.0f; 58 | if (glfwGetKey(window, GLFW_KEY_E)) z += 1.0f; 59 | // Apply the motion along the right axes depending on the camera type 60 | float offset[3]; 61 | float log_height_factor = 0.0f; 62 | switch (camera->type) { 63 | case camera_type_first_person: 64 | case camera_type_hemispherical: 65 | case camera_type_spherical: 66 | { 67 | // WASD move in the xy-plane and QE along the z-axis 68 | float sin_z = sinf(camera->rotation.angles[2]); 69 | float cos_z = cosf(camera->rotation.angles[2]); 70 | offset[0] = -cos_z * x - sin_z * y; 71 | offset[1] = sin_z * x - cos_z * y; 72 | offset[2] = z; 73 | break; 74 | } 75 | case camera_type_ortho: 76 | { 77 | float local_offset[3] = { x, -y, 0.0f }; 78 | float rotation[3 * 3]; 79 | rotation_matrix_from_angles(rotation, camera->rotation.angles); 80 | mat_vec_mul(offset, rotation, local_offset, 3, 3); 81 | log_height_factor = 0.1f * z; 82 | break; 83 | } 84 | default: 85 | offset[0] = offset[1] = offset[2] = 0.0f; 86 | break; 87 | } 88 | // Apply the motion 89 | float final_speed = camera->speed; 90 | if (glfwGetKey(window, GLFW_KEY_LEFT_SHIFT)) 91 | final_speed *= 10.0f; 92 | if (glfwGetKey(window, GLFW_KEY_LEFT_CONTROL)) 93 | final_speed *= 0.1f; 94 | float step = final_speed * get_frame_delta(); 95 | if (normalize(offset, 3)) 96 | for (uint32_t i = 0; i != 3; ++i) 97 | camera->position[i] += step * offset[i]; 98 | camera->height *= expf(step * log_height_factor); 99 | } 100 | 101 | 102 | void get_world_to_view_space(float world_to_view[4 * 4], const camera_t* camera) { 103 | // Construct the view to world space rotation matrix 104 | float rotation[3 * 3]; 105 | rotation_matrix_from_angles(rotation, camera->rotation.angles); 106 | // We actually want the world to view space, so we transform the position 107 | // and work with the transpose (i.e. inverse) rotation matrix 108 | float translation[3]; 109 | mat_mat_mul(translation, camera->position, rotation, 1, 3, 3); 110 | float new_world_to_view[4 * 4] = { 111 | rotation[0 * 3 + 0], rotation[1 * 3 + 0], rotation[2 * 3 + 0], -translation[0], 112 | rotation[0 * 3 + 1], rotation[1 * 3 + 1], rotation[2 * 3 + 1], -translation[1], 113 | rotation[0 * 3 + 2], rotation[1 * 3 + 2], rotation[2 * 3 + 2], -translation[2], 114 | 0.0f, 0.0f, 0.0f, 1.0f, 115 | }; 116 | memcpy(world_to_view, new_world_to_view, sizeof(new_world_to_view)); 117 | } 118 | 119 | 120 | void get_view_to_projection_space(float view_to_projection[4 * 4], const camera_t* camera, float aspect_ratio) { 121 | float near = camera->near; 122 | float far = camera->far; 123 | switch (camera->type) { 124 | case camera_type_first_person: 125 | case camera_type_hemispherical: 126 | case camera_type_spherical: 127 | { 128 | float top = tanf(0.5f * camera->fov); 129 | float right = aspect_ratio * top; 130 | float new_view_to_projection[4 * 4] = { 131 | -1.0f / right, 0.0f, 0.0f, 0.0f, 132 | 0.0f, 1.0f / top, 0.0f, 0.0f, 133 | 0.0f, 0.0f, (far + near) / (near - far), 2.0f * far * near / (near - far), 134 | 0.0f, 0.0f, -1.0f, 0.0f, 135 | }; 136 | memcpy(view_to_projection, new_view_to_projection, sizeof(new_view_to_projection)); 137 | break; 138 | } 139 | case camera_type_ortho: 140 | { 141 | float height = camera->height; 142 | float width = aspect_ratio * height; 143 | // Based on: https://registry.khronos.org/OpenGL-Refpages/gl2.1/xhtml/glOrtho.xml 144 | float new_view_to_projection[4 * 4] = { 145 | 2.0f / width, 0.0f, 0.0f, 0.0f, 146 | 0.0f, 2.0f / height, 0.0f, 0.0f, 147 | 0.0f, 0.0f, -2.0f / (far - near), -(far + near) / (far - near), 148 | 0.0f, 0.0f, 0.0f, 1.0f, 149 | }; 150 | memcpy(view_to_projection, new_view_to_projection, sizeof(new_view_to_projection)); 151 | break; 152 | } 153 | default: 154 | memset(view_to_projection, 0, sizeof(float) * 4 * 4); 155 | break; 156 | } 157 | } 158 | 159 | 160 | void get_world_to_projection_space(float world_to_projection[4 * 4], const camera_t* camera, float aspect_ratio) { 161 | // Retrieve and combine the relevant transforms 162 | float world_to_view[4 * 4]; 163 | float view_to_projection[4 * 4]; 164 | get_world_to_view_space(world_to_view, camera); 165 | get_view_to_projection_space(view_to_projection, camera, aspect_ratio); 166 | mat_mat_mul(world_to_projection, view_to_projection, world_to_view, 4, 4, 4); 167 | } 168 | -------------------------------------------------------------------------------- /src/camera.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | 5 | //! Forward declaration to be able to access mouse and keyboard input 6 | typedef struct GLFWwindow GLFWwindow; 7 | 8 | 9 | //! Describes a 3D rotation along with attributes needed to implement mouse 10 | //! control 11 | typedef struct { 12 | //! The current view- to world-space rotation in the form expected by 13 | //! rotation_matrix_from_angles() 14 | float angles[3]; 15 | //! Whether the camera rotation is currently being controlled by the mouse 16 | //! cursor 17 | bool mouse_active; 18 | //! While mouse rotation is active, these are the values that 19 | //! rotation_angles would have if the mouse were at pixel (0, 0) 20 | float origin_angles[3]; 21 | } controllable_rotation_t; 22 | 23 | 24 | //! Lists available types of cameras with different projections and controls 25 | typedef enum { 26 | //! A first-person free-flight camera with perspective projection 27 | camera_type_first_person, 28 | //! A camera with orthographic projection and controls similar to the 29 | //! first person camera 30 | camera_type_ortho, 31 | //! A camera that uses spherical coordinates to show an entire hemisphere 32 | camera_type_hemispherical, 33 | //! A camera that uses spherical coordinates to show an entire sphere 34 | camera_type_spherical, 35 | //! Number of entries in this enumeration 36 | camera_type_count, 37 | } camera_type_t; 38 | 39 | 40 | //! A camera with orthographic projection and controls similar to the 41 | //! camera_first_person 42 | typedef struct { 43 | //! The rotation of the camera 44 | controllable_rotation_t rotation; 45 | /*! The world space position of the camera. For perspective projections, 46 | this is the point of view, for orthographic projections it is a point 47 | that will be shown in the middle of the viewport.*/ 48 | float position[3]; 49 | //! The base speed of the position in world-space distance per second 50 | float speed; 51 | //! The signed distance of the near and far clipping plane to position. 52 | //! Usually near < far. 53 | float near, far; 54 | //! The type of this camera. Affects the kind of projection and the 55 | //! position controls. 56 | camera_type_t type; 57 | //! The vertical field of view in radians of a perspective camera, i.e. the 58 | //! angle between the top and bottom clipping planes 59 | float fov; 60 | //! The world-space distance between the top and bottom clipping plane for 61 | //! orthographic cameras 62 | float height; 63 | } camera_t; 64 | 65 | 66 | //! Retrieves mouse input from the given window and updates the given rotation 67 | //! accordingly 68 | void control_rotation(controllable_rotation_t* rotation, GLFWwindow* window); 69 | 70 | 71 | //! Retrieves keyboard and mouse input from the given window and updates the 72 | //! given camera accordingly. Requires regular calls to record_frame_time(). 73 | void control_camera(camera_t* camera, GLFWwindow* window); 74 | 75 | 76 | //! Like get_world_to_projection_space() but for world to view space 77 | void get_world_to_view_space(float world_to_view[4 * 4], const camera_t* camera); 78 | 79 | 80 | //! Like get_world_to_projection_space() but for view to projection space 81 | void get_view_to_projection_space(float view_to_projection[4 * 4], const camera_t* camera, float aspect_ratio); 82 | 83 | 84 | /*! Constructs the world to projection space transformation matrix to be used 85 | for the given camera. 86 | \param world_to_projection Output array for a 4x4 matrix in row-major 87 | layout, which is to be multiplied onto world-space colum vectors from 88 | the left. 89 | \param camera The camera for which to construct the transform. 90 | \param aspect_ratio The ratio of viewport width over viewport height.*/ 91 | void get_world_to_projection_space(float world_to_projection[4 * 4], const camera_t* camera, float aspect_ratio); 92 | -------------------------------------------------------------------------------- /src/main.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "vulkan_basics.h" 3 | #include "scene.h" 4 | #include "camera.h" 5 | #include "nuklear.h" 6 | #include 7 | #include 8 | 9 | 10 | //! The number of frames in flight, i.e. how many frames the host submits to 11 | //! the GPU before waiting for the oldest one to finish 12 | #define FRAME_IN_FLIGHT_COUNT 3 13 | //! The maximal number of spherical lights that can be placed in the scene. 14 | //! When changing this, also change the array size in shaders/constants.glsl. 15 | #define MAX_SPHERICAL_LIGHT_COUNT 32 16 | //! The maximal number of slides 17 | #define MAX_SLIDE_COUNT 100 18 | 19 | 20 | //! An enumeration of available scenes (i.e. *.vks files) 21 | typedef enum { 22 | scene_file_bistro_outside, 23 | scene_file_cornell_box, 24 | scene_file_arcade, 25 | scene_file_attic, 26 | scene_file_bistro_inside, 27 | scene_file_living_room_day, 28 | scene_file_living_room_night, 29 | //! Number of different scenes 30 | scene_file_count, 31 | } scene_file_t; 32 | 33 | 34 | //! Available tonemapping operators 35 | typedef enum { 36 | //! Simply convert the linear radiance values to sRGB, clamping channels 37 | //! that are above 1 38 | tonemapper_clamp, 39 | //! The tonemapper from the Academy Color Encoding System 40 | tonemapper_aces, 41 | //! The Khronos PBR neutral tone mapper as documented here: 42 | //! https://github.com/KhronosGroup/ToneMapping/blob/main/PBR_Neutral/README.md 43 | tonemapper_khronos_pbr_neutral, 44 | //! The number of available tonemapping operators 45 | tonemapper_count, 46 | //! Used to force an int32_t 47 | tonemapper_force_int_32 = 0x7fffffff, 48 | } tonemapper_t; 49 | 50 | 51 | 52 | /*! A complete specification of what is to be rendered without specifying 53 | different techniques that are supposed to render the same thing. This 54 | struct is what quicksave/quickload deal with.*/ 55 | typedef struct { 56 | //! The scene file that is to be loaded 57 | scene_file_t scene_file; 58 | //! The camera used to observe the scene 59 | camera_t camera; 60 | //! The tonemapping operator used to present colors 61 | tonemapper_t tonemapper; 62 | //! The factor by which HDR radiance is scaled during tonemapping 63 | float exposure; 64 | //! The index of the frame being rendered for the purpose of random seed 65 | //! generation 66 | uint32_t frame_index; 67 | //! The color of the sky (using Rec. 709, a.k.a. linear sRGB) 68 | float sky_color[3]; 69 | //! A factor applied to the sky color to get radiance 70 | float sky_strength; 71 | //! The color of light emitted by the material called _emission (Rec. 709) 72 | float emission_material_color[3]; 73 | //! A factor applied to the emission color to get radiance 74 | float emission_material_strength; 75 | //! Four floats that can be controlled from the GUI directly and can be 76 | //! used for any purpose while developing shaders 77 | float params[4]; 78 | } scene_spec_t; 79 | 80 | 81 | //! Different sampling strategies to use in a path tracer. More detailed 82 | //! documentation is available in the path_trace_*() functions in the shader. 83 | typedef enum { 84 | //! Sampling of the hemisphere by choosing spherical coordinates uniformly 85 | sampling_strategy_spherical, 86 | //! Projected-solid angle sampling in the whole hemisphere 87 | sampling_strategy_psa, 88 | //! BRDF sampling 89 | sampling_strategy_brdf, 90 | //! Next event estimation 91 | sampling_strategy_nee, 92 | //! The number of different sampling strategies 93 | sampling_strategy_count, 94 | } sampling_strategy_t; 95 | 96 | 97 | //! A specification of all the techniques and parameters used to render the 98 | //! scene without specifying the scene itself 99 | typedef struct { 100 | //! The sampling strategy to use for path tracing 101 | sampling_strategy_t sampling_strategy; 102 | //! The maximal number of vertices along a path, excluding the one at the 103 | //! eye 104 | uint32_t path_length; 105 | } render_settings_t; 106 | 107 | 108 | //! Available image file formats for taking screenshots 109 | typedef enum { 110 | //! Portable network graphics using 3*8-bit RGB 111 | image_file_format_png, 112 | //! Joint Photographic Experts Group (JPEG) using 3*8-bit RGB 113 | image_file_format_jpg, 114 | //! High-dynamic range image with 3*32-bit RGB using single-precision 115 | //! floats 116 | image_file_format_hdr, 117 | } image_file_format_t; 118 | 119 | 120 | //! Defines a slide, i.e. a configuration of the renderer that leads to a 121 | //! specific image and can be used in a presentation 122 | typedef struct { 123 | //! The file path to the quicksave that is to be loaded for this slide 124 | char* quicksave; 125 | //! The render settings to use for this slide 126 | render_settings_t render_settings; 127 | //! The file path to which a screenshot should be saved or NULL if no 128 | //! screen shot should be saved 129 | char* screenshot_path; 130 | //! The file format to use for the screenshot 131 | image_file_format_t screenshot_format; 132 | /*! The number of accumulated frames at which the screenshot should be 133 | taken. This essentially controls the sample count. After the screenshot 134 | the slideshow advances to the next slide.*/ 135 | uint32_t screenshot_frame; 136 | } slide_t; 137 | 138 | 139 | //! A set of slides through which the presentation can advance 140 | typedef struct { 141 | //! All slides that make up this slideshow 142 | slide_t slides[MAX_SLIDE_COUNT]; 143 | //! The number of slides 144 | uint32_t slide_count; 145 | //! The first slide that is to be displayed and the slide at which the 146 | //! application should terminate. Equal if no slides are used. 147 | uint32_t slide_begin, slide_end; 148 | //! The slide that is currently being displayed 149 | uint32_t slide_current; 150 | } slideshow_t; 151 | 152 | 153 | //! Parameters for the application 154 | typedef struct { 155 | //! The requested initial extent in pixels for the window. May be ignored 156 | //! initially and may diverge at run time. 157 | VkExtent2D initial_window_extent; 158 | //! Whether screenshots of slides should be taken automatically 159 | bool slide_screenshots; 160 | //! Whether the GUI should be displayed 161 | bool gui; 162 | //! Whether vertical synchronization should be enabled 163 | bool v_sync; 164 | } app_params_t; 165 | 166 | 167 | //! The vertex buffer for GUI rendering is an array of vertices in the 168 | //! following format 169 | typedef struct { 170 | //! The position in pixels from the left top of the viewport 171 | float pos[2]; 172 | //! The texture coordinate for the glyph image 173 | float tex_coord[2]; 174 | //! The color (RGBA) using sRGB 175 | uint8_t color[4]; 176 | //! The left, right, top and bottom of the scissor rectangle 177 | int16_t scissor[4]; 178 | } gui_vertex_t; 179 | 180 | 181 | //! The graphical user interface using Nuklear 182 | typedef struct { 183 | //! The font atlas used for text in the GUI 184 | struct nk_font_atlas atlas; 185 | //! The font used for text in the GUI 186 | struct nk_font* font; 187 | //! The glyph image using VK_FORMAT_R8_UNORM 188 | images_t glyph_image; 189 | //! Points to a white pixel in the glyph image 190 | struct nk_draw_null_texture null_texture; 191 | //! The Nuklear context, which handles persistent state of the GUI 192 | struct nk_context context; 193 | //! The maximal number of triangles supported for the GUI. If more are 194 | //! needed, GUI rendering fails. 195 | uint32_t max_triangle_count; 196 | //! Host-visible versions of the vertex buffer for GUI rendering. This 197 | //! buffer is stored redundantly per frame in flight (a.k.a. workload). 198 | buffers_t staging; 199 | //! A pointer to the mapped memory range for the shared allocation of all 200 | //! staging buffers 201 | void* staging_data; 202 | //! For each staging vertex buffer, this count indicates how many triangles 203 | //! need to be rendered 204 | uint32_t used_triangle_counts[FRAME_IN_FLIGHT_COUNT]; 205 | //! A single device-local buffer that contains the vertex buffer for the 206 | //! frame being rendered, thanks to a copy command at the start of 207 | //! rendering 208 | buffers_t buffer; 209 | } gui_t; 210 | 211 | 212 | //! Defines unique indices for all the render targets handled by 213 | //! render_targets_t 214 | typedef enum { 215 | //! An RGBA render target with single-precision floats at the same 216 | //! resolution as the swapchain to hold HDR radiance 217 | render_target_index_hdr_radiance, 218 | //! A depth buffer with the same resolution as the swapchain 219 | render_target_index_depth_buffer, 220 | //! The number of used render targets 221 | render_target_index_count, 222 | } render_target_index_t; 223 | 224 | 225 | //! Handles all render targets 226 | typedef struct { 227 | //! All the render targets, indexed by render_target_index_t 228 | images_t targets; 229 | //! The number of frames which have been accumulated in the HDR radiance 230 | //! render target 231 | uint32_t accum_frame_count; 232 | } render_targets_t; 233 | 234 | 235 | //! The CPU-side version of the constants defined in constants.glsl. Refer to 236 | //! this file for member documentation. Includes padding as required by GLSL. 237 | typedef struct { 238 | float world_to_projection_space[4 * 4]; 239 | float projection_to_world_space[4 * 4]; 240 | float camera_pos[3]; 241 | int camera_type; 242 | float hemispherical_camera_normal[3]; 243 | float pad_1; 244 | float dequantization_factor[3], pad_2, dequantization_summand[3], pad_3; 245 | float viewport_size[2], inv_viewport_size[2]; 246 | float exposure; 247 | uint32_t frame_index; 248 | uint32_t accum_frame_count; 249 | float pad_4; 250 | float sky_radiance[3]; 251 | float pad_5; 252 | float emission_material_radiance[3]; 253 | float pad_6; 254 | float params[4]; 255 | float spherical_lights[MAX_SPHERICAL_LIGHT_COUNT][4]; 256 | } constants_t; 257 | 258 | 259 | //! Handles all constant buffers that the application works with 260 | typedef struct { 261 | //! Host-visible versions of the constant buffer. These buffers are stored 262 | //! redundantly per frame in flight. 263 | buffers_t staging; 264 | //! A pointer to the mapped memory range for the shared allocation of all 265 | //! staging buffers 266 | void* staging_data; 267 | //! A single device-local buffer that contains the constant data for the 268 | //! frame that is currently being rendered, because at the start of each 269 | //! frame, data from the appropriate staging buffer is copied over. 270 | buffers_t buffer; 271 | } constant_buffers_t; 272 | 273 | 274 | //! The triangle mesh that is being displayed and a specification of the light 275 | //! sources in this scene 276 | typedef struct { 277 | //! The triangle mesh that is being displayed 278 | scene_t scene; 279 | //! The number of spherical lights placed in the scene 280 | uint32_t spherical_light_count; 281 | //! Positions and radii of all spherical lights 282 | float spherical_lights[MAX_SPHERICAL_LIGHT_COUNT][4]; 283 | } lit_scene_t; 284 | 285 | 286 | //! The render pass that performs all rasterization work for rendering one 287 | //! frame of the application and the framebuffers that it uses 288 | typedef struct { 289 | //! The render pass itself 290 | VkRenderPass render_pass; 291 | //! The framebuffer used with render_pass. Duplicated once per swapchain 292 | //! image. 293 | VkFramebuffer* framebuffers; 294 | //! The number of array entries in framebuffers 295 | uint32_t framebuffer_count; 296 | } render_pass_t; 297 | 298 | 299 | //! The objects needed for a subpass that renders the scene 300 | typedef struct { 301 | //! A sampler for material textures 302 | VkSampler sampler; 303 | //! The descriptor set used to render the scene 304 | descriptor_sets_t descriptor_set; 305 | //! A graphics pipelines used to render the scene, which discards the 306 | //! current contents of the render target 307 | VkPipeline pipeline_discard; 308 | //! Like pipeline_discard but adds onto the current contents of the render 309 | //! target for the purpose of progressive rendering 310 | VkPipeline pipeline_accum; 311 | //! The fragment and vertex shaders used to render the scene 312 | VkShaderModule vert_shader, frag_shader; 313 | } scene_subpass_t; 314 | 315 | 316 | //! The objects needed to copy the HDR render target to the screen with tone 317 | //! mapping 318 | typedef struct { 319 | //! The descriptor set used for tone mapping 320 | descriptor_sets_t descriptor_set; 321 | //! The graphics pipeline used for tone mapping 322 | VkPipeline pipeline; 323 | //! The fragment and vertex shaders used for tone mapping 324 | VkShaderModule vert_shader, frag_shader; 325 | } tonemap_subpass_t; 326 | 327 | 328 | //! The objects needed for a subpass that renders the GUI to the screen 329 | typedef struct { 330 | //! A sampler for the glyph image 331 | VkSampler sampler; 332 | //! The descriptor set used to render the GUI 333 | descriptor_sets_t descriptor_set; 334 | //! The graphics pipeline used to render the GUI 335 | VkPipeline pipeline; 336 | //! The fragment and vertex shaders used to render the GUI 337 | VkShaderModule vert_shader, frag_shader; 338 | } gui_subpass_t; 339 | 340 | 341 | //! Indices for timestamp queries in query pools 342 | typedef enum { 343 | //! Encloses the commands that perform the main shading work 344 | timestamp_index_shading_begin, timestamp_index_shading_end, 345 | //! The number of used timestamps 346 | timestamp_index_count, 347 | } timestamp_index_t; 348 | 349 | 350 | //! A command buffer for rendering a single frame and all the synchronization 351 | //! primitives that are needed for it 352 | typedef struct { 353 | //! The command buffer containing all commands for rendering a single 354 | //! frame. It is reused. 355 | VkCommandBuffer cmd; 356 | //! Signaled once an image has been acquired from the swapchain 357 | VkSemaphore image_acquired; 358 | //! Signaled once the command buffer has completed execution 359 | VkSemaphore queue_finished[2]; 360 | //! Signaled once the command buffer has completed execution 361 | VkFence frame_finished; 362 | //! The query pool used to retrieve timestamps for GPU work execution. Its 363 | //! timestamps are indexed by timestamp_index_t. 364 | VkQueryPool query_pool; 365 | } frame_workload_t; 366 | 367 | 368 | //! Handles an array of frame workloads 369 | typedef struct { 370 | //! Before waiting for a fence of a frame workload, the host will submit 371 | //! further frames. The size of this array determines how many. 372 | frame_workload_t frames_in_flight[FRAME_IN_FLIGHT_COUNT]; 373 | //! The number of frames that have been rendered since this object was 374 | //! created. The workload index is frame_index % FRAME_IN_FLIGHT_COUNT. 375 | uint64_t frame_index; 376 | //! The most recently retrieved values of GPU timestamps 377 | uint64_t timestamps[timestamp_index_count]; 378 | } frame_workloads_t; 379 | 380 | 381 | //! All state of the application that has a chance of persisting across a frame 382 | //! is found somewhere in the depths of this structure 383 | typedef struct { 384 | scene_spec_t scene_spec; 385 | render_settings_t render_settings; 386 | slideshow_t slideshow; 387 | //! Parameters specified when running the application. Some of them can be 388 | //! changed continuously. 389 | app_params_t params; 390 | //! The Vulkan device used for rendering and other GPU work 391 | device_t device; 392 | //! The window to which the application renders 393 | GLFWwindow* window; 394 | gui_t gui; 395 | //! The swapchain used to present images to the window 396 | swapchain_t swapchain; 397 | render_targets_t render_targets; 398 | constant_buffers_t constant_buffers; 399 | lit_scene_t lit_scene; 400 | render_pass_t render_pass; 401 | scene_subpass_t scene_subpass; 402 | tonemap_subpass_t tonemap_subpass; 403 | gui_subpass_t gui_subpass; 404 | frame_workloads_t frame_workloads; 405 | } app_t; 406 | 407 | 408 | /*! Holds one boolean per object in app_t that requires work to be freed. If 409 | the boolean is true, the object and all objects that depend on it will be 410 | freed and recreated by update_app().*/ 411 | typedef struct { 412 | bool device, window, gui, swapchain, render_targets, constant_buffers, lit_scene, render_pass, scene_subpass, tonemap_subpass, gui_subpass, frame_workloads; 413 | } app_update_t; 414 | 415 | 416 | //! Temporary objects needed to take a screenshot 417 | typedef struct { 418 | //! The image in host memory to which the off-screen render target is 419 | //! copied 420 | images_t staging; 421 | //! A copy of the image using single-precision floats 422 | float* hdr_copy; 423 | //! A copy of the image using 24-bit LDR colors 424 | uint8_t* ldr_copy; 425 | } screenshot_t; 426 | 427 | 428 | /*! Outputs the name, the path to the *.vks file, to the textures, to the 429 | lights and to the quicksave for the given scene file. Returns 0 upon 430 | success. Output strings must not be freed. Passing NULL for strings that 431 | are not required is legal.*/ 432 | int get_scene_file(scene_file_t scene_file, const char** scene_name, const char** scene_file_path, const char** texture_path, const char** light_path, const char** quicksave_path); 433 | 434 | 435 | //! Saves the scene specification to the quicksave file (overwriting it). 436 | //! Returns 0 upon success. 437 | int quicksave(const scene_spec_t* spec); 438 | 439 | 440 | /*! Loads the scene specification from the quicksave file and flags required 441 | application updates. Pass NULL as save_path for a default. Returns 0 upon 442 | success.*/ 443 | int quickload(scene_spec_t* spec, app_update_t* update, const char* save_path); 444 | 445 | 446 | //! Given an old and a new scene specification, this function marks the updates 447 | //! that need to be performed in response to this change 448 | void get_scene_spec_updates(app_update_t* update, const scene_spec_t* old_spec, const scene_spec_t* new_spec); 449 | 450 | 451 | //! Initializes the given scene specification with defaults or loads it from a 452 | //! quicksave 453 | void init_scene_spec(scene_spec_t* spec); 454 | 455 | 456 | //! Initializes the given render settings with defaults 457 | void init_render_settings(render_settings_t* settings); 458 | 459 | 460 | //! Writes all available slides into the given array and returns the slide 461 | //! count. Defined in slides.c. 462 | uint32_t create_slides(slide_t* slides); 463 | 464 | 465 | //! Creates all slides and starts a slideshow as requested by slide_begin/ 466 | //! slide_end. Returns 0 upon success. 467 | int create_slideshow(slideshow_t* slideshow, scene_spec_t* scene_spec, render_settings_t* render_settings, app_update_t* update); 468 | 469 | 470 | //! Begins showing the slide with the given index and updates app objects 471 | //! accordingly. Returns 0 upon success. 472 | int show_slide(slideshow_t* slideshow, scene_spec_t* scene_spec, render_settings_t* render_settings, app_update_t* update, uint32_t slide_index); 473 | 474 | 475 | //! Frees memory associated with a slideshow 476 | void free_slideshow(slideshow_t* slideshow); 477 | 478 | 479 | //! Creates the application window using the given extent in pixels, returns 0 480 | //! upon success 481 | int create_window(GLFWwindow** window, const VkExtent2D* extent); 482 | 483 | 484 | void free_window(GLFWwindow** window); 485 | 486 | 487 | //! \see gui_t 488 | int create_gui(gui_t* gui, const device_t* device, GLFWwindow* window); 489 | 490 | 491 | //! Feeds user input retrieved via GLFW to the GUI (also polls events) 492 | void handle_gui_input(gui_t* gui, GLFWwindow* window); 493 | 494 | 495 | //! Lets Nuklear produce vertex/index buffers and updates the set of staging 496 | //! buffers for GUI geometry with the given workload index. 497 | int write_gui_geometry(gui_t* gui, const device_t* device, uint32_t workload_index); 498 | 499 | 500 | void free_gui(gui_t* gui, const device_t* device, GLFWwindow* window); 501 | 502 | 503 | //! \see render_targets_t 504 | int create_render_targets(render_targets_t* render_targets, const device_t* device, const swapchain_t* swapchain); 505 | 506 | 507 | void free_render_targets(render_targets_t* render_targets, const device_t* device); 508 | 509 | 510 | //! \see constant_buffers_t 511 | int create_constant_buffers(constant_buffers_t* constant_buffers, const device_t* device); 512 | 513 | 514 | void free_constant_buffers(constant_buffers_t* constant_buffers, const device_t* device); 515 | 516 | 517 | //! Updates constant_buffers->constants based on the state of the app and moves 518 | //! its contents to the staging buffer with the given index instantly 519 | int write_constant_buffer(constant_buffers_t* constant_buffers, const app_t* app, uint32_t buffer_index); 520 | 521 | 522 | //! Forwards to load_scene() using parameters that are appropriate for the 523 | //! given scene specification and additionally loads light sources 524 | int create_lit_scene(lit_scene_t* lit_scene, const device_t* device, const scene_spec_t* scene_spec); 525 | 526 | 527 | void free_lit_scene(lit_scene_t* lit_scene, const device_t* device); 528 | 529 | 530 | //! \see render_pass_t 531 | int create_render_pass(render_pass_t* render_pass, const device_t* device, const swapchain_t* swapchain, const render_targets_t* targets); 532 | 533 | 534 | void free_render_pass(render_pass_t* render_pass, const device_t* device); 535 | 536 | 537 | //! \see scene_subpass_t 538 | int create_scene_subpass(scene_subpass_t* subpass, const device_t* device, const scene_spec_t* scene_spec, const render_settings_t* render_settings, const swapchain_t* swapchain, const constant_buffers_t* constant_buffers, const lit_scene_t* lit_scene, const render_pass_t* render_pass); 539 | 540 | 541 | void free_scene_subpass(scene_subpass_t* subpass, const device_t* device); 542 | 543 | 544 | //! \see tonemap_subpass_t 545 | int create_tonemap_subpass(tonemap_subpass_t* subpass, const device_t* device, const render_targets_t* render_targets, const constant_buffers_t* constant_buffers, const render_pass_t* render_pass, const scene_spec_t* scene_spec); 546 | 547 | 548 | void free_tonemap_subpass(tonemap_subpass_t* subpass, const device_t* device); 549 | 550 | 551 | //! \see gui_subpass_t 552 | int create_gui_subpass(gui_subpass_t* subpass, const device_t* device, const gui_t* gui, const swapchain_t* swapchain, const constant_buffers_t* constant_buffers, const render_pass_t* render_pass); 553 | 554 | 555 | void free_gui_subpass(gui_subpass_t* subpass, const device_t* device); 556 | 557 | 558 | //! \see frame_workloads_t 559 | int create_frame_workloads(frame_workloads_t* workloads, const device_t* device); 560 | 561 | 562 | void free_frame_workloads(frame_workloads_t* workloads, const device_t* device); 563 | 564 | 565 | //! Returns true iff anything is marked for update. 566 | bool update_needed(const app_update_t* update); 567 | 568 | 569 | /*! Whenever objects in app_t are created, recreated or freed, this function 570 | triggers it. 571 | \param app The app in which objects will be freed (if necessary) and 572 | (re-)created if requested. 573 | \param update All objects marked in this struct will be freed (if 574 | necessary) and (re-)created (if requested). Additionally, all objects 575 | which depend on freed objects will be freed (if necessary) and 576 | (re-)created (if requested). 577 | \param recreate false to only free objects, true to ensure that upon 578 | success all objects in app_t have been created. 579 | \return 0 upon success. With recreate == false, success is guaranteed. Upon 580 | failure, the return code of the create function that has failed is 581 | returned.*/ 582 | int update_app(app_t* app, const app_update_t* update, bool recreate); 583 | 584 | 585 | /*! Copies in the given objects (if any), initializes scene specification and 586 | render settings and forwards to update_app() to create all other objects in 587 | the given app.*/ 588 | int create_app(app_t* app, const app_params_t* app_params, const slideshow_t* slideshow); 589 | 590 | 591 | //! Short-hand to use update_app() to free all objects in the given app 592 | void free_app(app_t* app); 593 | 594 | 595 | //! \return true iff the key with the given GLFW_KEY_* keycode is pressed now 596 | //! but was not pressed at the last query (or there was no last query) 597 | bool key_pressed(GLFWwindow* window, int key_code); 598 | 599 | 600 | /*! Responds to user input, updating the state of the given app accordingly and 601 | triggering all sorts of requested actions. Invoke this once per frame. 602 | \param app The app whose state is to be updated. 603 | \param update Used to report required updates. 604 | \return 0 iff the application should keep running.*/ 605 | int handle_user_input(app_t* app, app_update_t* update); 606 | 607 | 608 | /*! Defines the GUI for one frame with all of its windows and elements and 609 | updates application state. 610 | \param ctx The Nuklear context to be used for the GUI. 611 | \param scene_spec Scene specification to be modified. 612 | \param render_settings Render settings to be modified. 613 | \param update Used to report required updates. 614 | \param render_targets Used to query the sample count. 615 | \param timestamps The timestamps from frame_workloads_t. 616 | \param timestamp_period The value from VkPhysicalDeviceLimits::timestampPeriod.*/ 617 | void define_gui(struct nk_context* ctx, scene_spec_t* scene_spec, render_settings_t* render_settings, app_update_t* update, const render_targets_t* render_targets, uint64_t timestamps[timestamp_index_count], float timestamp_period); 618 | 619 | 620 | /*! Updates constant buffers, takes care of synchronization, renders a single 621 | frame and presents it. 622 | \param app The app for which a frame is being rendered. 623 | \param update If something goes wrong with the swapchain, it is flagged for 624 | an update but the application does not terminate. 625 | \return 0 upon success, error code of the failed Vulkan function on 626 | failure (or 1 for non-Vulkan errors).*/ 627 | VkResult render_frame(app_t* app, app_update_t* update); 628 | 629 | 630 | /*! Saves a screenshot reflecting the state of the off-screen render target 631 | (without GUI) to a file. 632 | \param file_path The path to the output file. 633 | \param format The image file format to be used to store the image. For LDR 634 | formats, a basic tone mapping is performed. 635 | \param device Output of create_device(). 636 | \param render_targets The HDR radiance render target from this object is 637 | stored. 638 | \param scene_spec The scene specification with tone mapping parameters. 639 | \return 0 upon success.*/ 640 | int save_screenshot(const char* file_path, image_file_format_t format, const device_t* device, const render_targets_t* render_targets, const scene_spec_t* scene_spec); 641 | 642 | 643 | void free_screenshot(screenshot_t* screenshot, const device_t* device); 644 | -------------------------------------------------------------------------------- /src/math_utilities.c: -------------------------------------------------------------------------------- 1 | #include "math_utilities.h" 2 | #include 3 | 4 | 5 | uint32_t greatest_common_divisor(uint32_t a, uint32_t b) { 6 | // Use the Euclidean algorithm 7 | while (b != 0) { 8 | uint32_t t = b; 9 | b = a % b; 10 | a = t; 11 | } 12 | return a; 13 | } 14 | 15 | 16 | void mat_mat_mul(float* out, const float* lhs, const float* rhs, uint32_t m, uint32_t k, uint32_t n) { 17 | for (uint32_t i = 0; i != m; ++i) { 18 | for (uint32_t j = 0; j != n; ++j) { 19 | float entry = 0.0; 20 | for (uint32_t l = 0; l != k; ++l) { 21 | entry += lhs[i * k + l] * rhs[l * n + j]; 22 | } 23 | out[i * n + j] = entry; 24 | } 25 | } 26 | } 27 | 28 | 29 | bool normalize(float* vec, uint32_t entry_count) { 30 | float length_sq = 0.0f; 31 | for (uint32_t i = 0; i != entry_count; ++i) 32 | length_sq += vec[i] * vec[i]; 33 | if (length_sq != 0.0) { 34 | float scale = 1.0f / sqrtf(length_sq); 35 | for (uint32_t i = 0; i != entry_count; ++i) 36 | vec[i] *= scale; 37 | return true; 38 | } 39 | return false; 40 | } 41 | 42 | 43 | void rotation_matrix_from_angles(float out_rotation[3 * 3], const float angles[3]) { 44 | float sins[3] = { sinf(angles[0]), sinf(angles[1]), sinf(angles[2]) }; 45 | float coss[3] = { cosf(angles[0]), cosf(angles[1]), cosf(angles[2]) }; 46 | float rot_x[3 * 3] = { 47 | 1.0f, 0.0f, 0.0f, 48 | 0.0f, coss[0], sins[0], 49 | 0.0f, -sins[0], coss[0], 50 | }; 51 | float rot_y[3 * 3] = { 52 | coss[1], 0.0f, sins[1], 53 | 0.0f, 1.0f, 0.0f, 54 | -sins[1], 0.0f, coss[1], 55 | }; 56 | float rot_z[3 * 3] = { 57 | coss[2], sins[2], 0.0f, 58 | -sins[2], coss[2], 0.0f, 59 | 0.0f, 0.0f, 1.0f, 60 | }; 61 | float rot_xy[3 * 3]; 62 | mat_mat_mul(rot_xy, rot_y, rot_x, 3, 3, 3); 63 | mat_mat_mul(out_rotation, rot_z, rot_xy, 3, 3, 3); 64 | } 65 | 66 | 67 | static inline float determinant_mat4(const float mat[4 * 4]) { 68 | float result; 69 | result = 24.0f * mat[0] * mat[5] * mat[10] * mat[15]; 70 | result -= 24.0f * mat[0] * mat[5] * mat[11] * mat[14]; 71 | result -= 24.0f * mat[0] * mat[6] * mat[9] * mat[15]; 72 | result += 24.0f * mat[0] * mat[6] * mat[11] * mat[13]; 73 | result += 24.0f * mat[0] * mat[7] * mat[9] * mat[14]; 74 | result -= 24.0f * mat[0] * mat[7] * mat[10] * mat[13]; 75 | result -= 24.0f * mat[1] * mat[4] * mat[10] * mat[15]; 76 | result += 24.0f * mat[1] * mat[4] * mat[11] * mat[14]; 77 | result += 24.0f * mat[1] * mat[6] * mat[8] * mat[15]; 78 | result -= 24.0f * mat[1] * mat[6] * mat[11] * mat[12]; 79 | result -= 24.0f * mat[1] * mat[7] * mat[8] * mat[14]; 80 | result += 24.0f * mat[1] * mat[7] * mat[10] * mat[12]; 81 | result += 24.0f * mat[2] * mat[4] * mat[9] * mat[15]; 82 | result -= 24.0f * mat[2] * mat[4] * mat[11] * mat[13]; 83 | result -= 24.0f * mat[2] * mat[5] * mat[8] * mat[15]; 84 | result += 24.0f * mat[2] * mat[5] * mat[11] * mat[12]; 85 | result += 24.0f * mat[2] * mat[7] * mat[8] * mat[13]; 86 | result -= 24.0f * mat[2] * mat[7] * mat[9] * mat[12]; 87 | result -= 24.0f * mat[3] * mat[4] * mat[9] * mat[14]; 88 | result += 24.0f * mat[3] * mat[4] * mat[10] * mat[13]; 89 | result += 24.0f * mat[3] * mat[5] * mat[8] * mat[14]; 90 | result -= 24.0f * mat[3] * mat[5] * mat[10] * mat[12]; 91 | result -= 24.0f * mat[3] * mat[6] * mat[8] * mat[13]; 92 | result += 24.0f * mat[3] * mat[6] * mat[9] * mat[12]; 93 | return result; 94 | } 95 | 96 | 97 | static inline void adjoint_mat4(float result[4 * 4], const float mat[4 * 4]) { 98 | result[15] = 6.0f * mat[0] * mat[5] * mat[10]; 99 | result[11] = -6.0f * mat[0] * mat[5] * mat[11]; 100 | result[15] -= 6.0f * mat[0] * mat[6] * mat[9]; 101 | result[7] = 6.0f * mat[0] * mat[6] * mat[11]; 102 | result[11] += 6.0f * mat[0] * mat[7] * mat[9]; 103 | result[7] -= 6.0f * mat[0] * mat[7] * mat[10]; 104 | result[15] -= 6.0f * mat[1] * mat[4] * mat[10]; 105 | result[11] += 6.0f * mat[1] * mat[4] * mat[11]; 106 | result[15] += 6.0f * mat[1] * mat[6] * mat[8]; 107 | result[3] = -6.0f * mat[1] * mat[6] * mat[11]; 108 | result[11] -= 6.0f * mat[1] * mat[7] * mat[8]; 109 | result[3] += 6.0f * mat[1] * mat[7] * mat[10]; 110 | result[15] += 6.0f * mat[2] * mat[4] * mat[9]; 111 | result[7] -= 6.0f * mat[2] * mat[4] * mat[11]; 112 | result[15] -= 6.0f * mat[2] * mat[5] * mat[8]; 113 | result[3] += 6.0f * mat[2] * mat[5] * mat[11]; 114 | result[7] += 6.0f * mat[2] * mat[7] * mat[8]; 115 | result[3] -= 6.0f * mat[2] * mat[7] * mat[9]; 116 | result[11] -= 6.0f * mat[3] * mat[4] * mat[9]; 117 | result[7] += 6.0f * mat[3] * mat[4] * mat[10]; 118 | result[11] += 6.0f * mat[3] * mat[5] * mat[8]; 119 | result[3] -= 6.0f * mat[3] * mat[5] * mat[10]; 120 | result[7] -= 6.0f * mat[3] * mat[6] * mat[8]; 121 | result[3] += 6.0f * mat[3] * mat[6] * mat[9]; 122 | result[14] = -6.0f * mat[0] * mat[5] * mat[14]; 123 | result[10] = 6.0f * mat[0] * mat[5] * mat[15]; 124 | result[14] += 6.0f * mat[0] * mat[6] * mat[13]; 125 | result[6] = -6.0f * mat[0] * mat[6] * mat[15]; 126 | result[10] -= 6.0f * mat[0] * mat[7] * mat[13]; 127 | result[6] += 6.0f * mat[0] * mat[7] * mat[14]; 128 | result[14] += 6.0f * mat[1] * mat[4] * mat[14]; 129 | result[10] -= 6.0f * mat[1] * mat[4] * mat[15]; 130 | result[14] -= 6.0f * mat[1] * mat[6] * mat[12]; 131 | result[2] = 6.0f * mat[1] * mat[6] * mat[15]; 132 | result[10] += 6.0f * mat[1] * mat[7] * mat[12]; 133 | result[2] -= 6.0f * mat[1] * mat[7] * mat[14]; 134 | result[14] -= 6.0f * mat[2] * mat[4] * mat[13]; 135 | result[6] += 6.0f * mat[2] * mat[4] * mat[15]; 136 | result[14] += 6.0f * mat[2] * mat[5] * mat[12]; 137 | result[2] -= 6.0f * mat[2] * mat[5] * mat[15]; 138 | result[6] -= 6.0f * mat[2] * mat[7] * mat[12]; 139 | result[2] += 6.0f * mat[2] * mat[7] * mat[13]; 140 | result[10] += 6.0f * mat[3] * mat[4] * mat[13]; 141 | result[6] -= 6.0f * mat[3] * mat[4] * mat[14]; 142 | result[10] -= 6.0f * mat[3] * mat[5] * mat[12]; 143 | result[2] += 6.0f * mat[3] * mat[5] * mat[14]; 144 | result[6] += 6.0f * mat[3] * mat[6] * mat[12]; 145 | result[2] -= 6.0f * mat[3] * mat[6] * mat[13]; 146 | result[13] = 6.0f * mat[0] * mat[9] * mat[14]; 147 | result[9] = -6.0f * mat[0] * mat[9] * mat[15]; 148 | result[13] -= 6.0f * mat[0] * mat[10] * mat[13]; 149 | result[5] = 6.0f * mat[0] * mat[10] * mat[15]; 150 | result[9] += 6.0f * mat[0] * mat[11] * mat[13]; 151 | result[5] -= 6.0f * mat[0] * mat[11] * mat[14]; 152 | result[13] -= 6.0f * mat[1] * mat[8] * mat[14]; 153 | result[9] += 6.0f * mat[1] * mat[8] * mat[15]; 154 | result[13] += 6.0f * mat[1] * mat[10] * mat[12]; 155 | result[1] = -6.0f * mat[1] * mat[10] * mat[15]; 156 | result[9] -= 6.0f * mat[1] * mat[11] * mat[12]; 157 | result[1] += 6.0f * mat[1] * mat[11] * mat[14]; 158 | result[13] += 6.0f * mat[2] * mat[8] * mat[13]; 159 | result[5] -= 6.0f * mat[2] * mat[8] * mat[15]; 160 | result[13] -= 6.0f * mat[2] * mat[9] * mat[12]; 161 | result[1] += 6.0f * mat[2] * mat[9] * mat[15]; 162 | result[5] += 6.0f * mat[2] * mat[11] * mat[12]; 163 | result[1] -= 6.0f * mat[2] * mat[11] * mat[13]; 164 | result[9] -= 6.0f * mat[3] * mat[8] * mat[13]; 165 | result[5] += 6.0f * mat[3] * mat[8] * mat[14]; 166 | result[9] += 6.0f * mat[3] * mat[9] * mat[12]; 167 | result[1] -= 6.0f * mat[3] * mat[9] * mat[14]; 168 | result[5] -= 6.0f * mat[3] * mat[10] * mat[12]; 169 | result[1] += 6.0f * mat[3] * mat[10] * mat[13]; 170 | result[12] = -6.0f * mat[4] * mat[9] * mat[14]; 171 | result[8] = 6.0f * mat[4] * mat[9] * mat[15]; 172 | result[12] += 6.0f * mat[4] * mat[10] * mat[13]; 173 | result[4] = -6.0f * mat[4] * mat[10] * mat[15]; 174 | result[8] -= 6.0f * mat[4] * mat[11] * mat[13]; 175 | result[4] += 6.0f * mat[4] * mat[11] * mat[14]; 176 | result[12] += 6.0f * mat[5] * mat[8] * mat[14]; 177 | result[8] -= 6.0f * mat[5] * mat[8] * mat[15]; 178 | result[12] -= 6.0f * mat[5] * mat[10] * mat[12]; 179 | result[0] = 6.0f * mat[5] * mat[10] * mat[15]; 180 | result[8] += 6.0f * mat[5] * mat[11] * mat[12]; 181 | result[0] -= 6.0f * mat[5] * mat[11] * mat[14]; 182 | result[12] -= 6.0f * mat[6] * mat[8] * mat[13]; 183 | result[4] += 6.0f * mat[6] * mat[8] * mat[15]; 184 | result[12] += 6.0f * mat[6] * mat[9] * mat[12]; 185 | result[0] -= 6.0f * mat[6] * mat[9] * mat[15]; 186 | result[4] -= 6.0f * mat[6] * mat[11] * mat[12]; 187 | result[0] += 6.0f * mat[6] * mat[11] * mat[13]; 188 | result[8] += 6.0f * mat[7] * mat[8] * mat[13]; 189 | result[4] -= 6.0f * mat[7] * mat[8] * mat[14]; 190 | result[8] -= 6.0f * mat[7] * mat[9] * mat[12]; 191 | result[0] += 6.0f * mat[7] * mat[9] * mat[14]; 192 | result[4] += 6.0f * mat[7] * mat[10] * mat[12]; 193 | result[0] -= 6.0f * mat[7] * mat[10] * mat[13]; 194 | } 195 | 196 | 197 | void invert_mat4(float out_inv[4 * 4], float mat[4 * 4]) { 198 | float det = determinant_mat4(mat); 199 | adjoint_mat4(out_inv, mat); 200 | float scale = 4.0f / det; 201 | for (uint32_t i = 0; i != 4 * 4; ++i) 202 | out_inv[i] *= scale; 203 | } 204 | -------------------------------------------------------------------------------- /src/math_utilities.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | 5 | #ifndef M_PI 6 | #define M_PI 3.14159265358979323846 7 | #endif 8 | 9 | 10 | //! \return The greatest common divisor of the given two integers 11 | uint32_t greatest_common_divisor(uint32_t a, uint32_t b); 12 | 13 | 14 | //! \return The least common multiple of the given two integers 15 | static inline uint32_t least_common_multiple(uint32_t a, uint32_t b) { 16 | uint32_t gcd = greatest_common_divisor(a, b); 17 | // This formulation avoids overflow 18 | return (a / gcd) * b; 19 | } 20 | 21 | 22 | /*! Computes the product of two matrices. All matrices use row-major order. 23 | \param out An m x n matrix to which the product is written. Must not point 24 | to the same location as lhs or rhs. 25 | \param lhs An m x k matrix as left-hand side in the product. 26 | \param rhs A k x n matrix as right-hand side in the product.*/ 27 | void mat_mat_mul(float* out, const float* lhs, const float* rhs, uint32_t m, uint32_t k, uint32_t n); 28 | 29 | 30 | //! Multiplies an m x n matrix (row-major) by an n-entry column vector 31 | //! to output an m-entry vector (non-overlapping with the inputs). 32 | static inline void mat_vec_mul(float* out, const float* mat, const float* vec, uint32_t m, uint32_t n) { 33 | mat_mat_mul(out, mat, vec, m, n, 1); 34 | } 35 | 36 | 37 | //! Normalizes the given vector with respect to the 2-norm in place 38 | //! \return false if the squared length underflowed to zero, true otherwise 39 | bool normalize(float* vec, uint32_t entry_count); 40 | 41 | 42 | /*! Constructs a 3x3 rotation matrix (row-major, to be multiplied onto a column 43 | vector from the left) from angles in radians. The rotations are performed 44 | around x-, y- and z-axis, respectively and are applied in this order.*/ 45 | void rotation_matrix_from_angles(float out_rotation[3 * 3], const float angles[3]); 46 | 47 | 48 | //! Computes the inverse of a 4x4 matrix (row major) 49 | void invert_mat4(float out_inv[4 * 4], float mat[4 * 4]); 50 | 51 | 52 | //! A helper for half_to_float() to modify floats bit by bit 53 | typedef union { 54 | uint32_t u; 55 | float f; 56 | } float32_union_t; 57 | 58 | 59 | /*! Converts a half-precision float (given as an integer with same binary 60 | representation) into a single-precision float with identical value. 61 | Originally half_to_float_fast5() as proposed by Fabian Giesen, see: 62 | https://fgiesen.wordpress.com/2012/03/28/half-to-float-done-quic/ */ 63 | static inline float half_to_float(uint16_t half) { 64 | static const float32_union_t magic = { (254 - 15) << 23 }; 65 | static const float32_union_t was_infnan = { (127 + 16) << 23 }; 66 | float32_union_t o; 67 | // Copy exponent and mantissa bits 68 | o.u = (half & 0x7fff) << 13; 69 | // Make adjustments to the exponent 70 | o.f *= magic.f; 71 | // Make sure that inf/NaN survive 72 | if (o.f >= was_infnan.f) 73 | o.u |= 255 << 23; 74 | // Copy the sign bit 75 | o.u |= (half & 0x8000) << 16; 76 | return o.f; 77 | } 78 | -------------------------------------------------------------------------------- /src/nuklear.c: -------------------------------------------------------------------------------- 1 | #define NK_IMPLEMENTATION 2 | #include "nuklear.h" 3 | -------------------------------------------------------------------------------- /src/nuklear.h: -------------------------------------------------------------------------------- 1 | #define NK_INCLUDE_DEFAULT_ALLOCATOR 2 | #define NK_INCLUDE_STANDARD_BOOL 3 | #define NK_INCLUDE_STANDARD_IO 4 | #define NK_INCLUDE_STANDARD_VARARGS 5 | #define NK_INCLUDE_FIXED_TYPES 6 | #define NK_INCLUDE_VERTEX_BUFFER_OUTPUT 7 | #define NK_INCLUDE_FONT_BAKING 8 | #define NK_ZERO_COMMAND_MEMORY 9 | #define NK_UINT_DRAW_INDEX 10 | #define NK_KEYSTATE_BASED_INPUT 11 | #include "../ext/nuklear.h" 12 | -------------------------------------------------------------------------------- /src/scene.c: -------------------------------------------------------------------------------- 1 | #include "scene.h" 2 | #include "textures.h" 3 | #include "string_utilities.h" 4 | #include 5 | #include 6 | #include 7 | 8 | 9 | //! Holds a pointer to a scene and temporary objects needed to load it 10 | typedef struct { 11 | //! The device that is being used to load the scene 12 | const device_t* device; 13 | //! A pointer to the scene being loaded or NULL once it is loaded 14 | scene_t* scene; 15 | //! A temporary copy of the quantized positions 16 | uint32_t* quantized_poss; 17 | //! One buffer for geometry/instance data per BVH level 18 | buffers_t geometry_buffers; 19 | //! Scratch memory buffers for acceleration structure build 20 | buffers_t scratch_buffers; 21 | //! The command buffer used to record build commands 22 | VkCommandBuffer cmd; 23 | //! The file from which the scene is being loaded 24 | FILE* file; 25 | } scene_loader_t; 26 | 27 | 28 | void free_scene_loader(scene_loader_t* loader, const device_t* device); 29 | 30 | 31 | //! Callback for fill_buffers() to write geometry data for BVHs 32 | void write_geometry_buffers(void* buffer_data, uint32_t buffer_index, VkDeviceSize buffer_size, const void* context) { 33 | const scene_loader_t* loader = (const scene_loader_t*) context; 34 | const scene_t* scene = loader->scene; 35 | const device_t* device = loader->device; 36 | switch (buffer_index) { 37 | case bvh_level_bottom: { 38 | // Dequantize all vertex positions 39 | scene_file_header_t header = scene->header; 40 | uint32_t vertex_count = 3 * scene->header.triangle_count; 41 | const uint32_t* src = loader->quantized_poss; 42 | float* dst = (float*) buffer_data; 43 | for (uint32_t i = 0; i != vertex_count; ++i) { 44 | uint32_t a = src[2 * i + 0]; 45 | uint32_t b = src[2 * i + 1]; 46 | float pos[3] = { 47 | (float) (a & 0x1fffff), 48 | (float) (((a & 0xffe00000) >> 21) | ((b & 0x3ff) << 11)), 49 | (float) ((b & 0x7ffffc00) >> 10), 50 | }; 51 | for (uint32_t j = 0; j != 3; ++j) { 52 | (*dst) = pos[j] * header.dequantization_factor[j] + header.dequantization_summand[j]; 53 | ++dst; 54 | } 55 | } 56 | break; 57 | } 58 | case bvh_level_top: { 59 | // A single instance of the bottom level with identity transform 60 | VK_LOAD(vkGetAccelerationStructureDeviceAddressKHR); 61 | VkAccelerationStructureDeviceAddressInfoKHR address_info = { 62 | .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_DEVICE_ADDRESS_INFO_KHR, 63 | .accelerationStructure = scene->bvhs.bvhs[bvh_level_bottom], 64 | }; 65 | VkAccelerationStructureInstanceKHR instance = { 66 | .accelerationStructureReference = (*pvkGetAccelerationStructureDeviceAddressKHR)(loader->device->device, &address_info), 67 | .transform = { .matrix = { 68 | { 1.0f, 0.0f, 0.0f, 0.0f, }, 69 | { 0.0f, 1.0f, 0.0f, 0.0f, }, 70 | { 0.0f, 0.0f, 1.0f, 0.0f, }, 71 | } 72 | }, 73 | .mask = 0xFF, 74 | .flags = VK_GEOMETRY_INSTANCE_TRIANGLE_FACING_CULL_DISABLE_BIT_KHR | VK_GEOMETRY_INSTANCE_FORCE_OPAQUE_BIT_KHR, 75 | }; 76 | memcpy(buffer_data, &instance, sizeof(instance)); 77 | break; 78 | } 79 | default: 80 | break; 81 | } 82 | } 83 | 84 | 85 | /*! Creates a BVH for a scene being loaded. 86 | \param loader An active scene loader with quantized positions readily 87 | available. The calling side is responsible for freeing it. 88 | \param device Output of create_device. 89 | \return 0 upon success.*/ 90 | int create_bvh(scene_loader_t* loader, const device_t* device) { 91 | bvhs_t* bvhs = &loader->scene->bvhs; 92 | VK_LOAD(vkGetAccelerationStructureBuildSizesKHR); 93 | VK_LOAD(vkCreateAccelerationStructureKHR); 94 | VK_LOAD(vkCmdBuildAccelerationStructuresKHR); 95 | // Map levels to BVH types 96 | VkAccelerationStructureTypeKHR types[bvh_level_count]; 97 | types[bvh_level_bottom] = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR; 98 | types[bvh_level_top] = VK_ACCELERATION_STRUCTURE_TYPE_TOP_LEVEL_KHR; 99 | // Create buffers for geometry/instance data 100 | uint32_t triangle_count = loader->scene->header.triangle_count; 101 | uint32_t vertex_count = 3 * triangle_count; 102 | buffer_request_t geo_requests[bvh_level_count]; 103 | buffer_request_t bottom_geo_request = { 104 | .buffer_info = { 105 | .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, 106 | .size = vertex_count * 3 * sizeof(float), 107 | .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, 108 | }, 109 | }; 110 | geo_requests[bvh_level_bottom] = bottom_geo_request; 111 | buffer_request_t top_geo_request = { 112 | .buffer_info = { 113 | .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, 114 | .size = sizeof(VkAccelerationStructureInstanceKHR), 115 | .usage = bottom_geo_request.buffer_info.usage, 116 | }, 117 | }; 118 | geo_requests[bvh_level_top] = top_geo_request; 119 | if (create_buffers(&loader->geometry_buffers, device, geo_requests, COUNT_OF(geo_requests), VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 1)) 120 | return printf("Failed to create buffers to hold the geometry data for building an acceleration structure.\n"); 121 | // Define characteristics of the geometry for both BVH levels 122 | VkAccelerationStructureGeometryKHR geometries[bvh_level_count]; 123 | VkBufferDeviceAddressInfo bottom_buffer_address = { 124 | .sType = VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, 125 | .buffer = loader->geometry_buffers.buffers[bvh_level_bottom].buffer, 126 | }; 127 | VkAccelerationStructureGeometryKHR bottom_geometry = { 128 | .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_KHR, 129 | .flags = VK_GEOMETRY_OPAQUE_BIT_KHR, 130 | .geometryType = VK_GEOMETRY_TYPE_TRIANGLES_KHR, 131 | .geometry = { 132 | .triangles = { 133 | .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_TRIANGLES_DATA_KHR, 134 | .vertexData = { .deviceAddress = vkGetBufferDeviceAddress(device->device, &bottom_buffer_address) }, 135 | .maxVertex = vertex_count - 1, 136 | .vertexStride = 3 * sizeof(float), 137 | .vertexFormat = VK_FORMAT_R32G32B32_SFLOAT, 138 | .indexType = VK_INDEX_TYPE_NONE_KHR, 139 | }, 140 | }, 141 | }; 142 | geometries[bvh_level_bottom] = bottom_geometry; 143 | VkBufferDeviceAddressInfo top_buffer_address = { 144 | .sType = VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, 145 | .buffer = loader->geometry_buffers.buffers[bvh_level_top].buffer, 146 | }; 147 | VkAccelerationStructureGeometryKHR top_geometry = { 148 | .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_KHR, 149 | .flags = VK_GEOMETRY_OPAQUE_BIT_KHR, 150 | .geometryType = VK_GEOMETRY_TYPE_INSTANCES_KHR, 151 | .geometry = { 152 | .instances = { 153 | .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_INSTANCES_DATA_KHR, 154 | .arrayOfPointers = VK_FALSE, 155 | .data = { .deviceAddress = vkGetBufferDeviceAddress(device->device, &top_buffer_address) }, 156 | }, 157 | }, 158 | }; 159 | geometries[bvh_level_top] = top_geometry; 160 | // Figure out size requirements for acceleration structures 161 | VkAccelerationStructureBuildSizesInfoKHR sizes[bvh_level_count]; 162 | memset(sizes, 0, sizeof(sizes)); 163 | uint32_t primitive_counts[bvh_level_count]; 164 | primitive_counts[bvh_level_bottom] = triangle_count; 165 | primitive_counts[bvh_level_top] = 1; 166 | VkAccelerationStructureBuildGeometryInfoKHR build_infos[bvh_level_count]; 167 | for (uint32_t i = 0; i != bvh_level_count; ++i) { 168 | VkAccelerationStructureBuildGeometryInfoKHR build_info = { 169 | .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_GEOMETRY_INFO_KHR, 170 | .type = types[i], 171 | .flags = VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR, 172 | .mode = VK_BUILD_ACCELERATION_STRUCTURE_MODE_BUILD_KHR, 173 | .geometryCount = 1, 174 | .pGeometries = &geometries[i], 175 | }; 176 | build_infos[i] = build_info; 177 | sizes[i].sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_SIZES_INFO_KHR; 178 | (*pvkGetAccelerationStructureBuildSizesKHR)(device->device, VK_ACCELERATION_STRUCTURE_BUILD_TYPE_DEVICE_KHR, &build_infos[i], &primitive_counts[i], &sizes[i]); 179 | } 180 | // Create buffers to hold acceleration structures 181 | buffer_request_t bvh_buffer_requests[bvh_level_count]; 182 | for (int i = 0; i != bvh_level_count; ++i) { 183 | buffer_request_t request = { 184 | .buffer_info = { 185 | .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, 186 | .size = sizes[i].accelerationStructureSize, 187 | .usage = VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_STORAGE_BIT_KHR | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, 188 | }, 189 | }; 190 | bvh_buffer_requests[i] = request; 191 | } 192 | if (create_buffers(&bvhs->buffers, device, bvh_buffer_requests, COUNT_OF(bvh_buffer_requests), VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 1)) 193 | return printf("Failed to create buffers to hold acceleration structures.\n"); 194 | // Create acceleration structures 195 | for (uint32_t i = 0; i != bvh_level_count; ++i) { 196 | VkAccelerationStructureCreateInfoKHR bvh_info = { 197 | .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_CREATE_INFO_KHR, 198 | .buffer = bvhs->buffers.buffers[i].buffer, 199 | .size = sizes[i].accelerationStructureSize, 200 | .type = types[i], 201 | }; 202 | if ((*pvkCreateAccelerationStructureKHR)(device->device, &bvh_info, NULL, &loader->scene->bvhs.bvhs[i])) 203 | return printf("Failed to create an acceleration structure.\n"); 204 | } 205 | // Now that we can get a pointer to the bottom-level for the top-level, 206 | // fill the geometry buffers 207 | if (fill_buffers(&loader->geometry_buffers, device, &write_geometry_buffers, loader)) 208 | return printf("Failed to upload geometry data for building acceleration structures to the GPU.\n"); 209 | // Create buffers for scratch data 210 | buffer_request_t scratch_requests[bvh_level_count]; 211 | for (int i = 0; i != bvh_level_count; ++i) { 212 | buffer_request_t request = { 213 | .buffer_info = { 214 | .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, 215 | .size = sizes[i].buildScratchSize, 216 | .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, 217 | }, 218 | }; 219 | scratch_requests[i] = request; 220 | } 221 | if (create_buffers(&loader->scratch_buffers, device, scratch_requests, COUNT_OF(scratch_requests), VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, device->bvh_properties.minAccelerationStructureScratchOffsetAlignment)) 222 | return printf("Failed to create scratch buffers for the acceleration structure build.\n"); 223 | 224 | // Prepare to record commands 225 | VkCommandBufferAllocateInfo cmd_info = { 226 | .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, 227 | .commandPool = device->cmd_pool, 228 | .commandBufferCount = 1, 229 | }; 230 | if (vkAllocateCommandBuffers(device->device, &cmd_info, &loader->cmd)) 231 | return printf("Failed to create a command buffer to record acceleration structure build commands.\n"); 232 | VkCommandBufferBeginInfo begin_info = { .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; 233 | if (vkBeginCommandBuffer(loader->cmd, &begin_info)) 234 | return printf("Failed to begin recording a command buffer for building acceleration structures.\n"); 235 | // Build bottom- and top-level acceleration structures in this order 236 | for (uint32_t i = 0; i != bvh_level_count; ++i) { 237 | build_infos[i].dstAccelerationStructure = bvhs->bvhs[i]; 238 | VkBufferDeviceAddressInfo scratch_adress_info = { 239 | .sType = VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, 240 | .buffer = loader->scratch_buffers.buffers[i].buffer, 241 | }; 242 | build_infos[i].scratchData.deviceAddress = vkGetBufferDeviceAddress(device->device, &scratch_adress_info); 243 | VkAccelerationStructureBuildRangeInfoKHR build_range = { .primitiveCount = primitive_counts[i] }; 244 | const VkAccelerationStructureBuildRangeInfoKHR* build_range_ptr = &build_range; 245 | pvkCmdBuildAccelerationStructuresKHR(loader->cmd, 1, &build_infos[i], &build_range_ptr); 246 | // Enforce synchronization 247 | VkMemoryBarrier after_build_barrier = { 248 | .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, 249 | .srcAccessMask = VK_ACCESS_ACCELERATION_STRUCTURE_WRITE_BIT_KHR, 250 | .dstAccessMask = VK_ACCESS_ACCELERATION_STRUCTURE_READ_BIT_KHR, 251 | }; 252 | vkCmdPipelineBarrier(loader->cmd, VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR, 253 | VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR, 0, 254 | 1, &after_build_barrier, 0, NULL, 0, NULL); 255 | } 256 | // Submit the command buffer 257 | VkSubmitInfo cmd_submit = { 258 | .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, 259 | .commandBufferCount = 1, .pCommandBuffers = &loader->cmd, 260 | }; 261 | if (vkEndCommandBuffer(loader->cmd) || vkQueueSubmit(device->queue, 1, &cmd_submit, NULL)) 262 | return printf("Failed to end and submit the command buffer for building acceleration structures.\n"); 263 | return 0; 264 | } 265 | 266 | 267 | //! Callback for fill_buffers() that writes mesh data from the scene file 268 | //! directly to staging buffers 269 | void write_mesh_buffer(void* buffer_data, uint32_t buffer_index, VkDeviceSize buffer_size, const void* context) { 270 | const scene_loader_t* loader = (const scene_loader_t*) context; 271 | if (buffer_index == mesh_buffer_type_positions) { 272 | // Create a temporary copy of the quantized positions for acceleration 273 | // structure build 274 | fread(loader->quantized_poss, sizeof(uint8_t), buffer_size, loader->file); 275 | memcpy(buffer_data, loader->quantized_poss, buffer_size); 276 | } 277 | else 278 | fread(buffer_data, sizeof(uint8_t), buffer_size, loader->file); 279 | } 280 | 281 | 282 | int load_scene(scene_t* scene, const device_t* device, const char* file_path, const char* texture_path) { 283 | memset(scene, 0, sizeof(*scene)); 284 | scene_loader_t loader = { .scene = scene, .device = device }; 285 | // Open the source file 286 | FILE* file = loader.file = fopen(file_path, "rb"); 287 | if (!file) { 288 | printf("Failed to open the scene file at %s. Please check path and permissions.\n", file_path); 289 | free_scene_loader(&loader, device); 290 | return 1; 291 | } 292 | // Load the header 293 | fread(&scene->header.marker, sizeof(uint32_t), 1, file); 294 | if (scene->header.marker != 0xabcabc) { 295 | printf("The scene file at %s is not a valid *.vks file. Its marker does not match.\n", file_path); 296 | free_scene_loader(&loader, device); 297 | return 1; 298 | } 299 | fread(&scene->header.version, sizeof(uint32_t), 1, file); 300 | if (scene->header.version != 1) { 301 | printf("This renderer only supports *.vks files using version 1 of the file format, but the scene file at %s uses version %u.\n", file_path, scene->header.version); 302 | free_scene_loader(&loader, device); 303 | return 1; 304 | } 305 | fread(&scene->header.material_count, sizeof(uint64_t), 1, file); 306 | fread(&scene->header.triangle_count, sizeof(uint64_t), 1, file); 307 | fread(&scene->header.dequantization_factor, sizeof(float), 3, file); 308 | fread(&scene->header.dequantization_summand, sizeof(float), 3, file); 309 | // Load material names 310 | scene->header.material_names = calloc(scene->header.material_count, sizeof(char*)); 311 | for (uint64_t i = 0; i != scene->header.material_count; ++i) { 312 | uint64_t length = 0; 313 | fread(&length, sizeof(length), 1, file); 314 | scene->header.material_names[i] = malloc(length + 1); 315 | fread(scene->header.material_names[i], sizeof(char), length + 1, file); 316 | } 317 | // Create buffers for the geometry 318 | buffer_request_t buffer_requests[mesh_buffer_type_count]; 319 | memset(buffer_requests, 0, sizeof(buffer_requests)); 320 | for (uint32_t i = 0; i != mesh_buffer_type_count; ++i) { 321 | VkBufferCreateInfo* buffer_info = &buffer_requests[i].buffer_info; 322 | buffer_info->sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; 323 | buffer_info->usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT; 324 | if (i == mesh_buffer_type_positions || i == mesh_buffer_type_normals_and_tex_coords) { 325 | buffer_info->usage |= VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; 326 | buffer_info->size = sizeof(uint32_t) * 2 * scene->header.triangle_count * 3; 327 | } 328 | else if (i == mesh_buffer_type_material_indices) 329 | buffer_info->size = sizeof(uint8_t) * scene->header.triangle_count; 330 | VkBufferViewCreateInfo* view_info = &buffer_requests[i].view_info; 331 | view_info->sType = VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO; 332 | switch (i) { 333 | case mesh_buffer_type_positions: 334 | view_info->format = VK_FORMAT_R32G32_UINT; 335 | break; 336 | case mesh_buffer_type_normals_and_tex_coords: 337 | view_info->format = VK_FORMAT_R16G16B16A16_UNORM; 338 | break; 339 | case mesh_buffer_type_material_indices: 340 | view_info->format = VK_FORMAT_R8_UINT; 341 | break; 342 | default: 343 | view_info->sType = 0; 344 | break; 345 | } 346 | } 347 | if (create_buffers(&scene->mesh_buffers, device, buffer_requests, mesh_buffer_type_count, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 1)) { 348 | printf("Failed to create geometry buffers for the scene file at %s with %lu triangles.\n", file_path, scene->header.triangle_count); 349 | free_scene_loader(&loader, device); 350 | return 1; 351 | } 352 | // While filling buffers, we keep quantized positions around for acceleration structure build 353 | loader.quantized_poss = malloc(buffer_requests[mesh_buffer_type_positions].buffer_info.size); 354 | // Fill the geometry buffers with data from the file 355 | if (fill_buffers(&scene->mesh_buffers, device, &write_mesh_buffer, &loader)) { 356 | printf("Failed to write mesh data of the scene file at %s to device-local buffers.\n", file_path); 357 | free_scene_loader(&loader, device); 358 | return 1; 359 | } 360 | // If everything went well, we have reached an end of file marker now 361 | uint32_t eof_marker = 0; 362 | fread(&eof_marker, sizeof(eof_marker), 1, file); 363 | if (eof_marker != 0xe0fe0f) { 364 | printf("Finished reading data from the scene file at %s but did not encounter an end-of-file marker where expected. Either the file is invalid or the loader is buggy.\n", file_path); 365 | free_scene_loader(&loader, device); 366 | return 1; 367 | } 368 | // Close the scene file 369 | fclose(file); 370 | file = loader.file = NULL; 371 | // Build acceleration structures 372 | if (create_bvh(&loader, device)) { 373 | printf("Failed to create ray-tracing acceleration structures for the scene file at %s.\n", file_path); 374 | free_scene_loader(&loader, device); 375 | return 1; 376 | } 377 | // Load textures 378 | VkDeviceSize texture_count = scene->header.material_count * material_texture_type_count; 379 | char** texture_file_paths = calloc(texture_count, sizeof(char*)); 380 | const char* suffixes[material_texture_type_count]; 381 | suffixes[material_texture_type_base_color] = "_BaseColor.vkt"; 382 | suffixes[material_texture_type_specular] = "_Specular.vkt"; 383 | suffixes[material_texture_type_normal] = "_Normal.vkt"; 384 | for (VkDeviceSize i = 0; i != scene->header.material_count; ++i) { 385 | for (uint32_t j = 0; j != material_texture_type_count; ++j) { 386 | const char* parts[] = { texture_path, "/", scene->header.material_names[i], suffixes[j] }; 387 | texture_file_paths[i * material_texture_type_count + j] = cat_strings(parts, COUNT_OF(parts)); 388 | } 389 | } 390 | int result = load_textures(&scene->textures, device, (const char* const*) texture_file_paths, (uint32_t) texture_count, VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); 391 | for (VkDeviceSize i = 0; i != texture_count; ++i) 392 | free(texture_file_paths[i]); 393 | free(texture_file_paths); 394 | texture_file_paths = NULL; 395 | if (result) { 396 | printf("Failed to load textures for the scene file at %s.\n", file_path); 397 | free_scene_loader(&loader, device); 398 | return 1; 399 | } 400 | // Tidy up temporary objects created whilst loading 401 | loader.scene = NULL; 402 | free_scene_loader(&loader, device); 403 | return 0; 404 | } 405 | 406 | 407 | void free_scene(scene_t* scene, const device_t* device) { 408 | VK_LOAD(vkDestroyAccelerationStructureKHR); 409 | free_images(&scene->textures, device); 410 | free_buffers(&scene->mesh_buffers, device); 411 | if (scene->header.material_names) 412 | for (uint64_t i = 0; i != scene->header.material_count; ++i) 413 | free(scene->header.material_names[i]); 414 | for (uint32_t i = 0; i != bvh_level_count; ++i) 415 | if (scene->bvhs.bvhs[i]) 416 | (*pvkDestroyAccelerationStructureKHR)(device->device, scene->bvhs.bvhs[i], NULL); 417 | free_buffers(&scene->bvhs.buffers, device); 418 | free(scene->header.material_names); 419 | memset(scene, 0, sizeof(*scene)); 420 | } 421 | 422 | 423 | void free_scene_loader(scene_loader_t* loader, const device_t* device) { 424 | if (loader->scene) free_scene(loader->scene, device); 425 | free(loader->quantized_poss); 426 | if (loader->file) fclose(loader->file); 427 | free_buffers(&loader->geometry_buffers, device); 428 | free_buffers(&loader->scratch_buffers, device); 429 | if (loader->cmd) vkFreeCommandBuffers(device->device, device->cmd_pool, 1, &loader->cmd); 430 | } 431 | -------------------------------------------------------------------------------- /src/scene.h: -------------------------------------------------------------------------------- 1 | #include "vulkan_basics.h" 2 | 3 | 4 | //! Holds all header data for a scene file 5 | typedef struct { 6 | //! Should be 0xabcabc to mark a file as the right file format 7 | uint32_t marker; 8 | //! Should be 1 for a static scene 9 | uint32_t version; 10 | //! The number of unique materials 11 | uint64_t material_count; 12 | //! The number of triangles 13 | uint64_t triangle_count; 14 | //! Component-wise multiplication of quantized integer xyz coordinates by 15 | //! these factors followed by addition of these summands gives world-space 16 | //! coordinates 17 | float dequantization_factor[3], dequantization_summand[3]; 18 | //! An array holding material_count null-terminated strings for material 19 | //! names (with UTF-8 encoding) 20 | char** material_names; 21 | } scene_file_header_t; 22 | 23 | 24 | //! Enumeration of the different buffers that are needed to store a mesh 25 | typedef enum { 26 | //! Each position is stored in 64 bits using 21-bit quantization for each 27 | //! coordinate. Three subsequent positions form a triangle. 28 | mesh_buffer_type_positions, 29 | /*! Per position, this buffer provides 64 bits for the corresponding normal 30 | and texture coordinate. The normal uses 2x16 bits for an octahedral map 31 | the texture coordinate 2x16 bits for fixed-point values from 0 to 8.*/ 32 | mesh_buffer_type_normals_and_tex_coords, 33 | //! An 8-bit index per triangle indicating the used material 34 | mesh_buffer_type_material_indices, 35 | //! The number of valid values for this enum 36 | mesh_buffer_type_count, 37 | } mesh_buffer_type_t; 38 | 39 | 40 | //! Each material is defined completely by exactly three textures, as listed in 41 | //! this enumeration 42 | typedef enum { 43 | /*! The diffuse albedo is derived from this base color. For metallic 44 | materials, the base color also controls the specular albedo. The format 45 | is usually VK_FORMAT_BC1_RGB_SRGB_BLOCK.*/ 46 | material_texture_type_base_color, 47 | /*! A texture with three parameters controlling the specular BRDF. The 48 | format is usually VK_FORMAT_BC1_RGB_UNORM_BLOCK. 49 | R: An occlusion coefficient that is not used by the renderer currently, 50 | G: An artist-friendly roughness parameter between zero and one. 51 | B: Metallicity, i.e. 0 for dielectrics and 1 for metals.*/ 52 | material_texture_type_specular, 53 | /*! The normal vector of the surface in tangent space using Cartesian 54 | coordinates. It uses unsigned values such that the geometric normal is 55 | (0.5, 0.5, 1). The format is usually VK_FORMAT_BC5_UNORM_BLOCK.*/ 56 | material_texture_type_normal, 57 | //! The number of available texture types 58 | material_texture_type_count, 59 | } material_texture_type_t; 60 | 61 | 62 | //! The levels of acceleration structures in Vulkan (top and bottom level) 63 | typedef enum { 64 | //! Bottom-level acceleration structure (BLAS) 65 | bvh_level_bottom, 66 | //! Top-level acceleration structure (TLAS) 67 | bvh_level_top, 68 | //! Number of levels 69 | bvh_level_count, 70 | } bvh_level_t; 71 | 72 | 73 | //! Holds all acceleration structures for a scene (also known as bounding 74 | //! volume hierarchies, or BVH for short) 75 | typedef struct { 76 | //! The top- and bottom-level acceleration structure 77 | VkAccelerationStructureKHR bvhs[bvh_level_count]; 78 | //! The buffers that hold the acceleration structures 79 | buffers_t buffers; 80 | } bvhs_t; 81 | 82 | 83 | //! A scene that has been loaded from a scene file and is now device-local 84 | typedef struct { 85 | //! Header data as it was found in the scene file 86 | scene_file_header_t header; 87 | //! mesh_buffer_type_count buffers providing geometry information 88 | buffers_t mesh_buffers; 89 | //! material_texture_type_count consecutive textures per material 90 | images_t textures; 91 | //! The ray-tracing acceleration structures 92 | bvhs_t bvhs; 93 | } scene_t; 94 | 95 | 96 | /*! Loads a scene file and copies its data to device-local memory (i.e. to the 97 | VRAM of the GPU). 98 | \param scene The output. Clean up with free_scene(). 99 | \param device Output of create_device(). 100 | \param file_path Path to a *.vks file that is to be loaded. 101 | \param texture_path Path to a directory containing texture files in the 102 | *.vkt format. 103 | \return 0 upon success.*/ 104 | int load_scene(scene_t* scene, const device_t* device, const char* file_path, const char* texture_path); 105 | 106 | 107 | void free_scene(scene_t* scene, const device_t* device); 108 | -------------------------------------------------------------------------------- /src/shaders/brdfs.glsl: -------------------------------------------------------------------------------- 1 | #include "shading_data.glsl" 2 | 3 | #define M_PI 3.141592653589793238462643 4 | 5 | 6 | //! The Fresnel-Schlick approximation for the Fresnel term 7 | vec3 fresnel_schlick(vec3 f_0, vec3 f_90, float lambert) { 8 | float flip_1 = 1.0 - lambert; 9 | float flip_2 = flip_1 * flip_1; 10 | float flip_5 = flip_2 * flip_1 * flip_2; 11 | return flip_5 * (f_90 - f_0) + f_0; 12 | } 13 | 14 | 15 | /*! Evaluates the Frostbite BRDF as described at the links below for the given 16 | shading point and normalized incoming light direction: 17 | https://dl.acm.org/doi/abs/10.1145/2614028.2615431 18 | https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf */ 19 | vec3 frostbite_brdf(shading_data_t s, vec3 in_dir) { 20 | // The BRDF is zero in the lower hemisphere 21 | float lambert_in = dot(s.normal, in_dir); 22 | if (min(lambert_in, s.lambert_out) < 0.0) 23 | return vec3(0.0); 24 | // It uses the half-vector between incoming and outgoing light direction 25 | vec3 half_dir = normalize(in_dir + s.out_dir); 26 | float half_dot_out = dot(half_dir, s.out_dir); 27 | // This is the Disney diffuse BRDF 28 | float f_90 = (half_dot_out * half_dot_out) * (2.0 * s.roughness) + 0.5; 29 | float fresnel_diffuse = 30 | fresnel_schlick(vec3(1.0), vec3(f_90), s.lambert_out).x * 31 | fresnel_schlick(vec3(1.0), vec3(f_90), lambert_in).x; 32 | vec3 brdf = fresnel_diffuse * s.diffuse_albedo; 33 | // The Frostbite specular BRDF uses the GGX normal distribution function... 34 | float half_dot_normal = dot(half_dir, s.normal); 35 | float roughness_2 = s.roughness * s.roughness; 36 | float ggx = (roughness_2 * half_dot_normal - half_dot_normal) * half_dot_normal + 1.0; 37 | ggx = roughness_2 / (ggx * ggx); 38 | // ... Smith masking-shadowing... 39 | float masking = lambert_in * sqrt((s.lambert_out - roughness_2 * s.lambert_out) * s.lambert_out + roughness_2); 40 | float shadowing = s.lambert_out * sqrt((lambert_in - roughness_2 * lambert_in) * lambert_in + roughness_2); 41 | float smith = 0.5 / (masking + shadowing); 42 | // ... and the Fresnel-Schlick approximation 43 | vec3 fresnel = fresnel_schlick(s.fresnel_0, vec3(1.0), max(0.0, half_dot_out)); 44 | brdf += ggx * smith * fresnel; 45 | return brdf * (1.0 / M_PI); 46 | } 47 | 48 | 49 | /*! Samples the distribution of visible normals in the GGX normal distribution 50 | function for the given outgoing direction. All vectors must be in a 51 | coordinate frame where the shading normal is (0.0, 0.0, 1.0). 52 | \param out_dir Direction towards the camera along the path. Need not be 53 | normalized. 54 | \param roughness The roughness parameter of GGX for the local x- and 55 | y-axis. 56 | \param randoms A point distributed uniformly in [0,1)^2. 57 | \return The normalized half vector (*not* the incoming direction).*/ 58 | vec3 sample_ggx_vndf(vec3 out_dir, vec2 roughness, vec2 randoms) { 59 | // This implementation is based on: 60 | // https://gist.github.com/jdupuy/4c6e782b62c92b9cb3d13fbb0a5bd7a0 61 | // It is described in detail here: 62 | // https://doi.org/10.1111/cgf.14867 63 | 64 | // Warp to the hemisphere configuration 65 | vec3 out_dir_std = normalize(vec3(out_dir.xy * roughness, out_dir.z)); 66 | // Sample a spherical cap in (-out_dir_std.z, 1] 67 | float azimuth = (2.0 * M_PI) * randoms[0] - M_PI; 68 | float z = 1.0 - randoms[1] * (1.0 + out_dir_std.z); 69 | float sine = sqrt(max(0.0, 1.0 - z * z)); 70 | vec3 cap = vec3(sine * cos(azimuth), sine * sin(azimuth), z); 71 | // Compute the half vector in the hemisphere configuration 72 | vec3 half_dir_std = cap + out_dir_std; 73 | // Warp back to the ellipsoid configuration 74 | return normalize(vec3(half_dir_std.xy * roughness, half_dir_std.z)); 75 | } 76 | 77 | 78 | /*! Computes the density that is sampled by sample_ggx_vndf(). 79 | \param lambert_out Dot product between the shading normal and the outgoing 80 | light direction. 81 | \param half_dot_normal Dot product between the sampled half vector and 82 | the shading normal. 83 | \param half_dot_out Dot product between the sampled half vector and the 84 | outgoing light direction. 85 | \param roughness The GGX roughness parameter (along both x- and y-axis). 86 | \return The density w.r.t. solid angle for the sampled half vector (*not* 87 | for the incoming direction).*/ 88 | float get_ggx_vndf_density(float lambert_out, float half_dot_normal, float half_dot_out, float roughness) { 89 | // Based on Equation 2 in this paper: https://doi.org/10.1111/cgf.14867 90 | // A few factors have been cancelled to optimize evaluation. 91 | if (half_dot_normal < 0.0) 92 | return 0.0; 93 | float roughness_2 = roughness * roughness; 94 | float flip_roughness_2 = 1.0 - roughness * roughness; 95 | float length_M_inv_out_2 = roughness_2 + flip_roughness_2 * lambert_out * lambert_out; 96 | float D_vis_std = max(0.0, half_dot_out) * (2.0 / M_PI) / (lambert_out + sqrt(length_M_inv_out_2)); 97 | float length_M_half_2 = 1.0 - flip_roughness_2 * half_dot_normal * half_dot_normal; 98 | return D_vis_std * roughness_2 / (length_M_half_2 * length_M_half_2); 99 | } 100 | 101 | 102 | //! Forwards to sample_ggx_vndf() and constructs a local reflection vector from 103 | //! the result which can be used as incoming light direction. 104 | vec3 sample_ggx_in_dir(vec3 out_dir, float roughness, vec2 randoms) { 105 | vec3 half_dir = sample_ggx_vndf(out_dir, vec2(roughness), randoms); 106 | return -reflect(out_dir, half_dir); 107 | } 108 | 109 | 110 | //! Returns the density w.r.t. solid angle that is sampled by 111 | //! sample_ggx_in_dir(). Most parameters come from shading_data_t. 112 | float get_ggx_in_dir_density(float lambert_out, vec3 out_dir, vec3 in_dir, vec3 normal, float roughness) { 113 | vec3 half_dir = normalize(in_dir + out_dir); 114 | float half_dot_out = dot(half_dir, out_dir); 115 | float half_dot_normal = dot(half_dir, normal); 116 | // Compute the density of the half vector 117 | float density = get_ggx_vndf_density(lambert_out, half_dot_normal, half_dot_out, roughness); 118 | // Account for the mirroring in the density 119 | density /= 4.0 * half_dot_out; 120 | return density; 121 | } 122 | 123 | 124 | //! Constructs a special orthogonal matrix where the given normalized normal 125 | //! vector is the third column 126 | mat3 get_shading_space(vec3 n) { 127 | // This implementation is from: https://www.jcgt.org/published/0006/01/01/ 128 | float s = (n.z > 0.0) ? 1.0 : -1.0; 129 | float a = -1.0 / (s + n.z); 130 | float b = n.x * n.y * a; 131 | vec3 b1 = vec3(1.0 + s * n.x * n.x * a, s * b, -s * n.x); 132 | vec3 b2 = vec3(b, s + n.y * n.y * a, -n.y); 133 | return mat3(b1, b2, n); 134 | } 135 | 136 | 137 | //! Produces a sample distributed uniformly with respect to projected solid 138 | //! angle (PSA) in the upper hemisphere (positive z). 139 | vec3 sample_hemisphere_psa(vec2 randoms) { 140 | // Sample a disk uniformly 141 | float azimuth = (2.0 * M_PI) * randoms[0] - M_PI; 142 | float radius = sqrt(randoms[1]); 143 | // Project to the hemisphere 144 | float z = sqrt(1.0 - radius * radius); 145 | return vec3(radius * cos(azimuth), radius * sin(azimuth), z); 146 | } 147 | 148 | 149 | //! Returns the density w.r.t. solid angle sampled by sample_hemisphere_psa(). 150 | //! It only needs the z-coordinate of the sampled direction (in shading space). 151 | float get_hemisphere_psa_density(float sampled_dir_z) { 152 | return (1.0 / M_PI) * max(0.0, sampled_dir_z); 153 | } 154 | 155 | 156 | //! Heuristically computes a probability with which projected solid angle 157 | //! sampling should be used when sampling the BRDF for the given shading point. 158 | float get_diffuse_sampling_probability(shading_data_t s) { 159 | // In principle we use the luminance of the diffuse albedo. But in specular 160 | // highlights, the specular component is much more important than the 161 | // diffuse component. Thus, we always sample it at least 50% of the time in 162 | // the spirit of defensive sampling. 163 | return min(0.5, dot(s.diffuse_albedo, vec3(0.2126, 0.7152, 0.0722))); 164 | } 165 | 166 | 167 | /*! Samples an incoming light direction using sampling strategies that provide 168 | good importance sampling for the Frostbite BRDF times cosine (namely 169 | projected solid angle sampling and GGX VNDF sampling with probabilities as 170 | indicated by get_diffuse_sampling_probability()). 171 | \param s Complete shading data. 172 | \param randoms A uniformly distributed point in [0,1)^2. 173 | \return A normalized direction vector for the sampled direction.*/ 174 | vec3 sample_frostbite_brdf(shading_data_t s, vec2 randoms) { 175 | mat3 shading_to_world_space = get_shading_space(s.normal); 176 | // Decide stochastically whether to sample the specular or the diffuse 177 | // component 178 | float diffuse_prob = get_diffuse_sampling_probability(s); 179 | bool diffuse = randoms[0] < diffuse_prob; 180 | // Sample the direction and determine the density according to a 181 | // single-sample MIS estimate with the balance heuristic 182 | float density; 183 | vec3 sampled_dir; 184 | if (diffuse) { 185 | // Reuse the random number 186 | randoms[0] /= diffuse_prob; 187 | // Sample the projected solid angle uniformly 188 | sampled_dir = shading_to_world_space * sample_hemisphere_psa(randoms); 189 | } 190 | else { 191 | // Reuse the random number 192 | randoms[0] = (randoms[0] - diffuse_prob) / (1.0 - diffuse_prob); 193 | // Sample the half-vector from the GGX VNDF 194 | vec3 local_out_dir = transpose(shading_to_world_space) * s.out_dir; 195 | vec3 local_in_dir = sample_ggx_in_dir(local_out_dir, s.roughness, randoms); 196 | sampled_dir = shading_to_world_space * local_in_dir; 197 | } 198 | return sampled_dir; 199 | } 200 | 201 | 202 | //! Returns the density w.r.t. solid angle sampled by sample_frostbite_brdf(). 203 | float get_frostbite_brdf_density(shading_data_t s, vec3 sampled_dir) { 204 | float diffuse_prob = get_diffuse_sampling_probability(s); 205 | float specular_density = get_ggx_in_dir_density(s.lambert_out, s.out_dir, sampled_dir, s.normal, s.roughness); 206 | float diffuse_density = get_hemisphere_psa_density(dot(s.normal, sampled_dir)); 207 | return mix(specular_density, diffuse_density, diffuse_prob); 208 | } 209 | -------------------------------------------------------------------------------- /src/shaders/camera_utilities.glsl: -------------------------------------------------------------------------------- 1 | /*! Computes a point on the near clipping plane for a camera defined using a 2 | homogeneous transform. 3 | \param ray_tex_coord The screen-space location of the point in a coordinate 4 | frame where the left top of the viewport is (0, 0), the right top is 5 | (1, 0) and the right bottom (1, 1). 6 | \param proj_to_world_space The projection to world space transform of 7 | the camera (to be multiplied from the left). 8 | \return The ray origin on the near clipping plane in world space.*/ 9 | vec3 get_camera_ray_origin(vec2 ray_tex_coord, mat4 proj_to_world_space) { 10 | vec4 pos_0_proj = vec4(2.0 * ray_tex_coord - vec2(1.0), 0.0, 1.0); 11 | vec4 pos_0_world = proj_to_world_space * pos_0_proj; 12 | return pos_0_world.xyz / pos_0_world.w; 13 | } 14 | 15 | 16 | /*! Computes a ray direction for a camera defined using a homogeneous 17 | transform. 18 | \param ray_tex_coord The screen-space location of the point in a coordinate 19 | frame where the left top of the viewport is (0, 0), the right top is 20 | (1, 0) and the right bottom (1, 1). 21 | \param world_to_proj_space The world to projection space transform of the 22 | camera (to be multiplied from the left). 23 | \return The normalized world-space ray direction for the camera ray.*/ 24 | vec3 get_camera_ray_direction(vec2 ray_tex_coord, mat4 world_to_proj_space) { 25 | vec2 dir_proj = 2.0 * ray_tex_coord - vec2(1.0); 26 | vec3 ray_dir; 27 | // This implementation is a bit fancy. The code is automatically generated 28 | // but the derivation goes as follows: Construct Pluecker coordinates of 29 | // the ray in projection space, transform those to world space, then take 30 | // the intersection with the plane at infinity. Of course, the parts that 31 | // only operate on world_to_proj_space could be pre-computed, but 24 fused 32 | // multiply-adds is not so bad and I like how broadly applicable this 33 | // implementation is. 34 | ray_dir.x = (world_to_proj_space[1][1] * world_to_proj_space[2][3] - world_to_proj_space[1][3] * world_to_proj_space[2][1]) * dir_proj.x; 35 | ray_dir.x += (world_to_proj_space[1][3] * world_to_proj_space[2][0] - world_to_proj_space[1][0] * world_to_proj_space[2][3]) * dir_proj.y; 36 | ray_dir.x += world_to_proj_space[1][0] * world_to_proj_space[2][1] - world_to_proj_space[1][1] * world_to_proj_space[2][0]; 37 | ray_dir.y = (world_to_proj_space[0][3] * world_to_proj_space[2][1] - world_to_proj_space[0][1] * world_to_proj_space[2][3]) * dir_proj.x; 38 | ray_dir.y += (world_to_proj_space[0][0] * world_to_proj_space[2][3] - world_to_proj_space[0][3] * world_to_proj_space[2][0]) * dir_proj.y; 39 | ray_dir.y += world_to_proj_space[0][1] * world_to_proj_space[2][0] - world_to_proj_space[0][0] * world_to_proj_space[2][1]; 40 | ray_dir.z = (world_to_proj_space[0][1] * world_to_proj_space[1][3] - world_to_proj_space[0][3] * world_to_proj_space[1][1]) * dir_proj.x; 41 | ray_dir.z += (world_to_proj_space[0][3] * world_to_proj_space[1][0] - world_to_proj_space[0][0] * world_to_proj_space[1][3]) * dir_proj.y; 42 | ray_dir.z += world_to_proj_space[0][0] * world_to_proj_space[1][1] - world_to_proj_space[0][1] * world_to_proj_space[1][0]; 43 | return normalize(ray_dir); 44 | } 45 | -------------------------------------------------------------------------------- /src/shaders/constants.glsl: -------------------------------------------------------------------------------- 1 | layout (row_major, std140, binding = 0) uniform constants { 2 | //! The world to projection space transform 3 | mat4 g_world_to_projection_space; 4 | //! The projection to world space transform 5 | mat4 g_projection_to_world_space; 6 | //! The camera position in world-space coordinates 7 | vec3 g_camera_pos; 8 | //! The type of camera to be used, as indicated by the camera_type_t enum 9 | int g_camera_type; 10 | //! The normalized normal to feed to get_shading_space() to obtain a local 11 | //! frame for a projection based on spherical coordinates. 12 | vec3 g_hemispherical_camera_normal; 13 | //! Component-wise multiplication of quantized integer xyz coordinates by 14 | //! these factors followed by addition of these summands gives world-space 15 | //! coordinates of positions in the loaded scene 16 | vec3 g_dequantization_factor, g_dequantization_summand; 17 | //! The width and height of the viewport 18 | vec2 g_viewport_size; 19 | //! The reciprocal width and height of the viewport 20 | vec2 g_inv_viewport_size; 21 | //! The factor by which the HDR radiance is scaled during tonemapping 22 | float g_exposure; 23 | //! The index of the frame being rendered for the purpose of random seed 24 | //! generation 25 | uint g_frame_index; 26 | //! The number of frames which have been accumulated in the HDR radiance 27 | //! render target. Divided out during tone mapping. 28 | uint g_accum_frame_count; 29 | //! The radiance for rays that leave the scene (using Rec. 709, a.k.a. 30 | //! linear sRGB) 31 | vec3 g_sky_radiance; 32 | //! The radiance emitted by the material called _emission (Rec. 709) 33 | vec3 g_emission_material_radiance; 34 | //! Four floats that can be controlled from the GUI directly and can be 35 | //! used for any purpose while developing shaders 36 | vec4 g_params; 37 | //! Positions and radii for all spherical lights 38 | vec4 g_spherical_lights[32]; 39 | }; 40 | -------------------------------------------------------------------------------- /src/shaders/gui.frag.glsl: -------------------------------------------------------------------------------- 1 | #version 460 2 | #extension GL_GOOGLE_include_directive : enable 3 | #include "srgb_utility.glsl" 4 | 5 | //! The glyph image produced by Nuklear 6 | layout (binding = 1) uniform sampler2D g_glyph_image; 7 | 8 | //! The texture coordinate to use to access the glyph image 9 | layout (location = 0) smooth in vec2 g_tex_coord; 10 | 11 | //! The sRGB color with alpha multiplier 12 | layout (location = 1) smooth in vec4 g_color; 13 | 14 | //! The left, top, right and bottom of the scissor rectangle in pixels from the 15 | //! left top of the viewport 16 | layout (location = 2) flat in ivec4 g_scissor; 17 | 18 | 19 | //! The sRGB color and alpha to draw to the screen 20 | layout (location = 0) out vec4 g_out_color; 21 | 22 | 23 | void main() { 24 | // Apply the scissor rectangle 25 | vec4 scissor = vec4(g_scissor); 26 | if (gl_FragCoord.x < scissor.x || gl_FragCoord.x > scissor.z || gl_FragCoord.y < scissor.y || gl_FragCoord.y > scissor.w) 27 | discard; 28 | // Sample the texture and produce the output color 29 | float alpha = texture(g_glyph_image, g_tex_coord).r; 30 | g_out_color = vec4(srgb_to_linear_rgb(g_color.rgb), g_color.a * alpha); 31 | } 32 | -------------------------------------------------------------------------------- /src/shaders/gui.vert.glsl: -------------------------------------------------------------------------------- 1 | #version 460 2 | #extension GL_GOOGLE_include_directive : enable 3 | #include "constants.glsl" 4 | 5 | //! The vertex position in pixels from the left top of the viewport 6 | layout (location = 0) in vec2 g_pos; 7 | 8 | //! The texture coordinate for the glyph image 9 | layout (location = 1) in vec2 g_tex_coord; 10 | 11 | //! The sRGB color and alpha 12 | layout (location = 2) in vec4 g_color; 13 | 14 | //! The left, top, right and bottom of the scissor rectangle in pixels from the 15 | //! left top of the viewport 16 | layout (location = 3) in ivec4 g_scissor; 17 | 18 | 19 | //! Passed through texture coordinate 20 | layout (location = 0) smooth out vec2 g_out_tex_coord; 21 | 22 | //! Passed through color 23 | layout (location = 1) smooth out vec4 g_out_color; 24 | 25 | //! Passed through scissor 26 | layout (location = 2) flat out ivec4 g_out_scissor; 27 | 28 | 29 | void main() { 30 | gl_Position = vec4(g_pos * g_inv_viewport_size * 2.0 - vec2(1.0), 0.5, 1.0); 31 | g_out_tex_coord = g_tex_coord; 32 | g_out_color = g_color; 33 | g_out_scissor = g_scissor; 34 | } 35 | -------------------------------------------------------------------------------- /src/shaders/mesh_quantization.glsl: -------------------------------------------------------------------------------- 1 | //! Given a quantized 64-bit position as used in the *.vks scene file format 2 | //! and corresponding dequantization constants, this function produces a 3 | //! floating-point world-space position. 4 | vec3 dequantize_position(uvec2 quantized_position, vec3 dequantization_factor, vec3 dequantization_summand) { 5 | vec3 position = vec3( 6 | bitfieldExtract(quantized_position[0], 0, 21), 7 | bitfieldExtract(quantized_position[0], 21, 11) | ((quantized_position[1] & 0x3FF) << 11), 8 | bitfieldExtract(quantized_position[1], 10, 21) 9 | ); 10 | return fma(position, dequantization_factor, dequantization_summand); 11 | } 12 | 13 | 14 | //! Given two coordinates of a normal vector between 0 and 1, this function 15 | //! produces a normalized floating-point world-space normal 16 | vec3 dequantize_normal(vec2 octahedral_normal) { 17 | // The *.vks format specifies normals in such a way that the coordinate 18 | // zero can be represented exactly. Because of that, -1 corresponds to the 19 | // second-smallest fixed-point number. 20 | const float factor = 2.0 * (65534.0 / 65535.0); 21 | const float summand = -(32768.0 / 65535.0) * factor; 22 | octahedral_normal = fma(octahedral_normal, vec2(factor), vec2(summand)); 23 | // Undo the octahedral map 24 | vec3 normal = vec3(octahedral_normal.xy, 1.0 - abs(octahedral_normal.x) - abs(octahedral_normal.y)); 25 | vec2 non_zero_sign = vec2( 26 | (octahedral_normal.x >= 0.0) ? 1.0 : -1.0, 27 | (octahedral_normal.y >= 0.0) ? 1.0 : -1.0 28 | ); 29 | normal.xy = (normal.z < 0.0) ? ((1.0 - abs(normal.yx)) * non_zero_sign) : normal.xy; 30 | return normalize(normal); 31 | } 32 | -------------------------------------------------------------------------------- /src/shaders/pathtrace.frag.glsl: -------------------------------------------------------------------------------- 1 | #version 460 2 | #extension GL_GOOGLE_include_directive : enable 3 | #extension GL_EXT_nonuniform_qualifier : enable 4 | #extension GL_EXT_control_flow_attributes : enable 5 | #extension GL_EXT_ray_query : enable 6 | #include "camera_utilities.glsl" 7 | // Lots of other includes and bindings come indirectly through this one 8 | #include "brdfs.glsl" 9 | 10 | 11 | //! The BVH containing all scene geometry 12 | layout(binding = 2) uniform accelerationStructureEXT g_bvh; 13 | 14 | 15 | //! The outgoing radiance towards the camera as sRGB color 16 | layout (location = 0) out vec4 g_out_color; 17 | 18 | 19 | /*! Generates a pair of pseudo-random numbers. 20 | \param seed Integers that change with each invocation. They get updated so 21 | that you can reuse them. 22 | \return A uniform, pseudo-random point in [0,1)^2.*/ 23 | vec2 get_random_numbers(inout uvec2 seed) { 24 | // PCG2D, as described here: https://jcgt.org/published/0009/03/02/ 25 | seed = 1664525u * seed + 1013904223u; 26 | seed.x += 1664525u * seed.y; 27 | seed.y += 1664525u * seed.x; 28 | seed ^= (seed >> 16u); 29 | seed.x += 1664525u * seed.y; 30 | seed.y += 1664525u * seed.x; 31 | seed ^= (seed >> 16u); 32 | // Multiply by 2^-32 to get floats 33 | return vec2(seed) * 2.32830643654e-10; 34 | } 35 | 36 | 37 | //! The inverse of the error function (used to sample Gaussians). 38 | float erfinv(float x) { 39 | float w = -log(max(1.0e-37, 1.0 - x * x)); 40 | float a = w - 2.5; 41 | float b = sqrt(w) - 3.0; 42 | return x * ((w < 5.0) 43 | ? fma(fma(fma(fma(fma(fma(fma(fma(2.81022636e-08, a, 3.43273939e-07), a, -3.5233877e-06), a, -4.39150654e-06), a, 0.00021858087), a, -0.00125372503), a, -0.00417768164), a, 0.246640727), a, 1.50140941) 44 | : fma(fma(fma(fma(fma(fma(fma(fma(-0.000200214257, b, 0.000100950558), b, 0.00134934322), b, -0.00367342844), b, 0.00573950773), b, -0.0076224613), b, 0.00943887047), b, 1.00167406), b, 2.83297682)); 45 | } 46 | 47 | 48 | //! Samples a direction vector in the upper hemisphere (non-negative z) by 49 | //! sampling spherical coordinates uniformly. Not a good strategy. 50 | vec3 sample_hemisphere_spherical(vec2 randoms) { 51 | float azimuth = (2.0 * M_PI) * randoms[0] - M_PI; 52 | float inclination = (0.5 * M_PI) * randoms[1]; 53 | float radius = sin(inclination); 54 | return vec3(radius * cos(azimuth), radius * sin(azimuth), cos(inclination)); 55 | } 56 | 57 | 58 | //! Returns the density w.r.t. solid angle sampled by 59 | //! sample_hemisphere_spherical(). Only needs the local z-coordinate as input. 60 | float get_hemisphere_spherical_density(float sampled_dir_z) { 61 | if (sampled_dir_z < 0.0) 62 | return 0.0; 63 | return 1.0 / ((M_PI * M_PI) * sqrt(max(0.0, 1.0 - sampled_dir_z * sampled_dir_z))); 64 | } 65 | 66 | 67 | //! Returns the solid angle of the given spherical light for the given shading 68 | //! point divided by 2.0 * M_PI, or 0.0 if it is completely below the horizon. 69 | float get_spherical_light_importance(vec3 center, float radius, vec3 shading_pos, vec3 normal) { 70 | // If the light is completely below the horizon, return 0 71 | vec3 center_dir = center - shading_pos; 72 | if (dot(normal, center_dir) < -radius) 73 | return 0.0; 74 | // Compute the solid angle 75 | float center_dist_2 = dot(center_dir, center_dir); 76 | float z_min = sqrt(max(0.0, 1.0 - radius * radius / center_dist_2)); 77 | float z_range = 1.0 - z_min; 78 | return z_range; 79 | } 80 | 81 | 82 | /*! Samples a direction in the solid angle of the given spherical light 83 | uniformly. 84 | \param center The center position of the spherical light. 85 | \param importance The importance of the spherical light as returned by 86 | get_spherical_light_importance(). Must not be zero. The sampled density 87 | w.r.t. solid angle is 1.0 / (2.0 * M_PI * importance) or zero. 88 | \param shading_pos The position of the shading point. 89 | \param randoms A uniformly distributed point in [0,1)^2. 90 | \return A normalized direction vector towards the spherical light.*/ 91 | vec3 sample_spherical_light(vec3 center, float importance, vec3 shading_pos, vec2 randoms) { 92 | // Produce a sample in local coordinates 93 | float azimuth = (2.0 * M_PI) * randoms[0] - M_PI; 94 | float z_range = importance; 95 | float z = 1.0 - z_range * randoms[1]; 96 | float r = sqrt(max(0.0, 1.0 - z * z)); 97 | vec3 local_dir = vec3(r * cos(azimuth), r * sin(azimuth), z); 98 | // Construct a coordinate frame where the vector towards the spherical 99 | // light is the z-axis and transform to world space 100 | mat3 light_to_world_space = get_shading_space(normalize(center - shading_pos)); 101 | return light_to_world_space * local_dir; 102 | } 103 | 104 | 105 | /*! Randomly picks one of the spherical lights in the scene using selection 106 | probabilities proportional to get_spherical_light_importance() and samples 107 | a direction towards it, sampling its solid angle uniformly. 108 | \param out_total_importance The sum of importance values across all lights. 109 | Needed for density computation. 110 | \param shading_pos Position of the shading point w.r.t. which the solid 111 | angle is computed. 112 | \param normal The shading normal at the shading point. 113 | \param randoms A random point distributed uniformly in [0, 1)^2. 114 | \return The sampled direction towards a light as normalized vector. Zero if 115 | the total importance is zero (all lights below the horizon).*/ 116 | vec3 sample_lights(out float out_total_importance, vec3 shading_pos, vec3 normal, vec2 randoms) { 117 | // Compute the total importance of all lights combined 118 | out_total_importance = 0.0; 119 | [[loop]] 120 | for (uint i = 0; i != SPHERICAL_LIGHT_COUNT; ++i) 121 | out_total_importance += get_spherical_light_importance(g_spherical_lights[i].xyz, g_spherical_lights[i].w, shading_pos, normal); 122 | // Pick one 123 | float target_importance = randoms[0] * out_total_importance; 124 | float prefix_importance = 0.0; 125 | [[loop]] 126 | for (uint i = 0; i != SPHERICAL_LIGHT_COUNT; ++i) { 127 | vec4 light = g_spherical_lights[i]; 128 | float importance = get_spherical_light_importance(light.xyz, light.w, shading_pos, normal); 129 | prefix_importance += importance; 130 | if (prefix_importance > target_importance) { 131 | // Reuse the random number 132 | randoms[0] = (target_importance + importance - prefix_importance) / importance; 133 | // Sample the light 134 | return sample_spherical_light(light.xyz, importance, shading_pos, randoms); 135 | } 136 | } 137 | // Only happens if randoms[0] >= 1.0 or out_total_importance <= 0.0 138 | return vec3(0.0); 139 | } 140 | 141 | 142 | /*! Returns the density w.r.t. solid angle that is sampled by sample_lights(). 143 | sampled_dir must be above the horizon, otherwise results may be incorrect. 144 | Pass true for is_light_dir if sampled_dir has been generated as direction 145 | towards a spherical light. This is a work around for numerical issues.*/ 146 | float get_lights_density(float total_importance, vec3 shading_pos, vec3 normal, vec3 sampled_dir, bool is_light_dir) { 147 | if (total_importance <= 0.0) 148 | return 0.0; 149 | // Count how many lights are intersected by the given ray 150 | float light_count = 0.0; 151 | [[loop]] 152 | for (uint i = 0; i != SPHERICAL_LIGHT_COUNT; ++i) { 153 | vec4 light = g_spherical_lights[i]; 154 | vec3 center_dir = light.xyz - shading_pos; 155 | float center_dist_2 = dot(center_dir, center_dir); 156 | float center_dot_dir = dot(center_dir, sampled_dir); 157 | float radius_2 = light.w * light.w; 158 | float in_sphere = center_dist_2 - radius_2; 159 | float discriminant = center_dot_dir * center_dot_dir - in_sphere; 160 | // Add 1 if there is an intersection with non-negative ray parameter t 161 | light_count += (discriminant >= 0.0 && in_sphere >= 0.0 && center_dot_dir >= 0.0) ? 1.0 : 0.0; 162 | } 163 | // For small distant light sources, the procedure above will sometimes fail 164 | // to count them correctly. If that results in a light count of zero for 165 | // light samples, it distorts Monte Carlo estimates heavily. Thus, we set 166 | // light_count to at least 1 if we know that it must be like that. 167 | if (is_light_dir) 168 | light_count = max(1.0, light_count); 169 | // The density is proportional to this count 170 | return light_count / (2.0 * M_PI * total_importance); 171 | } 172 | 173 | 174 | /*! Traces the given ray (with normalized ray_dir). If it hits a scene surface, 175 | it constructs the shading data and returns true. Otherwise, it returns 176 | false and only writes the sky emission to the shading data.*/ 177 | bool trace_ray(out shading_data_t out_shading_data, vec3 ray_origin, vec3 ray_dir) { 178 | // Trace a ray 179 | rayQueryEXT ray_query; 180 | rayQueryInitializeEXT(ray_query, g_bvh, gl_RayFlagsOpaqueEXT, 0xff, ray_origin, 1.0e-3, ray_dir, 1e38); 181 | while (rayQueryProceedEXT(ray_query)) {} 182 | // If there was no hit, use the sky color 183 | if (rayQueryGetIntersectionTypeEXT(ray_query, true) == gl_RayQueryCommittedIntersectionNoneEXT) { 184 | out_shading_data.emission = g_sky_radiance; 185 | return false; 186 | } 187 | // Construct shading data 188 | else { 189 | int triangle_index = rayQueryGetIntersectionPrimitiveIndexEXT(ray_query, true); 190 | vec2 barys = rayQueryGetIntersectionBarycentricsEXT(ray_query, true); 191 | bool front = rayQueryGetIntersectionFrontFaceEXT(ray_query, true); 192 | out_shading_data = get_shading_data(triangle_index, barys, front, -ray_dir); 193 | return true; 194 | } 195 | } 196 | 197 | 198 | //! Like trace_ray() but only returns the emission of the shading data 199 | vec3 trace_ray_emission(vec3 ray_origin, vec3 ray_dir) { 200 | // Trace a ray 201 | rayQueryEXT ray_query; 202 | rayQueryInitializeEXT(ray_query, g_bvh, gl_RayFlagsOpaqueEXT, 0xff, ray_origin, 1.0e-3, ray_dir, 1e38); 203 | while (rayQueryProceedEXT(ray_query)) {} 204 | // If there was no hit, use the sky color 205 | if (rayQueryGetIntersectionTypeEXT(ray_query, true) == gl_RayQueryCommittedIntersectionNoneEXT) 206 | return g_sky_radiance; 207 | // Otherwise, check if it is an emissive material 208 | else { 209 | int triangle_index = rayQueryGetIntersectionPrimitiveIndexEXT(ray_query, true); 210 | if (texelFetch(g_material_indices, triangle_index).r == EMISSION_MATERIAL_INDEX) 211 | return g_emission_material_radiance; 212 | else 213 | return vec3(0.0); 214 | } 215 | } 216 | 217 | 218 | //! Like path_trace_psa() but samples spherical coordiantes uniformly for 219 | //! instructive purposes 220 | vec3 path_trace_spherical(vec3 ray_origin, vec3 ray_dir, inout uvec2 seed) { 221 | vec3 throughput_weight = vec3(1.0); 222 | vec3 radiance = vec3(0.0); 223 | [[unroll]] 224 | for (uint k = 1; k != PATH_LENGTH + 1; ++k) { 225 | shading_data_t s; 226 | bool hit = trace_ray(s, ray_origin, ray_dir); 227 | radiance += throughput_weight * s.emission; 228 | if (hit && k < PATH_LENGTH) { 229 | // Update the ray and the throughput weight 230 | mat3 shading_to_world_space = get_shading_space(s.normal); 231 | ray_origin = s.pos; 232 | vec3 sampled_dir = sample_hemisphere_spherical(get_random_numbers(seed)); 233 | ray_dir = shading_to_world_space * sampled_dir; 234 | float lambert_in = sampled_dir.z; 235 | float density = get_hemisphere_spherical_density(sampled_dir.z); 236 | throughput_weight *= frostbite_brdf(s, ray_dir) * lambert_in / density; 237 | } 238 | else 239 | // End the path 240 | break; 241 | } 242 | return radiance; 243 | } 244 | 245 | 246 | /*! Computes a Monte Carlo estimate of the radiance received along a ray. It 247 | uses path tracing with uniform sampling of the projected solid angle. 248 | \param ray_origin The ray origin. A small ray offset is used automatically 249 | to avoid incorrect self-shadowing. 250 | \param ray_dir Normalized ray direction. 251 | \param seed Used for get_random_numbers(). 252 | \return A Monte Carlo estimate of the incoming radiance in linear sRGB 253 | (a.k.a. Rec. 709).*/ 254 | vec3 path_trace_psa(vec3 ray_origin, vec3 ray_dir, inout uvec2 seed) { 255 | vec3 throughput_weight = vec3(1.0); 256 | vec3 radiance = vec3(0.0); 257 | [[unroll]] 258 | for (uint k = 1; k != PATH_LENGTH + 1; ++k) { 259 | shading_data_t s; 260 | bool hit = trace_ray(s, ray_origin, ray_dir); 261 | radiance += throughput_weight * s.emission; 262 | if (hit && k < PATH_LENGTH) { 263 | // Update the ray and the throughput weight 264 | mat3 shading_to_world_space = get_shading_space(s.normal); 265 | ray_origin = s.pos; 266 | vec3 sampled_dir = sample_hemisphere_psa(get_random_numbers(seed)); 267 | ray_dir = shading_to_world_space * sampled_dir; 268 | float lambert_in = sampled_dir.z; 269 | float density = get_hemisphere_psa_density(sampled_dir.z); 270 | throughput_weight *= frostbite_brdf(s, ray_dir) * lambert_in / density; 271 | } 272 | else 273 | // End the path 274 | break; 275 | } 276 | return radiance; 277 | } 278 | 279 | 280 | //! Like path_trace_psa() but with proper importance sampling of the BRDF times 281 | //! cosine. 282 | vec3 path_trace_brdf(vec3 ray_origin, vec3 ray_dir, inout uvec2 seed) { 283 | vec3 throughput_weight = vec3(1.0); 284 | vec3 radiance = vec3(0.0); 285 | [[unroll]] 286 | for (uint k = 1; k != PATH_LENGTH + 1; ++k) { 287 | shading_data_t s; 288 | bool hit = trace_ray(s, ray_origin, ray_dir); 289 | radiance += throughput_weight * s.emission; 290 | if (hit && k < PATH_LENGTH) { 291 | // Sample the BRDF and update the ray 292 | ray_origin = s.pos; 293 | ray_dir = sample_frostbite_brdf(s, get_random_numbers(seed)); 294 | float density = get_frostbite_brdf_density(s, ray_dir); 295 | // The sample may have ended up in the lower hemisphere 296 | float lambert_in = dot(s.normal, ray_dir); 297 | if (lambert_in <= 0.0) 298 | break; 299 | // Update the throughput weight 300 | throughput_weight *= frostbite_brdf(s, ray_dir) * lambert_in / density; 301 | } 302 | else 303 | // End the path 304 | break; 305 | } 306 | return radiance; 307 | } 308 | 309 | 310 | //! Like path_trace_brdf() but additionally uses next-event estimation 311 | vec3 path_trace_nee(vec3 ray_origin, vec3 ray_dir, inout uvec2 seed) { 312 | vec3 throughput_weight = vec3(1.0); 313 | vec3 nee_throughput_weight = throughput_weight; 314 | vec3 radiance = vec3(0.0); 315 | [[unroll]] 316 | for (uint k = 1; k != PATH_LENGTH + 1; ++k) { 317 | shading_data_t s; 318 | bool hit = trace_ray(s, ray_origin, ray_dir); 319 | radiance += nee_throughput_weight * s.emission; 320 | if (hit && k < PATH_LENGTH) { 321 | // Sample a direction towards a light 322 | float total_light_importance; 323 | vec3 light_dir = sample_lights(total_light_importance, s.pos, s.normal, get_random_numbers(seed)); 324 | // Discard the sample if it is in the lower hemisphere 325 | float lambert_in_0 = dot(s.normal, light_dir); 326 | if (lambert_in_0 > 0.0) { 327 | // Trace a ray towards the light and retrieve the emission 328 | vec3 light_emission = trace_ray_emission(s.pos, light_dir); 329 | // For MIS, compute the density for this direction with light 330 | // and BRDF sampling 331 | float light_density_0 = get_lights_density(total_light_importance, s.pos, s.normal, light_dir, true); 332 | float brdf_density_0 = get_frostbite_brdf_density(s, light_dir); 333 | // Evaluate the MIS estimate 334 | radiance += throughput_weight * frostbite_brdf(s, light_dir) * light_emission * (lambert_in_0 / (light_density_0 + brdf_density_0)); 335 | } 336 | // Sample the BRDF for MIS and to continue the path 337 | ray_origin = s.pos; 338 | ray_dir = sample_frostbite_brdf(s, get_random_numbers(seed)); 339 | float lambert_in_1 = dot(s.normal, ray_dir); 340 | // Abort path construction, if the sample is in the lower 341 | // hemisphere 342 | if (lambert_in_1 <= 0.0) 343 | break; 344 | // Compute the throughput weight for emission at the next vertex, 345 | // accounting for MIS 346 | float light_density_1 = get_lights_density(total_light_importance, s.pos, s.normal, ray_dir, false); 347 | float brdf_density_1 = get_frostbite_brdf_density(s, ray_dir); 348 | vec3 brdf_lambert_1 = frostbite_brdf(s, ray_dir) * lambert_in_1; 349 | nee_throughput_weight = throughput_weight * (brdf_lambert_1 / (light_density_1 + brdf_density_1)); 350 | // Update the throughput-weight for the path 351 | throughput_weight *= brdf_lambert_1 * (1.0 / brdf_density_1); 352 | } 353 | else 354 | // End the path 355 | break; 356 | } 357 | return radiance; 358 | } 359 | 360 | 361 | void main() { 362 | // Jitter the subpixel position using a Gaussian with the given standard 363 | // deviation in pixels 364 | uvec2 seed = uvec2(gl_FragCoord) ^ uvec2(g_frame_index << 16, (g_frame_index + 237) << 16); 365 | const float std = 0.9; 366 | vec2 randoms = 2.0 * get_random_numbers(seed) - vec2(1.0); 367 | vec2 jitter = (std * sqrt(2.0)) * vec2(erfinv(randoms.x), erfinv(randoms.y)); 368 | vec2 jittered = gl_FragCoord.xy + jitter; 369 | // Compute the primary ray using either a pinhole camera or a hemispherical 370 | // camera 371 | vec3 ray_origin, ray_dir; 372 | if (g_camera_type <= 1) { 373 | vec2 ray_tex_coord = jittered * g_inv_viewport_size; 374 | ray_origin = get_camera_ray_origin(ray_tex_coord, g_projection_to_world_space); 375 | ray_dir = get_camera_ray_direction(ray_tex_coord, g_world_to_projection_space); 376 | } 377 | else { 378 | mat3 hemisphere_to_world_space = get_shading_space(g_hemispherical_camera_normal); 379 | ray_origin = g_camera_pos; 380 | vec2 sphere_factor = vec2(1.0, (g_camera_type == 3) ? 2.0 : 1.0); 381 | ray_dir = hemisphere_to_world_space * sample_hemisphere_spherical(jittered * sphere_factor * g_inv_viewport_size); 382 | } 383 | // Perform path tracing using the requested technique 384 | #if SAMPLING_STRATEGY_SPHERICAL 385 | vec3 ray_radiance = path_trace_spherical(ray_origin, ray_dir, seed); 386 | #elif SAMPLING_STRATEGY_PSA 387 | vec3 ray_radiance = path_trace_psa(ray_origin, ray_dir, seed); 388 | #elif SAMPLING_STRATEGY_BRDF 389 | vec3 ray_radiance = path_trace_brdf(ray_origin, ray_dir, seed); 390 | #elif SAMPLING_STRATEGY_NEE 391 | vec3 ray_radiance = path_trace_nee(ray_origin, ray_dir, seed); 392 | #endif 393 | g_out_color = vec4(ray_radiance, 1.0); 394 | } 395 | -------------------------------------------------------------------------------- /src/shaders/pathtrace.vert.glsl: -------------------------------------------------------------------------------- 1 | #version 460 2 | 3 | 4 | void main() { 5 | int id = gl_VertexIndex; 6 | vec2 pos = vec2(0.0); 7 | if (id == 0) pos = vec2(-1.1, -1.1); 8 | if (id == 1) pos = vec2(-1.1, 3.5); 9 | if (id == 2) pos = vec2(3.5, -1.1); 10 | gl_Position = vec4(pos, 0.0, 1.0); 11 | } 12 | -------------------------------------------------------------------------------- /src/shaders/shading_data.glsl: -------------------------------------------------------------------------------- 1 | #include "mesh_quantization.glsl" 2 | #include "constants.glsl" 3 | 4 | 5 | //! Three textures for each material providing base color, specular and normal 6 | //! parameters 7 | layout (binding = 1) uniform sampler2D g_textures[3 * MATERIAL_COUNT]; 8 | 9 | //! Provides quantized world-space positions for each vertex 10 | layout (binding = 3) uniform utextureBuffer g_quantized_vertex_poss; 11 | //! Provides normals and texture coordinates for each vertex 12 | layout (binding = 4) uniform textureBuffer g_octahedral_normal_and_tex_coords; 13 | //! Provides a material index for each triangle 14 | layout (binding = 5) uniform utextureBuffer g_material_indices; 15 | 16 | 17 | //! Full description of a shading point on a surface and its BRDF. All vectors 18 | //! use the same coordinate frame, which has to be isometric to world space. 19 | struct shading_data_t { 20 | //! Position of the shading point 21 | vec3 pos; 22 | //! The normalized shading normal vector 23 | vec3 normal; 24 | //! The normalized outgoing light direction (i.e. towards the camera along 25 | //! the path) 26 | vec3 out_dir; 27 | //! dot(normal, out_dir) 28 | float lambert_out; 29 | //! The emitted radiance (Rec. 709) from this shading point into direction 30 | //! out_dir 31 | vec3 emission; 32 | //! The diffuse albedo specified using Rec. 709 (i.e. linear sRGB) 33 | vec3 diffuse_albedo; 34 | //! The color of specular reflection (Rec. 709) when looking at the surface 35 | //! from the zenith 36 | vec3 fresnel_0; 37 | //! The roughness parameter of the GGX distribution of normals 38 | float roughness; 39 | }; 40 | 41 | 42 | /*! Assembles the shading data for a point on the surface of the scene. 43 | \param triangle_index The index of the scene triangle on which a point is 44 | given. 45 | \param barycentrics Barycentric coordinates of the point on the triangle. 46 | The first barycentric coordinate is omitted. 47 | \param front true iff the ray hit the front of the triangle (i.e. the 48 | triangle vertices are clockwise when observed from the ray origin). 49 | \param out_dir The normalized direction towards the camera along the path. 50 | \return The complete shading data.*/ 51 | shading_data_t get_shading_data(int triangle_index, vec2 barycentrics, bool front, vec3 out_dir) { 52 | shading_data_t s; 53 | // Interpolate all vertex attributes for all triangle vertices 54 | vec3 barys = vec3(1.0 - barycentrics[0] - barycentrics[1], barycentrics[0], barycentrics[1]); 55 | vec3 poss[3]; 56 | s.pos = vec3(0.0); 57 | vec3 normal_geo = vec3(0.0); 58 | vec2 tex_coords[3]; 59 | vec2 tex_coord = vec2(0.0); 60 | [[unroll]] 61 | for (int i = 0; i != 3; ++i) { 62 | int vert_index = triangle_index * 3 + i; 63 | uvec2 quantized_pos = texelFetch(g_quantized_vertex_poss, vert_index).rg; 64 | vec4 normal_and_tex_coords = texelFetch(g_octahedral_normal_and_tex_coords, vert_index); 65 | poss[i] = dequantize_position(quantized_pos, g_dequantization_factor, g_dequantization_summand); 66 | s.pos += barys[i] * poss[i]; 67 | normal_geo += barys[i] * dequantize_normal(normal_and_tex_coords.xy); 68 | tex_coords[i] = normal_and_tex_coords.zw * vec2(8.0, -8.0) + vec2(0.0, 1.0); 69 | tex_coord += barys[i] * tex_coords[i]; 70 | } 71 | normal_geo = normalize(normal_geo); 72 | // Sample the material textures 73 | uint material_index = texelFetch(g_material_indices, triangle_index).r; 74 | vec3 base_color_tex = texture(g_textures[nonuniformEXT(3 * material_index + 0)], tex_coord).rgb; 75 | vec3 specular_tex = texture(g_textures[nonuniformEXT(3 * material_index + 1)], tex_coord).rgb; 76 | vec2 normal_tex = texture(g_textures[nonuniformEXT(3 * material_index + 2)], tex_coord).rg; 77 | vec3 normal_local; 78 | normal_local.xy = normal_tex * 2.0 - vec2(1.0); 79 | normal_local.z = sqrt(max(0.0, (1.0 - normal_local.x * normal_local.x) - normal_local.y * normal_local.y)); 80 | // Transform the normal to world space 81 | mat2 tex_edges = mat2(tex_coords[1] - tex_coords[0], tex_coords[2] - tex_coords[0]); 82 | vec3 pre_tangent_0 = cross(normal_geo, poss[1] - poss[0]); 83 | vec3 pre_tangent_1 = cross(normal_geo, poss[0] - poss[2]); 84 | vec3 tangent_0 = pre_tangent_1 * tex_edges[0][0] + pre_tangent_0 * tex_edges[1][0]; 85 | vec3 tangent_1 = pre_tangent_1 * tex_edges[0][1] + pre_tangent_0 * tex_edges[1][1]; 86 | float mean_length = sqrt(0.5 * (dot(tangent_0, tangent_0) + dot(tangent_1, tangent_1))); 87 | mat3 tangent_to_world_space = mat3(tangent_0, tangent_1, normal_geo); 88 | normal_local.z *= max(1.0e-8, mean_length); 89 | s.normal = normalize(tangent_to_world_space * normal_local); 90 | s.normal = front ? s.normal : -s.normal; 91 | // Adapt the normal such that out_dir is in the upper hemisphere 92 | s.out_dir = out_dir; 93 | float normal_offset = max(0.0f, 1.0e-3f - dot(s.normal, s.out_dir)); 94 | s.normal = normalize(fma(vec3(normal_offset), s.out_dir, s.normal)); 95 | // Complete the shading data 96 | s.lambert_out = dot(s.normal, s.out_dir); 97 | float metalicity = specular_tex.b; 98 | s.diffuse_albedo = base_color_tex - metalicity * base_color_tex; 99 | s.fresnel_0 = mix(vec3(0.02), base_color_tex, metalicity); 100 | s.roughness = max(0.006, specular_tex.g * specular_tex.g); 101 | s.emission = (material_index == EMISSION_MATERIAL_INDEX) ? g_emission_material_radiance : vec3(0.0); 102 | return s; 103 | } 104 | -------------------------------------------------------------------------------- /src/shaders/srgb_utility.glsl: -------------------------------------------------------------------------------- 1 | // Applies the non-linearity that maps linear RGB to sRGB 2 | float linear_to_srgb(float linear) { 3 | return (linear <= 0.0031308) ? (12.92 * linear) : (1.055 * pow(linear, 1.0 / 2.4) - 0.055); 4 | } 5 | 6 | // Inverse of linear_to_srgb() 7 | float srgb_to_linear(float non_linear) { 8 | return (non_linear <= 0.04045) ? ((1.0 / 12.92) * non_linear) : pow(non_linear * (1.0 / 1.055) + 0.055 / 1.055, 2.4); 9 | } 10 | 11 | // Turns a linear RGB color (i.e. rec. 709) into sRGB 12 | vec3 linear_rgb_to_srgb(vec3 linear) { 13 | return vec3(linear_to_srgb(linear.r), linear_to_srgb(linear.g), linear_to_srgb(linear.b)); 14 | } 15 | 16 | // Inverse of linear_rgb_to_srgb() 17 | vec3 srgb_to_linear_rgb(vec3 srgb) { 18 | return vec3(srgb_to_linear(srgb.r), srgb_to_linear(srgb.g), srgb_to_linear(srgb.b)); 19 | } 20 | -------------------------------------------------------------------------------- /src/shaders/tonemap.frag.glsl: -------------------------------------------------------------------------------- 1 | #version 460 2 | #extension GL_GOOGLE_include_directive : enable 3 | #include "constants.glsl" 4 | 5 | //! The render target containing HDR radiance values 6 | layout (binding = 1, input_attachment_index = 0) uniform subpassInput g_hdr_radiance; 7 | 8 | 9 | //! The sRGB color and alpha to draw to the screen 10 | layout (location = 0) out vec4 g_out_color; 11 | 12 | 13 | /*! Applies the Khronos PBR neutral tone mapper to the given Rec. 709 color 14 | (a.k.a. linear sRGB) and returns a tonemapped Rec. 709 color. For details 15 | see: 16 | https://github.com/KhronosGroup/ToneMapping/blob/main/PBR_Neutral/README.md */ 17 | vec3 tonemapper_khronos_pbr_neutral(vec3 color) { 18 | // The end of the brightness range where colors are mapped linearly 19 | const float start_compression = 0.8 - 0.04; 20 | // A parameter controling the strength of desaturation for oversaturated 21 | // colors 22 | const float desaturation = 0.15; 23 | // Non-linearly compress dark colors to compensate for the Fresnel term 24 | // in shading models 25 | float darkest = min(color.r, min(color.g, color.b)); 26 | float offset = (darkest < 0.08) ? (darkest - 6.25 * darkest * darkest) : 0.04; 27 | color -= vec3(offset); 28 | // If no channel has a value above start_compression, we only use this 29 | // linear offset 30 | float brightest = max(color.r, max(color.g, color.b)); 31 | if (brightest < start_compression) 32 | return color; 33 | else { 34 | // Otherwise compress colors using a rational function 35 | float compressed = 1.0 - start_compression; 36 | float new_brightest = 1.0 - compressed * compressed / (brightest + compressed - start_compression); 37 | color *= new_brightest / brightest; 38 | // Desaturate bright colors a bit 39 | float weight = 1.0 - 1.0 / (desaturation * (brightest - new_brightest) + 1.0); 40 | return mix(color, vec3(new_brightest), weight); 41 | } 42 | } 43 | 44 | 45 | /*! An implementation of the ACES tonemapper using an approximation derived by 46 | Stephen Hill and found here: 47 | https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl */ 48 | vec3 tonemapper_aces(vec3 color) { 49 | // sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT 50 | const mat3 aces_in = transpose(mat3( 51 | 0.59719, 0.35458, 0.04823, 52 | 0.07600, 0.90834, 0.01566, 53 | 0.02840, 0.13383, 0.83777)); 54 | // ODT_SAT => XYZ => D60_2_D65 => sRGB 55 | const mat3 aces_out = transpose(mat3( 56 | 1.60475, -0.53108, -0.07367, 57 | -0.10208, 1.10813, -0.00605, 58 | -0.00327, -0.07276, 1.07602)); 59 | vec3 v = aces_in * color; 60 | vec3 w = (v * (v + 0.0245786) - 0.000090537) / (v * (0.983729 * v + 0.4329510) + 0.238081); 61 | return aces_out * w; 62 | } 63 | 64 | 65 | void main() { 66 | vec4 hdr_radiance = subpassLoad(g_hdr_radiance); 67 | float factor = g_exposure / float(g_accum_frame_count); 68 | g_out_color = hdr_radiance * vec4(vec3(factor), hdr_radiance.a); 69 | #if TONEMAPPER_CLAMP 70 | g_out_color.rgb = clamp(g_out_color.rgb, 0.0, 1.0); 71 | #elif TONEMAPPER_ACES 72 | g_out_color.rgb = tonemapper_aces(g_out_color.rgb); 73 | #elif TONEMAPPER_KHRONOS_PBR_NEUTRAL 74 | g_out_color.rgb = tonemapper_khronos_pbr_neutral(g_out_color.rgb); 75 | #endif 76 | // Color NaN pixels violett 77 | if (isnan(hdr_radiance.r) || isnan(hdr_radiance.g) || isnan(hdr_radiance.b) || isnan(hdr_radiance.a)) 78 | g_out_color = vec4(1.0, 0.0, 1.0, 1.0); 79 | // And INF pixels red 80 | if (isinf(hdr_radiance.r) || isinf(hdr_radiance.g) || isinf(hdr_radiance.b) || isinf(hdr_radiance.a)) 81 | g_out_color = vec4(1.0, 0.0, 0.0, 1.0); 82 | } 83 | -------------------------------------------------------------------------------- /src/shaders/tonemap.vert.glsl: -------------------------------------------------------------------------------- 1 | #version 460 2 | 3 | 4 | void main() { 5 | int id = gl_VertexIndex; 6 | vec2 pos = vec2(0.0); 7 | if (id == 0) pos = vec2(-1.1, -1.1); 8 | if (id == 1) pos = vec2(-1.1, 3.5); 9 | if (id == 2) pos = vec2(3.5, -1.1); 10 | gl_Position = vec4(pos, 0.0, 1.0); 11 | } 12 | -------------------------------------------------------------------------------- /src/slides.c: -------------------------------------------------------------------------------- 1 | #include "main.h" 2 | #include "string_utilities.h" 3 | 4 | 5 | uint32_t create_slides(slide_t* slides) { 6 | uint32_t n = 0; 7 | render_settings_t quality = { 8 | .path_length = 4, 9 | .sampling_strategy = sampling_strategy_nee, 10 | }; 11 | // 0: A pretty view of the bistro with quality path tracing 12 | slides[n++] = (slide_t) { 13 | .quicksave = copy_string("data/saves/bistro/pretty.rt_save"), 14 | .render_settings = quality, 15 | .screenshot_frame = 2048, 16 | .screenshot_path = copy_string("/home/cpeters/projects/cg_lectures/graphics/renders/pretty_bistro.png"), 17 | }; 18 | // 1: Same view with emission only 19 | slides[n++] = (slide_t) { 20 | .quicksave = copy_string("data/saves/bistro/pretty.rt_save"), 21 | .render_settings = { .sampling_strategy = sampling_strategy_nee, .path_length = 1 }, 22 | .screenshot_frame = 1, 23 | .screenshot_path = copy_string("/home/cpeters/projects/cg_lectures/graphics/renders/pretty_bistro_emission.png"), 24 | }; 25 | // 2: An overview of the bistro to illustrate path construction 26 | slides[n++] = (slide_t) { 27 | .quicksave = copy_string("data/saves/bistro/path_overview.rt_save"), 28 | .render_settings = quality, 29 | .screenshot_frame = 2048, 30 | .screenshot_path = copy_string("/home/cpeters/projects/cg_lectures/graphics/renders/path_overview_bistro.png"), 31 | }; 32 | // 3: The pretty view with spherical sampling 33 | slides[n++] = (slide_t) { 34 | .quicksave = copy_string("data/saves/bistro/pretty.rt_save"), 35 | .render_settings = { .sampling_strategy = sampling_strategy_spherical, .path_length = 4 }, 36 | .screenshot_frame = 128, 37 | .screenshot_path = copy_string("/home/cpeters/projects/cg_lectures/graphics/renders/pretty_bistro_spherical.png"), 38 | }; 39 | // 4: Same thing during the day 40 | slides[n++] = (slide_t) { 41 | .quicksave = copy_string("data/saves/bistro/pretty_day.rt_save"), 42 | .render_settings = { .sampling_strategy = sampling_strategy_spherical, .path_length = 4 }, 43 | .screenshot_frame = 128, 44 | .screenshot_path = copy_string("/home/cpeters/projects/cg_lectures/graphics/renders/pretty_bistro_spherical_day.png"), 45 | }; 46 | // 5: Mirror closeup with spherical sampling 47 | slides[n++] = (slide_t) { 48 | .quicksave = copy_string("data/saves/bistro/mirror.rt_save"), 49 | .render_settings = { .sampling_strategy = sampling_strategy_spherical, .path_length = 4 }, 50 | .screenshot_frame = 4, 51 | .screenshot_path = copy_string("/home/cpeters/projects/cg_lectures/graphics/renders/mirror_spherical.png"), 52 | }; 53 | // 6: Mirror closeup with PSA sampling 54 | slides[n++] = (slide_t) { 55 | .quicksave = copy_string("data/saves/bistro/mirror.rt_save"), 56 | .render_settings = { .sampling_strategy = sampling_strategy_psa, .path_length = 4 }, 57 | .screenshot_frame = 4, 58 | .screenshot_path = copy_string("/home/cpeters/projects/cg_lectures/graphics/renders/mirror_psa.png"), 59 | }; 60 | // 7: Mirror closeup with BRDF sampling 61 | slides[n++] = (slide_t) { 62 | .quicksave = copy_string("data/saves/bistro/mirror.rt_save"), 63 | .render_settings = { .sampling_strategy = sampling_strategy_brdf, .path_length = 4 }, 64 | .screenshot_frame = 4, 65 | .screenshot_path = copy_string("/home/cpeters/projects/cg_lectures/graphics/renders/mirror_brdf.png"), 66 | }; 67 | // 8: Spherical view from a shading point in the bistro 68 | slides[n++] = (slide_t) { 69 | .quicksave = copy_string("data/saves/bistro/shading_point_day.rt_save"), 70 | .render_settings = quality, 71 | .screenshot_frame = 2048, 72 | .screenshot_path = copy_string("/home/cpeters/projects/cg_lectures/graphics/renders/shading_point_incoming.png"), 73 | }; 74 | // 9: The pretty view with BRDF sampling during day light 75 | slides[n++] = (slide_t) { 76 | .quicksave = copy_string("data/saves/bistro/pretty_day.rt_save"), 77 | .render_settings = { .sampling_strategy = sampling_strategy_brdf, .path_length = 4 }, 78 | .screenshot_frame = 16, 79 | .screenshot_path = copy_string("/home/cpeters/projects/cg_lectures/graphics/renders/pretty_bistro_brdf_day.png"), 80 | }; 81 | // 10: Same thing at night 82 | slides[n++] = (slide_t) { 83 | .quicksave = copy_string("data/saves/bistro/pretty.rt_save"), 84 | .render_settings = { .sampling_strategy = sampling_strategy_brdf, .path_length = 4 }, 85 | .screenshot_frame = 16, 86 | .screenshot_path = copy_string("/home/cpeters/projects/cg_lectures/graphics/renders/pretty_bistro_brdf.png"), 87 | }; 88 | // 11: Night-time pretty bistro with NEE 89 | slides[n++] = (slide_t) { 90 | .quicksave = copy_string("data/saves/bistro/pretty.rt_save"), 91 | .render_settings = { .sampling_strategy = sampling_strategy_nee, .path_length = 4 }, 92 | .screenshot_frame = 16, 93 | .screenshot_path = copy_string("/home/cpeters/projects/cg_lectures/graphics/renders/pretty_bistro_nee.png"), 94 | }; 95 | // 12: Spherical view from a shading point in the bistro at night with 96 | // reduced exposure 97 | slides[n++] = (slide_t) { 98 | .quicksave = copy_string("data/saves/bistro/shading_point_dark.rt_save"), 99 | .render_settings = quality, 100 | .screenshot_frame = 2048, 101 | .screenshot_path = copy_string("/home/cpeters/projects/cg_lectures/graphics/renders/shading_point_dark_incoming.png"), 102 | }; 103 | // 13: Single sample render of the bistro as HDR image 104 | slides[n++] = (slide_t) { 105 | .quicksave = copy_string("data/saves/bistro/pretty.rt_save"), 106 | .render_settings = quality, 107 | .screenshot_frame = 1, 108 | .screenshot_path = copy_string("/home/cpeters/projects/cg_lectures/graphics/renders/pretty_bistro_1_spp.hdr"), 109 | .screenshot_format = image_file_format_hdr, 110 | }; 111 | // Different path lengths for the pretty view of the bistro 112 | for (uint32_t i = 0; i != 10; ++i) { 113 | slides[n++] = (slide_t) { 114 | .quicksave = copy_string("data/saves/bistro/pretty.rt_save"), 115 | .render_settings = { .sampling_strategy = sampling_strategy_nee, .path_length = i }, 116 | .screenshot_frame = 16 * 2048, 117 | .screenshot_path = format_uint("/home/cpeters/projects/cg_lectures/graphics/renders/bistro_path_lengths/%u.png", i), 118 | }; 119 | } 120 | // Different path lengths for the Cornell box 121 | for (uint32_t i = 0; i != 10; ++i) { 122 | slides[n++] = (slide_t) { 123 | .quicksave = copy_string("data/saves/cornell_box/default.rt_save"), 124 | .render_settings = { .sampling_strategy = sampling_strategy_nee, .path_length = i }, 125 | .screenshot_frame = 2048, 126 | .screenshot_path = format_uint("/home/cpeters/projects/cg_lectures/graphics/renders/cornell_box_path_lengths/%u.png", i), 127 | }; 128 | } 129 | // Different path lengths for the living room 130 | for (uint32_t i = 0; i != 10; ++i) { 131 | slides[n++] = (slide_t) { 132 | .quicksave = copy_string("data/saves/living_room/day.rt_save"), 133 | .render_settings = { .sampling_strategy = sampling_strategy_nee, .path_length = i }, 134 | .screenshot_frame = 16 * 2048, 135 | .screenshot_path = format_uint("/home/cpeters/projects/cg_lectures/graphics/renders/living_room_path_lengths/%u.png", i), 136 | }; 137 | } 138 | printf("Defined %u slides.\n", n); 139 | if (n > MAX_SLIDE_COUNT) 140 | printf("WARNING: Wrote %u slides but MAX_SLIDE_COUNT is %u. Increase it.\n", n, MAX_SLIDE_COUNT); 141 | return n; 142 | } 143 | -------------------------------------------------------------------------------- /src/stb_image_write.c: -------------------------------------------------------------------------------- 1 | #define STB_IMAGE_WRITE_IMPLEMENTATION 2 | #include "stb_image_write.h" 3 | -------------------------------------------------------------------------------- /src/string_utilities.c: -------------------------------------------------------------------------------- 1 | #include "string_utilities.h" 2 | #include 3 | #include 4 | 5 | 6 | char* cat_strings(const char* const* const strings, size_t string_count) { 7 | size_t total_length = 0; 8 | for (size_t i = 0; i != string_count; ++i) 9 | total_length += strlen(strings[i]); 10 | char* result = malloc(total_length + 1); 11 | char* cursor = result; 12 | for (size_t i = 0; i != string_count; ++i) { 13 | size_t length = strlen(strings[i]); 14 | memcpy(cursor, strings[i], length); 15 | cursor += length; 16 | } 17 | (*cursor) = '\0'; 18 | return result; 19 | } 20 | 21 | 22 | char* copy_string(const char* string) { 23 | size_t length = strlen(string); 24 | char* result = malloc(length + 1); 25 | memcpy(result, string, length + 1); 26 | return result; 27 | } 28 | -------------------------------------------------------------------------------- /src/string_utilities.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | 8 | //! Provides the number of entries in the given object of array type using a 9 | //! ratio of sizeof() values. It does not work for pointers. 10 | #define COUNT_OF(ARRAY) (sizeof(ARRAY) / sizeof(ARRAY[0])) 11 | 12 | 13 | //! Concatenates arbitrarily many null-terminated strings. The returned 14 | //! null-terminated string that is returned must be freed by the calling side. 15 | char* cat_strings(const char* const* const strings, size_t string_count); 16 | 17 | 18 | //! Creates a copy of the given null-terminated string that must be freed by 19 | //! the calling side 20 | char* copy_string(const char* string); 21 | 22 | 23 | /*! Given a format string that expects exactly one uint32_t (e.g. due to one 24 | %u), this function constructs a null-terminated formatted string that has 25 | to be freed by the calling side.*/ 26 | static inline char* format_uint(const char* format, uint32_t integer) { 27 | int size = snprintf(NULL, 0, format, integer) + 1; 28 | char* result = (char*) malloc(size); 29 | sprintf(result, format, integer); 30 | return result; 31 | } 32 | -------------------------------------------------------------------------------- /src/textures.c: -------------------------------------------------------------------------------- 1 | #include "textures.h" 2 | #include 3 | #include 4 | #include 5 | 6 | 7 | //! The header of a *.vkt texture file 8 | typedef struct { 9 | //! This file format marker should be 0xbc1bc1 (although formats other than 10 | //! BC1 are supported) 11 | uint32_t marker; 12 | //! The used file format version (should be 1) 13 | uint32_t version; 14 | //! The number of mipmap levels 15 | uint32_t mipmap_count; 16 | //! The resolution of the highest-resolution mipmap level in pixels 17 | VkExtent2D extent; 18 | //! The format used for the texture 19 | VkFormat format; 20 | //! The data size in bytes of all mipmaps combined (excluding headers) 21 | VkDeviceSize size; 22 | } texture_header_t; 23 | 24 | 25 | //! Each mipmap in a *.vkt file begins with such a header 26 | typedef struct { 27 | //! The extent of this mipmap in pixels 28 | VkExtent2D extent; 29 | //! The size of the raw data for the mipmap in bytes, excluding headers 30 | VkDeviceSize size; 31 | //! The offset from the beginning of the payload to the mipmap in bytes 32 | VkDeviceSize offset; 33 | } mipmap_header_t; 34 | 35 | 36 | //! Handles data needed for loading a single texture 37 | typedef struct { 38 | //! A file handle for the texture file that is being loaded 39 | FILE* file; 40 | //! The header of the texture file 41 | texture_header_t header; 42 | //! header.mipmap_count headers of mipmaps 43 | mipmap_header_t* mipmap_headers; 44 | } texture_t; 45 | 46 | 47 | /*! Handles data needed whilst loading textures. Unlike most other structures 48 | like this, this one is freed once loading is finished and only the images 49 | are preserved.*/ 50 | typedef struct { 51 | //! \see load_textures() 52 | images_t* images; 53 | //! \see load_textures() 54 | uint32_t texture_count; 55 | //! Data about the individual textures being loaded 56 | texture_t* textures; 57 | } textures_t; 58 | 59 | 60 | /*! Opens the given texture file, loads its header and the headers of its 61 | mipmaps. Returns 0 upon success. Upon failure, the calling side must free 62 | the partially initialized texture.*/ 63 | int init_texture(texture_t* texture, const char* texture_file_path) { 64 | memset(texture, 0, sizeof(*texture)); 65 | FILE* file = texture->file = fopen(texture_file_path, "rb"); 66 | if (!file) { 67 | printf("Failed to open the texture file at %s. Please check path and permissions.\n", texture_file_path); 68 | return 1; 69 | } 70 | // Load the texture header 71 | fread(&texture->header.marker, sizeof(uint32_t), 1, file); 72 | if (texture->header.marker != 0xbc1bc1) { 73 | printf("The file at %s does not appear to be a valid *.vkt texture file as it does not start with the appropriate file marker. Images from widely used formats must be piped through a texture conversion tool before loading them.\n", texture_file_path); 74 | return 1; 75 | } 76 | fread(&texture->header.version, sizeof(uint32_t), 1, file); 77 | if (texture->header.version != 1) { 78 | printf("The texture file at %s uses file format version %u, which is not supported.\n", texture_file_path, texture->header.version); 79 | return 1; 80 | } 81 | fread(&texture->header.mipmap_count, sizeof(uint32_t), 1, file); 82 | fread(&texture->header.extent, sizeof(uint32_t), 2, file); 83 | fread(&texture->header.format, sizeof(uint32_t), 1, file); 84 | fread(&texture->header.size, sizeof(uint64_t), 1, file); 85 | // Load mipmap headers 86 | texture->mipmap_headers = calloc(texture->header.mipmap_count, sizeof(mipmap_header_t)); 87 | for (uint32_t i = 0; i != texture->header.mipmap_count; ++i) { 88 | mipmap_header_t* header = &texture->mipmap_headers[i]; 89 | fread(&header->extent, sizeof(uint32_t), 2, file); 90 | fread(&header->size, sizeof(uint64_t), 1, file); 91 | fread(&header->offset, sizeof(uint64_t), 1, file); 92 | } 93 | return 0; 94 | } 95 | 96 | 97 | //! A callback for fill_images() that loads a single mipmap from an already 98 | //! opened texture file 99 | void write_mipmap(void* image_data, uint32_t image_index, const VkImageSubresource* subresource, VkDeviceSize buffer_size, const VkImageCreateInfo* image_info, const VkExtent3D* subresource_extent, const void* context) { 100 | const textures_t* textures = (const textures_t*) context; 101 | const texture_t* texture = &textures->textures[image_index]; 102 | if (buffer_size == texture->mipmap_headers[subresource->mipLevel].size) 103 | fread(image_data, sizeof(uint8_t), buffer_size, texture->file); 104 | else 105 | printf("The data block for mipmap %u of texture %u was supposed to have %lu bytes but had %lu bytes. Skipping this mipmap.\n", subresource->mipLevel, image_index, buffer_size, texture->mipmap_headers[subresource->mipLevel].size); 106 | } 107 | 108 | 109 | void free_textures(textures_t* textures, const device_t* device); 110 | 111 | 112 | int load_textures(images_t* images, const device_t* device, const char* const* texture_file_paths, uint32_t texture_count, VkImageUsageFlags usage, VkImageLayout image_layout) { 113 | memset(images, 0, sizeof(*images)); 114 | textures_t textures = { 115 | .images = images, 116 | .texture_count = texture_count, 117 | }; 118 | textures.textures = calloc(texture_count, sizeof(texture_t)); 119 | // Load texture meta data and create images 120 | image_request_t* image_requests = calloc(texture_count, sizeof(image_request_t)); 121 | usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT; 122 | for (uint32_t i = 0; i != texture_count; ++i) { 123 | texture_t* texture = &textures.textures[i]; 124 | if (init_texture(texture, texture_file_paths[i])) { 125 | printf("Failed to load texture %u out of %u. Its file path is %s. Aborting.\n", i, texture_count, texture_file_paths[i]); 126 | free(image_requests); 127 | free_textures(&textures, device); 128 | return 1; 129 | } 130 | image_requests[i] = (image_request_t) { 131 | .image_info = { 132 | .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, 133 | .arrayLayers = 1, 134 | .extent = { texture->header.extent.width, texture->header.extent.height, 1, }, 135 | .format = texture->header.format, 136 | .imageType = VK_IMAGE_TYPE_2D, 137 | .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, 138 | .mipLevels = texture->header.mipmap_count, 139 | .samples = VK_SAMPLE_COUNT_1_BIT, 140 | .usage = usage, 141 | }, 142 | .view_info = { 143 | .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, 144 | .viewType = VK_IMAGE_VIEW_TYPE_2D, 145 | .subresourceRange = { 146 | .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, 147 | }, 148 | }, 149 | }; 150 | } 151 | if (create_images(images, device, image_requests, texture_count, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)) { 152 | printf("Failed to create/allocate images for %u textures.\n", texture_count); 153 | free(image_requests); 154 | free_textures(&textures, device); 155 | return 1; 156 | } 157 | free(image_requests); 158 | image_requests = NULL; 159 | if (fill_images(images, device, &write_mipmap, VK_IMAGE_LAYOUT_UNDEFINED, image_layout, &textures)) { 160 | printf("Failed to copy texture data for %u textures from files onto the GPU.\n", texture_count); 161 | free_textures(&textures, device); 162 | return 1; 163 | } 164 | textures.images = NULL; 165 | free_textures(&textures, device); 166 | return 0; 167 | } 168 | 169 | 170 | void free_textures(textures_t* textures, const device_t* device) { 171 | if (textures->images) free_images(textures->images, device); 172 | if (textures->textures) { 173 | for (uint32_t i = 0; i != textures->texture_count; ++i) { 174 | texture_t* texture = &textures->textures[i]; 175 | if (texture->file) fclose(texture->file); 176 | free(texture->mipmap_headers); 177 | } 178 | } 179 | free(textures->textures); 180 | memset(textures, 0, sizeof(*textures)); 181 | } 182 | -------------------------------------------------------------------------------- /src/textures.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "vulkan_basics.h" 3 | 4 | 5 | /*! Loads textures from *.vkt files into device-local memory. 6 | \param images The output. Clean up using free_images(). 7 | \param device Output of create_device(). 8 | \param texture_file_paths An array of absolute or relative paths to *.vkt 9 | files. Each entry corresponds to one entry of images->images. 10 | \param texture_count The number of array entries in texture_file_paths. 11 | \param usage The Vulkan usage flags that are to be used for all textures. 12 | Transfer destination usage is always added. 13 | \param image_layout The layout of all created images upon success. 14 | \return 0 upon success.*/ 15 | int load_textures(images_t* images, const device_t* device, const char* const* texture_file_paths, uint32_t texture_count, VkImageUsageFlags usage, VkImageLayout image_layout); 16 | -------------------------------------------------------------------------------- /src/timer.c: -------------------------------------------------------------------------------- 1 | #include "timer.h" 2 | #include "string_utilities.h" 3 | #include 4 | #include 5 | #include 6 | #define GLFW_INCLUDE_VULKAN 7 | #include 8 | 9 | 10 | //! A ring-buffer used to record times at which record_frame_time() was invoked 11 | typedef struct { 12 | //! All the recorded times in seconds as produced by glfwGetTime() 13 | double times[RECORDED_FRAME_COUNT]; 14 | //! The number of times which have been recorded 15 | uint64_t frame_count; 16 | //! The index of the next entry in times that will be overwritten 17 | uint64_t time_index; 18 | } frame_record_t; 19 | 20 | 21 | //! The only instance of frame_record_t 22 | static frame_record_t record = { 23 | .frame_count = 0, 24 | .time_index = 0, 25 | }; 26 | 27 | 28 | void record_frame_time() { 29 | record.times[record.time_index] = glfwGetTime(); 30 | record.time_index = (record.time_index + 1) % RECORDED_FRAME_COUNT; 31 | ++record.frame_count; 32 | } 33 | 34 | 35 | float get_frame_delta() { 36 | if (record.frame_count < 2) 37 | return 0.0f; 38 | else 39 | return (float) ( 40 | record.times[(record.time_index + RECORDED_FRAME_COUNT - 1) % RECORDED_FRAME_COUNT] - 41 | record.times[(record.time_index + RECORDED_FRAME_COUNT - 2) % RECORDED_FRAME_COUNT]); 42 | } 43 | 44 | 45 | //! Comparison function for qsort 46 | int cmp_floats(const void* raw_lhs, const void* raw_rhs) { 47 | float lhs = *(const float*) raw_lhs; 48 | float rhs = *(const float*) raw_rhs; 49 | return (lhs < rhs) ? -1 : ((lhs > rhs) ? 1 : 0); 50 | } 51 | 52 | 53 | frame_time_stats_t get_frame_stats() { 54 | frame_time_stats_t stats; 55 | memset(&stats, 0, sizeof(stats)); 56 | if (record.frame_count < 2) 57 | return stats; 58 | stats.last = get_frame_delta(); 59 | // Gather all recorded frame times 60 | float frame_times[RECORDED_FRAME_COUNT]; 61 | memset(frame_times, 0, sizeof(frame_times)); 62 | uint64_t frame_time_count = record.frame_count - 1; 63 | if (frame_time_count > RECORDED_FRAME_COUNT - 1) 64 | frame_time_count = RECORDED_FRAME_COUNT - 1; 65 | for (uint64_t i = 0; i != frame_time_count; ++i) 66 | frame_times[i] = (float) ( 67 | record.times[(record.time_index + RECORDED_FRAME_COUNT - i - 1) % RECORDED_FRAME_COUNT] - 68 | record.times[(record.time_index + RECORDED_FRAME_COUNT - i - 2) % RECORDED_FRAME_COUNT]); 69 | // Compute the mean 70 | float frame_time_sum = 0.0; 71 | for (uint64_t i = 0; i != frame_time_count; ++i) 72 | frame_time_sum += frame_times[i]; 73 | stats.mean = frame_time_sum / ((float) frame_time_count); 74 | // Sort the frame times 75 | qsort(frame_times, frame_time_count, sizeof(float), &cmp_floats); 76 | // Compute percentiles 77 | float percents[] = { 0.0f, 1.0f, 10.0f, 50.0f, 90.0f, 99.0f, 100.0f }; 78 | float* percentiles[] = { &stats.min, &stats.percentile_1, &stats.percentile_10, &stats.median, &stats.percentile_90, &stats.percentile_99, &stats.max }; 79 | for (uint32_t i = 0; i != COUNT_OF(percents); ++i) { 80 | // Perform linear interpolation between the two adjacent entries 81 | float float_index = 0.01 * percents[i] * ((float) (frame_time_count - 1)); 82 | uint64_t left = (uint64_t) float_index; 83 | float lerp = float_index - (float) left; 84 | uint64_t right = left + 1; 85 | (*percentiles[i]) = (1.0f - lerp) * frame_times[left] + lerp * frame_times[right]; 86 | } 87 | return stats; 88 | } 89 | -------------------------------------------------------------------------------- /src/timer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #define RECORDED_FRAME_COUNT 101 3 | 4 | 5 | //! Various statistics about frame times observed in the most recent 6 | //! RECORDED_FRAME_COUNT - 1 frames (all in seconds) 7 | typedef struct { 8 | //! The most recently measured frame time, as produced by get_frame_delta() 9 | float last; 10 | //! The arithmetic mean frame time 11 | float mean; 12 | //! The median frame time 13 | float median; 14 | //! Various percentiles of frame times (percentile_1 is smallest) 15 | float percentile_1, percentile_10, percentile_90, percentile_99; 16 | //! The minimal and maximal frame time 17 | float min, max; 18 | } frame_time_stats_t; 19 | 20 | 21 | //! Records the current time. Called exactly once per frame to indicate that a 22 | //! new frame has begun. 23 | void record_frame_time(); 24 | 25 | 26 | //! \return The time in seconds elapsed between the two most recent invocations 27 | //! of record_frame_time() or 0.0f if there were less than two invocations 28 | float get_frame_delta(); 29 | 30 | 31 | //! \return Statistics about the frame times observed in the most recent 32 | //! RECORDED_FRAME_COUNT frames or an all 0 object if less than two frames 33 | //! have been recorded. 34 | frame_time_stats_t get_frame_stats(); 35 | -------------------------------------------------------------------------------- /src/vulkan_basics.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #define GLFW_INCLUDE_VULKAN 3 | #include 4 | #include 5 | #include 6 | 7 | 8 | //! Uses device_t* device to load the Vulkan function with the given name. The 9 | //! identifier for the function pointer is the name prefixed with p. 10 | #define VK_LOAD(FUNCTION_NAME) PFN_##FUNCTION_NAME p##FUNCTION_NAME = (PFN_##FUNCTION_NAME) glfwGetInstanceProcAddress(device->instance, #FUNCTION_NAME); 11 | 12 | 13 | //! Gathers Vulkan objects created up to device creation including meta data 14 | typedef struct { 15 | //! The instance used to invoke Vulkan functions 16 | VkInstance instance; 17 | //! All physical devices enumerated by vkEnumeratePhysicalDevices() 18 | VkPhysicalDevice* physical_devices; 19 | //! The number of entries and the used index in physical_devices 20 | uint32_t physical_device_count, physical_device_index; 21 | //! The physical device that is being used 22 | VkPhysicalDevice physical_device; 23 | //! Properties of physical_device 24 | VkPhysicalDeviceProperties physical_device_properties; 25 | //! Memory types and heaps available through physical_device 26 | VkPhysicalDeviceMemoryProperties memory_properties; 27 | //! Properties for acceleration structures (also known as BVHs) 28 | VkPhysicalDeviceAccelerationStructurePropertiesKHR bvh_properties; 29 | //! The Vulkan device created using physical_device 30 | VkDevice device; 31 | //! A queue that supports graphics and compute 32 | VkQueue queue; 33 | //! All queue family properties for all possible queues as enumerated by 34 | //! vkGetPhysicalDeviceQueueFamilyProperties() 35 | VkQueueFamilyProperties* queue_family_properties; 36 | //! The number of entries and the used index in queue_family_properties 37 | uint32_t queue_family_count, queue_family_index; 38 | //! A command pool for the device and queue above 39 | VkCommandPool cmd_pool; 40 | } device_t; 41 | 42 | 43 | //! Possible return values for create_swapchain() 44 | typedef enum { 45 | //! The swapchain has been created successfully 46 | swapchain_result_success = 0, 47 | //! Creation of the swapchain has failed and there is no apparent way to 48 | //! recover from this failure 49 | swapchain_result_failure = 1, 50 | //! Swapchain creation has failed because the window reported a resolution 51 | //! with zero pixels. Presumably, it is minimized and once that changes, 52 | //! swapchain creation may succeed again. 53 | swapchain_result_minimized = 0x76543210, 54 | } swapchain_result_t; 55 | 56 | 57 | //! A swapchain along with corresponding images to be swapped 58 | typedef struct { 59 | //! A Vulkan surface for the window associated with the swapchain 60 | VkSurfaceKHR surface; 61 | //! The present mode that is being used for the swapchain 62 | VkPresentModeKHR present_mode; 63 | //! The swapchain itself 64 | VkSwapchainKHR swapchain; 65 | //! The format of all held images 66 | VkFormat format; 67 | //! The extent of all held images in pixels (i.e. the resolution on screen 68 | //! and the size of the client area of the window) 69 | VkExtent2D extent; 70 | //! The number of array entries in images and views 71 | uint32_t image_count; 72 | //! The images that are swapped by this swapchain 73 | VkImage* images; 74 | //! Views onto the images 75 | VkImageView* views; 76 | } swapchain_t; 77 | 78 | 79 | //! Arrays of buffer requests are to be passed to create_buffers(). They 80 | //! combine a buffer create info with a view create info. 81 | typedef struct { 82 | //! Complete creation info for the buffer 83 | VkBufferCreateInfo buffer_info; 84 | /*! Incpomplete creation info for a view onto the buffer. create_buffers() 85 | will use an appropriate value for buffer. If range is 0, VK_WHOLE_SIZE 86 | is used. If this is left zero-initialized, it indicates that no view 87 | should be created.*/ 88 | VkBufferViewCreateInfo view_info; 89 | } buffer_request_t; 90 | 91 | 92 | //! A single buffer handled by buffers_t. It keeps track of everything that 93 | //! buffers_t has to keep track of per buffer. 94 | typedef struct { 95 | //! A copy of the used buffer request with missing fields filled in 96 | buffer_request_t request; 97 | //! The buffer itself 98 | VkBuffer buffer; 99 | //! A view onto the buffer as requested or NULL if none was requested 100 | VkBufferView view; 101 | //! The offset in bytes from the start of the memory allocation to this 102 | //! buffer 103 | VkDeviceSize memory_offset; 104 | //! The size in bytes of the memory block for this buffer in its 105 | //! allocation. At least as large as the buffer itself, often bigger. 106 | VkDeviceSize memory_size; 107 | } buffer_t; 108 | 109 | 110 | //! Handles a list of buffers that all share one memory allocation 111 | typedef struct { 112 | //! The number of held buffers 113 | uint32_t buffer_count; 114 | //! An array of all held buffers 115 | buffer_t* buffers; 116 | //! The memory allocation used to serve all buffers 117 | VkDeviceMemory allocation; 118 | //! The total size of the memory allocation used for all buffers 119 | VkDeviceSize size; 120 | } buffers_t; 121 | 122 | 123 | //! Arrays of image requests are to be passed to create_images(). They combine 124 | //! an image create info with a view create info. 125 | typedef struct { 126 | //! Complete creation info for the image 127 | VkImageCreateInfo image_info; 128 | /*! Incpomplete creation info for a view onto the image. create_images() 129 | will use an appropriate value for image. If format, 130 | subresourceRange.layerCount or subresourceRange.levelCount are zero, 131 | create_images() uses values that match the image and use the whole 132 | range. If this is left zero-initialized, it indicates that no view 133 | should be created.*/ 134 | VkImageViewCreateInfo view_info; 135 | } image_request_t; 136 | 137 | 138 | //! A single image handled by images_t. It keeps track of everything that 139 | //! images_t has to keep track of per image. 140 | typedef struct { 141 | //! A copy of the request with a few incomplete entries filled in 142 | image_request_t request; 143 | //! The image itself 144 | VkImage image; 145 | //! A view onto the image or NULL if none was requested 146 | VkImageView view; 147 | //! The index of the memory allocation in images_t that serves this image 148 | uint32_t allocation_index; 149 | //! Whether the allocation for this image is only used by this image 150 | //! because it has indicated a preference for dedicated allocations 151 | bool uses_dedicated_allocation; 152 | //! The offset in bytes from the start of the memory allocation to this 153 | //! image 154 | VkDeviceSize memory_offset; 155 | //! The size in bytes of the memory block for this image in its allocation. 156 | //! At least as large as the image data itself, often bigger. 157 | VkDeviceSize memory_size; 158 | } image_t; 159 | 160 | 161 | //! Handles a list of images, maybe with views, along with enough memory 162 | //! allocations to serve all of them 163 | typedef struct { 164 | //! The number of held images 165 | uint32_t image_count; 166 | //! An array of all held images 167 | image_t* images; 168 | //! The number of memory allocations used to serve all images 169 | uint32_t allocation_count; 170 | //! The memory allocations used to serve all images 171 | VkDeviceMemory* allocations; 172 | } images_t; 173 | 174 | 175 | //! Different types of copies between buffers and images 176 | typedef enum { 177 | //! \see copy_buffer_to_buffer_t 178 | copy_type_buffer_to_buffer, 179 | //! \see copy_buffer_to_image_t 180 | copy_type_buffer_to_image, 181 | //! \see copy_image_to_image_t 182 | copy_type_image_to_image, 183 | } copy_type_t; 184 | 185 | 186 | //! Specification of how a memory region within one buffer should be copied to 187 | //! another buffer 188 | typedef struct { 189 | //! The source and destination buffers (with appropriate transfer usage 190 | //! flags) 191 | VkBuffer src, dst; 192 | //! Specification of the memory region to be copied within these buffers 193 | VkBufferCopy copy; 194 | } copy_buffer_to_buffer_t; 195 | 196 | 197 | //! Specification of how data from a buffer should be copied to an image 198 | typedef struct { 199 | //! The source buffer (with transfer source usage) 200 | VkBuffer src; 201 | //! The destination image (with transfer destination usage) 202 | VkImage dst; 203 | //! The layout of dst before and after the copy (copying makes a transition) 204 | VkImageLayout dst_old_layout, dst_new_layout; 205 | //! Specification of the parts of the buffer and image between which 206 | //! copying takes place 207 | VkBufferImageCopy copy; 208 | } copy_buffer_to_image_t; 209 | 210 | 211 | //! Specification of how part of an image should be copied to another image 212 | typedef struct { 213 | //! The source and destination image (with appropriate transfer usage 214 | //! flags) 215 | VkImage src, dst; 216 | //! The layout of src at the time of the copy 217 | VkImageLayout src_layout; 218 | //! The layout of dst before and after the copy (copying makes a transition) 219 | VkImageLayout dst_old_layout, dst_new_layout; 220 | //! Specification of the image regions to be copied within these images 221 | VkImageCopy copy; 222 | } copy_image_to_image_t; 223 | 224 | 225 | //! A union holding one of the copy_*_to_*_t types 226 | typedef struct { 227 | //! Indicates which entry of the union is meaningful 228 | copy_type_t type; 229 | union copy_request_u { 230 | copy_buffer_to_buffer_t buffer_to_buffer; 231 | copy_buffer_to_image_t buffer_to_image; 232 | copy_image_to_image_t image_to_image; 233 | } u; 234 | } copy_request_t; 235 | 236 | 237 | //! Specifies all the flags and defines that are needed to compile a GLSL 238 | //! or HLSL shader into SPIR-V 239 | typedef struct { 240 | //! The file path to the source code for the shader 241 | char* shader_path; 242 | /*! Defines that are to be used by the preprocessor whilst compiling the 243 | shader. These are null-terminated strings of the format "IDENTIFIER" 244 | or "IDENTIFIER=VALUE".*/ 245 | char** defines; 246 | //! The number of array-entries in defines 247 | uint32_t define_count; 248 | //! A single shader stage that is to be used for compilation 249 | VkShaderStageFlags stage; 250 | //! The name of the function used as entry point. Usually "main". 251 | char* entry_point; 252 | //! Additional arguments to be passed to glslangValidator. The options 253 | //! -e -g -o -Od -S --target-env -V are used internally. NULL for none. 254 | char* args; 255 | //! The file path for the output file containing the SPIR-V code. If this 256 | //! is NULL, it defaults to file_path with .spv attached. 257 | char* spirv_path; 258 | } shader_compilation_request_t; 259 | 260 | 261 | //! Handles multiple descriptor sets with a common layout and associated 262 | //! objects 263 | typedef struct { 264 | //! The descriptor set layout used by pipeline_layout 265 | VkDescriptorSetLayout descriptor_set_layout; 266 | //! A pipeline layout that matches descriptor_set_layout 267 | VkPipelineLayout pipeline_layout; 268 | //! The descriptor pool used to allocate descriptor_sets 269 | VkDescriptorPool descriptor_pool; 270 | //! An array of descriptor sets with the layout above 271 | VkDescriptorSet* descriptor_sets; 272 | //! The number of array entries in descriptor_sets 273 | uint32_t descriptor_set_count; 274 | } descriptor_sets_t; 275 | 276 | 277 | /*! Creates a Vulkan device and related objects. 278 | \param device The output. Clean up with free_device(). 279 | \param app_name An application name that goes into the application info. 280 | \param physical_device_index The index of the physical device (i.e. GPU) to 281 | be used. Available GPUs are printed by create_device(). If unsure, pass 282 | 0. 283 | \return 0 upon success.*/ 284 | int create_device(device_t* device, const char* app_name, uint32_t physical_device_index); 285 | 286 | 287 | void free_device(device_t* device); 288 | 289 | 290 | /*! Creates a swapchain in the given window. 291 | \param swapchain The output. Clean up with free_swapchain(). 292 | \param device Output of create_device(). 293 | \param window A window, typically created using glfwCreateWindow(). Make 294 | sure to invoke glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API) before 295 | creating it. 296 | \param use_vsync true to use a present mode that waits for vertical 297 | synchronization, thus capping the frame rate to the refresh rate of the 298 | display. false to churn out frames as fast as possible, tolerating 299 | tearing artifacts in the process. This request may be ignored. 300 | \return 0 upon success. In general one of the swapchain_result_t codes. 301 | swapchain_result_minimized deserves special treatment.*/ 302 | swapchain_result_t create_swapchain(swapchain_t* swapchain, const device_t* device, GLFWwindow* window, bool use_vsync); 303 | 304 | 305 | void free_swapchain(swapchain_t* swapchain, const device_t* device); 306 | 307 | 308 | /*! Finds a Vulkan memory type matching given conditions. 309 | \param type_index Overwritten by the smallest matching index. 310 | \param device Memory types of the physical device in this device are 311 | considered. 312 | \param memory_type_bits A bit mask as in 313 | VkMemoryRequirements.memoryTypeBits. The bit for the output type index 314 | will be set in this mask. 315 | \param memory_properties Flags that have to be met by the returned memory 316 | type. 317 | \return 0 upon success.*/ 318 | int find_memory_type(uint32_t* type_index, const device_t* device, uint32_t memory_type_bits, VkMemoryPropertyFlags memory_properties); 319 | 320 | 321 | //! Returns the smallest integer multiple of alignment that is at least as 322 | //! large as offset 323 | static inline VkDeviceSize align_offset(VkDeviceSize offset, VkDeviceSize alignment) { 324 | return ((offset + alignment - 1) / alignment) * alignment; 325 | } 326 | 327 | 328 | /*! Creates a list of buffers along with a single memory allocation serving all 329 | of them. 330 | \param buffers The output. Clean up with free_buffers(). 331 | \param device Output of create_device(). 332 | \param requests The complete create infos for each individual buffer to be 333 | created and optionally for the view onto it. 334 | \param request_count The number of buffers to be created. Also the number 335 | of valid array entries in requests. 336 | \param memory_properties Flags that are to be enforced for the used memory 337 | allocation. 338 | \param alignment The offset of each buffer in bytes within the memory 339 | allocation is guaranteed to be a multiple of this non-zero integer. 340 | Pass one, if you do not care. Alignment requirements of individual 341 | buffers are respected in all cases. 342 | \return 0 upon success.*/ 343 | int create_buffers(buffers_t* buffers, const device_t* device, const buffer_request_t* requests, uint32_t request_count, VkMemoryPropertyFlags memory_properties, VkDeviceSize alignment); 344 | 345 | 346 | void free_buffers(buffers_t* buffers, const device_t* device); 347 | 348 | 349 | //! Prints all key information about the given image requests to stdout. Useful 350 | //! for error reporting. 351 | void print_image_requests(const image_request_t* requests, uint32_t request_count); 352 | 353 | 354 | /*! Creates an arbitrary number of images, allocates memory for them and 355 | optionally creates views. 356 | \param images The output. Clean up with free_images(). 357 | \param device Output of create_device(). 358 | \param requests A description of every image that is to be created. 359 | \param request_count Number of array entries in requests. 360 | \param memory_properties Flags that are to be enforced for the used memory 361 | allocation. 362 | \return 0 upon success.*/ 363 | int create_images(images_t* images, const device_t* device, const image_request_t* requests, uint32_t request_count, VkMemoryPropertyFlags memory_properties); 364 | 365 | 366 | void free_images(images_t* images, const device_t* device); 367 | 368 | 369 | /*! Performs layout transitions for images using image memory barriers. 370 | \param images The images on which to operate. 371 | \param device Output of create_device(). 372 | \param old_layouts Array of size images->image_count or NULL if all images 373 | to transition are still in the layout they had at creation. Indicates 374 | the current layout for each image. 375 | \param new_layouts Array of size images->image_count indicating the layouts 376 | to which the images should be transitioned. Entries of 377 | VK_IMAGE_LAYOUT_UNDEFINED (which is 0) indicate that no layout 378 | transition should be performed for this image. 379 | \param ranges Subresource ranges to be transitioned for each image or NULL 380 | to use all layers, all levels and the color aspect. 381 | \return 0 upon success.*/ 382 | int transition_image_layouts(images_t* images, const device_t* device, const VkImageLayout* old_layouts, const VkImageLayout* new_layouts, const VkImageSubresourceRange* ranges); 383 | 384 | 385 | //! This little helper converts VkImageSubresourceLayers to 386 | //! VkImageSubresourceRange in the natural way 387 | static inline VkImageSubresourceRange image_subresource_layers_to_range(const VkImageSubresourceLayers* layers) { 388 | VkImageSubresourceRange result = { 389 | .aspectMask = layers->aspectMask, 390 | .baseArrayLayer = layers->baseArrayLayer, 391 | .baseMipLevel = layers->mipLevel, 392 | .levelCount = 1, 393 | .layerCount = layers->layerCount, 394 | }; 395 | return result; 396 | } 397 | 398 | 399 | /*! Performs the requested copies between buffers and images and blocks 400 | execution until this work is completed. 401 | \param device Output of create_device(). 402 | \param requests An array of copy requests. 403 | \param request_count Number of array entries. 404 | \return 0 upon success. Upon failure the contents and layouts of 405 | destination buffers/images are undefined.*/ 406 | int copy_buffers_or_images(const device_t* device, const copy_request_t* requests, uint32_t request_count); 407 | 408 | 409 | //! Specialized version of copy_buffers_or_images() to copy buffers to buffers 410 | int copy_buffers(const device_t* device, const copy_buffer_to_buffer_t* requests, uint32_t request_count); 411 | 412 | 413 | //! Specialized version of copy_buffers_or_images() to copy buffers to images 414 | int copy_buffers_to_images(const device_t* device, const copy_buffer_to_image_t* requests, uint32_t request_count); 415 | 416 | 417 | //! Specialized version of copy_buffers_or_images() to copy images to images 418 | int copy_images(const device_t* device, const copy_image_to_image_t* requests, uint32_t request_count); 419 | 420 | 421 | /*! A callback type used for fill_buffers(). It has to fill a range of mapped 422 | memory for a buffer with the data that is to go into that buffer. 423 | \param buffer_data The start of the mapped memory range into which the 424 | buffer data should be written. 425 | \param buffer_index The index of the buffer for which data is to be written 426 | within its buffers_t object. Upon success, it is guaranteed that 427 | fill_buffers() invokes this callback exactly once with each valid value 428 | of buffer_index in increasing order. 429 | \param buffer_size The number of bytes that should be written to 430 | buffer_data. 431 | \param context Passed through by fill_buffers().*/ 432 | typedef void (*write_buffer_t)(void* buffer_data, uint32_t buffer_index, VkDeviceSize buffer_size, const void* context); 433 | 434 | 435 | /*! Creates a host-local staging buffer for each of the given buffers, maps it 436 | and lets a callback fill it with arbitrary data. Then these staging buffers 437 | are copied to the given buffers and destroyed. 438 | \param buffers Output of create_buffers(). The data stored by these buffers 439 | will be overwritten. 440 | \param device Output of create_device(). 441 | \param write_buffer See write_buffer_t. 442 | \param context Passed to (*write_buffer)() as last parameter. Typically a 443 | pointer to a user-defined struct providing information needed by the 444 | callback. 445 | \param 0 upon success. Upon failure, the contents of the buffers are 446 | undefined.*/ 447 | int fill_buffers(const buffers_t* buffers, const device_t* device, write_buffer_t write_buffer, const void* context); 448 | 449 | 450 | /*! A callback type used for fill_images(). It has to fill a range of mapped 451 | memory for a buffer whose data will be copied into a subresource of an 452 | image with the image data. The buffer should be tightly packed. 453 | \param image_data The start of the mapped memory range into which the 454 | image data should be written. 455 | \param image_index The index of the image for which data is to be written 456 | within its images_t object. 457 | \param subresource The subresource of the image for which data is to be 458 | written (i.e. a mipmap of a level of an aspect of the image). The 459 | aspectMask is always VK_IMAGE_ASPECT_COLOR_BIT. 460 | \param buffer_size The number of bytes that should be written to 461 | image_data. 462 | \param image_info Info about the image for which data is to be written. 463 | \param subresource_extent The resolution of the subresource being written. 464 | \param context Passed through by fill_images(). 465 | \note fill_images will use each valid tuple (image_index, 466 | subresource.mipLevel, subresource.arrayLayer) exactly once and in 467 | lexicographic order.*/ 468 | typedef void (*write_image_subresource_t)(void* image_data, uint32_t image_index, const VkImageSubresource* subresource, VkDeviceSize buffer_size, const VkImageCreateInfo* image_info, const VkExtent3D* subresource_extent, const void* context); 469 | 470 | 471 | /*! Creates a host-local staging buffer for each subresource of the given 472 | images, maps it and lets a callback fill it with image data. Then these 473 | staging buffers are copied to the given images and destroyed. 474 | \param images Output of create_images(). The contents of these images will 475 | be overwritten. All of these images must have aspect color and any 476 | other aspects will be silently ignored. 477 | \param device Output of create_device(). 478 | \param write_subresource See write_image_subresource_t. 479 | \param old_layout The layout that the given images have prior to the 480 | operation. 481 | \param new_layout The layout that the given images should have after the 482 | operation concludes. A common choice would be 483 | VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL. 484 | \param context Passed to (*write_subresource)() as last parameter. 485 | Typically a pointer to a user-defined struct providing information 486 | needed by the callback. 487 | \param 0 upon success. Upon failure, the contents of the images are 488 | undefined.*/ 489 | int fill_images(const images_t* images, const device_t* device, write_image_subresource_t write_subresource, VkImageLayout old_layout, VkImageLayout new_layout, const void* context); 490 | 491 | 492 | //! \return The name of the given shader stage as expected by glslangValidator 493 | //! or an empty string if stage is not a single stage bit. 494 | const char* get_shader_stage_name(VkShaderStageFlags stage); 495 | 496 | 497 | //! Compiles a GLSL or HLSL shader to SPIR-V as specified in the given request. 498 | int compile_shader(const shader_compilation_request_t* request); 499 | 500 | 501 | //! Forwards to compile_shader(), but if that fails, it prompts the user on the 502 | //! command line whether to try again. 503 | int compile_shader_with_retry(const shader_compilation_request_t* request); 504 | 505 | 506 | //! Forwards to compile_shader() or compile_shader_with_retry() and creates a 507 | //! Vulkan shader module out of the compiled SPIR-V code 508 | int compile_and_create_shader_module(VkShaderModule* module, const device_t* device, const shader_compilation_request_t* request, bool retry); 509 | 510 | 511 | /*! A utility function to avoid redundant code when defining bindings in 512 | descriptor sets. 513 | \param bindings Binding specifications that are to be modified in place. 514 | \param binding_count The number of array entries in bindings. 515 | \param min_descriptor_count The minimal value for descriptorCount, e.g. 1. 516 | \param shared_stages stageFlags is ORed with these flags.*/ 517 | void complete_descriptor_set_layout_bindings(VkDescriptorSetLayoutBinding* bindings, uint32_t binding_count, uint32_t min_descriptor_count, VkShaderStageFlags shared_stages); 518 | 519 | 520 | /*! A utility function to avoid redundant code when defining descriptor set 521 | writes. For each write, it sets sType and dstSet appropriately and copies 522 | descriptorType and descriptorCount from the binding where binding matches 523 | dstBinding (if any). 524 | \param writes Descriptor set writes to modify in place. 525 | \param write_count Number of array entries in writes. 526 | \param bindings Bindings from which to copy information. 527 | \param binding_count Number of array entries in bindings. 528 | \param destination_set The value to use for dstSet in all writes. Pass NULL 529 | to leave it unchanged.*/ 530 | void complete_descriptor_set_writes(VkWriteDescriptorSet* writes, uint32_t write_count, const VkDescriptorSetLayoutBinding* bindings, uint32_t binding_count, VkDescriptorSet destination_set); 531 | 532 | 533 | /*! Creates all the objects needed to create descriptor sets, the descriptor 534 | sets themselves and a matching pipeline layout. 535 | \param descriptor_sets The output. Clean up with free_descriptor_sets(). 536 | \param device Output of create_device(). 537 | \param bindings Specifications of the bindings. You may want to use 538 | complete_descriptor_set_layout_bindings() to populate this array. 539 | \param binding_count The number of array entries in bindings. 540 | \param descriptor_set_count The number of descriptor sets with identical 541 | layout which should be created. 542 | \return 0 upon success.*/ 543 | int create_descriptor_sets(descriptor_sets_t* descriptor_sets, const device_t* device, VkDescriptorSetLayoutBinding* bindings, uint32_t binding_count, uint32_t descriptor_set_count); 544 | 545 | 546 | void free_descriptor_sets(descriptor_sets_t* descriptor_sets, const device_t* device); 547 | -------------------------------------------------------------------------------- /src/vulkan_formats.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #define GLFW_INCLUDE_VULKAN 3 | #include 4 | #include 5 | 6 | 7 | //! Enumeration of all possible format compatibility classes 8 | typedef enum { 9 | FORMAT_CLASS_8_BIT, 10 | FORMAT_CLASS_16_BIT, 11 | FORMAT_CLASS_8_BIT_ALPHA, 12 | FORMAT_CLASS_24_BIT, 13 | FORMAT_CLASS_32_BIT, 14 | FORMAT_CLASS_48_BIT, 15 | FORMAT_CLASS_64_BIT, 16 | FORMAT_CLASS_96_BIT, 17 | FORMAT_CLASS_128_BIT, 18 | FORMAT_CLASS_192_BIT, 19 | FORMAT_CLASS_256_BIT, 20 | FORMAT_CLASS_D16, 21 | FORMAT_CLASS_D24, 22 | FORMAT_CLASS_D32, 23 | FORMAT_CLASS_S8, 24 | FORMAT_CLASS_D16S8, 25 | FORMAT_CLASS_D24S8, 26 | FORMAT_CLASS_D32S8, 27 | FORMAT_CLASS_BC1_RGB, 28 | FORMAT_CLASS_BC1_RGBA, 29 | FORMAT_CLASS_BC2, 30 | FORMAT_CLASS_BC3, 31 | FORMAT_CLASS_BC4, 32 | FORMAT_CLASS_BC5, 33 | FORMAT_CLASS_BC6H, 34 | FORMAT_CLASS_BC7, 35 | FORMAT_CLASS_ETC2_RGB, 36 | FORMAT_CLASS_ETC2_RGBA, 37 | FORMAT_CLASS_ETC2_EAC_RGBA, 38 | FORMAT_CLASS_EAC_R, 39 | FORMAT_CLASS_EAC_RG, 40 | FORMAT_CLASS_ASTC_4X4, 41 | FORMAT_CLASS_ASTC_5X4, 42 | FORMAT_CLASS_ASTC_5X5, 43 | FORMAT_CLASS_ASTC_6X5, 44 | FORMAT_CLASS_ASTC_6X6, 45 | FORMAT_CLASS_ASTC_8X5, 46 | FORMAT_CLASS_ASTC_8X6, 47 | FORMAT_CLASS_ASTC_8X8, 48 | FORMAT_CLASS_ASTC_10X5, 49 | FORMAT_CLASS_ASTC_10X6, 50 | FORMAT_CLASS_ASTC_10X8, 51 | FORMAT_CLASS_ASTC_10X10, 52 | FORMAT_CLASS_ASTC_12X10, 53 | FORMAT_CLASS_ASTC_12X12, 54 | FORMAT_CLASS_32_BIT_G8B8G8R8, 55 | FORMAT_CLASS_32_BIT_B8G8R8G8, 56 | FORMAT_CLASS_8_BIT_3_PLANE_420, 57 | FORMAT_CLASS_8_BIT_2_PLANE_420, 58 | FORMAT_CLASS_8_BIT_3_PLANE_422, 59 | FORMAT_CLASS_8_BIT_2_PLANE_422, 60 | FORMAT_CLASS_8_BIT_3_PLANE_444, 61 | FORMAT_CLASS_64_BIT_R10G10B10A10, 62 | FORMAT_CLASS_64_BIT_G10B10G10R10, 63 | FORMAT_CLASS_64_BIT_B10G10R10G10, 64 | FORMAT_CLASS_10_BIT_3_PLANE_420, 65 | FORMAT_CLASS_10_BIT_2_PLANE_420, 66 | FORMAT_CLASS_10_BIT_3_PLANE_422, 67 | FORMAT_CLASS_10_BIT_2_PLANE_422, 68 | FORMAT_CLASS_10_BIT_3_PLANE_444, 69 | FORMAT_CLASS_64_BIT_R12G12B12A12, 70 | FORMAT_CLASS_64_BIT_G12B12G12R12, 71 | FORMAT_CLASS_64_BIT_B12G12R12G12, 72 | FORMAT_CLASS_12_BIT_3_PLANE_420, 73 | FORMAT_CLASS_12_BIT_2_PLANE_420, 74 | FORMAT_CLASS_12_BIT_3_PLANE_422, 75 | FORMAT_CLASS_12_BIT_2_PLANE_422, 76 | FORMAT_CLASS_12_BIT_3_PLANE_444, 77 | FORMAT_CLASS_64_BIT_G16B16G16R16, 78 | FORMAT_CLASS_64_BIT_B16G16R16G16, 79 | FORMAT_CLASS_16_BIT_3_PLANE_420, 80 | FORMAT_CLASS_16_BIT_2_PLANE_420, 81 | FORMAT_CLASS_16_BIT_3_PLANE_422, 82 | FORMAT_CLASS_16_BIT_2_PLANE_422, 83 | FORMAT_CLASS_16_BIT_3_PLANE_444, 84 | FORMAT_CLASS_PVRTC1_2BPP, 85 | FORMAT_CLASS_PVRTC1_4BPP, 86 | FORMAT_CLASS_PVRTC2_2BPP, 87 | FORMAT_CLASS_PVRTC2_4BPP, 88 | FORMAT_CLASS_8_BIT_2_PLANE_444, 89 | FORMAT_CLASS_10_BIT_2_PLANE_444, 90 | FORMAT_CLASS_12_BIT_2_PLANE_444, 91 | FORMAT_CLASS_16_BIT_2_PLANE_444, 92 | } format_class_t; 93 | 94 | 95 | //! Provides meta data about a Vulkan format that can for example be used to 96 | //! figure out how to copy data from a buffer to an image. Autogenerated from 97 | //! vk.xml by ../tools/vulkan_formats.py. 98 | typedef struct { 99 | //! The format compatibility class of the format. Copying between two 100 | //! formats of the same class is permitted. 101 | format_class_t cls; 102 | //! The number of bytes per texel block 103 | VkDeviceSize block_size; 104 | //! The number of texels per texel block 105 | uint32_t texels_per_block; 106 | //! For packed formats such as VK_FORMAT_R5G6B5_UNORM_PACK16, this is the 107 | //! number of bits into which a color is being packed. 0 otherwise. 108 | uint32_t packed_bits; 109 | } format_description_t; 110 | 111 | 112 | //! Provides meta data about the given Vulkan format. 113 | //! \see format_description_t 114 | format_description_t get_format_description(VkFormat format); 115 | -------------------------------------------------------------------------------- /tools/io_export_spherical_lights_blender40.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from struct import pack 3 | 4 | # To define light sources, create a mesh for the unit sphere named 5 | # "spherical_light" in Blender. Give it a material called _emission. Then place 6 | # suitably scaled instances of this mesh in your scene wherever you want a 7 | # spherical light. All of them have equal brightness. Run this script to create 8 | # the *.lights file and io_export_vulkan_blender28.py to export a *.vks file 9 | # that includes the geometry for the spherical lights. 10 | lights = [object for object in bpy.context.selected_objects if object.data is not None and object.data.name == "spherical_light"] 11 | with open("scene.lights", "wb") as file: 12 | file.write(pack("I", len(lights))) 13 | for light in lights: 14 | file.write(pack("fff", *light.location)) 15 | file.write(pack("f", light.scale[0])) 16 | -------------------------------------------------------------------------------- /tools/material_conversion.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021, Christoph Peters, Karlsruhe Institute of Technology 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | 17 | import numpy as np 18 | import os 19 | import imageio 20 | from subprocess import Popen 21 | import re 22 | from time import sleep 23 | 24 | 25 | def linear_to_srgb(linear): 26 | """ 27 | Converts all scalars in the given array (which should be normalized to the 28 | range from zero to one) from a linear scale to the sRGB scale. 29 | """ 30 | linear = np.maximum(0.0, linear) 31 | return np.where(linear <= 0.0031308, 12.92 * linear, 1.055 * linear ** (1.0/2.4) - 0.055) 32 | 33 | 34 | def complete_materials(directory, material_dict): 35 | """ 36 | Looks for material textures in a directory. If a set does not consist of 37 | base color, specular and normal, the missing textures are created using 38 | constant default values or values from the given dictionary. 39 | :param directory: The path to the directory to look in. 40 | :param material_dict: A dictionary mapping material names to triples 41 | (base_color, normal, specular). Each entry can be either None to 42 | indicate that the global default should be used, a triple of floats 43 | between 0.0 and 1.0 (-1.0 and 1.0 for normals) that will be converted 44 | to LDR values in the appropriate way or a triple of integers between 45 | 0 and 255. Either way, it defines what goes into the texture. If the 46 | dictionary holds material names that do not exist in the directory, 47 | new materials are created. It is legal to omit the normal entry. If 48 | specular is just a float, it is interpreted as roughness of a 49 | dielectric. If base_color is just a float, it is interpreted as grey. 50 | """ 51 | suffixes = ["_BaseColor", "_Normal", "_Specular"] 52 | global_defaults = ((0, 0, 0), (128, 128, 255), (255, 128, 0)) 53 | # Unify the meaning of dictionary entries to LDR values 54 | full_materials = list() 55 | for name, defaults in material_dict.items(): 56 | if len(defaults) == 2: 57 | defaults = (defaults[0], None, defaults[1]) 58 | defaults = [global_defaults[i] if default is None else default for i, default in enumerate(defaults)] 59 | base_color, normal, specular = defaults 60 | if isinstance(base_color, float): 61 | base_color = (base_color, base_color, base_color) 62 | if isinstance(base_color[0], float): 63 | base_color = tuple(np.asarray(np.round(linear_to_srgb(base_color) * 255.0), dtype=np.uint8)) 64 | if isinstance(normal[0], float): 65 | normal = tuple(np.asarray(np.round((np.asarray(normal) * 0.5 + 0.5) * 255.0), dtype=np.uint8)) 66 | if isinstance(specular, float): 67 | specular = (1.0, specular, 0.0) 68 | if isinstance(specular[0], float): 69 | specular = tuple(np.asarray(np.round(np.asarray(specular) * 255.0), dtype=np.uint8)) 70 | full_materials.append((name, (base_color, normal, specular))) 71 | # Add entries for (potentially incomplete) materials 72 | file_list = os.listdir(directory) 73 | for file in file_list: 74 | match = re.search(r"((_BaseColor\.)|(_Normal\.)|(_Specular\.))", file) 75 | if match is not None and os.path.splitext(file)[1] != ".vkt": 76 | prefix = file[0:match.start(1)] 77 | if prefix not in material_dict: 78 | full_materials.append((prefix, global_defaults)) 79 | # Create the missing texture files 80 | file_list_no_extension = frozenset([os.path.splitext(file)[0] for file in file_list]) 81 | for name, defaults in full_materials: 82 | for suffix, default in zip(suffixes, defaults): 83 | texture_name = name + suffix 84 | if texture_name not in file_list_no_extension: 85 | texture_path = os.path.join(directory, texture_name + ".png") 86 | image = np.asarray(default, dtype=np.uint8)[np.newaxis, np.newaxis, :] 87 | image = image.repeat(4, 0).repeat(4, 1) 88 | imageio.imsave(texture_path, image) 89 | print("Created %s." % texture_path) 90 | 91 | 92 | def convert_materials(destination_directory, source_directory, skip_existing=True, texture_conversion_path="texture_conversion/build/texture_conversion"): 93 | """ 94 | This function performs batch conversion of textures from a common file 95 | format (anything supported by stb_image) into the file format of the 96 | renderer, which has precomputed mipmaps and block compression. 97 | :param destination_directory: Texture files with identical name but file 98 | format extension .vkt are written to this directory. Gets created if it 99 | does not exist. 100 | :param source_directory: The directory that is searched for textures. Only 101 | file names ending with _BaseColor, _Normal or _Specular are considered. 102 | :param skip_existing: Pass True to skip files that would require the output 103 | file to be overwritten. Otherwise, output files are overwritten without 104 | prompt. 105 | :param texture_conversion_path: Path to a binary of the program in the 106 | texture_conversion folder. 107 | """ 108 | if not os.path.exists(destination_directory): 109 | os.makedirs(destination_directory) 110 | # Lists (terminated, source_file, args, process). First source_file and 111 | # args are set, once work starts process is set, once it is finished, 112 | # terminated is set to True 113 | tasks = list() 114 | for file in os.listdir(source_directory): 115 | match = re.search(r"(_BaseColor\.)|(_Normal\.)|(_Specular\.)", file) 116 | if match is not None and os.path.splitext(file)[1] != ".vkt": 117 | source_file = os.path.join(source_directory, file) 118 | is_srgb = match.group(1) is not None 119 | is_normal_map = match.group(2) is not None 120 | destination_file = os.path.join(destination_directory, os.path.splitext(file)[0] + ".vkt") 121 | if not skip_existing or not os.path.exists(destination_file): 122 | if is_srgb: 123 | format = 132 124 | elif is_normal_map: 125 | format = 141 126 | else: 127 | format = 131 128 | args = [texture_conversion_path, str(format), source_file, destination_file] 129 | tasks.append([False, source_file, args, None]) 130 | # Launch all processes without launching too many at once to avoid running 131 | # out of memory 132 | cpu_count = os.cpu_count() 133 | print("Using %d threads to convert materials." % cpu_count) 134 | if cpu_count is None: 135 | cpu_count = 32 136 | launched_count = terminated_count = 0 137 | while terminated_count < len(tasks): 138 | for task in tasks: 139 | terminated, source_file, args, process = task 140 | if not terminated and process is not None and (return_code := process.poll()) is not None: 141 | task[0] = True 142 | terminated_count += 1 143 | if return_code == 0: 144 | print("Finished %s" % source_file) 145 | else: 146 | print("Failed for %s with return code %d." % (source_file, return_code)) 147 | if process is None and launched_count - terminated_count < cpu_count: 148 | task[3] = Popen(args) 149 | launched_count += 1 150 | sleep(0.1) 151 | -------------------------------------------------------------------------------- /tools/texture_conversion/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 3.11) 2 | 3 | # Define an executable target 4 | project(texture_conversion) 5 | add_executable(texture_conversion) 6 | target_compile_definitions(texture_conversion 7 | PUBLIC _CRT_SECURE_NO_WARNINGS) 8 | 9 | # Specify the required C standard 10 | set_target_properties(texture_conversion PROPERTIES C_STANDARD 99) 11 | set_target_properties(texture_conversion PROPERTIES CMAKE_C_STANDARD_REQUIRED True) 12 | 13 | # Add source code 14 | target_sources(texture_conversion PRIVATE 15 | main.c 16 | stb_dxt.h 17 | stb_image.h 18 | ) 19 | 20 | if (UNIX) 21 | # Link math.h 22 | target_link_libraries(texture_conversion PRIVATE m) 23 | endif (UNIX) 24 | -------------------------------------------------------------------------------- /tools/texture_conversion/float_to_half.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021, Christoph Peters, Karlsruhe Institute of Technology 2 | // 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | 17 | #include 18 | 19 | //! A union for convenient access to bits of a float 20 | typedef union fp32_u { 21 | uint32_t u; 22 | float f; 23 | } fp32_t; 24 | 25 | 26 | //! Used to be float_to_half_fast3. Credit to Fabian Giessen: 27 | //! https://gist.githubusercontent.com/rygorous/2156668/raw/ef8408efac2ff0db549252883dd4c99dddfcc929/gistfile1.cpp 28 | static inline uint16_t float_to_half(float input_float) { 29 | fp32_t f = {.f = input_float}; 30 | fp32_t f32_infty = { 255 << 23 }; 31 | fp32_t f16_infty = { 31 << 23 }; 32 | fp32_t magic = { 15 << 23 }; 33 | uint32_t sign_mask = 0x80000000u; 34 | uint32_t round_mask = ~0xfffu; 35 | uint16_t o; 36 | 37 | uint32_t sign = f.u & sign_mask; 38 | f.u ^= sign; 39 | 40 | // NOTE all the integer compares in this function can be safely 41 | // compiled into signed compares since all operands are below 42 | // 0x80000000. Important if you want fast straight SSE2 code 43 | // (since there's no unsigned PCMPGTD). 44 | 45 | if (f.u >= f32_infty.u) // Inf or NaN (all exponent bits set) 46 | o = (f.u > f32_infty.u) ? 0x7e00 : 0x7c00; // NaN->qNaN and Inf->Inf 47 | else { // (De)normalized number or zero 48 | f.u &= round_mask; 49 | f.f *= magic.f; 50 | f.u -= round_mask; 51 | // Clamp to signed infinity if overflowed 52 | if (f.u > f16_infty.u) f.u = f16_infty.u; 53 | // Take the bits! 54 | o = f.u >> 13; 55 | } 56 | 57 | o |= sign >> 16; 58 | return o; 59 | } 60 | -------------------------------------------------------------------------------- /tools/texture_conversion/main.c: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021, Christoph Peters, Karlsruhe Institute of Technology 2 | // 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | 17 | #define STB_DXT_IMPLEMENTATION 18 | #include "stb_dxt.h" 19 | #define STB_IMAGE_IMPLEMENTATION 20 | #include "stb_image.h" 21 | #include "float_to_half.h" 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | 28 | /*! A subset of the VkFormat enumeration in Vulkan holding formats that can be 29 | output by this program. 30 | \see https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VkFormat.html */ 31 | typedef enum vk_format_e { 32 | VK_FORMAT_R8_UNORM = 9, 33 | VK_FORMAT_R8_SNORM = 10, 34 | VK_FORMAT_R8_UINT = 13, 35 | VK_FORMAT_R8_SINT = 14, 36 | VK_FORMAT_R8_SRGB = 15, 37 | VK_FORMAT_R8G8_UNORM = 16, 38 | VK_FORMAT_R8G8_SNORM = 17, 39 | VK_FORMAT_R8G8_UINT = 20, 40 | VK_FORMAT_R8G8_SINT = 21, 41 | VK_FORMAT_R8G8_SRGB = 22, 42 | VK_FORMAT_R8G8B8A8_UNORM = 37, 43 | VK_FORMAT_R8G8B8A8_SNORM = 38, 44 | VK_FORMAT_R8G8B8A8_UINT = 41, 45 | VK_FORMAT_R8G8B8A8_SINT = 42, 46 | VK_FORMAT_R8G8B8A8_SRGB = 43, 47 | VK_FORMAT_R16G16B16_SFLOAT = 90, 48 | VK_FORMAT_R16G16B16A16_SFLOAT = 97, 49 | VK_FORMAT_R32G32B32_SFLOAT = 106, 50 | VK_FORMAT_R32G32B32A32_SFLOAT = 109, 51 | VK_FORMAT_BC1_RGB_UNORM_BLOCK = 131, 52 | VK_FORMAT_BC1_RGB_SRGB_BLOCK = 132, 53 | VK_FORMAT_BC5_UNORM_BLOCK = 141, 54 | } vk_format_t; 55 | 56 | 57 | /*! The header for texture files created by this program.*/ 58 | typedef struct texture_file_header_s { 59 | //! The value 0xbc1bc1 60 | int32_t file_marker; 61 | //! The file format version 62 | int32_t version; 63 | //! The number of mipmaps and the image dimensions 64 | int32_t mipmap_count, width, height; 65 | //! The format, i.e. one of the values from the Vulkan enumeration VkFormat 66 | int32_t format; 67 | //! Combined size of all mipmaps, excluding headers in bytes 68 | size_t payload_size; 69 | } texture_file_header_t; 70 | 71 | 72 | /*! The header for each individual mipmap.*/ 73 | typedef struct mipmap_header_s { 74 | //! The extent of the mipmap in pixels 75 | int32_t width, height; 76 | //! The size of the mipmap in bytes, excluding headers 77 | size_t size; 78 | //! The offset from the beginning of the payload to the mipmap in bytes 79 | size_t offset; 80 | } mipmap_header_t; 81 | 82 | 83 | /*! Converts the given scalars (which should be normalized to the range from 84 | zero to one) from a linear scale to the sRGB scale (from 0 to 255).*/ 85 | static inline uint8_t linear_to_srgb(float linear) { 86 | linear = (linear < 0.0f) ? 0.0f : linear; 87 | float srgb = (linear <= 0.0031308f) ? (12.92f * linear) : (1.055f * powf(linear, 1.0f / 2.4f) - 0.055f); 88 | return (uint8_t) roundf(srgb * 255.0f); 89 | } 90 | 91 | 92 | /*! Quantizes a value between zero and one to the range from 0 to 255.*/ 93 | static inline uint8_t quantize_linear(float linear) { 94 | return (uint8_t) roundf(linear * 255.0f); 95 | } 96 | 97 | 98 | /*! Converts the given scalar from an sRGB scale to linear scale between zero 99 | and one.*/ 100 | static inline float srgb_to_linear(uint8_t srgb) { 101 | float srgb_float = srgb * (1.0f / 255.0f); 102 | return (srgb_float <= 0.04045f) ? (srgb_float * (1.0f / 12.92f)) : powf(srgb_float * (1.0f / 1.055f) + 0.055f / 1.055f, 2.4f); 103 | } 104 | 105 | 106 | /*! Returns the required mipmap count for a square image with the given edge 107 | length in pixels when producing a complete hierarchy.*/ 108 | static inline int32_t get_mipmap_count(int32_t extent) { 109 | int32_t padded_extent = 2 * extent - 1; 110 | int32_t mipmap_count = 0; 111 | while (padded_extent > 0) { 112 | padded_extent &= 0x7ffffffe; 113 | padded_extent >>= 1; 114 | ++mipmap_count; 115 | } 116 | return mipmap_count; 117 | } 118 | 119 | 120 | int main(int argc, char** argv) { 121 | // Grab and validate input arguments 122 | int32_t format_int = 0; 123 | if (argc >= 4) 124 | sscanf(argv[1], "%d", &format_int); 125 | vk_format_t format = (vk_format_t) format_int; 126 | 127 | // Prepare some information about the output format 128 | int32_t is_hdr = 0, is_half = 0, is_srgb = 0, is_bc1 = 0, is_8_bit = 0, format_known = 1; 129 | size_t block_size = 0; 130 | size_t bits_per_pixel; 131 | int32_t channel_count = 3; 132 | switch (format) { 133 | case VK_FORMAT_R8_SRGB: 134 | is_srgb = 1; 135 | case VK_FORMAT_R8_UNORM: 136 | case VK_FORMAT_R8_SNORM: 137 | case VK_FORMAT_R8_UINT: 138 | case VK_FORMAT_R8_SINT: 139 | channel_count = 1; 140 | bits_per_pixel = 8; 141 | is_8_bit = 1; 142 | break; 143 | case VK_FORMAT_R8G8_SRGB: 144 | is_srgb = 1; 145 | case VK_FORMAT_R8G8_UNORM: 146 | case VK_FORMAT_R8G8_SNORM: 147 | case VK_FORMAT_R8G8_UINT: 148 | case VK_FORMAT_R8G8_SINT: 149 | channel_count = 2; 150 | bits_per_pixel = 16; 151 | is_8_bit = 1; 152 | break; 153 | case VK_FORMAT_R8G8B8A8_SRGB: 154 | is_srgb = 1; 155 | case VK_FORMAT_R8G8B8A8_UNORM: 156 | case VK_FORMAT_R8G8B8A8_SNORM: 157 | case VK_FORMAT_R8G8B8A8_UINT: 158 | case VK_FORMAT_R8G8B8A8_SINT: 159 | channel_count = 4; 160 | bits_per_pixel = 32; 161 | is_8_bit = 1; 162 | break; 163 | case VK_FORMAT_R16G16B16A16_SFLOAT: 164 | channel_count = 4; 165 | bits_per_pixel = 64; 166 | is_hdr = 1; 167 | is_half = 1; 168 | break; 169 | case VK_FORMAT_R16G16B16_SFLOAT: 170 | channel_count = 3; 171 | bits_per_pixel = 48; 172 | is_hdr = 1; 173 | is_half = 1; 174 | break; 175 | case VK_FORMAT_R32G32B32A32_SFLOAT: 176 | channel_count = 4; 177 | bits_per_pixel = 128; 178 | is_hdr = 1; 179 | break; 180 | case VK_FORMAT_R32G32B32_SFLOAT: 181 | channel_count = 3; 182 | bits_per_pixel = 96; 183 | is_hdr = 1; 184 | break; 185 | case VK_FORMAT_BC5_UNORM_BLOCK: 186 | block_size = 16; 187 | bits_per_pixel = 8; 188 | channel_count = 2; 189 | break; 190 | case VK_FORMAT_BC1_RGB_SRGB_BLOCK: 191 | is_srgb = 1; 192 | case VK_FORMAT_BC1_RGB_UNORM_BLOCK: 193 | bits_per_pixel = 4; 194 | block_size = 8; 195 | is_bc1 = 1; 196 | break; 197 | default: 198 | format_known = 0; 199 | break; 200 | } 201 | if (argc < 4 || !format_known) { 202 | printf("Usage: texture_compression \n"); 203 | printf("vk_format can be one of the following integer values from the VkFormat enumeration in Vulkan:\n\ 204 | VK_FORMAT_R8_UNORM = 9\n\ 205 | VK_FORMAT_R8_SNORM = 10\n\ 206 | VK_FORMAT_R8_UINT = 13\n\ 207 | VK_FORMAT_R8_SINT = 14\n\ 208 | VK_FORMAT_R8_SRGB = 15\n\ 209 | VK_FORMAT_R8G8_UNORM = 16\n\ 210 | VK_FORMAT_R8G8_SNORM = 17\n\ 211 | VK_FORMAT_R8G8_UINT = 20\n\ 212 | VK_FORMAT_R8G8_SINT = 21\n\ 213 | VK_FORMAT_R8G8_SRGB = 22\n\ 214 | VK_FORMAT_R8G8B8A8_UNORM = 37\n\ 215 | VK_FORMAT_R8G8B8A8_SNORM = 38\n\ 216 | VK_FORMAT_R8G8B8A8_UINT = 41\n\ 217 | VK_FORMAT_R8G8B8A8_SINT = 42\n\ 218 | VK_FORMAT_R8G8B8A8_SRGB = 43\n\ 219 | VK_FORMAT_R16G16B16_SFLOAT = 90\n\ 220 | VK_FORMAT_R16G16B16A16_SFLOAT = 97\n\ 221 | VK_FORMAT_R32G32B32_SFLOAT = 106\n\ 222 | VK_FORMAT_R32G32B32A32_SFLOAT = 109\n\ 223 | VK_FORMAT_BC1_RGB_UNORM_BLOCK = 131\n\ 224 | VK_FORMAT_BC1_RGB_SRGB_BLOCK = 132\n\ 225 | VK_FORMAT_BC5_UNORM_BLOCK = 141\n"); 226 | printf("For a list of supported input file formats, see:\n"); 227 | printf("https://github.com/nothings/stb/blob/master/stb_image.h\n"); 228 | printf("The output format is *.vkt, which is a renderer specific format with mipmaps (similar to *.dds).\n"); 229 | return 1; 230 | } 231 | const char* input_file_path = argv[argc - 2]; 232 | const char* output_file_path = argv[argc - 1]; 233 | 234 | // Prepare the image 235 | int32_t width, height, input_channel_count, pixel_count; 236 | int32_t shared_channel_count = channel_count; 237 | float* linear_image; 238 | if (is_hdr) { 239 | // Open the image 240 | linear_image = stbi_loadf(input_file_path, &width, &height, &input_channel_count, channel_count); 241 | input_channel_count = channel_count; 242 | if (!linear_image) { 243 | printf("Failed to load the HDR image at path %s.\n", input_file_path); 244 | return 1; 245 | } 246 | pixel_count = width * height; 247 | } 248 | else { 249 | // Open the image 250 | uint8_t* loaded_image = stbi_load(input_file_path, &width, &height, &input_channel_count, 0); 251 | if (!loaded_image) { 252 | printf("Failed to load the image at path %s.\n", input_file_path); 253 | return 1; 254 | } 255 | if (shared_channel_count > input_channel_count) 256 | shared_channel_count = input_channel_count; 257 | // Convert to linear RGB and discard superfluous channels 258 | pixel_count = width * height; 259 | linear_image = malloc(pixel_count * shared_channel_count * sizeof(float)); 260 | if (is_srgb) 261 | for (int32_t i = 0; i != pixel_count; ++i) 262 | for (int32_t j = 0; j != shared_channel_count; ++j) 263 | linear_image[i * shared_channel_count + j] = srgb_to_linear(loaded_image[i * input_channel_count + j]); 264 | else 265 | for (int32_t i = 0; i != pixel_count; ++i) 266 | for (int32_t j = 0; j != shared_channel_count; ++j) 267 | linear_image[i * shared_channel_count + j] = loaded_image[i * input_channel_count + j] * (1.0f / 255.0f); 268 | // We no longer need the LDR image 269 | stbi_image_free(loaded_image); 270 | loaded_image = NULL; 271 | } 272 | 273 | // Check the channel count 274 | if (input_channel_count < channel_count && !is_8_bit) { 275 | printf("The image at path %s has %d channels but needs to have at least %d.\n", input_file_path, input_channel_count, channel_count); 276 | free(linear_image); 277 | return 1; 278 | } 279 | // Determine how many mipmaps we need (we do not go all the way to 1x1 if 280 | // the aspect ratio is not one) 281 | int32_t mipmap_count_width = get_mipmap_count(width); 282 | int32_t mipmap_count_height = get_mipmap_count(height); 283 | int32_t mipmap_count = (mipmap_count_width < mipmap_count_height) ? mipmap_count_width : mipmap_count_height; 284 | // The image must have power of two size 285 | if (width != (1 << (mipmap_count_width - 1)) || height != (1 << (mipmap_count_height - 1))) { 286 | printf("The image at path %s has extent %dx%d but it must be a power of two for both dimensions.\n", input_file_path, width, height); 287 | free(linear_image); 288 | return 1; 289 | } 290 | 291 | if (block_size) { 292 | // Block compression only goes down to 4x4 blocks 293 | mipmap_count -= 2; 294 | // 1x1 images are useful and easy enough to fix 295 | if (width == 1 && height == 1) { 296 | mipmap_count = 1; 297 | width = height = 4; 298 | pixel_count = 16; 299 | float color[4]; 300 | for (int32_t j = 0; j != shared_channel_count; ++j) 301 | color[j] = linear_image[j]; 302 | free(linear_image); 303 | linear_image = malloc(pixel_count * shared_channel_count * sizeof(float)); 304 | for (int32_t i = 0; i != pixel_count; ++i) 305 | for (int32_t j = 0; j != shared_channel_count; ++j) 306 | linear_image[i * shared_channel_count + j] = color[j]; 307 | } 308 | if (width < 4 || height < 4) { 309 | printf("The image at path %s has extent %dx%d but it must be at least 4x4 for block compression to work.\n", input_file_path, width, height); 310 | free(linear_image); 311 | return 1; 312 | } 313 | } 314 | 315 | // Open the output file for writing 316 | FILE* file = fopen(output_file_path, "wb"); 317 | if (!file) { 318 | printf("Failed to open the output file: %s\n", output_file_path); 319 | free(linear_image); 320 | return 1; 321 | } 322 | // Write the header 323 | texture_file_header_t header = { 324 | .file_marker = 0xbc1bc1, 325 | .version = 1, 326 | .mipmap_count = mipmap_count, 327 | .width = width, .height = height, 328 | .format = (int32_t) format 329 | }; 330 | for (int32_t i = 0; i != mipmap_count; ++i) { 331 | mipmap_header_t mipmap_header = { .width = width >> i, .height = height >> i }; 332 | header.payload_size += (mipmap_header.width * mipmap_header.height * bits_per_pixel) / 8; 333 | } 334 | fwrite((void*) &header, sizeof(int32_t), 8, file); 335 | // Write meta data about each mipmap (even though it is redundant) 336 | size_t mipmap_offset = 0; 337 | for (int32_t i = 0; i != mipmap_count; ++i) { 338 | mipmap_header_t mipmap_header = { .width = width >> i, .height = height >> i }; 339 | mipmap_header.size = (mipmap_header.width * mipmap_header.height * bits_per_pixel) / 8; 340 | mipmap_header.offset = mipmap_offset; 341 | mipmap_offset += mipmap_header.size; 342 | fwrite((void*) &mipmap_header, sizeof(mipmap_header), 1, file); 343 | } 344 | 345 | // Allocate scratch memory for the largest mipmap (it will be used for all 346 | // of them) 347 | float* linear_mipmap = malloc(((sizeof(float) * width * height) / 4) * shared_channel_count); 348 | // Generate mipmaps 349 | for (int32_t i = 0; i != mipmap_count; ++i) { 350 | int32_t mipmap_width = width >> i; 351 | int32_t mipmap_height = height >> i; 352 | // For the highest resolution mipmap, we skip filtering 353 | float* mipmap; 354 | if (mipmap_width == width) 355 | mipmap = linear_image; 356 | else { 357 | mipmap = linear_mipmap; 358 | // Prepare the normalized Gaussian filter 359 | int32_t filter_scale = (1 << i); 360 | int32_t stride = filter_scale; 361 | double standard_deviation = 0.4 * filter_scale; 362 | double gaussian_factor = -0.5 / (standard_deviation * standard_deviation); 363 | int32_t filter_extent = (int32_t) ceil(3.0 * standard_deviation); 364 | double filter_center = filter_extent - 0.5; 365 | double* filter_weights = malloc(2 * filter_extent * sizeof(double)); 366 | for (int32_t j = 0; j != 2 * filter_extent; ++j) 367 | filter_weights[j] = exp(gaussian_factor * (j - filter_center) * (j - filter_center)); 368 | double total_weight = 0.0; 369 | for (int32_t j = 0; j != 2 * filter_extent; ++j) 370 | for (int32_t k = 0; k != 2 * filter_extent; ++k) 371 | total_weight += filter_weights[j] * filter_weights[k]; 372 | double normalization = 1.0 / sqrt(total_weight); 373 | for (int32_t j = 0; j != 2 * filter_extent; ++j) 374 | filter_weights[j] *= normalization; 375 | int32_t offset = stride / 2 - filter_extent; 376 | // Iterate over output pixels 377 | int32_t mask_x = width - 1; 378 | int32_t mask_y = height - 1; 379 | for (int32_t y = 0; y != mipmap_height; ++y) { 380 | for (int32_t x = 0; x != mipmap_width; ++x) { 381 | double pixel[4] = { 0.0, 0.0, 0.0, 0.0 }; 382 | // Iterate over the filter footprint 383 | for (int32_t k = 0; k != 2 * filter_extent; ++k) { 384 | for (int32_t j = 0; j != 2 * filter_extent; ++j) { 385 | int32_t source_x = x * stride + offset + j; 386 | source_x &= mask_x; 387 | int32_t source_y = y * stride + offset + k; 388 | source_y &= mask_y; 389 | int32_t pixel_start = shared_channel_count * (source_y * width + source_x); 390 | double weight = filter_weights[j] * filter_weights[k]; 391 | for (int32_t l = 0; l != shared_channel_count; ++l) 392 | pixel[l] += weight * linear_image[pixel_start + l]; 393 | } 394 | } 395 | // Cast the pixel to float 396 | float* mipmap_pixel = mipmap + shared_channel_count * (y * mipmap_width + x); 397 | for (int32_t l = 0; l != shared_channel_count; ++l) 398 | mipmap_pixel[l] = (float) pixel[l]; 399 | } 400 | } 401 | free(filter_weights); 402 | } 403 | 404 | // Quantize, apply block compression and store 405 | if (is_bc1) { 406 | uint8_t block[4 * 4 * 4] = {0}, compressed[8]; 407 | // Iterate over blocks 408 | for (int32_t y = 0; y != mipmap_height; y += 4) { 409 | for (int32_t x = 0; x != mipmap_width; x += 4) { 410 | // Quantize the block 411 | for (int32_t k = 0; k != 4; ++k) 412 | for (int32_t j = 0; j != 4; ++j) 413 | for (int32_t l = 0; l != 3; ++l) 414 | block[(k * 4 + j) * 4 + l] = is_srgb ? linear_to_srgb(mipmap[3 * ((y + k) * mipmap_width + x + j) + l]) 415 | : quantize_linear(mipmap[3 * ((y + k) * mipmap_width + x + j) + l]); 416 | // Apply block compression 417 | stb_compress_dxt_block(compressed, block, 0, STB_DXT_HIGHQUAL); 418 | // Store the block 419 | fwrite((void*) compressed, sizeof(uint8_t), sizeof(compressed), file); 420 | } 421 | } 422 | } 423 | else if(format == VK_FORMAT_BC5_UNORM_BLOCK) { 424 | uint8_t block[4 * 4 * 2], compressed[16]; 425 | // Iterate over blocks 426 | for (int32_t y = 0; y != mipmap_height; y += 4) { 427 | for (int32_t x = 0; x != mipmap_width; x += 4) { 428 | // Quantize the block 429 | for (int32_t k = 0; k != 4; ++k) 430 | for (int32_t j = 0; j != 4; ++j) 431 | for (int32_t l = 0; l != 2; ++l) 432 | block[(k * 4 + j) * 2 + l] = quantize_linear(mipmap[2 * ((y + k) * mipmap_width + x + j) + l]); 433 | // Apply block compression 434 | stb_compress_bc5_block(compressed, block); 435 | // Store the block 436 | fwrite((void*) compressed, sizeof(uint8_t), sizeof(compressed), file); 437 | } 438 | } 439 | } 440 | else if (is_half) { 441 | uint16_t pixel[4] = {0}; 442 | for (int32_t y = 0; y != mipmap_height; ++y) { 443 | for (int32_t x = 0; x != mipmap_width; ++x) { 444 | for (int32_t l = 0; l != shared_channel_count; ++l) 445 | pixel[l] = float_to_half(mipmap[(y * mipmap_width + x) * shared_channel_count + l]); 446 | fwrite((void*) pixel, sizeof(uint16_t), channel_count, file); 447 | } 448 | } 449 | } 450 | else if (is_hdr) 451 | fwrite(mipmap, sizeof(float), mipmap_width * mipmap_height * channel_count, file); 452 | // Write low-dynamic range formats with 8 bits per pixel 453 | else if (is_8_bit) { 454 | for (int32_t y = 0; y != mipmap_height; ++y) { 455 | for (int32_t x = 0; x != mipmap_width; ++x) { 456 | uint8_t pixel[4] = { 0, 0, 0, 255 }; 457 | for (int32_t l = 0; l != shared_channel_count; ++l) 458 | pixel[l] = is_srgb ? linear_to_srgb(mipmap[shared_channel_count * (y * mipmap_width + x) + l]) 459 | : quantize_linear(mipmap[shared_channel_count * (y * mipmap_width + x) + l]); 460 | fwrite(pixel, bits_per_pixel / 8, 1, file); 461 | } 462 | } 463 | } 464 | } 465 | 466 | // Write an end of file marker 467 | int32_t eof = 0xe0fe0f; 468 | fwrite((void*) &eof, sizeof(eof), 1, file); 469 | // Clean up 470 | fclose(file); 471 | free(linear_image); 472 | free(linear_mipmap); 473 | return 0; 474 | } 475 | -------------------------------------------------------------------------------- /tools/vulkan_formats.py: -------------------------------------------------------------------------------- 1 | import re 2 | from xml.dom.minidom import parse 3 | 4 | 5 | def load_formats(vk_xml_path="vk.xml"): 6 | """ 7 | Extracts all tags from the Vulkan XML specification. 8 | :param vk_xml_path: Path to the vk.xml file. 9 | :return: Tuples of strings (name, cls, block_size, texels_per_block, 10 | packed_bits) some of which may be empty. 11 | """ 12 | doc = parse(vk_xml_path) 13 | attrs = ["name", "class", "blockSize", "texelsPerBlock", "packed"] 14 | return [[format.getAttribute(attr) for attr in attrs] for format in doc.getElementsByTagName("format")] 15 | 16 | 17 | def format_class(cls): 18 | """Turns the class attribute in the XML into the name of an enum entry.""" 19 | return "FORMAT_CLASS_" + re.sub(r"\W", r"_", cls.upper()) 20 | 21 | 22 | def generate_vulkan_formats(): 23 | """Generates vulkan_formats.h and vulkan_formats.c in ../src.""" 24 | classes = list() 25 | with open("../src/vulkan_formats.c", "w") as file: 26 | file.write( 27 | """#include \"vulkan_formats.h\" 28 | 29 | 30 | format_description_t get_format_description(VkFormat format) { 31 | \tformat_description_t desc = { .packed_bits = 0 }; 32 | \tswitch (format) { 33 | """ 34 | ) 35 | for name, cls, block_size, texels_per_block, packed_bits in load_formats(): 36 | if cls not in classes: 37 | classes.append(cls) 38 | file.write("\t\tcase %s:\n" % name) 39 | file.write("\t\t\tdesc.cls = %s;\n" % format_class(cls)) 40 | file.write("\t\t\tdesc.block_size = %s;\n" % block_size) 41 | file.write("\t\t\tdesc.texels_per_block = %s;\n" % texels_per_block) 42 | if len(packed_bits): 43 | file.write("\t\t\tdesc.packed_bits = %s;\n" % packed_bits) 44 | file.write("\t\t\tbreak;\n") 45 | file.write( 46 | """\t\tdefault: 47 | \t\t\tbreak; 48 | \t} 49 | \treturn desc; 50 | }\n""" 51 | ) 52 | with open("../src/vulkan_formats.h", "w") as file: 53 | file.write( 54 | """#pragma once 55 | #define GLFW_INCLUDE_VULKAN 56 | #include 57 | #include 58 | 59 | 60 | //! Enumeration of all possible format compatibility classes 61 | typedef enum { 62 | """ 63 | ) 64 | for cls in classes: 65 | file.write("\t%s,\n" % format_class(cls)) 66 | file.write( 67 | """} format_class_t; 68 | 69 | 70 | //! Provides meta data about a Vulkan format that can for example be used to 71 | //! figure out how to copy data from a buffer to an image. Autogenerated from 72 | //! vk.xml by ../tools/vulkan_formats.py. 73 | typedef struct { 74 | \t//! The format compatibility class of the format. Copying between two 75 | \t//! formats of the same class is permitted. 76 | \tformat_class_t cls; 77 | \t//! The number of bytes per texel block 78 | \tVkDeviceSize block_size; 79 | \t//! The number of texels per texel block 80 | \tuint32_t texels_per_block; 81 | \t//! For packed formats such as VK_FORMAT_R5G6B5_UNORM_PACK16, this is the 82 | \t//! number of bits into which a color is being packed. 0 otherwise. 83 | \tuint32_t packed_bits; 84 | } format_description_t; 85 | 86 | 87 | //! Provides meta data about the given Vulkan format. 88 | //! \see format_description_t 89 | format_description_t get_format_description(VkFormat format);\n""" 90 | ) 91 | 92 | 93 | if __name__ == "__main__": 94 | generate_vulkan_formats() 95 | --------------------------------------------------------------------------------