Three.js - Shaders

Aus Wikizone
Wechseln zu: Navigation, Suche

Links

https://www.shaderific.com/glsl-functions - Überblick über Funktionen
https://thebookofshaders.com/ - Gutes Tutorial und guter Überblick
http://localhost/www/LEARNING/ThreeJS/Graphtoy/Graphtoy.html
https://iquilezles.org/www/index.htm - useful math
https://www.youtube.com/watch?v=NQ-g6v7GtoI&list=PL4neAtv21WOmIrTrkNO3xCyrxg4LKkrF7&index=4 - Shader Liniting in VSC und Shader Tutorial
https://www.shadertoy.com/
https://learnopengl.com/Getting-started/Coordinate-Systems

Einführung

Shader sind ein komplexes Thema und du benötigst viel Zeit zum Üben. Man kann aber auch ohne ein Mathegenie zu sein tolle Shader schreiben und es stehen dir ganz neue grafische Möglichkeiten zur Verfügung, die sich sonst nicht realisieren lassen würden.

Quelle zu großen Teilen: https://threejs-journey.com/lessons/27 (Zeichnungen und englischsprachige Abschnitte)

Was ist ein Shader?

Ein Shader ist ein Programm der in der Programmiersprache GLSL (OpenGL ES Shading Language) GLSL Programme werden direkt an die GPU gesendet werden und können rasend schnell verarbeitet werden. Dies ist die Basis von WebGL.

Die Aufgabe des Shaders ist jeden Vertex einer Geometrie zu positionieren und jedes sichtbares Fragment dieser Geometrie einzufärben. Das Ergebnis ist ein fertiges Rendering das wir im Browser über das Canvas Element darstellen können. Die Pixel auf dem Monitor können sich von den Pixeln in einem Rendering unterscheiden. Deshalt nutzt man den Terminus Fragment statt Pixel. Die Fragments beziehen sich auf die kleinste Einheit beim Rendering und bilden quasi das Pendant zu Pixeln in der Renderwelt.

Shader sind nicht auf den Browser beschränkt. Auch native Programme oder Apps können Shader nutzen, hier geht es aber um den Einsatz von Shadern mit Three.js im Browser.

Vertex und Fragment Shader

Der Renderprozess nutzt 2 Arten von Shadern.

Der Vertex Shader verarbeitet alle Geometriedaten (Objekte, Kamera,...) und projiziert sie auf die 2D-Ebene des fertigen Renderbilds.

Der Fragment Shader färbt anschließend jedes sichtbare Fragment des Shaders ein.

Wie funktioniert der Shader?

Es ist wichtig die Arbeitsweise zu verstehen.

Der Vertex Shader wird für jeden Vertex ausgeführt. Dabei bekommt er Daten, wie z.b. die Position, die sich bei jedem Vertex ändern. Diese nennt man Attributes. Daten die für jeden Vertex gleich bleiben nennt man Uniform. Attribute kann man nur an den Vertex Shader senden. Wenn sie im Fragment Shader benötigt werden, muss der Vertex Shader als Varying weitersenden. Uniforms stehen auch direkt im Fragment Shader zur Verfügung.

Wenn der Vertex Shader die Positionierung der Vertices erledigt hat. Färbt der Fragment Shader jedes Fragment ein. Er färbt also nicht nur die Vertices sondern auch die Bereiche dazwischen. Dabei interpoliert er automatisch die Farbe auf Basis der vorhandenen Information (z.B.Farbe der umgebenden Vertices, Faces, Texturen...)

Zusammenfassung

Fehler beim Erstellen des Vorschaubildes: Datei fehlt
  • Der Vertex Shader positioniert Vertices auf dem Rendering.
  • Der Fragment Shader färbt jedes sichtbare Fragment (quasi Pixel) der Geometrie.
  • Der Fragment Shader wird nach dem Vertex Shader ausgeführt.
  • Daten die sich von Vertex zu Vertex unterscheiden nennt man Attribute und können nur an den Vertex Shader gesendet werden.
  • Daten die sich nicht zwischen den Vertices unterscheiden nennt man Uniform.
  • Auf Uniforms kann man im Vertex und im Fragment Shader zugreifen.
  • Wir können mit einem Varying Daten vom Vertex zum Fragmentshader senden.

Eigene Shader in Three.js

ShaderMaterial / RawShaderMaterial

Eigene Shader kann man in Three.js über besondere Materialien realisieren: ShaderMaterial oder RawShaderMaterial.

Bei einem ShaderMaterial kann man etwas Code sparen, da dieses automatisch Code voranstellt, den man (zumindest teilweise) sonst selbst einfügen müßte.

GLSF Code kann man direkt in die Objekte vertexShader und fragmentShader schreiben.

