PDA

Voir la version complète : Encapsuler un Mesh avec gestion des Effets (.fx)


janta
27/11/2005, 18h21
Allo :)

J'ai créé une classe très simple pour encapsuler un Mesh ( cad un pointeur sur ID3DXEffect + les autres pointeurs qui vont bien: materials, textures ainsi que des variables bool ou int, etc.)
Cette classe fonctionne au poil avec les membres:

...
HRESULT LoadFromFile(IDirect3DDevice9* pd3dDevice, LPWSTR fileName);
HRESULT Render(IDirect3DDevice9* pd3dDevice);
VOID Clean();
...

Maintenant j'essaye d'améliorer cette classe (sans héritage, pour commencer) en intégrant la gestion des fichier effets, puisque certains meshes au format .x ont besoin du fichier .fx associé pour être rendu correctement. (En fait j'ai pris pour exemple le mesh dwarf.x fourni dans le sdk directx, que l'on voit notamment dans l'exemple ProgressiveMesh => Mais Pour l'instant je ne cherche pas à gérer les progressive mesh !)

J'ai donc ajouté (essentiellement) les membres suivants: (et modifié un peu la fonction Clean() bien sur)

...
HRESULT LoadEffect(IDirect3DDevice9* pd3dDevice, LPWSTR szFxFileName);
HRESULT SetRenderTechnique(D3DXHANDLE hTechnique);
...

Jusqu'ici pas de souci, c'est du gâteau. Mais la fonction de Rendu !!!
Peut être que ca vient de mon inexpérience, mais voici comment je vois la chose:

Chaque fichier .fx possède ses propres variables globales et doit donc par conséquent avoir un traitement spécifique au moment du rendu du mesh. Il faut notamment:


Connaître le nom des variables globales
Savoir quelles variables doivent être initialisées au début du rendu
Savoir quelles variable doivent être modifiées pour chaque SubSet
Savoir quelles variable doivent être modifiées à chaque passe
Etc.


Par exemple mon fichier .fx commence par les déclarations suivantes:

//--------------------------------------------------------------------------------------
// Global variables
//--------------------------------------------------------------------------------------
float4 g_vDiffuse; // Material diffuse color (pour chaque subset)
float4x4 g_mWorld; // World matrix for object (début du rendu du mesh)
float4x4 g_mWorldViewProjection; // World * View * Projection matrix (début du rendu)
texture g_txScene; // Texture, pour chaque subset
...

Mais j'imagine que ce sera différent pour chaque fichier .fx :00000012:

Comment donc imagineriez vous la fonction de rendu ?

Certains paramètres doivent être transmis au moment de l'appel (les matrices de vue, du monde, de projection), d'autres sont "connus" par le mesh (texture, material) mais encore faut il savoir quoi passer et quand, dans un appel du type :
pMesh->SetXxxxx("g_Parameter", parameterValue);

Peut être faire une fonction à liste d'arguments variables ? Hum au niveau des performances je ne suis pas sur que ce soit une bonne idée...

Voila, je ne sais pas si ce thread est susceptible d'intéresser des gens ;) Et désolé pour la longueur mais j'espère au moins avoir exposé clairement mon problème et vous donner envie d'y réfléchir avec moi.

PS: Une autre possibilité à laquelle j'ai pensé serait de raisonner à l'envers avec une "procédure de traitement" définie dans ma classe (voire plusieurs), et de me limiter à des formats de fichier .fx conformes à ces procédures (notamment le nom des variables et à quel moment les définir). Pensez vous que la variabilité des fichier .fx soit trop importante pour ce genre de méthode (qui réduit déjà infiniment trop la flexibilité de ma classe à mon goût, mais bon...)

PS2: Mes objets Mesh ne sont pas des objets physiques, simplement de pures géométrie. Donc pas de propriétés physiques encapsulées (masse, taille, position, orientation, etc.) et par conséquent les différentes matrices liées au rendu (monde, projection, vue) doivent être fournies à la méthode de rendu. (Mais je suis ouvert à la discussion sur cette modélisation bien sur)

EDIT: si vous trouvez ce sujet ininteressant, trop simple, trop compliqué etc. vous pouvez aussi le dire :D (jai l'impression d'être boycotté :p lol)

Ruffi
28/11/2005, 02h57
Mais non t'es pas boycote, je suis meme content de voir quelqu'un qui se pose les meme questions que moi :)

