Three.js - Shaders: Unterschied zwischen den Versionen

Aus Wikizone
Wechseln zu: Navigation, Suche
 
(6 dazwischenliegende Versionen von 2 Benutzern werden nicht angezeigt)
Zeile 1: Zeile 1:
 
== Links ==
 
== Links ==
  Three.js - Shader Snippets
+
  [[ThreeJS - Snippets]]
 +
[[Three.js - Fireflies Shader]]
 +
[[Three.js - Perlin Noise Shader]]
  
 
  https://www.shaderific.com/glsl-functions - Überblick über Funktionen
 
  https://www.shaderific.com/glsl-functions - Überblick über Funktionen
Zeile 742: Zeile 744:
 
  gl_FragColor = vec4(vUv, 1.0, 1.0);
 
  gl_FragColor = vec4(vUv, 1.0, 1.0);
 
Ansonsten bei Fehlern in der Konsole schauen was passiert.
 
Ansonsten bei Fehlern in der Konsole schauen was passiert.
 +
 +
== Shader Snippets ==
 +
=== Starter ===
 +
vertex.glsl
 +
<syntaxhighlight lang="c">
 +
void main()
 +
{
 +
    vec4 modelPosition = modelMatrix * vec4(position, 1.0);
 +
    vec4 viewPosition = viewMatrix * modelPosition;
 +
    vec4 projectionPosition = projectionMatrix * viewPosition;
 +
 +
    gl_Position = projectionPosition;
 +
}
 +
</syntaxhighlight>
 +
 +
fragment.glsl
 +
<syntaxhighlight lang="c">
 +
void main()
 +
{
 +
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
 +
}
 +
</syntaxhighlight>

Aktuelle Version vom 6. Februar 2022, 10:50 Uhr

Links[Bearbeiten]

ThreeJS - Snippets
Three.js - Fireflies Shader
Three.js - Perlin Noise Shader
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

Quickstart[Bearbeiten]

  • Attributes - nur an Vertex, zum Ändern von einzelnen Vertices
  • Varying - Senden von Vertex zu Fragment Shader
  • Uniform - Senden von Settings aus JavaScript an Vertex und Fragment Shader

Einführung[Bearbeiten]

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?[Bearbeiten]

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[Bearbeiten]

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?[Bearbeiten]

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[Bearbeiten]

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[Bearbeiten]

ShaderMaterial / RawShaderMaterial[Bearbeiten]

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[Bearbeiten]

Extension für Syntaxhighlight in VSC[Bearbeiten]

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

Webpack anpassen[Bearbeiten]

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[Bearbeiten]

Properties[Bearbeiten]

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[Bearbeiten]

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[Bearbeiten]

Das Wichtigste in Kürze[Bearbeiten]

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[Bearbeiten]

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[Bearbeiten]

Das Wichtigste in Kürze[Bearbeiten]

  • 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[Bearbeiten]

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[Bearbeiten]

script.js - set Attribute with a random numbers for each vertex

// Geometry
const geometry = new THREE.PlaneGeometry(1, 1, 32, 32)
// count vertices
const count = geometry.attributes.position.count 
// create random for each vertex
const randoms = new Float32Array(count)
for(let i = 0; i < count; i++)
{
    randoms[i] = Math.random()
}
// set attribute and tell buffer to use one value per vertex
geometry.setAttribute('aRandom', new THREE.BufferAttribute(randoms, 1))

vertex.glsl - use submitted value for z-shift

attribute float aRandom; // make attribute accessible
//...
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;
}

Variying[Bearbeiten]

Attribute können nicht im Fragment Shader verwendet werden. Aber wir können ein Varying erzeugen und damit das Attribut weitergeben.

Im Beispiel nutzen wir das Zufalls-Attribut von oben und leiten es als Varying an den Fragmentshader weiter. Der nutzt es dann um die Farbe des Vertex anzupassen. Farben dazwischen werden automatisch interpoliert.

Beispiel: Farbe abhängig von Zufallswert ändern.

vertex.glsl

//...
attribute float aRandom;
varying float vRandom; // create new varying
void main()
{
    // ...
    vRandom = aRandom; // copy 
}

fragment.glsl

float vRandom; // get varying
void main()
{
    // use as blue value
    gl_FragColor = vec4(0.5, vRandom, 1.0, 1.0);
}

Uniform[Bearbeiten]

Mit Uniforms kann man Parameter von JavaScript aus sowohl an den Vertex, als auch an den Fragment Shader senden.

Der Parameter ist für jeden Vertex der Selbe (im Gegensatz zu Attributes, bei denen der Vertexshader für jeden Vertex einen eigenen Wert bekommen hat).

