WPF & DirectX 10 – Deuxième partie –Génération et récupération dans WPF 3D d’un maillage généré à l’aide de DX10.
Auteur : Guillaume Randon (Microsoft Services) - http://blogs.msdn.com/guillaumerandon/default.aspx
Cet article est le deuxième d’une série de documents courts décrivant différents aspects de l’utilisation possible de DirectX 10 au sein d’une application WPF.
Le premier article décrit le mécanisme utilisé pour intégrer une zone d’affichage gérée par DirectX 10 au sein d’une application WPF et se trouve à l’adresse suivante : http://msdn2.microsoft.com/fr-fr/library/54bf6e00-b066-4fe9-bdc9-279ebb49b9f4.aspx
Le code d’exemple est quant à lui téléchargeable à partir de la page suivante : http://blogs.msdn.com/guillaumerandon
L’article qui suit décrit l’utilisation qui est faite du ‘geometry shader’ et de la fonctionnalité ‘StreamOut’ dans cette application d’exemple. Pour une description plus détaillé de ces deux nouvelles fonctionnalités introduites par DirectX 10, le lecteur peut se reporter aux articles suivants :
Dans l’exemple de code fourni, le ‘geometry shader’ est utilisé pour générer un maillage représentant un ensemble de ‘metaballs’ suivant la méthode décrite à cette adresse :
http://developer.nvidia.com/object/dx10-practical-metaballs.html. Le résultat de ce calcul est ensuite récupéré dans la mémoire du GPU, copié en mémoire système puis rendu accessible à notre code managé qui utilise ces données pour afficher le maillage produit en utilisant WPF3D.
L’image ci-dessous est une capture d’écran de l’exemple de code. Le maillage en vert représente six ‘metaballs’ et est affiché à l’aide de DirectX 10. Les deux maillages en rouge correspondent aux mêmes données affichées à l’aide de WPF.
.png)
Iso-surfaces et Metaballs
Avant d’aborder la partie purement technique, attardons nous un instant sur la surface pour laquelle nous souhaitons générer un maillage à l’aide de DirectX 10. Etant donnée une série de points de notre espace à trois dimensions Ci et une série de réels Ri, par metaballs on entend généralement l’iso-surface de notre espace 3D définie comme étant l’ensemble des points P pour lesquels l’égalité suivante est vérifiée : .gif)
Si l’on fait un parallèle avec l’électromagnétisme, nos metaballs peuvent être assimilées à de minuscules aimants créant un champ de potentiel sur notre espace 3D. Les points de l’espace ayant un potentiel égal à 1 constituent notre surface.
Pour avoir une meilleure idée de l’aspect que peuvent avoir ces surfaces commençons par visualiser la courbe du plan définie par les points de celui-ci vérifiant l’équation ci-dessus. Le programme F# ci-dessous va nous permettre de générer rapidement de telles courbes (pour plus d’information sur F# : http://research.microsoft.com/fsharp/fsharp.aspx ) :
#light
open System.Drawing
open System.IO
open System.Windows.Forms
type Metaball = { Center : Point ; Radius : float }
let ourMetaballs = [ {Center = new Point(100, 100) ; Radius = 50.0} ; {Center = new Point(160, 160); Radius = 30.0} ]
let width = 256
let height = 256
let contrib (Point:Point) (metaball:Metaball) =
let center = metaball.Center in
let diffx = Point.X - center.X in
let diffy = Point.Y - center.Y in
let distcube = (diffx*diffx+diffy*diffy) in
(metaball.Radius*metaball.Radius)/ float(distcube)
let pot Point metaballs =
let acc = ref 0.0 in
List.iter (fun metaball -> acc := !acc + contrib Point metaball) metaballs
!acc
let pixelColor Point =
let pixelPot = pot Point ourMetaballs in
match pixelPot with
| pixelPot when pixelPot >= 1.0 && pixelPot < 1.05 -> Color.Black
| _ -> Color.White
let btmap =
let tempBtMap = new Bitmap(width, height, Imaging.PixelFormat.Format32bppArgb) in
for i = 0 to width-1 do
for j = 0 to height-1 do
tempBtMap.SetPixel(i, j, pixelColor (Point(i,j)))
tempBtMap
let form =
let tmp = new form()
tmp.Paint.Add
(fun e ->
e.Graphics.DrawImage(btmap, 0, 0))
tmp
Application.Run(form)
Voici l’image générée à l’aide du programme ci-dessus :
.png)
Dans le programme ci-dessus on a évalué la fonction suivante pour chaque pixel de notre bitmap : .gif)
Lorsque cette fonction est pratiquement égale à 1, le pixel est affiché en noir, ce qui donne une approximation de la courbe que nous souhaitions visualiser.
Il est bien entendu possible de changer rapidement le nombre d’éléments de notre liste ourMetaballs pour obtenir les courbes correspondants à des listes différentes de metaballs. Par exemple avec :
let ourMetaballs = [ {Center = new Point(100, 100) ; Radius = 50.0} ; {Center = new Point(160, 160); Radius = 30.0} ; {Center = new Point(140, 200); Radius = 20.0} ]
On obtient l’image suivante :
.png)
Réalisation technique
Maintenant que nous avons une idée plus précise de l’objet pour lequel nous souhaitons générer et afficher un maillage passons à la partie plus technique.
Quelques rappels sur DirectX 10
Mon propos n’est pas ici de redonner une description détaillée de DirectX 10 mais de faire de brefs rappels pour mieux resituer dans leur contexte les fonctionnalités utilisées dans notre exemple. Pour une description plus approfondie de l’API graphique DirectX 10 on pourra se reporter aux liens ci-dessous (en français) :
Puis consulter l’excellent article en anglais de David Blythe : http:// download.microsoft.com/download/f/2/d/f2d5ee2c-b7ba-4cd0-9686-b6508b5479a1/Direct3D10_web.pdf
Le pipeline graphique
Il est usuel de représenter les différentes fonctionnalités de nos cartes graphiques et leurs positionnements les unes par rapport aux autres sous forme d’un ‘pipeline’. En l’occurrence pour DirectX 10, voici un schéma qui représente bien ce avec quoi il est possible d’interagir par le biais de cette API :
.jpg)
On voit en particulier apparaitre dans le schéma ci-dessus le fait que nos cartes graphiques sont capables d’exécuter différents type de programmes appelés ‘shaders’. Dans notre exemple nous allons utiliser les trois types de programmes disponibles, à savoir ‘vertex’, ‘geometry’ et ‘pixel shader’ ainsi que la fonctionnalité dite de ‘Stream Out’ qui apparait entre l’étape d’exécution des ‘geometry shaders’ et l’étape de rastérisation. Dans le cadre de ce document nous allons surtout nous attacher à décrire la façon dont les ‘geometry shader’ et la fonctionnalité de ‘Stream Out’ sont utilisés dans notre exemple, aussi les deux rappels ci-dessous portent plus spécialement sur ces points.
Geometry Shader
Il s’agit d’un nouveau type de programme exécutable par les cartes graphiques supportant DirectX 10.
Pour fixer les idées, un ‘vertex shader’ permet d’effectuer des opérations sur les sommets des primitives graphiques fournies à notre pipeline. Le ‘pixel shader‘ permet d’effectuer des opérations sur l’ensemble des pixels issus de l’étape de rastérisation. Le ‘geometry shader’ permet quant à lui de manipuler par groupe les sommets de nos primitives 3D. Autant le ‘vertex shader’ ne donne accès lors de chaque exécution qu’à un sommet pour lequel on ne dispose d’aucune information concernant les autres sommets auxquels il peut être connectés, autant lors de l’exécution d’un ‘geometry shader’, l’ensemble des sommets utilisés pour définir une primitive graphique nous est fourni d’un coup ainsi que des informations sur d’autres sommets auxquels ils peuvent être reliés.
Par ailleurs ce type de programme offre la propriété intéressante de pouvoir générer de nouvelles primitives. Jusqu’à présent on ne pouvait ‘que’ Jmodifier des données fournies au pipeline, il est maintenant possible d’émettre (ou de supprimer d’ailleurs) des données.
Stream Out
Il s’agit aussi d’une nouvelle fonctionnalité introduite avec DirectX 10. Elle permet de récupérer le résultat d’opérations effectuées par notre pipeline graphique sans exécuter les dernières étapes de celui-ci, à savoir l’étape de rastérisation et l’exécution des ‘pixels shader’. Au niveau hardware ces dernières étapes sollicitent peu ou prou les mêmes parties du GPU que pour l’exécution des vertex et géométrie shader. Aussi, si seul le résultat de ceux-ci présente pour nous un intérêt, il est intéressant de ne pas avoir à exécuter l’ensemble du pipeline graphique et ainsi de concentrer notre puissance de calcul sur les étapes qui nous intéressent. Il serait tout aussi possible de récupérer des données résultant de l’exécution de l’ensemble de notre pipeline, mais en l’occurrence, cette fonctionnalité nous permet de les récupérer avant d’utiliser le GPU inutilement pour des étapes qui ne présentent pas d’intérêt pour nous. Il est par ailleurs possible de réinjecter dans le pipeline ce résultat sans intervention du CPU si plusieurs passes de traitements s’avéraient nécessaires. Cette dernière possibilité n’est par contre pas utilisée dans notre cas de figure.
Encore une fois, pour de plus amples détails sur ces points, vous pouvez vous reporter aux pages suivantes :
Et en anglais à celle-ci : http://download.microsoft.com/download/f/2/d/f2d5ee2c-b7ba-4cd0-9686-b6508b5479a1/ Direct3D10_web.pdf
Utilisation dans notre application d’exemple
Dans notre cas le ‘geometry shader’ va être utilisé pour générer le maillage d’un ensemble de ‘metaballs’ dont nous n’avons communiqué à notre GPU que les centres et les rayons. L’algorithme utilisé est celui des ‘marching tetrahedra’, il est décrit dans la présentation suivante : http://developer.nvidia.com/object/dx10-practical-metaballs.html.
Pour passer à une représentation 3D en lieu et place des pixels affichés en noir par le petit programme F# décrit dans la partie traitant des metaballs, il va nous falloir générer des triangles proches de notre iso surface et qui en suivent autant que possible le contour.
Pour donner une idée du principe décrit dans la présentation de nVidia qui est utilisé ici, une portion de notre espace 3D est remplie d’une grille de tétraèdres. La valeur du champ créé par nos ‘metaballs’ est évaluée pour tous les sommets de notre grille. Ensuite en fonction des valeurs du champ en ces points on estime la façon dont la surface traverse notre tétraèdre : les endroits où elle coupe les arrêtes de notre tétraèdre définissent un ou plusieurs triangles offrant une approximation de notre surface au sein de notre tétraèdre. L’ensemble de l’espace 3D qui nous intéresse étant lui-même quadrillé par nos tétraèdres, on obtient une approximation de notre surface sur cette portion de l’espace.
On voit bien ici que les nouvelles fonctionnalités introduites par DX 10 sont intéressantes dans ce cas de figure. En effet autant il est tout à fait possible d’évaluer le potentiel de notre champ en utilisant un ‘Vertex Shader’, autant la création des triangles approximant notre surface ne va être possible que par le biais du ‘geometry shader’ qui lui peut émettre des primitives graphiques qui n’existaient pas au préalable, à savoir nos triangles.
Dans la présentation de nVidia dont il est question plus haut, ces programmes sont utilisés pour générer l’iso-surface pour chaque frame, dans notre cas par contre comme les triangles sont destinés d’une part à être affichés à l’aide de DX et d’autre part à être récupérés par notre code managé pour afficher la même géométrie à l’aide de WPF, un choix différent a été effectué. Les triangles ne sont pas passés au reste du pipeline graphique mais sont récupérés à l’aide de la fonctionnalité de ‘StreamOut’ dont il a été question au préalable et qui permet de récupérer les informations en sortie de l’étape d’exécution du ‘geometry shader’ comme l’illustre le schéma ci-dessous :
.jpg)
Le Code
Mise en place
Geometry Shader
Voici ci-dessous le code de la méthode CCachedMetaballs ::Render qui contient une partie du code spécifique à l’utilisation de notre ‘geometry shader’. Les parties en bleu foncé sont celles qui vont nous intéresser le plus :
HRESULT CCachedMetaballs::Render(RenderContext* pRenderContext, ID3D10EffectTechnique* pTechnique)
{
if(m_pStrokesVB&&m_pStrokesIB)
{
D3DXMATRIX worldviewprojInverse;
D3DXMATRIX worldviewprojIT;
D3DXMatrixMultiply(&worldviewprojInverse, &(pRenderContext->m_pCamera->m_World), &(pRenderContext->m_pCamera->m_View) );
D3DXMatrixMultiply(&worldviewprojInverse, &worldviewprojInverse, &(pRenderContext->m_pCamera->m_Projection) );
float det;
D3DXMatrixInverse(&worldviewprojInverse, &det, &worldviewprojInverse);
D3DXMatrixTranspose(&worldviewprojIT, &worldviewprojInverse);
m_pWorldViewProjInverseVariable->SetMatrix( (float*)&(worldviewprojInverse));
m_pWorldViewProjITVariable->SetMatrix( (float*)&(worldviewprojIT));
m_pWorldVariable->SetMatrix( (float*)&(pRenderContext->m_pCamera->m_World) );
m_pViewVariable->SetMatrix( (float*)&(pRenderContext->m_pCamera->m_View) );
m_pEyeVariable->SetFloatVector( (float*)&(pRenderContext->m_pCamera->m_Eye) );
m_pLookAtVariable->SetFloatVector( (float*)&(pRenderContext->m_pCamera->m_At) );
m_pProjectionVariable->SetMatrix( (float*)&(pRenderContext->m_pCamera->m_Projection) );
m_pMeshColorVariable->SetFloatVector( (float*)m_vMeshColor );
m_pLightPosVariable->SetFloatVector( (float*)&(pRenderContext->m_pLight->m_vLightPosition));
m_pLightColorVariable->SetFloatVector( (float*)&(pRenderContext->m_pLight->m_pLightColor));
m_pAmbiantLightVariable->SetFloatVector( (float*)&(pRenderContext->m_pAmbiantLight));
m_pDiffuseVariable->SetFloatVector( (float*)m_pDiffuse);
m_pSpecularVariable->SetFloatVector( (float*)m_pSpecular);
m_pMetaballsVariable->SetFloatVectorArray( m_pMetaballs, 0, 4*m_NumPoints);
m_pDiffuseForMetaballsVariable->SetFloatVectorArray( m_pDiffuseForMetaballs, 0, 4*m_NumPoints);
m_pNumMetaballsVariable->SetInt(m_NumPoints);
Les lignes ci-dessus correspondent aux passages à notre shader d’une liste de points de l’espace 3D correspondants aux centres de nos métaballs, puis au passage d’une liste de ‘float’ correspondant à leurs rayons respectifs.
UINT stride = sizeof( VerySimpleVertex );
UINT offset = 0;
D3D10_TECHNIQUE_DESC techDesc;
// Set the input layout
m_pd3dDevice->IASetInputLayout( m_pVertexLayoutSO );
m_pd3dDevice->IASetVertexBuffers( 0, 1, &m_pStrokesVB, &stride, &offset );
// Set index buffer
m_pd3dDevice->IASetIndexBuffer( m_pStrokesIB, DXGI_FORMAT_R32_UINT, 0 );
// Set primitive topology
m_pd3dDevice->IASetPrimitiveTopology( D3D10_PRIMITIVE_TOPOLOGY_LINELIST_ADJ );
m_pd3dDevice->RSSetState(m_pRasterState);
pTechnique->GetDesc( &techDesc );
for( UINT p = 0; p < techDesc.Passes; ++p )
{
pTechnique->GetPassByIndex( p )->Apply(0);
m_pd3dDevice->DrawIndexed( m_NumIndices , 0, 0 );
}
……..
De fait, force et de constater que dans le code ci-dessus peut de choses semblent spécifiques à l’utilisation de notre géométrie shader. Il faut en fait se reporter à la méthode CCachedMetaballs::StreamGeometryOut pour voir que notre méthode Render va être appelée avec comme paramètre m_pSOTechnique qui est en fait une technique spécifique définie dans notre fichier d’effet.
Si on se reporte justement à celui-ci (fichier : CachedMetaballs.fx) on voit qu’il contient deux techniques (ce reporter aux documents portant sur DX10 pour une plus ample description du système de fichiers d’effets). La première intitulée ‘GenerateCachedGeometry’ utilise un ‘vertex shader’ et un ’geometry shader’. C’est la technique utilisée pour générer notre maillage. On voit par ailleurs qu’elle ne fait intervenir aucun ‘pixel shader’. La deuxième technique ‘RenderCachedGeometry’ est elle plus classique, fait intervenir un ‘vertex shader’ et un ‘pixel shader’ et est utilisé pour afficher le maillage générée à l’aide de ‘GenerateCachedGeometry’ (se reporter au fichier pour voir le code des shaders en eux-même) :
GeometryShader gsStreamOut = ConstructGSWithSO( CompileShader( gs_4_0, GS_TesselateTetrahedraCachedGeometry() ), "SV_Position;Normal.xyz;COLOR0" );
technique10 GenerateCachedGeometry
{
pass p0
{
SetVertexShader(CompileShader(vs_4_0,VS_SampleFieldCachedGeometry()));
SetGeometryShader(gsStreamOut);
SetPixelShader( NULL );
}
}
technique10 RenderCachedGeometry
{
pass P0
{
SetVertexShader( CompileShader( vs_4_0, VS_PositionAndLight() ) );
SetGeometryShader( NULL );
SetPixelShader( CompileShader( ps_4_0, PSCachedGeometry() ) );
}
}
Comme la technique ‘GenerateCachedGeometry’ ne nécessite l’éxécution d’aucun ‘pixel shader’ il est possible d’utiliser la fonctionalité de ‘Stream Out’ pour économiser autant que possible les ressources et la puissance de calcul de notre processeur graphique. Nous allons voir ci-dessous comment l’activer.
StreamOut
Voici la méthode responsable de l’essentiel des opérations en rapport avec l’utilisation de la fonctionnalité ’StreamOut’. Plusieurs parties de cette méthode sont intéressante et je vais essayer de les décrire au mieux directement dans le code source inclus ci-dessous :
HRESULT CCachedMetaballs::StreamGeometryOut(RenderContext* pRenderContext, BYTE** ppVertices, int* pNumVertices)
{
D3D10_BUFFER_DESC vbdesc =
{
1*sizeof(VertexWColor),
D3D10_USAGE_DEFAULT,
D3D10_BIND_VERTEX_BUFFER,
0,
0
};
D3D10_SUBRESOURCE_DATA vbInitData;
ZeroMemory( &vbInitData, sizeof(D3D10_SUBRESOURCE_DATA) );
VertexWColor vertStart =
{
D3DXVECTOR4(0,0,0,0),
D3DXVECTOR3(0,0,0),
D3DXVECTOR4(0,0,0,0),
};
vbInitData.pSysMem = &vertStart;
vbInitData.SysMemPitch = sizeof(VertexWColor);
long MAX_Vertices = *pNumVertices;
vbdesc.ByteWidth = MAX_Vertices*sizeof(VertexWColor);
vbdesc.BindFlags |= D3D10_BIND_STREAM_OUTPUT;
HRESULT hr = m_pd3dDevice->CreateBuffer( &vbdesc, NULL, &m_pStreamOut);
if(FAILED(hr))
{
return hr;
}
ID3D10Buffer *pBuffers[1];
pBuffers[0] = m_pStreamOut;
UINT offset[1] = { 0 };
// Point to the correct output buffer
m_pd3dDevice->SOSetTargets( 1, pBuffers, offset );
Les lignes ci-dessus sont spécialement importantes. Elles créent le buffer qui va être utilisé pour contenir notre maillage et spécifie celui-ci comme ‘cible’ pour la fonctionnalité StreamOut par le biais de la méthode SOSetTargets de notre device DX10. En pratique après cet appel, et si on se reporte au schémas illustrant cette fonctionnalité se trouvant plus haut dans ce document, on vient en fait d’indiquer que la cible de la flèche intitulée Stream Out est le buffer m_pStreamOut défini ci-dessus. Il est important de noter ici que même si nous avons un pointeur sur un buffer DX10, ce pointeur nous permet en fait de référencer et d’effectuer des opérations sur des données qui se trouvent à priori dans une zone mémoire accessible par notre processeur graphique mais pas par notre CPU ce qui pour l’instant nous interdit de les manipuler directement dans notre code C++. Pour rendre les données générées par notre GPU accessible par le CPU les opérations qui suivent sont nécessaires :
// We will need to know how many triangles we do output.
D3D10_QUERY_DESC queryDesc;
queryDesc.Query = D3D10_QUERY_SO_STATISTICS;
queryDesc.MiscFlags = 0;
ID3D10Query* pQuery;
hr = m_pd3dDevice->CreateQuery(&queryDesc, &pQuery);
if(FAILED(hr))
{
return hr;
}
pQuery->Begin();
Render(pRenderContext, m_pSOTechnique);
pQuery->End();
D3D10_QUERY_DATA_SO_STATISTICS stats;
while(S_OK !=pQuery->GetData(&stats, sizeof(D3D10_QUERY_DATA_SO_STATISTICS), 0))
{
}
// Okay let's save these values
m_numPrimitivesWritten = stats.NumPrimitivesWritten;
m_numPrimitiveStorageNeeded = stats.PrimitivesStorageNeeded;
m_bNotEnoughVideoMemoryForAllPrimitives = (m_numPrimitivesWritten!=m_numPrimitiveStorageNeeded);
m_bNeedCacheRefresh = false;
if(ppVertices)
{
*pNumVertices = (int)3*m_numPrimitivesWritten;
// Ok let's create a buffer we can read in.
vbdesc.CPUAccessFlags = D3D10_CPU_ACCESS_READ;
vbdesc.BindFlags = 0;
vbdesc.Usage = D3D10_USAGE_STAGING;
ID3D10Buffer *pCPUReadableStream;
hr = m_pd3dDevice->CreateBuffer( &vbdesc, NULL, &pCPUReadableStream);
if(FAILED(hr))
{
return hr;
}
m_pd3dDevice->CopyResource(pCPUReadableStream, m_pStreamOut);
Comme le laisse penser les noms des variables utilisés, suite à l’exécution des lignes ci-dessus les données générées par notre processeur graphique sont copiés dans un buffer accessible par notre CPU. Les lignes ci-dessous vont d’ailleurs immédiatement profiter de cette possibilitées pour copier les données en question dans une zone mémoire qui n’est plus gérée par DirectX mais que nous allons allouer nous même par le biais d’un appel à malloc :
// Cool let's now read the content of our CPU readable buffer
BYTE* pBuff;
hr = pCPUReadableStream->Map(D3D10_MAP_READ , 0, (void**)&pBuff);
if(FAILED(hr))
{
return hr;
}
*ppVertices = (BYTE*)malloc((*pNumVertices)*sizeof(VertexWColor));
memcpy(*ppVertices, pBuff, (*pNumVertices)*sizeof(VertexWColor));
pCPUReadableStream->Unmap();
// We should now be able to realease anything we have used.
pCPUReadableStream->Release();
}
// Get back to normal
pBuffers[0] = NULL;
m_pd3dDevice->SOSetTargets( 1, pBuffers, offset );
return S_OK;
}
A partir de maintenant les données crées par l’exécution de notre ‘geometry shader’ sont stockées dans cette zone de mémoire pointée par *ppVertices et que nous sommes libre de manipuler comme nous le désirons dans notre code C++. Il nous est même du coup possible de récupérer ces données pour les stocker dans la heap managé et de les rendre accessible à nos classes .NET comme nous allons le voir dans le paragraphe suivant.
Récupération maillage
Comme nous l’avons vu dans le paragraphe précédent nous avons récupéré et stockée dans une zone mémoire pointée par *ppVertices les données générées par notre processeur graphique. Ces données sont les coordonnées cartésiennes des point des l’espace constituant notre maillage. Nous allons maintenant voir comment rendre ces données accessibles à notre code .NET ce qui nous permettra ultérieurement d’afficher notre maillage à l’aide des fonctionnalités 3D de WPF comme je le décrirai dans un prochain article.
Pour mieux comprendre comment les données sont rendues accessibles à notre code managé il est intéressant de se pencher sur la méthode StreamGeometryOut de notre classe Host qui est utilisée pour rendre accessible les fonctionnalités que nous avons implémentées en C++ à notre code managé :
Stream<VertexP4N3C4>^ StreamGeometryOut(int maxNumberOfVertices)
{
int numVertices = maxNumberOfVertices;
Host::Stream<VertexP4N3C4>^ natStream = gcnew
Host::Stream<VertexP4N3C4>();
if(m_pViewer)
{
BYTE* pVertices = NULL;
m_pViewer->StreamGeometryOut(&pVertices, &numVertices);
natStream->_internalDataPointer = (void*)pVertices;
// We have float4 for position and float3 for normal
// and float4 for color
// so this makes 4+3+4 float per vertex.
natStream->size = numVertices*VertexP4N3C4::strideSize;
}
return natStream;
}
De fait on voit que l’on crée une instance de la classe Host ::Stream<VertexP4N3C4> à laquelle nous allons associer l’adresse mémoire de notre buffer contenant nos données.
Si l’on regarde alors un peu plus la class Stream et plus spécialement sa méthode Read :
generic <typename T> where T : value struct
public ref class Stream
{
………………
virtual int Read(array<T>^ managedarray, int length, int offset)
{
if((length+offset)*sizeof(T)>size)
{
throw gcnew ArgumentException("stream is too small for this");
}
if(!_internalDataPointer)
{
throw gcnew InvalidOperationException("stream is not in a valid state!");
}
GCHandle^ handle = GCHandle::Alloc(managedarray, GCHandleType::Pinned);
memcpy(handle->AddrOfPinnedObject().ToPointer(), (BYTE*)_internalDataPointer+offset*sizeof(T), length*sizeof(T));
handle->Free();
return length;
}
………………………….
};
On voit que l’on écrit le contenu de notre buffer dans une array. VertexP4N3C4 étant une value class, cela guarantit pour l’instant que notre array est une portion contigue dans la heap managé mais ce n’est pas suffisant. Nous allons donc indiquer au garbage collector qu’il ne doit pas déplacer cette zone de mémoire, l’opération GCHandle::Alloc en passant comme paramètre GCHandleType::Pinned permet d’obtenir cela.
Ensuite il ne nous reste plus qu’à faire un memcpy (un autre J) et à indiquer au garbage collector qu’il est à nouveau libre d’effectuer les opérations qu’il désire sur cette portion de son heap, ceci étant effectué par l’appel de la méthode Free sur le GCHandle que nous avions précédement récupéré lors e l’appel à GCHandle::Alloc. A partir de ce moment là on peut dire que notre array de VertexP4N3C4 est en tout point équivalente à une array de value class de ce type que nous aurions pu créer en code managé. De fait elle est aussi parfaitement utilisable en tant que tel. Je reviendrais sur ce point dans mon document suivant montrant comment utiliser ces vertex pour afficher nos métaballs à l’aide de WPF 3D.
Conclusion
J’espère que cet article vous aura donné envie d’utiliser DirectX 10 dans une grande variété de situations. J’espère aussi avoir rendu justice ce nouveau type de programme supporté par les GPUs DirectX 10 que sont les ‘geometry shader’, tout en montrant que leur mise en œuvre est simple et élargi de façon significative le champ d’application du GPU. En particulier lorsqu’ils sont utilisés en conjonction avec d’autres nouveauté introduites par DX 10 comme ‘StreamOut’. Enfin je l’avoue j’espère que cet article ainsi que le précédent de cette série traitant de DX10 et WPF vous aura convaincu que l’utilisation de DirectX 10 à partir de code managé reste aisé et agréable par le biais des possibilités offertes par C++/CLI. Si un doute pouvait subsister j’espère finir de vous convaincre avec le dernier document de cette série, qu’il me reste encore à écrire et détaillant la méthode utilisée pour afficher l’iso surface généré par DX 10 en utilisant WPF 3D.
Bonne journée à tous,
Pour toute remarque merci de me contacter à l’adresse suivante : guillara@microsoft.com