Aller au contenu principal

Session 92 — Lighting moderne JME complet + audit textures walls

Objectif

Revoir l’integralite du systeme d’eclairage (niveau + objets + vectobj) en
abandonnant le systeme Amiga historique (palette shading, brightness
per-zone/per-polygon baked-in) au profit d’un lighting JME standard :
AmbientLight + DirectionalLight + PointLight headlight + PointLights
par zone brillante.

En parallele, demarrage d’un audit des textures de murs (UV/wrap).

Diagnostic initial

Les sessions precedentes (75-90) avaient force tous les materiaux en
Unshaded.j3md via GameAppState.fixMaterials() car le lighting JME
avait donne des resultats decevants au debut. Resultat : les lumieres
(AmbientLight, DirectionalLight, PointLight headlight, et les ~40
PointLights par zone serialisees dans les .j3o par LevelSceneBuilder)
etaient toutes ignorees car rien n’etait Lit.

En realite, LevelSceneBuilder configurait deja tout correctement au
build-time (Lighting.j3md + UseMaterialColors + Ambient=0.5 + Diffuse=White).
Il suffisait d’arreter de l’ecraser au chargement.

Modifications code (Phase A — Lighting)

  1. GameAppState.java :
    – Nouvelles constantes : AMBIENT_COLOR (0.35,0.33,0.40), SUN_COLOR
    (0.55,0.52,0.50) + SUN_DIRECTION (-0.4,-1,-0.3), HEADLIGHT_COLOR
    (1.0,0.92,0.80), HEADLIGHT_RANGE=30
    – Nouveau champ DirectionalLight sunLight (detruit proprement dans
    cleanup())
    – Suppression de fixMaterials(), remplacement par
    upgradeMaterialsForLighting() qui :

    • garde les Lighting.j3md existants et force
      UseMaterialColors=true + Ambient=0.45 + Diffuse=White si absent
      (via ensureLightingParams())
    • convertit les Unshaded + VertexColor en Lighting.j3md avec
      UseVertexColor=true (compat future)
    • convertit les Unshaded + Alpha en Lighting.j3md avec
      AlphaDiscardThreshold=0.5 + BlendMode.Alpha
    • preserve le FaceCullMode et le bucket Transparent
    • Configuration du lighting pipeline :
      setPreferredLightMode(SinglePass) + setSinglePassLightBatchSize(8)
  2. LevelSceneBuilder.tryLoadSprite() : sprites billboards bitmap
    passent de Unshaded.j3md a Lighting.j3md (DiffuseMap + Ambient=0.6
    + AlphaDiscardThreshold + FaceCullMode.Off). Impact : les sprites
    reagissent maintenant au headlight et aux PointLights de zone.

  3. VectObjConverter.buildMaterial() : materiau partage des vectobj
    (armes, boss polygonaux) passe de Unshaded.j3md a Lighting.j3md
    avec UseVertexColor=true preserve (le VertexColor baked-in du
    brightness Amiga devient un modulateur du Diffuse).

  4. WeaponViewAppState.fixWeaponMaterials() : ajout d’une conversion
    on-the-fly Unshaded->Lighting via
    upgradeVectObjMaterialInPlace() pour rester compatible avec les
    anciens .j3o pre-session 92. Sans cela, il fallait forcer la
    reconversion complete des vectobj.

Modifications code (Phase B — Audit textures walls)

  1. Nouveau WallTextureDiagnostic.java (+ tache Gradle diagWallTextures) :
    audite tous les PNGs walls — dimensions, largeur en multiple de 16,
    hauteur = 128, canal alpha, distribution des largeurs.

  2. Nouveau WallUsageDiagnostic.java (+ tache Gradle diagWallUsage) :
    croise les JSON level_*.json avec les PNGs walls pour detecter :
    textures manquantes, murs avec wallLen > texW (wrap U), murs avec
    wallH > 128 (wrap V), distribution fromTile/yOffset.

Validation test

Retour utilisateur apres convertVectObj + buildScenes + run :

upgradeMaterialsForLighting : 96 Lighting (gardes) + 0 Unshaded->Lighting
  (alpha) + 0 Unshaded->Lighting (vertex color) + 0 inchanges
Pas de soucis particulier

Les 96 materiaux du level A etaient deja en Lighting.j3md des le build
(LevelSceneBuilder) : la correction consistait donc simplement a ne plus
les ecraser au chargement. L’ecran de jeu affiche maintenant le lighting
JME complet sans regression visible.

