Total directory

  • An introduction to
    • Environment configuration
    • The first working Shader
    • Draw shapes — rectangles and circles
    • Know SmoothStep
  • A preliminary GLSL
    • Vectors and matrices
    • Floating-point precision
    • uniform
  • Color and Shape
    • Color Basics
    • Formation gradient
    • Rounded rectangle rendering
    • Multi-deformation rendering
  • Mathematics and Graphics
    • Polar coordinates
    • Vector geometry
    • Trigonometric functions
    • parting
  • Generate the art
    • noise
    • Noise field
    • The superposition
    • The fuzzy

Environment configuration

We can download the installation package of the corresponding operating system and CPU instruction set from nodeJs.org, or use homebrew, APT, etc. Most front-end engineers already have nodeJS environment, so I won’t go into details here.

Step2. (optional) install vite globally. To make it easier to use vite, install vite globally. If we do not install vite globally, we must execute this project’s Vite using NPX. Run the NPM install -g vite command.

Step3. Initialize the project and create a new directory in a preferred path, such as here I create an element3-demo

mkdir element3-demo
cd element3-demo
Copy the code

Go to the directory, run NPM init, and fill in the required information. After that, we have a basic package.json file.

Step4. Next, we add the dependencies for the project and install the related packages

First, open package.json with your favorite text editing tool and add dependencies and devDependencies to it:

{" dependencies ": {" element3 - core" : "0.0.7", "vue" : "^ 3.0.5"}, "devDependencies" : {" @ vitejs/plugin - vue ": "^ 1.2.2 @", "vue/compiler - the SFC" : "^ 3.0.5", "a rollup - plugin - element3 - webgl" : "0.0.5", "typescript" : "^ 4.1.3", "vite" : "^2.3.0", "vue-tsc": "^0.0.24"}}Copy the code

Then we go back to the terminal and use the NPM install command.

Step5. Create files and a basic directory structure.

Write the index. HTML file:

<! DOCTYPEhtml>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
Copy the code

Write the SRC /main.ts file:

import { createApp } from "vue";
import App from "./App.vue";

createApp(App).mount("#app");
Copy the code

Write the SRC /app.vue file:

<template>
<div>
    Hello
</div>
</template>
<script lang="ts">

import { defineComponent } from "vue";

export default defineComponent({
  name: "App",
  components: {

  },
  setup(){
    return {
      
    }
  }
});

</script>
Copy the code

Write the vite. Config.js file:

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import element3Webgl from "rollup-plugin-element3-webgl";

// https://vitejs.dev/config/
export default defineConfig({
  base: "/".// TODO development environment is/production environment is/WebGL
  plugins: [vue(), element3Webgl()],
});
Copy the code

After writing, we use NPX vite in the command line, open the page and see Hello to indicate that the environment is configured.

The first working Shader

Next we create a SRC /pure.frag file.

The Fragment Shader doesn’t use JavaScript, but a specialized language called GLSL. I’ll walk you through the features of this language over the course of the tutorial, but let’s try to write our first working Fragment Shader.

First we need to understand the concept of a Fragment Shader, a Fragment Shader is the process of drawing a point on a screen. Its execution frequency is very high, drawing an image of a 100×100 region requires 10,000 executions of the code in the Shader, which is usually undertaken by the GPU.

Next, let’s write some code to paint the canvas area in a solid color:

precision mediump float;

void main(){
    gl_FragColor = vec4(1.0.0.0.0.0.1.0);
}
Copy the code

The Rollup plug-in for Element3 loads the Shader code directly into a Vue component, which helps us to ignore the tedious process of calling the WebGL API.

Next, let’s change the code of app.vue to show the effect of this drawing:

<template>
<div>
    <DrawBlock width=100 height=100></DrawBlock>
</div>
</template>
<script lang="ts">

import { defineComponent } from "vue";
import DrawBlock from "./pure.frag";

export default defineComponent({
  name: "App",
  components: {
    DrawBlock
  },
  setup(){
    return {
      
    }
  }
});

</script>
Copy the code

We can see a pure red square area.

Let’s explain this GLSL code a little bit.

Let’s start with the first sentence: Precision mediump float; . This sentence is necessary. It specifies the global floating-point precision of the program. Medium precision is used here, and almost every Fragment Shader contains this sentence.

All GLSL code is executed from the main function. In GLSL, the main function may not return a value; in this case, we use void instead of the part of the type.

Next, let’s look at the body of main, which contains only one statement. Here we use a gl_fragColor variable, which is a name specified by the GLSL language, not a variable that can be arbitrarily named. As we mentioned earlier, the Fragment Shader is a code that draws a point, and the gl_fragColor is the color of the point that we will output.

At the other end of the equal sign, vec4 represents a vector type of floating point of length 4, which can store four floating point numbers. You remember the vectors from linear algebra, right? Here vec4 comes from the concept of vectors in mathematics. It’s a little bit like an array in JavaScript, except it’s fixed length, and it’s a data structure that’s very useful for graphics algorithms, and we’ll be dealing with it a lot in the future.

Finally, GLSL does not allow omits of semicolons. Forgetting this will cause the entire program to fail to compile, so be careful.

At this point, we’ve learned how to use element3’s rollup plug-in to load a Fragment Shader, giving us a basic environment for debugging and running the code.

Next, let’s learn how to control the Shader to draw something we want.

Draw shapes — rectangles and circles

First, let’s try to narrow down the scope of the drawing. To control the scope, we need to know the coordinates of the point being drawn, which brings us to another important variable in GLSL: gl_fragCoord.

If gl_fragColor is the output of the Fragment Shader, gl_fragCoord is the input of the Fragment Shader. It represents the coordinates of the current drawing point. It is a VEC4, but we only need the first two terms here.

