From c1c6296ecb28cf3c17a7b2ba5060b2c86b5b67bc Mon Sep 17 00:00:00 2001 From: the-e Date: Tue, 4 Aug 2026 17:41:29 +0200 Subject: [PATCH 1/5] Add cockpit and OBJ_RAW_POF/OBJ_PROP to Vulkan RT shadow acceleration structures Closes three Vulkan/OpenGL cockpit-shadow parity gaps (design doc: vulkan-opengl-cockpit-shadow-parity.md): - Cockpit polymodel now gets a TLAS instance each frame (gatherCockpitShadowCasterInstance), so it can cast/receive raytraced shadows like it already does under the rasterized cascade path. BLAS building needed no changes -- it's already model-type-agnostic and the cockpit POF already flows through the generic model_load() -> onModelLoaded() warm-up path. - The viewer's own ship hull had no exclusion in the RT TLAS gather, unlike the rasterized path's explicit Viewer_obj skip, so it could incorrectly self-shadow the cockpit regardless of ship_render_player_ship_casts_shadow_on_cockpit(). Fixed via a dedicated TLAS instance mask bit on the viewer hull plus a new shadow_ray_cull_mask field (shadowCascadeParams uniform, shared by the forward and deferred cockpit passes via shadow_cascade_params_bind()) so shadow rays exclude it everywhere except the cockpit pass when casting is allowed. - OBJ_RAW_POF/OBJ_PROP were silently dropped by the TLAS gather's `default: break`, unlike shadows_render_all()'s rasterized path which handles them the same as ships. Cockpit detail-box culling in the TLAS walk uses Eye_position, which isn't correct for the cockpit model (the rasterized path evaluates a cockpit-relative eye position instead); ships with that gate for now via a new skipDetailBoxCheck parameter rather than threading cam_offset/rot_offset down to replicate the rasterizer exactly. Compiles and links clean (ninja code, ninja Freespace2). GLSL runtime compilation of the edited shaders is not yet verified against a live session -- launch attempts in this environment got stuck serializing table/model-parse warning dialogs before reaching a shadow-rendering scene. Co-Authored-By: Claude Sonnet 5 --- code/def_files/data/effects/deferred-f.sdr | 6 +- code/def_files/data/effects/main-f.sdr | 6 +- code/def_files/data/effects/main-v.sdr | 4 + code/def_files/data/effects/shadow_map-g.sdr | 4 + code/def_files/data/effects/shadow_map-v.sdr | 4 + code/def_files/data/effects/shadows.sdr | 12 +- code/graphics/shadows.cpp | 8 ++ code/graphics/util/uniform_structs.h | 15 +++ code/graphics/vulkan/VulkanRaytracing.h | 34 +++++- code/graphics/vulkan/VulkanRaytracingTlas.cpp | 107 ++++++++++++++++-- 10 files changed, 182 insertions(+), 18 deletions(-) diff --git a/code/def_files/data/effects/deferred-f.sdr b/code/def_files/data/effects/deferred-f.sdr index fb5d9021ebd..053210987ff 100644 --- a/code/def_files/data/effects/deferred-f.sdr +++ b/code/def_files/data/effects/deferred-f.sdr @@ -65,6 +65,10 @@ uniform shadowCascadeParams { float rtShadowBiasMax; mat4 shadow_mv_matrix; + + // Ray cull mask for raytraced shadow queries; see shadow_cascade_static_data (uniform_structs.h) + // and shadow_cascade_params_bind() (shadows.cpp) for how this gets set. + 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]; @@ -315,7 +319,7 @@ void main() 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..09aec46a2a4 100644 --- a/code/def_files/data/effects/main-f.sdr +++ b/code/def_files/data/effects/main-f.sdr @@ -103,6 +103,10 @@ uniform shadowCascadeParams { float rtShadowBiasMax; mat4 shadow_mv_matrix; + + // Ray cull mask for raytraced shadow queries; see shadow_cascade_static_data (uniform_structs.h) + // and shadow_cascade_params_bind() (shadows.cpp) for how this gets set. + 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]; @@ -248,7 +252,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; diff --git a/code/def_files/data/effects/main-v.sdr b/code/def_files/data/effects/main-v.sdr index 7933ecf3578..f3cde5e0187 100644 --- a/code/def_files/data/effects/main-v.sdr +++ b/code/def_files/data/effects/main-v.sdr @@ -117,6 +117,10 @@ uniform shadowCascadeParams { float rtShadowBiasMax; mat4 shadow_mv_matrix; + + // Ray cull mask for raytraced shadow queries; see shadow_cascade_static_data (uniform_structs.h) + // and shadow_cascade_params_bind() (shadows.cpp) for how this gets set. + 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..4468b3b0591 100644 --- a/code/def_files/data/effects/shadow_map-g.sdr +++ b/code/def_files/data/effects/shadow_map-g.sdr @@ -14,6 +14,10 @@ layout (std140) uniform shadowCascadeParams { float rtShadowBiasMax; mat4 shadow_mv_matrix; + + // Ray cull mask for raytraced shadow queries; see shadow_cascade_static_data (uniform_structs.h) + // and shadow_cascade_params_bind() (shadows.cpp) for how this gets set. + 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..7976a9e43c9 100644 --- a/code/def_files/data/effects/shadow_map-v.sdr +++ b/code/def_files/data/effects/shadow_map-v.sdr @@ -44,6 +44,10 @@ uniform shadowCascadeParams { float rtShadowBiasMax; mat4 shadow_mv_matrix; + + // Ray cull mask for raytraced shadow queries; see shadow_cascade_static_data (uniform_structs.h) + // and shadow_cascade_params_bind() (shadows.cpp) for how this gets set. + 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..782daf38220 100644 --- a/code/def_files/data/effects/shadows.sdr +++ b/code/def_files/data/effects/shadows.sdr @@ -114,14 +114,22 @@ 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 -- +// it's 0x7F outside the cockpit pass, excluding the viewer ship's own hull instance +// (tagged 0x80 in VulkanRaytracingManager::gatherShadowCasterInstances()) to match +// the rasterized path's exclusion of Viewer_obj from the main shadow cascades, and +// 0xFF inside the cockpit pass when the ship is allowed to cast onto its own cockpit. +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/shadows.cpp b/code/graphics/shadows.cpp index 82713c6c5db..80b767a53ba 100644 --- a/code/graphics/shadows.cpp +++ b/code/graphics/shadows.cpp @@ -1058,6 +1058,14 @@ 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; + // Default excludes the viewer ship's own hull from raytraced shadow rays + // (mirrors the rasterized path's unconditional exclusion of Viewer_obj from + // the main shadow cascades, shadows_render_all() below); only the cockpit's + // own shading pass allows it through, and only when the ship is actually + // supposed to cast onto its cockpit this frame. + static_data.shadow_ray_cull_mask = + (Lighting_mode == lighting_mode::COCKPIT && ship_render_player_ship_casts_shadow_on_cockpit()) ? 0xFF : 0x7F; + Shadow_cascade_count = cascade_count; offset += sizeof(graphics::shadow_cascade_static_data); diff --git a/code/graphics/util/uniform_structs.h b/code/graphics/util/uniform_structs.h index 974ad275732..ff264d4acb5 100644 --- a/code/graphics/util/uniform_structs.h +++ b/code/graphics/util/uniform_structs.h @@ -147,7 +147,22 @@ struct shadow_cascade_static_data { float rtShadowBiasMin; float rtShadowBiasMax; matrix4 shadow_mv_matrix; + + // Ray cull mask for raytraced shadow queries (traceShadowRay()/shadows.sdr). + // Lets the viewer ship's own hull -- tagged with a dedicated TLAS instance + // mask bit, see VulkanRaytracingManager::gatherShadowCasterInstances() -- + // be selectively excluded from shadow rays 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]; // 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]] = 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/VulkanRaytracing.h b/code/graphics/vulkan/VulkanRaytracing.h index 45fe2b22e1c..edb26db9e99 100644 --- a/code/graphics/vulkan/VulkanRaytracing.h +++ b/code/graphics/vulkan/VulkanRaytracing.h @@ -192,23 +192,51 @@ 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); + // defaults to 0xFF (visible to every ray). The one caller that needs + // something else is the viewer ship's own hull, which is tagged with a + // dedicated bit so shadow rays can selectively exclude it -- see + // gatherShadowCasterInstances()'s OBJ_SHIP case and shadows.sdr's + // traceShadowRay() for how the two ends of this scheme meet. 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 compares against the global + // `Eye_position` (see submodelPassesDetailBox()), which is correct for + // world-anchored objects (ships/asteroids/debris/props) but not for the + // cockpit model -- render_viewer_shadow()'s rasterized cockpit shadow + // pass evaluates its own detail-box checks against a cockpit-relative eye + // position instead (shadows.cpp), which Eye_position does not replicate. + // Rather than derive that (would need cam_offset/rot_offset threaded down + // from shadows_render_all(), see design doc), the cockpit call + // (gatherCockpitShadowCasterInstance) passes true here to skip the check + // entirely -- cockpit models are small and sit right against the camera, + // so render-box/render-sphere culling is unlikely to matter at that + // range. Every other caller passes false (default), unaffected. 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..f854fda6080 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,8 @@ #include "asteroid/asteroid.h" #include "debris/debris.h" +#include "globalincs/systemvars.h" +#include "mod_table/mod_table.h" #include "model/model.h" #include "model/modelrender.h" #include "object/object.h" @@ -40,12 +43,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 +61,8 @@ void VulkanRaytracingManager::addSingleSubmodelInstance(SCP_vector= pm->n_models) { return; @@ -80,14 +85,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 +142,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 +164,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 +194,20 @@ 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 +242,78 @@ 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]; + + // Mirrors the renderCockpitModel computation in ship_render_player_ship()/ + // render_viewer_shadow() (ship.cpp/shadows.cpp) -- kept as a fourth inline + // copy for consistency with those two, rather than factoring out a shared + // helper neither of them uses today. + const bool renderCockpitModel = + (Viewer_mode != VM_TOPDOWN) && sip->cockpit_model_num >= 0 && !Disable_cockpits; + if (!renderCockpitModel || Shadow_disable_overrides.disable_cockpit) { + return; // matches shadows.cpp:787's gate on the rasterized cockpit shadow pass + } + + 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); + 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 +448,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 From b2532b7d92936ca44808ac9b4f7d2aa83a4c6401 Mon Sep 17 00:00:00 2001 From: the-e Date: Tue, 4 Aug 2026 18:11:03 +0200 Subject: [PATCH 2/5] Add rt_shadow_debug console command to visualize raytraced shadow occlusion Paints raytraced shadow queries directly onto the model, for diagnosing "RT shadows on cockpits aren't working"-style reports: - untouched color: no RT shadow query ran for this fragment at all (shader permutation not compiled in, or shadow-receiving is off) - solid green: query ran, found no occluder - solid red: query ran, fully occluded (blends toward red as occlusion increases) Toggled live via the `rt_shadow_debug` debug-console command (no restart needed). Wired through the same shared shadowCascadeParams uniform block added for shadow_ray_cull_mask, so one CPU-side set (shadow_cascade_params_bind) reaches both the forward and deferred shadow-receiving passes. Repurposed one of shadow_cascade_static_data's existing padding floats for the new int field, keeping the struct's std140-verified size at 96 bytes. Co-Authored-By: Claude Sonnet 5 --- code/def_files/data/effects/deferred-f.sdr | 27 ++++++++++++++++++- code/def_files/data/effects/main-f.sdr | 28 +++++++++++++++++++- code/def_files/data/effects/main-v.sdr | 3 +++ code/def_files/data/effects/shadow_map-g.sdr | 3 +++ code/def_files/data/effects/shadow_map-v.sdr | 3 +++ code/graphics/shadows.cpp | 18 +++++++++++++ code/graphics/shadows.h | 7 +++++ code/graphics/util/uniform_structs.h | 14 +++++++--- 8 files changed, 97 insertions(+), 6 deletions(-) diff --git a/code/def_files/data/effects/deferred-f.sdr b/code/def_files/data/effects/deferred-f.sdr index 053210987ff..9cbcd5e8145 100644 --- a/code/def_files/data/effects/deferred-f.sdr +++ b/code/def_files/data/effects/deferred-f.sdr @@ -69,6 +69,9 @@ uniform shadowCascadeParams { // Ray cull mask for raytraced shadow queries; see shadow_cascade_static_data (uniform_structs.h) // and shadow_cascade_params_bind() (shadows.cpp) for how this gets set. int shadow_ray_cull_mask; + + // Debug visualization toggle; see shadow_cascade_static_data (uniform_structs.h). + int rt_shadow_debug_visualize; 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]; @@ -312,6 +315,15 @@ void main() float lightDist; GetLightInfo(position, alpha, reflectDir, lightDir, attenuation, area_normalisation, lightDist); + // Debug: occlusion from this pixel's raytraced shadow query, and whether a + // query actually ran at all -- rt_shadow_debug_visualize needs both, not + // just occlusion, since "no query ran" and "query ran, fully lit" would + // otherwise look identical (no tint either way). Both stay at their + // zero/false default whenever the CSM fallback runs instead (#else below) + // or enable_shadows is 0. + float rtShadowDebugOcclusion = 0.0; + bool rtShadowDebugQueried = false; + if (enable_shadows != 0) { #ifdef RT_SHADOWS vec3 worldPos = (inv_view_matrix * vec4(position, 1.0)).xyz; @@ -319,7 +331,10 @@ void main() 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, uint(shadow_ray_cull_mask)); + float rtShadowTerm = traceShadowRay(shadow_tlas, worldPos, worldNormal, worldLightDir, lightDist, rtShadowBias, uint(shadow_ray_cull_mask)); + rtShadowDebugOcclusion = 1.0 - rtShadowTerm; + rtShadowDebugQueried = true; + attenuation *= rtShadowTerm; #else vec4 fragShadowPos = shadow_mv_matrix * inv_view_matrix * vec4(position, 1.0); vec4 fragShadowUV[NUM_SHADOW_CASCADES]; @@ -334,6 +349,16 @@ void main() vec3 halfVec = normalize(lightDir + eyeDir); float NdotL = clamp(dot(normal, lightDir), 0.0, 1.0); fragmentColor.rgb = computeLighting(specColor.rgb, diffColor, lightDir, normal.xyz, halfVec, eyeDir, roughness, fresnel, NdotL).rgb * diffuseLightColor * attenuation * area_normalisation; + +#ifdef RT_SHADOWS + // Three readable states: untouched = no RT shadow query ran for this pixel + // (permutation/enable_shadows/m_shadow-init problem); solid green = query + // ran, found no occluder (TLAS/transform problem, or genuinely unoccluded); + // solid red = query ran, fully occluded. + if (rt_shadow_debug_visualize != 0 && rtShadowDebugQueried) { + fragmentColor.rgb = mix(vec3(0.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), rtShadowDebugOcclusion); + } +#endif } fragOut0 = max(fragmentColor, vec4(0.0)); diff --git a/code/def_files/data/effects/main-f.sdr b/code/def_files/data/effects/main-f.sdr index 09aec46a2a4..fd8b8255a10 100644 --- a/code/def_files/data/effects/main-f.sdr +++ b/code/def_files/data/effects/main-f.sdr @@ -107,6 +107,9 @@ uniform shadowCascadeParams { // Ray cull mask for raytraced shadow queries; see shadow_cascade_static_data (uniform_structs.h) // and shadow_cascade_params_bind() (shadows.cpp) for how this gets set. int shadow_ray_cull_mask; + + // Debug visualization toggle; see shadow_cascade_static_data (uniform_structs.h). + int rt_shadow_debug_visualize; 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]; @@ -244,6 +247,14 @@ vec3 CalculateLighting(vec3 normal, vec3 diffuseMaterial, vec3 specularMaterial, vec3 worldPos = (invView * vertIn.position).xyz; vec3 worldNormal = normalize((invView * vec4(normal, 0.0)).xyz); int shadowedDirectionalCount = 0; + // Debug: highest occlusion seen from any raytraced shadow query this fragment, + // and whether a query actually ran at all -- rt_shadow_debug_visualize needs + // both, not just occlusion, since "no query ran" and "query ran, fully lit" + // would otherwise look identical (no tint either way). Both stay at their + // zero/false default whenever RT shadows aren't actually being evaluated for + // this fragment (permutation not compiled in, or shadow-receiving is off). + float rtShadowDebugOcclusion = 0.0; + bool rtShadowDebugQueried = false; #endif #pragma optionNV unroll all @@ -253,6 +264,8 @@ vec3 CalculateLighting(vec3 normal, vec3 diffuseMaterial, vec3 specularMaterial, 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, uint(shadow_ray_cull_mask)); + rtShadowDebugOcclusion = max(rtShadowDebugOcclusion, 1.0 - shadow); + rtShadowDebugQueried = true; ++shadowedDirectionalCount; } else { shadow = 1.0; @@ -274,7 +287,20 @@ 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; + + vec3 litColor = diffuseMaterial * lightAmbient + lightSpecular; +#ifdef MODEL_SDR_FLAG_RT_SHADOWS + // Three readable states: untouched = no RT shadow query ran for this fragment + // (permutation/shadow-receiving problem); solid green = query ran, found no + // occluder (nothing between fragment and light, or a TLAS/transform problem); + // solid red = query ran, fully occluded. Partial occlusion (soft/PCSS-less RT + // shadows are hard-edged per light, but multiple lights can blend) interpolates + // green->red. + if (rt_shadow_debug_visualize != 0 && rtShadowDebugQueried) { + litColor = mix(vec3(0.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), rtShadowDebugOcclusion); + } +#endif + return litColor; } void main() diff --git a/code/def_files/data/effects/main-v.sdr b/code/def_files/data/effects/main-v.sdr index f3cde5e0187..721eaf06577 100644 --- a/code/def_files/data/effects/main-v.sdr +++ b/code/def_files/data/effects/main-v.sdr @@ -121,6 +121,9 @@ uniform shadowCascadeParams { // Ray cull mask for raytraced shadow queries; see shadow_cascade_static_data (uniform_structs.h) // and shadow_cascade_params_bind() (shadows.cpp) for how this gets set. int shadow_ray_cull_mask; + + // Debug visualization toggle; see shadow_cascade_static_data (uniform_structs.h). + int rt_shadow_debug_visualize; 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 4468b3b0591..76558508e8f 100644 --- a/code/def_files/data/effects/shadow_map-g.sdr +++ b/code/def_files/data/effects/shadow_map-g.sdr @@ -18,6 +18,9 @@ layout (std140) uniform shadowCascadeParams { // Ray cull mask for raytraced shadow queries; see shadow_cascade_static_data (uniform_structs.h) // and shadow_cascade_params_bind() (shadows.cpp) for how this gets set. int shadow_ray_cull_mask; + + // Debug visualization toggle; see shadow_cascade_static_data (uniform_structs.h). + int rt_shadow_debug_visualize; 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 7976a9e43c9..6f84131274a 100644 --- a/code/def_files/data/effects/shadow_map-v.sdr +++ b/code/def_files/data/effects/shadow_map-v.sdr @@ -48,6 +48,9 @@ uniform shadowCascadeParams { // Ray cull mask for raytraced shadow queries; see shadow_cascade_static_data (uniform_structs.h) // and shadow_cascade_params_bind() (shadows.cpp) for how this gets set. int shadow_ray_cull_mask; + + // Debug visualization toggle; see shadow_cascade_static_data (uniform_structs.h). + int rt_shadow_debug_visualize; 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/graphics/shadows.cpp b/code/graphics/shadows.cpp index 80b767a53ba..d3f6437d297 100644 --- a/code/graphics/shadows.cpp +++ b/code/graphics/shadows.cpp @@ -27,6 +27,7 @@ #include "ship/ship.h" #include "ship/shipfx.h" #include "render/3d.h" +#include "debugconsole/console.h" #include "tracing/tracing.h" #include "util/uniform_structs.h" @@ -37,6 +38,21 @@ matrix4 Shadow_view_matrix_render; SCP_vector Shadow_proj_matrix; SCP_vector Shadow_cascade_distances; +bool Rt_shadow_debug_visualize = false; + +DCF(rt_shadow_debug, "Toggles red visualization of raytraced shadow occlusion") +{ + if (dc_optional_string_either("status", "--status") || dc_optional_string_either("?", "--?")) { + dc_printf("Raytraced shadow debug visualization is %s", Rt_shadow_debug_visualize ? "ON" : "OFF"); + return; + } + + Rt_shadow_debug_visualize = !Rt_shadow_debug_visualize; + dc_printf("Raytraced shadow debug visualization %s. Occluded fragments will be tinted red wherever " + "a raytraced shadow query is actually evaluated (Vulkan, RT shadows enabled, a shadow-receiving " + "material).\n", Rt_shadow_debug_visualize ? "enabled" : "disabled"); +} + static SCP_vector Shadow_frustums; ShadowQuality Shadow_quality = ShadowQuality::Disabled; @@ -1066,6 +1082,8 @@ void shadow_cascade_params_bind(int cascade_offset, int cascade_count) { static_data.shadow_ray_cull_mask = (Lighting_mode == lighting_mode::COCKPIT && ship_render_player_ship_casts_shadow_on_cockpit()) ? 0xFF : 0x7F; + static_data.rt_shadow_debug_visualize = Rt_shadow_debug_visualize ? 1 : 0; + Shadow_cascade_count = cascade_count; offset += sizeof(graphics::shadow_cascade_static_data); diff --git a/code/graphics/shadows.h b/code/graphics/shadows.h index 63a3abd3258..0c442f26bff 100644 --- a/code/graphics/shadows.h +++ b/code/graphics/shadows.h @@ -95,6 +95,13 @@ extern SCP_vector Shadow_proj_matrix; extern SCP_vector Shadow_cascade_distances; extern int Shadow_cascade_count; +// Debug visualization: when true, fragments occluded by a raytraced shadow +// query (traceShadowRay()/shadows.sdr) are tinted red instead of shaded +// normally. Toggled via the `rt_shadow_debug` debug-console command. Packed +// into the shared shadowCascadeParams uniform (shadow_cascade_static_data) +// alongside shadow_ray_cull_mask -- see shadow_cascade_params_bind(). +extern bool Rt_shadow_debug_visualize; + 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, diff --git a/code/graphics/util/uniform_structs.h b/code/graphics/util/uniform_structs.h index ff264d4acb5..08562a1d1e4 100644 --- a/code/graphics/util/uniform_structs.h +++ b/code/graphics/util/uniform_structs.h @@ -155,13 +155,19 @@ struct shadow_cascade_static_data { // 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]; // keep shadow_proj_matrix[]'s offset 16-byte aligned (std140) + + // Debug visualization toggle (see Rt_shadow_debug_visualize, shadows.h): + // nonzero paints raytraced-shadow-occluded fragments red. Read by + // traceShadowRay()'s callers in main-f.sdr/deferred-f.sdr, not by + // traceShadowRay() itself. + int rt_shadow_debug_visualize; + float pad[2]; // 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]] = 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. +// + 4 [rt_shadow_debug_visualize] + 8 [pad[2]] = 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 { From 31e2a1198d16d1eca1bce8269a90afccdaec841e Mon Sep 17 00:00:00 2001 From: the-e Date: Tue, 4 Aug 2026 18:55:31 +0200 Subject: [PATCH 3/5] Fix RT shadows rendering as solid green on player cockpits (Vulkan) Two independent bugs, both surfaced via the rt_shadow_debug visualization: 1. Per-frame uniform buffer reuse: shadow_cascade_params_bind() is called up to 5x/frame with different content (main scene, viewer-ship-onto- cockpit, cockpit-self, forward cockpit, deferred cockpit), but used gr_update_buffer_data_offset() on Vulkan's streaming buffer path, which only bump-allocates a fresh GPU region on the frame's first call. Later calls silently overwrote that same region, so every draw that frame could end up reading whichever bind happened to run last. Switched to gr_update_buffer_data(), which always bump-allocates fresh. 2. Coordinate-space mismatch: ship_render_player_ship()'s cockpit-model draw positions its geometry via &cockpit_offset (rotate(sip->cockpit_offset) + sway) and its hull draw via &eye_offset (-eye_pos + sway), neither of which is combined with objp->pos -- unlike every other render path and unlike the RT shadow TLAS, which is built in true world space. The cockpit shader's inv_view_matrix reconstruction was therefore tracing rays from the wrong origin, never finding the TLAS geometry at all (query runs, occlusion never found -> solid green). Added a caller-supplied shadow_ray_world_offset field to the shared shadowCascadeParams uniform (uniform_structs.h + all 6 .sdr files), added to the reconstructed worldPos before tracing in main-f.sdr and deferred-f.sdr. shadow_cascade_params_bind() now takes the offset (and the existing self-shadow cull mask) as explicit parameters instead of inferring them from Lighting_mode internally -- necessary because the cockpit pass draws two different objects in two different internal frames (hull vs. cockpit), each needing a different correction, and a single implicit bind couldn't serve both. ship.cpp now rebinds with the correct offset before each of the four cockpit-pass draws (forward + TRANS, hull + cockpit). Known gap: the deferred lighting pass (gropengldeferred.cpp, VulkanPostProcessingLighting.cpp) shades every G-buffer texel from a single full-screen draw, so it can only carry one offset. This is exact for the common case (a ship with a cockpit model prerenders its hull separately, outside Lighting_mode::COCKPIT, so only cockpit fragments reach this G-buffer) but wrong for Cockpit_shares_coordinate_space=true ships or cockpit-less ships, where hull fragments also land in the same G-buffer and would need eye_pos-relative correction instead. Left unresolved; would need either a per-pixel discriminator or baking the correction into the G-buffer position at write time. Also: VulkanRaytracingManager::gatherCockpitShadowCasterInstance() now adds the same cockpit_sway_val * acceleration term ship.cpp applies to the rendered cockpit geometry, so the TLAS proxy doesn't drift from the actual mesh under acceleration. Includes a TEMPORARY one-shot mprintf in shadow_cascade_params_bind() logging the cockpit bind's |world_offset| -- intended to be removed once the next rt_shadow_debug in-game test confirms the fix; not yet validated against real rendering. Co-Authored-By: Claude Sonnet 5 --- code/def_files/data/effects/deferred-f.sdr | 11 ++- code/def_files/data/effects/main-f.sdr | 11 ++- code/def_files/data/effects/main-v.sdr | 5 ++ code/def_files/data/effects/shadow_map-g.sdr | 5 ++ code/def_files/data/effects/shadow_map-v.sdr | 5 ++ code/graphics/opengl/gropengldeferred.cpp | 14 +++- code/graphics/shadows.cpp | 70 +++++++++++++++---- code/graphics/shadows.h | 19 ++++- code/graphics/util/uniform_structs.h | 27 +++++-- .../vulkan/VulkanPostProcessingLighting.cpp | 11 ++- code/graphics/vulkan/VulkanRaytracingTlas.cpp | 8 +++ code/object/objectsort.cpp | 2 +- code/ship/ship.cpp | 35 +++++++++- 13 files changed, 198 insertions(+), 25 deletions(-) diff --git a/code/def_files/data/effects/deferred-f.sdr b/code/def_files/data/effects/deferred-f.sdr index 9cbcd5e8145..393ac827ee8 100644 --- a/code/def_files/data/effects/deferred-f.sdr +++ b/code/def_files/data/effects/deferred-f.sdr @@ -72,6 +72,11 @@ uniform shadowCascadeParams { // Debug visualization toggle; see shadow_cascade_static_data (uniform_structs.h). int rt_shadow_debug_visualize; + + // World-space correction for RT shadow ray reconstruction; see + // shadow_cascade_static_data (uniform_structs.h) and shadow_cascade_params_bind() + // (shadows.cpp) for the derivation. + 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]; @@ -326,7 +331,11 @@ void main() if (enable_shadows != 0) { #ifdef RT_SHADOWS - vec3 worldPos = (inv_view_matrix * vec4(position, 1.0)).xyz; + // shadow_ray_world_offset corrects for the cockpit pass's view matrix + // being anchored at leaning_position rather than the ship's actual world + // position -- see shadow_cascade_static_data (uniform_structs.h). Zero + // everywhere else, so this is a no-op outside the cockpit. + 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); diff --git a/code/def_files/data/effects/main-f.sdr b/code/def_files/data/effects/main-f.sdr index fd8b8255a10..b17d2fa79bc 100644 --- a/code/def_files/data/effects/main-f.sdr +++ b/code/def_files/data/effects/main-f.sdr @@ -110,6 +110,11 @@ uniform shadowCascadeParams { // Debug visualization toggle; see shadow_cascade_static_data (uniform_structs.h). int rt_shadow_debug_visualize; + + // World-space correction for RT shadow ray reconstruction; see + // shadow_cascade_static_data (uniform_structs.h) and shadow_cascade_params_bind() + // (shadows.cpp) for the derivation. + 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]; @@ -244,7 +249,11 @@ 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; + // shadow_ray_world_offset corrects for the cockpit pass's view matrix/model + // positions being anchored at leaning_position rather than the ship's actual + // world position -- see shadow_cascade_static_data (uniform_structs.h). Zero + // everywhere else, so this is a no-op outside the cockpit. + vec3 worldPos = (invView * vertIn.position).xyz + shadow_ray_world_offset; vec3 worldNormal = normalize((invView * vec4(normal, 0.0)).xyz); int shadowedDirectionalCount = 0; // Debug: highest occlusion seen from any raytraced shadow query this fragment, diff --git a/code/def_files/data/effects/main-v.sdr b/code/def_files/data/effects/main-v.sdr index 721eaf06577..91e60900b17 100644 --- a/code/def_files/data/effects/main-v.sdr +++ b/code/def_files/data/effects/main-v.sdr @@ -124,6 +124,11 @@ uniform shadowCascadeParams { // Debug visualization toggle; see shadow_cascade_static_data (uniform_structs.h). int rt_shadow_debug_visualize; + + // World-space correction for RT shadow ray reconstruction; see + // shadow_cascade_static_data (uniform_structs.h) and shadow_cascade_params_bind() + // (shadows.cpp) for the derivation. + 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 76558508e8f..2700c07318d 100644 --- a/code/def_files/data/effects/shadow_map-g.sdr +++ b/code/def_files/data/effects/shadow_map-g.sdr @@ -21,6 +21,11 @@ layout (std140) uniform shadowCascadeParams { // Debug visualization toggle; see shadow_cascade_static_data (uniform_structs.h). int rt_shadow_debug_visualize; + + // World-space correction for RT shadow ray reconstruction; see + // shadow_cascade_static_data (uniform_structs.h) and shadow_cascade_params_bind() + // (shadows.cpp) for the derivation. + 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 6f84131274a..ad5ba957718 100644 --- a/code/def_files/data/effects/shadow_map-v.sdr +++ b/code/def_files/data/effects/shadow_map-v.sdr @@ -51,6 +51,11 @@ uniform shadowCascadeParams { // Debug visualization toggle; see shadow_cascade_static_data (uniform_structs.h). int rt_shadow_debug_visualize; + + // World-space correction for RT shadow ray reconstruction; see + // shadow_cascade_static_data (uniform_structs.h) and shadow_cascade_params_bind() + // (shadows.cpp) for the derivation. + 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/graphics/opengl/gropengldeferred.cpp b/code/graphics/opengl/gropengldeferred.cpp index 35d67419977..55ffc6e6034 100644 --- a/code/graphics/opengl/gropengldeferred.cpp +++ b/code/graphics/opengl/gropengldeferred.cpp @@ -22,7 +22,9 @@ #include "nebula/neb.h" #include "nebula/volumetrics.h" #include "mod_table/mod_table.h" +#include "object/object.h" #include "render/3d.h" +#include "ship/ship.h" #include "tracing/tracing.h" #ifdef USE_OPENGL_ES #include "es_compatibility.h" @@ -318,7 +320,17 @@ void gr_opengl_deferred_lighting_finish() 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); + // See shadow_cascade_params_bind()'s declaration (shadows.h): this deferred + // lighting pass shades every G-buffer texel written during the cockpit block + // (ship.cpp) in one full-screen draw, so it can only carry one correction. + // That's exact for the common case (a ship with a cockpit model prerenders its + // hull separately, outside Lighting_mode::COCKPIT, so only cockpit fragments + // land in this G-buffer); it's a known gap for Cockpit_shares_coordinate_space + // ships or cockpit-less ships, where hull fragments also land here and would + // need a different correction than the cockpit's. + 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); } header->invScreenWidth = 1.0f / gr_screen.max_w; diff --git a/code/graphics/shadows.cpp b/code/graphics/shadows.cpp index d3f6437d297..b67935bb0b7 100644 --- a/code/graphics/shadows.cpp +++ b/code/graphics/shadows.cpp @@ -681,11 +681,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; } @@ -780,9 +780,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; @@ -803,7 +803,7 @@ static void render_viewer_shadow(object* objp, const matrix* light_matrix, 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); @@ -1051,7 +1051,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; } @@ -1076,14 +1077,41 @@ void shadow_cascade_params_bind(int cascade_offset, int cascade_count) { // Default excludes the viewer ship's own hull from raytraced shadow rays // (mirrors the rasterized path's unconditional exclusion of Viewer_obj from - // the main shadow cascades, shadows_render_all() below); only the cockpit's - // own shading pass allows it through, and only when the ship is actually - // supposed to cast onto its cockpit this frame. - static_data.shadow_ray_cull_mask = - (Lighting_mode == lighting_mode::COCKPIT && ship_render_player_ship_casts_shadow_on_cockpit()) ? 0xFF : 0x7F; + // the main shadow cascades, shadows_render_all() below); callers opt in via + // allow_viewer_self_shadow only for the specific draw where it's wanted + // (the cockpit's own shading, so the hull can cast onto the cockpit -- NOT + // the hull's own shading in the same pass, which would just be self-shadow + // acne). See shadow_cascade_params_bind()'s declaration (shadows.h). + static_data.shadow_ray_cull_mask = allow_viewer_self_shadow ? 0xFF : 0x7F; static_data.rt_shadow_debug_visualize = Rt_shadow_debug_visualize ? 1 : 0; + // See shadow_cascade_params_bind()'s declaration (shadows.h) and + // shadow_ray_world_offset's declaration (uniform_structs.h) for the + // derivation -- this is caller-supplied because a single pass can contain + // draws in different internal frames (e.g. ship_render_player_ship()'s + // eye-relative hull draw vs. its cockpit-offset-relative cockpit draw) that + // need different corrections; Lighting_mode alone can't distinguish them. + static_data.shadow_ray_world_offset = world_offset; + + // TEMPORARY: one-shot diagnostic for the cockpit coordinate-space fix. Every + // current cockpit-range bind (ship.cpp, gropengldeferred.cpp, + // VulkanPostProcessingLighting.cpp) passes exactly this (offset, count) + // pair, so this identifies "a cockpit-pass bind happened" regardless of + // whether world_offset came out zero or nonzero -- unlike gating on + // |world_offset|, this can't be silently skipped just because the ship + // happens to be near the origin this test run. Remove once confirmed + // against a live rt_shadow_debug test. + if (cascade_offset == 0 && cascade_count == Num_cockpit_shadow_cascades) { + static bool logged_once = false; + if (!logged_once) { + logged_once = true; + mprintf(("RT shadow space-offset check: |world_offset|=%.3f (%.2f, %.2f, %.2f) self_shadow=%d\n", + vm_vec_mag(&world_offset), world_offset.xyz.x, world_offset.xyz.y, world_offset.xyz.z, + allow_viewer_self_shadow ? 1 : 0)); + } + } + Shadow_cascade_count = cascade_count; offset += sizeof(graphics::shadow_cascade_static_data); @@ -1108,7 +1136,23 @@ 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 function is called + // multiple times per frame with different content (main scene, viewer-ship- + // onto-cockpit, cockpit-self, forward cockpit, deferred cockpit -- up to 5 + // times when the player's cockpit is rendered). On Vulkan's streaming buffer + // path, gr_update_buffer_data_offset() only bump-allocates a fresh GPU region + // on the first call of the frame; every later call this frame would silently + // overwrite that same region in place instead of getting its own. Since + // command buffers are recorded before the GPU executes any of them, every + // draw this frame would then read whichever bind happened to run last, + // regardless of which one was "current" when that draw was recorded -- e.g. + // the main scene's shadow-casting draws picking up the cockpit's cascade + // range, or the cockpit's shadow_ray_cull_mask read stale/wrong. + // gr_update_buffer_data() always bump-allocates fresh on Vulkan (matching the + // per-pass isolation deferred_global_data/deferred_light_data already use for + // the same reason), and is a safe, standard buffer-orphaning glBufferData() + // call 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); } diff --git a/code/graphics/shadows.h b/code/graphics/shadows.h index 0c442f26bff..68dea3e59bf 100644 --- a/code/graphics/shadows.h +++ b/code/graphics/shadows.h @@ -109,7 +109,24 @@ 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 (inv_view_matrix * +// viewSpacePos) position before tracing. Zero for every pass that already +// renders in true world space (objp->pos-anchored view + model matrices). +// The cockpit pass is the only exception -- see ship_render_player_ship() +// (ship.cpp) and shadow_cascade_static_data's shadow_ray_world_offset comment +// (uniform_structs.h) for the derivation. Callers whose draws share this pass +// but use a *different* internal frame (e.g. the eye-relative hull draw vs. +// the cockpit-offset-relative cockpit draw) must rebind with the offset +// appropriate to whichever draw is about to happen -- one bind cannot serve +// both. +// allow_viewer_self_shadow: lets the viewer ship's own hull TLAS instance +// (tagged with a dedicated mask bit, see +// VulkanRaytracingManager::gatherShadowCasterInstances()) participate in this +// pass's shadow rays. False everywhere except the cockpit's own shading, +// where the hull is meant to be able to cast onto the cockpit. +void shadow_cascade_params_bind(int cascade_offset, int cascade_count, const vec3d& world_offset, + bool allow_viewer_self_shadow = false); 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 08562a1d1e4..7592e57b614 100644 --- a/code/graphics/util/uniform_structs.h +++ b/code/graphics/util/uniform_structs.h @@ -161,14 +161,31 @@ struct shadow_cascade_static_data { // traceShadowRay()'s callers in main-f.sdr/deferred-f.sdr, not by // traceShadowRay() itself. int rt_shadow_debug_visualize; - float pad[2]; // keep shadow_proj_matrix[]'s offset 16-byte aligned (std140) + float pad[2]; // pre-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 + // (inv_view_matrix * viewSpacePos) before tracing. Zero everywhere except the + // cockpit pass: ship_render_player_ship() renders the cockpit using a view + // matrix anchored at `leaning_position` (a small head-lean vector, never + // combined with the ship's actual world position -- see playercontrol.cpp) + // and passes model positions as offsets in that same un-translated frame + // (ship.cpp's `cockpit_offset`/`eye_offset`, likewise never combined with + // objp->pos). So inv_view_matrix reconstructs positions in that frame, not + // true world space, while the shadow TLAS is built in true world space + // (VulkanRaytracingManager::gatherShadowCasterInstances()/ + // gatherCockpitShadowCasterInstance() both anchor at objp->pos). Adding the + // viewer ship's world position back here closes that gap -- see + // shadow_cascade_params_bind() for the derivation and 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] -// + 4 [rt_shadow_debug_visualize] + 8 [pad[2]] = 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)"); +// + 4 [rt_shadow_debug_visualize] + 8 [pad[2]] + 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..30700bddbce 100644 --- a/code/graphics/vulkan/VulkanPostProcessingLighting.cpp +++ b/code/graphics/vulkan/VulkanPostProcessingLighting.cpp @@ -24,6 +24,8 @@ #include "tracing/tracing.h" #include "nebula/neb.h" #include "mission/missionparse.h" +#include "object/object.h" +#include "ship/ship.h" extern float Sun_spot; extern int Game_subspace_effect; @@ -510,7 +512,14 @@ void VulkanDeferredLighting::render(vk::CommandBuffer cmd) 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); + // See shadow_cascade_params_bind()'s declaration (shadows.h) and the mirrored + // comment in gropengldeferred.cpp: this full-screen pass can only carry one + // world_offset for every G-buffer texel it shades, which is exact for the + // common case (cockpit-model ships prerender their hull outside + // Lighting_mode::COCKPIT) and a known gap otherwise. + 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); } } diff --git a/code/graphics/vulkan/VulkanRaytracingTlas.cpp b/code/graphics/vulkan/VulkanRaytracingTlas.cpp index f854fda6080..c49d9f7d226 100644 --- a/code/graphics/vulkan/VulkanRaytracingTlas.cpp +++ b/code/graphics/vulkan/VulkanRaytracingTlas.cpp @@ -306,6 +306,14 @@ void VulkanRaytracingManager::gatherCockpitShadowCasterInstance(SCP_vectorcockpit_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); 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..373bda0c8ec 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -8245,14 +8245,33 @@ 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; } + // This pass draws two different objects (hull, cockpit) in two different + // internal frames -- see the two model_render_immediate() calls below -- + // so each needs its own RT shadow-ray world-space correction; a single + // shadow_cascade_params_bind() call can't serve both. Derivations: + // + // Hull draw uses &eye_offset as its position arg (eye-relative frame), so + // reconstructed + (objp->pos - eye_offset) lands in the same true-world + // space the shadow TLAS is built in. + // + // Cockpit draw uses &cockpit_offset (rotate(sip->cockpit_offset) + sway, + // origin-relative frame -- see below), so reconstructed + objp->pos lands + // there instead. See shadow_cascade_static_data::shadow_ray_world_offset + // (uniform_structs.h) for the full 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(); + if (light_deferredcockpit_enabled()) { gr_deferred_lighting_begin(true); @@ -8282,6 +8301,9 @@ 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); + 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, MODEL_RENDER_OPAQUE); gr_zbuffer_clear(true); } @@ -8292,6 +8314,10 @@ 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; + 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, MODEL_RENDER_OPAQUE); } @@ -8320,10 +8346,17 @@ void ship_render_player_ship(object* objp, const vec3d* cam_offset, const matrix gr_zbuffer_set(ZBUFFER_TYPE_READ); if (deferredRenderShipModel) { + 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, MODEL_RENDER_TRANS); } if (renderCockpitModel) { + 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, MODEL_RENDER_TRANS); } From 100088a300dd5944ae60e60e3db28684e5bba0e7 Mon Sep 17 00:00:00 2001 From: the-e Date: Tue, 4 Aug 2026 19:02:13 +0200 Subject: [PATCH 4/5] Remove temporary RT shadow cockpit space-offset diagnostic Confirmed fixed in-game via rt_shadow_debug: cockpit now shows correct red/green occlusion instead of solid green. The one-shot mprintf that validated shadow_ray_world_offset's magnitude has served its purpose. Co-Authored-By: Claude Sonnet 5 --- code/graphics/shadows.cpp | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/code/graphics/shadows.cpp b/code/graphics/shadows.cpp index b67935bb0b7..f473a5d1b71 100644 --- a/code/graphics/shadows.cpp +++ b/code/graphics/shadows.cpp @@ -1094,24 +1094,6 @@ void shadow_cascade_params_bind(int cascade_offset, int cascade_count, const vec // need different corrections; Lighting_mode alone can't distinguish them. static_data.shadow_ray_world_offset = world_offset; - // TEMPORARY: one-shot diagnostic for the cockpit coordinate-space fix. Every - // current cockpit-range bind (ship.cpp, gropengldeferred.cpp, - // VulkanPostProcessingLighting.cpp) passes exactly this (offset, count) - // pair, so this identifies "a cockpit-pass bind happened" regardless of - // whether world_offset came out zero or nonzero -- unlike gating on - // |world_offset|, this can't be silently skipped just because the ship - // happens to be near the origin this test run. Remove once confirmed - // against a live rt_shadow_debug test. - if (cascade_offset == 0 && cascade_count == Num_cockpit_shadow_cascades) { - static bool logged_once = false; - if (!logged_once) { - logged_once = true; - mprintf(("RT shadow space-offset check: |world_offset|=%.3f (%.2f, %.2f, %.2f) self_shadow=%d\n", - vm_vec_mag(&world_offset), world_offset.xyz.x, world_offset.xyz.y, world_offset.xyz.z, - allow_viewer_self_shadow ? 1 : 0)); - } - } - Shadow_cascade_count = cascade_count; offset += sizeof(graphics::shadow_cascade_static_data); From 840954d25c2a376e8e44a4904979acb48a20865c Mon Sep 17 00:00:00 2001 From: the-e Date: Tue, 4 Aug 2026 20:19:27 +0200 Subject: [PATCH 5/5] Address code-quality review: dedup shadow-cascade bind logic, remove debug-viz Removes the rt_shadow_debug diagnostic feature now that the cockpit solid-green bug it was built to chase down is fixed; extracts the duplicated deferred-pass world_offset/allow_viewer_self_shadow computation (gropengldeferred.cpp/VulkanPostProcessingLighting.cpp) into shadow_cascade_params_bind_deferred(); factors the independently-duplicated "would the player's cockpit model render" gate (ship.cpp x2, shadows.cpp, VulkanRaytracingTlas.cpp) into ship_player_cockpit_model_would_render(); collapses ship.cpp's four duplicated shadow-bind-then-render blocks into two renderHull/ renderCockpit lambdas; names the TLAS ray-cull mask bits instead of bare 0x80/0x7F literals; and trims comment volume that had been duplicated near-verbatim across five .sdr files down to single pointers at the canonical explanation in uniform_structs.h. No behavior change; ninja code and ninja Freespace2 both build clean. Co-Authored-By: Claude Sonnet 5 --- code/def_files/data/effects/deferred-f.sdr | 41 ++--------- code/def_files/data/effects/main-f.sdr | 41 ++--------- code/def_files/data/effects/main-v.sdr | 12 +--- code/def_files/data/effects/shadow_map-g.sdr | 12 +--- code/def_files/data/effects/shadow_map-v.sdr | 12 +--- code/def_files/data/effects/shadows.sdr | 6 +- code/graphics/opengl/gropengldeferred.cpp | 16 +---- code/graphics/shadows.cpp | 68 +++++------------- code/graphics/shadows.h | 43 +++++------ code/graphics/util/uniform_structs.h | 44 ++++-------- .../vulkan/VulkanPostProcessingLighting.cpp | 14 +--- code/graphics/vulkan/VulkanRaytracing.h | 26 +++---- code/graphics/vulkan/VulkanRaytracingTlas.cpp | 26 +++---- code/ship/ship.cpp | 72 +++++++++---------- code/ship/ship.h | 6 ++ 15 files changed, 133 insertions(+), 306 deletions(-) diff --git a/code/def_files/data/effects/deferred-f.sdr b/code/def_files/data/effects/deferred-f.sdr index 393ac827ee8..c214f8b18a9 100644 --- a/code/def_files/data/effects/deferred-f.sdr +++ b/code/def_files/data/effects/deferred-f.sdr @@ -66,16 +66,10 @@ uniform shadowCascadeParams { mat4 shadow_mv_matrix; - // Ray cull mask for raytraced shadow queries; see shadow_cascade_static_data (uniform_structs.h) - // and shadow_cascade_params_bind() (shadows.cpp) for how this gets set. + // 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; - - // Debug visualization toggle; see shadow_cascade_static_data (uniform_structs.h). - int rt_shadow_debug_visualize; - - // World-space correction for RT shadow ray reconstruction; see - // shadow_cascade_static_data (uniform_structs.h) and shadow_cascade_params_bind() - // (shadows.cpp) for the derivation. vec3 shadow_ray_world_offset; mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES]; vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4]; @@ -320,30 +314,15 @@ void main() float lightDist; GetLightInfo(position, alpha, reflectDir, lightDir, attenuation, area_normalisation, lightDist); - // Debug: occlusion from this pixel's raytraced shadow query, and whether a - // query actually ran at all -- rt_shadow_debug_visualize needs both, not - // just occlusion, since "no query ran" and "query ran, fully lit" would - // otherwise look identical (no tint either way). Both stay at their - // zero/false default whenever the CSM fallback runs instead (#else below) - // or enable_shadows is 0. - float rtShadowDebugOcclusion = 0.0; - bool rtShadowDebugQueried = false; - if (enable_shadows != 0) { #ifdef RT_SHADOWS - // shadow_ray_world_offset corrects for the cockpit pass's view matrix - // being anchored at leaning_position rather than the ship's actual world - // position -- see shadow_cascade_static_data (uniform_structs.h). Zero - // everywhere else, so this is a no-op outside the cockpit. + // 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); - float rtShadowTerm = traceShadowRay(shadow_tlas, worldPos, worldNormal, worldLightDir, lightDist, rtShadowBias, uint(shadow_ray_cull_mask)); - rtShadowDebugOcclusion = 1.0 - rtShadowTerm; - rtShadowDebugQueried = true; - attenuation *= rtShadowTerm; + 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]; @@ -358,16 +337,6 @@ void main() vec3 halfVec = normalize(lightDir + eyeDir); float NdotL = clamp(dot(normal, lightDir), 0.0, 1.0); fragmentColor.rgb = computeLighting(specColor.rgb, diffColor, lightDir, normal.xyz, halfVec, eyeDir, roughness, fresnel, NdotL).rgb * diffuseLightColor * attenuation * area_normalisation; - -#ifdef RT_SHADOWS - // Three readable states: untouched = no RT shadow query ran for this pixel - // (permutation/enable_shadows/m_shadow-init problem); solid green = query - // ran, found no occluder (TLAS/transform problem, or genuinely unoccluded); - // solid red = query ran, fully occluded. - if (rt_shadow_debug_visualize != 0 && rtShadowDebugQueried) { - fragmentColor.rgb = mix(vec3(0.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), rtShadowDebugOcclusion); - } -#endif } fragOut0 = max(fragmentColor, vec4(0.0)); diff --git a/code/def_files/data/effects/main-f.sdr b/code/def_files/data/effects/main-f.sdr index b17d2fa79bc..ef16bafb900 100644 --- a/code/def_files/data/effects/main-f.sdr +++ b/code/def_files/data/effects/main-f.sdr @@ -104,16 +104,10 @@ uniform shadowCascadeParams { mat4 shadow_mv_matrix; - // Ray cull mask for raytraced shadow queries; see shadow_cascade_static_data (uniform_structs.h) - // and shadow_cascade_params_bind() (shadows.cpp) for how this gets set. + // 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; - - // Debug visualization toggle; see shadow_cascade_static_data (uniform_structs.h). - int rt_shadow_debug_visualize; - - // World-space correction for RT shadow ray reconstruction; see - // shadow_cascade_static_data (uniform_structs.h) and shadow_cascade_params_bind() - // (shadows.cpp) for the derivation. vec3 shadow_ray_world_offset; mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES]; vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4]; @@ -249,21 +243,10 @@ vec3 CalculateLighting(vec3 normal, vec3 diffuseMaterial, vec3 specularMaterial, rtShadowsActive = true; #prereplace ENDIF_FLAG //MODEL_SDR_FLAG_SHADOWS mat4 invView = inverse(viewMatrix); - // shadow_ray_world_offset corrects for the cockpit pass's view matrix/model - // positions being anchored at leaning_position rather than the ship's actual - // world position -- see shadow_cascade_static_data (uniform_structs.h). Zero - // everywhere else, so this is a no-op outside the cockpit. + // 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; - // Debug: highest occlusion seen from any raytraced shadow query this fragment, - // and whether a query actually ran at all -- rt_shadow_debug_visualize needs - // both, not just occlusion, since "no query ran" and "query ran, fully lit" - // would otherwise look identical (no tint either way). Both stay at their - // zero/false default whenever RT shadows aren't actually being evaluated for - // this fragment (permutation not compiled in, or shadow-receiving is off). - float rtShadowDebugOcclusion = 0.0; - bool rtShadowDebugQueried = false; #endif #pragma optionNV unroll all @@ -273,8 +256,6 @@ vec3 CalculateLighting(vec3 normal, vec3 diffuseMaterial, vec3 specularMaterial, 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, uint(shadow_ray_cull_mask)); - rtShadowDebugOcclusion = max(rtShadowDebugOcclusion, 1.0 - shadow); - rtShadowDebugQueried = true; ++shadowedDirectionalCount; } else { shadow = 1.0; @@ -297,19 +278,7 @@ vec3 CalculateLighting(vec3 normal, vec3 diffuseMaterial, vec3 specularMaterial, lightSpecular += lights[i].diffuse_color.rgb * computeLighting(specularMaterial, diffuseMaterial, lightDir, normal, halfVec, eyeDir, roughness, fresnel, NdotL) * attenuation * shadow; } - vec3 litColor = diffuseMaterial * lightAmbient + lightSpecular; -#ifdef MODEL_SDR_FLAG_RT_SHADOWS - // Three readable states: untouched = no RT shadow query ran for this fragment - // (permutation/shadow-receiving problem); solid green = query ran, found no - // occluder (nothing between fragment and light, or a TLAS/transform problem); - // solid red = query ran, fully occluded. Partial occlusion (soft/PCSS-less RT - // shadows are hard-edged per light, but multiple lights can blend) interpolates - // green->red. - if (rt_shadow_debug_visualize != 0 && rtShadowDebugQueried) { - litColor = mix(vec3(0.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), rtShadowDebugOcclusion); - } -#endif - return litColor; + return diffuseMaterial * lightAmbient + lightSpecular; } void main() diff --git a/code/def_files/data/effects/main-v.sdr b/code/def_files/data/effects/main-v.sdr index 91e60900b17..e6a94c194da 100644 --- a/code/def_files/data/effects/main-v.sdr +++ b/code/def_files/data/effects/main-v.sdr @@ -118,16 +118,10 @@ uniform shadowCascadeParams { mat4 shadow_mv_matrix; - // Ray cull mask for raytraced shadow queries; see shadow_cascade_static_data (uniform_structs.h) - // and shadow_cascade_params_bind() (shadows.cpp) for how this gets set. + // 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; - - // Debug visualization toggle; see shadow_cascade_static_data (uniform_structs.h). - int rt_shadow_debug_visualize; - - // World-space correction for RT shadow ray reconstruction; see - // shadow_cascade_static_data (uniform_structs.h) and shadow_cascade_params_bind() - // (shadows.cpp) for the derivation. vec3 shadow_ray_world_offset; mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES]; vec4 cascade_distances[(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 2700c07318d..475061f448d 100644 --- a/code/def_files/data/effects/shadow_map-g.sdr +++ b/code/def_files/data/effects/shadow_map-g.sdr @@ -15,16 +15,10 @@ layout (std140) uniform shadowCascadeParams { mat4 shadow_mv_matrix; - // Ray cull mask for raytraced shadow queries; see shadow_cascade_static_data (uniform_structs.h) - // and shadow_cascade_params_bind() (shadows.cpp) for how this gets set. + // 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; - - // Debug visualization toggle; see shadow_cascade_static_data (uniform_structs.h). - int rt_shadow_debug_visualize; - - // World-space correction for RT shadow ray reconstruction; see - // shadow_cascade_static_data (uniform_structs.h) and shadow_cascade_params_bind() - // (shadows.cpp) for the derivation. vec3 shadow_ray_world_offset; mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES]; vec4 cascade_distances[(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 ad5ba957718..dc746a5f2cb 100644 --- a/code/def_files/data/effects/shadow_map-v.sdr +++ b/code/def_files/data/effects/shadow_map-v.sdr @@ -45,16 +45,10 @@ uniform shadowCascadeParams { mat4 shadow_mv_matrix; - // Ray cull mask for raytraced shadow queries; see shadow_cascade_static_data (uniform_structs.h) - // and shadow_cascade_params_bind() (shadows.cpp) for how this gets set. + // 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; - - // Debug visualization toggle; see shadow_cascade_static_data (uniform_structs.h). - int rt_shadow_debug_visualize; - - // World-space correction for RT shadow ray reconstruction; see - // shadow_cascade_static_data (uniform_structs.h) and shadow_cascade_params_bind() - // (shadows.cpp) for the derivation. vec3 shadow_ray_world_offset; mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES]; vec4 cascade_distances[(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 782daf38220..b82937e139c 100644 --- a/code/def_files/data/effects/shadows.sdr +++ b/code/def_files/data/effects/shadows.sdr @@ -118,10 +118,8 @@ float computeRtShadowBias(float cameraDist, float biasMin, float biasMax) // 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 -- -// it's 0x7F outside the cockpit pass, excluding the viewer ship's own hull instance -// (tagged 0x80 in VulkanRaytracingManager::gatherShadowCasterInstances()) to match -// the rasterized path's exclusion of Viewer_obj from the main shadow cascades, and -// 0xFF inside the cockpit pass when the ship is allowed to cast onto its own cockpit. +// 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; diff --git a/code/graphics/opengl/gropengldeferred.cpp b/code/graphics/opengl/gropengldeferred.cpp index 55ffc6e6034..a198c7274e6 100644 --- a/code/graphics/opengl/gropengldeferred.cpp +++ b/code/graphics/opengl/gropengldeferred.cpp @@ -22,9 +22,7 @@ #include "nebula/neb.h" #include "nebula/volumetrics.h" #include "mod_table/mod_table.h" -#include "object/object.h" #include "render/3d.h" -#include "ship/ship.h" #include "tracing/tracing.h" #ifdef USE_OPENGL_ES #include "es_compatibility.h" @@ -318,19 +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; - // See shadow_cascade_params_bind()'s declaration (shadows.h): this deferred - // lighting pass shades every G-buffer texel written during the cockpit block - // (ship.cpp) in one full-screen draw, so it can only carry one correction. - // That's exact for the common case (a ship with a cockpit model prerenders its - // hull separately, outside Lighting_mode::COCKPIT, so only cockpit fragments - // land in this G-buffer); it's a known gap for Cockpit_shares_coordinate_space - // ships or cockpit-less ships, where hull fragments also land here and would - // need a different correction than the cockpit's. - 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_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 f473a5d1b71..e5168979e38 100644 --- a/code/graphics/shadows.cpp +++ b/code/graphics/shadows.cpp @@ -27,7 +27,6 @@ #include "ship/ship.h" #include "ship/shipfx.h" #include "render/3d.h" -#include "debugconsole/console.h" #include "tracing/tracing.h" #include "util/uniform_structs.h" @@ -38,21 +37,6 @@ matrix4 Shadow_view_matrix_render; SCP_vector Shadow_proj_matrix; SCP_vector Shadow_cascade_distances; -bool Rt_shadow_debug_visualize = false; - -DCF(rt_shadow_debug, "Toggles red visualization of raytraced shadow occlusion") -{ - if (dc_optional_string_either("status", "--status") || dc_optional_string_either("?", "--?")) { - dc_printf("Raytraced shadow debug visualization is %s", Rt_shadow_debug_visualize ? "ON" : "OFF"); - return; - } - - Rt_shadow_debug_visualize = !Rt_shadow_debug_visualize; - dc_printf("Raytraced shadow debug visualization %s. Occluded fragments will be tinted red wherever " - "a raytraced shadow query is actually evaluated (Vulkan, RT shadows enabled, a shadow-receiving " - "material).\n", Rt_shadow_debug_visualize ? "enabled" : "disabled"); -} - static SCP_vector Shadow_frustums; ShadowQuality Shadow_quality = ShadowQuality::Disabled; @@ -798,7 +782,7 @@ 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; @@ -1075,23 +1059,8 @@ void shadow_cascade_params_bind(int cascade_offset, int cascade_count, const vec static_data.rtShadowBiasMax = Rt_shadow_bias_max; static_data.shadow_mv_matrix = Shadow_view_matrix_light; - // Default excludes the viewer ship's own hull from raytraced shadow rays - // (mirrors the rasterized path's unconditional exclusion of Viewer_obj from - // the main shadow cascades, shadows_render_all() below); callers opt in via - // allow_viewer_self_shadow only for the specific draw where it's wanted - // (the cockpit's own shading, so the hull can cast onto the cockpit -- NOT - // the hull's own shading in the same pass, which would just be self-shadow - // acne). See shadow_cascade_params_bind()'s declaration (shadows.h). - static_data.shadow_ray_cull_mask = allow_viewer_self_shadow ? 0xFF : 0x7F; - - static_data.rt_shadow_debug_visualize = Rt_shadow_debug_visualize ? 1 : 0; - - // See shadow_cascade_params_bind()'s declaration (shadows.h) and - // shadow_ray_world_offset's declaration (uniform_structs.h) for the - // derivation -- this is caller-supplied because a single pass can contain - // draws in different internal frames (e.g. ship_render_player_ship()'s - // eye-relative hull draw vs. its cockpit-offset-relative cockpit draw) that - // need different corrections; Lighting_mode alone can't distinguish them. + // 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; @@ -1118,26 +1087,25 @@ void shadow_cascade_params_bind(int cascade_offset, int cascade_count, const vec } offset += sizeof(float) * padding; - // Must be the full-replacement update (not _offset): this function is called - // multiple times per frame with different content (main scene, viewer-ship- - // onto-cockpit, cockpit-self, forward cockpit, deferred cockpit -- up to 5 - // times when the player's cockpit is rendered). On Vulkan's streaming buffer - // path, gr_update_buffer_data_offset() only bump-allocates a fresh GPU region - // on the first call of the frame; every later call this frame would silently - // overwrite that same region in place instead of getting its own. Since - // command buffers are recorded before the GPU executes any of them, every - // draw this frame would then read whichever bind happened to run last, - // regardless of which one was "current" when that draw was recorded -- e.g. - // the main scene's shadow-casting draws picking up the cockpit's cascade - // range, or the cockpit's shadow_ray_cull_mask read stale/wrong. - // gr_update_buffer_data() always bump-allocates fresh on Vulkan (matching the - // per-pass isolation deferred_global_data/deferred_light_data already use for - // the same reason), and is a safe, standard buffer-orphaning glBufferData() - // call on OpenGL. + // 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 68dea3e59bf..317730f3d6e 100644 --- a/code/graphics/shadows.h +++ b/code/graphics/shadows.h @@ -95,12 +95,14 @@ extern SCP_vector Shadow_proj_matrix; extern SCP_vector Shadow_cascade_distances; extern int Shadow_cascade_count; -// Debug visualization: when true, fragments occluded by a raytraced shadow -// query (traceShadowRay()/shadows.sdr) are tinted red instead of shaded -// normally. Toggled via the `rt_shadow_debug` debug-console command. Packed -// into the shared shadowCascadeParams uniform (shadow_cascade_static_data) -// alongside shadow_ray_cull_mask -- see shadow_cascade_params_bind(). -extern bool Rt_shadow_debug_visualize; +// 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); @@ -110,24 +112,23 @@ void shadows_render_all(fov_t fov, matrix *eye_orient, vec3d *eye_pos, void shadow_cascade_params_init(); void shadow_cascade_params_shutdown(); -// world_offset: added to traceShadowRay()'s reconstructed (inv_view_matrix * -// viewSpacePos) position before tracing. Zero for every pass that already -// renders in true world space (objp->pos-anchored view + model matrices). -// The cockpit pass is the only exception -- see ship_render_player_ship() -// (ship.cpp) and shadow_cascade_static_data's shadow_ray_world_offset comment -// (uniform_structs.h) for the derivation. Callers whose draws share this pass -// but use a *different* internal frame (e.g. the eye-relative hull draw vs. -// the cockpit-offset-relative cockpit draw) must rebind with the offset -// appropriate to whichever draw is about to happen -- one bind cannot serve -// both. -// allow_viewer_self_shadow: lets the viewer ship's own hull TLAS instance -// (tagged with a dedicated mask bit, see -// VulkanRaytracingManager::gatherShadowCasterInstances()) participate in this -// pass's shadow rays. False everywhere except the cockpit's own shading, -// where the hull is meant to be able to cast onto the cockpit. +// 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 7592e57b614..f27d653abe3 100644 --- a/code/graphics/util/uniform_structs.h +++ b/code/graphics/util/uniform_structs.h @@ -148,43 +148,29 @@ struct shadow_cascade_static_data { float rtShadowBiasMax; matrix4 shadow_mv_matrix; - // Ray cull mask for raytraced shadow queries (traceShadowRay()/shadows.sdr). - // Lets the viewer ship's own hull -- tagged with a dedicated TLAS instance - // mask bit, see VulkanRaytracingManager::gatherShadowCasterInstances() -- - // be selectively excluded from shadow rays 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). + // 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) - // Debug visualization toggle (see Rt_shadow_debug_visualize, shadows.h): - // nonzero paints raytraced-shadow-occluded fragments red. Read by - // traceShadowRay()'s callers in main-f.sdr/deferred-f.sdr, not by - // traceShadowRay() itself. - int rt_shadow_debug_visualize; - float pad[2]; // pre-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 - // (inv_view_matrix * viewSpacePos) before tracing. Zero everywhere except the - // cockpit pass: ship_render_player_ship() renders the cockpit using a view - // matrix anchored at `leaning_position` (a small head-lean vector, never - // combined with the ship's actual world position -- see playercontrol.cpp) - // and passes model positions as offsets in that same un-translated frame - // (ship.cpp's `cockpit_offset`/`eye_offset`, likewise never combined with - // objp->pos). So inv_view_matrix reconstructs positions in that frame, not - // true world space, while the shadow TLAS is built in true world space - // (VulkanRaytracingManager::gatherShadowCasterInstances()/ - // gatherCockpitShadowCasterInstance() both anchor at objp->pos). Adding the + // 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() for the derivation and where this is set. + // 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] -// + 4 [rt_shadow_debug_visualize] + 8 [pad[2]] + 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. +// + 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 { diff --git a/code/graphics/vulkan/VulkanPostProcessingLighting.cpp b/code/graphics/vulkan/VulkanPostProcessingLighting.cpp index 30700bddbce..e9ce9dde7f9 100644 --- a/code/graphics/vulkan/VulkanPostProcessingLighting.cpp +++ b/code/graphics/vulkan/VulkanPostProcessingLighting.cpp @@ -24,8 +24,6 @@ #include "tracing/tracing.h" #include "nebula/neb.h" #include "mission/missionparse.h" -#include "object/object.h" -#include "ship/ship.h" extern float Sun_spot; extern int Game_subspace_effect; @@ -509,17 +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; - // See shadow_cascade_params_bind()'s declaration (shadows.h) and the mirrored - // comment in gropengldeferred.cpp: this full-screen pass can only carry one - // world_offset for every G-buffer texel it shades, which is exact for the - // common case (cockpit-model ships prerender their hull outside - // Lighting_mode::COCKPIT) and a known gap otherwise. - 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_cascade_params_bind_deferred(); } } diff --git a/code/graphics/vulkan/VulkanRaytracing.h b/code/graphics/vulkan/VulkanRaytracing.h index edb26db9e99..7023552c81d 100644 --- a/code/graphics/vulkan/VulkanRaytracing.h +++ b/code/graphics/vulkan/VulkanRaytracing.h @@ -192,12 +192,8 @@ 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); - // defaults to 0xFF (visible to every ray). The one caller that needs - // something else is the viewer ship's own hull, which is tagged with a - // dedicated bit so shadow rays can selectively exclude it -- see - // gatherShadowCasterInstances()'s OBJ_SHIP case and shadows.sdr's - // traceShadowRay() for how the two ends of this scheme meet. + // `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, @@ -211,18 +207,12 @@ class VulkanRaytracingManager { // 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 compares against the global - // `Eye_position` (see submodelPassesDetailBox()), which is correct for - // world-anchored objects (ships/asteroids/debris/props) but not for the - // cockpit model -- render_viewer_shadow()'s rasterized cockpit shadow - // pass evaluates its own detail-box checks against a cockpit-relative eye - // position instead (shadows.cpp), which Eye_position does not replicate. - // Rather than derive that (would need cam_offset/rot_offset threaded down - // from shadows_render_all(), see design doc), the cockpit call - // (gatherCockpitShadowCasterInstance) passes true here to skip the check - // entirely -- cockpit models are small and sit right against the camera, - // so render-box/render-sphere culling is unlikely to matter at that - // range. Every other caller passes false (default), unaffected. + // `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, diff --git a/code/graphics/vulkan/VulkanRaytracingTlas.cpp b/code/graphics/vulkan/VulkanRaytracingTlas.cpp index c49d9f7d226..ebc48321cdb 100644 --- a/code/graphics/vulkan/VulkanRaytracingTlas.cpp +++ b/code/graphics/vulkan/VulkanRaytracingTlas.cpp @@ -16,6 +16,7 @@ #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" @@ -194,16 +195,11 @@ void VulkanRaytracingManager::gatherShadowCasterInstances(SCP_vectorpos, &objp->orient); @@ -277,14 +273,8 @@ void VulkanRaytracingManager::gatherCockpitShadowCasterInstance(SCP_vectorinstance]; ship_info* sip = &Ship_info[shipp->ship_info_index]; - // Mirrors the renderCockpitModel computation in ship_render_player_ship()/ - // render_viewer_shadow() (ship.cpp/shadows.cpp) -- kept as a fourth inline - // copy for consistency with those two, rather than factoring out a shared - // helper neither of them uses today. - const bool renderCockpitModel = - (Viewer_mode != VM_TOPDOWN) && sip->cockpit_model_num >= 0 && !Disable_cockpits; - if (!renderCockpitModel || Shadow_disable_overrides.disable_cockpit) { - return; // matches shadows.cpp:787's gate on the rasterized cockpit shadow pass + 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); diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 373bda0c8ec..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; @@ -8254,24 +8256,33 @@ void ship_render_player_ship(object* objp, const vec3d* cam_offset, const matrix cockpit_shadow_rendering_active = true; } - // This pass draws two different objects (hull, cockpit) in two different - // internal frames -- see the two model_render_immediate() calls below -- - // so each needs its own RT shadow-ray world-space correction; a single - // shadow_cascade_params_bind() call can't serve both. Derivations: - // - // Hull draw uses &eye_offset as its position arg (eye-relative frame), so - // reconstructed + (objp->pos - eye_offset) lands in the same true-world - // space the shadow TLAS is built in. - // - // Cockpit draw uses &cockpit_offset (rotate(sip->cockpit_offset) + sway, - // origin-relative frame -- see below), so reconstructed + objp->pos lands - // there instead. See shadow_cascade_static_data::shadow_ray_world_offset - // (uniform_structs.h) for the full derivation. + // 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); @@ -8288,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); @@ -8301,10 +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); - 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, MODEL_RENDER_OPAQUE); + renderHull(MODEL_RENDER_OPAQUE); gr_zbuffer_clear(true); } if (renderCockpitModel) { @@ -8314,11 +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; - 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, MODEL_RENDER_OPAQUE); + + renderCockpit(MODEL_RENDER_OPAQUE); } if (light_deferredcockpit_enabled()) { @@ -8346,18 +8347,11 @@ void ship_render_player_ship(object* objp, const vec3d* cam_offset, const matrix gr_zbuffer_set(ZBUFFER_TYPE_READ); if (deferredRenderShipModel) { - 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, MODEL_RENDER_TRANS); + renderHull(MODEL_RENDER_TRANS); } if (renderCockpitModel) { - 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, 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 );