Compare commits

..
11 Commits
115 changed files with 6829 additions and 702 deletions
+3
View File
@@ -8,3 +8,6 @@ lightkeeper/assets_runtime
.idea/** .idea/**
build/** build/**
CLAUDE.md
docs/**
+1
View File
@@ -44,6 +44,7 @@
[submodule "destrum/third_party/imgui"] [submodule "destrum/third_party/imgui"]
path = destrum/third_party/imgui path = destrum/third_party/imgui
url = https://github.com/ocornut/imgui.git url = https://github.com/ocornut/imgui.git
branch = docking
[submodule "destrum/third_party/jolt"] [submodule "destrum/third_party/jolt"]
path = destrum/third_party/jolt path = destrum/third_party/jolt
url = https://github.com/jrouwe/JoltPhysics.git url = https://github.com/jrouwe/JoltPhysics.git
+1
View File
@@ -35,6 +35,7 @@ if (TARGET _internal_validate_engine_shaders)
add_dependencies(ValidateShaders _internal_validate_engine_shaders) add_dependencies(ValidateShaders _internal_validate_engine_shaders)
endif() endif()
destrum_enable_warnings(destrum_assets)
destrum_enable_warnings(destrum) destrum_enable_warnings(destrum)
destrum_enable_warnings(lightkeeper) destrum_enable_warnings(lightkeeper)
+1 -1
View File
@@ -119,7 +119,7 @@ cmake --install build --config Release
## Demo app ## Demo app
`lightkeeper` is the current game application. It launches **Star Catcher**, a small playable loop built from the engine's scene and rendering systems. Move the blue catcher with `A`/`D` or the arrow keys, catch orange stars, and press `R` to restart after three misses. Press `Escape` to quit. `lightkeeper` is the current demo application. It creates an SDL window, initializes the Destrum app, loads assets, sets up a scene, renders meshes, skyboxes, and a skinned animated character.
## Development notes ## Development notes
+1 -1
Submodule TheChef updated: 14f7dd9423...0273340e3b
+43 -14
View File
@@ -1,10 +1,43 @@
add_subdirectory(third_party) add_subdirectory(third_party)
# CPU side asset types and the cooked model format, shared with TheChef (no Vulkan / SDL dependencies)
add_library(destrum_assets
"src/Assets/ModelFile.cpp"
"src/Assets/TextureFile.cpp"
)
add_library(destrum::assets ALIAS destrum_assets)
set_target_properties(destrum_assets PROPERTIES
CXX_STANDARD 20
CXX_EXTENSIONS OFF
)
target_include_directories(destrum_assets PUBLIC "${CMAKE_CURRENT_LIST_DIR}/include")
target_link_libraries(destrum_assets
PUBLIC
glm::glm
assimp
spdlog::spdlog
)
target_compile_definitions(destrum_assets
PUBLIC
GLM_FORCE_CTOR_INIT
GLM_FORCE_XYZW_ONLY
GLM_FORCE_EXPLICIT_CTOR
GLM_FORCE_DEPTH_ZERO_TO_ONE
GLM_ENABLE_EXPERIMENTAL
)
set(SRC_FILES set(SRC_FILES
"src/App.cpp" "src/App.cpp"
"src/Event.cpp" "src/Event.cpp"
"src/Components/Animator.cpp" "src/Components/Animator.cpp"
"src/Components/CameraComponent.cpp"
"src/Components/LightComponent.cpp"
"src/Components/MeshRendererComponent.cpp" "src/Components/MeshRendererComponent.cpp"
"src/Components/Rotator.cpp" "src/Components/Rotator.cpp"
"src/Components/Spinner.cpp" "src/Components/Spinner.cpp"
@@ -32,18 +65,25 @@ set(SRC_FILES
"src/Graphics/Swapchain.cpp" "src/Graphics/Swapchain.cpp"
"src/Graphics/Util.cpp" "src/Graphics/Util.cpp"
"src/Graphics/RenderResources.cpp" "src/Graphics/RenderResources.cpp"
"src/Graphics/ResourcesInspector.cpp"
"src/Graphics/Managers/VulkanInstanceManager.cpp" "src/Graphics/Managers/VulkanInstanceManager.cpp"
"src/Graphics/Managers/MemoryManager.cpp" "src/Graphics/Managers/MemoryManager.cpp"
"src/Graphics/Managers/FrameManager.cpp" "src/Graphics/Managers/FrameManager.cpp"
"src/Graphics/Managers/ImageManager.cpp" "src/Graphics/Managers/ImageManager.cpp"
"src/Graphics/Managers/LineRenderingManager.cpp"
"src/Graphics/Managers/QuadRenderingManager.cpp"
"src/Graphics/Resources/GPUImage.cpp" "src/Graphics/Resources/GPUImage.cpp"
"src/Graphics/Resources/NBuffer.cpp" "src/Graphics/Resources/NBuffer.cpp"
"src/Graphics/Resources/Cubemap.cpp" "src/Graphics/Resources/Cubemap.cpp"
"src/Graphics/Pipelines/MeshPipeline.cpp" "src/Graphics/Pipelines/MeshPipeline.cpp"
"src/Graphics/Pipelines/LineRenderingPass.cpp"
"src/Graphics/Pipelines/QuadRendererPass.cpp"
"src/Graphics/Pipelines/SkyboxPipeline.cpp" "src/Graphics/Pipelines/SkyboxPipeline.cpp"
"src/Graphics/Pipelines/LightCullingPipeline.cpp"
"src/Graphics/Pipelines/ShadowMapPipeline.cpp"
"src/Graphics/Pipelines/SkinningPipeline.cpp" "src/Graphics/Pipelines/SkinningPipeline.cpp"
"src/Graphics/Pipelines/ImguiPass.cpp" "src/Graphics/Pipelines/ImguiPass.cpp"
@@ -92,6 +132,8 @@ target_include_directories(destrum PRIVATE
target_link_libraries(destrum target_link_libraries(destrum
PUBLIC PUBLIC
destrum_assets
volk::volk_headers volk::volk_headers
vk-bootstrap::vk-bootstrap vk-bootstrap::vk-bootstrap
GPUOpen::VulkanMemoryAllocator GPUOpen::VulkanMemoryAllocator
@@ -129,15 +171,6 @@ else ()
target_link_libraries(destrum PUBLIC SDL2::SDL2-static) target_link_libraries(destrum PUBLIC SDL2::SDL2-static)
endif () endif ()
target_compile_definitions(destrum
PUBLIC
GLM_FORCE_CTOR_INIT
GLM_FORCE_XYZW_ONLY
GLM_FORCE_EXPLICIT_CTOR
GLM_FORCE_DEPTH_ZERO_TO_ONE
GLM_ENABLE_EXPERIMENTAL
)
set(ASSETS_SRC_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_src") set(ASSETS_SRC_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_src")
set(ASSETS_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_runtime") set(ASSETS_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_runtime")
#set(OUTPUT_ENGINE_ASSETS_DIR "${CMAKE_BINARY_DIR}/assets/engine") #set(OUTPUT_ENGINE_ASSETS_DIR "${CMAKE_BINARY_DIR}/assets/engine")
@@ -161,10 +194,6 @@ add_custom_target(_internal_clean_engine_assets
) )
add_custom_target(_internal_cook_engine_assets add_custom_target(_internal_cook_engine_assets
COMMAND TheChef
--input "${ASSETS_SRC_DIR}"
--output "${ASSETS_RUNTIME_DIR}"
--clean
COMMAND TheChef COMMAND TheChef
--input "${ASSETS_SRC_DIR}" --input "${ASSETS_SRC_DIR}"
--output "${ASSETS_RUNTIME_DIR}" --output "${ASSETS_RUNTIME_DIR}"
@@ -221,7 +250,7 @@ function(destrum_cook_engine_assets GAME_TARGET GAME_OUTPUT_DIR)
install(DIRECTORY "${_engine_runtime}/" DESTINATION "bin/assets/engine") install(DIRECTORY "${_engine_runtime}/" DESTINATION "bin/assets/engine")
endfunction() endfunction()
install(TARGETS destrum install(TARGETS destrum destrum_assets
ARCHIVE DESTINATION lib ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib LIBRARY DESTINATION lib
RUNTIME DESTINATION bin) RUNTIME DESTINATION bin)
+4
View File
@@ -35,6 +35,10 @@ vec4 sampleTextureCubeLinear(uint texID, vec3 p) {
return texture(nonuniformEXT(samplerCube(textureCubes[texID], samplers[LINEAR_SAMPLER_ID])), p); return texture(nonuniformEXT(samplerCube(textureCubes[texID], samplers[LINEAR_SAMPLER_ID])), p);
} }
float sampleTexture2DShadow(uint texID, vec3 p) {
return texture(nonuniformEXT(sampler2DShadow(textures[texID], samplers[SHADOW_SAMPLER_ID])), p);
}
float sampleTextureArrayShadow(uint texID, vec4 p) { float sampleTextureArrayShadow(uint texID, vec4 p) {
return texture(nonuniformEXT(sampler2DArrayShadow(textureArrays[texID], samplers[SHADOW_SAMPLER_ID])), p); return texture(nonuniformEXT(sampler2DArrayShadow(textureArrays[texID], samplers[SHADOW_SAMPLER_ID])), p);
} }
@@ -0,0 +1,154 @@
#version 460
#extension GL_GOOGLE_include_directive : require
#extension GL_EXT_buffer_reference : require
#extension GL_EXT_scalar_block_layout : require
#include "bindless.glsl"
#include "scene_data.glsl"
// Forward+ light culling.
// One workgroup per LIGHT_TILE_SIZE x LIGHT_TILE_SIZE screen tile:
// 1. find the min/max depth of the tile from the depth pre-pass
// 2. build the tile frustum in view space
// 3. test every light against it and write the visible ones to the tile's light list
layout (push_constant, scalar) uniform constants
{
SceneDataBuffer sceneData;
uvec2 screenSize;
uint depthTexID;
uint padding;
} pcs;
layout (local_size_x = LIGHT_TILE_SIZE, local_size_y = LIGHT_TILE_SIZE, local_size_z = 1) in;
#define THREADS_PER_TILE (LIGHT_TILE_SIZE * LIGHT_TILE_SIZE)
shared uint tileMinDepth;
shared uint tileMaxDepth;
shared uint tileLightCount;
// left, right, bottom, top side planes (view space, through the camera origin)
shared vec4 tilePlanes[4];
// view space distances along -Z
shared float tileNearDistance;
shared float tileFarDistance;
vec3 unprojectToView(vec2 ndc, float depth)
{
vec4 p = pcs.sceneData.invProj * vec4(ndc, depth, 1.0);
return p.xyz / p.w;
}
// Plane through the camera origin and two points, normal facing towards `inside`
vec4 makeSidePlane(vec3 a, vec3 b, vec3 inside)
{
vec3 n = normalize(cross(a, b));
if (dot(n, inside) < 0.0) {
n = -n;
}
return vec4(n, 0.0);
}
bool sphereInsideTile(vec3 center, float radius)
{
float distance = -center.z;
if (distance + radius < tileNearDistance || distance - radius > tileFarDistance) {
return false;
}
for (int i = 0; i < 4; i++) {
if (dot(tilePlanes[i].xyz, center) + tilePlanes[i].w < -radius) {
return false;
}
}
return true;
}
void main()
{
uint localIndex = gl_LocalInvocationIndex;
uvec2 tileID = gl_WorkGroupID.xy;
uint tileIndex = tileID.y * pcs.sceneData.tileCountX + tileID.x;
uint tileOffset = tileIndex * LIGHT_TILE_STRIDE;
if (localIndex == 0) {
tileMinDepth = 0xFFFFFFFFu;
tileMaxDepth = 0u;
tileLightCount = 0u;
}
memoryBarrierShared();
barrier();
// 1. min / max depth
uvec2 pixel = gl_GlobalInvocationID.xy;
if (all(lessThan(pixel, pcs.screenSize))) {
float depth = texelFetch(
sampler2D(textures[pcs.depthTexID], samplers[NEAREST_SAMPLER_ID]),
ivec2(pixel),
0).r;
// depth == 1.0 is the cleared far plane (sky), nothing there receives light.
// Depth is always positive, so its bits can be compared as uints.
if (depth < 1.0) {
uint depthBits = floatBitsToUint(depth);
atomicMin(tileMinDepth, depthBits);
atomicMax(tileMaxDepth, depthBits);
}
}
memoryBarrierShared();
barrier();
// 2. tile frustum
bool tileHasGeometry = tileMinDepth != 0xFFFFFFFFu;
if (localIndex == 0 && tileHasGeometry) {
vec2 screenSize = vec2(pcs.screenSize);
vec2 ndcMin = vec2(tileID * LIGHT_TILE_SIZE) / screenSize * 2.0 - 1.0;
vec2 ndcMax = vec2((tileID + 1) * LIGHT_TILE_SIZE) / screenSize * 2.0 - 1.0;
vec2 ndcCenter = (ndcMin + ndcMax) * 0.5;
vec3 corner00 = unprojectToView(vec2(ndcMin.x, ndcMin.y), 1.0);
vec3 corner10 = unprojectToView(vec2(ndcMax.x, ndcMin.y), 1.0);
vec3 corner11 = unprojectToView(vec2(ndcMax.x, ndcMax.y), 1.0);
vec3 corner01 = unprojectToView(vec2(ndcMin.x, ndcMax.y), 1.0);
vec3 center = unprojectToView(ndcCenter, 1.0);
tilePlanes[0] = makeSidePlane(corner01, corner00, center);
tilePlanes[1] = makeSidePlane(corner10, corner11, center);
tilePlanes[2] = makeSidePlane(corner00, corner10, center);
tilePlanes[3] = makeSidePlane(corner11, corner01, center);
tileNearDistance = -unprojectToView(ndcCenter, uintBitsToFloat(tileMinDepth)).z;
tileFarDistance = -unprojectToView(ndcCenter, uintBitsToFloat(tileMaxDepth)).z;
}
memoryBarrierShared();
barrier();
// 3. cull lights, every thread takes a slice of the light list
if (tileHasGeometry) {
for (uint i = localIndex; i < pcs.sceneData.numLights; i += THREADS_PER_TILE) {
Light light = pcs.sceneData.lights.data[i];
bool visible = light.type == TYPE_DIRECTIONAL_LIGHT;
if (!visible) {
vec3 viewPos = (pcs.sceneData.view * vec4(light.position, 1.0)).xyz;
visible = sphereInsideTile(viewPos, light.range);
}
if (visible) {
uint slot = atomicAdd(tileLightCount, 1u);
if (slot < MAX_LIGHTS_PER_TILE) {
pcs.sceneData.tileLightIndices.data[tileOffset + 1 + slot] = i;
}
}
}
}
memoryBarrierShared();
barrier();
if (localIndex == 0) {
pcs.sceneData.tileLightIndices.data[tileOffset] = min(tileLightCount, uint(MAX_LIGHTS_PER_TILE));
}
}
+96
View File
@@ -0,0 +1,96 @@
#ifndef LIGHTING_GLSL
#define LIGHTING_GLSL
#include "lights.glsl"
#define PI 3.14159265359
// GGX / Trowbridge-Reitz normal distribution
float distributionGGX(float NoH, float roughness)
{
float a = roughness * roughness;
float a2 = a * a;
float d = NoH * NoH * (a2 - 1.0) + 1.0;
return a2 / (PI * d * d);
}
// Smith-Schlick GGX, k remapped for direct lighting
float geometrySmithGGX(float NoV, float NoL, float roughness)
{
float r = roughness + 1.0;
float k = (r * r) / 8.0;
float ggxV = NoV / (NoV * (1.0 - k) + k);
float ggxL = NoL / (NoL * (1.0 - k) + k);
return ggxV * ggxL;
}
vec3 fresnelSchlick(float VoH, vec3 f0)
{
return f0 + (1.0 - f0) * pow(1.0 - VoH, 5.0);
}
// Inverse square falloff, windowed so it reaches exactly 0 at the light range.
// The range is also the sphere the light culling uses, so no light gets cut off at a tile edge.
float calculateDistanceAttenuation(float distance, float range)
{
float window = clamp(1.0 - pow(distance / range, 4.0), 0.0, 1.0);
return (window * window) / max(distance * distance, 0.0001);
}
float calculateSpotAttenuation(Light light, vec3 l)
{
float cd = dot(normalize(light.direction), -l);
return smoothstep(light.spotOuterCos, light.spotInnerCos, cd);
}
vec3 calculateLight(Light light, vec3 fragPos, vec3 n, vec3 v,
vec3 diffuseColor, float roughness, float metallic, vec3 f0, float occlusion)
{
vec3 l;
float attenuation = 1.0;
if (light.type == TYPE_DIRECTIONAL_LIGHT) {
l = normalize(-light.direction);
} else {
vec3 toLight = light.position - fragPos;
float distance = length(toLight);
l = toLight / max(distance, 0.0001);
attenuation = calculateDistanceAttenuation(distance, light.range);
if (light.type == TYPE_SPOT_LIGHT) {
attenuation *= calculateSpotAttenuation(light, l);
}
}
float NoL = clamp(dot(n, l), 0.0, 1.0);
if (NoL <= 0.0 || attenuation <= 0.0) {
return vec3(0.0);
}
vec3 h = normalize(v + l);
float NoV = clamp(dot(n, v), 0.0001, 1.0);
float NoH = clamp(dot(n, h), 0.0, 1.0);
float VoH = clamp(dot(v, h), 0.0, 1.0);
float D = distributionGGX(NoH, roughness);
float G = geometrySmithGGX(NoV, NoL, roughness);
vec3 F = fresnelSchlick(VoH, f0);
vec3 specular = (D * G * F) / (4.0 * NoV * NoL + 0.0001);
vec3 kd = (vec3(1.0) - F) * (1.0 - metallic);
vec3 diffuse = kd * diffuseColor / PI;
return (diffuse + specular) * light.color * light.intensity * attenuation * NoL * occlusion;
}
// blue -> green -> red, used to visualise how many lights a tile has
vec3 lightHeatmapColor(uint lightCount)
{
float t = clamp(float(lightCount) / 16.0, 0.0, 1.0);
if (t < 0.5) {
return mix(vec3(0.0, 0.0, 1.0), vec3(0.0, 1.0, 0.0), t * 2.0);
}
return mix(vec3(0.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), (t - 0.5) * 2.0);
}
#endif // LIGHTING_GLSL
+42
View File
@@ -0,0 +1,42 @@
#ifndef LIGHTS_GLSL
#define LIGHTS_GLSL
#extension GL_EXT_buffer_reference : require
#define TYPE_DIRECTIONAL_LIGHT 0
#define TYPE_POINT_LIGHT 1
#define TYPE_SPOT_LIGHT 2
// Forward+ tiling, must match LightCullingPipeline::TILE_SIZE / MAX_LIGHTS_PER_TILE
#define LIGHT_TILE_SIZE 16
#define MAX_LIGHTS_PER_TILE 255
// Every tile stores its light count first, followed by MAX_LIGHTS_PER_TILE light indices
#define LIGHT_TILE_STRIDE (MAX_LIGHTS_PER_TILE + 1)
// Must match GPULight in Light.h
struct Light {
vec3 position;
float range;
vec3 color;
float intensity;
// direction the light is pointing in (directional + spot lights)
vec3 direction;
uint type;
float spotInnerCos;
float spotOuterCos;
uint castShadows;
float padding;
};
layout (buffer_reference, std430) readonly buffer LightsBuffer {
Light data[];
};
layout (buffer_reference, std430) buffer TileLightIndicesBuffer {
uint data[];
};
#endif // LIGHTS_GLSL
+9
View File
@@ -0,0 +1,9 @@
#version 460
layout (location = 0) in vec4 inColor;
layout (location = 0) out vec4 outFragColor;
void main()
{
outFragColor = inColor;
}
+16
View File
@@ -0,0 +1,16 @@
#version 460
layout (location = 0) in vec4 inPosition;
layout (location = 1) in vec4 inColor;
layout (location = 0) out vec4 outColor;
layout (push_constant) uniform LinePushConstants {
mat4 viewProjection;
} pcs;
void main()
{
gl_Position = pcs.viewProjection * inPosition;
outColor = inColor;
}
+74 -92
View File
@@ -5,6 +5,8 @@
#include "bindless.glsl" #include "bindless.glsl"
#include "scene_data.glsl" #include "scene_data.glsl"
#include "mesh_pcs.glsl" #include "mesh_pcs.glsl"
#include "lighting.glsl"
#include "shadows.glsl"
layout (location = 0) in vec3 inPos; layout (location = 0) in vec3 inPos;
layout (location = 1) in vec2 inUV; layout (location = 1) in vec2 inUV;
@@ -20,16 +22,13 @@ void main()
uint samplerID = pcs.sceneData.materials.data[pcs.materialID].textureFilteringMode; uint samplerID = pcs.sceneData.materials.data[pcs.materialID].textureFilteringMode;
vec4 diffuse = sampleTexture2D(material.diffuseTex, samplerID, inUV) * material.baseColor; vec4 diffuse = sampleTexture2D(material.diffuseTex, samplerID, inUV) * material.baseColor;
outFragColor = diffuse; vec3 baseColor = diffuse.rgb;
// if (diffuse.a < 0.1) { // if (diffuse.a < 0.1) {
// discard; // discard;
// } // }
//
// vec3 baseColor = material.baseColor.rgb * diffuse.rgb; vec3 normal = normalize(inNormal);
//
// vec3 normal = normalize(inNormal).rgb;
// if (inTangent != vec4(0.0)) { // if (inTangent != vec4(0.0)) {
// // FIXME: sometimes Blender doesn't export tangents for some objects // // FIXME: sometimes Blender doesn't export tangents for some objects
// // for some reason. When we will start computing tangents manually, // // for some reason. When we will start computing tangents manually,
@@ -39,91 +38,74 @@ void main()
// normal = inTBN * normalize(normal * 2.0 - 1.0); // normal = inTBN * normalize(normal * 2.0 - 1.0);
// normal = normalize(normal); // normal = normalize(normal);
// } // }
//
// #ifdef PBR
// float metallicF = material.metallicRoughnessEmissive.r;
// float roughnessF = material.metallicRoughnessEmissive.g;
//
// vec4 metallicRoughness = sampleTexture2DLinear(material.metallicRoughnessTex, inUV);
//
// float roughness = roughnessF * metallicRoughness.g;
// // roughness *= roughness; // from perceptual to linear
// roughness = max(roughness, 1e-2); // 0.0 roughness leads to NaNs
//
// float metallic = metallicF * metallicRoughness.b;
//
// vec3 dielectricSpecular = vec3(0.04);
// vec3 black = vec3(0.0);
// vec3 diffuseColor = mix(baseColor * (1.0 - dielectricSpecular.r), black, metallic);
// vec3 f0 = mix(dielectricSpecular, baseColor, metallic);
// #else
// vec3 diffuseColor = baseColor;
// float metallic = 0.f;
// float roughness = 1.f;
// vec3 f0 = vec3(0.0);
// #endif
//
// vec3 cameraPos = pcs.sceneData.cameraPos.xyz;
// vec3 fragPos = inPos.xyz;
// vec3 n = normal;
// vec3 v = normalize(cameraPos - fragPos);
//
// // diffuseColor = vec3(1.0);
// // baseColor = vec3(1.0);
//
// vec3 fragColor = vec3(0.0);
// for (int i = 0; i < pcs.sceneData.numLights; i++) {
// Light light = pcs.sceneData.lights.data[i];
//
// vec3 l = light.direction;
// if (light.type != TYPE_DIRECTIONAL_LIGHT) {
// l = normalize(light.position - fragPos);
// }
// float NoL = clamp(dot(n, l), 0.0, 1.0);
//
// float occlusion = 1.0;
// if (light.type == TYPE_DIRECTIONAL_LIGHT) {
// occlusion = calculateCSMOcclusion(
// fragPos, cameraPos, NoL,
// pcs.sceneData.csmShadowMapId,
// pcs.sceneData.cascadeFarPlaneZs,
// pcs.sceneData.csmLightSpaceTMs);
// } else if (light.type == TYPE_POINT_LIGHT && light.shadowMapID != 0) {
// occlusion = calculatePointShadow(
// fragPos, light.position, NoL,
// light.shadowMapID,
// pcs.sceneData.pointLightFarPlane);
// }
//
// fragColor += calculateLight(light, fragPos, n, v, l,
// diffuseColor, roughness, metallic, f0, occlusion);
// }
//
// // emissive
// float emissiveF = material.metallicRoughnessEmissive.b;
// vec3 emissiveColor = emissiveF * sampleTexture2DLinear(material.emissiveTex, inUV).rgb;
// fragColor += emissiveColor;
//
// // ambient
// fragColor += baseColor * pcs.sceneData.ambientColor * pcs.sceneData.ambientIntensity;
//
// #if 0
// // CSM DEBUG
// uint cascadeIndex = chooseCascade(inPos, cameraPos, pcs.sceneData.cascadeFarPlaneZs);
// fragColor *= debugShadowsFactor(cascadeIndex);
// #endif
//
// #if 0
// // TANGENT DEBUG
// if (inTangent == vec4(0.0)) {
// fragColor = vec3(1.0f, 0.0f, 0.0f);
// }
// #endif
//
// #if 1
// // NORMAL DEBUG
// // fragColor = normal;
// #endif
// outFragColor = vec4(1.0f, 0.0f, 0.0f, 1.0f); float metallicF = material.metallicRoughnessEmissive.r;
float roughnessF = material.metallicRoughnessEmissive.g;
vec4 metallicRoughness = sampleTexture2DLinear(material.metallicRoughnessTex, inUV);
float roughness = roughnessF * metallicRoughness.g;
// roughness *= roughness; // from perceptual to linear
roughness = max(roughness, 1e-2); // 0.0 roughness leads to NaNs
float metallic = metallicF * metallicRoughness.b;
vec3 dielectricSpecular = vec3(0.04);
vec3 black = vec3(0.0);
vec3 diffuseColor = mix(baseColor * (1.0 - dielectricSpecular.r), black, metallic);
vec3 f0 = mix(dielectricSpecular, baseColor, metallic);
vec3 cameraPos = pcs.sceneData.cameraPos.xyz;
vec3 fragPos = inPos.xyz;
vec3 n = normal;
vec3 v = normalize(cameraPos - fragPos);
// forward+: only loop over the lights the culling pass put in this pixel's tile
uvec2 tileID = uvec2(gl_FragCoord.xy) / LIGHT_TILE_SIZE;
uint tileOffset = (tileID.y * pcs.sceneData.tileCountX + tileID.x) * LIGHT_TILE_STRIDE;
uint tileLightCount = pcs.sceneData.tileLightIndices.data[tileOffset];
vec3 fragColor = vec3(0.0);
for (uint i = 0; i < tileLightCount; i++) {
uint lightIndex = pcs.sceneData.tileLightIndices.data[tileOffset + 1 + i];
Light light = pcs.sceneData.lights.data[lightIndex];
float occlusion = 1.0;
if (light.castShadows != 0) {
occlusion = calculateDirectionalShadow(
fragPos, n,
pcs.sceneData.lightViewProj,
pcs.sceneData.shadowMapId,
pcs.sceneData.shadowNormalOffset);
}
fragColor += calculateLight(light, fragPos, n, v,
diffuseColor, roughness, metallic, f0, occlusion);
}
// emissive
float emissiveF = material.metallicRoughnessEmissive.b;
vec3 emissiveColor = emissiveF * sampleTexture2DLinear(material.emissiveTex, inUV).rgb;
fragColor += emissiveColor;
// ambient
fragColor += baseColor * pcs.sceneData.ambientColor * pcs.sceneData.ambientIntensity;
if (pcs.sceneData.debugLightHeatmap != 0) {
fragColor = mix(fragColor, lightHeatmapColor(tileLightCount), 0.5);
}
#if 0
// TANGENT DEBUG
if (inTangent == vec4(0.0)) {
fragColor = vec3(1.0f, 0.0f, 0.0f);
}
#endif
#if 0
// NORMAL DEBUG
fragColor = normal;
#endif
outFragColor = vec4(fragColor, diffuse.a);
} }
+4
View File
@@ -10,6 +10,10 @@ layout (location = 2) out vec3 outNormal;
layout (location = 3) out vec4 outTangent; layout (location = 3) out vec4 outTangent;
layout (location = 4) out mat3 outTBN; layout (location = 4) out mat3 outTBN;
// The depth pre-pass reuses this shader, positions have to match bit for bit
// so the main pass can depth test against it with LESS_OR_EQUAL
invariant gl_Position;
void main() void main()
{ {
Vertex v = pcs.vertexBuffer.vertices[gl_VertexIndex]; Vertex v = pcs.vertexBuffer.vertices[gl_VertexIndex];
+16
View File
@@ -0,0 +1,16 @@
#version 460
#extension GL_GOOGLE_include_directive : require
#extension GL_EXT_nonuniform_qualifier : enable
#include "bindless.glsl"
layout(location = 0) in vec2 inUV;
layout(location = 1) in vec4 inColor;
layout(location = 2) flat in uint inTexId;
layout(location = 0) out vec4 outFragColor;
void main() {
vec4 texColor = sampleTexture2DLinear(inTexId, inUV);
outFragColor = texColor * inColor;
}
+22
View File
@@ -0,0 +1,22 @@
#version 460
#extension GL_GOOGLE_include_directive : require
layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec2 inUV;
layout(location = 2) in vec4 inColor;
layout(location = 3) in uint inTexId;
layout(location = 0) out vec2 outUV;
layout(location = 1) out vec4 outColor;
layout(location = 2) flat out uint outTexId;
layout(push_constant) uniform QuadPushConstants {
mat4 viewProjection;
} pcs;
void main() {
gl_Position = pcs.viewProjection * vec4(inPosition, 0.0, 1.0);
outUV = inUV;
outColor = inColor;
outTexId = inTexId;
}
@@ -6,11 +6,14 @@
#extension GL_EXT_buffer_reference : require #extension GL_EXT_buffer_reference : require
#include "materials.glsl" #include "materials.glsl"
#include "lights.glsl"
// Must match GameRenderer::GPUSceneData
layout (buffer_reference, scalar) readonly buffer SceneDataBuffer { layout (buffer_reference, scalar) readonly buffer SceneDataBuffer {
// camera // camera
mat4 view; mat4 view;
mat4 proj; mat4 proj;
mat4 invProj;
mat4 viewProj; mat4 viewProj;
vec4 cameraPos; vec4 cameraPos;
@@ -23,6 +26,19 @@ layout (buffer_reference, scalar) readonly buffer SceneDataBuffer {
float fogDensity; float fogDensity;
MaterialsBuffer materials; MaterialsBuffer materials;
// forward+
LightsBuffer lights;
TileLightIndicesBuffer tileLightIndices;
uint numLights;
uint tileCountX;
uint debugLightHeatmap;
// directional shadows
uint shadowMapId;
float shadowNormalOffset;
uint padding;
mat4 lightViewProj;
} sceneDataBuffer; } sceneDataBuffer;
#endif // SCENE_DATA_GLSL #endif // SCENE_DATA_GLSL
+12
View File
@@ -0,0 +1,12 @@
#version 460
#extension GL_GOOGLE_include_directive : require
#include "mesh_pcs.glsl"
void main()
{
Vertex v = pcs.vertexBuffer.vertices[gl_VertexIndex];
gl_Position = pcs.sceneData.lightViewProj * pcs.transform * vec4(v.position, 1.0f);
}
+34
View File
@@ -0,0 +1,34 @@
#ifndef SHADOWS_GLSL
#define SHADOWS_GLSL
// expects bindless.glsl to be included before this file (it has no include guard)
// Returns 1.0 when fully lit, 0.0 when fully in shadow
float calculateDirectionalShadow(vec3 fragPos, vec3 n, mat4 lightViewProj, uint shadowMapID, float normalOffset)
{
// pushing the sample point along the normal hides most of the shadow acne
vec4 lightClip = lightViewProj * vec4(fragPos + n * normalOffset, 1.0);
vec3 lightNDC = lightClip.xyz / lightClip.w;
vec2 uv = lightNDC.xy * 0.5 + 0.5;
// outside the shadow map counts as lit
if (any(lessThan(uv, vec2(0.0))) || any(greaterThan(uv, vec2(1.0))) || lightNDC.z > 1.0) {
return 1.0;
}
vec2 texelSize = 1.0 / vec2(textureSize(sampler2DShadow(textures[shadowMapID], samplers[SHADOW_SAMPLER_ID]), 0));
// 3x3 PCF, every tap is already bilinearly filtered by the compare sampler
float shadowed = 0.0;
for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) {
shadowed += sampleTexture2DShadow(shadowMapID, vec3(uv + vec2(x, y) * texelSize, lightNDC.z));
}
}
// the shadow sampler compares with GREATER_OR_EQUAL (it was set up for reversed Z),
// with regular depth that returns 1 when the fragment is behind the occluder
return 1.0 - shadowed / 9.0;
}
#endif // SHADOWS_GLSL
+2 -2
View File
@@ -10,7 +10,6 @@ layout(location = 0) out vec4 outColor;
layout(push_constant) uniform SkyboxPC { layout(push_constant) uniform SkyboxPC {
mat4 invViewProj; mat4 invViewProj;
vec4 skyboxRot[3]; vec4 skyboxRot[3];
vec3 cameraPos;
uint skyboxTextureId; uint skyboxTextureId;
} pcs; } pcs;
@@ -22,7 +21,8 @@ void main()
vec4 world = pcs.invViewProj * ndc; vec4 world = pcs.invViewProj * ndc;
vec3 worldPos = world.xyz / world.w; vec3 worldPos = world.xyz / world.w;
vec3 dir = normalize(worldPos - pcs.cameraPos); // invViewProj has no translation, so the camera sits at the origin
vec3 dir = normalize(worldPos);
dir.y *= -1.0; dir.y *= -1.0;
mat3 skyboxRot = mat3( mat3 skyboxRot = mat3(
+15
View File
@@ -11,6 +11,7 @@
#include <destrum/Graphics/GfxDevice.h> #include <destrum/Graphics/GfxDevice.h>
#include <destrum/Graphics/Renderer.h> #include <destrum/Graphics/Renderer.h>
#include <destrum/Graphics/Pipelines/ImguiPass.h> #include <destrum/Graphics/Pipelines/ImguiPass.h>
#include <destrum/Graphics/ResourcesInspector.h>
#include <destrum/Input/InputManager.h> #include <destrum/Input/InputManager.h>
@@ -41,6 +42,9 @@ public:
virtual void onWindowResize(int, int) {}; virtual void onWindowResize(int, int) {};
// Called inside the "Windows" menu of the main menu bar, add toggles for the game's own windows here
virtual void customWindowsMenu() {}
protected: protected:
SDL_Window* window{nullptr}; SDL_Window* window{nullptr};
AppParams m_params{}; AppParams m_params{};
@@ -72,6 +76,17 @@ protected:
float m_PhysicsTimeScale = 1.0f; float m_PhysicsTimeScale = 1.0f;
int m_PhysicsStepsLastFrame = 0; int m_PhysicsStepsLastFrame = 0;
struct DebugWindows {
bool stats{true};
bool physics{true};
bool inspector{true};
bool imageCache{false};
};
DebugWindows m_DebugWindows{};
ResourcesInspector m_ResourcesInspector;
void drawWindowsMenu();
void drawPhysicsPanel(float dt, float fixedDt, float accumulator); void drawPhysicsPanel(float dt, float fixedDt, float accumulator);
}; };
@@ -28,6 +28,10 @@ public:
private: private:
AssetManager() = default; AssetManager() = default;
[[nodiscard]] ModelDoc::ModelAsset LoadCookedOrImport(
const std::string& reference,
const ModelDoc::LoadOptions& options) const;
}; };
#endif // DESTRUM_ASSETMANAGER_H #endif // DESTRUM_ASSETMANAGER_H
@@ -0,0 +1,31 @@
#ifndef DESTRUM_MODELFILE_H
#define DESTRUM_MODELFILE_H
#include <cstdint>
#include <filesystem>
#include <optional>
#include <span>
#include <string_view>
#include <vector>
#include <destrum/Util/ModelDoc.h>
namespace ModelFile {
inline constexpr std::uint32_t Version = 1;
inline constexpr std::string_view Extension = ".dmodel";
inline constexpr ModelDoc::LoadOptions CookOptions{};
[[nodiscard]] bool SupportsLoadOptions(const ModelDoc::ModelAsset& model, const ModelDoc::LoadOptions& options);
void ApplyLoadOptions(ModelDoc::ModelAsset& model, const ModelDoc::LoadOptions& options);
[[nodiscard]] std::vector<std::uint8_t> Write(const ModelDoc::ModelAsset& model);
[[nodiscard]] ModelDoc::ModelAsset Read(std::span<const std::uint8_t> bytes);
void WriteToFile(const ModelDoc::ModelAsset& model, const std::filesystem::path& path);
[[nodiscard]] ModelDoc::ModelAsset ReadFromFile(const std::filesystem::path& path);
[[nodiscard]] std::optional<std::uint32_t> ReadVersionFromFile(const std::filesystem::path& path);
}
#endif // DESTRUM_MODELFILE_H
@@ -0,0 +1,38 @@
#ifndef DESTRUM_TEXTUREFILE_H
#define DESTRUM_TEXTUREFILE_H
#include <cstdint>
#include <filesystem>
#include <optional>
#include <span>
#include <string_view>
#include <vector>
namespace TextureFile {
inline constexpr std::uint32_t Version = 1;
inline constexpr std::string_view Extension = ".dtex";
enum class PixelFormat : std::uint32_t {
RGBA16Float,
};
struct Texture {
std::uint32_t width{0};
std::uint32_t height{0};
PixelFormat format{PixelFormat::RGBA16Float};
std::vector<std::uint8_t> pixels;
};
[[nodiscard]] std::size_t BytesPerPixel(PixelFormat format);
[[nodiscard]] Texture FromRGBA32Float(std::uint32_t width, std::uint32_t height, std::span<const float> rgba);
[[nodiscard]] std::vector<std::uint8_t> Write(const Texture& texture);
[[nodiscard]] Texture Read(std::span<const std::uint8_t> bytes);
void WriteToFile(const Texture& texture, const std::filesystem::path& path);
[[nodiscard]] Texture ReadFromFile(const std::filesystem::path& path);
[[nodiscard]] std::optional<std::uint32_t> ReadVersionFromFile(const std::filesystem::path& path);
}
#endif // DESTRUM_TEXTUREFILE_H
@@ -0,0 +1,44 @@
#ifndef CAMERACOMPONENT_H
#define CAMERACOMPONENT_H
#include <destrum/ObjectModel/Component.h>
#include <destrum/Graphics/Camera.h>
#include <glm/glm.hpp>
class CameraComponent final : public Component {
public:
explicit CameraComponent(GameObject& parent);
CameraComponent(const CameraComponent& other) = delete;
CameraComponent(CameraComponent&& other) noexcept = delete;
CameraComponent& operator=(const CameraComponent& other) = delete;
CameraComponent& operator=(CameraComponent&& other) noexcept = delete;
std::string GetTypeName() const override { return "CameraComponent"; }
nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override;
void ImGuiInspector() override;
// Places the camera at this object's world transform, looking down its local -Z.
[[nodiscard]] Camera& GetCamera(float aspectRatio);
void SetFieldOfView(float fovDegrees);
[[nodiscard]] float GetFieldOfView() const { return m_FieldOfView; }
void SetClipPlanes(float zNear, float zFar);
[[nodiscard]] float GetZNear() const { return m_ZNear; }
[[nodiscard]] float GetZFar() const { return m_ZFar; }
void MakeCurrent();
[[nodiscard]] bool IsCurrent() const;
private:
Camera m_Camera{glm::vec3{0.0f}, glm::vec3{0.0f, 1.0f, 0.0f}};
float m_FieldOfView{90.0f};
float m_ZNear{0.01f};
float m_ZFar{1000.0f};
};
#endif // CAMERACOMPONENT_H
@@ -0,0 +1,55 @@
#ifndef LIGHTCOMPONENT_H
#define LIGHTCOMPONENT_H
#include <destrum/ObjectModel/Component.h>
#include <destrum/Graphics/Light.h>
#include <glm/glm.hpp>
class LightComponent final : public Component {
public:
explicit LightComponent(GameObject& parent, LightType type = LightType::Point);
LightComponent(const LightComponent& other) = delete;
LightComponent(LightComponent&& other) noexcept = delete;
LightComponent& operator=(const LightComponent& other) = delete;
LightComponent& operator=(LightComponent&& other) noexcept = delete;
void Render(const RenderContext& ctx) override;
std::string GetTypeName() const override { return "LightComponent"; }
nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override;
void ImGuiInspector() override;
void SetType(LightType type) { m_Type = type; }
[[nodiscard]] LightType GetType() const { return m_Type; }
void SetColor(const glm::vec3& color) { m_Color = color; }
[[nodiscard]] const glm::vec3& GetColor() const { return m_Color; }
void SetIntensity(float intensity);
[[nodiscard]] float GetIntensity() const { return m_Intensity; }
void SetRange(float range);
[[nodiscard]] float GetRange() const { return m_Range; }
void SetConeAngles(float innerConeAngle, float outerConeAngle);
[[nodiscard]] float GetInnerConeAngle() const { return m_InnerConeAngle; }
[[nodiscard]] float GetOuterConeAngle() const { return m_OuterConeAngle; }
void SetCastShadows(bool castShadows) { m_CastShadows = castShadows; }
[[nodiscard]] bool GetCastShadows() const { return m_CastShadows; }
private:
LightType m_Type{LightType::Point};
glm::vec3 m_Color{1.0f};
float m_Intensity{1.0f};
float m_Range{10.0f};
float m_InnerConeAngle{0.35f};
float m_OuterConeAngle{0.5f};
bool m_CastShadows{false};
};
#endif // LIGHTCOMPONENT_H
@@ -101,6 +101,7 @@ public:
float GetZFar() const { return m_zFar; } float GetZFar() const { return m_zFar; }
void SetZFar(float zFar) { m_zFar = zFar; } void SetZFar(float zFar) { m_zFar = zFar; }
float GetFOVY() const { return fovAngle; } float GetFOVY() const { return fovAngle; }
void SetFOVY(float fovDegrees) { fovAngle = fovDegrees; }
float GetAspectRatio() const { return m_aspectRatio; } float GetAspectRatio() const { return m_aspectRatio; }
glm::vec3 m_position{2.f, 0, 0}; glm::vec3 m_position{2.f, 0, 0};
@@ -127,6 +128,7 @@ public:
void SetRotation(float yawRadians, float pitchRadians); void SetRotation(float yawRadians, float pitchRadians);
void SetRotation(const glm::vec2& yawPitchRadians); void SetRotation(const glm::vec2& yawPitchRadians);
void SetOrientation(const glm::vec3& forward, const glm::vec3& up);
float GetYaw() const { return m_yaw; } float GetYaw() const { return m_yaw; }
float GetPitch() const { return m_pitch; } float GetPitch() const { return m_pitch; }
+51
View File
@@ -0,0 +1,51 @@
#ifndef LIGHT_H
#define LIGHT_H
#include <cstdint>
#include <glm/vec2.hpp>
#include <glm/vec3.hpp>
enum class LightType : std::uint32_t {
Directional = 0,
Point = 1,
Spot = 2,
};
struct Light {
LightType type{LightType::Point};
glm::vec3 position{0.0f};
glm::vec3 direction{0.0f, -1.0f, 0.0f};
glm::vec3 color{1.0f};
float intensity{1.0f};
float range{10.0f};
float innerConeAngle{0.35f};
float outerConeAngle{0.5f};
// directional lights only, the first one submitted each frame gets the shadow map
bool castShadows{false};
};
struct GPULight {
glm::vec3 position;
float range;
glm::vec3 color;
float intensity;
glm::vec3 direction;
std::uint32_t type;
float spotInnerCos;
float spotOuterCos;
std::uint32_t castShadows;
float padding;
};
static_assert(sizeof(GPULight) == 64, "GPULight must match the struct in lights.glsl");
#endif //LIGHT_H
@@ -0,0 +1,37 @@
#ifndef DESTRUM_LINERENDERINGMANAGER_H
#define DESTRUM_LINERENDERINGMANAGER_H
#include <vector>
#include <glm/vec3.hpp>
#include <glm/vec4.hpp>
#include <destrum/Singleton.h>
struct Line {
glm::vec3 start{};
glm::vec3 end{};
glm::vec4 color{1.0f};
};
class LineRenderingManager final : public Singleton<LineRenderingManager> {
public:
friend class Singleton<LineRenderingManager>;
void SubmitLine(const Line& line);
void SubmitLine(
const glm::vec3& start,
const glm::vec3& end,
const glm::vec4& color = glm::vec4{1.0f});
[[nodiscard]] const std::vector<Line>& GetLines() const { return lines; }
void ClearLines();
private:
LineRenderingManager() = default;
std::vector<Line> lines;
};
#endif // DESTRUM_LINERENDERINGMANAGER_H
@@ -0,0 +1,45 @@
#ifndef DESTRUM_QUADRENDERINGMANAGER_H
#define DESTRUM_QUADRENDERINGMANAGER_H
#include <vector>
#include <glm/glm.hpp>
#include <destrum/Graphics/ids.h>
#include <destrum/Singleton.h>
struct QuadSubmission {
glm::vec2 position;
glm::vec2 size;
float rotation;
glm::vec4 color;
ImageID textureId;
};
class QuadRenderingManager final : public Singleton<QuadRenderingManager> {
public:
friend class Singleton<QuadRenderingManager>;
void SubmitQuad(
const glm::vec2& position,
const glm::vec2& size,
const glm::vec4& color = glm::vec4{1.0f},
ImageID textureId = NULL_IMAGE_ID);
void SubmitQuad(
const glm::vec2& position,
const glm::vec2& size,
float rotation,
const glm::vec4& color = glm::vec4{1.0f},
ImageID textureId = NULL_IMAGE_ID);
[[nodiscard]] const std::vector<QuadSubmission>& GetQuads() const { return quads; }
void ClearQuads();
private:
QuadRenderingManager() = default;
std::vector<QuadSubmission> quads;
};
#endif // DESTRUM_QUADRENDERINGMANAGER_H
@@ -1,8 +1,15 @@
#ifndef DESTRUM_IMGUI_PASS_H #ifndef DESTRUM_IMGUI_PASS_H
#define DESTRUM_IMGUI_PASS_H #define DESTRUM_IMGUI_PASS_H
#include <array>
#include <utility>
#include <vector>
#include <vulkan/vulkan.h> #include <vulkan/vulkan.h>
#include <SDL.h> #include <SDL.h>
#include <imgui.h>
#include <destrum/Graphics/Swapchain.h>
class GfxDevice; class GfxDevice;
@@ -33,9 +40,19 @@ public:
[[nodiscard]] bool wantsMouse() const; [[nodiscard]] bool wantsMouse() const;
[[nodiscard]] bool wantsKeyboard() const; [[nodiscard]] bool wantsKeyboard() const;
// Wraps an image view for ImGui::Image in the frame that's being built. Call it every frame
// you draw the image, the descriptor set is freed again once the GPU is done with that frame.
// The image has to be in imageLayout by the time the UI gets rendered at the end of the frame.
[[nodiscard]] ImTextureID getFrameTexture(
VkImageView imageView,
VkImageLayout imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
private: private:
void initVulkanBackend(); void initVulkanBackend();
void releaseTextures(std::vector<VkDescriptorSet>& textures);
void releaseAllTextures();
void transitionToColorAttachment( void transitionToColorAttachment(
VkCommandBuffer cmd, VkCommandBuffer cmd,
VkImage image, VkImage image,
@@ -50,6 +67,17 @@ private:
VkDescriptorPool descriptorPool = VK_NULL_HANDLE; VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
VkFormat colorFormat = VK_FORMAT_UNDEFINED; VkFormat colorFormat = VK_FORMAT_UNDEFINED;
struct FrameTexture {
VkImageView imageView{VK_NULL_HANDLE};
VkImageLayout imageLayout{VK_IMAGE_LAYOUT_UNDEFINED};
VkDescriptorSet descriptorSet{VK_NULL_HANDLE};
};
// created while building the current UI, not submitted yet
std::vector<FrameTexture> pendingTextures;
// submitted with a frame, freed once that frame's fence has been waited on
std::array<std::vector<VkDescriptorSet>, FRAMES_IN_FLIGHT> inFlightTextures;
bool initialized = false; bool initialized = false;
bool frameBegun = false; bool frameBegun = false;
bool contextCreated = false; bool contextCreated = false;
@@ -0,0 +1,78 @@
#ifndef LIGHTCULLINGPIPELINE_H
#define LIGHTCULLINGPIPELINE_H
#include <vulkan/vulkan.h>
#include <array>
#include <memory>
#include <glm/glm.hpp>
#include <destrum/Graphics/ComputePipeline.h>
#include <destrum/Graphics/Light.h>
#include <destrum/Graphics/ids.h>
#include <destrum/Graphics/Resources/AppendableBuffer.h>
#include <destrum/Graphics/Resources/Buffer.h>
class GfxDevice;
class RenderResources;
class LightCullingPipeline final {
public:
static constexpr std::uint32_t TILE_SIZE = 16;
static constexpr std::uint32_t MAX_LIGHTS_PER_TILE = 255;
static constexpr std::uint32_t TILE_STRIDE = MAX_LIGHTS_PER_TILE + 1;
static constexpr std::size_t MAX_LIGHTS = 1024;
void init(GfxDevice& gfxDevice, RenderResources& resources, glm::ivec2 drawImageSize);
void cleanup(GfxDevice& gfxDevice);
void resize(GfxDevice& gfxDevice, glm::ivec2 drawImageSize);
void beginDrawing(std::size_t frameIndex);
void appendLight(const Light& light);
void flushCurrentFrame(VkCommandBuffer cmd, GfxDevice& gfxDevice);
void cullLights(
VkCommandBuffer cmd,
const RenderResources& resources,
const GPUBuffer& sceneDataBuffer,
ImageID depthImageId,
VkExtent2D renderExtent);
[[nodiscard]] VkDeviceAddress getLightsBufferAddress() const;
[[nodiscard]] VkDeviceAddress getTileLightIndicesBufferAddress() const { return tileLightIndicesBuffer.address; }
[[nodiscard]] std::uint32_t getNumLights() const;
[[nodiscard]] glm::uvec2 getTileCount() const { return tileCount; }
private:
void createTileBuffer(GfxDevice& gfxDevice, glm::ivec2 drawImageSize);
VkPipelineLayout m_pipelineLayout{VK_NULL_HANDLE};
std::unique_ptr<ComputePipeline> lightCullingPipeline;
struct PushConstants {
VkDeviceAddress sceneDataBuffer;
glm::uvec2 screenSize;
std::uint32_t depthTexId;
std::uint32_t padding;
};
struct PerFrameData {
AppendableBuffer<GPULight> lightsBuffer;
};
std::array<PerFrameData, FRAMES_IN_FLIGHT> framesData;
std::size_t currentFrameIndex{0};
bool warnedAboutLightLimit{false};
GPUBuffer tileLightIndicesBuffer;
glm::uvec2 tileCount{0u, 0u};
PerFrameData& getCurrentFrameData();
const PerFrameData& getCurrentFrameData() const;
};
#endif //LIGHTCULLINGPIPELINE_H
@@ -0,0 +1,42 @@
#ifndef DESTRUM_LINERENDERINGPASS_H
#define DESTRUM_LINERENDERINGPASS_H
#include <cstddef>
#include <memory>
#include <vulkan/vulkan.h>
#include <destrum/Graphics/Managers/LineRenderingManager.h>
#include <destrum/Graphics/Resources/NBuffer.h>
class Camera;
class GfxDevice;
struct GPUImage;
class Pipeline;
class LineRenderingPass final {
public:
void init(GfxDevice& gfxDevice, VkFormat drawImageFormat);
void draw(
VkCommandBuffer cmd,
GfxDevice& gfxDevice,
const GPUImage& target,
const Camera& camera,
LineRenderingManager& lineManager);
void cleanup(GfxDevice& gfxDevice);
private:
struct LineVertex {
glm::vec4 position;
glm::vec4 color;
};
void ensureVertexCapacity(GfxDevice& gfxDevice, std::size_t requiredVertices);
VkPipelineLayout pipelineLayout{VK_NULL_HANDLE};
std::unique_ptr<Pipeline> pipeline;
NBuffer vertexBuffer;
std::size_t vertexCapacity{0};
};
#endif // DESTRUM_LINERENDERINGPASS_H
@@ -27,6 +27,15 @@ public:
VkFormat drawImageFormat, VkFormat drawImageFormat,
VkFormat depthImageFormat); VkFormat depthImageFormat);
void drawDepthPrepass(
VkCommandBuffer cmd,
VkExtent2D renderExtent,
const RenderResources& resources,
const Camera& camera,
const GPUBuffer& sceneDataBuffer,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands);
void draw( void draw(
VkCommandBuffer cmd, VkCommandBuffer cmd,
VkExtent2D renderExtent, VkExtent2D renderExtent,
@@ -55,9 +64,20 @@ private:
std::uint32_t padding; std::uint32_t padding;
}; };
void drawMeshes(
VkCommandBuffer cmd,
const Pipeline& pipeline,
VkExtent2D renderExtent,
const RenderResources& resources,
const Camera& camera,
const GPUBuffer& sceneDataBuffer,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands);
private: private:
VkPipelineLayout m_pipelineLayout{VK_NULL_HANDLE}; VkPipelineLayout m_pipelineLayout{VK_NULL_HANDLE};
std::unique_ptr<Pipeline> m_pipeline; std::unique_ptr<Pipeline> m_pipeline;
std::unique_ptr<Pipeline> m_depthPrepassPipeline;
bool m_renderWireframe{false}; bool m_renderWireframe{false};
}; };
@@ -0,0 +1,47 @@
#ifndef DESTRUM_QUADRENDERERPASS_H
#define DESTRUM_QUADRENDERERPASS_H
#include <cstddef>
#include <memory>
#include <glm/glm.hpp>
#include <vulkan/vulkan.h>
#include <destrum/Graphics/Resources/Buffer.h>
class GfxDevice;
class Pipeline;
struct GPUImage;
class RenderResources;
class QuadRendererPass final {
public:
void init(GfxDevice& gfxDevice, RenderResources& resources, VkFormat drawImageFormat);
void draw(
VkCommandBuffer cmd,
GfxDevice& gfxDevice,
RenderResources& resources,
const GPUImage& target,
const glm::mat4& projection);
void cleanup(GfxDevice& gfxDevice);
private:
struct QuadVertex {
glm::vec2 position;
glm::vec2 uv;
glm::vec4 color;
std::uint32_t textureId;
};
void ensureCapacity(GfxDevice& gfxDevice, std::size_t requiredQuads);
VkPipelineLayout pipelineLayout{VK_NULL_HANDLE};
std::unique_ptr<Pipeline> pipeline;
GPUBuffer vertexBuffer{};
GPUBuffer indexBuffer{};
std::size_t quadCapacity{0};
};
#endif // DESTRUM_QUADRENDERERPASS_H
@@ -0,0 +1,65 @@
#ifndef SHADOWMAPPIPELINE_H
#define SHADOWMAPPIPELINE_H
#include <cstdint>
#include <memory>
#include <vector>
#include <glm/glm.hpp>
#include <vulkan/vulkan.h>
#include <destrum/Graphics/Pipeline.h>
#include <destrum/Graphics/ids.h>
#include <destrum/Graphics/MeshDrawCommand.h>
#include <destrum/Graphics/Resources/Buffer.h>
class GfxDevice;
class RenderResources;
// Renders the depth of the scene as seen from a directional light, centered around the camera
class ShadowMapPipeline final
{
public:
static constexpr std::uint32_t SHADOW_MAP_SIZE = 2048;
void init(GfxDevice& gfxDevice, RenderResources& resources);
void cleanup(VkDevice device);
void draw(
VkCommandBuffer cmd,
const RenderResources& resources,
const GPUBuffer& sceneDataBuffer,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands);
[[nodiscard]] glm::mat4 calculateLightViewProj(const glm::vec3& lightDirection, const glm::vec3& center) const;
[[nodiscard]] ImageID getShadowMapId() const { return shadowMapId; }
[[nodiscard]] float getNormalOffset() const;
// half the width of the area around the camera that receives shadows
void setShadowDistance(float distance) { shadowDistance = glm::max(distance, 1.0f); }
[[nodiscard]] float getShadowDistance() const { return shadowDistance; }
private:
struct PushConstants
{
glm::mat4 transform;
VkDeviceAddress sceneDataBuffer;
VkDeviceAddress vertexBuffer;
std::uint32_t materialId;
std::uint32_t padding;
};
VkPipelineLayout m_pipelineLayout{VK_NULL_HANDLE};
std::unique_ptr<Pipeline> m_pipeline;
VkFormat shadowMapFormat{VK_FORMAT_D32_SFLOAT};
ImageID shadowMapId{NULL_IMAGE_ID};
float shadowDistance{25.0f};
// how far in front of / behind the camera (along the light) casters are still captured
float shadowDepthRange{100.0f};
};
#endif //SHADOWMAPPIPELINE_H
@@ -35,7 +35,6 @@ private:
struct SkyboxPushConstants { struct SkyboxPushConstants {
glm::mat4 invViewProj; glm::mat4 invViewProj;
glm::vec4 skyboxRot[3]; glm::vec4 skyboxRot[3];
glm::vec3 cameraPos;
std::uint32_t skyboxTextureId; std::uint32_t skyboxTextureId;
}; };
@@ -53,11 +53,19 @@ public:
[[nodiscard]] ImageID getWhiteTextureID() const { return whiteImageId; } [[nodiscard]] ImageID getWhiteTextureID() const { return whiteImageId; }
[[nodiscard]] ImageID getErrorTextureID() const { return errorImageId; } [[nodiscard]] ImageID getErrorTextureID() const { return errorImageId; }
[[nodiscard]] ImageID getDefaultNormalTextureID() const { return defaultNormalImageId; }
BindlessSetManager& getBindlessSetManager(); BindlessSetManager& getBindlessSetManager();
VkDescriptorSetLayout getBindlessDescSetLayout() const; VkDescriptorSetLayout getBindlessDescSetLayout() const;
const VkDescriptorSet& getBindlessDescSet() const; const VkDescriptorSet& getBindlessDescSet() const;
void bindBindlessDescSet(VkCommandBuffer cmd, VkPipelineLayout layout) const; void bindBindlessDescSet(
VkCommandBuffer cmd,
VkPipelineLayout layout,
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS) const;
[[nodiscard]] bool isInitialized() const { return imageCache != nullptr && materialCache != nullptr; }
const ImageCache& images() const { return *imageCache; }
MeshCache& meshes() { return *meshCache; } MeshCache& meshes() { return *meshCache; }
const MeshCache& meshes() const { return *meshCache; } const MeshCache& meshes() const { return *meshCache; }
+70 -12
View File
@@ -1,10 +1,16 @@
#ifndef RENDERER_H #ifndef RENDERER_H
#define RENDERER_H #define RENDERER_H
#include <glm/mat4x4.hpp>
#include <glm/vec3.hpp> #include <glm/vec3.hpp>
#include <destrum/Graphics/Camera.h> #include <destrum/Graphics/Camera.h>
#include <destrum/Graphics/Light.h>
#include <destrum/Graphics/Pipelines/LightCullingPipeline.h>
#include <destrum/Graphics/Pipelines/LineRenderingPass.h>
#include <destrum/Graphics/Pipelines/MeshPipeline.h> #include <destrum/Graphics/Pipelines/MeshPipeline.h>
#include <destrum/Graphics/Pipelines/QuadRendererPass.h>
#include <destrum/Graphics/Pipelines/ShadowMapPipeline.h>
#include <destrum/Graphics/ids.h> #include <destrum/Graphics/ids.h>
#include <destrum/Graphics/MeshDrawCommand.h> #include <destrum/Graphics/MeshDrawCommand.h>
#include <destrum/Graphics/Resources/NBuffer.h> #include <destrum/Graphics/Resources/NBuffer.h>
@@ -12,14 +18,17 @@
#include <memory> #include <memory>
#include <optional>
#include <destrum/Graphics/Pipelines/SkinningPipeline.h> #include <destrum/Graphics/Pipelines/SkinningPipeline.h>
#include "Pipelines/SkyboxPipeline.h" #include "Pipelines/SkyboxPipeline.h"
class GameRenderer { class GameRenderer
{
public: public:
struct SceneData { struct SceneData
{
const Camera& camera; const Camera& camera;
glm::vec3 ambientColor; glm::vec3 ambientColor;
float ambientIntensity; float ambientIntensity;
@@ -37,11 +46,15 @@ public:
void cleanup(GfxDevice& gfxDevice); void cleanup(GfxDevice& gfxDevice);
void drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId); void drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId);
void drawSkinnedMesh(MeshID id, const glm::mat4& transform, MaterialID materialId, SkinnedMesh* skinnedMesh, std::size_t jointMatricesStartIndex); void drawSkinnedMesh(MeshID id, const glm::mat4& transform, MaterialID materialId, SkinnedMesh* skinnedMesh,
std::size_t jointMatricesStartIndex);
void submitLight(const Light& light);
const GPUImage& getDrawImage() const; const GPUImage& getDrawImage() const;
void resize(GfxDevice& gfxDevice, const glm::ivec2& newSize) { void resize(GfxDevice& gfxDevice, const glm::ivec2& newSize)
{
createDrawImage(gfxDevice, newSize, false); createDrawImage(gfxDevice, newSize, false);
lightCullingPipeline->resize(gfxDevice, newSize);
} }
Material& getMaterialMutable(MaterialID id); Material& getMaterialMutable(MaterialID id);
@@ -51,23 +64,51 @@ public:
void flushMaterialUpdates(GfxDevice& gfxDevice); void flushMaterialUpdates(GfxDevice& gfxDevice);
[[nodiscard]] SkinningPipeline& getSkinningPipeline() const { [[nodiscard]] SkinningPipeline& getSkinningPipeline() const
{
return *skinningPipeline; return *skinningPipeline;
} }
void setRenderWireframe(bool wireframe) { void setRenderWireframe(bool wireframe)
{
meshPipeline->setRenderWireframe(wireframe); meshPipeline->setRenderWireframe(wireframe);
} }
bool getRenderWireframe() { bool getRenderWireframe()
{
return meshPipeline->getRenderWireframe(); return meshPipeline->getRenderWireframe();
} }
void setRenderLightHeatmap(bool heatmap)
{
renderLightHeatmap = heatmap;
}
bool getRenderLightHeatmap() const
{
return renderLightHeatmap;
}
void setShadowDistance(float distance)
{
shadowMapPipeline->setShadowDistance(distance);
}
float getShadowDistance() const
{
return shadowMapPipeline->getShadowDistance();
}
RenderResources* getResources() RenderResources* getResources()
{ {
return resources; return resources;
} }
void drawQuads(
VkCommandBuffer cmd,
GfxDevice& gfxDevice,
const glm::mat4& projection);
private: private:
void createDrawImage(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize, bool firstCreate); void createDrawImage(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize, bool firstCreate);
@@ -88,31 +129,48 @@ private:
VkSampleCountFlagBits samples{VK_SAMPLE_COUNT_1_BIT}; VkSampleCountFlagBits samples{VK_SAMPLE_COUNT_1_BIT};
struct GPUSceneData { struct GPUSceneData
// camera {
glm::mat4 view; glm::mat4 view;
glm::mat4 proj; glm::mat4 proj;
glm::mat4 invProj;
glm::mat4 viewProj; glm::mat4 viewProj;
glm::vec4 cameraPos; glm::vec4 cameraPos;
// ambient
glm::vec3 ambientColor; glm::vec3 ambientColor;
float ambientIntensity; float ambientIntensity;
// fog
glm::vec3 fogColor; glm::vec3 fogColor;
float fogDensity; float fogDensity;
VkDeviceAddress materialsBuffer; VkDeviceAddress materialsBuffer;
VkDeviceAddress lightsBuffer;
VkDeviceAddress tileLightIndicesBuffer;
std::uint32_t numLights;
std::uint32_t tileCountX;
std::uint32_t debugLightHeatmap;
std::uint32_t shadowMapId;
float shadowNormalOffset;
std::uint32_t padding;
glm::mat4 lightViewProj;
}; };
NBuffer sceneDataBuffer; NBuffer sceneDataBuffer;
std::unique_ptr<MeshPipeline> meshPipeline; std::unique_ptr<MeshPipeline> meshPipeline;
std::unique_ptr<SkyboxPipeline> skyboxPipeline; std::unique_ptr<SkyboxPipeline> skyboxPipeline;
std::unique_ptr<LineRenderingPass> lineRenderingPass;
std::unique_ptr<QuadRendererPass> quadRendererPass;
std::unique_ptr<SkinningPipeline> skinningPipeline; std::unique_ptr<SkinningPipeline> skinningPipeline;
std::unique_ptr<LightCullingPipeline> lightCullingPipeline;
std::unique_ptr<ShadowMapPipeline> shadowMapPipeline;
std::optional<glm::vec3> shadowLightDirection;
bool renderLightHeatmap{false};
bool initialized{false}; bool initialized{false};
}; };
@@ -0,0 +1,43 @@
#ifndef CPUMESH_H
#define CPUMESH_H
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
#include <glm/vec3.hpp>
#include <glm/vec4.hpp>
#include <destrum/Graphics/Skeleton.h>
#include <destrum/Assets/AssetReference.h>
// ─── CPU Mesh ─────────────────────────────────────────────────────────────────
struct CPUMesh {
struct Vertex {
glm::vec3 position;
float uv_x{};
glm::vec3 normal;
float uv_y{};
glm::vec4 tangent;
};
struct SkinningData {
glm::vec<4, std::uint32_t> jointIds;
glm::vec4 weights;
};
std::vector<std::uint32_t> indices;
std::vector<Vertex> vertices;
std::vector<SkinningData> skinningData; // empty if no skeleton
std::string name;
AssetReference assetReference;
glm::vec3 minPos;
glm::vec3 maxPos;
std::optional<Skeleton> skeleton; // present only for skinned meshes
};
#endif // CPUMESH_H
@@ -13,37 +13,8 @@
#include <glm/mat4x4.hpp> #include <glm/mat4x4.hpp>
#include <destrum/Graphics/Resources/Buffer.h> #include <destrum/Graphics/Resources/Buffer.h>
#include <destrum/Graphics/Resources/CPUMesh.h>
#include <destrum/Graphics/Frustum.h> #include <destrum/Graphics/Frustum.h>
#include <destrum/Graphics/Skeleton.h>
#include <destrum/Assets/AssetReference.h>
// ─── CPU Mesh ─────────────────────────────────────────────────────────────────
struct CPUMesh {
struct Vertex {
glm::vec3 position;
float uv_x{};
glm::vec3 normal;
float uv_y{};
glm::vec4 tangent;
};
struct SkinningData {
glm::vec<4, std::uint32_t> jointIds;
glm::vec4 weights;
};
std::vector<std::uint32_t> indices;
std::vector<Vertex> vertices;
std::vector<SkinningData> skinningData; // empty if no skeleton
std::string name;
AssetReference assetReference;
glm::vec3 minPos;
glm::vec3 maxPos;
std::optional<Skeleton> skeleton; // present only for skinned meshes
};
// ─── GPU Mesh ───────────────────────────────────────────────────────────────── // ─── GPU Mesh ─────────────────────────────────────────────────────────────────
@@ -0,0 +1,35 @@
#ifndef RESOURCESINSPECTOR_H
#define RESOURCESINSPECTOR_H
#include <destrum/Graphics/ids.h>
struct GPUImage;
class ImguiPass;
class RenderResources;
// ImGui windows for looking into the render resource caches
class ResourcesInspector final
{
public:
void drawImageCacheWindow(bool* open, const RenderResources& resources, ImguiPass& imguiPass);
private:
struct PreviewInfo
{
bool previewable{false};
const char* reason{""};
};
[[nodiscard]] static PreviewInfo getPreviewInfo(const GPUImage& image);
void drawImagePreview(const GPUImage& image, float maxSize, ImguiPass& imguiPass) const;
void drawImageDetails(ImageID id, const RenderResources& resources) const;
[[nodiscard]] bool matchesFilter(ImageID id, const RenderResources& resources) const;
char filter[128]{};
float thumbnailSize{48.0f};
bool onlyPreviewable{false};
ImageID selectedImageId{NULL_IMAGE_ID};
};
#endif //RESOURCESINSPECTOR_H
@@ -77,6 +77,8 @@ public:
[[nodiscard]] VkFormat getFormat() const { return m_swapchain.image_format; } [[nodiscard]] VkFormat getFormat() const { return m_swapchain.image_format; }
private: private:
void nameSwapchainObjects(VkDevice device) const;
struct FrameData { struct FrameData {
VkSemaphore swapchainSemaphore{VK_NULL_HANDLE}; VkSemaphore swapchainSemaphore{VK_NULL_HANDLE};
VkFence renderFence{VK_NULL_HANDLE}; VkFence renderFence{VK_NULL_HANDLE};
+61
View File
@@ -1,6 +1,8 @@
#ifndef UTIL_H #ifndef UTIL_H
#define UTIL_H #define UTIL_H
#include <cstdint>
#include <cstdio>
#include <filesystem> #include <filesystem>
#include <optional> #include <optional>
#include <stdexcept> #include <stdexcept>
@@ -87,6 +89,9 @@ namespace vkutil {
VkDeviceSize offset = 0, VkDeviceSize offset = 0,
VkDeviceSize size = VK_WHOLE_SIZE); VkDeviceSize size = VK_WHOLE_SIZE);
// Object names (VK_EXT_debug_utils), shown in RenderDoc, Nsight and validation messages.
// All of these are no-ops when the extension isn't loaded.
void addDebugLabel(VkDevice device, VkObjectType objectType, std::uint64_t objectHandle, const char* label);
void addDebugLabel(VkDevice device, VkImage image, const char* label); void addDebugLabel(VkDevice device, VkImage image, const char* label);
void addDebugLabel(VkDevice device, VkImageView imageView, const char* label); void addDebugLabel(VkDevice device, VkImageView imageView, const char* label);
void addDebugLabel(VkDevice device, VkShaderModule shaderModule, const char* label); void addDebugLabel(VkDevice device, VkShaderModule shaderModule, const char* label);
@@ -94,6 +99,62 @@ namespace vkutil {
void addDebugLabel(VkDevice device, VkPipelineLayout layout, const char* label); void addDebugLabel(VkDevice device, VkPipelineLayout layout, const char* label);
void addDebugLabel(VkDevice device, VkBuffer buffer, const char* label); void addDebugLabel(VkDevice device, VkBuffer buffer, const char* label);
void addDebugLabel(VkDevice device, VkSampler sampler, const char* label); void addDebugLabel(VkDevice device, VkSampler sampler, const char* label);
void addDebugLabel(VkDevice device, VkDescriptorPool descriptorPool, const char* label);
void addDebugLabel(VkDevice device, VkDescriptorSetLayout descriptorSetLayout, const char* label);
void addDebugLabel(VkDevice device, VkDescriptorSet descriptorSet, const char* label);
void addDebugLabel(VkDevice device, VkCommandPool commandPool, const char* label);
void addDebugLabel(VkDevice device, VkCommandBuffer commandBuffer, const char* label);
void addDebugLabel(VkDevice device, VkFence fence, const char* label);
void addDebugLabel(VkDevice device, VkSemaphore semaphore, const char* label);
void addDebugLabel(VkDevice device, VkQueue queue, const char* label);
// Command buffer regions, they nest like a stack: every begin needs a matching end
// in the same command buffer.
[[nodiscard]] bool debugLabelsEnabled();
void cmdBeginLabel(VkCommandBuffer cmd, const char* label, const glm::vec4& color = glm::vec4{1.0f});
void cmdEndLabel(VkCommandBuffer cmd);
void cmdInsertLabel(VkCommandBuffer cmd, const char* label, const glm::vec4& color = glm::vec4{1.0f});
// printf style, only formats when labels are actually recorded (cheap enough for per draw labels)
template <typename... Args>
void cmdBeginLabelFormatted(VkCommandBuffer cmd, const glm::vec4& color, const char* format, Args... args)
{
if (!debugLabelsEnabled()) {
return;
}
char label[256];
std::snprintf(label, sizeof(label), format, args...);
cmdBeginLabel(cmd, label, color);
}
class ScopedCmdLabel {
public:
ScopedCmdLabel(VkCommandBuffer cmd, const char* label, const glm::vec4& color = glm::vec4{1.0f});
~ScopedCmdLabel();
ScopedCmdLabel(const ScopedCmdLabel& other) = delete;
ScopedCmdLabel(ScopedCmdLabel&& other) noexcept = delete;
ScopedCmdLabel& operator=(const ScopedCmdLabel& other) = delete;
ScopedCmdLabel& operator=(ScopedCmdLabel&& other) noexcept = delete;
private:
VkCommandBuffer cmd{VK_NULL_HANDLE};
};
// Colors used for the renderer's labels, so passes are easy to tell apart in a capture
namespace labelColors {
inline const glm::vec4 frame{0.8f, 0.8f, 0.8f, 1.0f};
inline const glm::vec4 compute{0.9f, 0.5f, 0.1f, 1.0f};
inline const glm::vec4 shadow{0.3f, 0.3f, 0.6f, 1.0f};
inline const glm::vec4 depth{0.4f, 0.4f, 0.4f, 1.0f};
inline const glm::vec4 opaque{0.2f, 0.7f, 0.3f, 1.0f};
inline const glm::vec4 sky{0.3f, 0.6f, 0.9f, 1.0f};
inline const glm::vec4 debug{0.9f, 0.2f, 0.6f, 1.0f};
inline const glm::vec4 ui{0.9f, 0.9f, 0.2f, 1.0f};
inline const glm::vec4 transfer{0.6f, 0.4f, 0.2f, 1.0f};
inline const glm::vec4 draw{0.5f, 0.8f, 0.5f, 1.0f};
}
struct RenderingInfoParams { struct RenderingInfoParams {
VkExtent2D renderExtent; VkExtent2D renderExtent;
@@ -3,6 +3,7 @@
#include <filesystem> #include <filesystem>
#include <cstddef> #include <cstddef>
#include <vector>
#include <vulkan/vulkan.h> #include <vulkan/vulkan.h>
#include "destrum/Graphics/TextureIntent.h" #include "destrum/Graphics/TextureIntent.h"
@@ -19,11 +20,12 @@ struct ImageData {
if (this == &o) return *this; if (this == &o) return *this;
// free current contents first // free current contents first
this->~ImageData(); freePixels();
// steal // steal
pixels = o.pixels; pixels = o.pixels;
hdrPixels = o.hdrPixels; hdrPixels = o.hdrPixels;
cookedPixels = std::move(o.cookedPixels);
hdr = o.hdr; hdr = o.hdr;
width = o.width; width = o.width;
height = o.height; height = o.height;
@@ -50,8 +52,11 @@ struct ImageData {
ImageData(const ImageData&) = delete; ImageData(const ImageData&) = delete;
ImageData& operator=(const ImageData&) = delete; ImageData& operator=(const ImageData&) = delete;
void freePixels();
unsigned char* pixels{nullptr}; unsigned char* pixels{nullptr};
float* hdrPixels{nullptr}; float* hdrPixels{nullptr};
std::vector<unsigned char> cookedPixels; // owns `pixels` for cooked textures
bool hdr{false}; bool hdr{false};
int width{0}; int width{0};
+9 -5
View File
@@ -30,8 +30,8 @@ public:
int MouseX() const { return m_mouseX; } int MouseX() const { return m_mouseX; }
int MouseY() const { return m_mouseY; } int MouseY() const { return m_mouseY; }
int MouseDeltaX() const { return m_mouseDX; } int MouseDeltaX() const { return m_mouseRelDX; }
int MouseDeltaY() const { return m_mouseDY; } int MouseDeltaY() const { return m_mouseRelDY; }
int WheelX() const { return m_wheelX; } int WheelX() const { return m_wheelX; }
int WheelY() const { return m_wheelY; } int WheelY() const { return m_wheelY; }
@@ -81,6 +81,9 @@ public:
void SetAxisDeadzone(int deadzone) { m_axisDeadzone = deadzone; } // 0..32767 void SetAxisDeadzone(int deadzone) { m_axisDeadzone = deadzone; } // 0..32767
void SetMouseCaptured(bool captured);
[[nodiscard]] bool IsMouseCaptured() const { return m_MouseCaptured; }
private: private:
// Key states // Key states
std::unordered_set<SDL_Scancode> m_keysDown; std::unordered_set<SDL_Scancode> m_keysDown;
@@ -92,10 +95,9 @@ private:
std::unordered_set<Uint8> m_mousePressed; std::unordered_set<Uint8> m_mousePressed;
std::unordered_set<Uint8> m_mouseReleased; std::unordered_set<Uint8> m_mouseReleased;
// Mouse position + delta // Mouse position + relative delta (accumulated from SDL xrel/yrel)
int m_mouseX = 0, m_mouseY = 0; int m_mouseX = 0, m_mouseY = 0;
int m_prevMouseX = 0, m_prevMouseY = 0; int m_mouseRelDX = 0, m_mouseRelDY = 0;
int m_mouseDX = 0, m_mouseDY = 0;
int m_wheelX = 0, m_wheelY = 0; int m_wheelX = 0, m_wheelY = 0;
@@ -125,6 +127,8 @@ private:
int m_axisDeadzone = 8000; // typical deadzone int m_axisDeadzone = 8000; // typical deadzone
bool m_MouseCaptured = false;
private: private:
bool QueryBinding(const Binding& b, ButtonState state) const; bool QueryBinding(const Binding& b, ButtonState state) const;
@@ -59,6 +59,16 @@ public:
virtual void ResolveReferences(const ObjectMap&) { virtual void ResolveReferences(const ObjectMap&) {
} }
// Trigger callbacks called when this component's owner overlaps a sensor.
// Only called when the owner has a Collider set as a trigger (or overlaps one).
virtual void OnTriggerEnter(GameObject* other) {
(void)other;
}
virtual void OnTriggerExit(GameObject* other) {
(void)other;
}
bool HasStarted{false}; bool HasStarted{false};
protected: protected:
@@ -55,6 +55,8 @@ public:
float maxDistance, float maxDistance,
PhysicsRaycastHit& hit) const override; PhysicsRaycastHit& hit) const override;
std::vector<TriggerEvent> ConsumeTriggerEvents() override;
private: private:
class Impl; class Impl;
std::unique_ptr<Impl> m_Impl; std::unique_ptr<Impl> m_Impl;
@@ -21,6 +21,7 @@ class PhysicsSceneBridge final {
public: public:
explicit PhysicsSceneBridge(std::unique_ptr<PhysicsWorld> world); explicit PhysicsSceneBridge(std::unique_ptr<PhysicsWorld> world);
[[nodiscard]] bool IsValid() const { return m_World != nullptr; }
[[nodiscard]] PhysicsWorld& GetWorld() { return *m_World; } [[nodiscard]] PhysicsWorld& GetWorld() { return *m_World; }
[[nodiscard]] const PhysicsWorld& GetWorld() const { return *m_World; } [[nodiscard]] const PhysicsWorld& GetWorld() const { return *m_World; }
@@ -116,3 +116,9 @@ struct PhysicsBodyDesc {
bool useGravity{true}; bool useGravity{true};
bool allowSleep{true}; bool allowSleep{true};
}; };
struct TriggerEvent {
GameObject* owner{nullptr};
GameObject* other{nullptr};
bool entered{true};
};
@@ -1,5 +1,7 @@
#pragma once #pragma once
#include <vector>
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <unordered_set> #include <unordered_set>
@@ -49,6 +51,9 @@ public:
float maxDistance, float maxDistance,
PhysicsRaycastHit& hit) const = 0; PhysicsRaycastHit& hit) const = 0;
// Trigger events accumulated during the last Step().
virtual std::vector<TriggerEvent> ConsumeTriggerEvents() { return {}; }
private: private:
std::unordered_set<Rigidbody*> m_RegisteredRigidbodies; std::unordered_set<Rigidbody*> m_RegisteredRigidbodies;
}; };
+12 -1
View File
@@ -3,6 +3,8 @@
#include <functional> #include <functional>
#include <memory>
#include <destrum/Event.h> #include <destrum/Event.h>
#include <destrum/ObjectModel/ObjectId.h> #include <destrum/ObjectModel/ObjectId.h>
#include <destrum/Scene/SceneManager.h> #include <destrum/Scene/SceneManager.h>
@@ -10,8 +12,10 @@
#include "destrum/Physics/JoltPhysicsWorld.h" #include "destrum/Physics/JoltPhysicsWorld.h"
#include "destrum/Physics/PhysicsSceneBridge.h" #include "destrum/Physics/PhysicsSceneBridge.h"
class CameraComponent;
class GameObject; class GameObject;
class SceneSerializer; class SceneSerializer;
class PhysicsWorld;
class Scene final { class Scene final {
friend Scene& SceneManager::CreateScene(const std::string& name); friend Scene& SceneManager::CreateScene(const std::string& name);
@@ -30,7 +34,7 @@ public:
void FixedUpdate(float dt); void FixedUpdate(float dt);
void LateUpdate(float dt); void LateUpdate(float dt);
void Render(const RenderContext& ctx); void Render(const RenderContext& ctx);
void RenderImgui(); void RenderImgui(bool* open = nullptr);
void CleanupDestroyedGameObjects(); void CleanupDestroyedGameObjects();
void Unload(); void Unload();
@@ -102,8 +106,13 @@ public:
Event<> OnSceneLoaded; Event<> OnSceneLoaded;
PhysicsSceneBridge& GetPhysics() { return m_Physics; } PhysicsSceneBridge& GetPhysics() { return m_Physics; }
[[nodiscard]] CameraComponent* GetCurrentCamera() const;
[[nodiscard]] std::vector<CameraComponent*> GetCameras() const;
void SetCurrentCamera(const CameraComponent* camera);
private: private:
explicit Scene(const std::string& name); explicit Scene(const std::string& name);
explicit Scene(const std::string& name, std::unique_ptr<PhysicsWorld> physicsWorld);
PhysicsSceneBridge m_Physics{std::make_unique<JoltPhysicsWorld>()}; PhysicsSceneBridge m_Physics{std::make_unique<JoltPhysicsWorld>()};
@@ -123,6 +132,8 @@ private:
bool m_ShowDemoWindow{false}; bool m_ShowDemoWindow{false};
ObjectId m_SelectedObjectId{InvalidObjectId}; ObjectId m_SelectedObjectId{InvalidObjectId};
ObjectId m_CurrentCameraId{InvalidObjectId};
std::function<void()> m_registerBindings; std::function<void()> m_registerBindings;
std::function<void()> m_unregisterBindings; std::function<void()> m_unregisterBindings;
bool m_bindingsUnloaded{false}; bool m_bindingsUnloaded{false};
+6 -4
View File
@@ -16,13 +16,14 @@ public:
Scene& CreateScene(const std::string& name); Scene& CreateScene(const std::string& name);
Scene& GetCurrentScene() const; Scene& GetCurrentScene() const;
const std::vector<std::shared_ptr<Scene>>& GetActiveScenes() const { return m_activeScenes; }
void Update(float dt); void Update(float dt);
void FixedUpdate(float dt); void FixedUpdate(float dt);
void LateUpdate(float dt); void LateUpdate(float dt);
void Render(const RenderContext& ctx); void Render(const RenderContext& ctx);
void RenderImgui(); void RenderImgui(bool* open = nullptr);
void HandleGameObjectDestroy(); void HandleGameObjectDestroy();
void DestroyGameObjects(); void DestroyGameObjects();
@@ -34,7 +35,9 @@ public:
void Destroy(); void Destroy();
void SwitchScene(int index); void SwitchScene(int index);
int GetActiveSceneId() const { return m_scenes.empty() ? -1 : m_ActiveSceneIndex; } void AddActiveScene(int index);
void RemoveActiveScene(int index);
int GetActiveSceneId() const;
[[nodiscard]] const std::vector<std::shared_ptr<Scene>>& GetScenes() const { return m_scenes; } [[nodiscard]] const std::vector<std::shared_ptr<Scene>>& GetScenes() const { return m_scenes; }
@@ -47,8 +50,7 @@ private:
SceneManager() = default; SceneManager() = default;
int m_ActiveSceneIndex{0}; std::vector<std::shared_ptr<Scene>> m_activeScenes;
std::vector<std::shared_ptr<Scene>> m_scenes; std::vector<std::shared_ptr<Scene>> m_scenes;
}; };
@@ -1,9 +1,9 @@
#ifndef DESTRUM_ENGINECOMPONENTLIST_H #ifndef DESTRUM_ENGINECOMPONENTLIST_H
#define DESTRUM_ENGINECOMPONENTLIST_H #define DESTRUM_ENGINECOMPONENTLIST_H
// Keep the component manifest in one place. Consumers define X before
// expanding one of these lists to generate registration or other metadata.
#include <destrum/Components/Animator.h> #include <destrum/Components/Animator.h>
#include <destrum/Components/CameraComponent.h>
#include <destrum/Components/LightComponent.h>
#include <destrum/Components/MeshRendererComponent.h> #include <destrum/Components/MeshRendererComponent.h>
#include <destrum/Components/OrbitAndSpin.h> #include <destrum/Components/OrbitAndSpin.h>
#include <destrum/Components/Rotator.h> #include <destrum/Components/Rotator.h>
@@ -15,6 +15,8 @@
#define DESTRUM_ENGINE_COMPONENTS(X) \ #define DESTRUM_ENGINE_COMPONENTS(X) \
X(MeshRendererComponent) \ X(MeshRendererComponent) \
X(LightComponent) \
X(CameraComponent) \
X(Rotator) \ X(Rotator) \
X(Spinner) \ X(Spinner) \
X(OrbitAndSpin) \ X(OrbitAndSpin) \
@@ -22,6 +24,6 @@
X(Rigidbody) \ X(Rigidbody) \
X(BoxCollider) \ X(BoxCollider) \
X(SphereCollider) \ X(SphereCollider) \
X(CapsuleCollider) X(CapsuleCollider) \
#endif // DESTRUM_ENGINECOMPONENTLIST_H #endif // DESTRUM_ENGINECOMPONENTLIST_H
@@ -2,13 +2,34 @@
#define DESTRUM_SCENESERIALIZER_H #define DESTRUM_SCENESERIALIZER_H
#include <filesystem> #include <filesystem>
#include <functional>
#include <future>
#include <nlohmann/json.hpp>
class Scene; class Scene;
class SceneSerializer final { class SceneSerializer final {
public: public:
struct LoadResult {
bool success{false};
std::string errorMessage;
nlohmann::json root;
};
static bool Save(Scene& scene, const std::filesystem::path& path); static bool Save(Scene& scene, const std::filesystem::path& path);
static bool Load(Scene& scene, const std::filesystem::path& path);
static bool Load(Scene& scene, const std::filesystem::path& path,
std::function<void(float)> progressCallback = nullptr);
static LoadResult LoadSceneFile(const std::filesystem::path& path);
static bool ConstructScene(Scene& scene, LoadResult& result,
std::function<void(float)> progressCallback = nullptr);
// Parse the scene file on a background thread. Call LoadFromJson on the
// main thread once the future is ready.
static std::future<LoadResult> LoadSceneFileAsync(const std::filesystem::path& path);
}; };
#endif //DESTRUM_SCENESERIALIZER_H #endif //DESTRUM_SCENESERIALIZER_H
+4 -1
View File
@@ -1,6 +1,7 @@
#ifndef DESTRUM_IMGUIUTILS_H #ifndef DESTRUM_IMGUIUTILS_H
#define DESTRUM_IMGUIUTILS_H #define DESTRUM_IMGUIUTILS_H
#include <functional>
#include <memory> #include <memory>
#include <string_view> #include <string_view>
#include <vector> #include <vector>
@@ -21,7 +22,9 @@ namespace ImGuiUtils {
std::string_view sceneName, std::string_view sceneName,
const std::vector<std::shared_ptr<GameObject>>& objects, const std::vector<std::shared_ptr<GameObject>>& objects,
const std::vector<std::shared_ptr<GameObject>>& pendingAdditions, const std::vector<std::shared_ptr<GameObject>>& pendingAdditions,
ObjectId& selectedObjectId); ObjectId& selectedObjectId,
const std::function<GameObject*()>& createGameObject,
bool* open = nullptr);
} }
#endif // DESTRUM_IMGUIUTILS_H #endif // DESTRUM_IMGUIUTILS_H
+3 -1
View File
@@ -30,7 +30,9 @@
#include <glm/gtc/quaternion.hpp> #include <glm/gtc/quaternion.hpp>
#include <glm/gtc/type_ptr.hpp> #include <glm/gtc/type_ptr.hpp>
#include <destrum/Graphics/Resources/Mesh.h> #include <spdlog/spdlog.h>
#include <destrum/Graphics/Resources/CPUMesh.h>
#include <destrum/Assets/AssetReference.h> #include <destrum/Assets/AssetReference.h>
#include <destrum/Graphics/SkeletalAnimation.h> #include <destrum/Graphics/SkeletalAnimation.h>
#include <destrum/Graphics/Skeleton.h> #include <destrum/Graphics/Skeleton.h>
+59 -14
View File
@@ -131,6 +131,12 @@ void App::run()
imguiPass.handleEvent(event); imguiPass.handleEvent(event);
const bool mouseEvent =
event.type == SDL_MOUSEBUTTONDOWN ||
event.type == SDL_MOUSEBUTTONUP ||
event.type == SDL_MOUSEMOTION ||
event.type == SDL_MOUSEWHEEL;
if (event.type == SDL_QUIT) if (event.type == SDL_QUIT)
{ {
isRunning = false; isRunning = false;
@@ -149,19 +155,13 @@ void App::run()
} }
} }
const bool mouseEvent =
event.type == SDL_MOUSEBUTTONDOWN ||
event.type == SDL_MOUSEBUTTONUP ||
event.type == SDL_MOUSEMOTION ||
event.type == SDL_MOUSEWHEEL;
const bool keyboardEvent = const bool keyboardEvent =
event.type == SDL_KEYDOWN || event.type == SDL_KEYDOWN ||
event.type == SDL_KEYUP || event.type == SDL_KEYUP ||
event.type == SDL_TEXTINPUT; event.type == SDL_TEXTINPUT;
const bool capturedByImgui = const bool capturedByImgui =
(mouseEvent && imguiPass.wantsMouse()) || (mouseEvent && !InputManager::GetInstance().IsMouseCaptured() && imguiPass.wantsMouse()) ||
(keyboardEvent && imguiPass.wantsKeyboard()); (keyboardEvent && imguiPass.wantsKeyboard());
if (!capturedByImgui) if (!capturedByImgui)
@@ -176,7 +176,7 @@ void App::run()
} }
} }
if (!isRunning) break; if (!isRunning) break;
// Consume SDL events before updating the base camera so held and // Consume SDL events before updating the base camera so held and
// newly pressed inputs are applied in the same frame. // newly pressed inputs are applied in the same frame.
@@ -195,13 +195,31 @@ void App::run()
if (m_params.showDebugUi) if (m_params.showDebugUi)
{ {
ZoneScopedN("Debug ImGui"); ZoneScopedN("Debug ImGui");
SceneManager::GetInstance().RenderImgui(); drawWindowsMenu();
ImGui::Begin("Debug"); if (m_DebugWindows.inspector)
ImGui::Text("FPS: %.2f", avgFPS); {
ImGui::End(); SceneManager::GetInstance().RenderImgui(&m_DebugWindows.inspector);
}
drawPhysicsPanel(dt, fixedDt, accumulator); if (m_DebugWindows.stats)
{
if (ImGui::Begin("Debug", &m_DebugWindows.stats))
{
ImGui::Text("FPS: %.2f", avgFPS);
}
ImGui::End();
}
if (m_DebugWindows.physics)
{
drawPhysicsPanel(dt, fixedDt, accumulator);
}
if (m_DebugWindows.imageCache)
{
m_ResourcesInspector.drawImageCacheWindow(&m_DebugWindows.imageCache, resources, imguiPass);
}
TracyPlot("FPS", avgFPS); TracyPlot("FPS", avgFPS);
TracyPlot("Frame Time ms", dt * 1000.0f); TracyPlot("Frame Time ms", dt * 1000.0f);
@@ -359,9 +377,36 @@ void App::cleanup()
} }
} }
void App::drawWindowsMenu()
{
// ImGui appends to the main menu bar, so this ends up next to whatever menus the game added
if (!ImGui::BeginMainMenuBar())
{
return;
}
if (ImGui::BeginMenu("Windows"))
{
ImGui::MenuItem("Debug", nullptr, &m_DebugWindows.stats);
ImGui::MenuItem("Physics", nullptr, &m_DebugWindows.physics);
ImGui::MenuItem("Inspector", nullptr, &m_DebugWindows.inspector);
ImGui::MenuItem("Image Cache", nullptr, &m_DebugWindows.imageCache);
customWindowsMenu();
ImGui::EndMenu();
}
ImGui::EndMainMenuBar();
}
void App::drawPhysicsPanel(float dt, float fixedDt, float accumulator) void App::drawPhysicsPanel(float dt, float fixedDt, float accumulator)
{ {
ImGui::Begin("Physics"); if (!ImGui::Begin("Physics", &m_DebugWindows.physics))
{
ImGui::End();
return;
}
ImGui::Text("Frame dt: %.3f ms", dt * 1000.0f); ImGui::Text("Frame dt: %.3f ms", dt * 1000.0f);
ImGui::Text("Fixed dt: %.3f ms", fixedDt * 1000.0f); ImGui::Text("Fixed dt: %.3f ms", fixedDt * 1000.0f);
+52 -3
View File
@@ -1,7 +1,9 @@
#include <destrum/Assets/AssetManager.h> #include <destrum/Assets/AssetManager.h>
#include <optional>
#include <stdexcept> #include <stdexcept>
#include <destrum/Assets/ModelFile.h>
#include <destrum/FS/AssetFS.h> #include <destrum/FS/AssetFS.h>
namespace { namespace {
@@ -11,6 +13,34 @@ namespace {
} }
return std::string{reference}; return std::string{reference};
} }
std::optional<std::filesystem::path> FindCookedModel(const std::string& reference) {
if (reference.find("://") == std::string::npos) {
return std::nullopt;
}
try {
const std::filesystem::path cookedPath = AssetFS::GetInstance().GetCookedPathForFile(reference);
if (cookedPath.extension() == ModelFile::Extension) {
return cookedPath;
}
} catch (const std::exception& exception) {
spdlog::warn("Importing {} instead of using its cooked model: {}", reference, exception.what());
}
return std::nullopt;
}
// Cooked texture paths are relative to the model file, imported ones are absolute
void ResolveTexturePaths(ModelDoc::ModelAsset& model, const std::filesystem::path& modelDir) {
for (ModelDoc::MaterialInfo& material : model.materials) {
for (ModelDoc::TextureInfo& texture : material.textures) {
if (texture.embedded || texture.path.empty() || std::filesystem::path{texture.path}.is_absolute()) {
continue;
}
texture.path = (modelDir / texture.path).lexically_normal().generic_string();
}
}
}
} }
std::filesystem::path AssetManager::Resolve(std::string_view reference) const { std::filesystem::path AssetManager::Resolve(std::string_view reference) const {
@@ -32,9 +62,7 @@ ModelDoc::ModelAsset AssetManager::LoadModel(
const ModelDoc::LoadOptions& options) const const ModelDoc::LoadOptions& options) const
{ {
const std::string sourceReference = ToReference(reference); const std::string sourceReference = ToReference(reference);
ModelDoc::ModelAsset model = ModelDoc::LoadModel( ModelDoc::ModelAsset model = LoadCookedOrImport(sourceReference, options);
Resolve(sourceReference).generic_string(),
options);
model.sourcePath = sourceReference; model.sourcePath = sourceReference;
model.skeleton.assetReference.path = sourceReference; model.skeleton.assetReference.path = sourceReference;
@@ -44,6 +72,9 @@ ModelDoc::ModelAsset AssetManager::LoadModel(
primitive.mesh.assetReference.subresource = primitive.sourceMeshIndex != ModelDoc::InvalidIndex primitive.mesh.assetReference.subresource = primitive.sourceMeshIndex != ModelDoc::InvalidIndex
? "mesh:" + std::to_string(primitive.sourceMeshIndex) ? "mesh:" + std::to_string(primitive.sourceMeshIndex)
: "primitive:" + std::to_string(index); : "primitive:" + std::to_string(index);
if (primitive.mesh.skeleton) {
primitive.mesh.skeleton->assetReference.path = sourceReference;
}
} }
for (std::size_t index = 0; index < model.materials.size(); ++index) { for (std::size_t index = 0; index < model.materials.size(); ++index) {
auto& material = model.materials[index]; auto& material = model.materials[index];
@@ -59,6 +90,24 @@ ModelDoc::ModelAsset AssetManager::LoadModel(
return model; return model;
} }
ModelDoc::ModelAsset AssetManager::LoadCookedOrImport(
const std::string& reference,
const ModelDoc::LoadOptions& options) const
{
if (const auto cookedPath = FindCookedModel(reference)) {
ModelDoc::ModelAsset model = ModelFile::ReadFromFile(*cookedPath);
if (ModelFile::SupportsLoadOptions(model, options)) {
ModelFile::ApplyLoadOptions(model, options);
ResolveTexturePaths(model, cookedPath->parent_path());
spdlog::debug("Loaded cooked model {}", reference);
return model;
}
spdlog::debug("Importing {}, its cooked model doesn't match the requested load options", reference);
}
return ModelDoc::LoadModel(Resolve(reference).generic_string(), options);
}
std::vector<SkeletalAnimation> AssetManager::LoadAnimationClips( std::vector<SkeletalAnimation> AssetManager::LoadAnimationClips(
std::string_view reference, std::string_view reference,
const Skeleton& targetSkeleton) const const Skeleton& targetSkeleton) const
+206
View File
@@ -0,0 +1,206 @@
#ifndef DESTRUM_BINARYSTREAM_H
#define DESTRUM_BINARYSTREAM_H
#include <array>
#include <bit>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <optional>
#include <span>
#include <stdexcept>
#include <string>
#include <string_view>
#include <type_traits>
#include <vector>
namespace AssetBinary {
static_assert(std::endian::native == std::endian::little, "Cooked assets are stored little endian");
using Magic = std::array<char, 4>;
template <typename T>
concept RawCopyable = std::is_trivially_copyable_v<T>;
class BinaryWriter {
public:
template <RawCopyable T>
void Write(const T& value) {
const auto* bytes = reinterpret_cast<const std::uint8_t*>(&value);
m_Bytes.insert(m_Bytes.end(), bytes, bytes + sizeof(T));
}
template <RawCopyable T>
void WriteArray(const std::vector<T>& values) {
WriteCount(values.size());
const auto* bytes = reinterpret_cast<const std::uint8_t*>(values.data());
m_Bytes.insert(m_Bytes.end(), bytes, bytes + values.size() * sizeof(T));
}
void WriteCount(std::size_t count) {
Write(static_cast<std::uint64_t>(count));
}
void WriteBool(bool value) {
Write(static_cast<std::uint8_t>(value ? 1 : 0));
}
void WriteString(std::string_view value) {
WriteCount(value.size());
m_Bytes.insert(m_Bytes.end(), value.begin(), value.end());
}
void WriteStrings(const std::vector<std::string>& values) {
WriteCount(values.size());
for (const std::string& value : values) {
WriteString(value);
}
}
void WriteHeader(const Magic& magic, std::uint32_t version) {
Write(magic);
Write(version);
}
[[nodiscard]] std::vector<std::uint8_t> TakeBytes() {
return std::move(m_Bytes);
}
private:
std::vector<std::uint8_t> m_Bytes;
};
class BinaryReader {
public:
explicit BinaryReader(std::span<const std::uint8_t> bytes)
: m_Bytes(bytes) {
}
template <RawCopyable T>
[[nodiscard]] T Read() {
T value{};
std::memcpy(&value, Take(sizeof(T)).data(), sizeof(T));
return value;
}
template <RawCopyable T>
[[nodiscard]] std::vector<T> ReadArray() {
const std::size_t count = ReadCount(sizeof(T));
std::vector<T> values(count);
if (count > 0) {
std::memcpy(values.data(), Take(count * sizeof(T)).data(), count * sizeof(T));
}
return values;
}
[[nodiscard]] std::size_t ReadCount(std::size_t minElementSize) {
const auto count = Read<std::uint64_t>();
if (count > Remaining() / minElementSize) {
throw std::runtime_error("Cooked asset file is corrupt: element count is out of range");
}
return static_cast<std::size_t>(count);
}
[[nodiscard]] bool ReadBool() {
return Read<std::uint8_t>() != 0;
}
[[nodiscard]] std::string ReadString() {
const std::size_t length = ReadCount(1);
const auto bytes = Take(length);
return std::string(bytes.begin(), bytes.end());
}
[[nodiscard]] std::vector<std::string> ReadStrings() {
std::vector<std::string> values(ReadCount(sizeof(std::uint64_t)));
for (std::string& value : values) {
value = ReadString();
}
return values;
}
void ReadHeader(const Magic& magic, std::uint32_t version, std::string_view kind) {
if (Read<Magic>() != magic) {
throw std::runtime_error("Not a cooked " + std::string{kind} + " file");
}
const auto fileVersion = Read<std::uint32_t>();
if (fileVersion != version) {
throw std::runtime_error(
"Cooked " + std::string{kind} + " file has version " + std::to_string(fileVersion) +
", expected " + std::to_string(version) + " (recook the asset)");
}
}
void ExpectEnd(std::string_view kind) const {
if (m_Offset != m_Bytes.size()) {
throw std::runtime_error("Cooked " + std::string{kind} + " file has unexpected data at the end");
}
}
private:
[[nodiscard]] std::size_t Remaining() const {
return m_Bytes.size() - m_Offset;
}
[[nodiscard]] std::span<const std::uint8_t> Take(std::size_t size) {
if (size > Remaining()) {
throw std::runtime_error("Cooked asset file is truncated");
}
const auto bytes = m_Bytes.subspan(m_Offset, size);
m_Offset += size;
return bytes;
}
std::span<const std::uint8_t> m_Bytes;
std::size_t m_Offset{0};
};
inline void WriteAllBytes(const std::filesystem::path& path, const std::vector<std::uint8_t>& bytes) {
std::ofstream file(path, std::ios::binary | std::ios::trunc);
if (!file) {
throw std::runtime_error("Failed to open file for writing: " + path.string());
}
file.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
if (!file) {
throw std::runtime_error("Failed to write file: " + path.string());
}
}
[[nodiscard]] inline std::vector<std::uint8_t> ReadAllBytes(const std::filesystem::path& path) {
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file) {
throw std::runtime_error("Failed to open file: " + path.string());
}
std::vector<std::uint8_t> bytes(static_cast<std::size_t>(file.tellg()));
file.seekg(0);
file.read(reinterpret_cast<char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
if (!file) {
throw std::runtime_error("Failed to read file: " + path.string());
}
return bytes;
}
[[nodiscard]] inline std::optional<std::uint32_t> ReadVersion(const std::filesystem::path& path, const Magic& magic) {
std::ifstream file(path, std::ios::binary);
if (!file) {
return std::nullopt;
}
std::array<std::uint8_t, sizeof(Magic) + sizeof(std::uint32_t)> header{};
file.read(reinterpret_cast<char*>(header.data()), static_cast<std::streamsize>(header.size()));
if (!file) {
return std::nullopt;
}
BinaryReader reader{header};
if (reader.Read<Magic>() != magic) {
return std::nullopt;
}
return reader.Read<std::uint32_t>();
}
}
#endif // DESTRUM_BINARYSTREAM_H
+322
View File
@@ -0,0 +1,322 @@
#include <destrum/Assets/ModelFile.h>
#include "BinaryStream.h"
namespace {
using AssetBinary::BinaryReader;
using AssetBinary::BinaryWriter;
static_assert(sizeof(CPUMesh::Vertex) == 48);
static_assert(sizeof(CPUMesh::SkinningData) == 32);
static_assert(sizeof(SkeletalAnimation::Keyframe) == 44);
static_assert(sizeof(glm::mat4) == 64);
constexpr AssetBinary::Magic Magic{'D', 'M', 'D', 'L'};
constexpr std::string_view Kind = "model";
void WriteAssetReference(BinaryWriter& writer, const AssetReference& reference) {
writer.WriteString(reference.path);
writer.WriteString(reference.subresource);
}
AssetReference ReadAssetReference(BinaryReader& reader) {
AssetReference reference;
reference.path = reader.ReadString();
reference.subresource = reader.ReadString();
return reference;
}
void WriteSkeleton(BinaryWriter& writer, const Skeleton& skeleton) {
WriteAssetReference(writer, skeleton.assetReference);
writer.WriteCount(skeleton.hierarchy.size());
for (const Skeleton::JointNode& node : skeleton.hierarchy) {
writer.Write(node.id);
writer.WriteArray(node.children);
}
writer.WriteArray(skeleton.inverseBindMatrices);
writer.WriteCount(skeleton.joints.size());
for (const Joint& joint : skeleton.joints) {
writer.Write(joint.id);
writer.Write(joint.localTranslation);
writer.Write(joint.localRotation);
writer.Write(joint.localScale);
}
writer.WriteStrings(skeleton.jointNames);
writer.WriteArray(skeleton.parentIndex);
writer.Write(skeleton.rootPreTransform);
}
Skeleton ReadSkeleton(BinaryReader& reader) {
Skeleton skeleton;
skeleton.assetReference = ReadAssetReference(reader);
skeleton.hierarchy.resize(reader.ReadCount(sizeof(JointId)));
for (Skeleton::JointNode& node : skeleton.hierarchy) {
node.id = reader.Read<JointId>();
node.children = reader.ReadArray<JointId>();
}
skeleton.inverseBindMatrices = reader.ReadArray<glm::mat4>();
skeleton.joints.resize(reader.ReadCount(sizeof(JointId)));
for (Joint& joint : skeleton.joints) {
joint.id = reader.Read<JointId>();
joint.localTranslation = reader.Read<glm::vec3>();
joint.localRotation = reader.Read<glm::quat>();
joint.localScale = reader.Read<glm::vec3>();
}
skeleton.jointNames = reader.ReadStrings();
skeleton.parentIndex = reader.ReadArray<int>();
skeleton.rootPreTransform = reader.Read<glm::mat4>();
return skeleton;
}
void WriteMesh(BinaryWriter& writer, const CPUMesh& mesh) {
writer.WriteArray(mesh.indices);
writer.WriteArray(mesh.vertices);
writer.WriteArray(mesh.skinningData);
writer.WriteString(mesh.name);
WriteAssetReference(writer, mesh.assetReference);
writer.Write(mesh.minPos);
writer.Write(mesh.maxPos);
writer.WriteBool(mesh.skeleton.has_value());
if (mesh.skeleton) {
WriteSkeleton(writer, *mesh.skeleton);
}
}
CPUMesh ReadMesh(BinaryReader& reader) {
CPUMesh mesh;
mesh.indices = reader.ReadArray<std::uint32_t>();
mesh.vertices = reader.ReadArray<CPUMesh::Vertex>();
mesh.skinningData = reader.ReadArray<CPUMesh::SkinningData>();
mesh.name = reader.ReadString();
mesh.assetReference = ReadAssetReference(reader);
mesh.minPos = reader.Read<glm::vec3>();
mesh.maxPos = reader.Read<glm::vec3>();
if (reader.ReadBool()) {
mesh.skeleton = ReadSkeleton(reader);
}
return mesh;
}
void WritePrimitive(BinaryWriter& writer, const ModelDoc::MeshPrimitive& primitive) {
WriteMesh(writer, primitive.mesh);
writer.Write(primitive.materialIndex);
writer.WriteString(primitive.nodeName);
writer.Write(primitive.sourceMeshIndex);
writer.WriteBool(primitive.skinned);
writer.Write(primitive.nodeWorldTransform);
}
ModelDoc::MeshPrimitive ReadPrimitive(BinaryReader& reader) {
ModelDoc::MeshPrimitive primitive;
primitive.mesh = ReadMesh(reader);
primitive.materialIndex = reader.Read<std::uint32_t>();
primitive.nodeName = reader.ReadString();
primitive.sourceMeshIndex = reader.Read<std::uint32_t>();
primitive.skinned = reader.ReadBool();
primitive.nodeWorldTransform = reader.Read<glm::mat4>();
return primitive;
}
void WriteTexture(BinaryWriter& writer, const ModelDoc::TextureInfo& texture) {
writer.Write(static_cast<std::uint32_t>(texture.semantic));
writer.WriteString(texture.rawPath);
writer.WriteString(texture.path);
writer.Write(static_cast<std::uint32_t>(texture.assimpType));
writer.Write(texture.slot);
writer.WriteBool(texture.embedded);
}
ModelDoc::TextureInfo ReadTexture(BinaryReader& reader) {
ModelDoc::TextureInfo texture;
texture.semantic = static_cast<ModelDoc::TextureSemantic>(reader.Read<std::uint32_t>());
texture.rawPath = reader.ReadString();
texture.path = reader.ReadString();
texture.assimpType = static_cast<aiTextureType>(reader.Read<std::uint32_t>());
texture.slot = reader.Read<std::uint32_t>();
texture.embedded = reader.ReadBool();
return texture;
}
void WriteMaterial(BinaryWriter& writer, const ModelDoc::MaterialInfo& material) {
writer.WriteString(material.name);
WriteAssetReference(writer, material.assetReference);
writer.Write(material.baseColor);
writer.Write(material.emissiveColor);
writer.Write(material.metallicFactor);
writer.Write(material.roughnessFactor);
writer.Write(material.opacity);
writer.WriteCount(material.textures.size());
for (const ModelDoc::TextureInfo& texture : material.textures) {
WriteTexture(writer, texture);
}
}
ModelDoc::MaterialInfo ReadMaterial(BinaryReader& reader) {
ModelDoc::MaterialInfo material;
material.name = reader.ReadString();
material.assetReference = ReadAssetReference(reader);
material.baseColor = reader.Read<glm::vec4>();
material.emissiveColor = reader.Read<glm::vec3>();
material.metallicFactor = reader.Read<float>();
material.roughnessFactor = reader.Read<float>();
material.opacity = reader.Read<float>();
material.textures.resize(reader.ReadCount(1));
for (ModelDoc::TextureInfo& texture : material.textures) {
texture = ReadTexture(reader);
}
return material;
}
void WriteAnimation(BinaryWriter& writer, const SkeletalAnimation& animation) {
WriteAssetReference(writer, animation.assetReference);
writer.WriteCount(animation.tracks.size());
for (const SkeletalAnimation::Track& track : animation.tracks) {
writer.Write(track.jointIndex);
writer.WriteArray(track.keyframes);
}
writer.Write(animation.duration);
writer.WriteBool(animation.looped);
writer.WriteString(animation.name);
writer.Write(animation.startFrame);
writer.WriteCount(animation.events.size());
for (const auto& [frame, events] : animation.events) {
writer.Write(frame);
writer.WriteStrings(events);
}
}
SkeletalAnimation ReadAnimation(BinaryReader& reader) {
SkeletalAnimation animation;
animation.assetReference = ReadAssetReference(reader);
animation.tracks.resize(reader.ReadCount(sizeof(std::uint32_t)));
for (SkeletalAnimation::Track& track : animation.tracks) {
track.jointIndex = reader.Read<std::uint32_t>();
track.keyframes = reader.ReadArray<SkeletalAnimation::Keyframe>();
}
animation.duration = reader.Read<float>();
animation.looped = reader.ReadBool();
animation.name = reader.ReadString();
animation.startFrame = reader.Read<int>();
const std::size_t eventFrameCount = reader.ReadCount(sizeof(int));
for (std::size_t i = 0; i < eventFrameCount; ++i) {
const int frame = reader.Read<int>();
animation.events[frame] = reader.ReadStrings();
}
return animation;
}
}
std::vector<std::uint8_t> ModelFile::Write(const ModelDoc::ModelAsset& model) {
BinaryWriter writer;
writer.WriteHeader(Magic, Version);
writer.WriteString(model.sourcePath);
writer.WriteCount(model.primitives.size());
for (const ModelDoc::MeshPrimitive& primitive : model.primitives) {
WritePrimitive(writer, primitive);
}
writer.WriteCount(model.materials.size());
for (const ModelDoc::MaterialInfo& material : model.materials) {
WriteMaterial(writer, material);
}
WriteSkeleton(writer, model.skeleton);
writer.WriteCount(model.animations.size());
for (const SkeletalAnimation& animation : model.animations) {
WriteAnimation(writer, animation);
}
return writer.TakeBytes();
}
ModelDoc::ModelAsset ModelFile::Read(std::span<const std::uint8_t> bytes) {
BinaryReader reader{bytes};
reader.ReadHeader(Magic, Version, Kind);
ModelDoc::ModelAsset model;
model.sourcePath = reader.ReadString();
model.primitives.resize(reader.ReadCount(1));
for (ModelDoc::MeshPrimitive& primitive : model.primitives) {
primitive = ReadPrimitive(reader);
}
model.materials.resize(reader.ReadCount(1));
for (ModelDoc::MaterialInfo& material : model.materials) {
material = ReadMaterial(reader);
}
model.skeleton = ReadSkeleton(reader);
model.animations.resize(reader.ReadCount(1));
for (SkeletalAnimation& animation : model.animations) {
animation = ReadAnimation(reader);
}
reader.ExpectEnd(Kind);
return model;
}
void ModelFile::WriteToFile(const ModelDoc::ModelAsset& model, const std::filesystem::path& path) {
AssetBinary::WriteAllBytes(path, Write(model));
}
ModelDoc::ModelAsset ModelFile::ReadFromFile(const std::filesystem::path& path) {
return Read(AssetBinary::ReadAllBytes(path));
}
std::optional<std::uint32_t> ModelFile::ReadVersionFromFile(const std::filesystem::path& path) {
return AssetBinary::ReadVersion(path, Magic);
}
bool ModelFile::SupportsLoadOptions(const ModelDoc::ModelAsset& model, const ModelDoc::LoadOptions& options) {
const bool sameImport =
options.meshImportMode == CookOptions.meshImportMode &&
options.flipUVs == CookOptions.flipUVs &&
options.generateTangents == CookOptions.generateTangents &&
options.generateSmoothNormals == CookOptions.generateSmoothNormals &&
options.joinIdenticalVertices == CookOptions.joinIdenticalVertices &&
options.limitBoneWeights == CookOptions.limitBoneWeights &&
options.bakeStaticNodeTransforms == CookOptions.bakeStaticNodeTransforms;
// Meshes are built against the skeleton, so skinned models without it import differently
const bool sameMeshes = options.loadSkeleton || !model.IsSkinned();
return sameImport && sameMeshes;
}
void ModelFile::ApplyLoadOptions(ModelDoc::ModelAsset& model, const ModelDoc::LoadOptions& options) {
if (!options.loadMaterials) {
model.materials.clear();
for (ModelDoc::MeshPrimitive& primitive : model.primitives) {
primitive.materialIndex = ModelDoc::InvalidIndex;
}
}
if (!options.loadSkeleton) {
model.skeleton = Skeleton{};
}
if (!options.loadAnimations) {
model.animations.clear();
}
}
+90
View File
@@ -0,0 +1,90 @@
#include <destrum/Assets/TextureFile.h>
#include <glm/gtc/packing.hpp>
#include <glm/vec4.hpp>
#include "BinaryStream.h"
namespace {
using AssetBinary::BinaryReader;
using AssetBinary::BinaryWriter;
constexpr AssetBinary::Magic Magic{'D', 'T', 'E', 'X'};
constexpr std::string_view Kind = "texture";
std::size_t ExpectedByteSize(const TextureFile::Texture& texture) {
return std::size_t{texture.width} * texture.height * TextureFile::BytesPerPixel(texture.format);
}
}
std::size_t TextureFile::BytesPerPixel(PixelFormat format) {
switch (format) {
case PixelFormat::RGBA16Float:
return 4 * sizeof(std::uint16_t);
}
throw std::runtime_error("Unknown cooked texture pixel format");
}
TextureFile::Texture TextureFile::FromRGBA32Float(std::uint32_t width, std::uint32_t height, std::span<const float> rgba) {
const std::size_t pixelCount = std::size_t{width} * height;
if (rgba.size() != pixelCount * 4) {
throw std::invalid_argument("RGBA pixel data doesn't match the texture size");
}
Texture texture{
.width = width,
.height = height,
.format = PixelFormat::RGBA16Float,
};
texture.pixels.resize(pixelCount * BytesPerPixel(texture.format));
for (std::size_t i = 0; i < pixelCount; ++i) {
const glm::vec4 color{rgba[i * 4 + 0], rgba[i * 4 + 1], rgba[i * 4 + 2], rgba[i * 4 + 3]};
const std::uint64_t packed = glm::packHalf4x16(color);
std::memcpy(&texture.pixels[i * sizeof(packed)], &packed, sizeof(packed));
}
return texture;
}
std::vector<std::uint8_t> TextureFile::Write(const Texture& texture) {
if (texture.pixels.size() != ExpectedByteSize(texture)) {
throw std::invalid_argument("Texture pixel data doesn't match its size and format");
}
BinaryWriter writer;
writer.WriteHeader(Magic, Version);
writer.Write(texture.width);
writer.Write(texture.height);
writer.Write(static_cast<std::uint32_t>(texture.format));
writer.WriteArray(texture.pixels);
return writer.TakeBytes();
}
TextureFile::Texture TextureFile::Read(std::span<const std::uint8_t> bytes) {
BinaryReader reader{bytes};
reader.ReadHeader(Magic, Version, Kind);
Texture texture;
texture.width = reader.Read<std::uint32_t>();
texture.height = reader.Read<std::uint32_t>();
texture.format = static_cast<PixelFormat>(reader.Read<std::uint32_t>());
texture.pixels = reader.ReadArray<std::uint8_t>();
reader.ExpectEnd(Kind);
if (texture.pixels.size() != ExpectedByteSize(texture)) {
throw std::runtime_error("Cooked texture file is corrupt: pixel data doesn't match its size");
}
return texture;
}
void TextureFile::WriteToFile(const Texture& texture, const std::filesystem::path& path) {
AssetBinary::WriteAllBytes(path, Write(texture));
}
TextureFile::Texture TextureFile::ReadFromFile(const std::filesystem::path& path) {
return Read(AssetBinary::ReadAllBytes(path));
}
std::optional<std::uint32_t> TextureFile::ReadVersionFromFile(const std::filesystem::path& path) {
return AssetBinary::ReadVersion(path, Magic);
}
@@ -0,0 +1,90 @@
#include <destrum/Components/CameraComponent.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/ObjectModel/Transform.h>
#include <destrum/Scene/Scene.h>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtx/quaternion.hpp>
#include <algorithm>
#include <imgui.h>
CameraComponent::CameraComponent(GameObject& parent)
: Component(parent, "CameraComponent")
{
}
Camera& CameraComponent::GetCamera(float aspectRatio)
{
auto& transform = GetTransform();
const glm::quat rotation = transform.GetWorldRotation();
m_Camera.m_position = transform.GetWorldPosition();
m_Camera.SetOrientation(rotation * glm::vec3{0.0f, 0.0f, -1.0f}, rotation * glm::vec3{0.0f, 1.0f, 0.0f});
m_Camera.SetFOVY(m_FieldOfView);
m_Camera.SetZNear(m_ZNear);
m_Camera.SetZFar(m_ZFar);
m_Camera.setAspectRatio(aspectRatio);
return m_Camera;
}
void CameraComponent::SetFieldOfView(float fovDegrees)
{
m_FieldOfView = std::clamp(fovDegrees, 1.0f, 179.0f);
}
void CameraComponent::SetClipPlanes(float zNear, float zFar)
{
m_ZNear = std::max(zNear, 0.0001f);
m_ZFar = std::max(zFar, m_ZNear + 0.01f);
}
void CameraComponent::MakeCurrent()
{
if (Scene* scene = GetGameObject()->GetScene()) {
scene->SetCurrentCamera(this);
}
}
bool CameraComponent::IsCurrent() const
{
const Scene* scene = GetGameObject()->GetScene();
return scene != nullptr && scene->GetCurrentCamera() == this;
}
nlohmann::json CameraComponent::Serialize() const {
return {
{"fieldOfView", m_FieldOfView},
{"zNear", m_ZNear},
{"zFar", m_ZFar}
};
}
void CameraComponent::Deserialize(const nlohmann::json& data) {
if (data.contains("fieldOfView")) {
SetFieldOfView(data.at("fieldOfView").get<float>());
}
SetClipPlanes(data.value("zNear", m_ZNear), data.value("zFar", m_ZFar));
}
void CameraComponent::ImGuiInspector() {
bool current = IsCurrent();
if (ImGui::Checkbox("Current Camera", &current) && current) {
MakeCurrent();
}
float fieldOfView = m_FieldOfView;
if (ImGui::DragFloat("Field Of View", &fieldOfView, 0.5f, 1.0f, 179.0f)) {
SetFieldOfView(fieldOfView);
}
float zNear = m_ZNear;
float zFar = m_ZFar;
const bool nearChanged = ImGui::DragFloat("Near", &zNear, 0.01f, 0.0001f, 100.0f);
const bool farChanged = ImGui::DragFloat("Far", &zFar, 1.0f, 0.1f, 100000.0f);
if (nearChanged || farChanged) {
SetClipPlanes(zNear, zFar);
}
}
+135
View File
@@ -0,0 +1,135 @@
#include <destrum/Components/LightComponent.h>
#include <destrum/Graphics/RenderContext.h>
#include <destrum/ObjectModel/Transform.h>
#include <glm/gtc/constants.hpp>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtx/quaternion.hpp>
#include <algorithm>
#include <array>
#include <imgui.h>
LightComponent::LightComponent(GameObject& parent, LightType type)
: Component(parent, "LightComponent")
, m_Type(type)
{
}
void LightComponent::SetIntensity(float intensity)
{
m_Intensity = std::max(intensity, 0.0f);
}
void LightComponent::SetRange(float range)
{
m_Range = std::max(range, 0.01f);
}
void LightComponent::SetConeAngles(float innerConeAngle, float outerConeAngle)
{
m_InnerConeAngle = std::clamp(innerConeAngle, 0.0f, glm::half_pi<float>());
m_OuterConeAngle = std::clamp(outerConeAngle, m_InnerConeAngle, glm::half_pi<float>());
}
void LightComponent::Render(const RenderContext& ctx)
{
auto& transform = GetTransform();
ctx.renderer.submitLight(Light{
.type = m_Type,
.position = transform.GetWorldPosition(),
.direction = transform.GetWorldRotation() * glm::vec3{0.0f, 0.0f, -1.0f},
.color = m_Color,
.intensity = m_Intensity,
.range = m_Range,
.innerConeAngle = m_InnerConeAngle,
.outerConeAngle = m_OuterConeAngle,
.castShadows = m_CastShadows,
});
}
nlohmann::json LightComponent::Serialize() const {
return {
{"type", static_cast<std::uint32_t>(m_Type)},
{"color", {m_Color.x, m_Color.y, m_Color.z}},
{"intensity", m_Intensity},
{"range", m_Range},
{"innerConeAngle", m_InnerConeAngle},
{"outerConeAngle", m_OuterConeAngle},
{"castShadows", m_CastShadows}
};
}
void LightComponent::Deserialize(const nlohmann::json& data) {
if (data.contains("type")) {
const auto type = data.at("type").get<std::uint32_t>();
if (type <= static_cast<std::uint32_t>(LightType::Spot)) {
m_Type = static_cast<LightType>(type);
}
}
if (data.contains("color")) {
const auto& value = data.at("color");
m_Color = {value.at(0).get<float>(), value.at(1).get<float>(), value.at(2).get<float>()};
}
if (data.contains("intensity")) {
SetIntensity(data.at("intensity").get<float>());
}
if (data.contains("range")) {
SetRange(data.at("range").get<float>());
}
float innerConeAngle = m_InnerConeAngle;
float outerConeAngle = m_OuterConeAngle;
if (data.contains("innerConeAngle")) {
innerConeAngle = data.at("innerConeAngle").get<float>();
}
if (data.contains("outerConeAngle")) {
outerConeAngle = data.at("outerConeAngle").get<float>();
}
SetConeAngles(innerConeAngle, outerConeAngle);
if (data.contains("castShadows")) {
m_CastShadows = data.at("castShadows").get<bool>();
}
}
void LightComponent::ImGuiInspector() {
constexpr std::array typeNames{"Directional", "Point", "Spot"};
int type = static_cast<int>(m_Type);
if (ImGui::Combo("Type", &type, typeNames.data(), static_cast<int>(typeNames.size()))) {
SetType(static_cast<LightType>(type));
}
ImGui::ColorEdit3("Color", &m_Color.x);
float intensity = m_Intensity;
if (ImGui::DragFloat("Intensity", &intensity, 0.05f, 0.0f)) {
SetIntensity(intensity);
}
if (m_Type == LightType::Directional) {
ImGui::Checkbox("Cast Shadows", &m_CastShadows);
ImGui::SameLine();
ImGui::TextDisabled("(first directional light only)");
return;
}
float range = m_Range;
if (ImGui::DragFloat("Range", &range, 0.05f, 0.01f)) {
SetRange(range);
}
if (m_Type == LightType::Spot) {
float innerConeAngle = m_InnerConeAngle;
float outerConeAngle = m_OuterConeAngle;
bool changed = ImGui::SliderAngle("Inner Cone", &innerConeAngle, 0.0f, 90.0f);
changed |= ImGui::SliderAngle("Outer Cone", &outerConeAngle, 0.0f, 90.0f);
if (changed) {
SetConeAngles(innerConeAngle, outerConeAngle);
}
}
}
@@ -33,6 +33,9 @@ void MeshRendererComponent::Start() {
VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT |
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT VK_BUFFER_USAGE_VERTEX_BUFFER_BIT
); );
const std::string bufferName = GetGameObject()->GetName() + " (skinned vertices)";
vkutil::addDebugLabel(gfxDevice.getDevice(), m_skinnedMesh->skinnedVertexBuffer.buffer, bufferName.c_str());
} }
} }
@@ -74,6 +74,7 @@ void BindlessSetManager::init(
}; };
VK_CHECK(vkCreateDescriptorPool(device, &poolInfo, nullptr, &descPool)); VK_CHECK(vkCreateDescriptorPool(device, &poolInfo, nullptr, &descPool));
vkutil::addDebugLabel(device, descPool, "bindless descriptor pool");
} }
{ // build desc set layout { // build desc set layout
@@ -111,6 +112,7 @@ void BindlessSetManager::init(
}; };
VK_CHECK(vkCreateDescriptorSetLayout(device, &info, nullptr, &descSetLayout)); VK_CHECK(vkCreateDescriptorSetLayout(device, &info, nullptr, &descSetLayout));
vkutil::addDebugLabel(device, descSetLayout, "bindless descriptor set layout");
} }
{ // alloc desc set { // alloc desc set
@@ -122,6 +124,7 @@ void BindlessSetManager::init(
}; };
VK_CHECK(vkAllocateDescriptorSets(device, &allocInfo, &descSet)); VK_CHECK(vkAllocateDescriptorSets(device, &allocInfo, &descSet));
vkutil::addDebugLabel(device, descSet, "bindless descriptor set");
} }
initDefaultSamplers(device, maxAnisotropy); initDefaultSamplers(device, maxAnisotropy);
+10 -1
View File
@@ -114,7 +114,16 @@ void Camera::SetRotation(const glm::vec2& yawPitchRadians) {
SetRotation(yawPitchRadians.x, yawPitchRadians.y); SetRotation(yawPitchRadians.x, yawPitchRadians.y);
} }
void Camera::SetTarget(const glm::vec3& target) { void Camera::SetOrientation(const glm::vec3& forward, const glm::vec3& up) {
m_useTarget = false;
m_forward = glm::normalize(forward);
m_right = glm::normalize(glm::cross(m_forward, up));
m_up = glm::normalize(glm::cross(m_right, m_forward));
m_yaw = std::atan2(m_forward.z, m_forward.x);
m_pitch = std::asin(glm::clamp(m_forward.y, -1.0f, 1.0f));
}
void Camera::SetTarget(const glm::vec3&) {
} }
void Camera::ClearTarget() { void Camera::ClearTarget() {
+6 -1
View File
@@ -99,8 +99,13 @@ void ComputePipeline::CreateComputePipeline(const std::string& compPath,
throw std::runtime_error("Can't make compute pipeline!"); throw std::runtime_error("Can't make compute pipeline!");
} }
vkutil::addDebugLabel(m_device.getDevice(), m_compShaderModule, compFileName.c_str());
if (!configInfo.name.empty()) { if (!configInfo.name.empty()) {
vkutil::addDebugLabel(GameState::GetInstance().Gfx().getDevice(), m_computePipeline, configInfo.name.c_str()); vkutil::addDebugLabel(m_device.getDevice(), m_computePipeline, configInfo.name.c_str());
const std::string layoutName = configInfo.name + " Layout";
vkutil::addDebugLabel(m_device.getDevice(), configInfo.pipelineLayout, layoutName.c_str());
} }
} }
+4
View File
@@ -156,6 +156,7 @@ void GfxDevice::endFrame(VkCommandBuffer cmd, const GPUImage& drawImage, const E
auto swapchainLayout = VK_IMAGE_LAYOUT_UNDEFINED; { auto swapchainLayout = VK_IMAGE_LAYOUT_UNDEFINED; {
ZoneScopedN("Clear Swapchain Image"); ZoneScopedN("Clear Swapchain Image");
vkutil::ScopedCmdLabel label(cmd, "Clear Swapchain Image", vkutil::labelColors::transfer);
const VkImageSubresourceRange clearRange = vkinit::imageSubresourceRange(VK_IMAGE_ASPECT_COLOR_BIT); const VkImageSubresourceRange clearRange = vkinit::imageSubresourceRange(VK_IMAGE_ASPECT_COLOR_BIT);
vkutil::transitionImage(cmd, swapchainImage, swapchainLayout, VK_IMAGE_LAYOUT_GENERAL); vkutil::transitionImage(cmd, swapchainImage, swapchainLayout, VK_IMAGE_LAYOUT_GENERAL);
@@ -167,6 +168,7 @@ void GfxDevice::endFrame(VkCommandBuffer cmd, const GPUImage& drawImage, const E
if (true) { if (true) {
ZoneScopedN("Copy DrawImage To Swapchain"); ZoneScopedN("Copy DrawImage To Swapchain");
vkutil::ScopedCmdLabel label(cmd, "Copy DrawImage To Swapchain", vkutil::labelColors::transfer);
vkutil::transitionImage( vkutil::transitionImage(
cmd, cmd,
@@ -190,6 +192,7 @@ void GfxDevice::endFrame(VkCommandBuffer cmd, const GPUImage& drawImage, const E
if (props.imguiPass) { if (props.imguiPass) {
ZoneScopedN("ImGui Render"); ZoneScopedN("ImGui Render");
vkutil::ScopedCmdLabel label(cmd, "ImGui Render", vkutil::labelColors::ui);
props.imguiPass->render( props.imguiPass->render(
cmd, cmd,
@@ -202,6 +205,7 @@ void GfxDevice::endFrame(VkCommandBuffer cmd, const GPUImage& drawImage, const E
{ {
ZoneScopedN("Transition Swapchain To Present"); ZoneScopedN("Transition Swapchain To Present");
vkutil::ScopedCmdLabel label(cmd, "Transition Swapchain To Present", vkutil::labelColors::transfer);
vkutil::transitionImage(cmd, swapchainImage, swapchainLayout, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); vkutil::transitionImage(cmd, swapchainImage, swapchainLayout, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
swapchainLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; swapchainLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
+47 -1
View File
@@ -11,7 +11,14 @@
#include "spdlog/spdlog.h" #include "spdlog/spdlog.h"
#include <destrum/Assets/TextureFile.h>
ImageData::~ImageData() ImageData::~ImageData()
{
freePixels();
}
void ImageData::freePixels()
{ {
if (shouldSTBFree) { if (shouldSTBFree) {
if (pixels) stbi_image_free(pixels); if (pixels) stbi_image_free(pixels);
@@ -22,6 +29,12 @@ ImageData::~ImageData()
if (hdrPixels) free(hdrPixels); if (hdrPixels) free(hdrPixels);
spdlog::debug("Freed TinyEXR image memory"); spdlog::debug("Freed TinyEXR image memory");
} }
cookedPixels.clear();
pixels = nullptr;
hdrPixels = nullptr;
shouldSTBFree = false;
shouldEXRFree = false;
} }
namespace util namespace util
@@ -33,13 +46,46 @@ namespace util
return ext == ".exr"; return ext == ".exr";
} }
static bool isCookedTextureExt(const std::filesystem::path& p)
{
auto ext = p.extension().string();
for (auto& c : ext) c = char(::tolower(c));
return ext == TextureFile::Extension;
}
static VkFormat toVkFormat(TextureFile::PixelFormat format)
{
switch (format) {
case TextureFile::PixelFormat::RGBA16Float: return VK_FORMAT_R16G16B16A16_SFLOAT;
}
return VK_FORMAT_UNDEFINED;
}
ImageData loadImage(const std::filesystem::path& p, TextureIntent intent) ImageData loadImage(const std::filesystem::path& p, TextureIntent intent)
{ {
ImageData data; ImageData data;
// ---------- EXR ---------- if (isCookedTextureExt(p)) {
try {
TextureFile::Texture texture = TextureFile::ReadFromFile(p);
data.cookedPixels = std::move(texture.pixels);
data.pixels = data.cookedPixels.data();
data.width = static_cast<int>(texture.width);
data.height = static_cast<int>(texture.height);
data.channels = 4;
data.vkFormat = toVkFormat(texture.format);
data.byteSize = data.cookedPixels.size();
} catch (const std::exception& exception) {
spdlog::error("Failed to read cooked texture '{}': {}", p.string(), exception.what());
return {};
}
return data;
}
if (isExrExt(p)) { if (isExrExt(p)) {
float* out = nullptr; float* out = nullptr;
int w = 0, h = 0; int w = 0, h = 0;
@@ -40,6 +40,10 @@ void VulkanImmediateExecutor::init(
}; };
VK_CHECK(vkCreateFence(deviceHandle, &fenceCreateInfo, nullptr, &immFence)); VK_CHECK(vkCreateFence(deviceHandle, &fenceCreateInfo, nullptr, &immFence));
vkutil::addDebugLabel(deviceHandle, immCommandPool, "immediate command pool");
vkutil::addDebugLabel(deviceHandle, immCommandBuffer, "immediate command buffer");
vkutil::addDebugLabel(deviceHandle, immFence, "immediate fence");
initialized = true; initialized = true;
} catch (...) { } catch (...) {
cleanup(deviceHandle); cleanup(deviceHandle);
@@ -26,6 +26,11 @@ void FrameManager::init(VkDevice dev, std::uint32_t queueFamily) {
const auto cmdAllocInfo = vkinit::commandBufferAllocateInfo(commandPool, 1); const auto cmdAllocInfo = vkinit::commandBufferAllocateInfo(commandPool, 1);
auto& mainCommandBuffer = frames[i].commandBuffer; auto& mainCommandBuffer = frames[i].commandBuffer;
VK_CHECK(vkAllocateCommandBuffers(device, &cmdAllocInfo, &mainCommandBuffer)); VK_CHECK(vkAllocateCommandBuffers(device, &cmdAllocInfo, &mainCommandBuffer));
const std::string poolName = "frame " + std::to_string(i) + " command pool";
const std::string cmdName = "frame " + std::to_string(i) + " command buffer";
vkutil::addDebugLabel(device, commandPool, poolName.c_str());
vkutil::addDebugLabel(device, mainCommandBuffer, cmdName.c_str());
} }
} catch (...) { } catch (...) {
cleanup(dev); cleanup(dev);
@@ -178,7 +178,9 @@ std::optional<GPUImage> ImageManager::loadImageFromFile(
} }
image.debugName = path.string(); image.debugName = path.string();
vkutil::addDebugLabel(device, image.image, path.string().c_str()); vkutil::addDebugLabel(device, image.image, image.debugName.c_str());
const std::string viewName = image.debugName + " (view)";
vkutil::addDebugLabel(device, image.imageView, viewName.c_str());
return image; return image;
} }
@@ -0,0 +1,23 @@
#include <destrum/Graphics/Managers/LineRenderingManager.h>
void LineRenderingManager::SubmitLine(const Line& line)
{
lines.push_back(line);
}
void LineRenderingManager::SubmitLine(
const glm::vec3& start,
const glm::vec3& end,
const glm::vec4& color)
{
SubmitLine(Line{
.start = start,
.end = end,
.color = color,
});
}
void LineRenderingManager::ClearLines()
{
lines.clear();
}
@@ -0,0 +1,31 @@
#include <destrum/Graphics/Managers/QuadRenderingManager.h>
void QuadRenderingManager::SubmitQuad(
const glm::vec2& position,
const glm::vec2& size,
const glm::vec4& color,
ImageID textureId)
{
SubmitQuad(position, size, 0.0f, color, textureId);
}
void QuadRenderingManager::SubmitQuad(
const glm::vec2& position,
const glm::vec2& size,
float rotation,
const glm::vec4& color,
ImageID textureId)
{
quads.push_back(QuadSubmission{
.position = position,
.size = size,
.rotation = rotation,
.color = color,
.textureId = textureId,
});
}
void QuadRenderingManager::ClearQuads()
{
quads.clear();
}
+3
View File
@@ -156,6 +156,9 @@ void MeshCache::uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh
VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE); VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE);
const auto skinningBufferName = cpuMesh.name + " (skinning data)";
vkutil::addDebugLabel(gfxDevice.getDevice(), gpuMesh.skinningDataBuffer.buffer, skinningBufferName.c_str());
auto skinningStaging = auto skinningStaging =
gfxDevice.createBuffer( gfxDevice.createBuffer(
skinningDataSize, skinningDataSize,
+17 -18
View File
@@ -146,11 +146,11 @@ void Pipeline::CreateGraphicsPipeline(const std::string& vertPath, const std::st
CreateShaderModule(vertCode, &m_vertShaderModule); CreateShaderModule(vertCode, &m_vertShaderModule);
// vkutil::addDebugLabel( vkutil::addDebugLabel(
// m_device.getDevice(), m_device.getDevice(),
// m_fragShaderModule, m_vertShaderModule,
// VertFileName.c_str() VertFileName.c_str()
// ); );
VkPipelineShaderStageCreateInfo vertShaderStageInfo{}; VkPipelineShaderStageCreateInfo vertShaderStageInfo{};
vertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; vertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
@@ -173,11 +173,11 @@ void Pipeline::CreateGraphicsPipeline(const std::string& vertPath, const std::st
CreateShaderModule(fragCode, &m_fragShaderModule); CreateShaderModule(fragCode, &m_fragShaderModule);
// vkutil::addDebugLabel( vkutil::addDebugLabel(
// m_device.getDevice(), m_device.getDevice(),
// m_fragShaderModule, m_fragShaderModule,
// FragFileName.c_str() FragFileName.c_str()
// ); );
VkPipelineShaderStageCreateInfo fragShaderStageInfo{}; VkPipelineShaderStageCreateInfo fragShaderStageInfo{};
@@ -231,18 +231,17 @@ void Pipeline::CreateGraphicsPipeline(const std::string& vertPath, const std::st
pipelineInfo.basePipelineIndex = -1; pipelineInfo.basePipelineIndex = -1;
pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; pipelineInfo.basePipelineHandle = VK_NULL_HANDLE;
if (!configInfo.name.empty()) {
// DebugLabel::SetObjectName(
// reinterpret_cast<uint64_t>(pipelineInfo.layout),
// VK_OBJECT_TYPE_PIPELINE_LAYOUT,
// configInfo.name.c_str()
// );
}
if (vkCreateGraphicsPipelines(m_device.getDevice(), VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &m_graphicsPipeline) != VK_SUCCESS) { if (vkCreateGraphicsPipelines(m_device.getDevice(), VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &m_graphicsPipeline) != VK_SUCCESS) {
throw std::runtime_error("Can't make pipeline!"); throw std::runtime_error("Can't make pipeline!");
} }
if (!configInfo.name.empty()) {
vkutil::addDebugLabel(m_device.getDevice(), m_graphicsPipeline, configInfo.name.c_str());
const std::string layoutName = configInfo.name + " Layout";
vkutil::addDebugLabel(m_device.getDevice(), configInfo.pipelineLayout, layoutName.c_str());
}
} }
void Pipeline::CreateShaderModule(const std::vector<char>& code, VkShaderModule* shaderModule) const { void Pipeline::CreateShaderModule(const std::vector<char>& code, VkShaderModule* shaderModule) const {
@@ -122,8 +122,22 @@ void ImguiPass::beginFrame() {
return; return;
} }
// the previous UI was built but never rendered (e.g. the frame got skipped while resizing),
// so the GPU never saw these
std::vector<VkDescriptorSet> unusedTextures;
for (const FrameTexture& texture : pendingTextures) {
unusedTextures.push_back(texture.descriptorSet);
}
pendingTextures.clear();
releaseTextures(unusedTextures);
ImGui_ImplVulkan_NewFrame(); ImGui_ImplVulkan_NewFrame();
ImGui_ImplSDL2_NewFrame(); ImGui_ImplSDL2_NewFrame();
if (SDL_GetRelativeMouseMode() == SDL_TRUE) {
ImGui::GetIO().MousePos = ImVec2(-FLT_MAX, -FLT_MAX);
}
ImGui::NewFrame(); ImGui::NewFrame();
frameBegun = true; frameBegun = true;
@@ -150,6 +164,18 @@ void ImguiPass::render(
return; return;
} }
{
// beginFrame on the gfx device already waited for this frame index' fence,
// so the textures it used last time around can go
auto& inFlight = inFlightTextures[gfx->getCurrentFrameIndex() % FRAMES_IN_FLIGHT];
releaseTextures(inFlight);
for (const FrameTexture& texture : pendingTextures) {
inFlight.push_back(texture.descriptorSet);
}
pendingTextures.clear();
}
ImDrawData* drawData = ImGui::GetDrawData(); ImDrawData* drawData = ImGui::GetDrawData();
if (drawData == nullptr || drawData->CmdListsCount == 0) { if (drawData == nullptr || drawData->CmdListsCount == 0) {
return; return;
@@ -195,6 +221,8 @@ void ImguiPass::onSwapchainRecreated() {
const auto newFormat = gfx->getSwapchainFormat(); const auto newFormat = gfx->getSwapchainFormat();
if (newFormat != colorFormat) { if (newFormat != colorFormat) {
if (vulkanInitialized) { if (vulkanInitialized) {
// the swapchain recreate waited for the device, nothing is in flight anymore
releaseAllTextures();
ImGui_ImplVulkan_Shutdown(); ImGui_ImplVulkan_Shutdown();
vulkanInitialized = false; vulkanInitialized = false;
} }
@@ -219,6 +247,7 @@ void ImguiPass::cleanup() {
} }
if (vulkanInitialized) { if (vulkanInitialized) {
releaseAllTextures();
ImGui_ImplVulkan_Shutdown(); ImGui_ImplVulkan_Shutdown();
vulkanInitialized = false; vulkanInitialized = false;
} }
@@ -288,3 +317,49 @@ void ImguiPass::transitionToColorAttachment(
vkCmdPipelineBarrier2(cmd, &dependencyInfo); vkCmdPipelineBarrier2(cmd, &dependencyInfo);
} }
ImTextureID ImguiPass::getFrameTexture(VkImageView imageView, VkImageLayout imageLayout) {
if (!vulkanInitialized || imageView == VK_NULL_HANDLE) {
return ImTextureID{};
}
for (const FrameTexture& texture : pendingTextures) {
if (texture.imageView == imageView && texture.imageLayout == imageLayout) {
return reinterpret_cast<ImTextureID>(texture.descriptorSet);
}
}
const VkDescriptorSet descriptorSet = ImGui_ImplVulkan_AddTexture(imageView, imageLayout);
if (descriptorSet == VK_NULL_HANDLE) {
return ImTextureID{};
}
pendingTextures.push_back(FrameTexture{
.imageView = imageView,
.imageLayout = imageLayout,
.descriptorSet = descriptorSet,
});
return reinterpret_cast<ImTextureID>(descriptorSet);
}
void ImguiPass::releaseTextures(std::vector<VkDescriptorSet>& textures) {
if (vulkanInitialized) {
for (VkDescriptorSet descriptorSet : textures) {
ImGui_ImplVulkan_RemoveTexture(descriptorSet);
}
}
textures.clear();
}
void ImguiPass::releaseAllTextures() {
std::vector<VkDescriptorSet> textures;
for (const FrameTexture& texture : pendingTextures) {
textures.push_back(texture.descriptorSet);
}
pendingTextures.clear();
releaseTextures(textures);
for (auto& inFlight : inFlightTextures) {
releaseTextures(inFlight);
}
}
@@ -0,0 +1,272 @@
#include <destrum/Graphics/Pipelines/LightCullingPipeline.h>
#include "destrum/FS/AssetFS.h"
#include "destrum/Graphics/GfxDevice.h"
#include "destrum/Graphics/RenderResources.h"
#include "destrum/Graphics/Util.h"
#include <array>
#include <cassert>
#include <cmath>
#include <cstdint>
#include <stdexcept>
#include "volk.h"
#include "spdlog/spdlog.h"
void LightCullingPipeline::init(GfxDevice& gfxDevice, RenderResources& resources, glm::ivec2 drawImageSize)
{
try
{
const auto& device = gfxDevice.getDevice();
const auto pushConstant = VkPushConstantRange{
.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT,
.offset = 0,
.size = sizeof(PushConstants),
};
const auto pushConstants = std::array{pushConstant};
const auto layouts = std::array{
resources.getBindlessDescSetLayout()
};
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = static_cast<std::uint32_t>(layouts.size());
pipelineLayoutInfo.pSetLayouts = layouts.data();
pipelineLayoutInfo.pushConstantRangeCount = static_cast<std::uint32_t>(pushConstants.size());
pipelineLayoutInfo.pPushConstantRanges = pushConstants.data();
if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &m_pipelineLayout) != VK_SUCCESS)
{
throw std::runtime_error("Could not make light culling pipeline layout");
}
ComputePipelineConfigInfo pipelineConfig{};
pipelineConfig.name = "light culling compute pipeline";
pipelineConfig.pipelineLayout = m_pipelineLayout;
const auto lightCullingShaderPath = AssetFS::GetInstance().GetCookedPathForFile(
"engine://shaders/light_culling.comp");
lightCullingPipeline = std::make_unique<ComputePipeline>(
gfxDevice,
lightCullingShaderPath.generic_string(),
pipelineConfig
);
for (std::size_t i = 0; i < FRAMES_IN_FLIGHT; ++i)
{
auto& lightsBuffer = framesData[i].lightsBuffer;
lightsBuffer.capacity = MAX_LIGHTS;
lightsBuffer.buffer = gfxDevice.createBuffer(
MAX_LIGHTS * sizeof(GPULight),
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU);
vkutil::addDebugLabel(device, lightsBuffer.buffer.buffer, "lights");
}
createTileBuffer(gfxDevice, drawImageSize);
}
catch (...)
{
cleanup(gfxDevice);
throw;
}
}
void LightCullingPipeline::cleanup(GfxDevice& gfxDevice)
{
for (auto& frame : framesData)
{
gfxDevice.destroyBuffer(frame.lightsBuffer.buffer);
frame.lightsBuffer.buffer = {};
frame.lightsBuffer.size = 0;
}
gfxDevice.destroyBuffer(tileLightIndicesBuffer);
tileLightIndicesBuffer = {};
tileCount = glm::uvec2{0u, 0u};
if (m_pipelineLayout != VK_NULL_HANDLE && gfxDevice.getDevice() != VK_NULL_HANDLE)
{
vkDestroyPipelineLayout(gfxDevice.getDevice(), m_pipelineLayout, nullptr);
}
m_pipelineLayout = VK_NULL_HANDLE;
lightCullingPipeline.reset();
}
void LightCullingPipeline::resize(GfxDevice& gfxDevice, glm::ivec2 drawImageSize)
{
gfxDevice.waitIdle();
gfxDevice.destroyBuffer(tileLightIndicesBuffer);
tileLightIndicesBuffer = {};
createTileBuffer(gfxDevice, drawImageSize);
}
void LightCullingPipeline::createTileBuffer(GfxDevice& gfxDevice, glm::ivec2 drawImageSize)
{
assert(drawImageSize.x > 0 && drawImageSize.y > 0);
tileCount = glm::uvec2{
(static_cast<std::uint32_t>(drawImageSize.x) + TILE_SIZE - 1) / TILE_SIZE,
(static_cast<std::uint32_t>(drawImageSize.y) + TILE_SIZE - 1) / TILE_SIZE,
};
const std::size_t numTiles = static_cast<std::size_t>(tileCount.x) * tileCount.y;
tileLightIndicesBuffer = gfxDevice.createBuffer(
numTiles * TILE_STRIDE * sizeof(std::uint32_t),
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT);
vkutil::addDebugLabel(gfxDevice.getDevice(), tileLightIndicesBuffer.buffer, "tile light indices");
spdlog::debug("Forward+ light grid: {}x{} tiles", tileCount.x, tileCount.y);
}
void LightCullingPipeline::beginDrawing(std::size_t frameIndex)
{
currentFrameIndex = frameIndex;
getCurrentFrameData().lightsBuffer.clear();
}
void LightCullingPipeline::appendLight(const Light& light)
{
auto& lightsBuffer = getCurrentFrameData().lightsBuffer;
if (lightsBuffer.size >= lightsBuffer.capacity)
{
if (!warnedAboutLightLimit)
{
spdlog::warn("Light limit of {} reached, extra lights are ignored", MAX_LIGHTS);
warnedAboutLightLimit = true;
}
return;
}
const float directionLength = glm::length(light.direction);
const glm::vec3 direction = directionLength > 1e-6f
? light.direction / directionLength
: glm::vec3{0.0f, -1.0f, 0.0f};
const auto gpuLight = GPULight{
.position = light.position,
.range = light.range,
.color = light.color,
.intensity = light.intensity,
.direction = direction,
.type = static_cast<std::uint32_t>(light.type),
.spotInnerCos = std::cos(light.innerConeAngle),
.spotOuterCos = std::cos(light.outerConeAngle),
.castShadows = light.castShadows ? 1u : 0u,
.padding = 0.0f,
};
lightsBuffer.append(gpuLight);
}
void LightCullingPipeline::flushCurrentFrame(VkCommandBuffer cmd, GfxDevice& gfxDevice)
{
auto& buffer = getCurrentFrameData().lightsBuffer.buffer;
gfxDevice.getMemoryManager().flushAllocation(buffer, 0, VK_WHOLE_SIZE);
vkutil::bufferHostWriteToShaderReadBarrier(
cmd,
buffer.buffer,
0,
static_cast<VkDeviceSize>(MAX_LIGHTS * sizeof(GPULight)));
}
void LightCullingPipeline::cullLights(
VkCommandBuffer cmd,
const RenderResources& resources,
const GPUBuffer& sceneDataBuffer,
ImageID depthImageId,
VkExtent2D renderExtent)
{
if (!lightCullingPipeline)
{
return;
}
{
// previous frame's mesh pass might still be reading the tile lists
const VkBufferMemoryBarrier2 tileBufferBarrier{
.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2,
.srcStageMask = VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT,
.srcAccessMask = VK_ACCESS_2_SHADER_STORAGE_READ_BIT,
.dstStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
.dstAccessMask = VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.buffer = tileLightIndicesBuffer.buffer,
.offset = 0,
.size = VK_WHOLE_SIZE,
};
const VkDependencyInfo dependencyInfo{
.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
.bufferMemoryBarrierCount = 1,
.pBufferMemoryBarriers = &tileBufferBarrier,
};
vkCmdPipelineBarrier2(cmd, &dependencyInfo);
}
lightCullingPipeline->bind(cmd);
resources.bindBindlessDescSet(cmd, m_pipelineLayout, VK_PIPELINE_BIND_POINT_COMPUTE);
const auto cs = PushConstants{
.sceneDataBuffer = sceneDataBuffer.address,
.screenSize = glm::uvec2{renderExtent.width, renderExtent.height},
.depthTexId = static_cast<std::uint32_t>(depthImageId),
.padding = 0,
};
vkCmdPushConstants(cmd,
m_pipelineLayout,
VK_SHADER_STAGE_COMPUTE_BIT,
0,
sizeof(PushConstants),
&cs);
// one workgroup per tile
vkCmdDispatch(cmd, tileCount.x, tileCount.y, 1);
const VkBufferMemoryBarrier2 tileBufferBarrier{
.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2,
.srcStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
.srcAccessMask = VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT,
.dstStageMask = VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT,
.dstAccessMask = VK_ACCESS_2_SHADER_STORAGE_READ_BIT,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.buffer = tileLightIndicesBuffer.buffer,
.offset = 0,
.size = VK_WHOLE_SIZE,
};
const VkDependencyInfo dependencyInfo{
.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
.bufferMemoryBarrierCount = 1,
.pBufferMemoryBarriers = &tileBufferBarrier,
};
vkCmdPipelineBarrier2(cmd, &dependencyInfo);
}
VkDeviceAddress LightCullingPipeline::getLightsBufferAddress() const
{
return getCurrentFrameData().lightsBuffer.buffer.address;
}
std::uint32_t LightCullingPipeline::getNumLights() const
{
return static_cast<std::uint32_t>(getCurrentFrameData().lightsBuffer.size);
}
LightCullingPipeline::PerFrameData& LightCullingPipeline::getCurrentFrameData()
{
return framesData[currentFrameIndex % FRAMES_IN_FLIGHT];
}
const LightCullingPipeline::PerFrameData& LightCullingPipeline::getCurrentFrameData() const
{
return framesData[currentFrameIndex % FRAMES_IN_FLIGHT];
}
@@ -0,0 +1,232 @@
#include <destrum/Graphics/Pipelines/LineRenderingPass.h>
#include <algorithm>
#include <cstdint>
#include <limits>
#include <stdexcept>
#include <destrum/FS/AssetFS.h>
#include <destrum/Graphics/Camera.h>
#include <destrum/Graphics/GPUImage.h>
#include <destrum/Graphics/GfxDevice.h>
#include <destrum/Graphics/Pipeline.h>
#include <destrum/Graphics/Util.h>
#include "volk.h"
namespace {
constexpr std::size_t InitialVertexCapacity = 256;
}
void LineRenderingPass::init(GfxDevice& gfxDevice, VkFormat drawImageFormat)
{
try {
vertexBuffer.init(
gfxDevice,
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
InitialVertexCapacity * sizeof(LineVertex),
"line vertices");
vertexCapacity = InitialVertexCapacity;
const auto vertexShader =
AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/line.vert");
const auto fragmentShader =
AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/line.frag");
const auto pushConstantRange = VkPushConstantRange{
.stageFlags = VK_SHADER_STAGE_VERTEX_BIT,
.offset = 0,
.size = sizeof(glm::mat4),
};
const auto pipelineLayoutInfo = VkPipelineLayoutCreateInfo{
.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
.pushConstantRangeCount = 1,
.pPushConstantRanges = &pushConstantRange,
};
VK_CHECK(vkCreatePipelineLayout(
gfxDevice.getDevice(),
&pipelineLayoutInfo,
nullptr,
&pipelineLayout));
PipelineConfigInfo pipelineConfig{};
Pipeline::DefaultPipelineConfigInfo(pipelineConfig);
pipelineConfig.name = "Line Rendering Pipeline";
pipelineConfig.pipelineLayout = pipelineLayout;
pipelineConfig.inputAssemblyInfo.topology = VK_PRIMITIVE_TOPOLOGY_LINE_LIST;
pipelineConfig.rasterizationInfo.cullMode = VK_CULL_MODE_NONE;
pipelineConfig.depthStencilInfo.depthTestEnable = VK_FALSE;
pipelineConfig.depthStencilInfo.depthWriteEnable = VK_FALSE;
pipelineConfig.depthStencilInfo.depthCompareOp = VK_COMPARE_OP_ALWAYS;
pipelineConfig.colorAttachments = {drawImageFormat};
pipelineConfig.depthAttachment = VK_FORMAT_UNDEFINED;
pipelineConfig.vertexBindingDescriptions = {
VkVertexInputBindingDescription{
.binding = 0,
.stride = sizeof(LineVertex),
.inputRate = VK_VERTEX_INPUT_RATE_VERTEX,
}
};
pipelineConfig.vertexAttributeDescriptions = {
VkVertexInputAttributeDescription{
.location = 0,
.binding = 0,
.format = VK_FORMAT_R32G32B32A32_SFLOAT,
.offset = offsetof(LineVertex, position),
},
VkVertexInputAttributeDescription{
.location = 1,
.binding = 0,
.format = VK_FORMAT_R32G32B32A32_SFLOAT,
.offset = offsetof(LineVertex, color),
},
};
pipelineConfig.colorBlendAttachment.blendEnable = VK_TRUE;
pipelineConfig.colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
pipelineConfig.colorBlendAttachment.dstColorBlendFactor =
VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
pipelineConfig.colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD;
pipelineConfig.colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
pipelineConfig.colorBlendAttachment.dstAlphaBlendFactor =
VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
pipelineConfig.colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;
pipeline = std::make_unique<Pipeline>(
gfxDevice,
vertexShader.string(),
fragmentShader.string(),
pipelineConfig);
} catch (...) {
cleanup(gfxDevice);
throw;
}
}
void LineRenderingPass::draw(
VkCommandBuffer cmd,
GfxDevice& gfxDevice,
const GPUImage& target,
const Camera& camera,
LineRenderingManager& lineManager)
{
const auto& lines = lineManager.GetLines();
if (!pipeline || lines.empty()) {
lineManager.ClearLines();
return;
}
if (lines.size() > std::numeric_limits<std::size_t>::max() / 2) {
throw std::overflow_error("Too many lines submitted for rendering");
}
std::vector<LineVertex> vertices;
vertices.reserve(lines.size() * 2);
for (const Line& line : lines) {
vertices.push_back(LineVertex{
.position = glm::vec4{line.start, 1.0f},
.color = line.color,
});
vertices.push_back(LineVertex{
.position = glm::vec4{line.end, 1.0f},
.color = line.color,
});
}
if (vertices.size() > std::numeric_limits<std::uint32_t>::max()) {
throw std::overflow_error("Too many line vertices submitted for rendering");
}
ensureVertexCapacity(gfxDevice, vertices.size());
vertexBuffer.uploadNewData(
cmd,
gfxDevice.getCurrentFrameIndex(),
vertices.data(),
vertices.size() * sizeof(LineVertex));
auto renderInfo = vkutil::createRenderingInfo({
.renderExtent = target.getExtent2D(),
.colorImageView = target.imageView,
});
vkCmdBeginRendering(cmd, &renderInfo.renderingInfo);
pipeline->bind(cmd);
const auto viewport = VkViewport{
.x = 0.0f,
.y = 0.0f,
.width = static_cast<float>(target.extent.width),
.height = static_cast<float>(target.extent.height),
.minDepth = 0.0f,
.maxDepth = 1.0f,
};
vkCmdSetViewport(cmd, 0, 1, &viewport);
const auto scissor = VkRect2D{
.offset = {},
.extent = target.getExtent2D(),
};
vkCmdSetScissor(cmd, 0, 1, &scissor);
vkCmdSetPolygonModeEXT(cmd, VK_POLYGON_MODE_FILL);
const glm::mat4 viewProjection = camera.GetViewProjectionMatrix();
vkCmdPushConstants(
cmd,
pipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT,
0,
sizeof(viewProjection),
&viewProjection);
const VkBuffer vertexBufferHandle = vertexBuffer.getBuffer().buffer;
const VkDeviceSize vertexBufferOffset = 0;
vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBufferHandle, &vertexBufferOffset);
vkCmdDraw(cmd, static_cast<std::uint32_t>(vertices.size()), 1, 0, 0);
vkCmdEndRendering(cmd);
lineManager.ClearLines();
}
void LineRenderingPass::cleanup(GfxDevice& gfxDevice)
{
pipeline.reset();
if (pipelineLayout != VK_NULL_HANDLE && gfxDevice.getDevice() != VK_NULL_HANDLE) {
vkDestroyPipelineLayout(gfxDevice.getDevice(), pipelineLayout, nullptr);
}
pipelineLayout = VK_NULL_HANDLE;
vertexBuffer.cleanup(gfxDevice);
vertexCapacity = 0;
}
void LineRenderingPass::ensureVertexCapacity(
GfxDevice& gfxDevice,
std::size_t requiredVertices)
{
if (requiredVertices <= vertexCapacity) {
return;
}
std::size_t newCapacity = std::max(vertexCapacity, InitialVertexCapacity);
while (newCapacity < requiredVertices) {
if (newCapacity > std::numeric_limits<std::size_t>::max() / 2) {
newCapacity = requiredVertices;
break;
}
newCapacity *= 2;
}
gfxDevice.waitIdle();
vertexBuffer.cleanup(gfxDevice);
vertexBuffer.init(
gfxDevice,
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
newCapacity * sizeof(LineVertex),
"line vertices");
vertexCapacity = newCapacity;
}
+158 -60
View File
@@ -9,6 +9,7 @@
#include <destrum/Graphics/RenderResources.h> #include <destrum/Graphics/RenderResources.h>
#include <destrum/Graphics/Caches/MeshCache.h> #include <destrum/Graphics/Caches/MeshCache.h>
#include <destrum/Graphics/Frustum.h> #include <destrum/Graphics/Frustum.h>
#include <destrum/Graphics/Util.h>
#include "volk.h" #include "volk.h"
#include "spdlog/spdlog.h" #include "spdlog/spdlog.h"
@@ -23,66 +24,122 @@ void MeshPipeline::init(
VkFormat drawImageFormat, VkFormat drawImageFormat,
VkFormat depthImageFormat) VkFormat depthImageFormat)
{ {
try { try
const auto vertexShader = {
AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/mesh.vert"); const auto vertexShader =
AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/mesh.vert");
const auto fragShader = const auto fragShader =
AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/mesh.frag"); AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/mesh.frag");
const auto bufferRange = VkPushConstantRange{ const auto bufferRange = VkPushConstantRange{
.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, .stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
.offset = 0, .offset = 0,
.size = sizeof(PushConstants), .size = sizeof(PushConstants),
}; };
const auto pushConstantRanges = std::array{bufferRange}; const auto pushConstantRanges = std::array{bufferRange};
const auto layouts = std::array{ const auto layouts = std::array{
resources.getBindlessDescSetLayout() resources.getBindlessDescSetLayout()
}; };
VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = static_cast<std::uint32_t>(layouts.size()); pipelineLayoutInfo.setLayoutCount = static_cast<std::uint32_t>(layouts.size());
pipelineLayoutInfo.pSetLayouts = layouts.data(); pipelineLayoutInfo.pSetLayouts = layouts.data();
pipelineLayoutInfo.pushConstantRangeCount = pipelineLayoutInfo.pushConstantRangeCount =
static_cast<std::uint32_t>(pushConstantRanges.size()); static_cast<std::uint32_t>(pushConstantRanges.size());
pipelineLayoutInfo.pPushConstantRanges = pushConstantRanges.data(); pipelineLayoutInfo.pPushConstantRanges = pushConstantRanges.data();
if (vkCreatePipelineLayout( if (vkCreatePipelineLayout(
gfxDevice.getDevice(), gfxDevice.getDevice(),
&pipelineLayoutInfo, &pipelineLayoutInfo,
nullptr, nullptr,
&m_pipelineLayout) != VK_SUCCESS) &m_pipelineLayout) != VK_SUCCESS)
{ {
throw std::runtime_error("Could not create mesh pipeline layout"); throw std::runtime_error("Could not create mesh pipeline layout");
}
{
PipelineConfigInfo pipelineConfig{};
Pipeline::DefaultPipelineConfigInfo(pipelineConfig);
pipelineConfig.name = "Mesh Depth Prepass Pipeline";
pipelineConfig.pipelineLayout = m_pipelineLayout;
pipelineConfig.vertexAttributeDescriptions = {};
pipelineConfig.vertexBindingDescriptions = {};
pipelineConfig.colorAttachments = {};
pipelineConfig.colorBlendInfo.attachmentCount = 0;
pipelineConfig.depthAttachment = depthImageFormat;
m_depthPrepassPipeline = std::make_unique<Pipeline>(
gfxDevice,
vertexShader.string(),
"",
pipelineConfig
);
}
{
PipelineConfigInfo pipelineConfig{};
Pipeline::DefaultPipelineConfigInfo(pipelineConfig);
pipelineConfig.name = "Mesh Pipeline";
pipelineConfig.pipelineLayout = m_pipelineLayout;
pipelineConfig.vertexAttributeDescriptions = {};
pipelineConfig.vertexBindingDescriptions = {};
pipelineConfig.colorAttachments = {drawImageFormat};
pipelineConfig.depthAttachment = depthImageFormat;
// depth was already written by the pre-pass, only shade the visible surface
pipelineConfig.depthStencilInfo.depthWriteEnable = VK_FALSE;
pipelineConfig.depthStencilInfo.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
m_pipeline = std::make_unique<Pipeline>(
gfxDevice,
vertexShader.string(),
fragShader.string(),
pipelineConfig
);
}
} }
catch (...)
PipelineConfigInfo pipelineConfig{}; {
Pipeline::DefaultPipelineConfigInfo(pipelineConfig);
pipelineConfig.name = "Mesh Pipeline";
pipelineConfig.pipelineLayout = m_pipelineLayout;
pipelineConfig.vertexAttributeDescriptions = {};
pipelineConfig.vertexBindingDescriptions = {};
pipelineConfig.colorAttachments = {drawImageFormat};
pipelineConfig.depthAttachment = depthImageFormat;
m_pipeline = std::make_unique<Pipeline>(
gfxDevice,
vertexShader.string(),
fragShader.string(),
pipelineConfig
);
} catch (...) {
cleanup(gfxDevice.getDevice()); cleanup(gfxDevice.getDevice());
throw; throw;
} }
} }
void MeshPipeline::drawDepthPrepass(
VkCommandBuffer cmd,
VkExtent2D renderExtent,
const RenderResources& resources,
const Camera& camera,
const GPUBuffer& sceneDataBuffer,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands)
{
if (!m_depthPrepassPipeline)
{
return;
}
drawMeshes(
cmd,
*m_depthPrepassPipeline,
renderExtent,
resources,
camera,
sceneDataBuffer,
drawCommands,
sortedDrawCommands);
}
void MeshPipeline::draw( void MeshPipeline::draw(
VkCommandBuffer cmd, VkCommandBuffer cmd,
VkExtent2D renderExtent, VkExtent2D renderExtent,
@@ -92,13 +149,35 @@ void MeshPipeline::draw(
const std::vector<MeshDrawCommand>& drawCommands, const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands) const std::vector<std::size_t>& sortedDrawCommands)
{ {
if (!m_pipeline) { if (!m_pipeline)
{
return; return;
} }
drawMeshes(
cmd,
*m_pipeline,
renderExtent,
resources,
camera,
sceneDataBuffer,
drawCommands,
sortedDrawCommands);
}
void MeshPipeline::drawMeshes(
VkCommandBuffer cmd,
const Pipeline& pipeline,
VkExtent2D renderExtent,
const RenderResources& resources,
const Camera& camera,
const GPUBuffer& sceneDataBuffer,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands)
{
const MeshCache& meshCache = resources.meshes(); const MeshCache& meshCache = resources.meshes();
m_pipeline->bind(cmd); pipeline.bind(cmd);
resources.bindBindlessDescSet(cmd, m_pipelineLayout); resources.bindBindlessDescSet(cmd, m_pipelineLayout);
@@ -130,8 +209,10 @@ void MeshPipeline::draw(
const auto frustum = edge::createFrustumFromCamera(camera); const auto frustum = edge::createFrustumFromCamera(camera);
const auto drawOne = [&](const MeshDrawCommand& dc) { const auto drawOne = [&](const MeshDrawCommand& dc)
if (!edge::isInFrustum(frustum, dc.worldBoundingSphere)) { {
if (!edge::isInFrustum(frustum, dc.worldBoundingSphere))
{
return; return;
} }
@@ -139,7 +220,18 @@ void MeshPipeline::draw(
const auto& mesh = meshCache.getMesh(dc.meshId); const auto& mesh = meshCache.getMesh(dc.meshId);
if (dc.meshId != prevMeshId) { const std::string& meshKey = meshCache.getMeshKey(dc.meshId);
vkutil::cmdBeginLabelFormatted(
cmd,
vkutil::labelColors::draw,
"%s [mesh %zu, material %u%s]",
meshKey.empty() ? "Mesh" : meshKey.c_str(),
dc.meshId,
dc.materialId,
dc.skinnedMesh != nullptr ? ", skinned" : "");
if (dc.meshId != prevMeshId)
{
prevMeshId = dc.meshId; prevMeshId = dc.meshId;
vkCmdBindIndexBuffer( vkCmdBindIndexBuffer(
@@ -156,9 +248,9 @@ void MeshPipeline::draw(
.transform = dc.transformMatrix, .transform = dc.transformMatrix,
.sceneDataBuffer = sceneDataBuffer.address, .sceneDataBuffer = sceneDataBuffer.address,
.vertexBuffer = .vertexBuffer =
dc.skinnedMesh != nullptr dc.skinnedMesh != nullptr
? dc.skinnedMesh->skinnedVertexBuffer.address ? dc.skinnedMesh->skinnedVertexBuffer.address
: mesh.vertexBuffer.address, : mesh.vertexBuffer.address,
.materialId = dc.materialId, .materialId = dc.materialId,
.padding = 0, .padding = 0,
}; };
@@ -173,28 +265,34 @@ void MeshPipeline::draw(
); );
vkCmdDrawIndexed(cmd, mesh.numIndices, 1, 0, 0, 0); vkCmdDrawIndexed(cmd, mesh.numIndices, 1, 0, 0, 0);
vkutil::cmdEndLabel(cmd);
}; };
if (!sortedDrawCommands.empty()) { if (!sortedDrawCommands.empty())
for (std::size_t drawIndex : sortedDrawCommands) { {
for (std::size_t drawIndex : sortedDrawCommands)
{
assert(drawIndex < drawCommands.size()); assert(drawIndex < drawCommands.size());
drawOne(drawCommands[drawIndex]); drawOne(drawCommands[drawIndex]);
} }
} else { }
for (const MeshDrawCommand& dc : drawCommands) { else
{
for (const MeshDrawCommand& dc : drawCommands)
{
drawOne(dc); drawOne(dc);
} }
} }
// Optional:
// spdlog::debug("Actual mesh draw calls: {}", actualDrawCalls);
} }
void MeshPipeline::cleanup(VkDevice device) void MeshPipeline::cleanup(VkDevice device)
{ {
m_pipeline.reset(); m_pipeline.reset();
m_depthPrepassPipeline.reset();
if (m_pipelineLayout != VK_NULL_HANDLE && device != VK_NULL_HANDLE) { if (m_pipelineLayout != VK_NULL_HANDLE && device != VK_NULL_HANDLE)
{
vkDestroyPipelineLayout(device, m_pipelineLayout, nullptr); vkDestroyPipelineLayout(device, m_pipelineLayout, nullptr);
} }
m_pipelineLayout = VK_NULL_HANDLE; m_pipelineLayout = VK_NULL_HANDLE;
@@ -0,0 +1,338 @@
#include <destrum/Graphics/Pipelines/QuadRendererPass.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <limits>
#include <stdexcept>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <destrum/FS/AssetFS.h>
#include <destrum/Graphics/GfxDevice.h>
#include <destrum/Graphics/GPUImage.h>
#include <destrum/Graphics/Pipeline.h>
#include <destrum/Graphics/RenderResources.h>
#include <destrum/Graphics/Util.h>
#include <destrum/Graphics/Managers/QuadRenderingManager.h>
#include "volk.h"
namespace {
constexpr std::size_t InitialQuadCapacity = 256;
constexpr std::size_t VerticesPerQuad = 4;
constexpr std::size_t IndicesPerQuad = 6;
glm::vec2 rotatePoint(const glm::vec2& point, float cosA, float sinA) {
return glm::vec2{
point.x * cosA - point.y * sinA,
point.x * sinA + point.y * cosA,
};
}
}
void QuadRendererPass::init(GfxDevice& gfxDevice, RenderResources& resources, VkFormat drawImageFormat)
{
try {
const std::size_t vertexBytes = InitialQuadCapacity * VerticesPerQuad * sizeof(QuadVertex);
vertexBuffer = gfxDevice.createBuffer(
vertexBytes,
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
VMA_MEMORY_USAGE_AUTO_PREFER_HOST);
vkutil::addDebugLabel(gfxDevice.getDevice(), vertexBuffer.buffer, "quad vertices");
const std::size_t indexBytes = InitialQuadCapacity * IndicesPerQuad * sizeof(std::uint32_t);
indexBuffer = gfxDevice.createBuffer(
indexBytes,
VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
VMA_MEMORY_USAGE_AUTO_PREFER_HOST);
vkutil::addDebugLabel(gfxDevice.getDevice(), indexBuffer.buffer, "quad indices");
quadCapacity = InitialQuadCapacity;
const auto vertexShader =
AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/quad.vert");
const auto fragmentShader =
AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/quad.frag");
const auto pushConstantRange = VkPushConstantRange{
.stageFlags = VK_SHADER_STAGE_VERTEX_BIT,
.offset = 0,
.size = sizeof(glm::mat4),
};
const auto layouts = std::array{
resources.getBindlessDescSetLayout()
};
const auto pipelineLayoutInfo = VkPipelineLayoutCreateInfo{
.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
.setLayoutCount = static_cast<std::uint32_t>(layouts.size()),
.pSetLayouts = layouts.data(),
.pushConstantRangeCount = 1,
.pPushConstantRanges = &pushConstantRange,
};
VK_CHECK(vkCreatePipelineLayout(
gfxDevice.getDevice(),
&pipelineLayoutInfo,
nullptr,
&pipelineLayout));
PipelineConfigInfo pipelineConfig{};
Pipeline::DefaultPipelineConfigInfo(pipelineConfig);
pipelineConfig.name = "Quad Rendering Pipeline";
pipelineConfig.pipelineLayout = pipelineLayout;
pipelineConfig.inputAssemblyInfo.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
pipelineConfig.rasterizationInfo.cullMode = VK_CULL_MODE_NONE;
pipelineConfig.depthStencilInfo.depthTestEnable = VK_FALSE;
pipelineConfig.depthStencilInfo.depthWriteEnable = VK_FALSE;
pipelineConfig.depthStencilInfo.depthCompareOp = VK_COMPARE_OP_ALWAYS;
pipelineConfig.colorAttachments = {drawImageFormat};
pipelineConfig.depthAttachment = VK_FORMAT_UNDEFINED;
pipelineConfig.vertexBindingDescriptions = {
VkVertexInputBindingDescription{
.binding = 0,
.stride = sizeof(QuadVertex),
.inputRate = VK_VERTEX_INPUT_RATE_VERTEX,
}
};
pipelineConfig.vertexAttributeDescriptions = {
VkVertexInputAttributeDescription{
.location = 0,
.binding = 0,
.format = VK_FORMAT_R32G32_SFLOAT,
.offset = offsetof(QuadVertex, position),
},
VkVertexInputAttributeDescription{
.location = 1,
.binding = 0,
.format = VK_FORMAT_R32G32_SFLOAT,
.offset = offsetof(QuadVertex, uv),
},
VkVertexInputAttributeDescription{
.location = 2,
.binding = 0,
.format = VK_FORMAT_R32G32B32A32_SFLOAT,
.offset = offsetof(QuadVertex, color),
},
VkVertexInputAttributeDescription{
.location = 3,
.binding = 0,
.format = VK_FORMAT_R32_UINT,
.offset = offsetof(QuadVertex, textureId),
},
};
pipelineConfig.colorBlendAttachment.blendEnable = VK_TRUE;
pipelineConfig.colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
pipelineConfig.colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
pipelineConfig.colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD;
pipelineConfig.colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
pipelineConfig.colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
pipelineConfig.colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;
pipeline = std::make_unique<Pipeline>(
gfxDevice,
vertexShader.string(),
fragmentShader.string(),
pipelineConfig);
} catch (...) {
cleanup(gfxDevice);
throw;
}
}
void QuadRendererPass::draw(
VkCommandBuffer cmd,
GfxDevice& gfxDevice,
RenderResources& resources,
const GPUImage& target,
const glm::mat4& projection)
{
auto& quadManager = QuadRenderingManager::GetInstance();
const auto& quads = quadManager.GetQuads();
if (!pipeline || quads.empty()) {
quadManager.ClearQuads();
return;
}
const std::size_t numQuads = quads.size();
if (numQuads > std::numeric_limits<std::size_t>::max() / VerticesPerQuad) {
throw std::overflow_error("Too many quads submitted for rendering");
}
const std::size_t numVertices = numQuads * VerticesPerQuad;
const std::size_t numIndices = numQuads * IndicesPerQuad;
if (numVertices > std::numeric_limits<std::uint32_t>::max()) {
throw std::overflow_error("Too many quad vertices submitted for rendering");
}
ensureCapacity(gfxDevice, numQuads);
const ImageID whiteTextureId = resources.getWhiteTextureID();
auto* vertexMapped = reinterpret_cast<std::uint8_t*>(vertexBuffer.info.pMappedData);
auto* indexMapped = reinterpret_cast<std::uint8_t*>(indexBuffer.info.pMappedData);
constexpr std::array<glm::vec2, 4> localCorners = {{
glm::vec2{-0.5f, -0.5f},
glm::vec2{ 0.5f, -0.5f},
glm::vec2{ 0.5f, 0.5f},
glm::vec2{-0.5f, 0.5f},
}};
constexpr std::array<glm::vec2, 4> uvs = {{
glm::vec2{0.0f, 1.0f},
glm::vec2{1.0f, 1.0f},
glm::vec2{1.0f, 0.0f},
glm::vec2{0.0f, 0.0f},
}};
auto* vertWrite = reinterpret_cast<QuadVertex*>(vertexMapped);
auto* idxWrite = reinterpret_cast<std::uint32_t*>(indexMapped);
std::uint32_t vertexBase = 0;
for (const auto& quad : quads) {
const float cosA = std::cos(quad.rotation);
const float sinA = std::sin(quad.rotation);
const glm::vec2 halfSize = quad.size * 0.5f;
const ImageID texId = (quad.textureId == NULL_IMAGE_ID) ? whiteTextureId : quad.textureId;
for (std::size_t v = 0; v < VerticesPerQuad; ++v) {
glm::vec2 localPos = localCorners[v] * halfSize;
glm::vec2 rotatedPos = rotatePoint(localPos, cosA, sinA);
glm::vec2 worldPos = quad.position + rotatedPos;
vertWrite[vertexBase + v] = QuadVertex{
.position = worldPos,
.uv = uvs[v],
.color = quad.color,
.textureId = static_cast<std::uint32_t>(texId),
};
}
const std::uint32_t idxOffset = vertexBase / 4 * 6;
idxWrite[idxOffset + 0] = vertexBase + 0;
idxWrite[idxOffset + 1] = vertexBase + 1;
idxWrite[idxOffset + 2] = vertexBase + 2;
idxWrite[idxOffset + 3] = vertexBase + 0;
idxWrite[idxOffset + 4] = vertexBase + 2;
idxWrite[idxOffset + 5] = vertexBase + 3;
vertexBase += VerticesPerQuad;
}
auto& memMgr = gfxDevice.getMemoryManager();
memMgr.flushAllocation(vertexBuffer, 0, numVertices * sizeof(QuadVertex));
memMgr.flushAllocation(indexBuffer, 0, numIndices * sizeof(std::uint32_t));
auto renderInfo = vkutil::createRenderingInfo({
.renderExtent = target.getExtent2D(),
.colorImageView = target.imageView,
});
vkCmdBeginRendering(cmd, &renderInfo.renderingInfo);
pipeline->bind(cmd);
resources.bindBindlessDescSet(cmd, pipelineLayout);
const auto viewport = VkViewport{
.x = 0.0f,
.y = 0.0f,
.width = static_cast<float>(target.extent.width),
.height = static_cast<float>(target.extent.height),
.minDepth = 0.0f,
.maxDepth = 1.0f,
};
vkCmdSetViewport(cmd, 0, 1, &viewport);
const auto scissor = VkRect2D{
.offset = {},
.extent = target.getExtent2D(),
};
vkCmdSetScissor(cmd, 0, 1, &scissor);
vkCmdSetPolygonModeEXT(cmd, VK_POLYGON_MODE_FILL);
vkCmdPushConstants(
cmd,
pipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT,
0,
sizeof(glm::mat4),
&projection);
const VkBuffer vertexBufferHandle = vertexBuffer.buffer;
const VkDeviceSize vertexBufferOffset = 0;
vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBufferHandle, &vertexBufferOffset);
const VkBuffer indexBufferHandle = indexBuffer.buffer;
vkCmdBindIndexBuffer(cmd, indexBufferHandle, 0, VK_INDEX_TYPE_UINT32);
vkCmdDrawIndexed(cmd, static_cast<std::uint32_t>(numIndices), 1, 0, 0, 0);
vkCmdEndRendering(cmd);
quadManager.ClearQuads();
}
void QuadRendererPass::cleanup(GfxDevice& gfxDevice)
{
pipeline.reset();
if (pipelineLayout != VK_NULL_HANDLE && gfxDevice.getDevice() != VK_NULL_HANDLE) {
vkDestroyPipelineLayout(gfxDevice.getDevice(), pipelineLayout, nullptr);
}
pipelineLayout = VK_NULL_HANDLE;
if (vertexBuffer.buffer != VK_NULL_HANDLE) {
gfxDevice.destroyBuffer(vertexBuffer);
}
if (indexBuffer.buffer != VK_NULL_HANDLE) {
gfxDevice.destroyBuffer(indexBuffer);
}
quadCapacity = 0;
}
void QuadRendererPass::ensureCapacity(
GfxDevice& gfxDevice,
std::size_t requiredQuads)
{
if (requiredQuads <= quadCapacity) {
return;
}
std::size_t newCapacity = std::max(quadCapacity, InitialQuadCapacity);
while (newCapacity < requiredQuads) {
if (newCapacity > std::numeric_limits<std::size_t>::max() / 2) {
newCapacity = requiredQuads;
break;
}
newCapacity *= 2;
}
gfxDevice.waitIdle();
if (vertexBuffer.buffer != VK_NULL_HANDLE) {
gfxDevice.destroyBuffer(vertexBuffer);
}
vertexBuffer = gfxDevice.createBuffer(
newCapacity * VerticesPerQuad * sizeof(QuadVertex),
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
VMA_MEMORY_USAGE_AUTO_PREFER_HOST);
vkutil::addDebugLabel(gfxDevice.getDevice(), vertexBuffer.buffer, "quad vertices");
if (indexBuffer.buffer != VK_NULL_HANDLE) {
gfxDevice.destroyBuffer(indexBuffer);
}
indexBuffer = gfxDevice.createBuffer(
newCapacity * IndicesPerQuad * sizeof(std::uint32_t),
VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
VMA_MEMORY_USAGE_AUTO_PREFER_HOST);
vkutil::addDebugLabel(gfxDevice.getDevice(), indexBuffer.buffer, "quad indices");
quadCapacity = newCapacity;
}
@@ -0,0 +1,265 @@
#include <destrum/Graphics/Pipelines/ShadowMapPipeline.h>
#include <array>
#include <cassert>
#include <cmath>
#include <stdexcept>
#include <glm/gtc/matrix_transform.hpp>
#include <destrum/FS/AssetFS.h>
#include <destrum/Graphics/GfxDevice.h>
#include <destrum/Graphics/RenderResources.h>
#include <destrum/Graphics/Caches/MeshCache.h>
#include <destrum/Graphics/Util.h>
#include "volk.h"
#include "spdlog/spdlog.h"
void ShadowMapPipeline::init(GfxDevice& gfxDevice, RenderResources& resources)
{
try
{
const auto vertexShader =
AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/shadow.vert");
const auto bufferRange = VkPushConstantRange{
.stageFlags = VK_SHADER_STAGE_VERTEX_BIT,
.offset = 0,
.size = sizeof(PushConstants),
};
const auto pushConstantRanges = std::array{bufferRange};
const auto layouts = std::array{
resources.getBindlessDescSetLayout()
};
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = static_cast<std::uint32_t>(layouts.size());
pipelineLayoutInfo.pSetLayouts = layouts.data();
pipelineLayoutInfo.pushConstantRangeCount = static_cast<std::uint32_t>(pushConstantRanges.size());
pipelineLayoutInfo.pPushConstantRanges = pushConstantRanges.data();
if (vkCreatePipelineLayout(
gfxDevice.getDevice(),
&pipelineLayoutInfo,
nullptr,
&m_pipelineLayout) != VK_SUCCESS)
{
throw std::runtime_error("Could not create shadow map pipeline layout");
}
PipelineConfigInfo pipelineConfig{};
Pipeline::DefaultPipelineConfigInfo(pipelineConfig);
pipelineConfig.name = "Shadow Map Pipeline";
pipelineConfig.pipelineLayout = m_pipelineLayout;
pipelineConfig.vertexAttributeDescriptions = {};
pipelineConfig.vertexBindingDescriptions = {};
pipelineConfig.colorAttachments = {};
pipelineConfig.colorBlendInfo.attachmentCount = 0;
pipelineConfig.depthAttachment = shadowMapFormat;
// the light projection isn't Y flipped like the camera's, so the winding is mirrored
pipelineConfig.rasterizationInfo.cullMode = VK_CULL_MODE_NONE;
pipelineConfig.rasterizationInfo.depthBiasEnable = VK_TRUE;
pipelineConfig.rasterizationInfo.depthBiasConstantFactor = 0.0f;
pipelineConfig.rasterizationInfo.depthBiasSlopeFactor = 2.0f;
m_pipeline = std::make_unique<Pipeline>(
gfxDevice,
vertexShader.string(),
"",
pipelineConfig
);
const auto createInfo = vkutil::CreateImageInfo{
.format = shadowMapFormat,
.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
.extent = VkExtent3D{SHADOW_MAP_SIZE, SHADOW_MAP_SIZE, 1},
};
shadowMapId = resources.createImage(gfxDevice, createInfo, "shadow map", nullptr, shadowMapId);
spdlog::info("Created {0}x{0} shadow map with id {1}", SHADOW_MAP_SIZE, shadowMapId);
}
catch (...)
{
cleanup(gfxDevice.getDevice());
throw;
}
}
void ShadowMapPipeline::cleanup(VkDevice device)
{
m_pipeline.reset();
if (m_pipelineLayout != VK_NULL_HANDLE && device != VK_NULL_HANDLE)
{
vkDestroyPipelineLayout(device, m_pipelineLayout, nullptr);
}
m_pipelineLayout = VK_NULL_HANDLE;
shadowMapId = NULL_IMAGE_ID;
}
glm::mat4 ShadowMapPipeline::calculateLightViewProj(const glm::vec3& lightDirection, const glm::vec3& center) const
{
const glm::vec3 direction = glm::normalize(lightDirection);
const glm::vec3 up = std::abs(direction.y) > 0.99f ? glm::vec3{0.0f, 0.0f, 1.0f} : glm::vec3{0.0f, 1.0f, 0.0f};
const glm::mat4 lightView = glm::lookAt(glm::vec3{0.0f}, direction, up);
const float texelSize = (2.0f * shadowDistance) / static_cast<float>(SHADOW_MAP_SIZE);
glm::vec3 lightSpaceCenter = glm::vec3(lightView * glm::vec4(center, 1.0f));
lightSpaceCenter.x = std::floor(lightSpaceCenter.x / texelSize) * texelSize;
lightSpaceCenter.y = std::floor(lightSpaceCenter.y / texelSize) * texelSize;
const glm::mat4 lightProj = glm::orthoRH_ZO(
lightSpaceCenter.x - shadowDistance,
lightSpaceCenter.x + shadowDistance,
lightSpaceCenter.y - shadowDistance,
lightSpaceCenter.y + shadowDistance,
-lightSpaceCenter.z - shadowDepthRange,
-lightSpaceCenter.z + shadowDepthRange);
return lightProj * lightView;
}
float ShadowMapPipeline::getNormalOffset() const
{
const float texelSize = (2.0f * shadowDistance) / static_cast<float>(SHADOW_MAP_SIZE);
return texelSize * 1.5f;
}
void ShadowMapPipeline::draw(
VkCommandBuffer cmd,
const RenderResources& resources,
const GPUBuffer& sceneDataBuffer,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands)
{
if (!m_pipeline || shadowMapId == NULL_IMAGE_ID)
{
return;
}
const auto& shadowMap = resources.getImage(shadowMapId);
vkutil::transitionImage(
cmd,
shadowMap.image,
shadowMap.layout,
VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL);
shadowMap.setLayout(VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL);
auto renderInfo = vkutil::createRenderingInfo({
.renderExtent = shadowMap.getExtent2D(),
.depthImageView = shadowMap.imageView,
.depthImageClearValue = 1.f,
});
renderInfo.renderingInfo.pDepthAttachment = &renderInfo.depthAttachment;
vkCmdBeginRendering(cmd, &renderInfo.renderingInfo);
m_pipeline->bind(cmd);
resources.bindBindlessDescSet(cmd, m_pipelineLayout);
const auto viewport = VkViewport{
.x = 0.f,
.y = 0.f,
.width = static_cast<float>(SHADOW_MAP_SIZE),
.height = static_cast<float>(SHADOW_MAP_SIZE),
.minDepth = 0.f,
.maxDepth = 1.f,
};
vkCmdSetViewport(cmd, 0, 1, &viewport);
const auto scissor = VkRect2D{
.offset = {},
.extent = shadowMap.getExtent2D(),
};
vkCmdSetScissor(cmd, 0, 1, &scissor);
vkCmdSetPolygonModeEXT(cmd, VK_POLYGON_MODE_FILL);
const MeshCache& meshCache = resources.meshes();
MeshID prevMeshId = NULL_MESH_ID;
const auto drawOne = [&](const MeshDrawCommand& dc)
{
if (!dc.castShadow)
{
return;
}
const auto& mesh = meshCache.getMesh(dc.meshId);
const std::string& meshKey = meshCache.getMeshKey(dc.meshId);
vkutil::cmdBeginLabelFormatted(
cmd,
vkutil::labelColors::draw,
"%s [mesh %zu, material %u%s]",
meshKey.empty() ? "Mesh" : meshKey.c_str(),
dc.meshId,
dc.materialId,
dc.skinnedMesh != nullptr ? ", skinned" : "");
if (dc.meshId != prevMeshId)
{
prevMeshId = dc.meshId;
vkCmdBindIndexBuffer(cmd, mesh.indexBuffer.buffer, 0, VK_INDEX_TYPE_UINT32);
}
const auto pushConstants = PushConstants{
.transform = dc.transformMatrix,
.sceneDataBuffer = sceneDataBuffer.address,
.vertexBuffer =
dc.skinnedMesh != nullptr
? dc.skinnedMesh->skinnedVertexBuffer.address
: mesh.vertexBuffer.address,
.materialId = dc.materialId,
.padding = 0,
};
vkCmdPushConstants(
cmd,
m_pipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT,
0,
sizeof(PushConstants),
&pushConstants);
vkCmdDrawIndexed(cmd, mesh.numIndices, 1, 0, 0, 0);
vkutil::cmdEndLabel(cmd);
};
if (!sortedDrawCommands.empty())
{
for (std::size_t drawIndex : sortedDrawCommands)
{
assert(drawIndex < drawCommands.size());
drawOne(drawCommands[drawIndex]);
}
}
else
{
for (const MeshDrawCommand& dc : drawCommands)
{
drawOne(dc);
}
}
vkCmdEndRendering(cmd);
vkutil::transitionImage(
cmd,
shadowMap.image,
shadowMap.layout,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
shadowMap.setLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
}
@@ -53,6 +53,9 @@ void SkinningPipeline::init(GfxDevice& gfxDevice) {
MAX_JOINT_MATRICES * sizeof(glm::mat4), MAX_JOINT_MATRICES * sizeof(glm::mat4),
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU); VMA_MEMORY_USAGE_CPU_TO_GPU);
const std::string bufferName = "joint matrices (frame " + std::to_string(i) + ")";
vkutil::addDebugLabel(device, jointMatricesBuffer.buffer.buffer, bufferName.c_str());
} }
} catch (...) { } catch (...) {
cleanup(gfxDevice); cleanup(gfxDevice);
@@ -84,6 +87,9 @@ void SkinningPipeline::doSkinning(VkCommandBuffer cmd,
assert(mesh.hasSkeleton); assert(mesh.hasSkeleton);
assert(dc.skinnedMesh); assert(dc.skinnedMesh);
const std::string& meshKey = meshCache.getMeshKey(dc.meshId);
vkutil::ScopedCmdLabel label(cmd, meshKey.empty() ? "Skin Mesh" : meshKey.c_str(), vkutil::labelColors::compute);
const auto cs = PushConstants{ const auto cs = PushConstants{
.jointMatricesBuffer = getCurrentFrameData(frameIndex).jointMatricesBuffer.buffer.address, .jointMatricesBuffer = getCurrentFrameData(frameIndex).jointMatricesBuffer.buffer.address,
.jointMatricesStartIndex = static_cast<std::uint32_t>(dc.jointMatricesStartIndex), .jointMatricesStartIndex = static_cast<std::uint32_t>(dc.jointMatricesStartIndex),
@@ -119,14 +119,16 @@ void SkyboxPipeline::draw(
const glm::mat3 r = glm::mat3(skyboxRotation); const glm::mat3 r = glm::mat3(skyboxRotation);
// rotation only, translation in the inverse makes the sky jitter far from the origin
const glm::mat4 viewRotation = glm::mat4(glm::mat3(camera.GetViewMatrix()));
const auto pcs = SkyboxPushConstants{ const auto pcs = SkyboxPushConstants{
.invViewProj = glm::inverse(camera.GetViewProjectionMatrix()), .invViewProj = glm::inverse(camera.GetProjectionMatrix() * viewRotation),
.skyboxRot = { .skyboxRot = {
glm::vec4(r[0], 0.f), glm::vec4(r[0], 0.f),
glm::vec4(r[1], 0.f), glm::vec4(r[1], 0.f),
glm::vec4(r[2], 0.f) glm::vec4(r[2], 0.f)
}, },
.cameraPos = camera.GetPosition(),
.skyboxTextureId = static_cast<std::uint32_t>(skyboxTextureId), .skyboxTextureId = static_cast<std::uint32_t>(skyboxTextureId),
}; };
+7 -2
View File
@@ -130,6 +130,8 @@ ImageID RenderResources::createImage(
if (!debugName.empty()) { if (!debugName.empty()) {
vkutil::addDebugLabel(gfxDevice.getVkDevice(), image.image, debugName.c_str()); vkutil::addDebugLabel(gfxDevice.getVkDevice(), image.image, debugName.c_str());
const std::string viewName = debugName + " (view)";
vkutil::addDebugLabel(gfxDevice.getVkDevice(), image.imageView, viewName.c_str());
image.debugName = debugName; image.debugName = debugName;
} }
@@ -266,10 +268,13 @@ const VkDescriptorSet& RenderResources::getBindlessDescSet() const {
return imageCache->bindlessSetManager.getDescSet(); return imageCache->bindlessSetManager.getDescSet();
} }
void RenderResources::bindBindlessDescSet(VkCommandBuffer cmd, VkPipelineLayout layout) const { void RenderResources::bindBindlessDescSet(
VkCommandBuffer cmd,
VkPipelineLayout layout,
VkPipelineBindPoint bindPoint) const {
vkCmdBindDescriptorSets( vkCmdBindDescriptorSets(
cmd, cmd,
VK_PIPELINE_BIND_POINT_GRAPHICS, bindPoint,
layout, layout,
0, 0,
1, 1,
+201 -9
View File
@@ -1,10 +1,14 @@
#include <destrum/Graphics/Renderer.h> #include <destrum/Graphics/Renderer.h>
#include <destrum/Graphics/Managers/LineRenderingManager.h>
#include <destrum/Graphics/Managers/QuadRenderingManager.h>
#include <destrum/Graphics/Util.h> #include <destrum/Graphics/Util.h>
#include <algorithm> #include <algorithm>
#include <numeric> #include <numeric>
#include <glm/gtc/matrix_transform.hpp>
#include "volk.h" #include "volk.h"
#include "destrum/Util/GameState.h" #include "destrum/Util/GameState.h"
#include "spdlog/spdlog.h" #include "spdlog/spdlog.h"
@@ -17,7 +21,8 @@ GameRenderer::GameRenderer()
void GameRenderer::init(GfxDevice& gfxDevice, RenderResources& _resources, glm::ivec2 drawImageSize) void GameRenderer::init(GfxDevice& gfxDevice, RenderResources& _resources, glm::ivec2 drawImageSize)
{ {
try { try
{
resources = &_resources; resources = &_resources;
sceneDataBuffer.init( sceneDataBuffer.init(
gfxDevice, gfxDevice,
@@ -36,9 +41,23 @@ void GameRenderer::init(GfxDevice& gfxDevice, RenderResources& _resources, glm::
skinningPipeline = std::make_unique<SkinningPipeline>(); skinningPipeline = std::make_unique<SkinningPipeline>();
skinningPipeline->init(gfxDevice); skinningPipeline->init(gfxDevice);
lightCullingPipeline = std::make_unique<LightCullingPipeline>();
lightCullingPipeline->init(gfxDevice, _resources, drawImageSize);
shadowMapPipeline = std::make_unique<ShadowMapPipeline>();
shadowMapPipeline->init(gfxDevice, _resources);
lineRenderingPass = std::make_unique<LineRenderingPass>();
lineRenderingPass->init(gfxDevice, drawImageFormat);
quadRendererPass = std::make_unique<QuadRendererPass>();
quadRendererPass->init(gfxDevice, _resources, drawImageFormat);
GameState::GetInstance().SetRenderer(this); GameState::GetInstance().SetRenderer(this);
initialized = true; initialized = true;
} catch (...) { }
catch (...)
{
cleanup(gfxDevice); cleanup(gfxDevice);
throw; throw;
} }
@@ -49,6 +68,9 @@ void GameRenderer::beginDrawing(GfxDevice& gfxDevice)
flushMaterialUpdates(gfxDevice); flushMaterialUpdates(gfxDevice);
meshDrawCommands.clear(); meshDrawCommands.clear();
skinningPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex()); skinningPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex());
lightCullingPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex());
shadowLightDirection.reset();
QuadRenderingManager::GetInstance().ClearQuads();
} }
void GameRenderer::endDrawing() void GameRenderer::endDrawing()
@@ -59,13 +81,16 @@ void GameRenderer::endDrawing()
std::stable_sort( std::stable_sort(
sortedMeshDrawCommands.begin(), sortedMeshDrawCommands.begin(),
sortedMeshDrawCommands.end(), sortedMeshDrawCommands.end(),
[this](std::size_t left, std::size_t right) { [this](std::size_t left, std::size_t right)
{
const auto& lhs = meshDrawCommands[left]; const auto& lhs = meshDrawCommands[left];
const auto& rhs = meshDrawCommands[right]; const auto& rhs = meshDrawCommands[right];
if (lhs.meshId != rhs.meshId) { if (lhs.meshId != rhs.meshId)
{
return lhs.meshId < rhs.meshId; return lhs.meshId < rhs.meshId;
} }
if (lhs.materialId != rhs.materialId) { if (lhs.materialId != rhs.materialId)
{
return lhs.materialId < rhs.materialId; return lhs.materialId < rhs.materialId;
} }
return (lhs.skinnedMesh != nullptr) < (rhs.skinnedMesh != nullptr); return (lhs.skinnedMesh != nullptr) < (rhs.skinnedMesh != nullptr);
@@ -76,9 +101,11 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
{ {
ZoneScopedN("GameRenderer::draw"); ZoneScopedN("GameRenderer::draw");
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "GameRenderer::draw"); TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "GameRenderer::draw");
vkutil::ScopedCmdLabel frameLabel(cmd, "GameRenderer::draw", vkutil::labelColors::frame);
{ {
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Skinning"); TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Skinning");
vkutil::ScopedCmdLabel label(cmd, "Skinning", vkutil::labelColors::compute);
skinningPipeline->flushCurrentFrame( skinningPipeline->flushCurrentFrame(
cmd, cmd,
@@ -95,9 +122,24 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
} }
} }
{
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Lights Buffer Flush");
vkutil::ScopedCmdLabel label(cmd, "Lights Buffer Flush", vkutil::labelColors::transfer);
lightCullingPipeline->flushCurrentFrame(cmd, gfxDevice);
}
const glm::mat4 lightViewProj = shadowLightDirection
? shadowMapPipeline->calculateLightViewProj(
*shadowLightDirection,
sceneData.camera.GetPosition())
: glm::mat4{1.0f};
// TODO: there has to be a better way to do this
const auto gpuSceneData = GPUSceneData{ const auto gpuSceneData = GPUSceneData{
.view = sceneData.camera.GetViewMatrix(), .view = sceneData.camera.GetViewMatrix(),
.proj = sceneData.camera.GetProjectionMatrix(), .proj = sceneData.camera.GetProjectionMatrix(),
.invProj = glm::inverse(sceneData.camera.GetProjectionMatrix()),
.viewProj = sceneData.camera.GetViewProjectionMatrix(), .viewProj = sceneData.camera.GetViewProjectionMatrix(),
.cameraPos = glm::vec4{sceneData.camera.m_position, 1.f}, .cameraPos = glm::vec4{sceneData.camera.m_position, 1.f},
.ambientColor = {sceneData.ambientColor}, .ambientColor = {sceneData.ambientColor},
@@ -105,10 +147,20 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
.fogColor = {sceneData.fogColor}, .fogColor = {sceneData.fogColor},
.fogDensity = sceneData.fogDensity, .fogDensity = sceneData.fogDensity,
.materialsBuffer = resources->materials().getMaterialDataBufferAddress(), .materialsBuffer = resources->materials().getMaterialDataBufferAddress(),
.lightsBuffer = lightCullingPipeline->getLightsBufferAddress(),
.tileLightIndicesBuffer = lightCullingPipeline->getTileLightIndicesBufferAddress(),
.numLights = lightCullingPipeline->getNumLights(),
.tileCountX = lightCullingPipeline->getTileCount().x,
.debugLightHeatmap = renderLightHeatmap ? 1u : 0u,
.shadowMapId = static_cast<std::uint32_t>(shadowMapPipeline->getShadowMapId()),
.shadowNormalOffset = shadowMapPipeline->getNormalOffset(),
.padding = 0,
.lightViewProj = lightViewProj,
}; };
{ {
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Material Buffer Barrier"); TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Material Buffer Barrier");
vkutil::ScopedCmdLabel label(cmd, "Material Buffer Barrier", vkutil::labelColors::transfer);
vkutil::bufferHostWriteToShaderReadBarrier( vkutil::bufferHostWriteToShaderReadBarrier(
cmd, cmd,
@@ -120,6 +172,7 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
{ {
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Upload Scene Data"); TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Upload Scene Data");
vkutil::ScopedCmdLabel label(cmd, "Upload Scene Data", vkutil::labelColors::transfer);
sceneDataBuffer.uploadNewData( sceneDataBuffer.uploadNewData(
cmd, cmd,
@@ -154,16 +207,81 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
depthImage.setLayout(VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL); depthImage.setLayout(VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL);
} }
if (shadowLightDirection)
{
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Shadow Map");
vkutil::ScopedCmdLabel label(cmd, "Shadow Map", vkutil::labelColors::shadow);
shadowMapPipeline->draw(
cmd,
*resources,
sceneDataBuffer.getBuffer(),
meshDrawCommands,
sortedMeshDrawCommands);
}
{
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Depth Prepass");
vkutil::ScopedCmdLabel label(cmd, "Depth Prepass", vkutil::labelColors::depth);
auto depthPrepassInfo = vkutil::createRenderingInfo({
.renderExtent = drawImage.getExtent2D(),
.depthImageView = depthImage.imageView,
.depthImageClearValue = 1.f,
});
depthPrepassInfo.renderingInfo.pDepthAttachment = &depthPrepassInfo.depthAttachment;
vkCmdBeginRendering(cmd, &depthPrepassInfo.renderingInfo);
meshPipeline->drawDepthPrepass(
cmd,
drawImage.getExtent2D(),
*resources,
camera,
sceneDataBuffer.getBuffer(),
meshDrawCommands,
sortedMeshDrawCommands);
vkCmdEndRendering(cmd);
}
{
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Light Culling");
vkutil::ScopedCmdLabel label(cmd, "Light Culling", vkutil::labelColors::compute);
vkutil::transitionImage(
cmd,
depthImage.image,
depthImage.layout,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
depthImage.setLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
lightCullingPipeline->cullLights(
cmd,
*resources,
sceneDataBuffer.getBuffer(),
depthImageId,
drawImage.getExtent2D());
vkutil::transitionImage(
cmd,
depthImage.image,
depthImage.layout,
VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL);
depthImage.setLayout(VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL);
}
auto renderInfo = vkutil::createRenderingInfo({ auto renderInfo = vkutil::createRenderingInfo({
.renderExtent = drawImage.getExtent2D(), .renderExtent = drawImage.getExtent2D(),
.colorImageView = drawImage.imageView, .colorImageView = drawImage.imageView,
.colorImageClearValue = glm::vec4{0.f, 0.f, 0.f, 1.f}, .colorImageClearValue = glm::vec4{0.f, 0.f, 0.f, 1.f},
.depthImageView = depthImage.imageView, .depthImageView = depthImage.imageView,
.depthImageClearValue = 1.f,
}); });
renderInfo.renderingInfo.pColorAttachments = &renderInfo.colorAttachment; renderInfo.renderingInfo.pColorAttachments = &renderInfo.colorAttachment;
renderInfo.renderingInfo.pDepthAttachment = &renderInfo.depthAttachment; renderInfo.renderingInfo.pDepthAttachment = &renderInfo.depthAttachment;
vkutil::cmdBeginLabel(cmd, "Forward Pass", vkutil::labelColors::opaque);
{ {
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "vkCmdBeginRendering"); TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "vkCmdBeginRendering");
@@ -172,6 +290,7 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
{ {
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "MeshPipeline::draw"); TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "MeshPipeline::draw");
vkutil::ScopedCmdLabel label(cmd, "MeshPipeline::draw", vkutil::labelColors::opaque);
meshPipeline->draw( meshPipeline->draw(
cmd, cmd,
@@ -185,6 +304,7 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
{ {
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "SkyboxPipeline::draw"); TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "SkyboxPipeline::draw");
vkutil::ScopedCmdLabel label(cmd, "SkyboxPipeline::draw", vkutil::labelColors::sky);
skyboxPipeline->draw(cmd, *resources, camera); skyboxPipeline->draw(cmd, *resources, camera);
} }
@@ -194,20 +314,70 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
vkCmdEndRendering(cmd); vkCmdEndRendering(cmd);
} }
// vkutil::cmdEndLabel(cmd);
vkutil::cmdEndLabel(cmd);
{
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "LineRenderingPass::draw");
vkutil::ScopedCmdLabel label(cmd, "LineRenderingPass::draw", vkutil::labelColors::debug);
lineRenderingPass->draw(
cmd,
gfxDevice,
drawImage,
camera,
LineRenderingManager::GetInstance());
}
{
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "QuadRendererPass::draw");
vkutil::ScopedCmdLabel label(cmd, "QuadRendererPass::draw", vkutil::labelColors::ui);
quadRendererPass->draw(
cmd,
gfxDevice,
*resources,
drawImage,
glm::ortho(0.0f,
static_cast<float>(drawImage.extent.width),
static_cast<float>(drawImage.extent.height),
0.0f, -1.0f, 1.0f));
}
}
void GameRenderer::drawQuads(
VkCommandBuffer cmd,
GfxDevice& gfxDevice,
const glm::mat4& projection)
{
const auto& drawImage = resources->getImage(drawImageId);
quadRendererPass->draw(cmd, gfxDevice, *resources, drawImage, projection);
} }
void GameRenderer::cleanup(GfxDevice& gfxDevice) void GameRenderer::cleanup(GfxDevice& gfxDevice)
{ {
VkDevice device = gfxDevice.getDevice(); VkDevice device = gfxDevice.getDevice();
if (device != VK_NULL_HANDLE) { if (device != VK_NULL_HANDLE)
{
vkDeviceWaitIdle(device); vkDeviceWaitIdle(device);
} }
if (skinningPipeline) if (skinningPipeline)
skinningPipeline->cleanup(gfxDevice); skinningPipeline->cleanup(gfxDevice);
if (lineRenderingPass)
lineRenderingPass->cleanup(gfxDevice);
if (quadRendererPass)
quadRendererPass->cleanup(gfxDevice);
if (lightCullingPipeline)
lightCullingPipeline->cleanup(gfxDevice);
if (shadowMapPipeline)
shadowMapPipeline->cleanup(device);
if (skyboxPipeline) if (skyboxPipeline)
skyboxPipeline->cleanup(device); skyboxPipeline->cleanup(device);
@@ -228,6 +398,11 @@ void GameRenderer::cleanup(GfxDevice& gfxDevice)
meshPipeline.reset(); meshPipeline.reset();
skyboxPipeline.reset(); skyboxPipeline.reset();
skinningPipeline.reset(); skinningPipeline.reset();
lineRenderingPass.reset();
quadRendererPass.reset();
lightCullingPipeline.reset();
shadowMapPipeline.reset();
shadowLightDirection.reset();
pendingMaterialUploads.clear(); pendingMaterialUploads.clear();
meshDrawCommands.clear(); meshDrawCommands.clear();
sortedMeshDrawCommands.clear(); sortedMeshDrawCommands.clear();
@@ -270,6 +445,22 @@ void GameRenderer::drawSkinnedMesh(MeshID id,
}); });
} }
void GameRenderer::submitLight(const Light& light)
{
Light submittedLight = light;
// there's a single shadow map, the first shadow casting directional light gets it
submittedLight.castShadows =
light.castShadows && light.type == LightType::Directional && !shadowLightDirection;
if (submittedLight.castShadows)
{
shadowLightDirection = light.direction;
}
lightCullingPipeline->appendLight(submittedLight);
}
const GPUImage& GameRenderer::getDrawImage() const const GPUImage& GameRenderer::getDrawImage() const
{ {
assert(resources); assert(resources);
@@ -296,7 +487,8 @@ void GameRenderer::setSkyboxTexture(ImageID skyboxImageId)
void GameRenderer::flushMaterialUpdates(GfxDevice& gfxDevice) void GameRenderer::flushMaterialUpdates(GfxDevice& gfxDevice)
{ {
if (!pendingMaterialUploads.empty()) { if (!pendingMaterialUploads.empty())
{
// Material data is currently shared by all frame contexts. Wait before // Material data is currently shared by all frame contexts. Wait before
// mutating it so an older in-flight frame cannot read the overwritten // mutating it so an older in-flight frame cannot read the overwritten
// range. This can later be replaced with per-frame material buffers. // range. This can later be replaced with per-frame material buffers.
@@ -71,6 +71,8 @@ void CubeMap::RenderToCubemap(
} }
gfxDevice.getImmediateExecuter().immediateSubmit([&](VkCommandBuffer cmd) { gfxDevice.getImmediateExecuter().immediateSubmit([&](VkCommandBuffer cmd) {
vkutil::ScopedCmdLabel label(cmd, "HDRI To Cubemap", vkutil::labelColors::sky);
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
@@ -108,6 +110,8 @@ void CubeMap::RenderToCubemap(
scissor.extent = {size, size}; scissor.extent = {size, size};
for (std::uint32_t face = 0; face < 6; ++face) { for (std::uint32_t face = 0; face < 6; ++face) {
vkutil::cmdBeginLabelFormatted(cmd, vkutil::labelColors::draw, "Cubemap Face %u", face);
PC pc{}; PC pc{};
pc.viewMtx = viewMatrices[face]; pc.viewMtx = viewMatrices[face];
pc.projMtx = m_projection; pc.projMtx = m_projection;
@@ -153,6 +157,8 @@ void CubeMap::RenderToCubemap(
vkCmdDraw(cmd, 36, 1, 0, 0); vkCmdDraw(cmd, 36, 1, 0, 0);
vkCmdEndRendering(cmd); vkCmdEndRendering(cmd);
vkutil::cmdEndLabel(cmd);
} }
barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
@@ -199,6 +205,8 @@ void CubeMap::CreateCubeMap(
.mipMap = false, .mipMap = false,
.isCubemap = true .isCubemap = true
}); });
cubeMapImage.debugName = "skybox cubemap";
vkutil::addDebugLabel(gfxDevice.getDevice(), cubeMapImage.image, cubeMapImage.debugName.c_str());
std::array<VkImageView, 6> faceViews{}; std::array<VkImageView, 6> faceViews{};
@@ -220,6 +228,9 @@ void CubeMap::CreateCubeMap(
&viewInfo, &viewInfo,
nullptr, nullptr,
&faceViews[face])); &faceViews[face]));
const std::string faceViewName = "skybox cubemap face " + std::to_string(face) + " (view)";
vkutil::addDebugLabel(gfxDevice.getDevice(), faceViews[face], faceViewName.c_str());
} }
spdlog::info("HDRI image id = {}", m_hdrImage); spdlog::info("HDRI image id = {}", m_hdrImage);
@@ -305,6 +316,7 @@ void CubeMap::InitCubemapPipeline(
PipelineConfigInfo pipelineConfig{}; PipelineConfigInfo pipelineConfig{};
Pipeline::DefaultPipelineConfigInfo(pipelineConfig); Pipeline::DefaultPipelineConfigInfo(pipelineConfig);
pipelineConfig.name = "HDRI To Cubemap Pipeline";
pipelineConfig.vertexAttributeDescriptions = {}; pipelineConfig.vertexAttributeDescriptions = {};
pipelineConfig.vertexBindingDescriptions = {}; pipelineConfig.vertexBindingDescriptions = {};
+439
View File
@@ -0,0 +1,439 @@
#include <destrum/Graphics/ResourcesInspector.h>
#include <algorithm>
#include <cctype>
#include <charconv>
#include <system_error>
#include <cstring>
#include <string>
#include <string_view>
#include <vector>
#include <imgui.h>
#include <vulkan/vk_enum_string_helper.h>
#include <destrum/Graphics/Caches/ImageCache.h>
#include <destrum/Graphics/Caches/MaterialCache.h>
#include <destrum/Graphics/Pipelines/ImguiPass.h>
#include <destrum/Graphics/RenderResources.h>
namespace
{
[[nodiscard]] const char* StripPrefix(const char* name, const char* prefix)
{
const std::size_t length = std::strlen(prefix);
return std::strncmp(name, prefix, length) == 0 ? name + length : name;
}
[[nodiscard]] const char* FormatName(VkFormat format)
{
return StripPrefix(string_VkFormat(format), "VK_FORMAT_");
}
[[nodiscard]] const char* LayoutName(VkImageLayout layout)
{
return StripPrefix(string_VkImageLayout(layout), "VK_IMAGE_LAYOUT_");
}
[[nodiscard]] std::string UsageNames(VkImageUsageFlags usage)
{
struct UsageName
{
VkImageUsageFlagBits bit;
const char* name;
};
constexpr UsageName usageNames[] = {
{VK_IMAGE_USAGE_SAMPLED_BIT, "sampled"},
{VK_IMAGE_USAGE_STORAGE_BIT, "storage"},
{VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, "color attachment"},
{VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, "depth attachment"},
{VK_IMAGE_USAGE_TRANSFER_SRC_BIT, "transfer src"},
{VK_IMAGE_USAGE_TRANSFER_DST_BIT, "transfer dst"},
};
std::string result;
for (const UsageName& usageName : usageNames)
{
if ((usage & usageName.bit) == 0)
{
continue;
}
if (!result.empty())
{
result += ", ";
}
result += usageName.name;
}
return result.empty() ? "none" : result;
}
[[nodiscard]] bool IsDepthFormat(VkFormat format)
{
switch (format)
{
case VK_FORMAT_D16_UNORM:
case VK_FORMAT_X8_D24_UNORM_PACK32:
case VK_FORMAT_D32_SFLOAT:
case VK_FORMAT_D16_UNORM_S8_UINT:
case VK_FORMAT_D24_UNORM_S8_UINT:
case VK_FORMAT_D32_SFLOAT_S8_UINT:
return true;
default:
return false;
}
}
[[nodiscard]] bool ContainsCaseInsensitive(std::string_view haystack, std::string_view needle)
{
const auto it = std::search(
haystack.begin(), haystack.end(),
needle.begin(), needle.end(),
[](char a, char b)
{
return std::tolower(static_cast<unsigned char>(a)) == std::tolower(static_cast<unsigned char>(b));
});
return it != haystack.end();
}
[[nodiscard]] const char* TextureIntentName(TextureIntent intent)
{
switch (intent)
{
case TextureIntent::ColorSrgb: return "color (sRGB)";
case TextureIntent::DataLinear: return "data (linear)";
}
return "unknown";
}
}
ResourcesInspector::PreviewInfo ResourcesInspector::getPreviewInfo(const GPUImage& image)
{
if (image.image == VK_NULL_HANDLE || image.imageView == VK_NULL_HANDLE)
{
return {false, "no image view"};
}
if ((image.usage & VK_IMAGE_USAGE_SAMPLED_BIT) == 0)
{
return {false, "not sampleable"};
}
if (image.isCubemap)
{
return {false, "cubemap"};
}
if (image.numLayers > 1)
{
return {false, "image array"};
}
if (image.getLayout() == VK_IMAGE_LAYOUT_UNDEFINED)
{
return {false, "never written"};
}
// ImGui reads the image when the UI gets rendered at the very end of the frame. Render targets
// like the draw / depth image are in an attachment or transfer layout by then.
if (image.getLayout() != VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
{
return {false, "render target"};
}
return {true, ""};
}
void ResourcesInspector::drawImagePreview(const GPUImage& image, float maxSize, ImguiPass& imguiPass) const
{
const float width = static_cast<float>(image.extent.width);
const float height = static_cast<float>(image.extent.height);
const float scale = maxSize / std::max({width, height, 1.0f});
const ImVec2 size{std::max(width * scale, 1.0f), std::max(height * scale, 1.0f)};
const PreviewInfo preview = getPreviewInfo(image);
if (preview.previewable)
{
const ImTextureID textureId = imguiPass.getFrameTexture(image.imageView);
if (textureId != ImTextureID{})
{
ImGui::ImageWithBg(textureId, size, ImVec2{0.0f, 0.0f}, ImVec2{1.0f, 1.0f}, ImVec4{0.1f, 0.1f, 0.1f, 1.0f});
return;
}
}
// placeholder box with the reason it can't be shown
const ImVec2 boxSize{maxSize, std::min(maxSize, std::max(size.y, ImGui::GetTextLineHeight() * 2.0f))};
const ImVec2 min = ImGui::GetCursorScreenPos();
const ImVec2 max{min.x + boxSize.x, min.y + boxSize.y};
ImDrawList* drawList = ImGui::GetWindowDrawList();
drawList->AddRectFilled(min, max, ImGui::GetColorU32(ImGuiCol_FrameBg));
drawList->AddRect(min, max, ImGui::GetColorU32(ImGuiCol_Border));
// small thumbnails just get a dash, the reason is in the tooltip
const char* text = ImGui::CalcTextSize(preview.reason).x <= boxSize.x - 8.0f ? preview.reason : "-";
const ImVec2 textSize = ImGui::CalcTextSize(text);
drawList->AddText(
ImVec2{min.x + (boxSize.x - textSize.x) * 0.5f, min.y + (boxSize.y - textSize.y) * 0.5f},
ImGui::GetColorU32(ImGuiCol_TextDisabled),
text);
ImGui::Dummy(boxSize);
}
void ResourcesInspector::drawImageDetails(ImageID id, const RenderResources& resources) const
{
const ImageCache& cache = resources.images();
const GPUImage& image = cache.images[id];
ImGui::Text("Image ID: %u", static_cast<unsigned>(id));
ImGui::TextWrapped("Name: %s", image.debugName.empty() ? "<unnamed>" : image.debugName.c_str());
if (id == resources.getWhiteTextureID())
{
ImGui::TextColored(ImVec4{0.6f, 0.8f, 1.0f, 1.0f}, "Default white (also metallic/roughness + emissive default)");
}
if (id == resources.getErrorTextureID())
{
ImGui::TextColored(ImVec4{1.0f, 0.4f, 0.8f, 1.0f}, "Error texture (used when a load fails)");
}
if (id == resources.getDefaultNormalTextureID())
{
ImGui::TextColored(ImVec4{0.6f, 0.8f, 1.0f, 1.0f}, "Default normal map");
}
ImGui::Separator();
ImGui::Text("Size: %u x %u", image.extent.width, image.extent.height);
ImGui::Text("Format: %s%s", FormatName(image.format), IsDepthFormat(image.format) ? " (shown as red)" : "");
ImGui::Text("Mips: %u Layers: %u%s", image.mipLevels, image.numLayers, image.isCubemap ? " cubemap" : "");
ImGui::Text("Layout: %s", LayoutName(image.getLayout()));
if (const PreviewInfo preview = getPreviewInfo(image); !preview.previewable)
{
ImGui::TextDisabled("No preview: %s", preview.reason);
}
ImGui::TextWrapped("Usage: %s", UsageNames(image.usage).c_str());
if (const auto it = cache.loadedImagesInfo.find(id); it != cache.loadedImagesInfo.end())
{
ImGui::Separator();
ImGui::TextWrapped("File: %s", it->second.path.generic_string().c_str());
ImGui::Text("Intent: %s%s", TextureIntentName(it->second.intent), it->second.mipMap ? ", mipmapped" : "");
}
const MaterialCache& materials = resources.materials();
std::vector<std::size_t> users;
for (std::size_t materialId = 0; materialId < materials.materials.size(); ++materialId)
{
if (materials.materials[materialId].diffuseTexture == id)
{
users.push_back(materialId);
}
}
if (!users.empty())
{
ImGui::Separator();
ImGui::Text("Diffuse texture of %zu material(s):", users.size());
constexpr std::size_t maxListed = 8;
for (std::size_t i = 0; i < std::min(users.size(), maxListed); ++i)
{
const Material& material = materials.materials[users[i]];
ImGui::BulletText(
"#%zu %s",
users[i],
material.name.empty() ? "<unnamed>" : material.name.c_str());
}
if (users.size() > maxListed)
{
ImGui::TextDisabled("... and %zu more", users.size() - maxListed);
}
}
}
bool ResourcesInspector::matchesFilter(ImageID id, const RenderResources& resources) const
{
const std::string_view filterText{filter};
if (filterText.empty())
{
return true;
}
// a plain number matches the image id exactly
unsigned filterId = 0;
const auto [end, error] = std::from_chars(filterText.data(), filterText.data() + filterText.size(), filterId);
if (error == std::errc{} && end == filterText.data() + filterText.size())
{
return filterId == id;
}
const ImageCache& cache = resources.images();
if (ContainsCaseInsensitive(cache.images[id].debugName, filterText))
{
return true;
}
if (const auto it = cache.loadedImagesInfo.find(id); it != cache.loadedImagesInfo.end())
{
return ContainsCaseInsensitive(it->second.path.generic_string(), filterText);
}
return false;
}
void ResourcesInspector::drawImageCacheWindow(bool* open, const RenderResources& resources, ImguiPass& imguiPass)
{
if (!ImGui::Begin("Image Cache", open))
{
ImGui::End();
return;
}
if (!resources.isInitialized())
{
ImGui::TextDisabled("Render resources aren't initialized yet");
ImGui::End();
return;
}
const ImageCache& cache = resources.images();
std::vector<ImageID> visibleIds;
std::size_t imageCount = 0;
for (std::size_t i = 0; i < cache.images.size(); ++i)
{
const auto id = static_cast<ImageID>(i);
const GPUImage& image = cache.images[i];
if (image.image == VK_NULL_HANDLE)
{
continue;
}
++imageCount;
if (!matchesFilter(id, resources))
{
continue;
}
if (onlyPreviewable && !getPreviewInfo(image).previewable)
{
continue;
}
visibleIds.push_back(id);
}
ImGui::Text("%zu images (bindless capacity %u)", imageCount, cache.getMaxImageCount());
ImGui::SetNextItemWidth(220.0f);
ImGui::InputTextWithHint("##filter", "Filter by name, path or ID", filter, sizeof(filter));
ImGui::SameLine();
ImGui::SetNextItemWidth(140.0f);
ImGui::SliderFloat("Thumbnail", &thumbnailSize, 24.0f, 256.0f, "%.0f px");
ImGui::SameLine();
ImGui::Checkbox("Previewable only", &onlyPreviewable);
if (selectedImageId != NULL_IMAGE_ID &&
(selectedImageId >= cache.images.size() || cache.images[selectedImageId].image == VK_NULL_HANDLE))
{
selectedImageId = NULL_IMAGE_ID;
}
if (selectedImageId != NULL_IMAGE_ID)
{
if (ImGui::CollapsingHeader("Selected Image", ImGuiTreeNodeFlags_DefaultOpen))
{
drawImagePreview(cache.images[selectedImageId], 256.0f, imguiPass);
ImGui::SameLine();
ImGui::BeginGroup();
drawImageDetails(selectedImageId, resources);
if (ImGui::SmallButton("Clear selection"))
{
selectedImageId = NULL_IMAGE_ID;
}
ImGui::EndGroup();
}
}
constexpr ImGuiTableFlags tableFlags =
ImGuiTableFlags_Borders |
ImGuiTableFlags_RowBg |
ImGuiTableFlags_ScrollY |
ImGuiTableFlags_Resizable |
ImGuiTableFlags_SizingStretchProp;
if (ImGui::BeginTable("##images", 6, tableFlags))
{
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, 40.0f);
ImGui::TableSetupColumn("Preview", ImGuiTableColumnFlags_WidthFixed, thumbnailSize + 4.0f);
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch, 3.0f);
ImGui::TableSetupColumn("Size", ImGuiTableColumnFlags_WidthFixed, 90.0f);
ImGui::TableSetupColumn("Format", ImGuiTableColumnFlags_WidthStretch, 1.5f);
ImGui::TableSetupColumn("Layout", ImGuiTableColumnFlags_WidthStretch, 1.5f);
ImGui::TableHeadersRow();
// only the visible rows get a texture descriptor
const float rowHeight = thumbnailSize + ImGui::GetStyle().CellPadding.y * 2.0f;
ImGuiListClipper clipper;
clipper.Begin(static_cast<int>(visibleIds.size()), rowHeight);
while (clipper.Step())
{
for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row)
{
const ImageID id = visibleIds[static_cast<std::size_t>(row)];
const GPUImage& image = cache.images[id];
ImGui::PushID(static_cast<int>(id));
ImGui::TableNextRow(ImGuiTableRowFlags_None, thumbnailSize);
ImGui::TableNextColumn();
const std::string idLabel = std::to_string(id);
if (ImGui::Selectable(
idLabel.c_str(),
selectedImageId == id,
ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap,
ImVec2{0.0f, thumbnailSize}))
{
selectedImageId = selectedImageId == id ? NULL_IMAGE_ID : id;
}
if (ImGui::BeginItemTooltip())
{
drawImagePreview(image, 256.0f, imguiPass);
drawImageDetails(id, resources);
ImGui::EndTooltip();
}
ImGui::TableNextColumn();
drawImagePreview(image, thumbnailSize, imguiPass);
ImGui::TableNextColumn();
if (const auto it = cache.loadedImagesInfo.find(id); it != cache.loadedImagesInfo.end())
{
// loaded images are named after their full path, the file name is enough here
ImGui::TextWrapped("%s", it->second.path.filename().generic_string().c_str());
}
else
{
ImGui::TextWrapped("%s", image.debugName.empty() ? "<unnamed>" : image.debugName.c_str());
}
ImGui::TableNextColumn();
ImGui::Text("%u x %u", image.extent.width, image.extent.height);
if (image.mipLevels > 1)
{
ImGui::TextDisabled("%u mips", image.mipLevels);
}
ImGui::TableNextColumn();
ImGui::TextUnformatted(FormatName(image.format));
ImGui::TableNextColumn();
ImGui::TextUnformatted(LayoutName(image.getLayout()));
ImGui::PopID();
}
}
ImGui::EndTable();
}
ImGui::End();
}
+20
View File
@@ -21,6 +21,22 @@ void Swapchain::initSync(VkDevice device) {
for (std::uint32_t i = 0; i < FRAMES_IN_FLIGHT; ++i) { for (std::uint32_t i = 0; i < FRAMES_IN_FLIGHT; ++i) {
VK_CHECK(vkCreateFence(device, &fenceCreateInfo, nullptr, &frames[i].renderFence)); VK_CHECK(vkCreateFence(device, &fenceCreateInfo, nullptr, &frames[i].renderFence));
VK_CHECK(vkCreateSemaphore(device, &semaphoreCreateInfo, nullptr, &frames[i].swapchainSemaphore)); VK_CHECK(vkCreateSemaphore(device, &semaphoreCreateInfo, nullptr, &frames[i].swapchainSemaphore));
const auto fenceName = std::format("frame {} render fence", i);
const auto semaphoreName = std::format("frame {} swapchain semaphore", i);
vkutil::addDebugLabel(device, frames[i].renderFence, fenceName.c_str());
vkutil::addDebugLabel(device, frames[i].swapchainSemaphore, semaphoreName.c_str());
}
}
void Swapchain::nameSwapchainObjects(VkDevice device) const {
for (std::size_t i = 0; i < images.size(); ++i) {
const auto imageName = std::format("swapchain image {}", i);
const auto viewName = std::format("swapchain image {} (view)", i);
const auto semaphoreName = std::format("swapchain image {} render semaphore", i);
vkutil::addDebugLabel(device, images[i], imageName.c_str());
vkutil::addDebugLabel(device, imageViews[i], viewName.c_str());
vkutil::addDebugLabel(device, imageRenderSemaphores[i], semaphoreName.c_str());
} }
} }
@@ -85,6 +101,8 @@ void Swapchain::createSwapchain(
VK_CHECK(vkCreateSemaphore(vkbDevice, &sci, nullptr, &sem)); VK_CHECK(vkCreateSemaphore(vkbDevice, &sci, nullptr, &sem));
} }
nameSwapchainObjects(vkbDevice);
extent = m_swapchain.extent; extent = m_swapchain.extent;
dirty = false; dirty = false;
} }
@@ -187,6 +205,8 @@ void Swapchain::recreateSwapchain(
VK_CHECK(vkCreateSemaphore(vkbDevice, &sci, nullptr, &sem)); VK_CHECK(vkCreateSemaphore(vkbDevice, &sci, nullptr, &sem));
} }
nameSwapchainObjects(vkbDevice);
extent = m_swapchain.extent; extent = m_swapchain.extent;
dirty = false; dirty = false;
} }
+101 -101
View File
@@ -188,54 +188,124 @@ void vkutil::bufferHostWriteToTransferReadBarrier(
} }
void vkutil::addDebugLabel(VkDevice device, VkImage image, const char* label) { void vkutil::addDebugLabel(VkDevice device, VkObjectType objectType, std::uint64_t objectHandle, const char* label) {
if (!vkSetDebugUtilsObjectNameEXT || device == VK_NULL_HANDLE || objectHandle == 0 || label == nullptr) {
return;
}
const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{ const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT, .sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT,
.objectType = VK_OBJECT_TYPE_IMAGE, .objectType = objectType,
.objectHandle = (std::uint64_t)image, .objectHandle = objectHandle,
.pObjectName = label, .pObjectName = label,
}; };
if (vkSetDebugUtilsObjectNameEXT) vkSetDebugUtilsObjectNameEXT(device, &nameInfo); vkSetDebugUtilsObjectNameEXT(device, &nameInfo);
}
void vkutil::addDebugLabel(VkDevice device, VkImage image, const char* label) {
addDebugLabel(device, VK_OBJECT_TYPE_IMAGE, reinterpret_cast<std::uint64_t>(image), label);
}
void vkutil::addDebugLabel(VkDevice device, VkImageView imageView, const char* label) {
addDebugLabel(device, VK_OBJECT_TYPE_IMAGE_VIEW, reinterpret_cast<std::uint64_t>(imageView), label);
} }
void vkutil::addDebugLabel(VkDevice device, VkShaderModule shaderModule, const char* label) { void vkutil::addDebugLabel(VkDevice device, VkShaderModule shaderModule, const char* label) {
const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{ addDebugLabel(device, VK_OBJECT_TYPE_SHADER_MODULE, reinterpret_cast<std::uint64_t>(shaderModule), label);
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT,
.objectType = VK_OBJECT_TYPE_SHADER_MODULE,
.objectHandle = (std::uint64_t)shaderModule,
.pObjectName = label,
};
if (vkSetDebugUtilsObjectNameEXT) vkSetDebugUtilsObjectNameEXT(device, &nameInfo);
} }
void vkutil::addDebugLabel(VkDevice device, VkPipeline pipeline, const char* label) { void vkutil::addDebugLabel(VkDevice device, VkPipeline pipeline, const char* label) {
const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{ addDebugLabel(device, VK_OBJECT_TYPE_PIPELINE, reinterpret_cast<std::uint64_t>(pipeline), label);
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT, }
.objectType = VK_OBJECT_TYPE_PIPELINE,
.objectHandle = (std::uint64_t)pipeline, void vkutil::addDebugLabel(VkDevice device, VkPipelineLayout layout, const char* label) {
.pObjectName = label, addDebugLabel(device, VK_OBJECT_TYPE_PIPELINE_LAYOUT, reinterpret_cast<std::uint64_t>(layout), label);
};
if (vkSetDebugUtilsObjectNameEXT) vkSetDebugUtilsObjectNameEXT(device, &nameInfo);
} }
void vkutil::addDebugLabel(VkDevice device, VkBuffer buffer, const char* label) { void vkutil::addDebugLabel(VkDevice device, VkBuffer buffer, const char* label) {
const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{ addDebugLabel(device, VK_OBJECT_TYPE_BUFFER, reinterpret_cast<std::uint64_t>(buffer), label);
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT,
.objectType = VK_OBJECT_TYPE_BUFFER,
.objectHandle = (std::uint64_t)buffer,
.pObjectName = label,
};
if (vkSetDebugUtilsObjectNameEXT) vkSetDebugUtilsObjectNameEXT(device, &nameInfo);
} }
void vkutil::addDebugLabel(VkDevice device, VkSampler sampler, const char* label) { void vkutil::addDebugLabel(VkDevice device, VkSampler sampler, const char* label) {
const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{ addDebugLabel(device, VK_OBJECT_TYPE_SAMPLER, reinterpret_cast<std::uint64_t>(sampler), label);
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT, }
.objectType = VK_OBJECT_TYPE_SAMPLER,
.objectHandle = (std::uint64_t)sampler, void vkutil::addDebugLabel(VkDevice device, VkDescriptorPool descriptorPool, const char* label) {
.pObjectName = label, addDebugLabel(device, VK_OBJECT_TYPE_DESCRIPTOR_POOL, reinterpret_cast<std::uint64_t>(descriptorPool), label);
}
void vkutil::addDebugLabel(VkDevice device, VkDescriptorSetLayout descriptorSetLayout, const char* label) {
addDebugLabel(device, VK_OBJECT_TYPE_DESCRIPTOR_SET_LAYOUT, reinterpret_cast<std::uint64_t>(descriptorSetLayout), label);
}
void vkutil::addDebugLabel(VkDevice device, VkDescriptorSet descriptorSet, const char* label) {
addDebugLabel(device, VK_OBJECT_TYPE_DESCRIPTOR_SET, reinterpret_cast<std::uint64_t>(descriptorSet), label);
}
void vkutil::addDebugLabel(VkDevice device, VkCommandPool commandPool, const char* label) {
addDebugLabel(device, VK_OBJECT_TYPE_COMMAND_POOL, reinterpret_cast<std::uint64_t>(commandPool), label);
}
void vkutil::addDebugLabel(VkDevice device, VkCommandBuffer commandBuffer, const char* label) {
addDebugLabel(device, VK_OBJECT_TYPE_COMMAND_BUFFER, reinterpret_cast<std::uint64_t>(commandBuffer), label);
}
void vkutil::addDebugLabel(VkDevice device, VkFence fence, const char* label) {
addDebugLabel(device, VK_OBJECT_TYPE_FENCE, reinterpret_cast<std::uint64_t>(fence), label);
}
void vkutil::addDebugLabel(VkDevice device, VkSemaphore semaphore, const char* label) {
addDebugLabel(device, VK_OBJECT_TYPE_SEMAPHORE, reinterpret_cast<std::uint64_t>(semaphore), label);
}
void vkutil::addDebugLabel(VkDevice device, VkQueue queue, const char* label) {
addDebugLabel(device, VK_OBJECT_TYPE_QUEUE, reinterpret_cast<std::uint64_t>(queue), label);
}
bool vkutil::debugLabelsEnabled() {
return vkCmdBeginDebugUtilsLabelEXT != nullptr && vkCmdEndDebugUtilsLabelEXT != nullptr;
}
void vkutil::cmdBeginLabel(VkCommandBuffer cmd, const char* label, const glm::vec4& color) {
if (!debugLabelsEnabled() || label == nullptr) {
return;
}
const auto labelInfo = VkDebugUtilsLabelEXT{
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT,
.pLabelName = label,
.color = {color.x, color.y, color.z, color.w},
}; };
if (vkSetDebugUtilsObjectNameEXT) vkSetDebugUtilsObjectNameEXT(device, &nameInfo); vkCmdBeginDebugUtilsLabelEXT(cmd, &labelInfo);
}
void vkutil::cmdEndLabel(VkCommandBuffer cmd) {
if (!debugLabelsEnabled()) {
return;
}
vkCmdEndDebugUtilsLabelEXT(cmd);
}
void vkutil::cmdInsertLabel(VkCommandBuffer cmd, const char* label, const glm::vec4& color) {
if (!vkCmdInsertDebugUtilsLabelEXT || label == nullptr) {
return;
}
const auto labelInfo = VkDebugUtilsLabelEXT{
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT,
.pLabelName = label,
.color = {color.x, color.y, color.z, color.w},
};
vkCmdInsertDebugUtilsLabelEXT(cmd, &labelInfo);
}
vkutil::ScopedCmdLabel::ScopedCmdLabel(VkCommandBuffer cmd, const char* label, const glm::vec4& color)
: cmd(cmd) {
cmdBeginLabel(cmd, label, color);
}
vkutil::ScopedCmdLabel::~ScopedCmdLabel() {
cmdEndLabel(cmd);
} }
vkutil::RenderInfo vkutil::createRenderingInfo(const RenderingInfoParams& params) { vkutil::RenderInfo vkutil::createRenderingInfo(const RenderingInfoParams& params) {
@@ -319,76 +389,6 @@ VkShaderModule vkutil::loadShaderModule(const std::filesystem::path& path, VkDev
return shaderModule; return shaderModule;
} }
void addDebugLabel(VkDevice device, VkImage image, const char* label) {
const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT,
.objectType = VK_OBJECT_TYPE_IMAGE,
.objectHandle = (std::uint64_t)image,
.pObjectName = label,
};
if (vkSetDebugUtilsObjectNameEXT) vkSetDebugUtilsObjectNameEXT(device, &nameInfo);
}
void addDebugLabel(VkDevice device, VkImageView imageView, const char* label) {
const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT,
.objectType = VK_OBJECT_TYPE_IMAGE_VIEW,
.objectHandle = (std::uint64_t)imageView,
.pObjectName = label,
};
if (vkSetDebugUtilsObjectNameEXT) vkSetDebugUtilsObjectNameEXT(device, &nameInfo);
}
void addDebugLabel(VkDevice device, VkShaderModule shaderModule, const char* label) {
const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT,
.objectType = VK_OBJECT_TYPE_SHADER_MODULE,
.objectHandle = (std::uint64_t)shaderModule,
.pObjectName = label,
};
if (vkSetDebugUtilsObjectNameEXT) vkSetDebugUtilsObjectNameEXT(device, &nameInfo);
}
void addDebugLabel(VkDevice device, VkPipeline pipeline, const char* label) {
const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT,
.objectType = VK_OBJECT_TYPE_PIPELINE,
.objectHandle = (std::uint64_t)pipeline,
.pObjectName = label,
};
if (vkSetDebugUtilsObjectNameEXT) vkSetDebugUtilsObjectNameEXT(device, &nameInfo);
}
void addDebugLabel(VkDevice device, VkPipelineLayout layout, const char* label) {
const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT,
.objectType = VK_OBJECT_TYPE_PIPELINE_LAYOUT,
.objectHandle = (std::uint64_t)layout,
.pObjectName = label,
};
if (vkSetDebugUtilsObjectNameEXT) vkSetDebugUtilsObjectNameEXT(device, &nameInfo);
}
void addDebugLabel(VkDevice device, VkBuffer buffer, const char* label) {
const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT,
.objectType = VK_OBJECT_TYPE_BUFFER,
.objectHandle = (std::uint64_t)buffer,
.pObjectName = label,
};
if (vkSetDebugUtilsObjectNameEXT) vkSetDebugUtilsObjectNameEXT(device, &nameInfo);
}
void addDebugLabel(VkDevice device, VkSampler sampler, const char* label) {
const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT,
.objectType = VK_OBJECT_TYPE_SAMPLER,
.objectHandle = (std::uint64_t)sampler,
.pObjectName = label,
};
if (vkSetDebugUtilsObjectNameEXT) vkSetDebugUtilsObjectNameEXT(device, &nameInfo);
}
vkutil::RenderInfo createRenderingInfo(const vkutil::RenderingInfoParams& params) { vkutil::RenderInfo createRenderingInfo(const vkutil::RenderingInfoParams& params) {
return vkutil::createRenderingInfo(params); return vkutil::createRenderingInfo(params);
} }
+13 -5
View File
@@ -46,11 +46,11 @@ void InputManager::BeginFrame() {
m_textInput.clear(); m_textInput.clear();
// Update mouse delta based on last known position // Relative mouse accumulators are filled during SDL event processing
m_mouseDX = m_mouseX - m_prevMouseX; // and consumed by game code each frame. Reset them here (before events
m_mouseDY = m_mouseY - m_prevMouseY; // are processed) so this frame accumulates fresh deltas.
m_prevMouseX = m_mouseX; m_mouseRelDX = 0;
m_prevMouseY = m_mouseY; m_mouseRelDY = 0;
// Clear controller "edge" sets + snapshot axes // Clear controller "edge" sets + snapshot axes
for (auto& [id, pad]: m_pads) { for (auto& [id, pad]: m_pads) {
@@ -60,6 +60,12 @@ void InputManager::BeginFrame() {
} }
} }
void InputManager::SetMouseCaptured(bool captured) {
if (captured == m_MouseCaptured) return;
m_MouseCaptured = captured;
SDL_SetRelativeMouseMode(captured ? SDL_TRUE : SDL_FALSE);
}
void InputManager::AddController(int deviceIndex) { void InputManager::AddController(int deviceIndex) {
if (!SDL_IsGameController(deviceIndex)) return; if (!SDL_IsGameController(deviceIndex)) return;
@@ -134,6 +140,8 @@ bool InputManager::ProcessEvent(const SDL_Event& e) {
case SDL_MOUSEMOTION: { case SDL_MOUSEMOTION: {
m_mouseX = e.motion.x; m_mouseX = e.motion.x;
m_mouseY = e.motion.y; m_mouseY = e.motion.y;
m_mouseRelDX += e.motion.xrel;
m_mouseRelDY += e.motion.yrel;
} }
break; break;
+137
View File
@@ -26,9 +26,12 @@
#include <cstdarg> #include <cstdarg>
#include <cstdio> #include <cstdio>
#include <memory> #include <memory>
#include <mutex>
#include <stdexcept> #include <stdexcept>
#include <thread> #include <thread>
#include <unordered_map> #include <unordered_map>
#include <unordered_set>
#include <vector>
#include <glm/gtx/norm.hpp> #include <glm/gtx/norm.hpp>
@@ -276,6 +279,84 @@ namespace
throw std::runtime_error("Cannot create Jolt body without a valid shape."); throw std::runtime_error("Cannot create Jolt body without a valid shape.");
} }
} }
class SensorContactListener final : public ContactListener
{
public:
using BodyPair = std::pair<BodyID, BodyID>;
struct PairHash {
std::size_t operator()(const BodyPair& p) const {
return std::hash<uint32_t>{}(
p.first.GetIndexAndSequenceNumber() ^
(p.second.GetIndexAndSequenceNumber() << 7));
}
};
struct PairData {
GameObject* objA{nullptr};
GameObject* objB{nullptr};
bool sensorA{false};
bool sensorB{false};
};
ValidateResult OnContactValidate(const Body&, const Body&, RVec3Arg,
const CollideShapeResult&) override
{
return ValidateResult::AcceptAllContactsForThisBodyPair;
}
void OnContactAdded(const Body& body1, const Body& body2,
const ContactManifold&, ContactSettings& ioSettings) override
{
if (body1.IsSensor() || body2.IsSensor())
{
PairData data;
data.objA = reinterpret_cast<GameObject*>(body1.GetUserData());
data.objB = reinterpret_cast<GameObject*>(body2.GetUserData());
data.sensorA = body1.IsSensor();
data.sensorB = body2.IsSensor();
std::lock_guard<std::mutex> lock(m_Mutex);
m_Overlaps.insert_or_assign(MakePair(body1.GetID(), body2.GetID()), data);
}
}
void OnContactPersisted(const Body& body1, const Body& body2,
const ContactManifold&, ContactSettings&) override
{
}
void OnContactRemoved(const SubShapeIDPair& subShapePair) override
{
std::lock_guard<std::mutex> lock(m_Mutex);
m_Overlaps.erase(MakePair(subShapePair.GetBody1ID(), subShapePair.GetBody2ID()));
}
void Clear()
{
std::lock_guard<std::mutex> lock(m_Mutex);
m_Overlaps.clear();
}
std::unordered_map<BodyPair, PairData, PairHash> SwapOverlaps()
{
std::lock_guard<std::mutex> lock(m_Mutex);
return std::move(m_Overlaps);
}
private:
static BodyPair MakePair(BodyID a, BodyID b)
{
if (a.GetIndexAndSequenceNumber() < b.GetIndexAndSequenceNumber())
{
return {a, b};
}
return {b, a};
}
std::mutex m_Mutex;
std::unordered_map<BodyPair, PairData, PairHash> m_Overlaps;
};
} // namespace } // namespace
class JoltPhysicsWorld::Impl class JoltPhysicsWorld::Impl
@@ -307,6 +388,7 @@ public:
m_ObjectVsBroadPhaseLayerFilter, m_ObjectVsBroadPhaseLayerFilter,
m_ObjectLayerPairFilter); m_ObjectLayerPairFilter);
m_PhysicsSystem.SetContactListener(&m_SensorListener);
m_PhysicsSystem.SetGravity(ToJoltVec3(settings.gravity)); m_PhysicsSystem.SetGravity(ToJoltVec3(settings.gravity));
} }
@@ -426,6 +508,15 @@ public:
collisionSteps, collisionSteps,
m_TempAllocator.get(), m_TempAllocator.get(),
m_JobSystem.get()); m_JobSystem.get());
ProcessSensorOverlaps();
}
std::vector<TriggerEvent> ConsumeTriggerEvents()
{
std::vector<TriggerEvent> events;
m_TriggerEvents.swap(events);
return events;
} }
void SyncKinematicBodiesToPhysics() void SyncKinematicBodiesToPhysics()
@@ -626,6 +717,43 @@ private:
return {}; return {};
} }
void ProcessSensorOverlaps()
{
const auto currentOverlaps = m_SensorListener.SwapOverlaps();
for (const auto& [pair, data] : currentOverlaps)
{
if (m_PreviousSensorOverlaps.find(pair) == m_PreviousSensorOverlaps.end())
{
if (data.sensorA && data.objA)
{
m_TriggerEvents.push_back({data.objA, data.objB, true});
}
if (data.sensorB && data.objB)
{
m_TriggerEvents.push_back({data.objB, data.objA, true});
}
}
}
for (const auto& [pair, data] : m_PreviousSensorOverlaps)
{
if (currentOverlaps.find(pair) == currentOverlaps.end())
{
if (data.sensorA && data.objA)
{
m_TriggerEvents.push_back({data.objA, data.objB, false});
}
if (data.sensorB && data.objB)
{
m_TriggerEvents.push_back({data.objB, data.objA, false});
}
}
}
m_PreviousSensorOverlaps = std::move(currentOverlaps);
}
Settings m_Settings{}; Settings m_Settings{};
BPLayerInterfaceImpl m_BPLayerInterface{}; BPLayerInterfaceImpl m_BPLayerInterface{};
@@ -639,6 +767,10 @@ private:
std::unordered_map<std::uint32_t, BodyRecord> m_Bodies; std::unordered_map<std::uint32_t, BodyRecord> m_Bodies;
std::uint32_t m_NextHandle{0}; std::uint32_t m_NextHandle{0};
SensorContactListener m_SensorListener;
std::unordered_map<SensorContactListener::BodyPair, SensorContactListener::PairData, SensorContactListener::PairHash> m_PreviousSensorOverlaps;
std::vector<TriggerEvent> m_TriggerEvents;
}; };
JoltPhysicsWorld::JoltPhysicsWorld(const Settings& settings) JoltPhysicsWorld::JoltPhysicsWorld(const Settings& settings)
@@ -710,3 +842,8 @@ bool JoltPhysicsWorld::Raycast(const glm::vec3& origin,
{ {
return m_Impl->Raycast(origin, direction, maxDistance, hit); return m_Impl->Raycast(origin, direction, maxDistance, hit);
} }
std::vector<TriggerEvent> JoltPhysicsWorld::ConsumeTriggerEvents()
{
return m_Impl->ConsumeTriggerEvents();
}
+20 -1
View File
@@ -1,6 +1,7 @@
#include <destrum/Physics/PhysicsSceneBridge.h> #include <destrum/Physics/PhysicsSceneBridge.h>
#include <destrum/Components/Physics/Rigidbody.h> #include <destrum/Components/Physics/Rigidbody.h>
#include <destrum/ObjectModel/Component.h>
#include <destrum/ObjectModel/GameObject.h> #include <destrum/ObjectModel/GameObject.h>
PhysicsSceneBridge::PhysicsSceneBridge(std::unique_ptr<PhysicsWorld> world) PhysicsSceneBridge::PhysicsSceneBridge(std::unique_ptr<PhysicsWorld> world)
@@ -8,14 +9,16 @@ PhysicsSceneBridge::PhysicsSceneBridge(std::unique_ptr<PhysicsWorld> world)
} }
void PhysicsSceneBridge::RegisterGameObject(GameObject& object) { void PhysicsSceneBridge::RegisterGameObject(GameObject& object) {
if (!m_World) return;
if (auto* rb = object.GetComponent<Rigidbody>()) { if (auto* rb = object.GetComponent<Rigidbody>()) {
if (!rb->HasPhysicsBody() || rb->GetPhysicsWorld() != &GetWorld()) { if (!rb->HasPhysicsBody() || rb->GetPhysicsWorld() != m_World.get()) {
m_World->RegisterRigidbody(*rb); m_World->RegisterRigidbody(*rb);
} }
} }
} }
void PhysicsSceneBridge::UnregisterGameObject(GameObject& object) { void PhysicsSceneBridge::UnregisterGameObject(GameObject& object) {
if (!m_World) return;
if (auto* rb = object.GetComponent<Rigidbody>()) { if (auto* rb = object.GetComponent<Rigidbody>()) {
if (rb->GetPhysicsWorld() == m_World.get()) { if (rb->GetPhysicsWorld() == m_World.get()) {
m_World->UnregisterRigidbody(*rb); m_World->UnregisterRigidbody(*rb);
@@ -24,13 +27,29 @@ void PhysicsSceneBridge::UnregisterGameObject(GameObject& object) {
} }
void PhysicsSceneBridge::RefreshGameObject(GameObject& object) { void PhysicsSceneBridge::RefreshGameObject(GameObject& object) {
if (!m_World) return;
if (auto* rb = object.GetComponent<Rigidbody>()) { if (auto* rb = object.GetComponent<Rigidbody>()) {
m_World->RefreshRigidbody(*rb); m_World->RefreshRigidbody(*rb);
} }
} }
void PhysicsSceneBridge::FixedUpdate(float fixedDt) { void PhysicsSceneBridge::FixedUpdate(float fixedDt) {
if (!m_World) return;
m_World->SyncKinematicBodiesToPhysics(); m_World->SyncKinematicBodiesToPhysics();
m_World->Step(fixedDt); m_World->Step(fixedDt);
for (const auto& event : m_World->ConsumeTriggerEvents()) {
if (event.owner == nullptr) continue;
for (const auto& component : event.owner->GetComponents()) {
if (component && !component->IsBeingDestroyed() && component->isEnabled()) {
if (event.entered) {
component->OnTriggerEnter(event.other);
} else {
component->OnTriggerExit(event.other);
}
}
}
}
m_World->SyncDynamicBodiesToTransforms(); m_World->SyncDynamicBodiesToTransforms();
} }
+39 -3
View File
@@ -1,4 +1,5 @@
#include <destrum/Scene/Scene.h> #include <destrum/Scene/Scene.h>
#include <destrum/Components/CameraComponent.h>
#include <destrum/ObjectModel/GameObject.h> #include <destrum/ObjectModel/GameObject.h>
#include <destrum/Util/ImGuiUtils.h> #include <destrum/Util/ImGuiUtils.h>
@@ -23,7 +24,12 @@ namespace {
} }
Scene::Scene(const std::string& name) Scene::Scene(const std::string& name)
: m_name(name), : Scene(name, std::make_unique<JoltPhysicsWorld>()) {
}
Scene::Scene(const std::string& name, std::unique_ptr<PhysicsWorld> physicsWorld)
: m_Physics(std::move(physicsWorld)),
m_name(name),
m_id(++m_idCounter) { m_id(++m_idCounter) {
} }
@@ -159,12 +165,42 @@ void Scene::Render(const RenderContext& ctx) {
} }
void Scene::RenderImgui() { CameraComponent* Scene::GetCurrentCamera() const {
const auto cameras = GetCameras();
for (CameraComponent* camera : cameras) {
if (camera->GetGameObject()->GetId() == m_CurrentCameraId) {
return camera;
}
}
return cameras.empty() ? nullptr : cameras.front();
}
std::vector<CameraComponent*> Scene::GetCameras() const {
std::vector<CameraComponent*> cameras;
for (const auto& object : m_objects) {
if (object == nullptr || object->IsBeingDestroyed() || !object->IsActiveInHierarchy()) {
continue;
}
CameraComponent* camera = object->GetComponent<CameraComponent>();
if (camera != nullptr && camera->isEnabled()) {
cameras.push_back(camera);
}
}
return cameras;
}
void Scene::SetCurrentCamera(const CameraComponent* camera) {
m_CurrentCameraId = camera != nullptr ? camera->GetGameObject()->GetId() : InvalidObjectId;
}
void Scene::RenderImgui(bool* open) {
ImGuiUtils::RenderSceneInspector( ImGuiUtils::RenderSceneInspector(
m_name, m_name,
m_objects, m_objects,
m_pendingAdditions, m_pendingAdditions,
m_SelectedObjectId); m_SelectedObjectId,
[this] { return CreateGameObject("GameObject"); },
open);
} }
void Scene::CleanupDestroyedGameObjects() { void Scene::CleanupDestroyedGameObjects() {
+73 -64
View File
@@ -8,50 +8,40 @@
#include <destrum/Util/DeltaTime.h> #include <destrum/Util/DeltaTime.h>
Scene& SceneManager::GetCurrentScene() const { Scene& SceneManager::GetCurrentScene() const {
if (m_scenes.empty()) { if (m_activeScenes.empty()) {
throw std::out_of_range("No scenes are available"); throw std::out_of_range("No active scenes are available");
} }
return *m_activeScenes.front();
if (m_ActiveSceneIndex < 0 || m_ActiveSceneIndex >= static_cast<int>(m_scenes.size())) {
throw std::out_of_range("Active scene index is invalid");
}
return *m_scenes[static_cast<std::size_t>(m_ActiveSceneIndex)];
} }
void SceneManager::Update(float dt) { void SceneManager::Update(float dt) {
if (m_scenes.empty()) return; for (const auto& scene : m_activeScenes) {
(void)GetCurrentScene(); scene->Update(dt);
const auto scene = m_scenes.at(static_cast<std::size_t>(m_ActiveSceneIndex)); }
scene->Update(dt);
} }
void SceneManager::FixedUpdate(float dt) { void SceneManager::FixedUpdate(float dt) {
if (m_scenes.empty()) return; for (const auto& scene : m_activeScenes) {
(void)GetCurrentScene(); scene->FixedUpdate(dt);
const auto scene = m_scenes.at(static_cast<std::size_t>(m_ActiveSceneIndex)); }
scene->FixedUpdate(dt);
} }
void SceneManager::LateUpdate(float dt) { void SceneManager::LateUpdate(float dt) {
if (m_scenes.empty()) return; for (const auto& scene : m_activeScenes) {
(void)GetCurrentScene(); scene->LateUpdate(dt);
const auto scene = m_scenes.at(static_cast<std::size_t>(m_ActiveSceneIndex)); }
scene->LateUpdate(dt);
} }
void SceneManager::Render(const RenderContext& ctx) { void SceneManager::Render(const RenderContext& ctx) {
if (m_scenes.empty()) return; for (const auto& scene : m_activeScenes) {
(void)GetCurrentScene(); scene->Render(ctx);
const auto scene = m_scenes.at(static_cast<std::size_t>(m_ActiveSceneIndex)); }
scene->Render(ctx);
} }
void SceneManager::RenderImgui() { void SceneManager::RenderImgui(bool* open) {
if (m_scenes.empty()) return; for (const auto& scene : m_activeScenes) {
(void)GetCurrentScene(); scene->RenderImgui(open);
const auto scene = m_scenes.at(static_cast<std::size_t>(m_ActiveSceneIndex)); }
scene->RenderImgui();
} }
void SceneManager::HandleGameObjectDestroy() { void SceneManager::HandleGameObjectDestroy() {
@@ -61,7 +51,7 @@ void SceneManager::HandleGameObjectDestroy() {
} }
void SceneManager::DestroyGameObjects() { void SceneManager::DestroyGameObjects() {
for (const auto& scene: m_scenes) { for (const auto& scene : m_scenes) {
scene->DestroyGameObjects(); scene->DestroyGameObjects();
} }
} }
@@ -73,37 +63,22 @@ void SceneManager::UnloadAllScenes() {
} }
void SceneManager::HandleSceneDestroy() { void SceneManager::HandleSceneDestroy() {
const std::shared_ptr<Scene> activeScene =
m_ActiveSceneIndex >= 0 &&
m_ActiveSceneIndex < static_cast<int>(m_scenes.size())
? m_scenes[static_cast<std::size_t>(m_ActiveSceneIndex)]
: nullptr;
for (auto it = m_scenes.begin(); it != m_scenes.end();) { for (auto it = m_scenes.begin(); it != m_scenes.end();) {
if ((*it)->IsBeingUnloaded()) { if ((*it)->IsBeingUnloaded()) {
const auto activeIt = std::find(m_activeScenes.begin(), m_activeScenes.end(), *it);
if (activeIt != m_activeScenes.end()) {
m_activeScenes.erase(activeIt);
}
it = m_scenes.erase(it); it = m_scenes.erase(it);
} else { } else {
++it; ++it;
} }
} }
if (m_scenes.empty()) { // Remove any stale active scenes that are no longer in the scene list.
m_ActiveSceneIndex = 0; std::erase_if(m_activeScenes, [this](const auto& scene) {
return; return std::find(m_scenes.begin(), m_scenes.end(), scene) == m_scenes.end();
} });
if (activeScene != nullptr) {
const auto activeIt = std::find(m_scenes.begin(), m_scenes.end(), activeScene);
if (activeIt != m_scenes.end()) {
m_ActiveSceneIndex = static_cast<int>(std::distance(m_scenes.begin(), activeIt));
return;
}
}
m_ActiveSceneIndex = std::clamp(
m_ActiveSceneIndex,
0,
static_cast<int>(m_scenes.size()) - 1);
} }
void SceneManager::HandleScene() { void SceneManager::HandleScene() {
@@ -113,7 +88,6 @@ void SceneManager::HandleScene() {
void SceneManager::Destroy() { void SceneManager::Destroy() {
if (m_scenes.empty()) { if (m_scenes.empty()) {
m_ActiveSceneIndex = 0;
return; return;
} }
@@ -124,25 +98,60 @@ void SceneManager::Destroy() {
} }
void SceneManager::SwitchScene(int index) { void SceneManager::SwitchScene(int index) {
// InputManager::GetInstance().RemoveAllBindings();
if (index < 0 || index >= static_cast<int>(m_scenes.size())) { if (index < 0 || index >= static_cast<int>(m_scenes.size())) {
throw std::out_of_range("Scene index out of range"); throw std::out_of_range("Scene index out of range");
} }
if (index == m_ActiveSceneIndex) {
return;
}
m_scenes[static_cast<std::size_t>(m_ActiveSceneIndex)]->UnloadBindings(); for (const auto& scene : m_activeScenes) {
m_ActiveSceneIndex = index; scene->UnloadBindings();
m_scenes[static_cast<std::size_t>(m_ActiveSceneIndex)]->LoadBindings(); }
m_activeScenes.clear();
m_activeScenes.push_back(m_scenes[static_cast<std::size_t>(index)]);
m_activeScenes.back()->LoadBindings();
} }
Scene &SceneManager::CreateScene(const std::string &name) { void SceneManager::AddActiveScene(int index) {
if (index < 0 || index >= static_cast<int>(m_scenes.size())) {
throw std::out_of_range("Scene index out of range");
}
const auto& scene = m_scenes[static_cast<std::size_t>(index)];
if (std::find(m_activeScenes.begin(), m_activeScenes.end(), scene) == m_activeScenes.end()) {
scene->LoadBindings();
m_activeScenes.push_back(scene);
}
}
void SceneManager::RemoveActiveScene(int index) {
if (index < 0 || index >= static_cast<int>(m_scenes.size())) {
throw std::out_of_range("Scene index out of range");
}
const auto& scene = m_scenes[static_cast<std::size_t>(index)];
const auto it = std::find(m_activeScenes.begin(), m_activeScenes.end(), scene);
if (it != m_activeScenes.end()) {
(*it)->UnloadBindings();
m_activeScenes.erase(it);
}
}
int SceneManager::GetActiveSceneId() const {
if (m_scenes.empty() || m_activeScenes.empty()) {
return -1;
}
const auto it = std::find(m_scenes.begin(), m_scenes.end(), m_activeScenes.front());
if (it == m_scenes.end()) {
return -1;
}
return static_cast<int>(std::distance(m_scenes.begin(), it));
}
Scene& SceneManager::CreateScene(const std::string& name) {
const auto scene = std::shared_ptr<Scene>(new Scene(name)); const auto scene = std::shared_ptr<Scene>(new Scene(name));
m_scenes.push_back(scene); m_scenes.push_back(scene);
if (m_scenes.size() == 1) { if (m_scenes.size() == 1) {
m_ActiveSceneIndex = 0; m_activeScenes.push_back(scene);
} }
return *scene; return *scene;
} }
+162 -20
View File
@@ -16,8 +16,10 @@
#include <destrum/ObjectModel/GameObject.h> #include <destrum/ObjectModel/GameObject.h>
#include <destrum/ObjectModel/ObjectId.h> #include <destrum/ObjectModel/ObjectId.h>
#include <destrum/ObjectModel/Transform.h> #include <destrum/ObjectModel/Transform.h>
#include <destrum/Assets/AssetReference.h>
#include <destrum/Components/Physics/Collider.h> #include <destrum/Components/Physics/Collider.h>
#include <destrum/Components/Physics/Rigidbody.h> #include <destrum/Components/Physics/Rigidbody.h>
#include <destrum/FS/AssetFS.h>
#include <destrum/Serialization/ComponentFactory.h> #include <destrum/Serialization/ComponentFactory.h>
#include <destrum/Serialization/ComponentRegistry.h> #include <destrum/Serialization/ComponentRegistry.h>
@@ -201,6 +203,78 @@ namespace {
return true; return true;
} }
[[nodiscard]] bool AssetFileExists(const std::string& path) {
if (path.empty()) {
return true;
}
std::filesystem::path fullPath;
if (path.find("://") != std::string::npos) {
try {
fullPath = AssetFS::GetInstance().GetFullPath(path);
} catch (const std::exception&) {
return false;
}
} else {
fullPath = path;
}
return std::filesystem::exists(fullPath);
}
void ValidateAssetReferences(const json& root, const std::filesystem::path& scenePath) {
const auto report = [&](const std::string& objectName, const std::string& detail) {
std::cerr << "Scene asset warning (" << scenePath << "): " << objectName
<< " references \"" << detail << "\" which could not be found\n";
};
for (const auto& objectJson : root.at("objects")) {
const std::string objectName = objectJson.value("name", "GameObject");
if (!objectJson.contains("components")) {
continue;
}
for (const auto& componentJson : objectJson.at("components")) {
if (!componentJson.contains("data")) {
continue;
}
const auto& data = componentJson.at("data");
// MeshRendererComponent: meshKey / materialKey are asset cache keys.
for (const char* key : {"meshKey", "materialKey"}) {
if (data.contains(key) && data.at(key).is_string()) {
const std::string cacheKey = data.at(key).get<std::string>();
if (cacheKey.empty()) continue;
const auto asset = AssetReference::fromCacheKey(cacheKey);
if (asset && !AssetFileExists(asset->path)) {
report(objectName, cacheKey);
}
}
}
// Generic: any nested object with a "path" field is an AssetReference.
std::function<void(const json&)> walk = [&](const json& node) {
if (node.is_object()) {
if (node.contains("path") && node.at("path").is_string()) {
const std::string path = node.at("path").get<std::string>();
if (!path.empty() && !AssetFileExists(path)) {
report(objectName, path);
}
}
for (auto it = node.begin(); it != node.end(); ++it) {
walk(it.value());
}
} else if (node.is_array()) {
for (const auto& element : node) {
walk(element);
}
}
};
walk(data);
}
}
}
} }
bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) { bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
@@ -223,6 +297,9 @@ bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
root["version"] = 2; root["version"] = 2;
root["name"] = scene.GetName(); root["name"] = scene.GetName();
root["objects"] = json::array(); root["objects"] = json::array();
if (scene.m_CurrentCameraId != InvalidObjectId) {
root["currentCamera"] = scene.m_CurrentCameraId;
}
for (const auto& objectPtr : scene.GetObjects()) { for (const auto& objectPtr : scene.GetObjects()) {
if (!objectPtr || objectPtr->IsBeingDestroyed()) { if (!objectPtr || objectPtr->IsBeingDestroyed()) {
@@ -290,30 +367,45 @@ bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
return true; return true;
} }
bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) { SceneSerializer::LoadResult SceneSerializer::LoadSceneFile(const std::filesystem::path& path) {
LoadResult result;
std::ifstream file(path);
if (!file.is_open()) {
result.errorMessage = "Failed to open scene file for reading: " + path.string();
std::cerr << result.errorMessage << '\n';
return result;
}
try {
file >> result.root;
if (!ValidateSceneJson(result.root)) {
result.errorMessage = "Invalid scene data: " + path.string();
std::cerr << result.errorMessage << '\n';
return result;
}
} catch (const std::exception& exception) {
result.errorMessage = "Failed to parse scene file: " + std::string(exception.what());
std::cerr << result.errorMessage << '\n';
return result;
}
ValidateAssetReferences(result.root, path);
result.success = true;
return result;
}
bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
std::function<void(float)> progressCallback) {
if (scene.IsIterating()) { if (scene.IsIterating()) {
std::cerr << "Cannot load a scene during an update or render phase: " std::cerr << "Cannot load a scene during an update or render phase: "
<< scene.GetName() << '\n'; << scene.GetName() << '\n';
return false; return false;
} }
RegisterEngineComponents(); if (!result.success) {
std::cerr << "Cannot construct scene from a failed load result: "
std::ifstream file(path); << result.errorMessage << '\n';
if (!file.is_open()) {
std::cerr << "Failed to open scene file for reading: " << path << '\n';
return false;
}
json root;
try {
file >> root;
if (!ValidateSceneJson(root)) {
std::cerr << "Invalid scene data: " << path << '\n';
return false;
}
} catch (const std::exception& exception) {
std::cerr << "Failed to parse scene file: " << exception.what() << '\n';
return false; return false;
} }
@@ -322,9 +414,16 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) {
return false; return false;
} }
RegisterEngineComponents();
if (progressCallback) progressCallback(0.35f);
const json& root = result.root;
// Build the replacement separately. The current scene is not touched // Build the replacement separately. The current scene is not touched
// until all objects, transforms, and components have loaded successfully. // until all objects, transforms, and components have loaded successfully.
Scene stagingScene(scene.GetName()); // No physics world is created for the staging scene - physics bodies are
// registered directly in the live scene's world once loading succeeds.
Scene stagingScene(scene.GetName(), nullptr);
std::unordered_map<ObjectId, GameObject*> idMap; std::unordered_map<ObjectId, GameObject*> idMap;
std::vector<const json*> objectJsonList; std::vector<const json*> objectJsonList;
std::vector<GameObject*> registeredObjects; std::vector<GameObject*> registeredObjects;
@@ -343,7 +442,16 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) {
objectJsonList.push_back(&objectJson); objectJsonList.push_back(&objectJson);
} }
stagingScene.CommitPendingAdditions(); if (progressCallback) progressCallback(0.4f);
// Move objects from pending to objects list without physics registration
// (staging scene has no physics world).
for (auto& pending : stagingScene.m_pendingAdditions) {
if (pending) {
stagingScene.m_objects.emplace_back(std::move(pending));
}
}
stagingScene.m_pendingAdditions.clear();
for (const json* objectJson : objectJsonList) { for (const json* objectJson : objectJsonList) {
const ObjectId id = objectJson->at("id").get<ObjectId>(); const ObjectId id = objectJson->at("id").get<ObjectId>();
@@ -392,6 +500,8 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) {
} }
} }
if (progressCallback) progressCallback(0.5f);
for (const json* objectJson : objectJsonList) { for (const json* objectJson : objectJsonList) {
GameObject* object = idMap.at(objectJson->at("id").get<ObjectId>()); GameObject* object = idMap.at(objectJson->at("id").get<ObjectId>());
if (!objectJson->contains("components")) { if (!objectJson->contains("components")) {
@@ -412,6 +522,8 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) {
} }
} }
if (progressCallback) progressCallback(0.6f);
for (const auto& objectPtr : stagingScene.GetObjects()) { for (const auto& objectPtr : stagingScene.GetObjects()) {
if (!objectPtr) { if (!objectPtr) {
continue; continue;
@@ -424,6 +536,8 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) {
} }
} }
if (progressCallback) progressCallback(0.7f);
// Register the replacement bodies in the live physics world before // Register the replacement bodies in the live physics world before
// touching the current scene. If any body fails, the old scene and // touching the current scene. If any body fails, the old scene and
// its physics state can remain intact. // its physics state can remain intact.
@@ -438,6 +552,8 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) {
registeredObjects.push_back(objectPtr.get()); registeredObjects.push_back(objectPtr.get());
} }
} }
if (progressCallback) progressCallback(0.8f);
} catch (const std::exception& exception) { } catch (const std::exception& exception) {
std::cerr << "Failed to load scene: " << exception.what() << '\n'; std::cerr << "Failed to load scene: " << exception.what() << '\n';
for (GameObject* object : registeredObjects) { for (GameObject* object : registeredObjects) {
@@ -449,12 +565,16 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) {
return false; return false;
} }
scene.UnloadBindings();
scene.RemoveAll(); scene.RemoveAll();
scene.m_objects = std::move(stagingScene.m_objects); scene.m_objects = std::move(stagingScene.m_objects);
scene.m_pendingAdditions = std::move(stagingScene.m_pendingAdditions); scene.m_pendingAdditions = std::move(stagingScene.m_pendingAdditions);
if (root.contains("name")) { if (root.contains("name")) {
scene.m_name = root.at("name").get<std::string>(); scene.m_name = root.at("name").get<std::string>();
} }
scene.m_CurrentCameraId = root.value("currentCamera", InvalidObjectId);
if (progressCallback) progressCallback(0.9f);
for (const auto& object : scene.m_objects) { for (const auto& object : scene.m_objects) {
if (object) { if (object) {
@@ -467,5 +587,27 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) {
} }
} }
if (progressCallback) progressCallback(1.0f);
return true; return true;
} }
bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path,
std::function<void(float)> progressCallback) {
if (progressCallback) progressCallback(0.0f);
auto result = LoadSceneFile(path);
if (!result.success) {
if (progressCallback) progressCallback(1.0f);
return false;
}
if (progressCallback) progressCallback(0.3f);
return ConstructScene(scene, result, std::move(progressCallback));
}
std::future<SceneSerializer::LoadResult> SceneSerializer::LoadSceneFileAsync(
const std::filesystem::path& path) {
return std::async(std::launch::async, [path]() {
return LoadSceneFile(path);
});
}
+13 -2
View File
@@ -123,7 +123,9 @@ namespace ImGuiUtils {
std::string_view sceneName, std::string_view sceneName,
const std::vector<std::shared_ptr<GameObject>>& objectsSource, const std::vector<std::shared_ptr<GameObject>>& objectsSource,
const std::vector<std::shared_ptr<GameObject>>& pendingAdditions, const std::vector<std::shared_ptr<GameObject>>& pendingAdditions,
ObjectId& selectedObjectId) { ObjectId& selectedObjectId,
const std::function<GameObject*()>& createGameObject,
bool* open) {
RegisterEngineComponents(); RegisterEngineComponents();
std::vector<GameObject*> objects; std::vector<GameObject*> objects;
@@ -154,7 +156,7 @@ namespace ImGuiUtils {
} }
ImGui::SetNextWindowSize(ImVec2(640.0f, 520.0f), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(640.0f, 520.0f), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Inspector")) { if (!ImGui::Begin("Inspector", open)) {
ImGui::End(); ImGui::End();
return; return;
} }
@@ -165,6 +167,15 @@ namespace ImGuiUtils {
ImGui::Text("Scene: %s", displaySceneName.c_str()); ImGui::Text("Scene: %s", displaySceneName.c_str());
ImGui::SameLine(); ImGui::SameLine();
ImGui::TextDisabled("%zu GameObjects | %zu roots", objects.size(), roots.size()); ImGui::TextDisabled("%zu GameObjects | %zu roots", objects.size(), roots.size());
if (createGameObject) {
ImGui::SameLine();
if (ImGui::Button("+ Add GameObject")) {
if (GameObject* created = createGameObject()) {
objects.push_back(created);
selectedObjectId = created->GetId();
}
}
}
ImGui::Separator(); ImGui::Separator();
const float availableWidth = ImGui::GetContentRegionAvail().x; const float availableWidth = ImGui::GetContentRegionAvail().x;
+24
View File
@@ -64,8 +64,32 @@ set(ASSIMP_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(ASSIMP_INJECT_DEBUG_POSTFIX OFF CACHE BOOL "" FORCE) set(ASSIMP_INJECT_DEBUG_POSTFIX OFF CACHE BOOL "" FORCE)
set(ASSIMP_INSTALL OFF CACHE BOOL "" FORCE) set(ASSIMP_INSTALL OFF CACHE BOOL "" FORCE)
set(ASSIMP_WARNINGS_AS_ERRORS OFF CACHE BOOL "" FORCE) set(ASSIMP_WARNINGS_AS_ERRORS OFF CACHE BOOL "" FORCE)
# assimp is unbearably slow unoptimized (a big FBX takes ~10s), so build it optimized even in Debug.
# This only affects targets created inside the assimp directory (including its bundled zlib).
set(DESTRUM_SAVED_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG}")
set(DESTRUM_SAVED_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}")
if (MSVC)
foreach (flags_var CMAKE_C_FLAGS_DEBUG CMAKE_CXX_FLAGS_DEBUG)
string(REPLACE "/RTC1" "" ${flags_var} "${${flags_var}}")
string(REPLACE "/Od" "/O2" ${flags_var} "${${flags_var}}")
string(REPLACE "/Ob0" "/Ob2" ${flags_var} "${${flags_var}}")
endforeach ()
else ()
string(APPEND CMAKE_C_FLAGS_DEBUG " -O2")
string(APPEND CMAKE_CXX_FLAGS_DEBUG " -O2")
endif ()
add_subdirectory(assimp) add_subdirectory(assimp)
# assimp's own CMakeLists appends "/Od" to the Debug flags again, target options come later and win
if (MSVC)
target_compile_options(assimp PRIVATE $<$<CONFIG:Debug>:/O2>)
endif ()
set(CMAKE_C_FLAGS_DEBUG "${DESTRUM_SAVED_C_FLAGS_DEBUG}")
set(CMAKE_CXX_FLAGS_DEBUG "${DESTRUM_SAVED_CXX_FLAGS_DEBUG}")
set(IMGUI_DIR "${CMAKE_CURRENT_LIST_DIR}/imgui") set(IMGUI_DIR "${CMAKE_CURRENT_LIST_DIR}/imgui")
add_library(imgui STATIC add_library(imgui STATIC

Some files were not shown because too many files have changed in this diff Show More