Revenir au plan du site

Effet de lentille déformante

(rediff partielle et modifiée d'un article paru dans 64Nops n°1) Cet effet a été utilisé dans la démo [ Sphere ]

Je ne sais pas si la démo Second Reality vous parle, mais c'est une des premières démos PC qui a marqué les esprits (il faut voir qu'à cette époque les démos Amiga étaient au même niveau avec moins de puissance, que la catégorie PC était naissante, principalement à cause du manque d'outils pour faire des musiques et gérer les cartes sons).

Un des effets marquants de cette démo était une bulle passant devant un visage de démon, faisant une jolie déformation. Cet effet de zoom déformant ne représente pourtant pas du tout ce qui se passe quand on regarde au travers d'une boule de cristal. Regarder à travers une boule de cristal n'est pas pratique, c'est un peu comme regarder dans un objectif fish-eye qui inverserait haut, bas, gauche et droite... L'effet n'est pas du tout grossissant mais au contraire, rend tout minuscule!

Bon, comme on est demomaker, on va envoyer bouler les lois de la physique et faire un effet déformant parce que c'est plus intéressant à regarder :)

 
I'm not an Atomic playboy! - Second Realiy (Future Crew)
Une vraie sphère ne fait pas ça du tout ^_^


Je m'épuise car Thalès est toujours à faire

L'idée générale de l'effet va être d'utiliser une table de correspondance ou "offset map" et d'afficher un sprite carré (oui, carré, vous avez bien lu!). Le contenu de cette table va être un déplacement d'adresse. Si les points sont à l'intérieur d'un cercle, on va se déplacer en accord avec la distance rapport au centre pour obtenir un effet bombé. Si les points sont à l'extérieur du cercle, on ne fait pas de déplacement et c'est en fait le fond qui sera copié.

Ainsi, notre routine d'affichage est simple et générique mais à l'écran, on aura bien une sphère qui se déplace et qui efface autour d'elle.

Les mathématiciens cachent leurs bornes sous un flot de formules

Pour la texture, il ne s'agit pas d'une simple image par dessus laquelle on passe, chaque octet ne va contenir qu'un seul pixel! Et en lisant la texture, on va reconstruire l'octet avec le pixel d'où qu'il soit. On ne stockera que des pixels à droite. Le pixel de gauche s'obtient (voir format écran) en décalant d'un coup vers la gauche, avec un simple ADD.

Bien que notre écran fasse 64 octets de large, la texture devra avoir des lignes de 256 octets de long (pour qu'un INC H ou un DEC H passe à la ligne suivante ou précédente)

À cause de la longueur des lignes, il n'est pas possible de faire tout tenir en mémoire centrale alors nous allons réduire la résolution verticale par deux (notre texture fera donc 32K, ça passe!).



Alors histoire de me faciliter la conversion, je vais ajouter du blanc à droite et à gauche pour que la texture fasse 256 pixels de large. Ensuite avec convGeneric, il existe une option pour extraire 1 pixel vers 1 octet, notre textureMap sera prête sans avoir la manipuler.
convgeneric.exe -m 0 -single -g face.png -flat

On se servira de cette texture pour afficher l'écran de départ.
affiche_reference
ld hl,texturemap+64
ld de,#C000

ld xl,128 ; parcourir 128 lignes de la texture
.affiche_reference
ld b,64 ; on va construire 64 octets
push de
.dispr ld a,(hl) : add a : inc l : or (hl) : inc l : ld (de),a : inc e : djnz .dispr ; à partir de 128 pixels
exx : pop de : ld a,e : ld hl,#800 : add hl,de : ex hl,de : ld bc,64 : ldir : exx ; duplication des lignes
ld e,a
ex hl,de
ld bc,2*#800 : add hl,bc : jr nc,.pas_doverflow ; sauter 2 lignes
ld bc,64-#4000 : add hl,bc
.pas_doverflow
ex hl,de
inc h : ld a,l : sub 128 : ld l,a ; sauter le blanc dans le sprite
dec xl
jr nz,.affiche_reference
ret

La boule est formelle

Comme nous avons organisé notre texture en lignes de 256 octets, le tableau correspondant aux vecteurs déplacement va mettre le déplacement vertical dans le poids fort d'une valeur 16 bits et le déplacement horizontal dans le poids faible. En chaque point du tableau, on calcule si on est dans le cercle, ensuite on fait une mise au carré de la valeur, c'est purement empirique, la division par 2.3 aussi (vous pourrez jouer avec cette valeur).

Si vous voulez jouer sur l'effet de zoom, c'est ce calcul qu'il faudra modifier.
BULLE_SIZE=64
BULLE_HALF=BULLE_SIZE>>1
boule_carree
repeat BULLE_SIZE+2,y
repeat BULLE_SIZE+2,x

  cx=(x-BULLE_HALF-2)/BULLE_HALF
  cy=(y-BULLE_HALF-2)/BULLE_HALF
  no=sqrt(cx*cx+cy*cy) ; distance rapport au centre

  if no>1 || x<=2 || x>=BULLE_SIZE+1 || y==BULLE_SIZE+2; ça veut dire qu'on est hors du cercle, on met simplement la valeur normale
    lowbyte=x-BULLE_HALF-2
    highbyte=hi(texturemap)+y-1 ;64+y-BULLE_HALF-2
  else
    coef=1+no*no ; va nous donner une valeur entre 1 et 2
    coef=coef*BULLE_HALF/2.3 ; Ici on peut jouer avec une valeur entre 2 et 5
    if coef>BULLE_HALF : coef=BULLE_HALF : endif
    lowbyte=cx*coef
    highbyte=hi(texturemap)+cy*coef+BULLE_HALF ;+64
  endif
; inscrire la valeur 16 bits dans le tableau
  defb lowbyte+33+64,highbyte
rend
rend

L'effet de la translation

L'affichage se fait presque comme celui d'un sprite, on va parcourir le rectangle de notre sprite, récupérer avec des POP les vecteurs translation et faire un ADD pour se déplacer.
; SP = données de translation de notre sprite
; DE = adresse écran des lignes paires
; HL'= adresse écran des lignes paires
; DE'= adresse écran des lignes impaires
affiche_boule
ld sp,boule_carree
repeat BULLE_HALF+1
  pop hl : add hl,bc : ld a,(hl) : add a : pop hl : add hl,bc : or (hl) : ld (de),a : inc e ; on traite les pixels 2 à 2 pour remplir les octets
rend
exx
repeat BULLE_HALF+1 : ldi : rend
ld bc,2*#800-BULLE_HALF-1 : add hl,bc : jr nc,.padov
ld bc,64-#4000 : add hl,bc : .padov
ld a,h : add 8 : ld d,a : ld e,l
exx
ld a,-BULLE_HALF-1 : add e : ld l,a : ld a,d : add #10 : ld h,a : jr nc,.pas_doverflow
ld de,64-#4000 : add hl,de
.pas_doverflow
ex hl,de
dec xl
jp nz,affiche_boule

Je vous invite à télécharger la [ texture ] avant de compiler ce programme.
BUILDSNA : BANKSET 0
org #100 : run #100
textureMap equ #4000
BULLE_SIZE=64
BULLE_HALF=BULLE_SIZE>>1
ld sp,#100
ld bc,#7F00 : ld e,17 : ld hl,palette
setPal out (c),c : inc c : inc b : outi : dec e : jr nz,setPal
ld bc,#7F80+%1100 : out (c),c ; MODE 0
ld bc,#BC01 : out (c),c : ld a,32 : inc b : out (c),a ; largeur visible
ld bc,#BC02 : out (c),c : ld a,42 : inc b : out (c),a ; position X
ld bc,#BC06 : out (c),c : ld a,32 : inc b : out (c),a ; hauteur visible
ld bc,#BC07 : out (c),c : ld a,34 : inc b : out (c),a ; position Y
; *** affiche_reference ***
ld hl,texturemap+64
ld de,#C000

ld xl,128 ; parcourir 128 lignes de la texture
.affiche_reference
ld b,64 ; on va construire 64 octets
push de
.dispr ld a,(hl) : add a : inc l : or (hl) : inc l : ld (de),a : inc e : djnz .dispr ; à partir de 128 pixels
exx : pop de : ld a,e : ld hl,#800 : add hl,de : ex hl,de : ld bc,64 : ldir : exx ; duplication des lignes
ld e,a
ex hl,de
ld bc,2*#800 : add hl,bc : jr nc,.pas_doverflow ; sauter 2 lignes
ld bc,64-#4000 : add hl,bc
.pas_doverflow
ex hl,de
inc h : ld a,l : sub 128 : ld l,a ; sauter le blanc dans le sprite
dec xl
jr nz,.affiche_reference
;*************************************************************
           LaBoucle
;*************************************************************
ld de,#C000 : exx : ld hl,#C000 : ld de,#C800 : exx
ld sp,boule_carree
ld xl,BULLE_SIZE+1
ld bc,63 ; pixel de la texture correspondant au coin supérieur gauche de l'écran

affiche_boule
repeat BULLE_HALF+1
  pop hl : add hl,bc : ld a,(hl) : add a : pop hl : add hl,bc : or (hl) : ld (de),a : inc e ; on traite les pixels 2 à 2 pour remplir les octets
rend
exx
repeat BULLE_HALF+1 : ldi : rend
ld bc,2*#800-BULLE_HALF-1 : add hl,bc : jr nc,.padov
ld bc,64-#4000 : add hl,bc : .padov
ld a,h : add 8 : ld d,a : ld e,l
exx
ld a,-BULLE_HALF-1 : add e : ld l,a : ld a,d : add #10 : ld h,a : jr nc,.pas_doverflow
ld de,64-#4000 : add hl,de
.pas_doverflow
ex hl,de
dec xl
jp nz,affiche_boule
jr $

palette defb #54,#44,#5C,#58,#46,#5E,#40,#4C,#4E,#45,#47,#5F,#43,#5B,#4F,#4B,#4B

boule_carree
repeat BULLE_SIZE+2,y
repeat BULLE_SIZE+2,x

  cx=(x-BULLE_HALF-2)/BULLE_HALF
  cy=(y-BULLE_HALF-2)/BULLE_HALF
  no=sqrt(cx*cx+cy*cy) ; distance rapport au centre

  if no>1 || x<=2 || x>=BULLE_SIZE+1 || y==BULLE_SIZE+2; ça veut dire qu'on est hors du cercle, on met simplement la valeur normale
    lowbyte=x-BULLE_HALF-2
    highbyte=hi(texturemap)+y-1 ;64+y-BULLE_HALF-2
  else
    no*=sqrt(BULLE_HALF)
    noref=no
    no*=no
    no=(noref+no+no)/2.7
    if no>BULLE_HALF : no=BULLE_HALF : endif
    lowbyte=cx*no
    highbyte=hi(texturemap)+cy*no+BULLE_HALF ;+64
  endif
; inscrire la valeur 16 bits dans le tableau
  defb lowbyte+BULLE_HALF+2,highbyte
rend
rend

org textureMap : incbin 'face.bin'

On devine déjà quelque chose, mais ça ne bouge pas!


On va devoir ajouter quelques petites routines pour cela.

L'idée générale du déplacement de notre sprite, c'est qu'on va devoir déplacer la boule ET en même temps, changer la valeur de notre décalage pour qu'il corresponde à la position de la boule. On ira poker notre initialisation dans le code aux adresses destinationEcran et decalageTexture.
Tout se passe au chapitre "deplacement", vous verrez, ce n'est pas violent.
BUILDSNA : BANKSET 0
org #100 : run #100
textureMap equ #4000
BULLE_SIZE=64
BULLE_HALF=BULLE_SIZE>>1
ld sp,#100
ld bc,#7F00 : ld e,17 : ld hl,palette
setPal out (c),c : inc c : inc b : outi : dec e : jr nz,setPal
ld bc,#7F80+%1100 : out (c),c ; MODE 0
ld bc,#BC01 : out (c),c : ld a,32 : inc b : out (c),a ; largeur visible
ld bc,#BC02 : out (c),c : ld a,42 : inc b : out (c),a ; position X
ld bc,#BC06 : out (c),c : ld a,32 : inc b : out (c),a ; hauteur visible
ld bc,#BC07 : out (c),c : ld a,34 : inc b : out (c),a ; position Y
; *** affiche_reference ***
ld hl,texturemap+64
ld de,#C000

ld xl,128 ; parcourir 128 lignes de la texture
.affiche_reference
ld b,64 ; on va construire 64 octets
push de
.dispr ld a,(hl) : add a : inc l : or (hl) : inc l : ld (de),a : inc e : djnz .dispr ; à partir de 128 pixels
exx : pop de : ld a,e : ld hl,#800 : add hl,de : ex hl,de : ld bc,64 : ldir : exx ; duplication des lignes
ld e,a
ex hl,de
ld bc,2*#800 : add hl,bc : jr nc,.pas_doverflow ; sauter 2 lignes
ld bc,64-#4000 : add hl,bc
.pas_doverflow
ex hl,de
inc h : ld a,l : sub 128 : ld l,a ; sauter le blanc dans le sprite
dec xl
jr nz,.affiche_reference
;*************************************************************
           LaBoucle
;*************************************************************
ld sp,#100 ; besoin d'une pile valide pour notre PUSH/POP
ld de,#C000 : destinationEcran=$-2 : push de : exx : pop hl : ld e,l : ld a,h : add 8 : ld d,a : exx
ld sp,boule_carree
ld xl,BULLE_SIZE+2
ld bc,0 : decalageTexture=$-2 ; pixel de la texture correspondant au coin supérieur gauche de l'écran

;****************
affiche_boule
;****************
repeat BULLE_HALF+1
  pop hl : add hl,bc : ld a,(hl) : add a : pop hl : add hl,bc : or (hl) : ld (de),a : inc e ; on traite les pixels 2 à 2 pour remplir les octets
rend
exx
repeat BULLE_HALF+1 : ldi : rend
ld bc,2*#800-BULLE_HALF-1 : add hl,bc : jr nc,.padov
ld bc,64-#4000 : add hl,bc : .padov
ld a,h : add 8 : ld d,a : ld e,l
exx
ld a,-BULLE_HALF-1 : add e : ld l,a : ld a,d : add #10 : ld h,a : jr nc,.pas_doverflow
ld de,64-#4000 : add hl,de
.pas_doverflow
ex hl,de
dec xl
jp nz,affiche_boule
;****************
deplacement
;****************
ld bc,1 : incrementX=$-2
ld hl,(destinationEcran) : add hl,bc : ld (destinationEcran),hl
ld hl,(decalageTexture) : add hl,bc : add hl,bc : ld (decalageTexture),hl ; double pour la texture car 1 pixel par octet!
.compteurX ld a,28 : dec a : jr nz,.suite
ld h,a : ld l,a : or a : sbc hl,bc : ld (incrementX),hl ; inverser l'incrément
ld a,28 ; on repart pour une série de 28!
.suite
ld (.compteurX+1),a

ld sp,#100 ; besoin d'une pile valide pour le CALL
ld a,1 : incrementY=$-1
or a : jr z,remonte
; descend
ld hl,(destinationEcran) : ld a,h : add 8 : ld h,a : call NextLineHL : ld (destinationEcran),hl
ld hl,(decalageTexture) : inc h : ld (decalageTexture),hl
jr compteurY
remonte
ld hl,(destinationEcran) : call PreviousLineHL : res 3,h : ld (destinationEcran),hl
ld hl,(decalageTexture) : dec h : ld (decalageTexture),hl

compteurY ld a,61 : dec a : jr nz,.suiteY
ld a,(incrementY) : inc a : and 1 : ld (incrementY),a ; 1, 0, 1, 0, ...
ld a,61 ; on repart pour une série de 41!
.suiteY
ld (compteurY+1),a

jp LaBoucle

NextLineHL ld a,h : add 8 : ld h,a : ret nc
ld a,64 : add l : ld l,a : ld a,#C0 : adc h : ld h,a : res 3,h : ret
PreviousLineHL ld a,h : sub 8 : ld h,a : and #38 : cp #38 : ret nz
ld a,lo(16384-64) : add l : ld l,a : ld a,#3F : adc h : ld h,a : set 3,h : ret

palette defb #54,#44,#5C,#58,#46,#5E,#40,#4C,#4E,#45,#47,#5F,#43,#5B,#4F,#4B,#4B

boule_carree
repeat BULLE_SIZE+2,y
repeat BULLE_SIZE+2,x

  cx=(x-BULLE_HALF-2)/BULLE_HALF
  cy=(y-BULLE_HALF-2)/BULLE_HALF
  no=sqrt(cx*cx+cy*cy) ; distance rapport au centre

  if no>1 || x<=2 || x>=BULLE_SIZE+1 || y==BULLE_SIZE+2; ça veut dire qu'on est hors du cercle, on met simplement la valeur normale
    lowbyte=x-BULLE_HALF-2
    highbyte=hi(texturemap)+y-1 ;64+y-BULLE_HALF-2
  else
    coef=1+no*no ; va nous donner une valeur entre 1 et 2
    coef=coef*BULLE_HALF/2.3 ; Ici on peut jouer avec une valeur entre 2 et 5
    if coef>BULLE_HALF : coef=BULLE_HALF : endif
    lowbyte=cx*coef
    highbyte=hi(texturemap)+cy*coef+BULLE_HALF ;+64
  endif
; inscrire la valeur 16 bits dans le tableau
  defb lowbyte+33+64,highbyte
rend
rend

org textureMap : incbin 'face.bin'

Et voici la preview du résulat que vous obtiendrez avec ce source. N'hésitez pas à venir changer la valeur 2.3 dans la génération des données de déformation et la remplacer par une valeur entre 2 et 5, ça change le bombé de la déformation.