const material = new THREE.RawShaderMaterial({
    vertexShader: `// vertex shader code goes here`,
    fragmentShader: `// fragment shader code goes here`
})

Den Code zwischen die Backticks schreiben ist allerdings nicht besonders sinnvoll. Besser geht es über separate Dateien. wir legen zwei Dateien an. Den Code müssen wir noch nicht verstehen.

/shaders/test/vertex.glsl

        uniform mat4 projectionMatrix;
        uniform mat4 viewMatrix;
        uniform mat4 modelMatrix;

        attribute vec3 position;

        void main()
        {
            gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
        }

/shaders/test/fragment.glsl

        precision mediump float;

        void main()
        {
            gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
        }

Jetzt können wir die Dateien als Variable importieren (wir gehen hier von einem Wepack Projekt aus) und in unseren Shader einfügen.

/script.js

import testVertexShader from './shaders/test/vertex.glsl'
import testFragmentShader from './shaders/test/fragment.glsl'
//...

// Geometry
const geometry = new THREE.PlaneGeometry(1, 1, 32, 32)
// Material
const material = new THREE.RawShaderMaterial({
    vertexShader: testVertexShader,
    fragmentShader: testFragmentShader
})
// Mesh
const mesh = new THREE.Mesh(geometry, material)

scene.add(mesh)

Wenn alles passt können wir den ersten Shader in Aktion sehen.

Eventuell gibt es ein paar Probleme mit unserem Setup. Die gehen wir im folgenden Exkurs an...

Exkurs: VisualStudioCode und Webpack für glsl Dateien einrichten

Extension für Syntaxhighlight in VSC

Shader languages support for VS Code von slevesque
Syntax highlighter for shader language (hlsl, glsl, cg)

Webpack anpassen

Wir müssen Webpack beibringen, wie es mit .glsl files umgehen soll. Dafür müssen wir das rules array anpassen. Das kann in unterschiedlichen Files liegen. Einfach mal von package.json ausgehend über die scripts Property schauen wo die Konfigurationsdateien liegen.

Folgende Regel anlegen:

module.exports = {
    // ...

    module:
    {
        rules:
        [
            // ...

            // Shaders
            {
                test: /\.(glsl|vs|fs|vert|frag)$/,
                type: 'asset/source',
                generator:
                {
                    filename: 'assets/images/[hash][ext]'
                }
            }
        ]
    }
}

This rule solely tells Webpack to provide the raw content of the files having .glsl, .vs, .fs, .vert or .frag as extension.

Re-launch the server with npm run dev and the Webpack error will disappear.

If you log testVertexShader and testFragmentShader, you'll get the shader code as a plain string. We can use these two variables in our RawShaderMaterial.

Shader programmieren

Properties

Most of the common properties we've covered with other materials such as wireframe, side, transparent or flatShading are still available for the RawShaderMaterial:

const material = new THREE.RawShaderMaterial({
    vertexShader: testVertexShader,
    fragmentShader: testFragmentShader,
    wireframe: true
})

But properties like map, alphaMap, opacity, color, etc. won't work anymore because we need to write these features in the shaders ourselves.

GLSL ähnelt sehr stark C. Es ist eine typisierte Sprache. Entsprechend müssen auch Variablen und Funktionen deklariert werden. Es gibt auch ein paar neue Typen. Wir gehen hier nicht in die Tiefe, aber hier ein paar interessante Beispiele für den Einstieg.

float a = 1.0;
int b = 2;
float c = a * float(b); // we have to cast

// functions need return value declared (or void if no return value)
float loremIpsum()
{
    float a = 1.0;
    float b = 2.0;

    return a + b;
}


vec2 foo = vec2(1.0, 2.0); // vector types
foo.x = 1.0; // changing values in vector
foo *= 2.0; // changes both values

vec3 bar = vec3(1.0, 2.0, 3.0); // vec3 is like vec2 with 3 vals
vec3 foo = vec3(0.0); // sets all three vals

vec3 purpleColor = vec3(0.0);
purpleColor.r = 0.5; // use r,g,b or x,y,z to access vals. Both is possible
purpleColor.b = 1.0;

vec3 foo = vec3(1.0, 2.0, 3.0);
vec2 bar = foo.xy; // use xy of foo to setz vec2 (all combinations work)

vec4 foo = vec4(1.0, 2.0, 3.0, 4.0); // vec4 has xyzw alias rgba
vec4 bar = vec4(foo.zw, vec2(5.0, 6.0)); // you can fill with the smaller vecs

// other types are mat2, mat3, mat4, sampler2d

GLSL - Native functions

GLSL has many built-in classic functions such as sin, cos, max, min, pow, exp, mod, clamp, but also very practical functions like cross, dot, mix, step, smoothstep, length, distance, reflect, refract, normalize.