Impact technique

  • Le headlight PointLight suit la camera (range 30) → ambiance FPS classique
  • Les PointLights de zone (luminosite >=40 dans le brightness Amiga) creent
    des halos colores dans les salles allumees
  • Les vectobj reagissent aux lumieres (arme en main eclairee, boss aliens
    teintes selon la zone)
  • setSinglePassLightBatchSize=8 : 8 lumieres max par objet par passe,
    suffisant pour un FPS zonal (peu d’objets sont dans le field de plusieurs
    PointLights simultanement)

Outils d’audit disponibles

./gradlew diagWallTextures           # Check PNG dimensions, alpha, distribution
./gradlew diagWallUsage              # Croise JSON levels x PNG walls
./gradlew diagWallUsage -Plevel=A    # Un seul niveau

Phase B suite — Corrections textures walls apres audit

L’audit diagWallTextures a revele que 12 textures sur 13 ont une
largeur non-multiple de 16
:

hullmetal        258x128  (= 256 + 2 pixels padding)
chevrondoor      129x128  (= 128 + 1)
brownpipes       258x128  (= 256 + 2)
gieger           642x128  (= 640 + 2)
rocky            513x128  (= 512 + 1)
stonewall        195x64   (= 192 + 3, + hauteur != 128)
steampunk         64x64   (hauteur != 128)
brownstonestep   129x32   (= 128 + 1, + hauteur != 128)

Cause : le format .256wad stocke les pixels 3-par-3 dans des WORDs
16 bits (5 bits par pixel). Pour une texture de 256 px, on a
ceil(256/3) = 86 groupes = 258 pixels encodes (les 2 derniers sont
des pixels de padding sans valeur visuelle). Le calcul inverse de la
largeur dans WallTextureExtractor.computeWidth() ne tenait pas compte
de ce padding et retournait 258 au lieu de 256.

De plus, plusieurs textures ont une hauteur differente de 128 (64 ou
32 px), ce qui les fait etirer/compresser verticalement lors du mapping
UV. Par exemple, stonewall (64 px) etait etiree x2 sur chaque mur de
128 unites de haut.

Modifications code (correctives)

  1. WallTextureExtractor.java :
    – Nouvelle methode snapToAmigaWidth(rawWidth) : arrondit au multiple
    de 16 inferieur si l’ecart est <=3 pixels. Tolere l’ecart exceptionnel
    de stonewall (195 -> 192, ecart 3).
    computeWidth() appelle maintenant snapToAmigaWidth(). Impact
    apres regeneration : les 12 PNGs non-multiples de 16 deviennent
    correctement dimensionnes.

  2. LevelSceneBuilder.java :
    – Nouveau champ int[] wallTexHeights (hauteur reelle lue depuis le
    PNG, comme pour wallTexWidths).
    – Calcul UV vM utilise maintenant wallH / texH (hauteur reelle) au
    lieu de wallH / TEX_V (128 fixe). Les textures de 64 ou 32 px de
    haut sont maintenant repetees verticalement au lieu d’etre etirees.
    – Meme correction pour makeDoorSegGeo() (segments de portes
    animees). Le parametre texH est passe en plus de texW.

Procedure de mise a jour apres ces corrections

./gradlew convertAssets    # Regenere PNGs walls (snap largeurs a 16)
./gradlew buildScenes      # Regenere scene_*.j3o avec le nouveau UV mapping
./gradlew run              # Test visuel
./gradlew diagWallTextures # Verification : 0 "w!%16"

Phase B suite (fix 2) — Respect du tileWidth Amiga

Apres le fix des largeurs, l’utilisateur rapporte que certaines textures sont
entierement affichees sur un mur, alors que le jeu original n’affiche
qu’une partie (une « tile » logique).

Diagnostic via l’ASM

Analyse de hiresgourwall.s (dessin des murs avec shading Gouraud) :

move.w  d1, d6
and.w   draw_WallTextureWidthMask_w, d6   ; wrap par mask (tileWidth-1)
...
add.w   draw_FromTile_w(pc), d6           ; ajoute fromTile*16 APRES le wrap

Conclusion : l’Amiga utilise un mask de puissance de 2 (typiquement
widthMask = 127 pour une tile de 128 pixels) pour wrapper horizontalement.
Le fromTile sert ensuite a selectionner quelle tile dans la texture
totale. Une texture 256×128 contient donc 2 tiles de 128×128 accessibles
via fromTile=0 (gauche) ou fromTile=8 (droite).

Pour un mur plus long que la tile, l’Amiga repete la MEME tile, il ne
glisse pas sur la tile suivante (celle qui pourrait etre visuellement
differente).

Probleme avec JME

JME WrapMode.Repeat wrappe sur la texture entiere (UV [0, 1] = toute
la largeur du PNG), pas sur une sous-region. Un UV a 1.5 mod 1.0 devient
0.5 de la texture totale, pas de la sous-tile courante -> debordement
sur la tile d’a cote.

Solution : decoupage de mur en sous-quads