Par contre, mon cas est un peu different : j'utilise un shader sur tout le mesh, ce qui n'est peut etre pas bon si ton shader doit changer suivant les subset du mesh. Je les utilise pas uniquement car certains mesh ne veulent pas marcher sans, mais surtout pour apliquer mes propres effets.


L'idée est de separer l'effet de l'objet, du coup tu met à jours ses paramettres ou tu veux, que ce soit dans la classe mesh ou ailleur du moment que tu ai acces a ton effet :

Pour gerer un objet 3D, je suis parti du principe que sa geometrie (le LPD3DXMESH), sa position, ses effets, sa bounding box... tout doit etre separe pour eviter d'avoir 500 fois les meme informations en memoire si 500 objets l'utilise.

Ma classe mesh n'est donc qu'une encapsulation de LPD3DXMESH et tout le tatouin qui vient avec.
Apres, j'ai une classe cMeshObject qui contient un cMesh*, un cEffect*, un cWorldPosition...

Du coup, plusieurs cMeshObject peuvent avoir le meme mesh a des positions differentes et avec des effets differents.
Tu peut aussi utiliser le meme effet pour plusieurs cMeshObject.

Pour utiliser l'effect avec le mesh, j'ai juste un truc de ce genre là :

// pseudo code
if( m_effect == NULL)
mesh->render();
else
{
effect->begin(); // ajouter les begin pass et end pass
mesh->render();
effect->end();
}

janta
28/11/2005, 05h46
Oh yeah ;)
Déjà je vois qu'on la même conception objet, la géométrie du mesh: ta classe CMesh, est bien encapsulée dans un objet comportant en particulier la matrice de transformation du monde, ou des informations permettant de la calculer (XYZ+YPR etc.)

Reformuler est un bon moyen de vérifier que l'on se comprend bien ^^ :p

Admettons pour le moment que dessiner tout le mesh avec un seul effet nous convienne ;) (on est pas maso, du moins pas tout de suite :p)

Le problème de ta méthode (ou bien je me trompe ce qui est **très** possible :00000025: ) est qu'elle ne peut pas tenir compte du fait que pour un mesh donné, la texture et le matériau va très probablement être différent pour chaque subset. Or de nombreux effets utilisent en entrée la texture et le matériau. Exemple concret (©Microsoft)

Code abrégé du fichier .fx
// Global variables
float4 g_vDiffuse; // Material diffuse color
float4x4 g_mWorld; // World matrix for object
float4x4 g_mWorldViewProjection; // World * View * Projection matrix
texture g_txScene;

sampler g_samScene =
sampler_state
{
Texture = <g_txScene>;
...
};


void VertScene( float4 Pos : POSITION,
float3 Normal : NORMAL,
float2 Tex : TEXCOORD0,
out float4 oPos : POSITION,
out float2 oTex : TEXCOORD0,
out float4 Diffuse : COLOR0 )
{
...
}


float4 PixScene( float2 Tex : TEXCOORD0,
float4 Diffuse : COLOR0 ) : COLOR0
{
...
}

// Techniques
technique RenderScene
{
pass P0
{
VertexShader = compile vs_1_1 VertScene();
PixelShader = compile ps_1_1 PixScene();
}
}

Alors là OK tu peux charger les matrices de vue et composite un fois avant le rendu de tout le mesh MAIS la texture et le matériau (sa couleur diffuse en fait) doivent être modifiées pour chaque subset:

if( SUCCEEDED( pd3dDevice->BeginScene() ) )
{
// Get the projection & view matrix from the camera class
mWorld = g_mWorldCenter * *g_Camera.GetWorldMatrix();
mProj = *g_Camera.GetProjMatrix();
mView = *g_Camera.GetViewMatrix();

mWorldViewProjection = mWorld * mView * mProj;

if( g_ppPMeshes )
{

g_pEffect->SetMatrix( "g_mWorldViewProjection",
&mWorldViewProjection );
g_pEffect->SetMatrix( "g_mWorld", &mWorld );

// Set and draw each of the materials in the mesh
for( DWORD i = 0; i < g_dwNumMaterials; i++ )
{
g_pEffect->SetVector( "g_vDiffuse",
(D3DXVECTOR4*) &g_mtrlMeshMaterials[i] );
g_pEffect->SetTexture( "g_txScene", g_ppMeshTextures[i] );
UINT cPasses;
g_pEffect->Begin( &cPasses, 0 );
for( UINT p = 0; p < cPasses; ++p )
{
g_pEffect->BeginPass( p );
g_ppPMeshes[g_iPMeshCur]->DrawSubset( i );
g_pEffect->EndPass();
}
g_pEffect->End();
}
}

pd3dDevice->EndScene();
}

