Wer auf das FX-Framework verzichtet und seine Shader noch von Hand kompiliert, wird CreateVertexShader(), CreateGeometryShader() und CreatePixelShader() kennen. Doch die meisten Leute – auch die Direct3D-Samples selbst – nutzen die vereinigte Shader-Architektur nicht aus und machen sich das Leben schwer.
An dieser Stelle ein Hinweis: Mit D3D11 kommen Compute-, Hull- und Domain-Shader … es wird also nicht einfacher ;) Darum erarbeiten wir uns hier eine kurze, sichere Methode zum Laden von Shadern, die sich zunutze macht, dass sich die vereinigten Shader nur im Profil und dem entsprechenden Aufruf von Create…Shader() unterscheiden.
Schauen wir uns zuerst einmal die gängigen Praktiken an:
1. Welches Problem? Was lösen?
Leider am weitesten verbreitet ist, jeden Gedanken an dieses Problem mit Strg+C und Strg+V zu unterdrücken. Oder: Es wird eine Funktion zum Laden eines Vertex-Shaders geschrieben, dann kopiert und für Geo- und Pixelshader angepasst. Beispiel (ohne Fehlerverarbeitung zwecks Übersicht):
Code: Alles auswählen
void LoadVertexShader(::ID3D10Device * p_pDevice, const char p_Dateiname[], const char p_Funktionsname[], ::ID3D10VertexShader ** p_ppShader) {
// Shader aus Datei laden und kompilieren
::ID3DBlob * l_Code;
::ID3DBlob * l_Fehlermeldungen;
D3DXCompileShaderFromFile(p_Dateiname, …, p_Funktionsname, "vs_4_0", … l_Code, l_Fehlermeldungen);
// Hier Shader erzeugen
p_pDevice->CreateVertexShader(l_Code->GetBufferPointer(), l_Code->GetBufferSize(), p_ppShader);
}
void LoadGeoShader(::ID3D10Device * p_pDevice, const char p_Dateiname[], const char p_Funktionsname[], ::ID3D10GeometryShader ** p_ppShader) {
// Shader aus Datei laden und kompilieren
::ID3DBlob * l_Code;
::ID3DBlob * l_Fehlermeldungen;
D3D10CompileShader(p_Dateiname, …, p_Funktionsname, "gs_4_0", … l_Code, l_Fehlermeldungen);
// Hier Shader erzeugen
p_pDevice->CreateGeometryShader(l_Code->GetBufferPointer(), l_Code->GetBufferSize(), p_ppShader);
}
void LoadPixelShader(::ID3D10Device * p_pDevice, const char p_Dateiname[], const char p_Funktionsname[], ::ID3D10PixelShader ** p_ppShader) {
// Shader aus Datei laden und kompilieren
::ID3DBlob * l_Code;
::ID3DBlob * l_Fehlermeldungen;
D3D10CompileShader(p_Dateiname, …, p_Funktionsname, "ps_4_0", … l_Code, l_Fehlermeldungen);
// Hier Shader erzeugen
p_pDevice->CreatePixelShader(l_Code->GetBufferPointer(), l_Code->GetBufferSize(), p_ppShader);
}
2. Problem verstanden, aber falsch gelöst
Findige Zeitgenossen haben schon erkannt, dass sich nur die beiden oben genannten Stellen vom restlichen Code unterscheiden, und kamen deshalb auf die Idee: Wenn individuelle Unterschiede behandelt werden müssen, dann natürlich mit if-else-Blöcken! (Zugegeben: dazu gehörte ich auch mal ;) ) Beispiel:
Code: Alles auswählen
enum ShaderTyp {
VertexShader,
PixelShader,
GeoShader
};
void LoadShader(::ID3D10Device * p_pDevice, const char p_Dateiname[], const char p_Funktionsname[], ShaderTyp p_Typ, void ** p_ppShader) {
const char * ProfilNachTyp[3] = { "vs_4_0", "gs_4_0", "ps_4_0" };
// Shader aus Datei laden und kompilieren
::ID3DBlob * l_Code;
::ID3DBlob * l_Fehlermeldungen;
D3D10CompileShader(p_Dateiname, …, p_Funktionsname, ProfilNachTyp[p_Typ], … l_Code, l_Fehlermeldungen);
// Hier Shader erzeugen
switch(p_Typ) {
case VertexShader:
p_pDevice->CreateVertexShader(l_Code->GetBufferPointer(), l_Code->GetBufferSize(), (::ID3D10VertexShader**)p_ppShader);
case GeoShader:
p_pDevice->CreateGeometryShader(l_Code->GetBufferPointer(), l_Code->GetBufferSize(), (::ID3D10GeometryShader**)p_ppShader);
case PixelShader:
p_pDevice->CreatePixelShader(l_Code->GetBufferPointer(), l_Code->GetBufferSize(), (::ID3D10PixelShader**)p_ppShader);
}
}
Denken wir also nach. Wie können wir automatisch Code aufrufen, der immer zum Typ passt? Durch Überladungen? Ja, aber dann müssten wir wieder drei Funktionen wie oben schreiben… natürlich meine ich Templates!
3. Problem erkannt und gebannt
Zur Erinnerung: Deklarieren wir ein template, setzt der Compiler beim Aufruf die passenden template-Parameter ein und kompiliert damit. Es sei denn, es existiert eine Spezialisierung, die auf die Parameter passt. Beispiel:
Code: Alles auswählen
// Ein generisches template
template <typename t_Datentyp> void MachWas(t_Datentyp & p_Parameter) {
std::cout<<"Macht was mit irgendeinem Typ..."<<endl;
}
// Eine Spezialisierung für Doubles
template <> void MachWas(double & p_Parameter) {
std::cout<<"Macht was mit double!"<<endl;
}
// In der main():
MachWas(1); // Ausgabe: Macht was mit irgendeinem Typ...
MachWas('x'); // Ausgabe: Macht was mit irgendeinem Typ...
MachWas(5.0); // Ausgabe: Macht was mit double!
Zum Aufwärmen abstrahieren wir erst einmal CreateVertex-, Geometry- und -PixelShader(). Dazu eine Anmerkung: Von nun an übergeben wir den Shader der Funktion nicht mehr als Zeiger auf einen Zeiger, sondern als Referenz auf einen Zeiger – so können wir den Zeiger immernoch verändern, sparen uns aber eine Dereferenzierung und verhindern, dass man NULL übergeben kann.
Code: Alles auswählen
// Deklarieren
template <typename t_ShaderTyp> HRESULT CreateShaderFromBytecode(::ID3D10Device & p_Device, ::ID3D10Blob & p_Bytecode, t_ShaderTyp *& p_pShader);
// Für jeden Shadertyp spezialisieren
template <> HRESULT CreateShaderFromBytecode(::ID3D10Device & p_Device, ::ID3D10Blob & p_Bytecode, ::ID3D10VertexShader *& p_pShader) {
return p_Device.CreateVertexShader(p_Bytecode.GetBufferPointer(), p_Bytecode.GetBufferLength(), &p_pShader);
}
template <> HRESULT CreateShaderFromBytecode(::ID3D10Device & p_Device, ::ID3D10Blob & p_Bytecode, ::ID3D10GeometryShader *& p_pShader) {
return p_Device.CreateGeometryShader(p_Bytecode.GetBufferPointer(), p_Bytecode.GetBufferLength(), &p_pShader);
}
template <> HRESULT CreateShaderFromBytecode(::ID3D10Device & p_Device, ::ID3D10Blob & p_Bytecode, ::ID3D10PixelShader *& p_pShader) {
return p_Device.CreatePixelShader(p_Bytecode.GetBufferPointer(), p_Bytecode.GetBufferLength(), &p_pShader);
}
Code: Alles auswählen
// Deklarieren
template <typename t_ShaderTyp> const char * ProfilDesShaderTyps(void);
// Eine Spezialisierung für jeden Shadertyp:
template <> const char * ProfilDesShaderTyps<::ID3D10VertexShader>(void) { return "vs_4_0"; }
template <> const char * ProfilDesShaderTyps<::ID3D10GeometryShader>(void) { return "gs_4_0"; }
template <> const char * ProfilDesShaderTyps<::ID3D10PixelShader>(void) { return "ps_4_0"; }
Code: Alles auswählen
template <typename t_ShaderTyp> void LoadShader(::ID3D10Device & p_Device, const char p_Dateiname[], const char p_Funktionsname[], t_ShaderTyp *& p_pShader) {
// Shader aus Datei laden und kompilieren
::ID3DBlob * l_Code;
::ID3DBlob * l_Fehlermeldungen;
D3D10CompileShader(p_Dateiname, …, p_Funktionsname, ProfilDesShaderTyps<t_ShaderTyp>(), … l_Code, l_Fehlermeldungen);
CreateShaderFromBytecode(p_Device, l_Code, p_pShader);
}
Angewendet sieht das ganze nun so aus:
Code: Alles auswählen
::ID3D10VertexShader * MeinVertexShader = NULL;
::ID3D10PixelShader * MeinPixelShader = NULL;
LoadShader(MeinDevice, "Blubb.hlsl", "VSMain", MeinVertexShader);
LoadShader(MeinDevice, "Blubb.hlsl", "PSMain", MeinPixelShader);
4. Der komplette Code
Jetzt wo ihr euch all das durchgelesen habt, solltet ihr eure eigenen Shader-Funktionen vielleicht überarbeiten. Seid kreativ, z.B. kann man auf dieselbe Weise auch VSSetShader(), GSSetShader() und PSSetShader() abstrahieren oder gleich alle Shader samt Input-Layout in Klassen kapseln.
Weil auch immer wieder vergessen wird, die temporären Puffer zu löschen und das mit den Fehlermeldungen so eine Sache ist, spendiere ich euch hier meinen eigenen Quellcode zum Laden von Shadern. Ich habe ihn um den Parameter Feature-Level erweitert – momentan kann man seine Shader damit, je nachdem was die GPU unterstützt, für Shader Model 4.0 oder 4.1 kompilieren, in D3D11 wird dieser Parameter noch an Einfluss gewinnen. Denkt daran, dass mein Code in einer Klasse CGPU steckt (weil globale Funktionen, außer zu Anschauungszwecken, böse sind ;) )
Code: Alles auswählen
class CGPU {
private:
// Diese Funktionen werden ausschließlich spezialisiert, darum stehen in der Klassendeklaration nur ihre Deklarationen.
template <typename t_ShaderType> const char * const ShaderProfile(const ::D3D10_FEATURE_LEVEL1 p_FeatureLevel);
template <typename t_ShaderType> void CreateShader(t_ShaderType *& p_pShader, const void * const p_pBytecode, const size_t p_iBytecodeLength);
public:
// Diese Funktion wird nicht spezialisiert sondern ist ein generisches Template. Deshalb muss sie komplett in der Klassendeklaration definiert
// werden (extern templates werden nicht von jedem Compiler unterstützt!).
template <typename t_ShaderType> void LoadShader(
t_ShaderType *& p_pShader,
const char p_sFilename[],
const char p_sEntryPointName[]
) {
// Speichert den Bytecode (falls die Kompilierung erfolgreich war) sowie die Fehlermeldungen (falls die Kompilierung fehl schlug).
::ID3DBlob * l_pBytecode = NULL;
::ID3DBlob * l_pErrors = NULL;
// Kompilieren des Shaders mit dem D3D-Shader-Compiler.
::D3D10CompileShader(
NULL, 0,
p_sFilename,
NULL, NULL,
p_sEntryPointName,
ShaderProfile<t_ShaderType>(),
0,
l_pBytecode, l_pErrors);
// Sind Fehler aufgetreten?
if(NULL != l_pErrors) {
// Fehlermeldung ausgeben
::MessageBoxA(NULL, l_pErrors->GetBufferPointer(), "Fehler beim Kompilieren eines Shaders", MB_OK|MB_ICONERROR|MB_SETFOREGROUND);
// Fehlermeldungen wieder freigeben! Wird oft vergessen…
l_pErrors->Release();
}
else {
// Shader aus dem Bytecode erzeugen.
this->CreateShader(p_pShader, l_pBytecode->GetBufferPointer(), l_pBytecode->GetBufferSize());
// Bytecode wieder freigeben. Wird auch oft vergessen…
l_pBytecode->Release();
}
}
} // class CGPU
template <> const char * const CGPU::ShaderProfile<::ID3D10VertexShader>(void) {
switch(this->FeatureLevel()) {
case ::D3D10_FEATURE_LEVEL_10_0:
return "vs_4_0";
case ::D3D10_FEATURE_LEVEL_10_1:
return "vs_4_1";
}
}
template <> const char * const CGPU::ShaderProfile<::ID3D10GeometryShader>(void) {
switch(this->FeatureLevel()) {
case ::D3D10_FEATURE_LEVEL_10_0:
return "gs_4_0";
case ::D3D10_FEATURE_LEVEL_10_1:
return "gs_4_1";
}
}
template <> const char * const CGPU::ShaderProfile<::ID3D10PixelShader>(void) {
switch(this->FeatureLevel()) {
case ::D3D10_FEATURE_LEVEL_10_0:
return "ps_4_0";
case ::D3D10_FEATURE_LEVEL_10_1:
return "ps_4_1";
}
}
template <> void CGPU::CreateShader(::ID3D10VertexShader *& p_pShader, const void * const p_pBytecode, const size_t p_iBytecodeLength) {
this->D3DDevice().CreateVertexShader(p_pBytecode, p_iBytecodeLength, &p_pShader);
}
template <> void CGPU::CreateShader(::ID3D10GeometryShader *& p_pShader, const void * const p_pBytecode, const size_t p_iBytecodeLength) {
this->D3DDevice().CreateGeometryShader(p_pBytecode, p_iBytecodeLength, &p_pShader);
}
template <> void CGPU::CreateShader(::ID3D10PixelShader *& p_pShader, const void * const p_pBytecode, const size_t p_iBytecodeLength) {
this->D3DDevice().CreatePixelShader(p_pBytecode, p_iBytecodeLength, &p_pShader);
}
Fragen, Kritik, Lob, Verbesserungsvorschläge usw. könnt ihr wie immer direkt hier los werden :)