We can access its coordinates using gl_fragcoord. x and gl_fragcoord. y, respectively, or we can turn it into a 2-dimensional vector using GL_fragcoord. xy.

So, back to our question, how do I draw a rectangle? We just need to determine its range of coordinates, look at the example code:

precision mediump float;

void main(){
    if(gl_FragCoord.x > 25.0 && gl_FragCoord.x < 75.0 && 
        gl_FragCoord.y > 25.0 && gl_FragCoord.y < 75.0)
        gl_FragColor = vec4(1.0.0.0.0.0.1.0);
    else
        gl_FragColor = vec4(0.0.0.0.1.0.1.0);
}
Copy the code

Here we should note that many students from JS brought the custom is that the integer type and the floating point number type do not distinguish, and in GLSL, integer and floating point number are completely two types, without casting, there is no mixing operation. When we write a direct quantity, we also want to be very explicit with the decimal point, to show that this is a floating point number.

We could also use code like float(25) to cast integers to floating-point types, but I wouldn’t recommend it here, either for readability or for efficiency.

After drawing the square, let’s try a more complicated circle. According to the knowledge of analytical geometry in junior high school, we can know that a circle is the set of points whose distance from the center of the circle is less than the radius, so we can draw a circle according to the formula x²+y²

We can do this by multiplying, but according to the DRY principle, we are better off using the built-in functions. In GLSL, most Math functions can be used directly without adding Math like in JS.

The final implementation code is as follows:

precision mediump float;

void main(){
    if(pow(gl_FragCoord.x - 50.0.2.0) + pow(gl_FragCoord.y - 50.0.2.0) < pow(25.0.2.0))
        gl_FragColor = vec4(1.0.0.0.0.0.1.0);
    else
        gl_FragColor = vec4(0.0.0.0.1.0.1.0);
}
Copy the code

And that completes our circle. However, if we zoom in on the circle a little bit, you will see that it is heavily jagged, and we will introduce an important function in GLSL to solve this problem.

Know smoothstep

We tried to analyze why the circles look so jagged. In our Shader code, we used a black and white strategy. Due to the display device, we couldn’t make the pixels too small for the naked eye to see.

So, the general computer graphics display scheme is how to deal with? The simple way to do this is to create a subtle gradient on the edge of the circle, so that the color transition is less rigid.

Let’s first clean up the Shader code and set the distance from the point to the center of the circle as a separate variable. Here we use a new function, the square root function SQRT:

    float l = sqrt(pow(gl_FragCoord.x - 50.0.2.0) + pow(gl_FragCoord.y - 50.0.2.0));
Copy the code

Next, let’s try mixing the two colors according to the variable L. Here we introduce a new function, mix, which can mix the two colors according to the ratio (there are other uses, but I’ll leave the table). Mix takes three arguments. The first two are the values to be mixed, and the last is the ratio of the mixture.

We try to mix the two colors according to the distance l from the point to the center of the circle. The final code is as follows:

precision mediump float;

void main(){
    float l = sqrt(pow(gl_FragCoord.x - 50.0.2.0) + pow(gl_FragCoord.y - 50.0.2.0));
    gl_FragColor = mix(vec4(1.0.0.0.0.0.1.0), vec4(0.0.0.0.1.0.1.0), l / 25.0);
}
Copy the code

After execution, we can see clearly the gradient, but this is not our final desired effect, we do not want the round into a gradient, we only hope to find a few pixels wide circle near the edge of the gradient, although we can use the arithmetic and the if out of this effect, but provides a more elegant solution in GLSL, That’s the SmoothStep function.

Smoothstep takes three parameters, min, Max, and x, and its function is to return 0.0 when x is less than min, 1.0 when x is greater than Max, and a value between 0.0 and 1.0 when x is between min and Max, representing the proportion of the distance between x and min within that interval.

Next, let’s modify the GLSL code to draw a soft circle using SmoothStep. In order to make the smoothStep more obvious, the smoothStep range is deliberately set. In actual use, only 1-2 pixels of blur is more appropriate.

precision mediump float;

void main(){
    float l = sqrt(pow(gl_FragCoord.x - 50.0.2.0) + pow(gl_FragCoord.y - 50.0.2.0));
    gl_FragColor = mix(vec4(1.0.0.0.0.0.1.0), vec4(0.0.0.0.1.0.1.0), smoothstep(20.5.25.5, l));
}
Copy the code

At this point, you should know the basics of SmoothStep. Now, let’s use it to our advantage. Our next task is to draw a straight line.

When I say draw a line, it actually has a width, and that requires us to be able to figure out the distance from the point to the line. Here we directly use the conclusion from vector geometry:

Theorem: Given A line L, the direction vector is M. A is A point outside L. If you want to know the distance d from A to l, you can take any point B on L, and call the vector from A to B n, The d = ∣ m ⋅ n ∣ ∣ n ∣ d = \ frac {| | m. n} {n | |} d = ∣ n ∣ ∣ m ⋅ n ∣

According to this formula, we need to use the vector dot product operation dot and the vector length function length, and finally write the GLSL code as follows:

precision mediump float;

void main(){
    vec2 m = vec2(1..1.);
    vec2 n = vec2(25..0.) - gl_FragCoord.xy;
    
    float d = length(dot(m, n)) / length(m);
    gl_FragColor = mix(vec4(1.0.0.0.0.0.1.0), vec4(0.0.0.0.1.0.1.0), smoothstep(0.0.1.0, d));
}
Copy the code

This is where we see the power of vector operations, and the combination of analytic geometry and linear algebra allows us to use simple code to deal with all kinds of graphics problems.

exercises

After reading the above content, are you eager to try? Here’s a little exercise for you:

Use Fragment Shader to draw a Vue Logo.

Please post the Shader code for discussion.