Code refactore dans LevelSceneBuilder.buildScene() :

  1. Pour chaque mur, calcule tileWidth (128 pour texW >= 128, sinon texW)
  2. Si wallLen > tileWidth, decoupe le mur en N = ceil(wallLen/tileWidth)
    sous-quads
  3. Chaque sous-quad a un UV borne strictement a [uOffset, uOffset + tileUvWidth]
    ou tileUvWidth = tileWidth/texW

Ainsi, le UV ne depasse jamais la zone de la tile selectionnee par
fromTile, et JME repete proprement cette tile quand le mur est plus long.

Modifications code

  1. LevelSceneBuilder.java :
    – Nouvelle methode deduceTileWidth(texW) : retourne 128 si texW >= 128,
    sinon texW.
    – La boucle de construction des murs utilise maintenant un sous-bouclage
    sur numRepeats pour generer plusieurs quads par mur long.
    – Impact: ~1479 murs (sur 6891 total, 21%) sont maintenant decoupes en
    2+ quads. Les 5412 autres restent en 1 quad (cas majoritaire).

  2. Nouveau WallTileDiagnostic.java (+ tache Gradle diagWallTiles) :
    outil d’extraction visuelle des tiles logiques par texture. Genere dans
    build/wall-tiles/ :
    wall_XX_*_annotated.png : PNG zoome x2 avec lignes rouges sur les
    frontieres de tiles et labels fromTile=N
    wall_XX_*_tileN.png : chaque tile extraite individuellement

Procedure de mise a jour (fix 2)

./gradlew buildScenes      # Regenere scenes avec le nouveau decoupage
./gradlew run              # Verification visuelle
./gradlew diagWallTiles    # Optionnel : voir les tiles extraites
./gradlew diagWallTiles -Pwall=02    # Juste hullmetal

Phase B suite (fix 3) — Masks Amiga par-mur (tile dimensions exactes)

Le fix 2 utilisait une regle empirique (tileWidth = 128 si texW >= 128) pour
le decoupage en sous-quads. Mais l’analyse approfondie de hireswall.s a
revele que chaque mur stocke ses propres masks dans le binaire du niveau :

moveq    #0,d1
move.b  (a0)+,d1
move.w  d1,draw_WallTextureHeightMask_w   ; hMask = textureHeight - 1
moveq   #0,d1
move.b  (a0)+,d1
move.w  d1,draw_WallTextureHeightShift_w  ; log2(textureHeight)
moveq   #0,d1
move.b  (a0)+,d1                          ; texture width - 1
move.w  d1,draw_WallTextureWidthMask_w    ; wMask

Donc les vraies dimensions de tile sont wMask + 1 et hMask + 1, baked-in
par le LevelED dans chaque entree de mur. Ces valeurs etaient deja parsees
dans WallRenderEntry mais jamais exportees dans le JSON.

De plus, les portes/lifts utilisaient encore l’ancien makeDoorSegGeo simple
qui ne respectait pas le tile mapping, d’ou les textures mal appliquees
rapportees par l’utilisateur.

Modifications code

  1. LevelJsonExporter.java : ajout des champs wMask, hMask, hShift
    dans le JSON exporte pour chaque mur. Format :
    json
    {"leftPt":..., ..., "wMask":127, "hMask":127, "hShift":7}

  2. LevelSceneBuilder.java :
    parseWalls() lit les nouveaux champs wMask/hMask (compat avec
    anciens JSON : valeur 0 = fallback sur deduceTileWidth)
    – Le calcul tileWidth/tileHeight utilise les vraies valeurs des masks au
    lieu de la regle empirique du fix 2
    makeDoorSegGeo() accepte maintenant tileW et tileH en parametres
    supplementaires et les utilise pour calculer uM/vM (au lieu de texW/texH)
    DoorAccum.addSeg() propage wMask et hMask aux segments

Impact

  • Murs : meme effet visuel que fix 2 dans la majorite des cas (tile=128)
    mais maintenant exact pour des cas exotiques (textures avec des tiles
    non-128 dans certains niveaux)
  • Portes/lifts : enfin afficher la bonne tile et pas la texture entiere.
    L’utilisateur rapportait des portes avec textures « mal appliquees » – fix
    conforme a l’ASM original.
  • Marches/differences de niveau : les murs courts (8-32 unites) avec des
    textures speciales (bandes jaune-noire, brownstonestep) repetent maintenant
    correctement leur tile au lieu d’etirer/comprimer.

Procedure de mise a jour (fix 3)

./gradlew convertLevels    # Regenere les JSON avec wMask/hMask
./gradlew buildScenes      # Regenere les scenes avec le bon mapping UV
./gradlew run              # Test visuel - portes + marches + tile-aware

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *