From 5b3f0dd31b5cd8a71b0ece6dab5b71742e87a3c6 Mon Sep 17 00:00:00 2001 From: kirides <13602143+kirides@users.noreply.github.com> Date: Mon, 17 Aug 2026 16:23:54 +0200 Subject: [PATCH] player focus highlight --- D3D11Engine/D3D12Engine/D3D12GraphicsEngine.h | 3 +- .../D3D12Engine/D3D12PipelineState.cpp | 5 +- D3D11Engine/D3D12Engine/D3D12Scene.cpp | 50 +++++++++++++++++-- D3D11Engine/D3D12Engine/D3D12ShadowMap.cpp | 2 + D3D11Engine/Shaders/D3D12/Skeletal.hlsl | 1 + D3D11Engine/Shaders/D3D12/Vob.hlsl | 8 ++- 6 files changed, 62 insertions(+), 7 deletions(-) diff --git a/D3D11Engine/D3D12Engine/D3D12GraphicsEngine.h b/D3D11Engine/D3D12Engine/D3D12GraphicsEngine.h index 9b4f7d8e..6fa78a9d 100644 --- a/D3D11Engine/D3D12Engine/D3D12GraphicsEngine.h +++ b/D3D11Engine/D3D12Engine/D3D12GraphicsEngine.h @@ -332,9 +332,10 @@ class D3D12GraphicsEngine : public BaseGraphicsEngine { // them, so DrawGhostVobs draws them later. ONLY the main-view animated-skeletal call passes true — it is the // D3D12 stand-in for the reroute D3D11 does in GothicAPI::DrawWorldMeshNaive (:1394), which this backend // never calls. Shadow callers must leave it false: ghosts cast no shadows on either backend. + // playerFocusVob: the interact/melee-focus highlight vob (see ComputeSkeletalFocusVob in D3D12Scene.cpp). void PrepareFrameSkeletals( std::vector& vobs, const Frustum* cullFrustum = nullptr, int shadowCascade = -1, const DirectX::XMFLOAT3* sphereCenter = nullptr, float sphereRange = 0.0f, UINT cascadeCount = 1, - bool collectGhosts = false ); + bool collectGhosts = false, const zCVob* playerFocusVob = nullptr ); void DrawSkeletalDepthPrepass(); // lay down skeletal base + node-attachment depth into the Forward+ prepass void DrawSkeletalColor(); // draw the collected skeletal base meshes + node attachments (post-cull, lit) // Turn this frame's g_FrameSkelDraws/g_FrameAttachDraws into the two main-view ExecuteIndirect argument diff --git a/D3D11Engine/D3D12Engine/D3D12PipelineState.cpp b/D3D11Engine/D3D12Engine/D3D12PipelineState.cpp index 3ba38597..7017d21a 100644 --- a/D3D11Engine/D3D12Engine/D3D12PipelineState.cpp +++ b/D3D11Engine/D3D12Engine/D3D12PipelineState.cpp @@ -1046,6 +1046,8 @@ bool D3D12PipelineState::CreateDepthPrepass() { { "INSTANCE_WORLD_MATRIX", 3, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 48, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 }, { "INSTANCE_COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 1, 128, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 }, { "INSTANCE_WINDFLUENCE", 0, DXGI_FORMAT_R32G32_FLOAT, 1, 132, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 }, + // VSDepthAttach takes the full VS_IN (Vob.hlsl), which declares INSTANCE_GP_SLOT — required here too. + { "INSTANCE_GP_SLOT", 0, DXGI_FORMAT_R32_UINT, 1, 140, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 }, }; pso.VS = { World.DepthPrepassVobAttachVsBlob->GetBufferPointer(), World.DepthPrepassVobAttachVsBlob->GetBufferSize() }; pso.PS = { World.DepthPrepassVobIndirectPsBlob->GetBufferPointer(), World.DepthPrepassVobIndirectPsBlob->GetBufferSize() }; @@ -1196,7 +1198,7 @@ bool D3D12PipelineState::CreateVob() { // Slot 0 = ExVertexStruct (Position@0, Normal@12, TexCoord0@24); slot 1 = per-instance data // read from VobInstanceInfo (stride 144): world matrix rows @0/16/32/48, instance color @128, - // {windStrenth, canBeAffectedByPlayer} @132 (see Vob.hlsl's wind sway — VSMain only, not the depth prepass). + // {windStrenth, canBeAffectedByPlayer} @132, GP_Slot @140 (bit 31 = focus highlight). const D3D12_INPUT_ELEMENT_DESC layout[] = { { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, { "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, @@ -1207,6 +1209,7 @@ bool D3D12PipelineState::CreateVob() { { "INSTANCE_WORLD_MATRIX", 3, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 48, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 }, { "INSTANCE_COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 1, 128, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 }, { "INSTANCE_WINDFLUENCE", 0, DXGI_FORMAT_R32G32_FLOAT, 1, 132, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 }, + { "INSTANCE_GP_SLOT", 0, DXGI_FORMAT_R32_UINT, 1, 140, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 }, }; // Reuse the world root signature (b0 ViewProj + t0 SRV + static sampler s0 — identical needs). diff --git a/D3D11Engine/D3D12Engine/D3D12Scene.cpp b/D3D11Engine/D3D12Engine/D3D12Scene.cpp index f5a3209a..e70155f8 100644 --- a/D3D11Engine/D3D12Engine/D3D12Scene.cpp +++ b/D3D11Engine/D3D12Engine/D3D12Scene.cpp @@ -23,6 +23,9 @@ #include "../zFont.h" #include "../zCCamera.h" #include "../oCGame.h" +#include "../oCNPC.h" +#include "../oCMobInter.h" +#include "../zCParser.h" #include "../oCVisFX.h" #include "../DXGIHelpers.h" #include "../WindAnimation.h" @@ -270,6 +273,40 @@ namespace { } + // Mirrors D3D11GraphicsEngine's DrawSkeletalMeshVobs playerFocusVob filtering. + const zCVob* ComputeSkeletalFocusVob() { + const bool interactiveFocusEnabled = oCGame::GetHighlightInteractFocus(); + const bool meleeFocusEnabled = oCGame::GetHighlightMeleeFocus() >= 2 && oCGame::GetNpcFocusIsHighlightActive(); + zCVob* playerFocusVob = oCGame::GetPlayer() ? oCGame::GetPlayer()->GetFocusVob() : nullptr; + if ( !playerFocusVob ) return nullptr; + + if ( auto npc = playerFocusVob->As() ) { +#ifdef BUILD_GOTHIC_1_CLASSIC + static int idxZsTalk = -2; + if ( idxZsTalk == -2 ) { idxZsTalk = zCParser::GetParser()->GetIndex( "ZS_TALK" ); } + if ( npc->GetStates()->IsInState( idxZsTalk ) ) { + return nullptr; + } +#endif + if ( !meleeFocusEnabled ) { + return nullptr; + } + } else if ( !interactiveFocusEnabled ) { + return nullptr; + } else { + if ( oCMobInter* mobInter = playerFocusVob->As() ) { + if ( mobInter->IsInteractingWith( oCGame::GetPlayer() ) || !mobInter->HasName() ) { + return nullptr; + } + } else if ( oCMob* mob = playerFocusVob->As() ) { + if ( !mob->HasName() ) { + return nullptr; + } + } + } + return playerFocusVob; + } + gtl::flat_hash_map g_vobInfoVisualToBucket; std::vector g_vobInfoVisualIndexToVisualInfo; RenderView g_GeometryPassVobs; @@ -2176,8 +2213,9 @@ XRESULT D3D12GraphicsEngine::OnStartWorldRendering() { // collectGhosts=true ONLY here: this is the list D3D11's GothicAPI::DrawWorldMeshNaive walks, and the // reroute of ghost NPCs into TransparencyVobs is that function's job. Static MOBs (g_FrameMobs) keep the // plain drop — D3D11 does not reroute them either. - PrepareFrameSkeletals( Engine::GAPI->GetAnimatedSkeletalMeshVobs(), nullptr, -1, nullptr, 0.0f, 1, true ); - PrepareFrameSkeletals( g_FrameMobs ); + const zCVob* skeletalFocusVob = ComputeSkeletalFocusVob(); + PrepareFrameSkeletals( Engine::GAPI->GetAnimatedSkeletalMeshVobs(), nullptr, -1, nullptr, 0.0f, 1, true, skeletalFocusVob ); + PrepareFrameSkeletals( g_FrameMobs, nullptr, -1, nullptr, 0.0f, 1, false, skeletalFocusVob ); // Refresh the wind CB's player position ONCE here (before shadows/prepass/color all run this frame) — windDir/ // globalTime were already advanced once in OnBeginFrame; minHeight/maxHeight are refreshed per-visual right @@ -4340,7 +4378,7 @@ XRESULT D3D12GraphicsEngine::DrawVobsInstanced() { void D3D12GraphicsEngine::PrepareFrameSkeletals( std::vector& vobs, const Frustum* cullFrustum, int shadowCascade, - const DirectX::XMFLOAT3* sphereCenter, float sphereRange, UINT cascadeCount, bool collectGhosts ) { + const DirectX::XMFLOAT3* sphereCenter, float sphereRange, UINT cascadeCount, bool collectGhosts, const zCVob* playerFocusVob ) { // Run each candidate skeletal vob's once-per-frame animation update, upload its instance + bone CBs and // its attachments' VOB-instance data ONCE (cached in g_SkelUploadCache — the pose is view-independent), // and record the resulting GPU addresses into the caller's list: the main view's g_FrameSkelDraws/ @@ -4565,7 +4603,9 @@ void D3D12GraphicsEngine::PrepareFrameSkeletals( std::vector& SkeletalInstanceCB inst = {}; XMStoreFloat4x4( &inst.World, xmWorld ); - inst.ModelColor = XMFLOAT4( groundLight.x, groundLight.y, groundLight.z, groundLight.w ); + // ModelColor.w doubles as the focus-highlight sentinel — Skeletal.hlsl's PSMain reads it as i.col.a. + const float focusSentinel = ( playerFocusVob && playerFocusVob == vi->Vob ) ? 2.0f : groundLight.w; + inst.ModelColor = XMFLOAT4( groundLight.x, groundLight.y, groundLight.z, focusSentinel ); inst.Fatness = model->GetModelFatness(); // StoreVobPreviousTransforms (end of the previous frame) is what fills PrevWorldMatrix; before // its first run HasValidPrevTransforms is false and the current world doubles as the previous. @@ -4706,6 +4746,8 @@ void D3D12GraphicsEngine::PrepareFrameSkeletals( std::vector& vii.world = attWorld; vii.prevWorld = attPrevWorld; // motion vectors — see attPrevWorld above vii.color = groundLight.ToDWORD(); + // Focus-highlight bit for node-attached MOBs (tree-saw trunks, beds) — mirrors Vob.hlsl's VSMainAttach. + vii.GP_Slot |= ( playerFocusVob && playerFocusVob == vi->Vob ) ? ( 1u << 31 ) : 0u; vii.windStrenth = attFatness; // reinterpreted as Fatness — see VSMainAttach vii.canBeAffectedByPlayer = attScaling; // reinterpreted as Scaling — see VSMainAttach const UINT instOffset = m_VobInstanceBufferOffset; diff --git a/D3D11Engine/D3D12Engine/D3D12ShadowMap.cpp b/D3D11Engine/D3D12Engine/D3D12ShadowMap.cpp index 5bf30d97..25b7df69 100644 --- a/D3D11Engine/D3D12Engine/D3D12ShadowMap.cpp +++ b/D3D11Engine/D3D12Engine/D3D12ShadowMap.cpp @@ -312,6 +312,8 @@ bool D3D12ShadowMap::Init() { { "INSTANCE_WORLD_MATRIX", 3, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 48, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 }, { "INSTANCE_COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 1, 128, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 }, { "INSTANCE_WINDFLUENCE", 0, DXGI_FORMAT_R32G32_FLOAT, 1, 132, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 }, + // VSDepthAttach takes the full VS_IN (Vob.hlsl), which declares INSTANCE_GP_SLOT — required here too. + { "INSTANCE_GP_SLOT", 0, DXGI_FORMAT_R32_UINT, 1, 140, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 }, }; pso.pRootSignature = m_E->m_Pipelines.World.RootSig.Get(); pso.VS = { m_E->m_Pipelines.World.DepthPrepassVobAttachVsBlob->GetBufferPointer(), m_E->m_Pipelines.World.DepthPrepassVobAttachVsBlob->GetBufferSize() }; diff --git a/D3D11Engine/Shaders/D3D12/Skeletal.hlsl b/D3D11Engine/Shaders/D3D12/Skeletal.hlsl index 6c8bd3e6..b87364fc 100644 --- a/D3D11Engine/Shaders/D3D12/Skeletal.hlsl +++ b/D3D11Engine/Shaders/D3D12/Skeletal.hlsl @@ -162,6 +162,7 @@ float4 PSMain( VS_OUT i ) : SV_TARGET rgb *= lerp( 1.0, 0.8, wetness ); rgb += AccumTiledPointLights( i.clip.xyz, i.wpos, N, albedo, orm.g, orm.b ); // dynamic point lights on top (PBR) rgb += wetSheen * ( 1.0 + shadow ) * SrgbToLinear( SunColor ) * SunIntensity; + rgb *= 1.0f + step( 1.5f, i.col.a ); // focus highlight float f = saturate( ( i.fogDist - FogNear ) / max( 1.0, FogFar - FogNear ) ); return float4( lerp( rgb, SrgbToLinear( FogColor ), f ), 1.0 ); } diff --git a/D3D11Engine/Shaders/D3D12/Vob.hlsl b/D3D11Engine/Shaders/D3D12/Vob.hlsl index 345864e7..0f73c075 100644 --- a/D3D11Engine/Shaders/D3D12/Vob.hlsl +++ b/D3D11Engine/Shaders/D3D12/Vob.hlsl @@ -112,8 +112,10 @@ struct VS_IN // VobInstanceInfo::{windStrenth, canBeAffectedByPlayer} (@132/@136) — 0 for non-wind-flagged vobs, so the // branches below are no-ops for ordinary instances (matches D3D11's per-instance InstanceWind.x/y > 0 gate). float2 iwind : INSTANCE_WINDFLUENCE; + // VobInstanceInfo::GP_Slot (@140) — bit 31 is the focus-highlight flag. + uint igpslot : INSTANCE_GP_SLOT; }; -struct VS_OUT { float4 clip : SV_POSITION; float2 uv : TEXCOORD0; float4 col : TEXCOORD1; float fogDist : TEXCOORD2; float3 wpos : TEXCOORD3; float3 wnrm : TEXCOORD4; }; +struct VS_OUT { float4 clip : SV_POSITION; float2 uv : TEXCOORD0; float4 col : TEXCOORD1; float fogDist : TEXCOORD2; float3 wpos : TEXCOORD3; float3 wnrm : TEXCOORD4; float focus : TEXCOORD5; }; VS_OUT VSMain( VS_IN i ) { @@ -127,6 +129,7 @@ VS_OUT VSMain( VS_IN i ) o.wpos = worldPos; o.wnrm = mul( i.nrm, (float3x3)i.iworld ); o.fogDist = length( worldPos - CamPosWS ); + o.focus = ( i.igpslot >> 31u ) ? 2.0f : 0.0f; return o; } @@ -154,6 +157,7 @@ float4 PSMain( VS_OUT i ) : SV_TARGET rgb *= lerp( 1.0, 0.8, wetness ); rgb += AccumTiledPointLights( i.clip.xyz, i.wpos, N, albedo, orm.g, orm.b ); rgb += wetSheen * ( 1.0 + shadow ) * SrgbToLinear( SunColor ) * SunIntensity; + rgb *= 1.0f + step( 1.5f, i.focus ); // focus highlight float f = saturate( ( i.fogDist - FogNear ) / max( 1.0, FogFar - FogNear ) ); return float4( lerp( rgb, SrgbToLinear( FogColor ), f ), 1.0 ); } @@ -208,6 +212,7 @@ VS_OUT VSMainAttach( VS_IN i ) o.wpos = worldPos; o.wnrm = mul( i.nrm, (float3x3)i.iworld ); o.fogDist = length( worldPos - CamPosWS ); + o.focus = ( i.igpslot >> 31u ) ? 2.0f : 0.0f; return o; } @@ -251,6 +256,7 @@ float4 PSMainBindless( VS_OUT i ) : SV_TARGET rgb *= lerp( 1.0, 0.8, wetness ); rgb += AccumTiledPointLights( i.clip.xyz, i.wpos, N, albedo, orm.g, orm.b ); rgb += wetSheen * ( 1.0 + shadow ) * SrgbToLinear( SunColor ) * SunIntensity; + rgb *= 1.0f + step( 1.5f, i.focus ); // focus highlight float f = saturate( ( i.fogDist - FogNear ) / max( 1.0, FogFar - FogNear ) ); return float4( lerp( rgb, SrgbToLinear( FogColor ), f ), 1.0 ); }