Est ce que je dis des betises ? :00000002:

De mon coté j'essaye de construire une classe assez robuste qui puisse gérer "n'importe quel" (comprendre: pas trop tordu quand même ^^) fichier fx dont on ne connaît pas le contenu à priori.

Cela impliquerait donc (à mon avis):


Parser le fichier effet, ce qui est très facile puisque une fois l'effet créé on a les méthodes ID3DXBaseEffect::GetXxxxDesc(...) (GetDesc, GetParameterDesc, GetPassDesc, GetTechniqueDesc) qui remplissent des descripteurs appropriés.
Permettre à celui qui utilise la classe de spécifier à quel moment et avec quelles valeurs chaque variable du fx doit être (ré-)initialisée. (C'est pour ca que j'ai pensé aux fonctions a liste d'arguments variable...)


Bref je suis très certainement en train de partir un peu dans une (aussi) superbe (qu'inefficace :D) usinagaz :P

Le moyen le plus rationnel d'arriver à ce que je recherche est peut être finalement d'écrire une classe abstraite avec une méthode render() virtuelle pure, obligeant ainsi à écrire pour chaque classe une méthode render() conforme au fichier effet utilisé (je pense que c'est le meilleurs compromis entre la réutilisation de code pour le chargement et la libération du fx et un truc compliqué et laborieux à mort...)

So what say you ? :D
(encore un pave :00000023: )

Ruffi
28/11/2005, 17h16
Pour le moment, ma methode ne marche qu'avec un seul effet sont les paramettres ne changent pas pour chaque subset du mesh. Surtout car actuelement je teste avec tiny.x qui contient qu'une seule texture.

Pour ton cas, je pense que je pencherai plutot pour une solution utilisant des pointeurs de fonctions.

typedef int (*PtrFonctEffect)(cMesh* /*mesh*/, int /*subset*/, cEffect* /*effet*/);

Tu ajoute un PtrFonctEffect dans ta classe mesh qui s'occupera de manipuler ton effet.
Tu peut aussi créer un tableau ou une liste de la meme taille que ton nombre de materiaux si tu veux que chacun ai sa propre fonction.

exemple :

for( DWORD i = 0; i < g_dwNumMaterials; i++ )
{
if( m_effectFunction )
m_effectFunction(this, i, g_pEffect);
UINT cPasses;
g_pEffect->Begin( &cPasses, 0 );
for( UINT p = 0; p < cPasses; ++p )
{
g_pEffect->BeginPass( p );
g_ppPMeshes[g_iPMeshCur]->DrawSubset( i );
g_pEffect->EndPass();
}
g_pEffect->End();
}
int setDwarfEffect( cMesh* mesh, int subset, cEffect* effect)
{
effect->SetVector( "g_vDiffuse", (D3DXVECTOR4*) &mesh->g_mtrlMeshMaterials[i] ); // en suposant que tu ai acces au aux matieriaux/texure
effect->SetTexture( "g_txScene", mesh->g_ppMeshTextures[i] );
}

...
mesh.load( "dwarf.x" );
mesh.setEffectFunction( setDwarfEffect )
...

C'est surement comme ça que je ferai, mais pour le moment ce n'est que theorique.
Ca oblige l'utilisateur a creer un fonction pour informer comment son shader doit etre mis à jours.
Dit moi ce que t'en pense :)

janta
28/11/2005, 18h08
Mmh ca m'a l'air good mais alors il faudrait aussi un callback à appeller avant le début du for(i...) ainsi qu'un coallback à appeller en début de chaque passe (pour ce dernier je dis peut être une enormité: on peut modifier les variables globales entre chaque passe ?:00000029: )

En tout cas c'est dans le meme style que moi (l'utilisateur ecrit plus ou moins sa fonction de rendu lui meme) en un peu plus simple néamoins (uniquement l'effet à gérer, pas le rendu du mesh)

A mediter sur cette question je vais m'employer ;)
merci pour tes reponses

Ruffi
05/12/2005, 04h33
Il va faloir que je repense un peu mon systeme :
Jusqu'a present, je ne tenais pas compte des lumieres.
Maintenant que je veux les ajouter, il va faloir envoyer tout plein de parametre au shaders (position de la lumiere, sa couleur, position de l'oeuil...).
Et je sais pas trop comment faire ça en evitant de foutre le bordel de partout.