Home
Mapping d'animation Flash interactive sur un objet 3D
Alexandre Labedade - 2004



Dans ce tutorial, vous allez voir comment utiliser une animation Flash en tant que texture animée sur un objet 3D, tout en conservant les interactions avec la souris.

Cliquez ici pour voir les news

Utiliser une animation Flash en tant que texture animée est relativement simple : un acteur Flash possède une propriété posterFrame qui permet de récupérer une copie de son image...
Si votre animation est un simple 'dessin animé', cela fonctionnera sans problème, vu que rien n'est créé dynamiquement.

Le seul problème avec cette propriété, c'est qu'elle ne supporte pas les évènements action scripts et tout ce qui est création dynamique de movieclips... c'est un peu comme si elle capturait l'image de la scène de l'anim flash lorsqu'on navigue dedans depuis l'interface de Flash : simplement les éléments créés 'en dur' sont visibles.

Un exemple avec posterFrame :

Ce script est un comportement à assigner sur un sprite Flash pour faire changer la propriété poster frame de son acteur, et pour redéfinir en même temps la texture d'un acteur 3D :

property spritenum

on enterframe me

   MyFrame = sprite(spritenum).frame
   MyMember = sprite(spritenum).member
   
   MyFrame = MyFrame +
1
   
   if Myframe > MyMember.framecount then
      MyFrame = 1
   end if
   
   MyMember.
posterFrame = MyFrame
   
   member("3D").texture(1).image = MyMember.image
   
end

Mais bon... ce n'est pas ça qui nous interesse ici.

L'autre façon de créer une "texture Flash" sur un objet 3D est un peu plus complexe :

L'image actuelle du sprite Flash va etre capturée en temps réel directement depuis la scène, pour ensuite etre utilisée en tant que texture.
En procédant ainsi, on conserve le résultat visuel de l'execution de tous les scripts contenus dans l'animation Flash, ce qui n'est pas le cas avec posterFrame.

Le premier problème que l'on va donc rencontrer, c'est de capturer le sprite Flash sur la scene, sans que celui-ci ne soit visible... et ce problème va en engendrer un autre : il ne sera pas possible d'utiliser cette méthode en mode projecteur fenetré (à moins de trouver une ruse que je ne connais pas encore), mais il sera possible, en bidouillant un peu, de la faire fonctionner en plein ecran.
Pour ce qui est du web, ça dépendra du navigateur utilisé pour visionner l'animation shockwave, avec Internet Explorer, il n'y a pas de soucis... avec les autres c'est une autre histoire.

"Mais quel est ce problème ?" me direz-vous...
Et bien le problème est que director ne peut pas capturer l'image de la scene en dehors de sa taille déterminée au moment de la compilation. Or, pour une raison qui sera expliquée plus bas, nous allons avoir besoin de "marges" tout autour du sprites 3D.

Par exemple, vous avez une scène qui a les dimensions 640x480, dans ce cas, si vous capturez une zone se trouvant au delà de 640 horizontalement ou 480 verticalement vous obtiendrez uniquement du blanc sur cette partie.

La ruse consiste donc à créer une animation plus grande, mais de n'en afficher qu'une partie (celle qui nous interesse) à l'execution finale.
Donc pour une execution depuis un navigateur internet il faudra utiliser du DHTML pour effectuer un recadrage sur l'animation Shockwave.

1) Capturer l'image du sprite Flash sur la scène sans que celui-ci ne soit visible :

Ceci est relativement simple : il suffit de placer le sprite flash sur la scène en désactivant sa propriété Direct to Stage



et de placer par dessus le sprite 3D en activant la propriété Direct to Stage de l'acteur correspondant.


Cette propriété permet d'inclure ou non le sprite dans le moteur de rendu graphique de Director, si DTS (= Direct to Stage) est inactif, le sprite sera considéré comme n'importe quel autre sprite (ce qui peut provoquer des ralentissements dans son animation), sinon il sera affiché en dernier, par dessus tous les autres sprites et n'apparaitra pas dans la propriété image de la scene.

De plus, le fait d'activer la propriété DTS permet d'obtenir un affichage plus fluide sur l'acteur concerné, dans notre cas, l'acteur 3D.

