Höhenbasierter Nebel per Post
Transcription
Höhenbasierter Nebel per Post
Höhenbasierter Nebel per Post-Processing Problem Darstellung eines realistischen Nebels mit unterer und oberer Begrenzung in einer Echtzeit-3DAnwendung. Die gängigen 3D-APIs (Direct3D und OpenGL) unterstützen nur einfachen entfernungsabhängigen Nebel, d.h. die gerenderten Pixel werden abhängig von ihrer Entfernung zur Kamera mehr oder weniger mit der Farbe des Nebels eingefärbt. Diese Methode wird direkt von allen 3D fähigen Grafikkarten unterstützt und geschieht ohne Performanceverlust. Ein höhenbasierter Nebel lässt sich damit allerdings nicht umsetzen. Um die 'Vernebelung' eines Pixels bei Abbildung 1 Foto eines realen Nebels höhenbasiertem Nebel zu berechnen, muss man die Entfernung ermitteln, die ein Strahl von der Kamera zum Pixel innerhalb des Nebels zurücklegt. Über einen Vertexshader lässt sich dies ohne weiteres auf Vertexebene umsetzen, dies hat aber einige Nachteile: • Da die 'Vernebelung' nur pro Vertex berechnet wird und für die einzelnen Pixel des Polygons interpoliert wird, ergeben sich nur befriedigende Ergebnisse, wenn sämtliche Objekte aus ausreichend vielen Polygonen modeliert sind. Z.B. darf eine Wolkenschicht über dem Nebel nicht einfach aus zwei Polygonen bestehen, sondern muss in viele einzelne Dreiecke unterteilt werden. • Bei der Darstellung eines jeden Objekts muss der Nebel berücksichtigt werden. D.h. Objekte mit unterschiedlicher Rendermethode benötigen jeweils einen angepassten Vertexshader zur Berechnung des Nebels. Objekte, die von sich aus bereits einen eigenen Vertex-/Pixelshader verwenden, müssen in mehreren Durchgängen gerendert werden. Die Folge davon ist, dass sich die Programmierung des höhenbasiertem Nebels durch die gesamte Engine zieht und keine getrennte Einheit ist. Idee Um höhenbasierten Nebel umzusetzen und dabei Probleme mit der oben erwähnten Methode zu vermeiden, bietet sich Post-Processing an. Anstatt den Nebel beim Rendern eines jeden Pixels zu berücksichtigen, wird der Nebel erst auf das fertige Bild aufgetragen, d.h. das gerenderte Bild wird nachbearbeitet. Anhand der Tiefeninformation des Bildes und der beiden Begrenzungsebenen des Nebels lässt sich durch einfache Rechnung die Nebelstärke jedes Pixels berechnen. Abbildung 2 Tiefeninformation der Landschaft Abbildung 3 Tiefeninformation der unteren Nebelbegrenzungsebene Abbildung 4 Tiefeninformation der oberen Nebelbegrenzungsebene 1. Minimum der Tiefeninformation der Landschaft und der unteren Nebelbegrenzungsebene errechnen: Abbildung 5 Minimum aus Tiefe der Landschaft und der unteren Nebelbegrenzungsebene 2. Differenz aus der Tiefeninformation der oberen Nebelbegrenzungsebene und dem Ergebnis aus 1. bilden. Anschließend das Ergebnis bis zur gewünschten Nebelstärke verstärken: Abbildung 6 Berechnete Nebelstärke aus den Tiefeninformationen Das Ergebnis ist ein Alpha-Layer zum Einfärben des gerenderten Bilds mit einer Nebelfarbe. Theoretische Umsetzung Gewinnung der Tiefeninformationen Eigentlich sollte es kein Problem sein, an die Tiefeninformationen einer Szene zu kommen, da Grafikkarten diese Informationen intern bereits gespeichert haben, um verdeckte Pixel nicht zu zeichnen. Dieser z-Buffer (oder Depth-Buffer) ist allerdings auf neueren Grafikkarten kein einfaches Graustufenbild, sondern eine komplexe Datenstrukur. Ein einfaches Umwandeln des zBuffers in eine Textur ist damit nicht möglich. Auch in einem Pixelshader kann nicht auf den bestehenden Tiefenwert eines Pixels der Szene zugegriffen werden. Folglich muss die Tiefeninformation separat gewonnen werden, in dem die gesamte Szene nach dem normalen Rendern nochmals gerendert und die Tiefe jedes Pixels in einer Textur gespeichert wird. Dies kann mit zwei verschiedenen Methoden erledigt werden: 1. Eine sehr einfache Methode ist, alle Objekte mit einer schwarzen Farbe zu rendern und einen weißen, linearen, entfernungsabhängigen Standardnebel zu aktivieren, der von der Kamera bis zum Horizont reicht. Dieses Verfahren erzeugt ein Bild mit den Tiefeninformationen, hat aber einen entscheidenden Nachteil: Die Tiefeninformation wird nur als 8-Bit-Grauwert gespeichert und damit existieren nur maximal 256 verschiedene Tiefen. Das mag zur Darstellung der Tiefe reichen, versagt aber, wenn die Differenz zur Feststellung der Nebelstärke berechnet werden muss. Der Nebel wird sehr blockig und fehlerhaft, womit diese Methode bei großen Sichtweiten nicht anwendbar ist. 2. Sinnvoller, aber auch aufwändiger, ist dieses Verfahren: Anstatt die Tiefeninformation nur als Grauwert zu speichern, wird der komplette RGB-Farbraum verwendet. Damit stehen 24-Bit zur Verfügung, die für jede erdenkliche Sichtweite ausreichen sollten. Ein Bild mit einer so kodierten Tiefeninformation lässt sich leider nicht mit einem einfachen entfernungsabhängigen Nebel erzeugen, sondern benötigt einen Vertex- und Pixelshader. Diese brauchen aber nur die Geometrieinformationen eines Objekts, womit die Shader praktisch zu allen Objekten einer Szene kompatibel sind und keine speziellen Anpassungen benötigen. Da die erste Methode ausscheidet, wird nachfolgend nur die zweite beschrieben. Die Aufgabe ist, die Tiefeninformation, die als Fließkommazahl vorliegt, in drei Integerwerte mit je 8-Bit umzuwandeln. Dies kann zum größten Teil in einem Vertexshader geschehen, da Tiefen ohne weiteres auf einem Polygon interpoliert werden können. Der Pseudocode für den Vertexshader sieht folgendermaßen aus: Depth = Tiefe des Pixel (0.0=Kamera, 1.0=Horizont); TextureCoordinate0.u = Depth * 1.0; TextureCoordinate1.u = Depth * 256.0; TextureCoordinate2.u = Depth * 65536.0; Anhand der drei erstellten Texturkoordinaten kann im Pixelshader auf die Farben von diesen drei Texturen zugegriffen werden, die einen linearen Farbverlauf der drei Grundfarben enthalten: Abbildung 7 Roter Farbverlauf Abbildung 8 Grüner Farbverlauf Abbildung 9 Blauer Farbverlauf In dem man für den Texturzugriff 'wrap' (Umbruch) einstellt, wird durch den Texturzugriff im Prinzip ein Modulo von 1.0 berechnet, also nur die Nachkommastellen der drei Texturkoordinaten verwendet. Pro Rotwert werden 256 Grünwerte verwendet und pro Grünwert 256 Blauwerte. Somit kommt man zu denen im Vertexshader verwendeten Faktoren. Abbildung 10 Bild mit kodierter Tiefe Der Rotkanal ist auf diesem Bild vorhanden, aber auf Grund der geringen Sichtweite nur in sehr dunklen Tönen. Die einzelnen Farbkanäle dieses Bilds sehen folgendermaßen aus: Abbildung 11 Kodierte Tiefe (Rotkanal) Abbildung 12 Kodierte Tiefe (Grünkanal) Abbildung 13 Kodierte Tiefe (Blaukanal) Mit folgender einfacher Formel kann die Tiefe wieder aus dem Farbwert eines Pixels dekodiert werden: Depth = Pixel.r/1.0 + Pixel.g/256.0 + Pixel.b/65536.0; Diese Rechnung entspricht einem Punktprodukt des Pixels mit dem Vektor (1.0, 1.0/256.0, 1.0/65536.0) und kann somit ohne Probleme in einem Pixelshader berechnet werden. Mit dem Erzeugen der drei Texturen mit den kodierten Tiefeninformationen der Szene, der unteren und der oberen Nebelbegrenzungsebene sind die Vorarbeiten zur Erzeugung der höhenbasiertem Nebels abgeschlossen. Kombinierung der Szenentiefe mit der Tiefe der unteren Nebelbegrenzungsebene Um die untere Begrenzung des Nebels zu berücksichtigen, muss sie mit der Tiefeninformation der Szene kombiniert werden. Für die Berechnung des Nebels sind die Tiefeninformationen der Szene unterhalb der unteren Nebelbegrenzungsebene unerheblich. Es muss also von beiden Bildern für jeden Pixel jeweils die kleinere Tiefe übernommen werden. Dies kann komplett in einem Pixelshader geschehen: Diff.rgb = ScenePixel.rgb – BackPlanePixel.rgb; DepthDiff = Diff.r/1.0 + Diff.g/256.0 + Diff.b/65536.0; if (DepthDiff >= 0) OutputPixel.rgb = BackPlanePixel.rgb; else OutputPixel.rgb = ScenePixel.rgb; Berechnung der Nebelstärke Anschließend kann ähnlich einfach durch Kombination mit der Tiefeninformation der oberen Nebelbegrenzungsebene die Nebelstärke für jeden Pixel berechnet werden, da nur die Differenz der beiden Bilder erzeugt werden muss: Diff.rgb = SceneAndBackPlanePixel.rgb – FrontPlanePixel.rgb; DepthDiff = Diff.r/1.0 + Diff.g/256.0 + Diff.b/65536.0; DepthDiff = DepthDiff * FogDensity; OutputPixel.rgb = FogColor.rgb OutputPixel.a = DepthDiff; Die Nebelstärke wird als Alphawert verwendet, womit der Nebel auf die darunterliegende Szene korrekt aufgetragen wird. Damit ist der höhenbasierte Nebel per Post-Processing fertig, aber natürlich gibt es noch Probleme und Sonderfälle zu berücksichtigen. Probleme Halbtransparente Objekte in der Szene Die oben beschriebene Methode zur Gewinnung der Tiefeninformationen der Szene funktioniert nur bei soliden Objekten. Bei Objekten mit halbdurchsichtigen Flächen (entweder durch Alphatextur oder bei manuellen Alphawerte) müssen diese Alphawerte ebenfalls berücksichtig werden. Leider können in den Bildern keine Tiefen von halbdurchsichtigen Flächen gespeichert werden, da pro Pixel nur eine Tiefe vorhanden sein kann. Foglich muss bei solchen Objekte eine Schwelle verwendet werden, die festlegt, ob ein Pixel komplett transparent oder komplett sichtbar ist. Je nachdem wird die Tiefe des vorhandenen oder des neuen Pixels gespeichert. Damit werden halbtransparente Flächen zwangsläufig falsch 'eingenebelt'. Dies lässt sich nicht vermeiden, aber dürfte in vielen Fällen nicht auffallen. Beliebige Kamerapositionen Das bis jetzt beschriebene Verfahren zur Nebelberechnung funktioniert nur, wenn sich die Kamera oberhalb der oberen Nebelgrenze befindet. Diese Einschränkung lässt sich leicht aufheben, indem die Methode an die drei möglichen Kamerazuständen angepasst wird: 1. Kamera befindet sich oberhalb der oberen Nebelgrenze (wie oben beschrieben): • Szenentiefen mit Tiefen der unteren Nebelbegrenzungsebene kombinieren (Minimum) • Ergebnis mit Tiefen der oberen Nebelbegrenzungsebene kombinieren (Differenz) 2. Kamera befindet sich unterhalb der unteren Nebelgrenze: • Szenentiefen mit Tiefen der oberen Nebelbegrenzungsebene kombinieren (Minimum) • Ergebnis mit Tiefen der unteren Nebelbegrenzungsebene kombinieren (Differenz) 3. Kamera befindet zwischen der unteren und oberen Nebelgrenze: • Szenentiefen mit Tiefen der unteren und oberen Nebelbegrenzungsebene kombinieren (Minimum) • Ergebnis mit einem schwarzen Bild kombinieren (Differenz) Fehler durch Clipplane der Kamera vermeiden Jede Kamera in einer üblichen 3DAnwendung besitzt einen Sichtkegel, dessen Spitze sich an der Position der Kamera befindet. Innerhalb dieses Kegels befinden sich zwei Ebenen, die eine begrenzt die Sichtweite (FarClipPlane), die andere verhindert Störungen knapp vor der Kamera (NearClipPlane). Nur Objekte zwischen diesen beiden Ebenen und innerhalb des Kegels werden gerendert. Ein Problem taucht nun auf, wenn sich die Kamera nah an der Nebelgrenze befindet und die Ebene die NearClipPlane schneidet. Abbildung 14 Clip-Problem Im unteren Bereich des Bildes werden damit keine Tiefeninformationen der Nebelgrenze erzeugt, was zu Fehlern in der Berechnung des Nebels führt. Eine einfache (etwas unsaubere) Lösung ist, die Nebelgrenzen stets so zu korrigieren, dass sie nicht die NearClipPlane schneiden. Da die NearClipPlane im Allgemeinen sehr nah an der Kamera ist, fällt die Korrektur der Nebelgrenzen in der Praxis kaum auf. Wer darauf achtet, wird es allerdings bemerken. Das selbe Problem existiert natürlich auch ander unteren Nebelgrenze. Praktische Umsetzung Die nun folgenden Codeausschnitte stammen aus einer C++, Direct3D, Pixelshader1.1, Vertexshader1.1 Anwendung, sie sollten aber ohne weiteres in andere Programmiersprachen und 3D-APIs übertragbar sein. Diese Ausschnitte sollen nur Anhaltspunkte für die Programmierung sein und sind mit Sicherheit nicht eins zu eins in anderen Anwendung verwendbar. Rendern der Tiefeninformationen Einstellungen und Aufruf des Renderers: //set constants for depth vertex shader //factors for converting depth in r, g, b pD3DDevice->SetVertexShaderConstantF(22, D3DXVECTOR4(1.0f, 256.0f, 65536.0f, 1.0f/MAX_VIEWDISTANCE), 1); //world*view*projection matrix to calculate vertex clip space position D3DXMatrixTranspose( &matTemp, &matWorldViewProj); pD3DDevice->SetVertexShaderConstantF(0, matTemp, 4); //set encode textures (1-3) and optional an alpha texture (0) pD3DDevice->SetTexture(0, NULL); pD3DDevice->SetTexture(1, pRampRedTexture); pD3DDevice->SetTexture(2, pRampGreenTexture); pD3DDevice->SetTexture(3, pRampBlueTexture); //enable testing... but not blending (MIN_ALPHA = treshold) pD3DDevice->SetRenderState(D3DRS_ALPHATESTENABLE, true); pD3DDevice->SetRenderState(D3DRS_ALPHAREF, MIN_ALPHA); pD3DDevice->SetRenderState(D3DRS_ALPHAFUNC, D3DCMP_GREATEREQUAL); //set wrap mode and disable filtering for all ramp textures pD3DDevice->SetSamplerState(1, D3DSAMP_ADDRESSU, D3DTADDRESS_WRAP); pD3DDevice->SetSamplerState(1, pD3DDevice->SetSamplerState(1, pD3DDevice->SetSamplerState(1, pD3DDevice->SetSamplerState(1, pD3DDevice->SetSamplerState(1, D3DSAMP_ADDRESSV, D3DTADDRESS_WRAP); D3DSAMP_ADDRESSW, D3DTADDRESS_WRAP); D3DSAMP_MINFILTER, D3DTEXF_POINT); D3DSAMP_MAGFILTER, D3DTEXF_POINT); D3DSAMP_MIPFILTER, D3DTEXF_NONE); pD3DDevice->SetSamplerState(2, pD3DDevice->SetSamplerState(2, pD3DDevice->SetSamplerState(2, pD3DDevice->SetSamplerState(2, pD3DDevice->SetSamplerState(2, pD3DDevice->SetSamplerState(2, D3DSAMP_ADDRESSU, D3DTADDRESS_WRAP); D3DSAMP_ADDRESSV, D3DTADDRESS_WRAP); D3DSAMP_ADDRESSW, D3DTADDRESS_WRAP); D3DSAMP_MINFILTER, D3DTEXF_POINT); D3DSAMP_MAGFILTER, D3DTEXF_POINT); D3DSAMP_MIPFILTER, D3DTEXF_NONE); pD3DDevice->SetSamplerState(3, pD3DDevice->SetSamplerState(3, pD3DDevice->SetSamplerState(3, pD3DDevice->SetSamplerState(3, pD3DDevice->SetSamplerState(3, pD3DDevice->SetSamplerState(3, D3DSAMP_ADDRESSU, D3DTADDRESS_WRAP); D3DSAMP_ADDRESSV, D3DTADDRESS_WRAP); D3DSAMP_ADDRESSW, D3DTADDRESS_WRAP); D3DSAMP_MINFILTER, D3DTEXF_POINT); D3DSAMP_MAGFILTER, D3DTEXF_POINT); D3DSAMP_MIPFILTER, D3DTEXF_NONE); //set depth pixel and vertex shader pD3DDevice->SetPixelShader(pDepthPixelShader); pD3DDevice->SetVertexShader(pDepthVertexShader); pD3DDevice->SetVertexDeclaration(pDepthVertexDecl); //begin rendering depth texture LPDIRECT3DSURFACE9 pDepthSurface = NULL; if (FAILED(hr=pDepthTexture->GetSurfaceLevel(0,&pDepthSurface))) return hr; if (FAILED(hr=pRenderToSurface->BeginScene(pDepthSurface,NULL))) return hr; //clear device pD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0xFFFFFFFF, 1.0f, 0.0f); //render all things of szene /*TODO*/ //end rendering texture if (FAILED(hr=pRenderToSurface->EndScene(D3DX_FILTER_NONE))) return hr; SAFE_RELEASE(pDepthSurface); Vertexshader: vs.1.1 dcl_position v0 dcl_texcoord v1 ; Transform dp4 oPos.x, dp4 oPos.y, dp4 oPos.z, dp4 oPos.w, position to clip space and output it v0, c0 v0, c1 v0, c2 v0, c3 ; calculate depth dp4 r1.x, v0, c2 ; scale down mul r1.x, r1.x, c22.w ; set value to other coordinates mov r1.y, r1.x mov r1.z, r1.x ; calculate look up table coordinates for depth mul r0.xyz, r1.xyz, c22.xyz ; output as texture coordinate for red, green and blue ramp texture mov oT1.x, r0.x mov oT2.x, r0.y mov oT3.x, r0.z ; output alpha texture coordinate mov oT0, v1 Pixelshader: ps.1.1 ; get textures tex t0 ; alpha texture tex t1 ; red color ramp tex t2 ; green color ramp tex t3 ; blue color ramp ; put colors from ramps in output mov r0, t1 add r0, r0, t2 add r0, r0, t3 ; set alpha from alpha texture mov r0.a, t0 Kombination der Szenentiefen mit den Tiefen der hinteren Nebelbegrenzungsebene Einstellungen und Aufruf des Renderers: #define D3DFVF_POSTPROCESSINGVERTEX (D3DFVF_XYZRHW | D3DFVF_TEX3) struct POSTPROCESSINGVERTEX { D3DXVECTOR3 p; float rhw; D3DXVECTOR2 t0; D3DXVECTOR2 t1; }; //set post processing settings pD3DDevice->SetRenderState(D3DRS_ZENABLE, FALSE); pD3DDevice->SetRenderState(D3DRS_ZWRITEENABLE, FALSE); pD3DDevice->SetFVF(D3DFVF_POSTPROCESSINGVERTEX); //set back pixel shader pD3DDevice->SetPixelShader(pBackPixelShader); //set pixel shader constant pD3DDevice->SetPixelShaderConstantF(1, D3DXCOLOR(0,0,0,0.5f), 1); pD3DDevice->SetPixelShaderConstantF(3, D3DXVECTOR4(1.0f, 1.0f/256.0f, 1.0f/65536.0f, MAX_VIEWDISTANCE), 1); //set textures pD3DDevice->SetTexture(0, pDepthTexture); pD3DDevice->SetTexture(1, pBackFogLayerTexture); //combine back fog layer with depth texture LPDIRECT3DSURFACE9 pDepthAndBackFogLayerSurface = NULL; if (FAILED(hr=pDepthAndBackFogLayerTexture->GetSurfaceLevel(0,&pDepthAndBackFogLayerSurface))) return hr; if (FAILED(hr=pRenderToSurface->BeginScene(pDepthAndBackFogLayerSurface,NULL))) return hr; //clear device pD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0xFFFFFFFF, 1.0f, 0.0f); //set vertex stream pD3DDevice->SetStreamSource(0, pPostProcessingVB, NULL, sizeof(POSTPROCESSINGVERTEX)); //render screen quad if (FAILED(hr=pD3DDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2))) return hr; //end rendering texture if (FAILED(hr=pRenderToSurface->EndScene(D3DX_FILTER_NONE))) return hr; SAFE_RELEASE(pDepthAndBackFogLayerSurface); pPostProcessingVB ist ein Rechteck mit den Koordinaten (0, 0, Bildschirmbreite, Bildschirmhöhe). Pixelshader: ps.1.1 ; get textures tex t0 ; depth image tex t1 ; back fog layer ; make difference between both distances add r1, -t1, t0 ; calculate real depth dp3 r0, r1, c3 ; compare against 0.5 (pixel shader 1.1 restriction) add r0.a, r0, c1 ; use nearer depth cnd r0, r0.a, t1, t0 Kombination der Szenentiefen mit den Tiefen der hinteren Nebelbegrenzungsebene Einstellungen und Aufruf des Renderers: #define D3DFVF_POSTPROCESSINGVERTEX (D3DFVF_XYZRHW | D3DFVF_TEX3) struct POSTPROCESSINGVERTEX { D3DXVECTOR3 p; float rhw; D3DXVECTOR2 t0; D3DXVECTOR2 t1; }; //set post processing settings pD3DDevice->SetRenderState(D3DRS_ZENABLE, FALSE); pD3DDevice->SetRenderState(D3DRS_ZWRITEENABLE, FALSE); pD3DDevice->SetFVF(D3DFVF_POSTPROCESSINGVERTEX); //enable alpha blending pD3DDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, true); pD3DDevice->SetRenderState(D3DRS_SRCBLEND,D3DBLEND_SRCALPHA); pD3DDevice->SetRenderState(D3DRS_DESTBLEND,D3DBLEND_INVSRCALPHA); pD3DDevice->SetRenderState(D3DRS_ALPHATESTENABLE, true); pD3DDevice->SetRenderState(D3DRS_ALPHAREF, MIN_ALPHA); pD3DDevice->SetRenderState(D3DRS_ALPHAFUNC, D3DCMP_GREATEREQUAL); //set final pixel shader pD3DDevice->SetPixelShader(pPixelShaderFinal); //set pixel shader constant pD3DDevice->SetPixelShaderConstantF(3, D3DXVECTOR4(1.0f, 1.0f/256.0f, 1.0f/65536.0f, MAX_VIEWDISTANCE), 1); //set textures pD3DDevice->SetTexture(0, pDepthAndBackFogLayerTexture); pD3DDevice->SetTexture(1, pFrontFogLayerTexture); //set vertex stream pD3DDevice->SetStreamSource(0, pPostProcessingVB, NULL, sizeof(POSTPROCESSINGVERTEX)); //render quad if (FAILED(hr=pD3DDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2))) return hr; pPostProcessingVB ist ein Rechteck mit den Koordinaten (0, 0, Bildschirmbreite, Bildschirmhöhe). Pixelshader: ps.1.1 ; pixel color from depth + back fog layer texture tex t0 ; get pixel color from front fog layer tex t1 ; get difference of both depth add t2, t0, -t1 ; transform rgb depth to real depth dp3 t3, t2, c3 ; more add_x4 add_x4 add_x4 power... hr, hr, hr t3.a, t3, t3 t3.a, t3, t3 t3.a, t3, t3 ; set color and density mov r0, c0 ; set alpha = depth mul r0.a, r0, t3 Ergebnis Ein mit diesem Verfahren erzeugtes Bild sieht folgendermaßen aus: Abbildung 15 Ergebnis Hinweise Die Beispielbilder und der Code stammen aus der Anwendung (http://www.scapemaker.de.vu), für die dieses Verfahren entwickelt wurde. ScapeMaker Grundlegende Ideen wurden aus der Nvidia-Präsentation 'Direct3D Special Effects' entnommen. Ersteller und Rechteinhaber für dieses Dokument: Dirk Plate (email@dplate.de)