Uniforms werden im direkt im Material und nicht wie die Attribute in der Geometrie gesetzt. Logisch - Attribute werden ja auch nur auf die Geometrie angewandt.

Die Variablen projectionMatrix, viewMatrix, und modelMatrix sind Uniforms, die Three.js für uns bereits erstellt hat.

Im Beispiel möchten wir den Frequenzparameter unserer Welle von JavaScript aus setzen. Wir definieren im Shader Material

const material = new THREE.RawShaderMaterial({
    vertexShader: testVertexShader,
    fragmentShader: testFragmentShader,
    uniforms:
    {
        uFrequency: { value: 10 }
    }
})

Der Name des Uniforms kann frei gewählt werden. Hier ist es uFrequency.

Hinweis: In älteren Beispielen sieht man auch andere Varianten die früher in Three.js genutzt werden mußte z.b. in dieser Art:

uFrequency: { value: 10, type: 'float' }. 

Im Vertex Shader können wir jetzt den Parameter uFrequency nutzen. Z.b so

// ...
uniform vec2 uFrequency;
// ...
void main()
{
    // ...
    modelPosition.z += sin(modelPosition.x * uFrequency.x) * 0.1;
    modelPosition.z += sin(modelPosition.y * uFrequency.y) * 0.1;
    // ...
}

Cool - jetzt kann man auch über die gui Werte setzen:

gui.add(material.uniforms.uFrequency.value, 'x').min(0).max(20).step(0.01).name('frequencyX')
gui.add(material.uniforms.uFrequency.value, 'y').min(0).max(20).step(0.01).name('frequencyY')

Animation[Bearbeiten]

Bewegungsparameter[Bearbeiten]

Wir nutzen ein weiteres Uniform uTime mit dem wir die elapsedTime übergeben. Diese können wir in der Funktion nutzen und erzeugen so eine Abhängigkeit von der Zeit -> wellenaertige Animation. für mehr Kontrolle erstellen wir noch ein uSpeed und ein uAmplitude. Für diese können wir noch ein gui Element erstellen und nutzen den Wert ebenfalls in unserer Formel. So können wir die Bewegung gut testen.

Mesh skalieren[Bearbeiten]

Damit das quadratische Mesh mehr nach Flagge ausschaut skalieren wir es außerdem noch. Das können wir direkt im Mesh machen ohne dass wir im Shader etwas tun müssen. script.js

// Material
const material = new THREE.RawShaderMaterial({
    vertexShader: testVertexShader,
    fragmentShader: testFragmentShader,
    uniforms:{
        uFrequency: { value: new THREE.Vector2(10, 5) },
        uTime: {value: 0},
        uSpeed: {value: 1},
        uAmplitude: {value: 0.1}
    }
})  

// Mesh
const mesh = new THREE.Mesh(geometry, material)
mesh.scale.y = 2/3 // auf 3:2 skalieren indem wir in y-Richtung verkleinern
scene.add(mesh)

gui.add(material.uniforms.uFrequency.value, 'x').min(0).max(20).step(0.01).name('frequencyX')
gui.add(material.uniforms.uFrequency.value, 'y').min(0).max(20).step(0.01).name('frequencyY')
gui.add(material.uniforms.uSpeed, 'value').min(0).max(10).step(0.1).name('speed')
gui.add(material.uniforms.uAmplitude, 'value').min(0).max(0.2).step(0.01).name('amplitude')
//..
const tick = () =>
{
    const elapsedTime = clock.getElapsedTime()

    // Update material
    material.uniforms.uTime.value = elapsedTime

    //...

    // Call tick again on the next frame
    window.requestAnimationFrame(tick)
}

tick()

vertex.glsl

//...
uniform vec2 uFrequency; // our uniform set in material
uniform float uTime;
uniform float uSpeed;
uniform float uAmplitude;
//...
void main()
{
  //...
  modelPosition.z += sin(modelPosition.x * uFrequency.x - uTime * uSpeed ) * uAmplitude; 
  modelPosition.z += sin(modelPosition.y * uFrequency.y - uTime * uSpeed) * uAmplitude; 
}

Texture und Color im eigenen Shader[Bearbeiten]

Farbe[Bearbeiten]

Farben lassen sich sehr einfach an den Fragment Shader übermitteln. Im Script fügen wir ein weiteres Uniform hinzu:

uColor: {value: new THREE.Color('#ff99dd')}

Da Three.js intern ein Vector3 Objekt können wir das direkt zum Shader schicken. Dort steht es dann als vec3 Objekt zur Verfügung:

        precision mediump float;
        
        uniform vec3 uColor;

        void main()
        {

            gl_FragColor = vec4(uColor.r, uColor.g, uColor.b, 1.0);
        }

