Cutaway v3: regione preservata per raggiungibilita + rimozione per occlusione screen-space #279

Open
opened 2026-08-08 08:26:09 +02:00 by panda · 0 comments
Collaborator

Il cutaway rimuove il terreno con una regola "tutto sopra una quota strutturale". Funziona come interruttore, ma cancella anche il terreno percorribile: una strada che sale da un cratere sparisce nell'istante in cui inizia a salire, e il contatto a terra del personaggio si perde.

Questa issue sostituisce la forma della rimozione. L'attivazione (UNDER / SHUT IN, TerrainCutaway) non cambia.

Il modello — tre insiemi distinti

  1. Attivazione (se tagliare) — strutturale, invariata: UNDER (soffitto sopra il supporto) o SHUT IN (≤1 via d'uscita su 8). Non guarda lo schermo.
  2. Regione preservata (cosa deve restare visibile) — flood fill di raggiungibilità dalla cella del giocatore, con il predicato di movimento reale (TileMapRules walkable + maxSlopeZ, |Δz| ≤ WorldRules.MaxWalkStepZ), esteso a tutta la componente raggiungibile dentro la finestra di rendering.
  3. Insieme rimosso (cosa sparisce) — solo i frammenti che occludono la regione preservata in screen-space. Nessuna soglia di quota, nessun semipiano, nessun raggio.

Proprietà emergente: si rimuove solo terreno non raggiungibile. Una collina camminabile è preservata, quindi mai rimossa — il danno si limita da sé.

Regola in una frase: vedi ovunque tu possa camminare, e sparisce solo ciò che te lo copre.

Limite del flood fill — nessuna euristica in v1

Niente budget geodetico né raggio. Motivo: un raggio geodetico di N celle percorre N celle in un corridoio ma riempie un'area di raggio N su un pianoro (migliaia di celle), quindi non produce il comportamento "corridoio lungo, pianoro corto" che sembrerebbe promettere. Un budget sul numero di celle visitate non garantisce comunque la strada fino al bordo: il BFS può esaurirlo riempiendo il fondo largo del cratere prima di risalire la rampa.

  • limite = la finestra di rendering, e nient'altro;
  • hard cap solo come protezione tecnica, pari al numero massimo di celle contenibili nella finestra;
  • troncamento tecnico → degrada in modo conservativo (si rimuove meno) e imposta Truncated; mai spegnere il cutaway, che farebbe sfarfallare l'attivazione camminando su una rampa.

Il rischio "rampa → pianoro" si verifica dal vivo. Se produce uno squarcio enorme, allora è l'invariante "preserva tutto ciò che è raggiungibile" a essere sbagliato in quella topologia: un budget arbitrario lo nasconderebbe soltanto, e renderebbe il risultato dipendente dalla posizione.

Implementazione — pre-pass GPU su RT colore

Pass P (pre-pass). La stessa mesh di finestra su un RenderTarget2D a virgola mobile, clip() sui frammenti non preservati, scrive (validità, cameraDepth, 0, 0). Canale colore, non depth texture: non dipendiamo da niente di campionabile che SM3/DesktopGL non garantisca. La cameraDepth contiene interpolazioni e trasformazioni frazionarie — non è "esatta", ma è ampiamente precisa nel range ±32768 (IsoCamera.DepthWorldExtent); l'epsilon va determinato con un test, non dedotto dall'interezza del range.

Pass G (ground). Ogni frammento campiona il RT al proprio pixel:

if (!currentFragmentIsPreserved
    && targetIsValid
    && currentCameraDepth < preservedCameraDepth - epsilon)
{
    clip();
}

I frammenti preservati sono disegnati incondizionatamente: altrimenti la correttezza dipenderebbe dall'uguaglianza numerica perfetta fra i due pass, cioè proprio ciò che l'epsilon esiste per evitare.

Fallback. Se il render target non è creabile: flag di sessione, ricaduta sulla regola a quota attuale (RevealZ strutturale) — non "nessun taglio", così su una macchina che non regge il RT resti comunque visibile dentro una caverna — e flag diagnostico visibile nell'overlay admin. Stessa strategia di _visibilityUnsupported nel visibility buffer del deferred shading.

Il pass P gira solo quando cutMode è attivo.

Invariants Check

  • Scope ✓ — costruisce i tre insiemi e nient'altro; KO 3 (personaggio che vola) e la silhouette restano issue separate, dichiarate fuori scopo.
  • Server-authoritative N/A — nessun intent nuovo sul filo. Il renderer non è un confine di sicurezza: tutta la geometria è già disponibile al client, e il server continua a validare movimento, collisioni e interazioni indipendentemente da ciò che il client sceglie di disegnare.
  • GM authorization ✓ — le uniche superfici nuove sono le righe diagnostiche nel blocco HUD già gated da adminPanel.IsAdmin.
  • Identity model N/A — non tocca account, character, sub o char.
  • Protocol versioned N/A — nessun ClientMessage/ServerMessage, enum o DTO wire cambia; ProtocolVersion.Current resta 21.
  • String catalog ✓ — nessun testo giocatore; l'overlay è diagnostica admin come la riga terrain-z.
  • Single-threaded sim N/A — interamente client-side; World non viene toccato, nessun InvokeAsync.
  • World.cs HARD GATE N/A — nessun metodo di World aggiunto o modificato.
  • Screen HARD GATE ✓ — ReachableRegion in Client.Core, i due pass in GroundBlendRenderer, l'overlay in WorldRenderer. GameScreen non guadagna né un case né una passata di disegno.
  • Client engine-independence ✓ — il flood fill è una funzione pura su IReadOnlyTileMap in Client.Core, senza MonoGame: è ciò che rende scrivibile il test rampa/pianoro. Nel progetto Client restano solo RT, pass e uniform.
  • Gameplay/Networking separation N/A — nessuna regola di gioco e nessun trasporto toccati.
  • Act on the instance ✓ — la maschera è un buffer transitorio ricostruito per passo, non una collezione laterale che rispecchia stato per-entità.
  • Extend by type, not switch ✓ — nessuno switch su tipo di terreno: un nuovo terreno scalabile entra nella regione con una riga di tiledata, zero codice.
  • Typed content-def fields N/A — nessun campo content-def aggiunto.
  • Server-paced actions N/A — nessuna azione a tempo.
  • Persistence (GameServer) N/A — niente viene serializzato; regione e maschera sono derivate.
  • Persistence (Auth) N/A — nessuno schema, nessuna migration.
  • Process separation N/A — cambia solo il client; il contratto JWT è intatto.
  • Typed options ✓ — hard cap ed epsilon sono costanti di render dichiarate in un solo posto; nessun IConfiguration letto in un servizio.
  • Broadcasts / AoI N/A — nessun broadcast, nessuna entità o evento osservabile.
  • Multi-platform ✓ — RenderTarget2D + SM3 sono il percorso già in produzione per il visibility buffer (Win/macOS), con fallback obbligatorio se il formato non è creabile.
  • Assets required ✓ — nessuna arte nuova; dove si rimuove non si inventa un placeholder: si vede il vuoto, ed è un limite dichiarato (mesh a foglio unico).
  • Asset naming HARD GATE N/A — nessun id asset nuovo.
  • Walkability by slope ✓ — la regione è definita da TileMapRules (walkable + maxSlopeZ + MaxWalkStepZ), non da una copia: è il motivo per cui la parete del cratere non entra e la strada sì.
  • Occupancy contains render ✓ — l'attivazione resta letta dall'occupancy canonica; mesh e DisplayZ restano presentazione. La regione si calcola sulla Z autoritativa (raggiungibilità = simulazione) e si rasterizza a DisplayZ (render): i due non vanno mescolati, ed è la crepa da cui esce il KO 3.
  • Cutaway is structural ✓ — l'attivazione non cambia; cambia solo cosa si rimuove. Il bullet della checklist va aggiornato nello stesso commit, perché oggi dice "tutto sopra una quota strutturale".
  • ModernUO as reference ✓ — divergenza consapevole: il client UO nasconde statics per-tile sopra il tuo livello, perché la sua mappa è fatta di tile discreti con layer; su un heightfield continuo "il livello sopra" non esiste come insieme di oggetti, quindi prendiamo l'idea (nascondi ciò che copre il posto in cui sei) con l'unico strumento che una superficie continua consente: l'occlusione in screen-space.
  • Docs & DoD ✓ — stesso commit: bullet + invariante in CLAUDE.md, sezione Level-focus visibility nel design doc del modello a volumi, e la DoD qui sotto.

Piano di verifica

Unit puri in Client.Core (nessun MonoGame, mappe costruite a mano):

  • rampa → pianoro aperto (OBBLIGATORIO) — cratere con rampa percorribile che sbocca su un pianoro esteso: documenta quanto la regione si estende e che il cap tecnico non scatta prima della finestra;
  • strada che sale dal cratere: preservata per tutta la sua lunghezza dentro la finestra;
  • parete del cratere: non raggiungibile → mai preservata;
  • collina dolce camminabile: raggiungibile → preservata → mai rimossa;
  • cella di partenza sempre nella regione, anche se isolata.

Nessun test server — niente cambia lato server.

Screenshot (DB fresco + harness): fondo cratere con la strada visibile fino a bordo schermo · la stessa scena camminando 8-10 passi, per giudicare se il bordo "respira" · foresta, dove cutMode è False e nulla deve cambiare · fallback forzato.

Gap dichiarato: i casi UNDER (caverna) non sono dimostrabili dal vivo finché non esiste una cavità autorata; restano coperti dai test.

Definition of Done

  • Su una strada che sale dal fondo del cratere, la strada è visibile fino al bordo schermo; dimostrato con screenshot.
  • Camminando 8-10 passi sul fondo, il bordo del taglio non "respira": giudicato su screenshot consecutivi.
  • In foresta (cutMode False) nessun frammento viene rimosso: removedSheets 0 e nessuna area vuota.
  • Terreno raggiungibile e camminabile non viene mai rimosso — coperto da unit test.
  • Il test "rampa → pianoro aperto" esiste e passa, e il suo esito è riportato nel PR.
  • Con il render target non disponibile il gioco resta giocabile sulla regola a quota, e l'overlay admin lo dichiara.
  • L'overlay admin espone: celle preservate, Truncated, stato del fallback.
  • CLAUDE.md (bullet + invariante) e il design doc del modello a volumi aggiornati nello stesso commit.
Il cutaway rimuove il terreno con una regola "tutto sopra una quota strutturale". Funziona come interruttore, ma cancella anche il terreno percorribile: una strada che sale da un cratere sparisce nell'istante in cui inizia a salire, e il contatto a terra del personaggio si perde. Questa issue sostituisce la **forma** della rimozione. L'**attivazione** (UNDER / SHUT IN, `TerrainCutaway`) non cambia. ## Il modello — tre insiemi distinti 1. **Attivazione** (*se* tagliare) — strutturale, invariata: UNDER (soffitto sopra il supporto) o SHUT IN (≤1 via d'uscita su 8). Non guarda lo schermo. 2. **Regione preservata** (*cosa deve restare visibile*) — flood fill di **raggiungibilità** dalla cella del giocatore, con il predicato di movimento reale (`TileMapRules` walkable + `maxSlopeZ`, `|Δz| ≤ WorldRules.MaxWalkStepZ`), esteso a **tutta la componente raggiungibile dentro la finestra di rendering**. 3. **Insieme rimosso** (*cosa sparisce*) — **solo** i frammenti che occludono la regione preservata in screen-space. Nessuna soglia di quota, nessun semipiano, nessun raggio. Proprietà emergente: si rimuove solo terreno **non raggiungibile**. Una collina camminabile è preservata, quindi mai rimossa — il danno si limita da sé. Regola in una frase: **vedi ovunque tu possa camminare, e sparisce solo ciò che te lo copre.** ## Limite del flood fill — nessuna euristica in v1 Niente budget geodetico né raggio. Motivo: un raggio geodetico di N celle percorre N celle in un corridoio ma riempie un'**area** di raggio N su un pianoro (migliaia di celle), quindi non produce il comportamento "corridoio lungo, pianoro corto" che sembrerebbe promettere. Un budget sul numero di celle visitate non garantisce comunque la strada fino al bordo: il BFS può esaurirlo riempiendo il fondo largo del cratere prima di risalire la rampa. - limite = **la finestra di rendering**, e nient'altro; - hard cap **solo come protezione tecnica**, pari al numero massimo di celle contenibili nella finestra; - troncamento tecnico → degrada in modo conservativo (si rimuove *meno*) e imposta `Truncated`; **mai** spegnere il cutaway, che farebbe sfarfallare l'attivazione camminando su una rampa. Il rischio "rampa → pianoro" si verifica **dal vivo**. Se produce uno squarcio enorme, allora è l'invariante "preserva tutto ciò che è raggiungibile" a essere sbagliato in quella topologia: un budget arbitrario lo nasconderebbe soltanto, e renderebbe il risultato dipendente dalla posizione. ## Implementazione — pre-pass GPU su RT colore **Pass P (pre-pass).** La stessa mesh di finestra su un `RenderTarget2D` a virgola mobile, `clip()` sui frammenti non preservati, scrive `(validità, cameraDepth, 0, 0)`. **Canale colore, non depth texture**: non dipendiamo da niente di campionabile che SM3/DesktopGL non garantisca. La `cameraDepth` contiene interpolazioni e trasformazioni frazionarie — non è "esatta", ma è **ampiamente precisa nel range ±32768** (`IsoCamera.DepthWorldExtent`); l'`epsilon` va determinato con un test, non dedotto dall'interezza del range. **Pass G (ground).** Ogni frammento campiona il RT al proprio pixel: ``` if (!currentFragmentIsPreserved && targetIsValid && currentCameraDepth < preservedCameraDepth - epsilon) { clip(); } ``` I frammenti **preservati sono disegnati incondizionatamente**: altrimenti la correttezza dipenderebbe dall'uguaglianza numerica perfetta fra i due pass, cioè proprio ciò che l'epsilon esiste per evitare. **Fallback.** Se il render target non è creabile: flag di sessione, ricaduta sulla regola a quota attuale (`RevealZ` strutturale) — non "nessun taglio", così su una macchina che non regge il RT resti comunque visibile dentro una caverna — e **flag diagnostico visibile nell'overlay admin**. Stessa strategia di `_visibilityUnsupported` nel visibility buffer del deferred shading. Il pass P gira **solo** quando `cutMode` è attivo. ## Invariants Check - **Scope** ✓ — costruisce i tre insiemi e nient'altro; KO 3 (personaggio che vola) e la silhouette restano issue separate, dichiarate fuori scopo. - **Server-authoritative** N/A — nessun intent nuovo sul filo. Il renderer **non è un confine di sicurezza**: tutta la geometria è già disponibile al client, e il server continua a validare movimento, collisioni e interazioni indipendentemente da ciò che il client sceglie di disegnare. - **GM authorization** ✓ — le uniche superfici nuove sono le righe diagnostiche nel blocco HUD già gated da `adminPanel.IsAdmin`. - **Identity model** N/A — non tocca account, character, `sub` o `char`. - **Protocol versioned** N/A — nessun `ClientMessage`/`ServerMessage`, enum o DTO wire cambia; `ProtocolVersion.Current` resta 21. - **String catalog** ✓ — nessun testo giocatore; l'overlay è diagnostica admin come la riga `terrain-z`. - **Single-threaded sim** N/A — interamente client-side; `World` non viene toccato, nessun `InvokeAsync`. - **`World.cs` HARD GATE** N/A — nessun metodo di `World` aggiunto o modificato. - **`Screen` HARD GATE** ✓ — `ReachableRegion` in `Client.Core`, i due pass in `GroundBlendRenderer`, l'overlay in `WorldRenderer`. `GameScreen` non guadagna né un `case` né una passata di disegno. - **Client engine-independence** ✓ — il flood fill è una funzione pura su `IReadOnlyTileMap` in `Client.Core`, senza MonoGame: è ciò che rende scrivibile il test rampa/pianoro. Nel progetto `Client` restano solo RT, pass e uniform. - **Gameplay/Networking separation** N/A — nessuna regola di gioco e nessun trasporto toccati. - **Act on the instance** ✓ — la maschera è un buffer transitorio ricostruito per passo, non una collezione laterale che rispecchia stato per-entità. - **Extend by type, not switch** ✓ — nessuno `switch` su tipo di terreno: un nuovo terreno scalabile entra nella regione con una riga di tiledata, zero codice. - **Typed content-def fields** N/A — nessun campo content-def aggiunto. - **Server-paced actions** N/A — nessuna azione a tempo. - **Persistence (GameServer)** N/A — niente viene serializzato; regione e maschera sono derivate. - **Persistence (Auth)** N/A — nessuno schema, nessuna migration. - **Process separation** N/A — cambia solo il client; il contratto JWT è intatto. - **Typed options** ✓ — hard cap ed epsilon sono costanti di render dichiarate in un solo posto; nessun `IConfiguration` letto in un servizio. - **Broadcasts / AoI** N/A — nessun broadcast, nessuna entità o evento osservabile. - **Multi-platform** ✓ — `RenderTarget2D` + SM3 sono il percorso già in produzione per il visibility buffer (Win/macOS), con fallback obbligatorio se il formato non è creabile. - **Assets required** ✓ — nessuna arte nuova; dove si rimuove non si inventa un placeholder: si vede il vuoto, ed è un limite dichiarato (mesh a foglio unico). - **Asset naming HARD GATE** N/A — nessun id asset nuovo. - **Walkability by slope** ✓ — la regione **è definita** da `TileMapRules` (walkable + `maxSlopeZ` + `MaxWalkStepZ`), non da una copia: è il motivo per cui la parete del cratere non entra e la strada sì. - **Occupancy contains render** ✓ — l'attivazione resta letta dall'occupancy canonica; mesh e `DisplayZ` restano presentazione. La regione si calcola sulla Z **autoritativa** (raggiungibilità = simulazione) e si rasterizza a `DisplayZ` (render): i due non vanno mescolati, ed è la crepa da cui esce il KO 3. - **Cutaway is structural** ✓ — l'attivazione non cambia; cambia solo *cosa* si rimuove. Il bullet della checklist va aggiornato **nello stesso commit**, perché oggi dice "tutto sopra una quota strutturale". - **ModernUO as reference** ✓ — divergenza consapevole: il client UO nasconde *statics per-tile* sopra il tuo livello, perché la sua mappa è fatta di tile discreti con layer; su un heightfield continuo "il livello sopra" non esiste come insieme di oggetti, quindi prendiamo l'idea (nascondi ciò che copre il posto in cui sei) con l'unico strumento che una superficie continua consente: l'occlusione in screen-space. - **Docs & DoD** ✓ — stesso commit: bullet + invariante in `CLAUDE.md`, sezione *Level-focus visibility* nel design doc del modello a volumi, e la DoD qui sotto. ## Piano di verifica **Unit puri in `Client.Core`** (nessun MonoGame, mappe costruite a mano): - **rampa → pianoro aperto (OBBLIGATORIO)** — cratere con rampa percorribile che sbocca su un pianoro esteso: documenta quanto la regione si estende e che il cap tecnico non scatta prima della finestra; - strada che sale dal cratere: preservata per tutta la sua lunghezza dentro la finestra; - parete del cratere: non raggiungibile → mai preservata; - collina dolce camminabile: raggiungibile → preservata → mai rimossa; - cella di partenza sempre nella regione, anche se isolata. **Nessun test server** — niente cambia lato server. **Screenshot** (DB fresco + harness): fondo cratere con la strada visibile fino a bordo schermo · la stessa scena camminando 8-10 passi, per giudicare se il bordo "respira" · foresta, dove `cutMode` è False e nulla deve cambiare · fallback forzato. **Gap dichiarato:** i casi UNDER (caverna) non sono dimostrabili dal vivo finché non esiste una cavità autorata; restano coperti dai test. ## Definition of Done - [ ] Su una strada che sale dal fondo del cratere, la strada è visibile **fino al bordo schermo**; dimostrato con screenshot. - [ ] Camminando 8-10 passi sul fondo, il bordo del taglio non "respira": giudicato su screenshot consecutivi. - [ ] In foresta (`cutMode False`) nessun frammento viene rimosso: `removedSheets 0` e nessuna area vuota. - [ ] Terreno raggiungibile e camminabile non viene **mai** rimosso — coperto da unit test. - [ ] Il test "rampa → pianoro aperto" esiste e passa, e il suo esito è riportato nel PR. - [ ] Con il render target non disponibile il gioco resta giocabile sulla regola a quota, e l'overlay admin lo dichiara. - [ ] L'overlay admin espone: celle preservate, `Truncated`, stato del fallback. - [ ] `CLAUDE.md` (bullet + invariante) e il design doc del modello a volumi aggiornati nello stesso commit.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
marco/IsoMmo#279
No description provided.