So, heute mal ein paar Worte zum neuen Shader-System:
Da die Engine zur Zeit noch sehr spärlich dokumentiert ist, hier mal ein kleines "Tutorial":
Schritt 1: Demo-Projekt anlegen
Für das Demo-Projekt verwendet man folgenden Sourcecode:
Code: Alles auswählen
using OpenTK;
using OpenWorld.Engine;
using OpenWorld.Engine.SceneManagement;
namespace OpenWorld.Demo.ShaderDemo
{
class ShaderDemo : Game
{
static void Main(string[] args)
{
// Just create an instance and run it...
new ShaderDemo().Run();
}
// Some global fields
Scene scene;
SceneRenderer renderer;
CompositeCamera camera;
protected override PresentationParameters GetPresentationParameters()
{
var p = base.GetPresentationParameters();
// Change the demo window title.
p.Title = "Shader Demo";
return p;
}
protected override void OnLoad()
{
// Basic setup
Assets.Sources.Add(new FileSystemAssetSource("./Assets/"));
this.scene = new Scene();
this.renderer = new DeferredRenderer();
this.renderer.Sky = new ColorSky() { Color = Color.DeepSkyBlue };
this.camera = new CompositeCamera();
this.camera.ProjectionMatrixSource = new Perspective(75);
this.camera.ViewMatrixSource = new LookAt(
new Vector3(-4, 2.5f, -2.5f),
new Vector3(0, 0.5f, 0));
var root = this.scene.Root;
var cube = Model.CreateCube(1.0f);
// Create a default cube
var @default = new SceneNode();
@default.Components.Add<Renderer>().Model = cube;
@default.Parent = root;
// Create a cube with emissive lighting (deferred renderer will apply bloom)
var mtl = new SceneNode();
mtl.Components.Add<Renderer>().Model = cube;
mtl.Transform.LocalPosition = new Vector3(0.5f, 0.1f, 1.6f);
mtl.Material = new Material()
{
Emissive = Color.Lime
};
mtl.Parent = root;
// Create a node with a custom shader on it
var shd = new SceneNode();
shd.Components.Add<Renderer>().Model = cube;
shd.Transform.LocalPosition = new Vector3(-0.5f, 0.1f, -1.6f);
shd.Material = new NoiseMaterial()
{
// Load our custom noise shader here:
Shader = Assets.Load<Shader>("noise")
};
shd.Parent = root;
}
protected override void OnUpdate(GameTime time)
{
this.scene.Update(time);
}
protected override void OnDrawPreState(GameTime time)
{
FrameBuffer.Clear();
this.scene.Draw(this.camera, this.renderer, time);
}
}
}
Der Code sollte selbsterklärend sein, also keine weiteren Worte dazu...
Schritt 2: Shader erstellen
Um einen Custom-Shader zu schreiben, verwendet man Shader-Z (fragt nicht nach dem Namen, ich habe einen gebraucht), eine Sprache ähnlich zu Unitys ShaderLab, aber wesentlich flexibler.
Shader-Z ist ein Lua-Script, welches Shadercode initalisiert. Man könnte also auch beliebige Scripte darin ausführen, welche Shader-Code erzeugen oder laden...
Shader-Z bietet eine Menge Möglichkeiten, schnell und einfach eigene Shader zu entwickeln, welche sich vollständig in die Engine integrieren lassen.
Der Beispielshader hier zeigt einen Noise-Shader, welcher eine leuchtende, wabernde Oberfläche erzeugt:
Code: Alles auswählen
shader:include("time")
shader:include("noise3d");
shader:addDefault("vertex")
shader:add
{
type = "fragment",
source =
[[
out vec4 color;
in vec3 position;
void main()
{
float f =
0.5f + 0.5f * snoise3d(
5.0f * position +
1.0f * vec3(sin(timeTotal), 0.0f, sin(1.1f * timeTotal + 0.3f)) +
0.3f * timeTotal * vec3(1.0f, 0.8f, -0.7f));
if(f < 0.6f)
color.rgb = vec3(1.0f, 1.0f, 0.0f); // Base yellow
else
color.rgb = vec3(100.0f, 0.0f, 0.0f); // "Hot" red
color.a = 1.0f;
}
]]
}
Shader-Z stellt eine globale Variable "shader" im Script bereit, welche dazu verwendet wird, den Shader zu initialisieren und zu erstellen.
shader:include bindet eine Include-Datei in das Shader-Script ein, welche am Anfang zum Shader hinzugefügt wird.
Die Include-Dateien können Engine-Intern (mesh, transform, material, ...) sein oder aber wie hier eine externe Include-Datei namens "noise3d".
Externe Dateien werden über das Asset-System geladen und benötigen die Dateiendung .shi
shader:addDefault fügt einen der Default-Shader hinzu, hier der Vertex-Shader.
Die Default-Shader sind optimiert für die Benutzung mit der Engine und ersparen einem die eigene Implementierung.
shader:add fügt dem Shader ein Fragment hinzu.
Ein Shader-Fragment ist ein Teil eines Shaders, welches nachher zur bedingten Shader-Kompilierung verwendet wird. In diesem Beispiel definieren wir nur ein Fragment für den Fragment-Shader, es sind aber Fragmente für alle normalen Shadertypen (vertex, tess-control, tess-eval, geometry und fragment) verfügbar.
Ein Fragment kann zusätzlich für eine Klasse definiert werden. Die Engine wählt dann je nach Klasse die passenden Fragmente und setzt sich so einen passenden Shader zusammen. So lassen sich für eigene Renderer sehr leicht Spezialfälle hinzufügen (Es gibt die Standardklasse "DeferredRenderer", welche für den internen deferred Renderer verwendet werden kann).
Zudem kann man beim Selektieren eines Shaders in der Engine auswählen, ob man Shader-Fragmente überschreiben möchte. So kann man zum Beispiel sehr einfach Shadowmapping implementieren, da man keinen speziellen Shader dafür schreibt, sondern nur den Fragment-Shader überschreibt, welcher dann die Tiefeninformationen in das RT schreibt.
Es gibt folgende Möglichkeiten, einen Shader zu selektieren:
Code: Alles auswählen
// Load a shader
shader = Assets.LoadSync<Shader>("myCustomShader");
// Select the default shader:
CompiledShader cs = shader.Select();
// Select the shader with class modifier:
CompiledShader cs = shader.Select("DeferredRenderer");
// Create a custom fragment shader
var geometryPixelShader = new ShaderFragment(ShaderType.FragmentShader, geomPixSource);
// Select the shader with class and override the pixel shader.
CompiledShader cs = shader.Select("DeferredRenderer", geometryPixelShader);
// Calls glUseProgram for the compiled shader.
cs.Bind();
Zudem unterstützt die Engine ein sehr bequemes System, um Uniforms an die Shader zu passen:
Code: Alles auswählen
var obj = new Material(); // Create some demo object
cs.BindUniform(obj); // Bind all uniforms defined in obj to the shader.
Das Codeschnipsel erzeugt ein Material (
Source Code), welches verschiedene, via Attribut definierte Uniforms direkt in den Shader passed. Nicht vorhandene Uniforms werden ignoriert.
Für Texture-Uniforms kann man zudem noch eine Default-Textur (Farbe) angeben, welche anstelle der Textur-Fehlt-Textur gebunden wird.
Man kann aber natürlich Uniforms auch manuell setzen:
Wenn für ein Uniform ein Wert gesetzt wird, überprüft die Engine dessen Typ und lässt nur Uniforms vom passenden Typ zu. Heißt, man kann einem Sampler-Objekt nicht einen float-Wert zuweisen und vice versa.
Falls ihr Fragen habt, fragt!
Hier noch ein recht unhübsches Bild des Shaders/Scripts oben in Aktion:
Grüße
Felix