Donc dans ce cas, on pourrait placer le sprite Flash sur la piste numéro 2 et le sprite 3D au dessus, sur la piste numéro 3, celui-ci étant en DTS, il apparaitra de toute façon au dessus.

(je n'utilise pas le sprite 1 par habitude... au cas où j'aurais besoin d'insérer un fond par la suite...)

Le problème dans ce cas, c'est que l'interaction entre la souris et le sprite Flash sera perdu car "physiquement" le sprite 3D est au dessus dans la liste des sprites.

Il faut donc placer le sprite Flash par dessus le sprite 3D dans le scenario, afin de conserver les interactions souris sur celui-ci, et comme le sprite 3D est en DTS, il masquera quand même le sprite Flash, donc on est content.



Voici un script d'animation à ajouter pour effectuer un premier test :
Il faut bien sur avoir un objet 3d qui utilise la texture n°1, et qui dispose de coordonnées de projection UV
Par defaut les primitives (sphère, cube...) ont ces coordonnées générées, donc pas d'ennui à ce niveau, par contre, si vous importez un objet 3D depuis une application externe, il faudra penser à générer ces coordonnées correctement.

on EnterFrame
   
   
-- sprite 2 : 3D
   sprite3D = 2
   
   
-- sprite 3 : Flash
   spriteFlash = 3
   
   
   TextureResolution =
512
   
   
-- Redimensionnement du sprite flash en fonction de la resolution
   sprite(spriteFlash).width=TextureResolution
   sprite(spriteFlash).height=TextureResolution
   sprite(spriteFlash).member.defaultrect=rect(0,0,TextureResolution,TextureResolution)
   
   
   
-- creation de l'image
   Img=image(TextureResolution,TextureResolution,24)
   
   
   
-- capture de l'image actuelle du sprite flash
   Img.copypixels((the stage).image,Img.rect,sprite(spriteFlash).rect)
   
   
   
-- remplacement de la texture par la nouvelle image capturée
   sprite(sprite3D).member.texture[1].image=Img
end

on exitframe
   go
to the frame
end



2) Les évènements souris :

Maintenant que l'on dispose d'une animation Flash qui est correctement affichée sur notre objet 3D, il faut pouvoir interagir directement avec en promenant le pointeur de la souris directement sur l'objet 3D quelle que soit sa forme / position / orientation / taille, et non sur le le sprite flash qui lui ne sera de toute façon pas visible au final.

Comme l'objet 3D peut avoir n'importe quelle forme ou orientation, on ne peut pas utiliser le repère orthonormé de la scène pour récupérer les coordonnées de la souris
Il va donc falloir utiliser les coordonnées UV du pointeur sur l'objet qu'il est en train de survoler, pour obtenir la position correspondante en 2D sur la surface de l'objet, et ainsi replacer le sprite flash juste en dessous afin que la zone survolée corresponde.

En plus clair : lorsque le pointeur se balade sur la surface de l'objet 3D, le sprite flash est déplacé de façon "inverse" afin que la zone survolée sur l'objet 3D coïncide avec celle du sprite Flash

Voici le script de frame précédent avec cette modification :
J'utilise la fonction ModelsUnderLoc pour détecter l'objet se trouvant sous le pointeur, ceci va donner une liste d'info sur le point d'impact trouvé ou non, dont l'identifiant de la Face percutée ainsi que les coordonnées UV sur cette face.

Le problème ici, c'est que les coordonnées UV retournées ne peuvent pas etre utilisées telles quelles et doivent être converties afin d'être utilisables correctement en 2D.
Pour cela, j'utilise la fonction mMapPointToTexture ecrite par James Newton qui permet de corriger ces coordonnées, cette fonction est à placer dans un script d'animation afin de la rendre accessible de n'importe quel autre script.

on EnterFrame
   
   
-- sprite 2 : 3D
   sprite3D = 2
   
   
-- sprite 3 : Flash
   spriteFlash = 3
   
   
   TextureResolution =
512
   
   
-- Redimensionnement du sprite flash en fonction de la resolution
   sprite(spriteFlash).width=TextureResolution
   sprite(spriteFlash).height=TextureResolution
   sprite(spriteFlash).member.defaultrect=rect(0,0,TextureResolution,TextureResolution)
   
   
   
