The project address

Train of thought

Recently, I was learning webGL native, and I didn’t have many 3D projects, so I came to the idea that 2D projects could also be practiced with webGL. After receiving this project, I came up with the idea of making particles, which is to obtain the effective pixels of the picture as the XY coordinates of the point. Z axis to realize 3 d particle effects, down the ideas they want to, perspective projection of words that I have to and z x, y coordinates to do a perspective to reverse change, make them in perspective projection is normal time or see pictures, but found that there are two problems, one is my mathematical paste carted the inverse matrix is too confused, The second is that in perspective projection, the z-axis has different dimensions, which means that it is far smaller and near larger, so each particle has a different size, so I changed the idea, which is easy to understand and that is the orthogonal projection tangent perspective projection.

When I think of the characteristics of orthogonal projection, I can arrange the particles into patterns, and when I think of perspective projection, I have the following ideas:

| generated coordinates – orthogonal perspective switch

Effect:

implementation

1. Generate coordinates

The idea here is to use Canvas getImageData to read valid pixels (alpha! =0), generate x,y and random Z coordinates, and generate buffer combined with pixel colors for reference by webGL.

GetImageData also has a few tricks, such as collision detectionCopy the code

The code is as follows:

// image is already ready
const data = [];
const canvas = <HTMLCanvasElement>document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
ctx.drawImage(image,0.0);
// Get pixel data
const imageData = ctx.getImageData(0.0,canvas.width,canvas.height);
// Position with center point
const offsetX = imageData.width/2;
const offsetY = imageData.height/2;
for(let x = 0; x<imageData.width; x++){for(let y = 0; y<imageData.height; y++){const r = x*4+y*imageData.width*4;
        // Filter invalid pixels to reduce the amount of data
        if(imageData.data[r+3]) {// The axis and color values are recorded in the buffer
            data.push(
                x-offsetX,                  //x
                -(y-offsetY),               //y
                (Math.random()-. 5)*offsetX, //z
                imageData.data[r+0] /255.//r
                imageData.data[r+1] /255.//g
                imageData.data[r+2] /255.//b
                imageData.data[r+3] /255.//a); }}}Copy the code

Then pass the data to webGL(routine shader creation, program will not be described, refer to hand hand 3d greeting card) :

    / / get the attribute
    aPosition = gl.getAttribLocation(program,'aPosition');
    gl.enableVertexAttribArray(aPosition);
    aColor = gl.getAttribLocation(program,'aColor');
    gl.enableVertexAttribArray(aColor);
    
    // Convert the world coordinates to homogeneous coordinates
    uViewPort = gl.getUniformLocation(program,'uViewPort');
    gl.uniform2f(uViewPort,window.innerWidth,window.innerHeight);
    
    // Write data to gl.array_buffer and set attribute to read data
    gl.bindBuffer(gl.ARRAY_BUFFER,gl.createBuffer());
    gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(data),gl.STATIC_DRAW);
    gl.vertexAttribPointer(aPosition,3,gl.FLOAT,false.7*4.0);
    gl.vertexAttribPointer(aColor,3,gl.FLOAT,false.7*4.3*4);
    gl.drawArrays(
        gl.POINTS,
        0,
        data.length/7   // Divide by 7 because I wrote 7 data for each vertex (xyz,rgba)
    );
Copy the code

The shader is simple (gl_PointSize is set because gl.points mode is used) :

/ / ___________________ vertex shader _________________________
    precision mediump float;
    attribute vec3 aPosition;
    attribute vec4 aColor;
    uniform vec2 uViewPort;
    varying vec4 vColor;
    void main(void) {
        gl_Position = vec4(aPosition/uViewPort.xyx, 1);
        gl_PointSize = 1.0;         / / must be set up
        vColor = aColor;
    }
/ / ___________________ fragment shader _________________________
    varying vec4 vColor;
    void main(void) {
        gl_FragColor = vColor;
    }
Copy the code

This is essentially what a still image looks like, but essentially each pixel is actually a point in 3D, no screenshots.

2. Orthogonal matrix tangent perspective matrix

In 1 I didn’t write the code in the sense of orthogonal matrix, actually does not write itself can be seen as an x, y, z from the orthogonal projection of (1, 1), so I didn’t write, want to use the perspective projection in this step, so made some adjustments for the code above, in the above code uViewPort replace matrix, the rest is for matrix modification, Modified as follows:

//___________________TypeScript_________________________
    import * as Matrix from 'gl-mat4';
    const ortho = Matrix.create();
    Matrix.ortho(
        ortho,
        -canvas.width/2,
        canvas.width/2,
        -canvas.height/2,
        canvas.height/2,
        -canvas.width/2,
        canvas.width/2
    );
    worldViewProjection = gl.getUniformLocation(program,'worldViewProjection');
    gl.uniformMatrix4fv(worldViewProjection,false,ortho);
    
/ / ___________________ vertex shader _________________________
    precision mediump float;
    attribute vec3 aPosition;
    attribute vec4 aColor;
    uniform mat4 worldViewProjection;
    varying vec4 vColor;
    void main(void) {
        gl_Position = worldViewProjection*vec4(aPosition, 1);
        gl_PointSize = 1.0;         / / must be set up
        vColor = aColor;
    }
Copy the code

According to the features of openGL perspective division, I only need to dynamically adjust ortho[11] to adjust the perspective degree during each rendering, and the projection is orthogonal if and only if ortho[11]==0.

render();
functionOrtho ortho[11] = (math.cos (time/1000)+1)/50; render(time){// update ortho ortho[11] = (math.cos (time/1000)+1)/50; gl.uniformMatrix4fv(worldViewProjection,false,ortho); / / to empty gl. The clear (gl. COLOR_BUFFER_BIT | gl. DEPTH_BUFFER_BIT); Gl / / rendering. DrawArrays (gl) POINTS, 0, data. Length / 7); requestAnimationFrame(render); }Copy the code

Results the following

The rest is left to play with, since the Z-axis is random and the projection matrix [11] changes regularly, simple calculations can be made based on these two parameters in the vertex shader.

My calculations are:

/ / ___________________ vertex shader _________________________
    precision mediump float;
    attribute vec3 aPosition;
    attribute vec4 aColor;
    uniform mat4 worldViewProjection;
    varying vec4 vColor;
    varying float vAlpha;
    void main(void) {
        gl_Position = worldViewProjection * vec4(aPosition, 1.0);
        vAlpha = 1.-pow(worldViewProjection[3].b*50..2.);
        gl_Position.xy += gl_Position.z*pow((1.-vAlpha)*gl_Position.w,gl_Position.w);
        gl_PointSize = 1.0;
        vColor = aColor;
    }
/ / ___________________ fragment shader _________________________
    varying vec4 vColor;
    varying float vAlpha;
    void main(void) {
        gl_FragColor = vColor*vAlpha;
    }
Copy the code

The resulting effect has the effect of launching to the left and down

3. Cheese Shop — Perspective division

Perspective division is just a term for converting the W component in homogeneous coordinates to 1

It is in the rasterization phase of the rendering pipeline, so it is automatic, and I searched for a long time to find the location of perspective division.

Please refer to the OpengL graphics pipeline for the related rendering pipeline. Opengl ES3 is described in this article, but it is roughly the same, you can refer to it

To the left of the projection matrix in the picture below is my model in the project, mainly is the matrix of the fourth row third position switch from 0 to other values, can cause the original coordinate w weight not to 1, not after 1 after rasterizer stage division of perspective, the rest of the xyz component will be changed accordingly, so as to realize the effect of perspective ~