diff --git a/code/def_files/data/effects/deferred-f.sdr b/code/def_files/data/effects/deferred-f.sdr index fb5d9021ebd..b8702e9a434 100644 --- a/code/def_files/data/effects/deferred-f.sdr +++ b/code/def_files/data/effects/deferred-f.sdr @@ -65,6 +65,12 @@ uniform shadowCascadeParams { float rtShadowBiasMax; mat4 shadow_mv_matrix; + + // shadow_ray_cull_mask/shadow_ray_world_offset: see shadow_cascade_static_data + // (uniform_structs.h) for what these mean and shadow_cascade_params_bind() + // (shadows.cpp) for how they're set. + vec3 shadow_ray_world_offset; + int shadow_ray_cull_mask; mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES]; vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4]; vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4]; @@ -310,12 +316,13 @@ void main() if (enable_shadows != 0) { #ifdef RT_SHADOWS - vec3 worldPos = (inv_view_matrix * vec4(position, 1.0)).xyz; + // Zero outside the cockpit pass; see shadow_cascade_static_data (uniform_structs.h). + vec3 worldPos = (inv_view_matrix * vec4(position, 1.0)).xyz + shadow_ray_world_offset; vec3 worldNormal = normalize((inv_view_matrix * vec4(normal, 0.0)).xyz); vec3 worldLightDir = normalize((inv_view_matrix * vec4(lightDir, 0.0)).xyz); float rtShadowBias = computeRtShadowBias(length(position), rtShadowBiasMin, rtShadowBiasMax); - attenuation *= traceShadowRay(shadow_tlas, worldPos, worldNormal, worldLightDir, lightDist, rtShadowBias); + attenuation *= traceShadowRay(shadow_tlas, worldPos, worldNormal, worldLightDir, lightDist, rtShadowBias, uint(shadow_ray_cull_mask)); #else vec4 fragShadowPos = shadow_mv_matrix * inv_view_matrix * vec4(position, 1.0); vec4 fragShadowUV[NUM_SHADOW_CASCADES]; diff --git a/code/def_files/data/effects/main-f.sdr b/code/def_files/data/effects/main-f.sdr index f8390b496fc..2829e2be796 100644 --- a/code/def_files/data/effects/main-f.sdr +++ b/code/def_files/data/effects/main-f.sdr @@ -103,6 +103,12 @@ uniform shadowCascadeParams { float rtShadowBiasMax; mat4 shadow_mv_matrix; + + // shadow_ray_cull_mask/shadow_ray_world_offset: see shadow_cascade_static_data + // (uniform_structs.h) for what these mean and shadow_cascade_params_bind() + // (shadows.cpp) for how they're set. + vec3 shadow_ray_world_offset; + int shadow_ray_cull_mask; mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES]; vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4]; vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4]; @@ -237,7 +243,8 @@ vec3 CalculateLighting(vec3 normal, vec3 diffuseMaterial, vec3 specularMaterial, rtShadowsActive = true; #prereplace ENDIF_FLAG //MODEL_SDR_FLAG_SHADOWS mat4 invView = inverse(viewMatrix); - vec3 worldPos = (invView * vertIn.position).xyz; + // Zero outside the cockpit pass; see shadow_cascade_static_data (uniform_structs.h). + vec3 worldPos = (invView * vertIn.position).xyz + shadow_ray_world_offset; vec3 worldNormal = normalize((invView * vec4(normal, 0.0)).xyz); int shadowedDirectionalCount = 0; #endif @@ -248,7 +255,7 @@ vec3 CalculateLighting(vec3 normal, vec3 diffuseMaterial, vec3 specularMaterial, if (rtShadowsActive && lights[i].light_type == LT_DIRECTIONAL && shadowedDirectionalCount < MAX_RT_SHADOW_LIGHTS) { vec3 worldSunDir = normalize((invView * vec4(lights[i].position.xyz, 0.0)).xyz); float rtShadowBias = computeRtShadowBias(length(vertIn.position.xyz), rtShadowBiasMin, rtShadowBiasMax); - shadow = traceShadowRay(shadow_tlas, worldPos, worldNormal, worldSunDir, RT_SHADOW_MAX_DISTANCE, rtShadowBias); + shadow = traceShadowRay(shadow_tlas, worldPos, worldNormal, worldSunDir, RT_SHADOW_MAX_DISTANCE, rtShadowBias, uint(shadow_ray_cull_mask)); ++shadowedDirectionalCount; } else { shadow = 1.0; @@ -270,6 +277,7 @@ vec3 CalculateLighting(vec3 normal, vec3 diffuseMaterial, vec3 specularMaterial, lightDiffuse += (lights[i].diffuse_color.rgb * diffuseFactor * NdotL * attenuation) * shadow; lightSpecular += lights[i].diffuse_color.rgb * computeLighting(specularMaterial, diffuseMaterial, lightDir, normal, halfVec, eyeDir, roughness, fresnel, NdotL) * attenuation * shadow; } + return diffuseMaterial * lightAmbient + lightSpecular; } diff --git a/code/def_files/data/effects/main-v.sdr b/code/def_files/data/effects/main-v.sdr index 7933ecf3578..d7639d84a55 100644 --- a/code/def_files/data/effects/main-v.sdr +++ b/code/def_files/data/effects/main-v.sdr @@ -117,6 +117,12 @@ uniform shadowCascadeParams { float rtShadowBiasMax; mat4 shadow_mv_matrix; + + // shadow_ray_cull_mask/shadow_ray_world_offset: see shadow_cascade_static_data + // (uniform_structs.h) for what these mean and shadow_cascade_params_bind() + // (shadows.cpp) for how they're set. + vec3 shadow_ray_world_offset; + int shadow_ray_cull_mask; mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES]; vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4]; vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4]; diff --git a/code/def_files/data/effects/shadow_map-g.sdr b/code/def_files/data/effects/shadow_map-g.sdr index 585276dca69..75e370faeda 100644 --- a/code/def_files/data/effects/shadow_map-g.sdr +++ b/code/def_files/data/effects/shadow_map-g.sdr @@ -14,6 +14,12 @@ layout (std140) uniform shadowCascadeParams { float rtShadowBiasMax; mat4 shadow_mv_matrix; + + // shadow_ray_cull_mask/shadow_ray_world_offset: see shadow_cascade_static_data + // (uniform_structs.h) for what these mean and shadow_cascade_params_bind() + // (shadows.cpp) for how they're set. + vec3 shadow_ray_world_offset; + int shadow_ray_cull_mask; mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES]; vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4]; vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4]; diff --git a/code/def_files/data/effects/shadow_map-v.sdr b/code/def_files/data/effects/shadow_map-v.sdr index f3e92cd21d1..ed416a4bb18 100644 --- a/code/def_files/data/effects/shadow_map-v.sdr +++ b/code/def_files/data/effects/shadow_map-v.sdr @@ -44,6 +44,12 @@ uniform shadowCascadeParams { float rtShadowBiasMax; mat4 shadow_mv_matrix; + + // shadow_ray_cull_mask/shadow_ray_world_offset: see shadow_cascade_static_data + // (uniform_structs.h) for what these mean and shadow_cascade_params_bind() + // (shadows.cpp) for how they're set. + vec3 shadow_ray_world_offset; + int shadow_ray_cull_mask; mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES]; vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4]; vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4]; diff --git a/code/def_files/data/effects/shadows.sdr b/code/def_files/data/effects/shadows.sdr index b13e4cbf41c..b82937e139c 100644 --- a/code/def_files/data/effects/shadows.sdr +++ b/code/def_files/data/effects/shadows.sdr @@ -114,14 +114,20 @@ float computeRtShadowBias(float cameraDist, float biasMin, float biasMax) // // bias is a world-unit offset applied along worldNormal before tracing, to clear // the source triangle -- see computeRtShadowBias() above for how callers derive it. -float traceShadowRay(accelerationStructureEXT tlas, vec3 worldPos, vec3 worldNormal, vec3 worldLightDir, float tMax, float bias) +// +// cullMask selects which TLAS instances this ray can hit (Vulkan spec: an instance +// is a candidate iff (cullMask & instance.mask) != 0). Callers should pass +// shadow_ray_cull_mask (shadowCascadeParams uniform block) rather than a literal -- +// see SHADOW_RAY_CULL_MASK_EXCLUDE_VIEWER_HULL/TLAS_MASK_VIEWER_HULL (shadows.h) +// for how it excludes the viewer ship's own hull outside the cockpit pass. +float traceShadowRay(accelerationStructureEXT tlas, vec3 worldPos, vec3 worldNormal, vec3 worldLightDir, float tMax, float bias, uint cullMask) { vec3 origin = worldPos + worldNormal * bias; vec3 direction = normalize(worldLightDir); rayQueryEXT rq; rayQueryInitializeEXT(rq, tlas, gl_RayFlagsOpaqueEXT | gl_RayFlagsTerminateOnFirstHitEXT, - 0xFF, origin, 0.001, direction, tMax); + cullMask, origin, 0.001, direction, tMax); while (rayQueryProceedEXT(rq)) {} return (rayQueryGetIntersectionTypeEXT(rq, true) == gl_RayQueryCommittedIntersectionNoneEXT) ? 1.0 : 0.0; diff --git a/code/graphics/opengl/gropengldeferred.cpp b/code/graphics/opengl/gropengldeferred.cpp index 35d67419977..a198c7274e6 100644 --- a/code/graphics/opengl/gropengldeferred.cpp +++ b/code/graphics/opengl/gropengldeferred.cpp @@ -316,9 +316,7 @@ void gr_opengl_deferred_lighting_finish() auto header = light_uniform_aligner.getHeader(); if (Shadow_quality != ShadowQuality::Disabled) { vm_inverse_matrix4(&header->inv_view_matrix, &Shadow_view_matrix_render); - int offset = (Lighting_mode == lighting_mode::COCKPIT) ? 0 : Num_cockpit_shadow_cascades; - int count = (Lighting_mode == lighting_mode::COCKPIT) ? Num_cockpit_shadow_cascades : Num_shadow_cascades; - shadow_cascade_params_bind(offset, count); + shadow_cascade_params_bind_deferred(); } header->invScreenWidth = 1.0f / gr_screen.max_w; diff --git a/code/graphics/shadows.cpp b/code/graphics/shadows.cpp index 82713c6c5db..6dfa331b448 100644 --- a/code/graphics/shadows.cpp +++ b/code/graphics/shadows.cpp @@ -665,11 +665,11 @@ matrix shadows_start_render(matrix *eye_orient, vec3d *eye_pos, fov_t fov, fov_t gr_shadow_map_start(&Shadow_view_matrix_light, &light_matrix, eye_pos, true); if (cascade_distances_override) - shadow_cascade_params_bind(max_skip_override, num_cascades - max_skip_override); + shadow_cascade_params_bind(max_skip_override, num_cascades - max_skip_override, vmd_zero_vector); else if (render_cockpit_cascades) - shadow_cascade_params_bind(0, num_cascades); + shadow_cascade_params_bind(0, num_cascades, vmd_zero_vector); else - shadow_cascade_params_bind(Num_cockpit_shadow_cascades, Num_shadow_cascades); + shadow_cascade_params_bind(Num_cockpit_shadow_cascades, Num_shadow_cascades, vmd_zero_vector); return light_matrix; } @@ -764,9 +764,9 @@ static void render_viewer_shadow(object* objp, const matrix* light_matrix, gr_shadow_map_start(&dummy_view, light_matrix, &vmd_zero_vector, false); if (casts_shadow_on_cockpit) - shadow_cascade_params_bind(0, Num_cockpit_shadow_cascades + Num_shadow_cascades); + shadow_cascade_params_bind(0, Num_cockpit_shadow_cascades + Num_shadow_cascades, vmd_zero_vector); else - shadow_cascade_params_bind(Num_cockpit_shadow_cascades, Num_shadow_cascades); + shadow_cascade_params_bind(Num_cockpit_shadow_cascades, Num_shadow_cascades, vmd_zero_vector); model_clear_instance(sip->model_num); polymodel_instance* pmi = nullptr; @@ -782,17 +782,14 @@ static void render_viewer_shadow(object* objp, const matrix* light_matrix, viewer_list.render_all(); } - const bool renderCockpitModel = (Viewer_mode != VM_TOPDOWN) && sip->cockpit_model_num >= 0 && !Disable_cockpits; + const bool renderCockpitModel = ship_player_cockpit_model_would_render(sip); if (renderCockpitModel && !Shadow_disable_overrides.disable_cockpit) { matrix4 dummy_view; gr_shadow_map_start(&dummy_view, light_matrix, &vmd_zero_vector, false); - shadow_cascade_params_bind(0, Num_cockpit_shadow_cascades); + shadow_cascade_params_bind(0, Num_cockpit_shadow_cascades, vmd_zero_vector); - vec3d cockpit_offset = sip->cockpit_offset; - vm_vec_unrotate(&cockpit_offset, &cockpit_offset, &objp->orient); - if (!Disable_cockpit_sway) - cockpit_offset += sip->cockpit_sway_val * objp->phys_info.acceleration; + vec3d cockpit_offset = ship_cockpit_render_offset(sip, objp); model_clear_instance(sip->cockpit_model_num); polymodel_instance* cockpit_pmi = nullptr; @@ -1035,7 +1032,8 @@ void shadow_cascade_params_shutdown() { } int Shadow_cascade_count = 0; -void shadow_cascade_params_bind(int cascade_offset, int cascade_count) { +void shadow_cascade_params_bind(int cascade_offset, int cascade_count, const vec3d& world_offset, + bool allow_viewer_self_shadow) { if (!Shadow_cascade_params_buffer.isValid()) { return; } @@ -1058,6 +1056,10 @@ void shadow_cascade_params_bind(int cascade_offset, int cascade_count) { static_data.rtShadowBiasMax = Rt_shadow_bias_max; static_data.shadow_mv_matrix = Shadow_view_matrix_light; + // See shadow_cascade_params_bind()'s declaration (shadows.h) for what these mean. + static_data.shadow_ray_cull_mask = allow_viewer_self_shadow ? 0xFF : SHADOW_RAY_CULL_MASK_EXCLUDE_VIEWER_HULL; + static_data.shadow_ray_world_offset = world_offset; + Shadow_cascade_count = cascade_count; offset += sizeof(graphics::shadow_cascade_static_data); @@ -1082,10 +1084,25 @@ void shadow_cascade_params_bind(int cascade_offset, int cascade_count) { } offset += sizeof(float) * padding; - gr_update_buffer_data_offset(Shadow_cascade_params_buffer, 0, required_size, buffer.data()); + // Must be the full-replacement update, not _offset: this is called several times per + // frame with different content, and on Vulkan's streaming buffer _offset only bump- + // allocates fresh GPU memory on the first call, silently overwriting the same region + // on later calls -- so every draw this frame (command buffers are pre-recorded) would + // read whichever bind ran last, not the one current when it was recorded. This always + // bump-allocates fresh on Vulkan (matching deferred_global_data/deferred_light_data) + // and is a plain buffer-orphaning glBufferData() on OpenGL. + gr_update_buffer_data(Shadow_cascade_params_buffer, required_size, buffer.data()); gr_bind_uniform_buffer(uniform_block_type::ShadowCascadeParams, 0, required_size, Shadow_cascade_params_buffer); } +void shadow_cascade_params_bind_deferred() { + int offset = (Lighting_mode == lighting_mode::COCKPIT) ? 0 : Num_cockpit_shadow_cascades; + int count = (Lighting_mode == lighting_mode::COCKPIT) ? Num_cockpit_shadow_cascades : Num_shadow_cascades; + vec3d world_offset = (Lighting_mode == lighting_mode::COCKPIT && Viewer_obj != nullptr) ? Viewer_obj->pos : vmd_zero_vector; + bool allow_viewer_self_shadow = Lighting_mode == lighting_mode::COCKPIT && ship_render_player_ship_casts_shadow_on_cockpit(); + shadow_cascade_params_bind(offset, count, world_offset, allow_viewer_self_shadow); +} + shadow_render_list::shadow_render_list() { reset(); } diff --git a/code/graphics/shadows.h b/code/graphics/shadows.h index 63a3abd3258..317730f3d6e 100644 --- a/code/graphics/shadows.h +++ b/code/graphics/shadows.h @@ -95,6 +95,15 @@ extern SCP_vector Shadow_proj_matrix; extern SCP_vector Shadow_cascade_distances; extern int Shadow_cascade_count; +// TLAS ray-cull mask bit reserved for the viewer ship's own hull instance +// (VulkanRaytracingManager::gatherShadowCasterInstances()'s OBJ_SHIP case), so shadow +// rays can selectively exclude it -- see shadow_cascade_params_bind()'s +// shadow_ray_cull_mask and traceShadowRay() (shadows.sdr). +constexpr uint8_t TLAS_MASK_VIEWER_HULL = 0x80; +// Default TLAS instance mask (visible to every ray) minus TLAS_MASK_VIEWER_HULL -- +// the shadow_ray_cull_mask used everywhere the viewer's own hull must not self-shadow. +constexpr uint8_t SHADOW_RAY_CULL_MASK_EXCLUDE_VIEWER_HULL = static_cast(~TLAS_MASK_VIEWER_HULL); + void shadows_construct_light_frustum(vec3d *min_out, vec3d *max_out, vec3d light_vec, matrix *orient, vec3d *pos, fov_t fov, float aspect, float z_near, float z_far); bool shadows_obj_in_frustum(object *objp, vec3d *min, vec3d *max, matrix *light_orient); void shadows_render_all(fov_t fov, matrix *eye_orient, vec3d *eye_pos, @@ -102,7 +111,23 @@ void shadows_render_all(fov_t fov, matrix *eye_orient, vec3d *eye_pos, void shadow_cascade_params_init(); void shadow_cascade_params_shutdown(); -void shadow_cascade_params_bind(int cascade_offset, int cascade_count); + +// world_offset: added to traceShadowRay()'s reconstructed world position before tracing. +// Zero for every pass rendering in true world space; nonzero only for the cockpit pass +// (see shadow_cascade_static_data::shadow_ray_world_offset, uniform_structs.h, for the +// derivation). A pass with draws in more than one internal frame -- e.g. +// ship_render_player_ship()'s hull vs. cockpit draws -- must rebind before each one. +// allow_viewer_self_shadow: lets the viewer ship's own hull (TLAS_MASK_VIEWER_HULL, +// shadows.h) cast shadows in this pass. True only for the cockpit's own shading. +void shadow_cascade_params_bind(int cascade_offset, int cascade_count, const vec3d& world_offset, + bool allow_viewer_self_shadow = false); + +// Binds the cascade range/world_offset/self-shadow-mask appropriate for the current +// Lighting_mode, for callers that (unlike ship_render_player_ship()) only ever shade +// one frame per pass -- currently the deferred-lighting full-screen passes +// (gropengldeferred.cpp, VulkanPostProcessingLighting.cpp). See shadow_cascade_params_bind() +// above for what world_offset/allow_viewer_self_shadow mean and their known limitation here. +void shadow_cascade_params_bind_deferred(); matrix shadows_start_render(matrix *eye_orient, vec3d *eye_pos, fov_t fov, fov_t cockpit_fov, float aspect, const std::optional>& cascade_distances_override = std::nullopt); void shadows_end_render(); diff --git a/code/graphics/util/uniform_structs.h b/code/graphics/util/uniform_structs.h index 974ad275732..638b3d1a6a5 100644 --- a/code/graphics/util/uniform_structs.h +++ b/code/graphics/util/uniform_structs.h @@ -147,7 +147,33 @@ struct shadow_cascade_static_data { float rtShadowBiasMin; float rtShadowBiasMax; matrix4 shadow_mv_matrix; + + // World-space correction added to the RT shadow ray's reconstructed position before + // tracing. Zero everywhere except the cockpit pass: ship_render_player_ship() renders + // the cockpit using a view matrix anchored at `leaning_position` and model positions + // offset in that same un-translated frame (never combined with objp->pos), so the + // reconstructed position isn't true world space like the shadow TLAS is. Adding the + // viewer ship's world position back here closes that gap -- see + // shadow_cascade_params_bind() (shadows.cpp) for where this is set. + // + // Declared before shadow_ray_cull_mask so the trailing int packs into this vec3's + // std140 alignment padding instead of needing a manual pad field (std140 gives a vec3 + // 16-byte alignment but only 12-byte size, so a following scalar fills the gap for free). + vec3d shadow_ray_world_offset; + + // Ray cull mask for raytraced shadow queries (traceShadowRay()/shadows.sdr). Excludes + // the viewer ship's own hull TLAS instance (TLAS_MASK_VIEWER_HULL, shadows.h) outside + // the cockpit pass, matching the rasterized path's exclusion of Viewer_obj from the + // main shadow cascades. Set in shadow_cascade_params_bind() (shadows.cpp). + int shadow_ray_cull_mask; }; +// Must match the GLSL shadowCascadeParams block's implicit std140 padding +// exactly (16 [4 leading scalars] + 64 [matrix4] + 12 [shadow_ray_world_offset] +// + 4 [shadow_ray_cull_mask, packed into the vec3's alignment padding] = 96) -- +// shadow_cascade_params_bind() packs shadow_proj_matrix[] immediately after +// this struct via sizeof(), so a mismatch here silently shifts every cascade +// matrix in the buffer, in both backends. +static_assert(sizeof(shadow_cascade_static_data) == 96, "shadow_cascade_static_data must match the GLSL shadowCascadeParams layout (see comment above)"); enum class NanoVGShaderType: int32_t { FillGradient = 0, FillImage = 1, Simple = 2, Image = 3 diff --git a/code/graphics/vulkan/VulkanPostProcessing.cpp b/code/graphics/vulkan/VulkanPostProcessing.cpp index 13d13675437..813636cc707 100644 --- a/code/graphics/vulkan/VulkanPostProcessing.cpp +++ b/code/graphics/vulkan/VulkanPostProcessing.cpp @@ -425,10 +425,12 @@ bool VulkanPostProcessor::createSceneTargets(vk::Extent2D extent) m_sceneColor.height = extent.height; // Scene depth target + // eTransferDst needed for restoreSceneDepth (backup→depth copy after the cockpit render) if (!createImage(extent.width, extent.height, m_ctx.depthFormat, vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eSampled - | vk::ImageUsageFlagBits::eTransferSrc, + | vk::ImageUsageFlagBits::eTransferSrc + | vk::ImageUsageFlagBits::eTransferDst, vk::ImageAspectFlagBits::eDepth, // View uses depth-only aspect m_sceneDepth.image, m_sceneDepth.view, m_sceneDepth.allocation)) { nprintf(("vulkan", "VulkanPostProcessor: Failed to create scene depth image!\n")); @@ -463,6 +465,22 @@ bool VulkanPostProcessor::createSceneTargets(vk::Extent2D extent) m_sceneDepthCopy.width = extent.width; m_sceneDepthCopy.height = extent.height; + // Scene depth backup (holds the scene's depth while the cockpit renders) + // Copy target on the way out, copy source on the way back in. No shader ever reads it, + // but eSampled stays: createImage() always builds a view, and a view needs at least one + // non-transfer usage bit (VUID-VkImageViewCreateInfo-image-04441). + if (!createImage(extent.width, extent.height, m_ctx.depthFormat, + vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eTransferSrc + | vk::ImageUsageFlagBits::eSampled, + vk::ImageAspectFlagBits::eDepth, + m_sceneDepthBackup.image, m_sceneDepthBackup.view, m_sceneDepthBackup.allocation)) { + nprintf(("vulkan", "VulkanPostProcessor: Failed to create scene depth backup image!\n")); + return false; + } + m_sceneDepthBackup.format = m_ctx.depthFormat; + m_sceneDepthBackup.width = extent.width; + m_sceneDepthBackup.height = extent.height; + return true; } @@ -476,6 +494,7 @@ void VulkanPostProcessor::destroySceneTargets() m_ctx.destroyTarget(m_sceneColor); m_ctx.destroyTarget(m_sceneDepth); m_ctx.destroyTarget(m_sceneDepthCopy); + m_ctx.destroyTarget(m_sceneDepthBackup); } bool VulkanPostProcessor::createSceneFramebuffer() @@ -581,6 +600,29 @@ void VulkanPostProcessor::copySceneDepth(vk::CommandBuffer cmd) const imageAspectFromFormat(m_ctx.depthFormat)); } +void VulkanPostProcessor::saveSceneDepth(vk::CommandBuffer cmd) const +{ + // Called outside a render pass, so scene depth is in eDepthStencilAttachmentOptimal + // (the scene/G-buffer render pass finalLayout). The backup keeps no content between + // frames, hence eUndefined: the copy overwrites every texel. + copyImageToImage(cmd, + m_sceneDepth.image, vk::ImageLayout::eDepthStencilAttachmentOptimal, vk::ImageLayout::eDepthStencilAttachmentOptimal, + m_sceneDepthBackup.image, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferSrcOptimal, + m_ctx.sceneExtent, + imageAspectFromFormat(m_ctx.depthFormat)); +} + +void VulkanPostProcessor::restoreSceneDepth(vk::CommandBuffer cmd) const +{ + // The backup was left in eTransferSrcOptimal by saveSceneDepth() and stays there -- + // the next save transitions it from eUndefined anyway. + copyImageToImage(cmd, + m_sceneDepthBackup.image, vk::ImageLayout::eTransferSrcOptimal, vk::ImageLayout::eTransferSrcOptimal, + m_sceneDepth.image, vk::ImageLayout::eDepthStencilAttachmentOptimal, vk::ImageLayout::eDepthStencilAttachmentOptimal, + m_ctx.sceneExtent, + imageAspectFromFormat(m_ctx.depthFormat)); +} + void VulkanPostProcessor::blitToSwapChain(vk::CommandBuffer cmd) { // C8: unlike PostProcessContext::drawFullscreenTriangle (which begins and ends @@ -822,15 +864,34 @@ void VulkanPostProcessor::encodeOutputSdr(vk::CommandBuffer cmd, vk::RenderPass void vulkan_post_process_begin() {} void vulkan_post_process_end() {} -// No-op: In OpenGL, save/restore swap the depth attachment between -// Scene_depth_texture and Cockpit_depth_texture to isolate cockpit -// depth from the main scene. In Vulkan, the render pass loadOp=eClear -// clears depth at the start of each scene pass, and separate cockpit -// depth isolation is not yet implemented. Called from ship.cpp during -// cockpit rendering but degrades gracefully as a no-op (cockpit just -// shares the scene depth buffer). -void vulkan_post_process_save_zbuffer() {} -void vulkan_post_process_restore_zbuffer() {} +// Isolate the cockpit's depth from the scene's, the way OpenGL does by swapping the +// depth attachment between Scene_depth_texture and Cockpit_depth_texture. A Vulkan +// framebuffer owns its attachments, so the scene depth is parked in a backup image and +// the live depth buffer is cleared for the cockpit; the restore puts the scene back. +// +// Both halves are needed. Without the clear the ship hull the scene already drew wins +// the depth test against the cockpit around the camera. Without the restore the +// post-processing passes that sample scene depth (lightshafts) see cockpit depth. +void vulkan_post_process_save_zbuffer() +{ + auto* renderer = getRendererInstance(); + if (renderer != nullptr) { + renderer->saveSceneDepth(); + } + + // Unconditional, exactly as in OpenGL: when there is nothing to park (post-processing + // off, or no scene render pass), clearing is still what gives the cockpit a depth + // buffer of its own. + gr_zbuffer_clear(TRUE); +} + +void vulkan_post_process_restore_zbuffer() +{ + auto* renderer = getRendererInstance(); + if (renderer != nullptr) { + renderer->restoreSceneDepth(); + } +} void vulkan_post_process_set_effect(const char* name, int value, const vec3d* rgb) { diff --git a/code/graphics/vulkan/VulkanPostProcessing.h b/code/graphics/vulkan/VulkanPostProcessing.h index b66867a0676..20748afa160 100644 --- a/code/graphics/vulkan/VulkanPostProcessing.h +++ b/code/graphics/vulkan/VulkanPostProcessing.h @@ -990,6 +990,31 @@ class VulkanPostProcessor { */ void copySceneDepth(vk::CommandBuffer cmd) const; + /** + * @brief Copy scene depth aside so the cockpit can render on a cleared depth buffer + * + * The cockpit model shares the world with the ship hull around it, so it needs a + * depth buffer of its own -- OpenGL gets one by swapping the depth attachment to + * Cockpit_depth_texture. Here the scene depth stays the attachment and its content + * is parked in a backup image instead, which restoreSceneDepth() puts back. + * + * Must be called outside a render pass. Leaves scene depth in + * eDepthStencilAttachmentOptimal and the backup in eTransferSrcOptimal. + * + * @param cmd Active command buffer (must be outside a render pass) + */ + void saveSceneDepth(vk::CommandBuffer cmd) const; + + /** + * @brief Put the depth that saveSceneDepth() parked back into the scene depth buffer + * + * Discards whatever the cockpit wrote, which is the point: the post-processing + * passes that sample scene depth (lightshafts) must see the scene, not the cockpit. + * + * @param cmd Active command buffer (must be outside a render pass) + */ + void restoreSceneDepth(vk::CommandBuffer cmd) const; + /** * @brief Check if LDR targets are available (tonemapping + FXAA ready) */ @@ -1164,6 +1189,7 @@ class VulkanPostProcessor { RenderTarget m_sceneColor; // RGBA16F HDR scene color RenderTarget m_sceneDepth; // Depth buffer for scene RenderTarget m_sceneDepthCopy; // Samplable copy of scene depth (for soft particles) + RenderTarget m_sceneDepthBackup; // Scene depth parked across the cockpit render (transfer only) RenderTarget m_sceneEffect; // RGBA16F effect/composite (snapshot of scene color) // Scene render pass and framebuffer diff --git a/code/graphics/vulkan/VulkanPostProcessingLighting.cpp b/code/graphics/vulkan/VulkanPostProcessingLighting.cpp index 08dda92663c..e9ce9dde7f9 100644 --- a/code/graphics/vulkan/VulkanPostProcessingLighting.cpp +++ b/code/graphics/vulkan/VulkanPostProcessingLighting.cpp @@ -507,10 +507,7 @@ void VulkanDeferredLighting::render(vk::CommandBuffer cmd) if (m_shadow->isInitialized() && Shadow_quality != ShadowQuality::Disabled) { vm_inverse_matrix4(&header->inv_view_matrix, &Shadow_view_matrix_render); - - int offset = (Lighting_mode == lighting_mode::COCKPIT) ? 0 : Num_cockpit_shadow_cascades; - int count = (Lighting_mode == lighting_mode::COCKPIT) ? Num_cockpit_shadow_cascades : Num_shadow_cascades; - shadow_cascade_params_bind(offset, count); + shadow_cascade_params_bind_deferred(); } } diff --git a/code/graphics/vulkan/VulkanRaytracing.h b/code/graphics/vulkan/VulkanRaytracing.h index 45fe2b22e1c..7023552c81d 100644 --- a/code/graphics/vulkan/VulkanRaytracing.h +++ b/code/graphics/vulkan/VulkanRaytracing.h @@ -192,23 +192,41 @@ class VulkanRaytracingManager { // Shared by walkSubmodelTree/addSingleSubmodelInstance: appends one TLAS // instance referencing blasAddress, placed at the given world orient/pos. + // `mask` is the instance's ray-cull mask (vk::AccelerationStructureInstanceKHR::mask, + // see TLAS_MASK_VIEWER_HULL in shadows.h); defaults to 0xFF (visible to every ray). static void pushInstance(SCP_vector& instances, vk::DeviceAddress blasAddress, const matrix& orient, - const vec3d& pos); + const vec3d& pos, + uint8_t mask = 0xFF); void gatherShadowCasterInstances(SCP_vector& instances); + // Adds one instance walk for Viewer_obj's cockpit polymodel (sip->cockpit_model_num), + // which -- unlike ships/asteroids/debris -- has no backing `object` for + // gatherShadowCasterInstances() to discover it through. Mirrors the gating + // render_viewer_shadow()'s cockpit block uses (shadows.cpp) so the cockpit only + // gets a TLAS instance when it would also get a rasterized shadow-map pass. + void gatherCockpitShadowCasterInstance(SCP_vector& instances); + // `skipDetailBoxCheck`: the detail-box gate (submodelPassesDetailBox()) compares + // against the global `Eye_position`, which isn't correct for the cockpit model (its + // rasterized detail-box checks use a cockpit-relative eye position instead -- see + // shadows.cpp). gatherCockpitShadowCasterInstance() passes true to skip the check + // rather than derive that; cockpit models are small and sit right against the camera, + // so detail-box culling is unlikely to matter there. Every other caller defaults false. void walkSubmodelTree(SCP_vector& instances, transform_stack& stack, const polymodel* pm, const polymodel_instance* pmi, - int submodel_num); + int submodel_num, + uint8_t mask = 0xFF, + bool skipDetailBoxCheck = false); void addSingleSubmodelInstance(SCP_vector& instances, const polymodel* pm, const polymodel_instance* pmi, int submodel_num, const matrix& orient, - const vec3d& pos); + const vec3d& pos, + uint8_t mask = 0xFF); // One full set of grow-only TLAS resources per frame-in-flight slot, indexed // by currentFrameIndex() -- NOT a single shared instance. buildTlas() diff --git a/code/graphics/vulkan/VulkanRaytracingTlas.cpp b/code/graphics/vulkan/VulkanRaytracingTlas.cpp index d1ba33a9878..29278110868 100644 --- a/code/graphics/vulkan/VulkanRaytracingTlas.cpp +++ b/code/graphics/vulkan/VulkanRaytracingTlas.cpp @@ -1,5 +1,6 @@ // Per-frame TLAS: gathers the current shadow-casting object set (ships, -// asteroids, debris) into one top-level acceleration structure each frame. +// asteroids, debris, raw POFs/props) into one top-level acceleration +// structure each frame. // Split out from VulkanRaytracing.cpp since this is the only part of the // raytracing manager that reaches into Ships/Asteroids/Debris/Objects, a // distinctly different dependency set from the BLAS cache in @@ -14,6 +15,9 @@ #include "asteroid/asteroid.h" #include "debris/debris.h" +#include "globalincs/systemvars.h" +#include "graphics/shadows.h" +#include "mod_table/mod_table.h" #include "model/model.h" #include "model/modelrender.h" #include "object/object.h" @@ -40,12 +44,13 @@ static bool submodelPassesDetailBox(const polymodel* pm, int submodel_num, const void VulkanRaytracingManager::pushInstance(SCP_vector& instances, vk::DeviceAddress blasAddress, const matrix& orient, - const vec3d& pos) + const vec3d& pos, + uint8_t mask) { vk::AccelerationStructureInstanceKHR instance; instance.transform = toVkTransform(orient, pos); instance.instanceCustomIndex = 0; - instance.mask = 0xFF; + instance.mask = mask; instance.instanceShaderBindingTableRecordOffset = 0; instance.flags = static_cast(vk::GeometryInstanceFlagBitsKHR::eTriangleFacingCullDisable); instance.accelerationStructureReference = blasAddress; @@ -57,7 +62,8 @@ void VulkanRaytracingManager::addSingleSubmodelInstance(SCP_vector= pm->n_models) { return; @@ -80,14 +86,16 @@ void VulkanRaytracingManager::addSingleSubmodelInstance(SCP_vectoraddress, orient, pos); + pushInstance(instances, entry->address, orient, pos, mask); } void VulkanRaytracingManager::walkSubmodelTree(SCP_vector& instances, transform_stack& stack, const polymodel* pm, const polymodel_instance* pmi, - int submodel_num) + int submodel_num, + uint8_t mask, + bool skipDetailBoxCheck) { if (submodel_num < 0 || submodel_num >= pm->n_models) { return; @@ -135,19 +143,19 @@ void VulkanRaytracingManager::walkSubmodelTree(SCP_vectorid, submodel_num); if (entry != nullptr) { - pushInstance(instances, entry->address, world_orient, world_pos); + pushInstance(instances, entry->address, world_orient, world_pos, mask); } for (int child = sm.first_child; child >= 0; child = pm->submodel[child].next_sibling) { if (!pm->submodel[child].flags[Model::Submodel_flags::Is_thruster]) { - walkSubmodelTree(instances, stack, pm, pmi, child); + walkSubmodelTree(instances, stack, pm, pmi, child, mask, skipDetailBoxCheck); } } @@ -157,7 +165,7 @@ void VulkanRaytracingManager::walkSubmodelTree(SCP_vector& instances) { // Mirrors the object selection in shadows_render_all() (shadows.cpp) -- - // ships/asteroids/debris -- but without its per-cascade frustum + // ships/asteroids/debris/raw POFs/props -- but without its per-cascade frustum // pre-filter, which lives in shadows.cpp's private state (Shadow_frustums) // and is specific to the rasterized cascade layout. Starting unfiltered is // simpler and safe (never wrongly excludes a caster); spatial culling of @@ -187,9 +195,15 @@ void VulkanRaytracingManager::gatherShadowCasterInstances(SCP_vectorpos, &objp->orient); - walkSubmodelTree(instances, stack, pm, pmi, pm->detail[0]); + walkSubmodelTree(instances, stack, pm, pmi, pm->detail[0], instanceMask); break; } case OBJ_ASTEROID: { @@ -224,12 +238,68 @@ void VulkanRaytracingManager::gatherShadowCasterInstances(SCP_vectororient, objp->pos); break; } + case OBJ_RAW_POF: + case OBJ_PROP: { + // Mirrors shadows.cpp's OBJ_RAW_POF/OBJ_PROP case: same shape as + // OBJ_SHIP (a polymodel + optional polymodel_instance), just + // resolved generically instead of via ship_info/Ships[]. + int model_num = object_get_model_num(objp); + polymodel* pm = model_get(model_num); + if (pm == nullptr || pm->detail[0] < 0) { + continue; + } + + int instance_num = object_get_model_instance_num(objp); + polymodel_instance* pmi = instance_num < 0 ? nullptr : model_get_instance(instance_num); + + transform_stack stack; + stack.push(&objp->pos, &objp->orient); + walkSubmodelTree(instances, stack, pm, pmi, pm->detail[0]); + break; + } default: break; } } } +void VulkanRaytracingManager::gatherCockpitShadowCasterInstance(SCP_vector& instances) +{ + object* objp = Viewer_obj; + if (objp == nullptr || objp->type != OBJ_SHIP || objp->instance < 0) { + return; + } + + ship* shipp = &Ships[objp->instance]; + ship_info* sip = &Ship_info[shipp->ship_info_index]; + + if (!ship_player_cockpit_model_would_render(sip) || Shadow_disable_overrides.disable_cockpit) { + return; // matches render_viewer_shadow()'s gate on the rasterized cockpit shadow pass (shadows.cpp) + } + + polymodel* cockpit_pm = model_get(sip->cockpit_model_num); + if (cockpit_pm == nullptr || cockpit_pm->detail[0] < 0) { + return; + } + polymodel_instance* cockpit_pmi = + shipp->cockpit_model_instance < 0 ? nullptr : model_get_instance(shipp->cockpit_model_instance); + + // World-space anchor for the cockpit model. ship_cockpit_render_offset() (ship.h) + // gives the same rotated+swayed offset ship_render_player_ship() draws the cockpit + // with, but that offset is camera-relative there (never combined with objp->pos) -- + // a rendering convenience specific to the rasterized forward-draw call. The TLAS is + // a persistent world-space structure (gatherShadowCasterInstances' OBJ_SHIP case + // anchors at true objp->pos/objp->orient, same as here), so adding it onto objp->pos + // here is what anchors the cockpit instance the same way. + vec3d cockpit_render_offset = ship_cockpit_render_offset(sip, objp); + vec3d cockpit_world_pos = objp->pos; + vm_vec_add2(&cockpit_world_pos, &cockpit_render_offset); + + transform_stack stack; + stack.push(&cockpit_world_pos, &objp->orient); + walkSubmodelTree(instances, stack, cockpit_pm, cockpit_pmi, cockpit_pm->detail[0], /* mask */ 0xFF, /* skipDetailBoxCheck */ true); +} + bool VulkanRaytracingManager::ensureInstanceCapacity(FrameTlasResources& frame, vk::DeviceSize requiredBytes) { if (requiredBytes <= frame.instanceCapacity) { @@ -364,6 +434,7 @@ void VulkanRaytracingManager::buildTlas() SCP_vector instances; gatherShadowCasterInstances(instances); + gatherCockpitShadowCasterInstance(instances); if (instances.empty()) { return; // keep whatever TLAS (if any) was built last time this slot was used diff --git a/code/graphics/vulkan/VulkanRenderer.h b/code/graphics/vulkan/VulkanRenderer.h index 3d890b18a84..06a29f21499 100644 --- a/code/graphics/vulkan/VulkanRenderer.h +++ b/code/graphics/vulkan/VulkanRenderer.h @@ -245,6 +245,24 @@ class VulkanRenderer { */ void copySceneDepthForParticles(); + /** + * @brief Park the scene depth so the cockpit can render on a cleared depth buffer + * + * Called by vulkan_post_process_save_zbuffer(). Ends the current scene render + * pass, copies scene depth → backup image, then resumes the pass with + * loadOp=eLoad. The caller clears the depth buffer afterwards. No-op when a + * save is already outstanding, or outside scene rendering. + */ + void saveSceneDepth(); + + /** + * @brief Put the parked scene depth back, discarding what the cockpit wrote + * + * Called by vulkan_post_process_restore_zbuffer(). No-op unless saveSceneDepth() + * parked something. + */ + void restoreSceneDepth(); + /** * @brief Check if scene depth copy is available for sampling this frame */ @@ -336,6 +354,12 @@ class VulkanRenderer { */ void resumeScenePassAfterCopy(); + /** + * @brief Restore the color attachment layouts a depth-only copy left behind, + * then resume the scene (or G-buffer) render pass + */ + void resumeScenePassAfterDepthCopy(); + bool initDisplayDevice() const; bool initializeInstance(); @@ -473,6 +497,7 @@ class VulkanRenderer { std::unique_ptr m_postProcessor; bool m_sceneRendering = false; bool m_sceneDepthCopiedThisFrame = false; + bool m_sceneDepthSaved = false; // True between saveSceneDepth() and restoreSceneDepth() bool m_useGbufRenderPass = false; // True when scene uses G-buffer (deferred lighting) bool m_supportsShaderViewportLayerOutput = false; // VK_EXT_shader_viewport_index_layer diff --git a/code/graphics/vulkan/VulkanRendererLoop.cpp b/code/graphics/vulkan/VulkanRendererLoop.cpp index 601dad04485..c08b0c10d52 100644 --- a/code/graphics/vulkan/VulkanRendererLoop.cpp +++ b/code/graphics/vulkan/VulkanRendererLoop.cpp @@ -139,6 +139,7 @@ void VulkanRenderer::setupFrame() // Reset per-frame flags m_sceneDepthCopiedThisFrame = false; + m_sceneDepthSaved = false; // Reset per-frame draw statistics Assertion(m_drawManager, "Vulkan DrawManager not initialized in setupFrame!"); @@ -431,9 +432,18 @@ void VulkanRenderer::copySceneDepthForParticles() // Copy scene depth → samplable depth copy (handles all depth image transitions) m_postProcessor->copySceneDepth(m_currentCommandBuffer); + resumeScenePassAfterDepthCopy(); + + m_sceneDepthCopiedThisFrame = true; +} + +// Shared tail of every mid-scene copy that touches only depth: put the color +// attachments back the way the resumed loadOp=eLoad pass expects them, then resume. +void VulkanRenderer::resumeScenePassAfterDepthCopy() +{ // Transition scene color: eShaderReadOnlyOptimal → eColorAttachmentOptimal // (needed for the resumed render pass with loadOp=eLoad, which expects - // initialLayout=eColorAttachmentOptimal; copySceneDepth only touches depth) + // initialLayout=eColorAttachmentOptimal; a depth copy only touches depth) { ImageBarrier2 barrier; barrier.image = m_postProcessor->getSceneColorImage(); @@ -460,8 +470,64 @@ void VulkanRenderer::copySceneDepthForParticles() // Resume the scene render pass with loadOp=eLoad resumeScenePassAfterCopy(); +} - m_sceneDepthCopiedThisFrame = true; +// The cockpit model is rendered around the camera, inside the ship hull that the scene +// pass already drew, so it needs a depth buffer that does not know about that hull -- +// otherwise the hull wins the depth test and shows through the cockpit. OpenGL swaps +// the depth attachment to Cockpit_depth_texture for the duration; Vulkan bakes the +// attachment into the framebuffer, so the scene depth is copied aside and cleared +// instead. Only the scene depth image is parked: with MSAA the cockpit renders against +// the multisampled depth, which the G-buffer pass clears on its own, and +// gr_deferred_lighting_msaa() resolves back into the scene depth before restore runs. +void VulkanRenderer::saveSceneDepth() +{ + if (m_sceneDepthSaved || !m_sceneRendering || !m_postProcessor || !m_postProcessor->isInitialized()) { + return; + } + + // resumeScenePassAfterCopy() knows the scene pass and the non-MSAA G-buffer pass only. + // Both callers (ship_render_player_ship) run between one deferred pass and the next, so + // the multisampled G-buffer pass is never the live one -- resuming the wrong pass would + // desync the state tracker and build pipelines against it, so catch a future move here. + Assertion(m_stateTracker->getCurrentSampleCount() == vk::SampleCountFlagBits::e1, + "Tried to park the scene depth while a multisampled render pass was active!"); + + // End the current scene render pass + // This transitions: color → eShaderReadOnlyOptimal, depth → eDepthStencilAttachmentOptimal + // For G-buffer: all 6 color attachments → eShaderReadOnlyOptimal + m_currentCommandBuffer.endRenderPass(); + + m_postProcessor->saveSceneDepth(m_currentCommandBuffer); + + resumeScenePassAfterDepthCopy(); + + m_sceneDepthSaved = true; +} + +void VulkanRenderer::restoreSceneDepth() +{ + if (!m_sceneDepthSaved) { + return; + } + + // Cleared first: a scene that ends between the save and the restore (a failed + // resize dropping the post-processor, say) must not leave the flag set for the + // next frame's restore to act on stale content. + m_sceneDepthSaved = false; + + if (!m_sceneRendering || !m_postProcessor || !m_postProcessor->isInitialized()) { + return; + } + + Assertion(m_stateTracker->getCurrentSampleCount() == vk::SampleCountFlagBits::e1, + "Tried to put the scene depth back while a multisampled render pass was active!"); + + m_currentCommandBuffer.endRenderPass(); + + m_postProcessor->restoreSceneDepth(m_currentCommandBuffer); + + resumeScenePassAfterDepthCopy(); } void VulkanRenderer::beginRenderTarget(tcache_slot_vulkan* ts, int face) diff --git a/code/graphics/vulkan/VulkanRendererSetup.cpp b/code/graphics/vulkan/VulkanRendererSetup.cpp index 90ebc51b567..30fb46c5bd0 100644 --- a/code/graphics/vulkan/VulkanRendererSetup.cpp +++ b/code/graphics/vulkan/VulkanRendererSetup.cpp @@ -1190,6 +1190,7 @@ bool VulkanRenderer::recreateSwapChain() m_drawManager->onResize(); } m_sceneDepthCopiedThisFrame = false; + m_sceneDepthSaved = false; // Update VulkanRenderFrame handles to point to the new swap chain, and // recreate their semaphores: an acquire that succeeded against the old swap diff --git a/code/object/objectsort.cpp b/code/object/objectsort.cpp index 7629f6fe3ef..40097ba4e6f 100644 --- a/code/object/objectsort.cpp +++ b/code/object/objectsort.cpp @@ -371,7 +371,7 @@ void obj_render_queue_all() scene.init_render(); if (Shadow_quality != ShadowQuality::Disabled) { - shadow_cascade_params_bind(Num_cockpit_shadow_cascades, Num_shadow_cascades); + shadow_cascade_params_bind(Num_cockpit_shadow_cascades, Num_shadow_cascades, vmd_zero_vector); } scene.render_all(ZBUFFER_TYPE_FULL); diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 5e20b90d07e..ff826511582 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -8119,6 +8119,19 @@ static bool ship_render_player_renderShipModel(const ship_info* sip) { && (!Viewer_mode || (Viewer_mode & VM_PADLOCK_ANY) || (Viewer_mode & VM_OTHER_SHIP) || (Viewer_mode & VM_TRACK) || !(Viewer_mode & VM_EXTERNAL)); } +bool ship_player_cockpit_model_would_render(const ship_info* sip) { + return (Viewer_mode != VM_TOPDOWN) && sip->cockpit_model_num >= 0 && !Disable_cockpits; +} + +vec3d ship_cockpit_render_offset(const ship_info* sip, const object* objp) { + vec3d offset; + vm_vec_unrotate(&offset, &sip->cockpit_offset, &objp->orient); + if (!Disable_cockpit_sway) { + offset += sip->cockpit_sway_val * objp->phys_info.acceleration; + } + return offset; +} + bool ship_render_player_ship_casts_shadow_on_cockpit() { if (Viewer_obj == nullptr) return false; @@ -8146,9 +8159,7 @@ bool ship_render_player_has_closeup_visuals() { ship* shipp = &Ships[Viewer_obj->instance]; ship_info* sip = &Ship_info[shipp->ship_info_index]; - const bool hasCockpitModel = sip->cockpit_model_num >= 0; - - const bool renderCockpitModel = (Viewer_mode != VM_TOPDOWN) && hasCockpitModel && !Disable_cockpits; + const bool renderCockpitModel = ship_player_cockpit_model_would_render(sip); const bool renderShipModel = ship_render_player_renderShipModel(sip); return renderCockpitModel || renderShipModel; @@ -8161,7 +8172,7 @@ void ship_render_player_ship(object* objp, const vec3d* cam_offset, const matrix const bool hasCockpitModel = sip->cockpit_model_num >= 0; - const bool renderCockpitModel = (Viewer_mode != VM_TOPDOWN) && hasCockpitModel && !Disable_cockpits; + const bool renderCockpitModel = ship_player_cockpit_model_would_render(sip); const bool renderShipModel = ship_render_player_renderShipModel(sip); Cockpit_active = renderCockpitModel; @@ -8252,14 +8263,42 @@ void ship_render_player_ship(object* objp, const vec3d* cam_offset, const matrix Shadow_view_matrix_render = gr_view_matrix; matrix4 shadow_view_light_backup = Shadow_view_matrix_light; + bool cockpit_shadow_rendering_active = false; if (shadow_maybe_start_frame(Shadow_disable_overrides.disable_cockpit)) { Shadow_override = false; Shadow_view_matrix_light.a1d[12] = 0; Shadow_view_matrix_light.a1d[13] = 0; Shadow_view_matrix_light.a1d[14] = 0; - shadow_cascade_params_bind(0, Num_cockpit_shadow_cascades); + cockpit_shadow_rendering_active = true; } + // Hull and cockpit render in different internal frames (see the &eye_offset vs. + // &cockpit_offset draws below), so each needs its own RT shadow-ray world-space + // correction -- see shadow_cascade_static_data::shadow_ray_world_offset + // (uniform_structs.h) for the derivation. + vec3d hull_shadow_ray_world_offset; + vm_vec_sub(&hull_shadow_ray_world_offset, &objp->pos, &eye_offset); + const vec3d& cockpit_shadow_ray_world_offset = objp->pos; + const bool cockpit_shadow_allows_hull_self_shadow = ship_render_player_ship_casts_shadow_on_cockpit(); + + model_render_params ship_render_info; + model_render_params cockpit_render_info; + vec3d cockpit_offset = vmd_zero_vector; + + auto renderHull = [&](int render_pass) { + if (cockpit_shadow_rendering_active) { + shadow_cascade_params_bind(0, Num_cockpit_shadow_cascades, hull_shadow_ray_world_offset); + } + model_render_immediate(&ship_render_info, sip->model_num, shipp->model_instance_num, &objp->orient, &eye_offset, render_pass); + }; + auto renderCockpit = [&](int render_pass) { + if (cockpit_shadow_rendering_active) { + shadow_cascade_params_bind(0, Num_cockpit_shadow_cascades, cockpit_shadow_ray_world_offset, + cockpit_shadow_allows_hull_self_shadow); + } + model_render_immediate(&cockpit_render_info, sip->cockpit_model_num, shipp->cockpit_model_instance, &objp->orient, &cockpit_offset, render_pass); + }; + if (light_deferredcockpit_enabled()) { gr_deferred_lighting_begin(true); @@ -8276,10 +8315,6 @@ void ship_render_player_ship(object* objp, const vec3d* cam_offset, const matrix render_flags |= MR_NO_GLOWMAPS; } - model_render_params ship_render_info; - model_render_params cockpit_render_info; - vec3d cockpit_offset = sip->cockpit_offset; - //Properly render ship and cockpit model if (deferredRenderShipModel) { ship_render_info.set_detail_level_lock(0); @@ -8289,17 +8324,16 @@ void ship_render_player_ship(object* objp, const vec3d* cam_offset, const matrix if (sip->uses_team_colors) ship_render_info.set_team_color(shipp->team_name, shipp->secondary_team_name, 0, 0); - model_render_immediate(&ship_render_info, sip->model_num, shipp->model_instance_num, &objp->orient, &eye_offset, MODEL_RENDER_OPAQUE); + renderHull(MODEL_RENDER_OPAQUE); gr_zbuffer_clear(true); } if (renderCockpitModel) { cockpit_render_info.set_detail_level_lock(0); cockpit_render_info.set_flags(render_flags); cockpit_render_info.set_replacement_textures(Player_cockpit_textures); - vm_vec_unrotate(&cockpit_offset, &cockpit_offset, &objp->orient); - if (!Disable_cockpit_sway) - cockpit_offset += sip->cockpit_sway_val * objp->phys_info.acceleration; - model_render_immediate(&cockpit_render_info, sip->cockpit_model_num, shipp->cockpit_model_instance, &objp->orient, &cockpit_offset, MODEL_RENDER_OPAQUE); + cockpit_offset = ship_cockpit_render_offset(sip, objp); + + renderCockpit(MODEL_RENDER_OPAQUE); } if (light_deferredcockpit_enabled()) { @@ -8327,11 +8361,11 @@ void ship_render_player_ship(object* objp, const vec3d* cam_offset, const matrix gr_zbuffer_set(ZBUFFER_TYPE_READ); if (deferredRenderShipModel) { - model_render_immediate(&ship_render_info, sip->model_num, shipp->model_instance_num, &objp->orient, &eye_offset, MODEL_RENDER_TRANS); + renderHull(MODEL_RENDER_TRANS); } if (renderCockpitModel) { - model_render_immediate(&cockpit_render_info, sip->cockpit_model_num, shipp->cockpit_model_instance, &objp->orient, &cockpit_offset, MODEL_RENDER_TRANS); + renderCockpit(MODEL_RENDER_TRANS); } if (light_deferredcockpit_enabled()) { diff --git a/code/ship/ship.h b/code/ship/ship.h index 1d0c40112e1..7d857931329 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -1780,6 +1780,19 @@ extern void ship_process_post( object * objp, float frametime ); extern void ship_render( object * obj, model_draw_list * scene ); extern bool ship_render_player_ship_casts_shadow_on_cockpit(); extern bool ship_render_player_has_closeup_visuals(); +// True if the player ship's cockpit model would be rendered right now (viewer mode, +// cockpit-model presence, and the Disable_cockpits override). Shared by every path that +// needs to know whether a cockpit is currently on-screen: ship_render_player_ship(), +// render_viewer_shadow() (shadows.cpp), and gatherCockpitShadowCasterInstance() +// (VulkanRaytracingTlas.cpp). +extern bool ship_player_cockpit_model_would_render(const ship_info* sip); +// The cockpit model's render offset: sip->cockpit_offset rotated into world orientation, +// plus acceleration-driven sway (unless Disable_cockpit_sway). This is the camera-relative +// offset used to draw the cockpit model, NOT combined with objp->pos -- callers that need +// a world-space position must add objp->pos themselves. Shared by every path that draws or +// otherwise anchors the cockpit model: ship_render_player_ship(), render_viewer_shadow() +// (shadows.cpp), and gatherCockpitShadowCasterInstance() (VulkanRaytracingTlas.cpp). +extern vec3d ship_cockpit_render_offset(const ship_info* sip, const object* objp); extern void ship_render_player_ship( object * objp, const vec3d* offset = nullptr, const matrix* rot_offset = nullptr, const fov_t* fov_override = nullptr); extern void ship_delete( object * objp ); extern int ship_check_collision_fast( object * obj, object * other_obj, vec3d * hitpos );