-- creation de l'image
   Img=image(TextureResolution,TextureResolution,24)
   
   
   
-- capture de l'image actuelle du sprite flash
   Img.copypixels((the stage).image,Img.rect,sprite(spriteFlash).rect)
   
   
   
-- remplacement de la texture par la nouvelle image capturée
   sprite(sprite3D).member.texture[1].image=Img
   
   
   
   
   
-- Récupération du point d'impact du curseur sur l'objet 3D
   pt = the mouseLoc - point(sprite(sprite3D).left, sprite(sprite3D).top)
   m =
sprite(sprite3D).camera.modelsUnderLoc(pt, 1, #detailed)
   
   
   
-- Si un point d'impact est trouvé :
   if m<>[] then
      
      
-- récupération des coordonnées d'uv mapping au point d'impact
      uv=mMapPointToTexture(m[1],point(sprite(spriteFlash).width,sprite(spriteFlash).height))
      
      
-- si les coordonnées sont bonnes, on replace l'acteur flash
      -- pour que la zone survolée sur l'objet 3D coincide avec celle
      -- du sprite, on peut ainsi cliquer sur les boutons
      
      if not voidp(uv) then
         
         
-- repositionnement du sprite Flash sous la souris
         sprite(spriteFlash).loc = (the mouseloc - uv) + sprite(spriteFlash).member.regpoint
         
         
      end if
      
   else
      -- Sinon, le sprite flash est placé en haut à gauche, hors de portée du pointeur
      sprite(spriteFlash).loc = sprite(spriteFlash).member.regpoint
   end if
   
   
end

on exitframe
   go
to the frame
end





-- MAP POINT TO TEXTURE --
--
-- © October 2001, James Newton <newton@planetb.fr>


on mMapPointToTexture(iSectList, anImage) ----------------------------
   -- RETURNS the point in the Bitmap member used by the texture
   -- defined implicitly in <iSectList>
   --
   -- INPUT:
   -- <iSectList> should be a property list of the format:
   --    [#model:             model("model name"),
   --      #distance:         <float>,
   --      #isectPosition: <vector>,
   --      #isectNormal:    <vector>,
   --      #meshID:            <integer>,
   --      #faceID:            <integer>,
   --      #vertices:       [<vector>, <vector>, <vector>],
   --      #uvCoord:         [#u: <float>, #v: <float>]]
   --
   -- <anImage> can be VOID, an image object, or any type of object
   --   with width and height properties (eg: propList)
   --
   -- When you use aCamera.modelsUnderLoc(aLoc, #detailed) or
   -- aMember.modelsUnderRay(aPoint, aVector, #detailed), the result is
   -- a linear list of lists with this format.
   ---------------------------------------------------------------------
   
   tModel      = iSectList.
model
   tResource = tModel.resource
   
   isMesh = (tResource.
type = #mesh)
   
   if not isMesh then
      -- We need to use the mesh deform modifier to get texture data
      if not((tModel.modifier).getPos(#meshdeform))then
         -- Add mesh deform modifier... but remember to remove it later
         tModel.addModifier(#meshdeform)
         isModified =
TRUE
      end if
   end if
   
   tFace         = iSectList.
faceID
   tUVCoord    = iSectList.uvCoord
   
   
-- Determine how the iSect data maps to this particular face
   if isMesh then
      tCoordList = tResource.textureCoordinateList
      tFaceList   = tResource.face[tFace].textureCoordinates
      
   else
      tCoordList = tModel.meshdeform.mesh[iSectList.meshID].textureCoordinateList
      tFaceList   = tModel.meshdeform.mesh[iSectList.meshID].face[tFace]
      if isModified then
         -- Remove the modifier now that it has done its job
         tModel.removeModifier(#meshdeform)
      end if
   end if
   
   case anImage.ilk of
      #void:
         tWidth =
1
         tHeight = 1
         
      #point,#list:
         tWidth = anImage[
1]
         tHeight = anImage[
2]
         
      #image,#rect,#propList:
         tWidth = anImage.
width
         tHeight = anImage.height
   end case

   -- tCoordList will be a list of lists, each containing two floating-
   -- point numbers between 0.0 and 1.0.   The first number defines the
   -- relative horizontal position of a point in the texture, the
   -- second number defines the relative vertical point.   The origin
   -- point [0.0, 0.0] is in the bottom left hand corner.
   --
   -- tFaceList will be a list with three integer values [a, b, c]
   -- These values determine which entry in tCoordList is used by the
   -- chosen face.   The first entry <a> defines the origin.   The u
   -- value increases from <a> to <b>.   Any point on the line between
   -- <a> and <b> will have a v value of zero.   The v value increases
   -- from <a> to <c>.   Any point on the line between <a> and <b> will
   -- have a v value of zero.
   
   
-- Calculate the position of the points <a>, <b> and <c> within the
   -- Bitmap member.
   
   tLocA = tCoordList[tFaceList[
1]] -- [<float>, <float>]
   tLocA = point(tLocA[1] * tWidth, (1 - tLocA[2]) * tHeight)
   
   tLocB = tCoordList[tFaceList[
2]] -- [<float>, <float>]
   tLocB = point(tLocB[1] * tWidth, (1 - tLocB[2]) * tHeight)
   
   tLocC = tCoordList[tFaceList[
3]] -- [<float>, <float>]
   tLocC = point(tLocC[1] * tWidth, (1 - tLocC[2]) * tHeight)
   
   tUVector = (tLocB - tLocA) * tUVCoord.u
-- actually a 2D point...
   tVVector = (tLocC - tLocA) * tUVCoord.v -- ... rather than a vector
   
   
-- Start from the origin <a>, move first in the u direction then in
   -- the v direction to end up at the point in the texture
   
   return tLocA + tUVector + tVVector
end mMapPointToTexture   



Et voilà... vous avez maintenant une animation Flash mappée sur un objet 3D, et les interactions avec l'animation Flash son toujours actives.

Comme le sprite Flash bouge sans arret en fonction de la position de la souris, il arrive (souvent) que celui-ci passe en dehors de la zone visible de la scene, et donc il possède parfois des zones blanches... ce qui est plutot dérangeant.

Pour corriger ce problème, voici comment procéder :
a) Déterminez quelle va etre la taille maximale de la texture que vous allez utiliser. 512 et déjà une bonne taille, au dessus, ça commence à vraiment ramer
b) Multipliez cette taille par 3 et utilisez cette valeur comme dimensions pour votre scene : 512 * 3 = 1536
c) Placez le sprite 3D exactement au centre de la scene et donnez lui les dimension 512x512, vous aurez ainsi une marge de 512 pixels tout autour de votre sprite 3D



Bien entendu, vous pouvez donner les dimensions que vous souhaitez à votre sprite 3D, dans ce cas, pour calculer les dimensions de la scene, il faudra faire le calcul suivant :
Largeur de la scene = (Largeur maxi de la texture Flash * 2) + largeur du sprite 3D
Hauteur de la scene = (Hauteur maxi de la texture Flash * 2) + hauteur du sprite 3D

Mais continuons avec notre exemple en 512x512, pour faire plus simple.

Publiez maintenant votre animation en Shockwave, et éditez la page html générée dans un editeur texte quelconque, et ajoutez cette ligne juste avant le début du tag <OBJECT> pour créer un nouveau calque DHTML :

<div style="position:absolute; width:1536; height:1536; top:-512px; left:-512px; clip: rect(512px, 1024px, 1024px, 512px); ">

et pensez bien à fermer ce calque juste apres la fin du tage <OBJECT> en tapant :</div>

Vous devriez alors avoir quelque chose comme ça :

<div id=disp style="position:absolute; width:1536; height:1536; top:-512px; left:-512px; clip: rect(512px, 1024px, 1024px, 512px); ">

<OBJECT classid="clsid:166B1BCA-3F9C-11CF-8075-444553540000"
codebase="http://download.macromedia.com/pub/shockwave/cabs/director/sw.cab#version=8,5,1,0"
ID=tutorial WIDTH=1536 HEIGHT=1536>
<param name=src value="tutorial.dcr">
<param name=swStretchStyle value=fill>
<param name=swRemote value="swSaveEnabled='false' swVolume='false' swRestart='false' swPausePlay='false' swFastForward='false' swContextMenu='false' ">
<PARAM NAME=bgColor VALUE=#696882>
<EMBED SRC="tutorial.dcr" bgColor=#696882 WIDTH=1536 HEIGHT=1536 swRemote="swSaveEnabled='false' swVolume='false' swRestart='false' swPausePlay='false' swFastForward='false' swContextMenu='false' " swStretchStyle=fill
TYPE="application/x-director" PLUGINSPAGE="http://www.macromedia.com/shockwave/download/">
</EMBED>
</OBJECT>

</div>


Ce qui nous interesse ici, c'est le Style appliqué au calque :
style="position:absolute; width:1536; height:1536; top:-512px; left:-512px; clip: rect(512px, 1024px, 1024px, 512px); "

Explication de chaque attribut :

position:absolute; Ceci permet de placer le calque de façon absolue sur la page html (par rapport à son coin supérieur gauche), indépendamment de tout le reste, et surtout cela permet de rendre utilisable l'attribut CLIP qui ne fonctionne qu'en mode absolute
width:1536; height:1536; Ces attributs permettent de définir les dimensions du calque, mais ceci est facultatif, car le calque se redimensionne automatiquement par rapport à son contenu
top:-512px; left:-512px; Ici on détermine la position du calque sur la page. Comme la zone qui nous interesse est le sprite 3D et que le coin supérieur gauche de celui-ci se trouve aux coordonnées 512,512, on décale d'autant le calque dans la direction inverse, pour que finallement le sprite 3d soit visible aux coordonnées 0,0 de la page HTML
clip: rect(512px, 1024px, 1024px, 512px); Et enfin, le plus beau, l'attribut CLIP, qui permet de recadrer la zone visible du calque, et donc qui va nous permettre de masquer les marges tout autour de notre sprite 3D.
Maintenant, peu importe que les marges soient visibles ou non, la capture de l'image de la scène ne sera pas affectée par le recadrage en DHTML, donc les zones blanches n'apparaitront pas.

l'ordre des coordonnées pour le recadrage est le suivant :
clip: rect(top right bottom left)

Ici le coin supérieur gauche du sprite 3d se trouve au coordonnées 512,512, donc top et left sont à 512
Ce sprite fait 512 pixels de haut et de large, donc le bottom et le right sont à 1024.

En gros, ça fonctionne de la même façon que pour un rect sous director, sauf que l'ordre des paramètres n'est pas le même.


bien sur, il vous sera possible de placer ce calque ailleurs qu'au point 0,0 : il suffira de changer ses attributs top et left, en n'oubliant pas de soustraire à chaque fois la dimension des marges.

Une autre méthode peut être utilisée pour recadrer l'animation shockwave : ouvrez la page dans une popup aux dimension 512x512 et spécifiez simplement les coordonnées top et left dans le style du calque.


Cliquez ici pour voir un demonstration en ligne : Exemple (6ko)
Le fichier source est disponible ici : Source (13ko)

Ici, un autre exemple avec plus d'animations Flash : Gros Test (attention, c'est plus gros : 3Mo)


NEWS :
Ce qui reste à faire :

- Trouver une solution pour ce problème de recadrage afin que çela puisse marcher en mode projecteur.
Il existe bien un moyen, mais ceci necessite d'utiliser 3 fois la fonction updatestage à chaque frame, et donc la fluidité de l'animation est divisée par 3... donc c'est pas bon

- Un petit truc que je n'arrive pas à rectifier : lorsque le cube se trouve bien en face de la camera, la texture pixelise, alors que si on l'incline légèrement, elle devient lissée grace au mip mapping... mais je ne comprend pas pourquoi ça pixelise, alors qu'elle devrait etre toujours lissée.
(le 11/03/2004) Solution trouvée !
Pour ne plus avoir ce problème d'aliasing sur la texture, il faut utiliser la valeur #LowFiltered sur la qualité de la texture.
Dans ce cas, il n'y aura plus de mipmapping, mais la texture sera bien propre lorsque les polygones seront face à la camera.

J'ais mis le fichier source à jour avec cette nouvelle option.