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)
-
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 champDirectionalLight sunLight(detruit proprement dans
cleanup())
– Suppression defixMaterials(), remplacement par
upgradeMaterialsForLighting()qui :- garde les
Lighting.j3mdexistants et force
UseMaterialColors=true + Ambient=0.45 + Diffuse=Whitesi absent
(viaensureLightingParams()) - convertit les
Unshaded+ VertexColor enLighting.j3mdavec
UseVertexColor=true(compat future) - convertit les
Unshaded+ Alpha enLighting.j3mdavec
AlphaDiscardThreshold=0.5 + BlendMode.Alpha - preserve le
FaceCullModeet le bucketTransparent - Configuration du lighting pipeline :
setPreferredLightMode(SinglePass)+setSinglePassLightBatchSize(8)
- garde les
-
LevelSceneBuilder.tryLoadSprite(): sprites billboards bitmap
passent deUnshaded.j3mdaLighting.j3md(DiffuseMap + Ambient=0.6
+ AlphaDiscardThreshold + FaceCullMode.Off). Impact : les sprites
reagissent maintenant au headlight et aux PointLights de zone. -
VectObjConverter.buildMaterial(): materiau partage des vectobj
(armes, boss polygonaux) passe deUnshaded.j3mdaLighting.j3md
avecUseVertexColor=truepreserve (le VertexColor baked-in du
brightness Amiga devient un modulateur du Diffuse). -
WeaponViewAppState.fixWeaponMaterials(): ajout d’une conversion
on-the-flyUnshaded->Lightingvia
upgradeVectObjMaterialInPlace()pour rester compatible avec les
anciens.j3opre-session 92. Sans cela, il fallait forcer la
reconversion complete des vectobj.
Modifications code (Phase B — Audit textures walls)
-
Nouveau
WallTextureDiagnostic.java(+ tache GradlediagWallTextures) :
audite tous les PNGs walls — dimensions, largeur en multiple de 16,
hauteur = 128, canal alpha, distribution des largeurs. -
Nouveau
WallUsageDiagnostic.java(+ tache GradlediagWallUsage) :
croise les JSONlevel_*.jsonavec les PNGs walls pour detecter :
textures manquantes, murs avecwallLen > texW(wrap U), murs avec
wallH > 128(wrap V), distributionfromTile/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)
-
WallTextureExtractor.java:
– Nouvelle methodesnapToAmigaWidth(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 maintenantsnapToAmigaWidth(). Impact
apres regeneration : les 12 PNGs non-multiples de 16 deviennent
correctement dimensionnes. -
LevelSceneBuilder.java:
– Nouveau champint[] wallTexHeights(hauteur reelle lue depuis le
PNG, comme pourwallTexWidths).
– Calcul UV vM utilise maintenantwallH / texH(hauteur reelle) au
lieu dewallH / TEX_V(128 fixe). Les textures de 64 ou 32 px de
haut sont maintenant repetees verticalement au lieu d’etre etirees.
– Meme correction pourmakeDoorSegGeo()(segments de portes
animees). Le parametretexHest passe en plus detexW.
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() :
- Pour chaque mur, calcule
tileWidth(128 pour texW >= 128, sinon texW) - Si
wallLen > tileWidth, decoupe le mur enN = ceil(wallLen/tileWidth)
sous-quads - Chaque sous-quad a un UV borne strictement a
[uOffset, uOffset + tileUvWidth]
outileUvWidth = 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
-
LevelSceneBuilder.java:
– Nouvelle methodededuceTileWidth(texW): retourne 128 si texW >= 128,
sinon texW.
– La boucle de construction des murs utilise maintenant un sous-bouclage
surnumRepeatspour 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). -
Nouveau
WallTileDiagnostic.java(+ tache GradlediagWallTiles) :
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 labelsfromTile=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
-
LevelJsonExporter.java: ajout des champswMask,hMask,hShift
dans le JSON exporte pour chaque mur. Format :
json
{"leftPt":..., ..., "wMask":127, "hMask":127, "hShift":7} -
LevelSceneBuilder.java:
–parseWalls()lit les nouveaux champswMask/hMask(compat avec
anciens JSON : valeur 0 = fallback surdeduceTileWidth)
– Le calcultileWidth/tileHeightutilise les vraies valeurs des masks au
lieu de la regle empirique du fix 2
–makeDoorSegGeo()accepte maintenanttileWettileHen parametres
supplementaires et les utilise pour calculer uM/vM (au lieu de texW/texH)
–DoorAccum.addSeg()propagewMaskethMaskaux 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