Hinweis: Die Werte in vec3 Objekten kann man über x,y,z aber auch r,g,b auslesen. Das ist in diesem Fall naheliegender.

Texture[Bearbeiten]

Um eine Textur im Fragment Shader zu nutzen gibt es eine eigene Funktion: texture2D. Diese Funktion benötigt die Textur und zusätzlich die uv Koordinaten als Anweisung wie die Textur aufgebracht werden soll.

1. Textur laden

Mit dem TextureLoader -> Textur laden. Und neues Uniform erstellen mit dem wir die Textur senden. Im FragmentShader die Textur als 2DSampler empfangen

'2 UV-Koordinaten senden

Die uv-Koordinaten finden wir in der Geometrie. In unserem Plane hat Three.js das uv Property erstellt. Dieses holen wir uns direkt im Vertex Shader ab und senden es an den Fragment Shader. Dafür erstellen wir ein Varying vUv und füllen es in main mit den jeweils anfallenden Werten.

script.js

/**
 * Textures
 */
const textureLoader = new THREE.TextureLoader()
const flagTexture = textureLoader.load('/textures/flag-germany-sq.png')
//...
// Material
const material = new THREE.RawShaderMaterial({
    vertexShader: testVertexShader,
    fragmentShader: testFragmentShader,
    uniforms:{
        //...
        uTexture: {value: flagTexture}
    },
})

vertex.glsl

// ...
attribute vec2 uv; // get uv property from plane
varying vec2 vUv; // create new varying to store uv values
void main()
{
    // ...
    vUv = uv; // fill vUv with uv values
    // ...
}

fragment.glsl

        precision mediump float;
    
        uniform sampler2D uTexture;
        varying vec2 vUv;

        void main()
        {
            vec4 fragTexture = texture2D(uTexture, vUv);
            gl_FragColor = fragTexture; 

        }

Schatten simulieren Wir können die Höhe der Vertices nutzen umd Schatten zu simulieren. Das reicht für viele Zwecke aus. Dazu übergeben wir die Höhe vom vertex and den fragment shader und nutzen diesen Wert um tiefe Stellen abzudunkeln:

float elevation = 0.0;
// ...
varying float vElevation;

// ... in main loop ...
  elevation += sin(modelPosition.x * uFrequency.x - uTime * uSpeed ) * uAmplitude; 
  elevation += sin(modelPosition.y * uFrequency.y - uTime * uSpeed) * uAmplitude; 
  modelPosition.z += elevation;
  vElevation = elevation;

Fragment Shader

//...
varying float vElevation;
// in main loop ...
  vec4 fragTexture = texture2D(uTexture, vUv); // add texture
  fragTexture.rgb *= vElevation * 2.0 + 0.5; // simulate shadow / since -0.5 to 0.5 add 0.5 / * 2 to spread values

Komplettes Beispiel[Bearbeiten]

script.js

import './style.css'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import * as dat from 'lil-gui'
import testVertexShader from './shaders/test/vertex.glsl'
import testFragmentShader from './shaders/test/fragment.glsl'

/**
 * Base
 */
// Debug
const gui = new dat.GUI()

// Canvas
const canvas = document.querySelector('canvas.webgl')

// Scene
const scene = new THREE.Scene()

/**
 * Textures
 */
const textureLoader = new THREE.TextureLoader()
const flagTexture = textureLoader.load('/textures/flag-germany-sq.png')

/**
 * Test mesh
 */
// 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))

// Material
const material = new THREE.RawShaderMaterial({
    vertexShader: testVertexShader,
    fragmentShader: testFragmentShader,
    uniforms:{
        uFrequency: { value: new THREE.Vector2(10, 5) },
        uTime: {value: 0},
        uSpeed: {value: 1},
        uAmplitude: {value: 0.1},
        uColor: {value: new THREE.Color('#ff99dd')},
        uTexture: {value: flagTexture}
    },
})  

// Mesh
const mesh = new THREE.Mesh(geometry, material)
mesh.scale.y = 2/3
scene.add(mesh)

gui.add(material.uniforms.uFrequency.value, 'x').min(0).max(20).step(0.01).name('frequencyX')
gui.add(material.uniforms.uFrequency.value, 'y').min(0).max(20).step(0.01).name('frequencyY')
gui.add(material.uniforms.uSpeed, 'value').min(0).max(10).step(0.1).name('speed')
gui.add(material.uniforms.uAmplitude, 'value').min(0).max(0.2).step(0.01).name('amplitude')


