animations en 2d
Transcription
animations en 2d
CHAPITRE2 ANIMATIONSEN2D AlexandreBlondinMassé Départementd'informatique UniversitéduQuébecàMontréal 23septembre2015 INF5071-Infographie EXERCICE(COURS PRÉCÉDENT) 1. ÉcrivezdesfonctionsenJavaScriptquitransformentunpointd'un systèmedecoordonnéesàunautre: function cartesian2screen(x, y) { // Compléter } function polar2cartesian(r, a) { // Compléter } 2. ÉcrivezunefonctionJavaScriptquitraceunpolygonerégulierden côtéscentréen(x, y),inscritdansuncerclederayonretd'angle initiala(enradians) Graphics.prototype.drawRegularPolygon = function (x, y, n, r, a) { // Compléter } SOLUTION 1. Unesolutionpossibleestlasuivante: function cartesian2screen(x, y) { return [x, -y]; } function polar2cartesian(r, a) { return [r * Math.cos(a), r * Math.sin(a)]; } Onpeutaussiutiliserdesobjets: function cartesian2screen(x, y) { return {x: x, y: -y}; } function polar2cartesian(r, a) { return {x: r * Math.cos(a), y: r * Math.sin(a)}; } SOLUTION(SUITE) 2. Voiciunesolutionpossible: Phaser.Graphics.prototype.drawRegularPolygon = function (x, y, n, r, a) { this.moveTo(x + r * Math.cos(a), y + r * Math.sin(a)); for (i = 1; i <= n; i++) { var a2 = i * 2 * Math.PI / n; var x2 = x + r * Math.cos(a + a2); var y2 = y + r * Math.sin(a + a2); this.lineTo(x2, y2); } } ANIMATIONSDEBASE En2D,lestransformationsgéométriquesprincipalessont: lestranslations; lesrotations; leshomothéties(changementd'échelle)et lesréflexions. Toutescestransformationssontdestransformationslinéaires,à l'exceptiondelatranslation,quiestunetransformationaffine; Autrementdit,ellespeuventêtrereprésentéespardesmatrices. TRANSLATIONS(1/2) Soitv = (a, b)unvecteur; LafonctionTv:ℝ2 → ℝ2définiepar T(x, y) = (x + a, y + b) estappeléetranslationdevecteur`v`. L'inversedeTvestlatranslationdevecteur − v. DansPhaser,appliquerunetranslationesttrèsfacile:ilsuffitde mettreàjourlesattributsxetydel'objetGraphicscorrespondant. TRANSLATIONS(2/2) var game = new Phaser.Game(800, 600, Phaser.AUTO, 'game', {preload: preload, create: create, update: update}); var square; function preload() { } function create() { square = game.add.graphics(); square.clear(); square.lineStyle(2, 0xFF0000, 1); square.drawRect(0, 0, 30, 30); } function update() { var xSpeed = 300; var ySpeed = 200; var elapsed = game.time.elapsed / 1000; // Temps écoulé en secondes square.x += elapsed * xSpeed; square.x %= game.width; square.y += elapsed * ySpeed; square.y %= game.height; } Voirlerésultat ROTATIONS(1/2) Soitθunnombreréel. LafonctionRθ:ℝ2 → ℝ2définiepar Rθ(x, y) = ⎡⎜⎣cosθ − sinθ sinθcosθ⎤⎜⎦⎡⎜⎣x y ⎤⎜⎦ estappeléerotationd'angleθ(autourdel'origine). L'inversedeRθestlarotationd'angle − θ. DansPhaser,appliquerunerotationestaussitrèsfacile: Lecentrederotationestdonnéparlesattributsxety; Ilsuffitdemodifierl'attributangle(endegrés)ourotation(en radians).Apparemment,rotationestlégèrementplusefficace,car iln'yapasdeconversionàl'internequiesteffectuée. ROTATIONS(2/2) var game = new Phaser.Game(800, 600, Phaser.AUTO, 'game', {preload: preload, create: create, update: update}); var square; var ellipse; function preload() { } function create() { square = game.add.graphics(200, 200); square.lineStyle(2, 0xFF0000, 1); square.drawRect(0, 0, 50, 50); ellipse = game.add.graphics(400, 300); ellipse.lineStyle(2, 0x00FFFF, 1); ellipse.drawEllipse(-15, -25, 30, 50); } function update() { var squareSpeed = Math.PI / 2; var ellipseSpeed = -2 * Math.PI; var elapsed = game.time.elapsed / 1000; // Temps écoulé en secondes square.rotation += elapsed * squareSpeed; ellipse.rotation += elapsed * ellipseSpeed; } Voirlerésultat HOMOTHÉTIES(1/2) Soitkunnombreréelpositif. LafonctionHk:ℝ2 → ℝ2définiepar Hk(x, y) = ⎡⎜⎣k0 0k⎤⎜⎦⎡⎜⎣x y⎤⎜⎦ estappeléehomothétiedefacteurk(centréeàl'origine). L'inversedeHkestl'homothétiedefacteur1 ⁄ k. HOMOTHÉTIES(2/2) var game = new Phaser.Game(800, 600, Phaser.AUTO, 'game', {preload: preload, create: create, update: update}); var circle; var startTime; function preload() { } function create() { circle = game.add.graphics(game.width / 2, game.height / 2); startTime = game.time.time; } function update() { var amplitude = 200; var period = 3; var t = (game.time.time - startTime) / 1000; // Temps écoulé depuis le début circle.clear(); circle.lineStyle(2, 0xFF0000, 1); circle.drawCircle(0, 0, 200 + amplitude * Math.sin(2 * Math.PI * t / period)); } Voirlerésultat RÉFLEXION Soitv = (a, b)unvecteur. LafonctionSv:ℝ2 → ℝ2définiepar Sv(x, y) = ⎡⎜⎣a2 − b22ab 2abb2 − a2⎤⎜⎦⎡⎜⎣x y ⎤⎜⎦ estappeléeréflexionparrapportauvecteurv(baséàl'origine). L'inversedeSvestSv(onappellecetypedefonctionune involution). AUTRESOPÉRATIONSGÉOMÉTRIQUES D'autrestransformationslinéairessonttrèspratiquesen infographie: Lesprojections; Lestransvections. LESCOULEURS Sionmanipulecorrectementlescoordonnéesdecouleur,onpeut obtenirdesanimationsintéressantes: var game = new Phaser.Game(800, 600, Phaser.AUTO, 'game', {preload: preload, create: create, update: update}); var circle; var startTime; function preload() { } function create() { circle = game.add.graphics(game.width / 2, game.height / 2); startTime = game.time.time; } function update() { var amplitude = 100; var period = 5; var t = (game.time.time - startTime) / 1000; // Temps écoulé depuis le début var red = 255 - amplitude * (1 + Math.sin(2 * Math.PI * t / period)); var blue = 255 - amplitude * (1 + Math.cos(2 * Math.PI * t / period)); var color = red << 16 | blue; circle.clear(); circle.lineStyle(10, color, 1); circle.drawCircle(0, 0, 200); } Voirlerésultat LATRANSPARENCE(1/3) Latransparence(alphablending)estégalementunenotiontrès utiliséeeninfographie; Typiquement,ils'agitd'unnombreentre0et1,maisonvoitaussi entre0et255,enparticulierlorsqu'ilestintégréausystèmeRGB OnparlealorsdesystèmedecouleursRGBA R:red G:green B:blue A:alpha Lorsqueα = 0,alorsl'objetesttransparent; Sionaplutôtα = 1(ouα = 255),alorsl'objetestopaque; Ellepermetdecréerfacilementdifférentseffets. LATRANSPARENCE(2/3) Onpeutsuperposerdesimages: var game = new Phaser.Game(800, 600, Phaser.AUTO, 'game', {preload: preload, create: create, update: update}); var circle1, circle2; function preload() { } function create() { var color1 = 0xFF0000; var color2 = 0x00FF00; circle1 = game.add.graphics(); circle1.lineStyle(0, color1); circle1.beginFill(color1); circle1.drawCircle(300, 300, 300); circle1.endFill(); circle2 = game.add.graphics(); circle2.lineStyle(0, color2); circle2.beginFill(color2, 0.5); // Transparence de 0.5 circle2.drawCircle(500, 300, 300); circle2.endFill(); } function update() { } Voirlerésultat LATRANSPARENCE(3/3) Onpeutfaireapparaître/disparaître(fadein/fadeout)desobjets: var game = new Phaser.Game(800, 600, Phaser.AUTO, 'game', {preload: preload, create: create, update: update}); var circle; var startTime; function preload() { } function create() { circle = game.add.graphics(game.width / 2, game.height / 2); circle.lineStyle(0, 0x00FFFF); circle.beginFill(0x00FFFF); circle.drawCircle(0, 0, 300); circle.endFill(); startTime = game.time.time; } function update() { var period = 3; var t = (game.time.time - startTime) / 1000; circle.alpha = 0.5 * (1 + Math.sin(2 * Math.PI * t / period)); } Voirlerésultat LEJEUSUPERHEXAGON SCÈNEDEBASE(1/2) Dansunpremiertemps,ondessineunhexagone; OnpeututiliserlafonctiondrawRegularPolygondel'exerciceprésenté plustôt; Nousallonségalementtracerleszonesinduitesparlessixcôtésde l'hexagone; Ceszonespeuventêtredessinéescommedestrapèzes(c'est-à-dire destrianglestronqués)quidépassentlecanvas; Afinderendrelecodeunpeupluspropre,nousallonscréerun objetScenequireprésentetouslesélémentsdelascène. SCÈNEDEBASE(2/2) // ---------------- // // Global variables // // ---------------- // var game = new Phaser.Game(800, 600, Phaser.AUTO, 'game', {preload: preload, create: create, update: update}); game.cx = game.width / 2; game.cy = game.height / 2; var scene; // ----- // // Utils // // ----- // Phaser.Graphics.prototype.drawRegularPolygon = function (x, y, n, r, a) { this.moveTo(x + r * Math.cos(a), y + r * Math.sin(a)); for (i = 1; i <= n; i++) { var a2 = i * 2 * Math.PI / n; var x2 = x + r * Math.cos(a + a2); var y2 = y + r * Math.sin(a + a2); this.lineTo(x2, y2); } } // ------------ // // Scene object // // ------------ // function Scene (graphics) { this.graphics = graphics; } Voirlerésultat Scene.prototype.drawZone = function (angle, color) { this.graphics.lineStyle(0, color, 1); this.graphics.moveTo(50 * Math.cos(angle), 50 * Math.sin(angle)); PARAMÉTRERLERAYON(1/2) Notrescènen'estévidemmentpasgénériqueactuellement; Nousallonsmaintenantdonnerlapossibilitédeparamétrerlerayon del'hexagonepourqu'ilpuisse"vibrer"; Ilestpossibled'utiliserunefonctionpériodique(parexemplela fonctionsin)aussicomplexequel'onveut; Nousallonscependantnouscontenterd'unefonctionaléatoire uniforme. PARAMÉTRERLERAYON(2/2) // ---------------- // // Global variables // // ---------------- // var game = new Phaser.Game(800, 600, Phaser.AUTO, 'game', {preload: preload, create: create, update: update}); game.cx = game.width / 2; game.cy = game.height / 2; var scene; // ----- // // Utils // // ----- // Phaser.Graphics.prototype.drawRegularPolygon = function (x, y, n, r, a) { this.moveTo(x + r * Math.cos(a), y + r * Math.sin(a)); for (i = 1; i <= n; i++) { var a2 = i * 2 * Math.PI / n; var x2 = x + r * Math.cos(a + a2); var y2 = y + r * Math.sin(a + a2); this.lineTo(x2, y2); } } // ------------ // // Scene object // // ------------ // function Scene (graphics, radius) { // NOUVEAU this.graphics = graphics; this.radius = radius; // NOUVEAU } Voirlerésultat Scene.prototype.drawZone = function (angle, color) { this.graphics.lineStyle(0, color, 1); this.graphics.moveTo(this.radius * Math.cos(angle), // NOUVEAU PARAMÉTRERL'ANGLE Àpartirducode,ilestfaciledepermettreàl'angledelascènede varierenfonctiondutemps: // ---------------- // // Global variables // // ---------------- // var game = new Phaser.Game(800, 600, Phaser.AUTO, 'game', {preload: preload, create: create, update: update}); game.cx = game.width / 2; game.cy = game.height / 2; var scene; // ----- // // Utils // // ----- // Phaser.Graphics.prototype.drawRegularPolygon = function (x, y, n, r, a) { this.moveTo(x + r * Math.cos(a), y + r * Math.sin(a)); for (i = 1; i <= n; i++) { var a2 = i * 2 * Math.PI / n; var x2 = x + r * Math.cos(a + a2); var y2 = y + r * Math.sin(a + a2); this.lineTo(x2, y2); } } // ------------ // // Scene object // // ------------ // function Scene (graphics, angle, radius) { // NOUVEAU this.graphics = graphics; this.angle = angle; // NOUVEAU Voirlerésultat PARAMÉTRERLESCOULEURS(1/2) Laparamétrisationdescouleursestrelativementsimpleaussi; OnpeututiliserlesystèmeHSVpourobtenirdestonsplusfoncés pourunecouleurdonnée: Phaser.Color.updateValue = function(color, valueChange) { var rgb = Phaser.Color.getRGB(color); var hsv = Phaser.Color.RGBtoHSV(rgb.r, rgb.g, rgb.b); hsv.v += valueChange; rgb = Phaser.Color.HSVtoRGB(hsv.h, hsv.s, hsv.v); console.log(Phaser.Color.toRGBA(rgb.r, rgb.g, rgb.b)); return rgb.r << 16 | rgb.g << 8 | rgb.b; } PARAMÉTRERLESCOULEURS(2/2) // ---------------- // // Global variables // // ---------------- // var game = new Phaser.Game(800, 600, Phaser.AUTO, 'game', {preload: preload, create: create, update: update}); game.cx = game.width / 2; game.cy = game.height / 2; var scene; var colorChangeTime; // ----- // // Utils // // ----- // Phaser.Graphics.prototype.drawRegularPolygon = function (x, y, n, r, a) { this.moveTo(x + r * Math.cos(a), y + r * Math.sin(a)); for (i = 1; i <= n; i++) { var a2 = i * 2 * Math.PI / n; var x2 = x + r * Math.cos(a + a2); var y2 = y + r * Math.sin(a + a2); this.lineTo(x2, y2); } } Voirlerésultat Phaser.Color.updateValue = function(color, valueChange) { var rgb = Phaser.Color.getRGB(color); var hsv = Phaser.Color.RGBtoHSV(rgb.r, rgb.g, rgb.b); hsv.v += valueChange; rgb = Phaser.Color.HSVtoRGB(hsv.h, hsv.s, hsv.v); console.log(Phaser.Color.toRGBA(rgb.r, rgb.g, rgb.b)); return rgb.r << 16 | rgb.g << 8 | rgb.b; } // ------------ // SPRITES Laressourcedebasegraphiqueeninfographie2Destappelée sprite; Ilconvientdedistinguerlesdifférentstypesdesprites: Cellesquin'influencentpaslaphysiquedujeu(imagedefond); Cellesquiinfluencentlaphysique,maissonimmobiles(mur, plafond,obstacle,etc.); Cellesquisontmobiles(personnage,animal,etc.). IMAGESDEFOND(1/3) EnPhaser,lechargementd'uneimagedefonds'effectueendeux étapes: Onprécharge(preload)l'image; Onajoutel'imageentantquesprite; Remarque:ilestpossible(etmêmefréquent)queplusieurssprites soientcrééesàpartird'unemêmeimage. IMAGESDEFOND(2/3) var game = new Phaser.Game(800, 600, Phaser.AUTO, 'game', {preload: preload, create: create, update: update}); function preload() { game.load.image('background', 'assets/background.png'); } function create() { game.add.sprite(0, 0, 'background'); } function update() { } Voirlerésultat IMAGESDEFOND(3/3) Noteimportante: Pourdesraisonsdesécurité,lechargementd'imagesne fonctionnerapassilefichierindex.htmlestludefaçonlocale; Ceciestdûaufaitqu'onnepeutpaspermettreàJavaScript d'accéderàn'importequelfichiersurnotremachine; Danscecas,ilfautcréerunserveurlocal; Lapagesuivantecontientplusd'informationàcesujet. SPRITESIMMOBILES(1/2) Souvent,ilestimportantquecertainsobjetsd'unescènesoient considérésdefaçondistincte; Danslecasd'objetsimmobiles,lesdeuxraisonssuivantessontles plusfréquentes: Onsouhaitequecetobjetapparaisseenplusieursexemplaires; Onsouhaitequ'ilsoitconsidérécommeunobstacle; SPRITESIMMOBILES(2/2) var game = new Phaser.Game(800, 600, Phaser.AUTO, 'game', {preload: preload, create: create, update: update}); function preload() { game.load.image('background', 'assets/background.png'); game.load.image('rock', 'assets/rock.png'); } function create() { game.add.sprite(0, 0, 'background'); game.add.sprite(400,500, 'rock'); game.add.sprite(400,450, 'rock'); game.add.sprite(400,400, 'rock'); } function update() { } Voirlerésultat SPRITESMOBILES Enplusd'avoirdesobjetsprovenantd'uneimage,ilestévidemment intéressantqueceux-cipuissentsedéplacer: var game = new Phaser.Game(800, 600, Phaser.AUTO, 'game', {preload: preload, create: create, update: update}); function preload() { game.load.image('background', 'assets/background.png'); game.load.image('rock', 'assets/rock.png'); game.load.image('cloud', 'assets/cloud.png'); } function create() { game.add.sprite(0, 0, 'background'); game.add.sprite(400,500, 'rock'); game.add.sprite(400,450, 'rock'); game.add.sprite(400,400, 'rock'); var cloud = game.add.sprite(0,200, 'cloud'); game.physics.startSystem(Phaser.Physics.ARCADE); game.physics.arcade.enable(cloud); // On démarre le système physique cloud.enableBody = true; // Le nuage est géré dans le système cloud.body.velocity.x = 200; // La vitesse horizontale du nuage cloud.body.bounce.x = 1.0; // La collision est parfaitement élastique cloud.body.collideWorldBounds = true; // Collision avec les bords du canvas } function update() { } Voirlerésultat SPRITESANIMÉES(1/3) Lorsqu'unobjetévoluedansunescène,onobtientdeseffetsplus réalistessisonapparenceévolueégalement; Onpeutbienentenduutiliserdestransformationsgéométriqueset décomposerl'imageenplusieursimagesmobiles; Cependant,cetteapprochen'estpaspratique,carilfautdécrireen détailstouteslestransformations,cequidevientrapidementtrès lourd; Unefaçonsimpleetpopulaireconsisteàutiliserdessprites animées; Autrementdit,ondonneunelisted'imagesplutôtqu'uneseule image. SPRITESANIMÉES(2/3) Unelisted'imageestappeléenanglaisspritesheet: Unespritesheetd'unoiseau(1200 × 1256) Ilestpréférablequechaquepetiteimagesoitdemêmetaille Dansl'exemple,chaquespriteestdedimension240 × 314. SPRITESANIMÉES(3/3) var game = new Phaser.Game(800, 600, Phaser.AUTO, 'game', {preload: preload, create: create, update: update}); var bird; function preload() { game.load.image('background', 'assets/background.png'); game.load.image('rock', 'assets/rock.png'); game.load.image('cloud', 'assets/cloud.png'); game.load.spritesheet('bird', 'assets/bird.png', 240, 314); } function create() { game.add.sprite(0, 0, 'background'); game.add.sprite(400,500, 'rock'); game.add.sprite(400,450, 'rock'); game.add.sprite(400,400, 'rock'); var cloud = game.add.sprite(0,200, 'cloud'); bird = game.add.sprite(400, 200, 'bird'); bird.scale.setTo(0.4, 0.4); game.physics.startSystem(Phaser.Physics.ARCADE); game.physics.arcade.enable(cloud); game.physics.arcade.enable(bird); cloud.enableBody = true; cloud.body.velocity.x = 200; cloud.body.bounce.x = 1.0; cloud.body.collideWorldBounds = true; bird.animations.add('fly'); bird.animations.play('fly', 30, true); // 50 IPS, boucle infinie = vrai bird.enableBody = true; bird.body.velocity.x = -100; } Voirlerésultat function update() { PHYSIQUE Danslaplupartdesapplicationsgraphiquesavecanimations,on utiliselesprincipesdebasedelaphysiquemécanique; Ceux-cisontgérésparcequ'onappelleunmoteurphysique; Lesmoteursphysiquesgèrentlestroisvariablesdebased'un systèmemécanique: Laposition; Lavitesse; L'accélération. Ilspeuventégalementprendreencharged'autresphénomènes: lescollisions; lesrebonds; lagravité; lafriction; etc. LAPHYSIQUEDANSPHASER Phaserproposetroismoteursphysiquespardéfaut: ArcadePhysics:pourgérerlescollisionsavoirboîteenglobantes rectangulaires(sansrotation); NinjaPhysics:pourgérerlestuilespluscomplexes,lespentes,les rotations,etc. P2:pourmodéliserdesphénomènesphysiquespluscomplexes, aveccontraintes,ressorts,polygones,etc. Ilexisteégalementuneinterface(malheureusementnilibreni gratuite)avecBox2D,quioffrebeaucoupplusdefonctionnalités (malgréqueBox2Dsoitopen-source). LEMOUVEMENT Lessixquantitésfondamentalesdumouvementd'uncorpssont: Laposition; Lavitesse; L'accélération. L'angle; Lavitesseangulaire; L'accélérationangulaire. DansilssontprisenchargeparlesystèmephysiqueArcade. LAGRAVITÉETLESCOLLISIONS DansPhaser,lagestiondelagravitéesttrèssimple; Ilsuffitdemodifierlesattributs.body.gravity.xet.body.gravity.yde l'objet; Enparticulier,lagravitépeutêtredifférentesurchacundesobjets; Aussi,enplusdedétecterlescollisions,onpeutfacilementindiquer letypedecollision(élastique,partiellementélastiqueounon); Ilsuffitdemodifierlesattributs.body.bounce.xet.body.bounce.y(où1 indiqueunecollisionélastiqueet0unecollisionnonélastique); Voirl'exemplesurPhaser AUTRESPHÉNOMÈNESPLUS COMPLEXES Phaseroffreplusieursexemplessurdesphénomènespluscomplexes: Accélérationverslejoueur Avecdesressorts Mouvementd'unechaîne