Für die 3D-Darstellungen nutzen wir ja threejs. Damit kann man viel machen. Bei besonderen Effekten geht es aber ans Eingemachte: Dann programmieren wir direkt die GPU mit Shadern.
Shader sind kleine Programme, geschrieben in der Sprache GLSL, die auf der GPU ausgeführt werden. Sie haben die Aufgabe, eher abstrakte 3D-Modelle schrittweise umzurechnen, bis sie auf dem Bildschirm dargestellt werden.
Das Besondere an der Shader-Entwicklung ist, dass die GPU hochgradig parallelisiert ist. Die Shader werden also massiv parallel ausgeführt. Darauf muss man natürlich achten.
Die Graphik zeigt, in welchen Schritten innerhalb der GPU aus einem 3D-Modell eine Bildschirmdarstellung wird:
varying vec3 vNormal;
void main() {
vNormal = normal;
gl_Position = projectionMatrix *modelViewMatrix *vec4(position, 1.0);
}
Das ist der langweiligste Vertex-Shader, den man sich vorstellen kann.
Er berechnet zwei Ergebnisse: vNormal
ist der Normalen-Vektor an dem Punkt. Er wird unverändert übernommen. Die Tatsache, dass die Variable als varying
definiert ist, bedeutet, dass sie bei der Rasterisierung interpoliert wird. Variablen die nur einen Wert haben, sind uniform
.
gl_Position
ist die finale Position des Punktes im Projektionsraum. threejs gibt netterweise alle Matrizen direkt mit, die notwendig sind, um den 3D-Punkt auf den 2D-Punkt zu projizieren.
Was man sieht, ist, dass Vektor- und Matrixoperationen in GLSL wirklich kein Problem darstellen.
varying vec3 vNormal;
void main() {
vec3light = vec3(0.5, 0.2, 1.0);
light = normalize(light);
floatdotProd = max(0.0, dot(vNormal, light));
gl_FragColor = vec4(dotProd, dotProd, dotProd, 1.0);
}
In diesem Shader wird angenommen dass aus (0.5, 0.2, 1.0) ein DirectionalLight kommt. Das Skalarprodukt mit dem Normalenvektor ergibt im einfachsten Fall die Helligkeit der Oberfläche an dieser Stelle (einfach mal mit den beiden Händen nachstellen…).
Der Shader gibt das in Graustufen ohne Transparenz aus und man erhält:
Wie kann man nun in threejs mit Shadern konkret umgehen?
Ganz einfach: Man tut es bereits ;-). Wenn man einem Mesh ein Material zuweist, passiert nichts anderes, als dass man damit einen Shader auswählt. Beim PhongMaterial den PhongShader, beim StandardMaterial eben den PBR Shader.
Man kann es aber auch auf die harte Tour machen, indem man ein Material wählt, dem man Shader explizit mitgeben muss:
const vertextShader = "...";
const fragmentShader = "...";
const material = new THREE.RawShaderMaterial({
uniforms: {
time: {
value: 1.0
},
resolution: {
value: new THREE.Vector2()
}
},
vertexShader: vertextShader,
fragmentShader: fragmentShader
});
var displacements = new Float32Array( MAX_POINTS );
for (i=0; i<MAX_POINTS; i++) displacements[i] = Math.random();
geometry.setAttribute( 'displacement', new THREE.BufferAttribute( displacements, 1 ) );
Hier werden die beiden Shader ganz einfach als String-Konstanten in JavaScript deklariert
und dann dem RawShaderMaterial
mitgegeben.
Gezeigt ist auch noch, wie man uniforms
übergibt (hier time
und resolution
) und Attribute an Vertices setzt (hier displacement
).
Man wird aber selten einen Shader komplett selbst schreiben, weil die eingebauten ja schon ziemlich gut sind. Wenn man einen vorhandenen Shader manipulieren will kann man so etwas tun wie:
material.onBeforeCompile = shader => {
const my_uv_vertex = `#ifdef USE_UV
...eigener Code...
#endif`;
shader.vertexShader =
"uniform float uOffset;\nuniform float flip;\n" +
shader.vertexShader.replace("#include <uv_vertex>", my_uv_vertex);
}
Um zu sehen, was da geht, kann man sich einfach mal in der Chrome-Console THREE.ShaderLib
ansehen, da hat man Zugriff auf alle Quelltexte…
Have fun!
Ach ja: Schöne Beispiele für Shader gibts auf shadertoy.