/**
 * Helpers
 */
const axesHelper = new THREE.AxesHelper()
scene.add(axesHelper)
/**
 * Sizes
 */
const sizes = {
    width: window.innerWidth,
    height: window.innerHeight
}

window.addEventListener('resize', () =>
{
    // Update sizes
    sizes.width = window.innerWidth
    sizes.height = window.innerHeight

    // Update camera
    camera.aspect = sizes.width / sizes.height
    camera.updateProjectionMatrix()

    // Update renderer
    renderer.setSize(sizes.width, sizes.height)
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})

/**
 * Camera
 */
// Base camera
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100)
camera.position.set(0.25, - 0.25, 1)
scene.add(camera)

// Controls
const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true

/**
 * Renderer
 */
const renderer = new THREE.WebGLRenderer({
    canvas: canvas,
})
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
renderer.setClearColor('#262837')

/**
 * Animate
 */
const clock = new THREE.Clock()

const tick = () =>
{
    const elapsedTime = clock.getElapsedTime()

    // Update material
    material.uniforms.uTime.value = elapsedTime

    // Update controls
    controls.update()

    // Render
    renderer.render(scene, camera)

    // Call tick again on the next frame
    window.requestAnimationFrame(tick)
}

tick()

vertex.glsl

uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
uniform vec2 uFrequency; // our uniform set in material
uniform float uTime;
uniform float uSpeed;
uniform float uAmplitude;


attribute vec3 position; 
attribute float aRandom; // our own attribute set in geometry
attribute vec2 uv;

varying float vRandom; // our varying to send to fragment shader
varying vec2 vUv; // stores uv coordinates to send to fragment shader
varying float vElevation;

float elevation = 0.0;

void main()
{
  vRandom = aRandom;
  vUv = uv;
  vec4 modelPosition = modelMatrix * vec4(position, 1.0);
  
  elevation += sin(modelPosition.x * uFrequency.x - uTime * uSpeed ) * uAmplitude; 
  elevation += sin(modelPosition.y * uFrequency.y - uTime * uSpeed) * uAmplitude; 
  modelPosition.z += elevation;
  vElevation = elevation;
  //modelPosition.z += 0.05*sin(floor(10.0 * modelPosition.x)*4321.0); // cornered sin 
  modelPosition.z += aRandom * 0.01; // change via attribute
  vec4 viewPosition = viewMatrix * modelPosition;
  vec4 projectedPosition = projectionMatrix * viewPosition;
  gl_Position = projectedPosition;
}

fragment.glsl

        precision mediump float;
        
        uniform vec3 uColor;
        uniform sampler2D uTexture;
        varying float vRandom;
        varying vec2 vUv;
        varying float vElevation;

        void main()
        {
            //gl_FragColor = vec4(0.5, 0.0, 1.0, 1.0); // purple
            //gl_FragColor = vec4(0.3, vRandom, 1.0, 1.0); // blue based on random varying
            //gl_FragColor = vec4(uColor.r, uColor.g, uColor.b, 1.0); // color from color object
            vec4 fragTexture = texture2D(uTexture, vUv); // add texture
            fragTexture.rgb *= vElevation * 2.0 + 0.5; // simulate shadow / since -0.5 to 0.5 add 0.5 / * 2 to spread values
            gl_FragColor = fragTexture; 
        }

ShaderMaterial statt RawShaderMaterial[Bearbeiten]

Das ShaderMaterial übermittelt automatisch einige Parameter. Daher kann man folgende in den Shadern weglassen, diese stehen automatisch zur Verfügung.

    uniform mat4 projectionMatrix;
    uniform mat4 viewMatrix;
    uniform mat4 modelMatrix;
    attribute vec3 position;
    attribute vec2 uv;
    precision mediump float;

Es werden noch einige weitere übergeben die wir bisher aber nicht benutzt haben.

Shader Debugging[Bearbeiten]

Ist nicht so einfach. Man kann z.B. Werte direkt als Farbwerte nutzen, dann bekommt man einen visuelle Repräsentation der Werte und kann eher Einschätzen was passiert.

gl_FragColor = vec4(vUv, 1.0, 1.0);

Ansonsten bei Fehlern in der Konsole schauen was passiert.

Shader Snippets[Bearbeiten]

Starter[Bearbeiten]

vertex.glsl

void main()
{
    vec4 modelPosition = modelMatrix * vec4(position, 1.0);
    vec4 viewPosition = viewMatrix * modelPosition;
    vec4 projectionPosition = projectionMatrix * viewPosition;

    gl_Position = projectionPosition;
}

fragment.glsl

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