From 7a388d2a6fe239712dcd37f5829f601f81745583 Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Fri, 11 Sep 2026 21:53:36 +0300 Subject: [PATCH] Fixed light bleeding throw scene switching doors --- src/features/editScene/AGENTS.md | 14 ++- src/features/editScene/GameFeatures202609.md | 20 ++- src/features/editScene/components/Door.hpp | 7 ++ .../demo_main.cpp | 56 ++++++++- .../editScene/systems/DoorBuilder.cpp | 115 +++++++++++++++++- .../editScene/systems/DoorBuilder.hpp | 14 ++- src/features/editScene/systems/DoorSystem.cpp | 7 +- 7 files changed, 214 insertions(+), 19 deletions(-) diff --git a/src/features/editScene/AGENTS.md b/src/features/editScene/AGENTS.md index a4a8e3d..b31d122 100644 --- a/src/features/editScene/AGENTS.md +++ b/src/features/editScene/AGENTS.md @@ -642,7 +642,15 @@ door builder also adds a black occluder covering the doorway, childed to the grid node (it does NOT swing with the hinge) and tracked via `DoorComponent::occluder`; `DoorSystem` hides it while the door is fully closed so the player never sees the missing room interior through the -opened doorway before the switch fires. Every scene-switch door gets the +opened doorway before the switch fires. A second flat black **gap shield** +quad (`CellGridDoorOccluderShield_*`, tracked via `DoorComponent::gapShield`) +sits 1 cm behind the closed leaf's back face on the same void side and is +visible exactly while the door is fully closed: spanning the leaf box plus +a 6 cm margin, it blacks out the leaf/frame clearance slits (0.02 m per +side, 0.05 m at the top) that would otherwise show the ungenerated void +(sky/terrain) around the closed door; its margin edges end up inside the +jamb boxes and wall thickness, so it never pokes out of the frame. Every +scene-switch door gets the same open-front tunnel mesh (`CellGridDoorOccluderTunnel_*`, shared per size and side) on the VOID side of the doorway: door-local +Z points outward from the owning cell, where normal grids hide the missing room, @@ -651,7 +659,9 @@ while exteriorOnly grids hide their missing interior inward, so builder default is +1; standalone doors keep it). The tunnel depth covers the fully-open leaf sweep, so the leaf stays visible whether it swings towards or away from the player, and the side/top/bottom walls flared -1 cm into the frame keep the darkness hole-free. The shared +6 cm into the frame — past the 0.02 m side / 0.05 m top leaf clearances, +so their edges sit inside the jamb boxes and no sight line slips between +the frame and the tunnel into the void — keep the darkness hole-free. The shared `CellGridDoorOccluderBlack` material keeps lighting ENABLED with all-black colours (diffuse/ambient/specular/emissive) — with lighting disabled RTSS ignores material colours and renders the pass white. diff --git a/src/features/editScene/GameFeatures202609.md b/src/features/editScene/GameFeatures202609.md index caf6870..f2d9336 100644 --- a/src/features/editScene/GameFeatures202609.md +++ b/src/features/editScene/GameFeatures202609.md @@ -779,8 +779,18 @@ side of the doorway — door-local +Z points outward from the owning cell `occluderSide = -1` for exteriorOnly grids, whose missing interior is inward. The back panel sits behind the fully-open leaf sweep so the swinging leaf stays visible for both swing directions, and -side/top/bottom walls flared 1 cm into the frame keep the darkness -hole-free; `DoorSystem` hides the occluder while fully closed. The +side/top/bottom walls flared 6 cm into the frame — past the 0.02 m side / +0.05 m top leaf clearances, so their edges sit inside the jamb boxes and +no sight line slips between the frame and the tunnel into the void — keep the darkness +hole-free; `DoorSystem` hides the occluder while fully closed. A second +flat black gap-shield quad (`CellGridDoorOccluderShield_*`, tracked via +`DoorComponent::gapShield`) sits 1 cm behind the closed leaf's back face +on the same void side and is visible exactly while the door is fully +closed: spanning the leaf box plus a 6 cm margin, it blacks out the +leaf/frame clearance slits (0.02 m per side, 0.05 m at the top) that +otherwise show the ungenerated void (sky/terrain) around the closed +door; its margin edges end up inside the jamb boxes and wall thickness, +so it never pokes out of the frame. The occluder material keeps lighting enabled with all-black colours — an unlit pass renders white under RTSS (FFPColour defaults the output to white when no lighting SRS runs). Scene-switch doors never @@ -822,7 +832,11 @@ sub-millisecond while swings are real-time). Its visibility follows the door state: hidden while fully closed, visible while open/swinging (visible flag toggled by `DoorSystem` from `currentAngle`), so the player never sees the missing room interior - behind the opened doorway before the switch fires. The material is lit + behind the opened doorway before the switch fires. A complementary flat + gap-shield quad just behind the closed leaf (leaf box + 6 cm margin, + covering the 0.02 m side / 0.05 m top clearances) is visible exactly + while the door is fully closed, blacking out the clearance slits. The + material is lit with all-black colours (unlit passes render white under RTSS). - The prompt stays "E Open" (never "E Close") for scene-switch doors, as today. diff --git a/src/features/editScene/components/Door.hpp b/src/features/editScene/components/Door.hpp index fb62849..0f521d1 100644 --- a/src/features/editScene/components/Door.hpp +++ b/src/features/editScene/components/Door.hpp @@ -78,6 +78,13 @@ struct DoorComponent { // so it does NOT swing with the hinge. Hidden while the door is // fully closed. Runtime only. Ogre::Entity *occluder = nullptr; + + // F1: flat black gap-shield quad just behind the closed leaf of a + // scene-switch door (same void side as the tunnel), visible only + // while the door is fully closed - it blacks out the leaf/frame + // clearance slits (0.02 m sides, 0.05 m top) the tunnel, hidden + // while closed, does not cover. Runtime only. + Ogre::Entity *gapShield = nullptr; }; #endif // EDITSCENE_DOOR_HPP diff --git a/src/features/editScene/demos/demo-interior-exterior-dynamics/demo_main.cpp b/src/features/editScene/demos/demo-interior-exterior-dynamics/demo_main.cpp index 014aafc..c692f94 100644 --- a/src/features/editScene/demos/demo-interior-exterior-dynamics/demo_main.cpp +++ b/src/features/editScene/demos/demo-interior-exterior-dynamics/demo_main.cpp @@ -484,7 +484,9 @@ struct SceneSwitchTestListener : public Ogre::FrameListener { } /* F1: every scene-switch door gets the void-side * tunnel occluder (depth covers the leaf sweep, - * so both swing directions stay visible). */ + * so both swing directions stay visible) plus the + * gap shield backing the leaf/frame clearance + * slits while the door is closed. */ { const DoorComponent &d = findDoorById(exitDoorId) @@ -499,6 +501,49 @@ struct SceneSwitchTestListener : public Ogre::FrameListener { "F1 exit door has no tunnel occluder"; break; } + if (!d.gapShield || + d.gapShield->getMesh() + ->getName() + .find("CellGridDoorOccluderShield") != + 0) { + failed = true; + failReason = + "F1 exit door has no gap shield"; + break; + } + /* The door is still closed here: the shield + * backs the slits, the tunnel stays hidden. */ + if (!d.gapShield->isVisible() || + d.occluder->isVisible()) { + failed = true; + failReason = + "F1 closed-door occluder visibility wrong"; + break; + } + /* The tunnel walls must reach past the + * leaf/frame clearances (0.02 m sides, + * 0.05 m top) or the void shows between the + * frame and the black corridor while the + * door is open; both meshes are hinge-local, + * so the tunnel footprint must cover at + * least the gap shield's (6 cm margin). */ + { + Ogre::Vector3 th = + d.occluder->getMesh() + ->getBounds() + .getHalfSize(); + Ogre::Vector3 sh = + d.gapShield->getMesh() + ->getBounds() + .getHalfSize(); + if (th.x < sh.x - 0.001f || + th.y < sh.y - 0.001f) { + failed = true; + failReason = + "F1 tunnel walls inside the leaf/frame clearances"; + break; + } + } } if (!requestDoorSwitch(exitDoorId)) break; @@ -730,7 +775,10 @@ struct SceneSwitchTestListener : public Ogre::FrameListener { * Z:0:0:0 is the way back. E on such a door swings the leaf open first; * the scene switch fires only when the leaf reaches the open angle, and * a black occluder box behind the doorway hides the missing half of the - * building while it swings. Both scenes carry their own player + * building while it swings; while the door is closed a flat black gap + * shield behind the leaf blacks out the leaf/frame clearance slits, so + * no sky/terrain bleeds through the closed door. Both scenes carry + * their own player * controller, so each switch is a clean takeover (see * EditorApp::performSceneSwitch); travel back and forth is endless. * @@ -752,7 +800,9 @@ struct SceneSwitchTestListener : public Ogre::FrameListener { * persisted and restored across the scene switches) and * both F1 scene-switch doors (interior -> exterior and * back each fire only when the leaf is fully open, - * black occluder present), verifying the round-trip + * black occluder tunnel + closed-door gap shield + * present with the right closed-state visibility), + * verifying the round-trip * arrival teleports (the exterior arrival check reads * the expected Y from TerrainSystem::getHeightAt and * polls while the streaming window loads and the diff --git a/src/features/editScene/systems/DoorBuilder.cpp b/src/features/editScene/systems/DoorBuilder.cpp index 4bc8231..0159ab5 100644 --- a/src/features/editScene/systems/DoorBuilder.cpp +++ b/src/features/editScene/systems/DoorBuilder.cpp @@ -64,9 +64,16 @@ occluderTunnelMesh(const Ogre::Vector3 ¢er, const float y0 = center.y - hy, y1 = center.y + hy; const float z0 = center.z + side * (hz + 0.02f); const float z1 = z0 + side * (x1 + hz * 2.0f + 0.05f); - /* Flare the walls 1 cm INTO the frame/jamb: no gap at the - * opening edges and no z-fighting with the jamb faces. */ - const float f = 0.01f; + /* Flare the walls INTO the frame/jamb past the leaf/frame + * clearances (0.02 m per side, 0.05 m at the top): with a smaller + * flare, sight lines through the open doorway slip over the top + * wall / past the side walls into the void between the jamb inner + * face and the wall, showing sky/terrain around the black + * corridor. 6 cm puts the wall edges inside the jamb boxes (0.1 m + * thick, inner faces at the clearances), so any ray past a wall + * hits the frame instead, with no z-fighting against the jamb + * faces. */ + const float f = 0.06f; char name[160]; snprintf(name, sizeof(name), @@ -123,6 +130,67 @@ occluderTunnelMesh(const Ogre::Vector3 ¢er, return name; } +/* F1: flat black gap shield for scene-switch doors, shown while the + * door is fully closed (the tunnel covers the swinging/open states): + * the leaf is sized with clearances against the frame opening (0.02 m + * per side, 0.05 m at the top), and without a backing those slits show + * the ungenerated void (sky/terrain) around the closed door. The + * shield is a single quad 1 cm behind the closed leaf's back face on + * the void side (`side` * Z), spanning the leaf box plus a margin that + * exceeds the clearances; the margin edges end up inside the jamb + * boxes and the wall thickness, so the quad never pokes out of the + * frame. Like the tunnel, the coordinates are baked into the mesh in + * hinge-local space and meshes are shared between doors of equal size + * and side. */ +static Ogre::String +occluderShieldMesh(const Ogre::Vector3 ¢er, + const Ogre::Vector3 &halfExtents, float side, + const Ogre::String &group) +{ + const float hx = halfExtents.x; + const float hy = halfExtents.y; + const float hz = halfExtents.z; + const float x0 = center.x - hx, x1 = center.x + hx; + const float y0 = center.y - hy, y1 = center.y + hy; + const float z = center.z + side * (hz + 0.01f); + /* Margin beyond the leaf box: must exceed the leaf/frame + * clearances (0.02 m sides, 0.05 m top). */ + const float m = 0.06f; + + char name[160]; + snprintf(name, sizeof(name), + "CellGridDoorOccluderShield_%d_%d_%d_%d_%d_%d_%d", + (int)lroundf(center.x * 1000.0f), + (int)lroundf(center.y * 1000.0f), + (int)lroundf(center.z * 1000.0f), + (int)lroundf(hx * 1000.0f), (int)lroundf(hy * 1000.0f), + (int)lroundf(hz * 1000.0f), (int)side); + if (!Ogre::MeshManager::getSingleton().getByName(name, group).isNull()) + return name; + + const float x0m = x0 - m, x1m = x1 + m; + const float y0m = y0 - m, y1m = y1 + m; + + Ogre::ManualObject mo(name); + mo.begin("CellGridDoorOccluderBlack", + Ogre::RenderOperation::OT_TRIANGLE_LIST); + Ogre::Vector3 normal(0, 0, -side); + Ogre::uint32 base = mo.getCurrentVertexCount(); + mo.position(x0m, y0m, z); + mo.normal(normal); + mo.position(x1m, y0m, z); + mo.normal(normal); + mo.position(x1m, y1m, z); + mo.normal(normal); + mo.position(x0m, y1m, z); + mo.normal(normal); + mo.triangle(base, base + 1, base + 2); + mo.triangle(base, base + 2, base + 3); + mo.end(); + mo.convertToMesh(name, group); + return name; +} + flecs::entity DoorBuilder::build(flecs::world &world, Ogre::SceneManager *sceneMgr, flecs::entity parentEntity, @@ -286,7 +354,10 @@ flecs::entity DoorBuilder::build(flecs::world &world, * the frame. The geometry is baked into the mesh in hinge-local * space and the node hangs off the PARENT node (not the hinge), * so it does not swing with the leaf; DoorSystem toggles its - * visibility from the swing angle. */ + * visibility from the swing angle. A second flat gap-shield quad + * sits just behind the closed leaf and is visible only while the + * door is fully closed, blacking out the leaf/frame clearance + * slits the tunnel (hidden while closed) does not cover. */ if (!door.sceneSwitchPath.empty()) { const Ogre::String group = Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME; @@ -328,6 +399,42 @@ flecs::entity DoorBuilder::build(flecs::world &world, doorEntity.get_mut().occluder = occEnt; } + + Ogre::String shieldMeshName = occluderShieldMesh( + colliderCenter, colliderHalfExtents, + params.occluderSide, group); + Ogre::Entity *shieldEnt = nullptr; + try { + shieldEnt = sceneMgr->createEntity(shieldMeshName); + } catch (const std::exception &e) { + Ogre::LogManager::getSingleton().logMessage( + "DoorBuilder: Error creating door gap shield: " + + std::string(e.what())); + } + if (shieldEnt) { + shieldEnt->setMaterialName("CellGridDoorOccluderBlack"); + shieldEnt->setVisible(!wasOpen); + + Ogre::SceneNode *shieldNode = + parentNode->createChildSceneNode(); + shieldNode->setPosition(occPos); + shieldNode->setOrientation(doorwayRot); + shieldNode->setScale(occScale); + shieldNode->attachObject(shieldEnt); + + flecs::entity shieldEntity = world.entity(); + shieldEntity.child_of(doorEntity); + shieldEntity.set( + {shieldNode, occPos, doorwayRot, occScale}); + RenderableComponent shieldRenderable; + shieldRenderable.entity = shieldEnt; + shieldRenderable.meshName = shieldMeshName; + shieldEntity.set( + shieldRenderable); + + doorEntity.get_mut().gapShield = + shieldEnt; + } } return doorEntity; diff --git a/src/features/editScene/systems/DoorBuilder.hpp b/src/features/editScene/systems/DoorBuilder.hpp index e0da887..0da32fa 100644 --- a/src/features/editScene/systems/DoorBuilder.hpp +++ b/src/features/editScene/systems/DoorBuilder.hpp @@ -18,11 +18,15 @@ * leaf, DoorComponent, RigidBodyComponent static, ActuatorComponent) * ├── collider child (PhysicsColliderComponent box of the closed * │ leaf; disabled by DoorSystem while not fully closed) - * └── occluder child (F1, scene-switch doors only: open-front - * black tunnel on the void side of the doorway - door-local - * +Z normally, -Z for exteriorOnly grids -, node on the - * PARENT node so it does not swing; DoorSystem toggles its - * visibility) + * ├── occluder child (F1, scene-switch doors only: open-front + * │ black tunnel on the void side of the doorway - door-local + * │ +Z normally, -Z for exteriorOnly grids -, node on the + * │ PARENT node so it does not swing; DoorSystem toggles its + * │ visibility, hidden while the door is fully closed) + * └── gap-shield child (F1, scene-switch doors only: flat black + * quad just behind the closed leaf, visible only while the + * door is fully closed; blacks out the leaf/frame clearance + * slits the hidden tunnel does not cover) * * The builder also declares the F6 store defaults for persistent doors * (DoorSystem::declareDoorDefaults) and snaps a door that was left open diff --git a/src/features/editScene/systems/DoorSystem.cpp b/src/features/editScene/systems/DoorSystem.cpp index c65b348..7c3ddad 100644 --- a/src/features/editScene/systems/DoorSystem.cpp +++ b/src/features/editScene/systems/DoorSystem.cpp @@ -175,10 +175,13 @@ void DoorSystem::update(float deltaTime) } } - // F1: the black occluder is visible unless the door is - // fully closed + // F1: the black occluder tunnel is visible unless the door + // is fully closed; the gap shield backs the leaf/frame + // clearance slits exactly while it is if (door.occluder) door.occluder->setVisible(door.currentAngle != 0.0f); + if (door.gapShield) + door.gapShield->setVisible(door.currentAngle == 0.0f); }); // Refresh the per-door "door_unlock_" subscriptions