Documentation

https://www.shaderific.com/glsl-functions - Meant for Shaderific iOS application but documentation isn't too bad.
https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/indexflat.php - Deals with OpenGL, but most of the standard functions compatible with WebGL. Let's not forget that WebGL is just a JavaScript API to access OpenGL.

Book of shaders documentation

https://thebookofshaders.com/ - Focused on fragment shaders, great resource to learn and it has its own glossary.

Vertex Shader Quickstart

Das Wichtigste in Kürze

Remember: The vertex shader will convert the 3D vertices coordinates to our 2D canvas coordinates.

  • Die main() Funktion wird für jeden Vertex ausgeführt
  • Der Vertex Shader benötigt Daten über das Objekt aber auch über die Kamera bzw. die Projektion, den Renderausschnitt und das Model. Aus diesen Informationen berechnet er, wo ein Vertex im 2D Raum gesetzt wird.
  • Zur Berechnung nutzt der Shader 3 Matrizen die er nacheinander abarbeitet, bis die Endkoordinaten feststehen:
uniform mat4 modelMatrix; //apply all transformations relative to the Mesh (scale, rotate... in mesh)
uniform mat4 viewMatrix; //apply transformations relative to the camera
uniform mat4 projectionMatrix; //apply clip space transformation

Dies spiegelt sich in der erste Zeile im Beispielcode oben wieder.

  • Um eine vec4 Position zu projizieren multipliziert man ihn einfach mit einer mat4 Matrix. Man "wendet eine Matrix auf einen Vektor" an.
  • The main function will be called automatically. As you can see, it doesn't return anything (void).
  • The gl_Position variable already exists. This variable will contain the position of the vertex on the screen. The goal of the instructions in the main function is to set this variable properly with a vec4.
  • A clip space is a space that goes in all 3 directions (x, y , and z) in a range from -1 to +1. It's like positioning everything in a 3D box. Anything out of this range will be "clipped" and disappear. The fourth value (w) is responsible for the perspective.

Beispiele

Um eine vec4 Position zu projizieren multipliziert man ihn einfach mit einer mat4 Matrix. Das kann man sich zunutze machen. Wenn man den Code oben umschreibt, bekommt man einfachen Zugriff auf die Position des Models.

 vec4 modelPosition = modelMatrix * vec4(position, 1.0);
 //modelPosition.z -= 0.1; // komplettes Modell verschieben
 modelPosition.z += sin(modelPosition.x * 10.0) * 0.1;// sinus(x-position des vertex) > auf y position anwenden
 vec4 viewPosition = viewMatrix * modelPosition;
 vec4 projectedPosition = projectionMatrix * viewPosition;
 gl_Position = projectedPosition;

Fragment Shader Quickstart

Das Wichtigste in Kürze

  • The fragment shader code will be applied to every visible fragment of the geometry. That is why the fragment shader comes after the vertex shader.
  • Die main() Funktion wird für jedes Fragment ausgeführt
  • Man kann die Präzision für Float Werte einstellen: precision mediump float; (highp, mediump,lowp)sollte das aber auf dem mittleren Wert belassen.
  • Ziel der main Funktion ist es die gl_FragColor zu setzen.
  • Jeder Wert von gl_FragColor liegt zwischen 0.0 und 1.0. Werden die Werte überschritten gibt es keinen Fehler aber auch keine Wirkung.
  • Die Werte stehen für rgba (rot, grün, blau, alpha) Damit die Transparenz funktioniert muss im RawShaderMaterial / ShaderMaterial transparent = true gesetzt werden.

Attribute

Attributes können sich für jeden Vertex ändern. Das position Attribut enthält z.B. für jeden Vertex ein eigenes vec3.

Wir können eigene Attribute erstellen und direkt an die Geometrie übergeben.

We will add a random value for each vertex and move that vertex on the z axis according to that value for this lesson.

Beispiel Attribut setzen und nutzen

script.js

// Geometry
const geometry = new THREE.PlaneGeometry(1, 1, 32, 32)

const count = geometry.attributes.position.count
const randoms = new Float32Array(count)
for(let i = 0; i < count; i++)
{
    randoms[i] = Math.random()
}
geometry.setAttribute('aRandom', new THREE.BufferAttribute(randoms, 1))

vertex.gsls

attribute float aRandom;
//...
void main(){
  vec4 modelPosition = modelMatrix * vec4(position, 1.0);
  modelPosition.z += aRandom * 0.1; // change via attribute
  vec4 viewPosition = viewMatrix * modelPosition;
  vec4 projectedPosition = projectionMatrix * viewPosition;
  gl_Position = projectedPosition;
}