diff --git a/code/def_files/data/effects/deferred-f.sdr b/code/def_files/data/effects/deferred-f.sdr index fb5d9021ebd..c214f8b18a9 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. + int shadow_ray_cull_mask; + vec3 shadow_ray_world_offset; 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..ef16bafb900 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. + int shadow_ray_cull_mask; + vec3 shadow_ray_world_offset; 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..e6a94c194da 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. + int shadow_ray_cull_mask; + vec3 shadow_ray_world_offset; 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..475061f448d 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. + int shadow_ray_cull_mask; + vec3 shadow_ray_world_offset; 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..dc746a5f2cb 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. + int shadow_ray_cull_mask; + vec3 shadow_ray_world_offset; 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..e5168979e38 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,12 +782,12 @@ 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); @@ -1035,7 +1035,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 +1059,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 +1087,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..f27d653abe3 100644 --- a/code/graphics/util/uniform_structs.h +++ b/code/graphics/util/uniform_structs.h @@ -147,7 +147,31 @@ struct shadow_cascade_static_data { float rtShadowBiasMin; float rtShadowBiasMax; matrix4 shadow_mv_matrix; + + // 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; + float pad[3]; // pad shadow_ray_world_offset's offset to 16-byte alignment (std140 vec3 rule) + + // 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. + vec3d shadow_ray_world_offset; + float pad2; // keep shadow_proj_matrix[]'s offset 16-byte aligned (std140) }; +// Must match the GLSL shadowCascadeParams block's implicit std140 padding +// exactly (16 [4 leading scalars] + 64 [matrix4] + 4 [shadow_ray_cull_mask] +// + 12 [pad[3]] + 12 [shadow_ray_world_offset] + 4 [pad2] = 112) -- +// 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) == 112, "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/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..ebc48321cdb 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,80 @@ 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. This is deliberately NOT the + // eye-relative offset ship_render_player_ship() passes to + // model_render_immediate() (ship.cpp, `cockpit_offset` unrotated but never + // combined with objp->pos) -- that's a camera-relative 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 the cockpit instance must + // be anchored the same way. Direction of vm_vec_unrotate (local -> world) is + // the same convention used to turn ship-local points into world positions + // elsewhere (e.g. gun firing points, ship.cpp). + vec3d rotated_offset; + vm_vec_unrotate(&rotated_offset, &sip->cockpit_offset, &objp->orient); + // Matches ship.cpp's sway addition (ship_render_player_ship()) so the TLAS + // proxy tracks the same per-frame jitter as the rendered cockpit geometry; + // otherwise the two drift apart by up to cockpit_sway_val * acceleration. + if (!Disable_cockpit_sway) { + vec3d sway_offset; + vm_vec_copy_scale(&sway_offset, &objp->phys_info.acceleration, sip->cockpit_sway_val); + vm_vec_add2(&rotated_offset, &sway_offset); + } + vec3d cockpit_world_pos = objp->pos; + vm_vec_add2(&cockpit_world_pos, &rotated_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 +446,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/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 799c8c09d45..df37e3c4b85 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -8112,6 +8112,10 @@ 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; +} + bool ship_render_player_ship_casts_shadow_on_cockpit() { if (Viewer_obj == nullptr) return false; @@ -8139,9 +8143,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; @@ -8154,7 +8156,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; @@ -8245,14 +8247,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 = sip->cockpit_offset; + + 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); @@ -8269,10 +8299,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); @@ -8282,7 +8308,7 @@ 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) { @@ -8292,7 +8318,8 @@ void ship_render_player_ship(object* objp, const vec3d* cam_offset, const matrix 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); + + renderCockpit(MODEL_RENDER_OPAQUE); } if (light_deferredcockpit_enabled()) { @@ -8320,11 +8347,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 c04c6930899..c4c9b375964 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -1778,6 +1778,12 @@ 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); 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 );