Hallo 1unitedpower
Erstmal danke für die Antwort!
Der ganze <script>-Kram ist in diesem Zusammenhang nur nebensächlich. Wichtig ist eben, dass Shader als Strings an die WebGL-Schnittstelle übergeben werden müssen.
Jep, so ist es. - Ich wollte die Geschichte nur ein wenig ausschmücken. ;-)
Es gibt noch weitere Möglichkeiten: Du kannst beispielsweise statt einem monolithischem Shader auch modular vorgehen und diverse kleine Shader programmieren. Bei Three.js benutzt man einen hybriden Ansatz: Man hat mehrere Shader, aber man kann sie zusätzlich über JavaScript parametresieren.
Naja, das mit dem monolithischen Shader, wie du es so schön genannt hast, ist natürlich in der Praxis vollkommen unrealistisch, ich habe es nur erwähnt, um Uneingeweihten den theoretischen Rahmen der Angelegenheit etwas zu verdeutlichen, beziehungsweise um eben diese "Option" von vorneherein aus den Überlegungen auszuschließen, nur für den Fall, dass jemand dies als Lösungsansatz in Betracht ziehen würde.
Was den modularen Ansatz angeht, das ist keine weitere Option, sondern im Prinzip genau das, was ich mit tatsächlichem Bedarf meinte. Das habe ich wohl schlecht erklärt!
Also die Sache funktioniert bei mir eigentlich genau wie HTML und CSS, das heißt, man erstellt Elemente in etwa nach dem Schema
var earth = scene.createNode({'type' : 'regular', 'parent' : 'root', 'geometry' : 'sphere'});
und weist dann diesen Elementen Eigenschaften zu, entweder direkt bei der Initialisierung wie oben, oder nachträglich einzeln
earth.setNodeName('earth');
oder zusammen:
earth.setProperties({
'position' : [0.0, -2.8, -10.2],
'scale' : [1.3, 1.3, 1.3],
'color' : true,
'color-type' : 'uniform',
'color-rgb' : [20, 130, 210],
'alpha' : 1.0,
'lighting' : true,
'light-model' : 'phong',
'light-position' : [2.4, 8.0, -4.7] // etc...
});
Jedes node-Objekt gewinnt dadurch ein mehr oder weniger individuelles Eigenschaftenprofil und je nach dem, welche Eigenschaften zugewiesen wurden, wird dann automatisch ein exakt passendes Shader-Programm generiert (sofern ein solches nicht bereits auf dem Stack liegt), welches dann von dem Elementknoten verwendet wird.
Man könnte hier also von maximaler Modularisierung sprechen! ;-)
Dabei sei hinsichtlich der Problemstellung noch erwähnt, dass die Werte, die wie oben gesehen für jeden Knoten festgelegt werden, nicht zwingend und in jedem Fall vor Initialisierung des Shaders bestimmt werden müssen: Zwar gibt es Eigenschaften - wie etwa die Wahl des Beleuchtungs-Modells - die für die Shader konstitutiv sind, aber andere Werte, wie beispielsweise die Position usw. können (und müssen) natürlich zur Laufzeit an den Shader übermittelt werden.
Darüber hinaus bestimmt der Benutzer hier über den Grad der Einflussnahme weitestgehend selbst, das heißt, wenn er zum Beispiel nur {'lighting' : true}
angibt, ohne Typ-, Farb- oder Positionsangabe, dann wird halt die Szene einfach entlang der Y-Achse mit weißem Licht von oben angeleuchtet...
Shader sind in der WebGL-Rendering-Pipeline soweit ich weiß starr und können nicht mehr zur Laufzeit verändert werden. Du kannst Shader höchstens nachladen, das ist vermutlich eine sehr teure Operation un solltest du nicht bei jedem Rendering Call machen.
Das ist richtig. Ein Programm in WebGL (bestehend aus Vertexshader und Fragmentshader) muss kompiliert und gelinkt werden, bevor es verwendet werden kann. Eine nachträgliche Änderung ist dann nicht mehr möglich. Man kann das Programm höchstens löschen, Modifikationen am Source-String vornehmen und dann die ganze Prozedur wiederholen. Und ja, das ist eine teure Operation! - Deshalb meine anfänglichen Bedenken hinsichtlich der Shader-Generierung zur Laufzeit (die aber aus Gründen, auf die ich weiter unten noch eingehen werde, nicht so problematisch ist, wie zunächst angenommen).
Welche Einflüsse können denn während einer laufenden Animation dazu führen, dass der Shader ausgetauscht werden muss?
Naja, wie bereits erklärt, werden die Shader exakt nach den Eigenschaftenprofilen der einzelnen Elemente erstellt, das heißt, sie repräsentieren den genauen Status eines Knotens zu einem bestimmten Zeitpunkt. Aber es wäre natürlich ohne weiteres denkbar, dass jemand mit meiner Bibliothek ein Programm schreibt, bei dem sich bestimmte Objekt-Eigenschaften, die für den Shader konstitutiv sind, verändern, wie beispielsweise die Anzahl der Lichtquellen oder gleich das ganze Beleuchtungsmodell, - ich meine, da kommen doch recht viele Szenarien in Frage...
Kann man dieses Problem vielleicht schon auf Shader-Ebene lösen?
Hierzu mal ein kleines Beispiel. Ein sehr einfacher Fragmentshader könnte so aussehen:
precision highp float;
uniform float uAlpha;
uniform vec3 uColor;
uniform vec3 uLightDirection;
uniform vec3 uAmbientColor;
uniform vec3 uLightColor;
varying vec3 vTransformedNormal;
void main (void) {
vec3 reflectedLight = max(dot(vTransformedNormal, uLightDirection), 0.0);
vec3 lightWeighting = uAmbientColor + reflectedLight * uLightColor;
gl_FragColor = vec4(uColor * lightWeighting, uAlpha);
}
Oder aber so:
precision highp float;
uniform float uAlpha;
uniform vec3 uColor;
uniform vec3 uLightDirection;
uniform vec3 uLightPosition;
uniform bool uUseDirectionalLighting;
uniform vec3 uAmbientColor;
uniform vec3 uLightColor;
varying vec4 vPosition;
varying vec3 vTransformedNormal;
void main (void) {
vec3 reflectedLight;
if (uUseDirectionalLighting) {
reflectedLight = max(dot(vTransformedNormal, uLightDirection), 0.0);
} else {
vec3 lightDirection = normalize(uLightPosition - vPosition.xyz);
reflectedLight = max(dot(normalize(vTransformedNormal), lightDirection), 0.0);
}
vec3 lightWeighting = uAmbientColor + reflectedLight * uLightColor;
gl_FragColor = vec4(uColor * lightWeighting, uAlpha);
}
Während der Shader im ersten Fall nur directional-lighting berechnen kann, kann der zweite Shader sowohl directional- als auch position-lighting darstellen, je nach dem, ob zur Laufzeit an die uniform uUseDirectionalLighting
der Wert true
oder false
übergeben wurde, und dieses Spiel kann man natürlich nahezu beliebig weiter treiben (oder bestimmte Komplexe gar gleich in externe Funktionen außerhalb von main
ausgliedern).
Das Problem ist hierbei aber natürlich, dass branching eigentlich unerwünscht weil unperformant ist, insbesondere bei Shadern!
Und das ist eben das Dilemma: Entweder ich gestatte (oder fordere), dass alle Eigenschafts-Zustände, die im Lebenszyklus eines node-Objektes vorkommen sollen, bereits bei der Initialisierung des Shaders festgelegt werden, damit dieser die entsprechende Funktionalität bereitstellen kann, was dann zwangsläufig zu ineffizientem GLSL-Code führt, oder aber ich erlaube es, auch solche Objekteigenschaften zur Laufzeit dynamisch anzupassen, die für den Shader konstitutiv sind, wobei dann aber freilich jedesmal ein neues Shaderprogramm generiert werden muss, - was dann auch wieder Ressourcen kostet. (Zumindest in dem Fall, dass ein solches Programm nicht bereits existiert.)
Warum Letzteres trotzdem die weniger schlechte Alternative ist, habe ich in meinem Nachtrag schon angedeutet, aber es kann sicher nicht schaden, meinen Gedankengang nocheinmal etwas zu erläutern...
Ich denke, bezüglich der Lösung des Problems müssen wir uns folgendes klar machen:
Shader werden auf der GPU per-Vertex ausgeführt, das heißt, Programme wie in dem Beispiel weiter oben werden unter Umständen, je nach dem, wie komplex das darzustellende 3D-Objekt aufgebaut ist, zigtausende Male aufgerufen, um ein Bild zu rendern!
Demgegenüber ist die Erstellung eines Shaderprogramms ein singuläres Ereignis, welches zudem unabhängig von der GPU auf dem Hauptprozessor berechnet wird, so dass der Flaschenhals hier meiner Ansicht nach eindeutig bei den Shadern liegt.
Auch ist zu berücksichtigen, dass WebGL es erlaubt, eine beliebige Anzahl solcher Programme zu linken, so dass Shader, die einmal erstellt wurden, nahezu ohne Performanceeinbußen wiederverwendet werden können.
Und schließlich gibt es zwar eine beachtliche Anzahl an Kombinationsmöglichkeiten hinsichtlich der Komponenten aus denen die Shader aufgebaut sind, aber bezogen auf ein einzelnes Objekt ist in der Praxis davon auszugehen, dass nur eine überschaubare Menge an Veränderungen zur Laufzeit in Betracht kommen werden die eine Neuerstellung des Shaders erfordern, und sich diese Menge aufgrund der Wiederverwendbarkeit der Programme mit jeder Änderung um 1
reduziert. ;-)
Aus genannten Gründen wird es also wohl das Beste sein, an meinem Konzept prinzipiell festzuhalten und auch die Veränderung von Shader-relevanten Objekteigenschaften zur Laufzeit zuzulassen, wobei man allerdings darüber nachdenken sollte, dem Benutzer hier einen Entscheidungsspielraum zuzugestehen, schon alleine aufgrund der kaum Überschaubaren Menge potentieller Use-Cases.
Ich denke hier an drei Möglichkeiten:
Erstens wäre es natürlich möglich, jedem Knoten-Objekt mehr als ein Set an Eigenschaften anzuhängen, so dass man schon vor der Laufzeit alternative Shaderprogramme erstellen könnte.
Zweitens könnte ich es so einrichten, dass multioptionale Shader durch entsprechende Parametrisierung zumindest möglich sind, wobei ich - wie du in deinem Post richtig angemerkt hast - dabei allerdings sehr streng darauf achten muss, dass es nicht zu inkonsistenten Programmzuständen kommt.
Und sollte das alles im Einzelfall nicht zu den gewünschten Ergebnissen führen, bliebe dem Benutzer schließlich noch die Möglichkeit, ein eigenes Shaderprogramm zu verwenden.
Das scheint mir jedenfalls im Moment die beste Lösung zu sein...
Beste Grüße,
var