精华内容
下载资源
问答
  • webgl不同图像不同纹理In this tutorial you will learn how to use WebGL for image processing. We will cover basic stuff like initialization, texture loading, and simple fragment shaders. I will try to ...

    webgl不同图像不同纹理

    In this tutorial you will learn how to use WebGL for image processing. We will cover basic stuff like initialization, texture loading, and simple fragment shaders. I will try to cover all aspects of interaction with WebGL but you should have decent understanding of vanila javascript. If you want more in depth explanations, there is a good book called "Learn WebGL", check it out.

    在本教程中,您将学习如何使用WebGL进行图像处理。 我们将介绍一些基本内容,例如初始化,纹理加载和简单的片段着色器。 我将尝试涵盖与WebGL交互的所有方面,但是您应该对vanila javascript有相当的了解。 如果您想要更深入的解释,有一本好书叫做“ Learn WebGL”,请阅读。

    Here are examples of what we will do:

    以下是我们将执行的操作的示例:

    图像扭曲 (Image twist)

    影像模糊 (Image blur)

    SOURCE CODE

    源代码

    If you want to skip theory and build setup click here.

    如果您想跳过理论并建立设置,请单击此处

    理论 (Theory)

    Everybody who tries to implement good graphics quickly understands he now has severe performance issues. The amount of computation required to produce any decent scene is simply huge. You need to process every polygon (some models have thousands of them) to project them on screen. Then you need to rasterize them. Apply textures, shading, reflections for every pixel on the screen. And you also need to do all of this stuff at least 30 times in a second. CPUs just can't handle this very well. Especially when using some scripting language with lots of overhead for every operation.

    每个尝试实现良好图形的人都会很快了解他现在面临严重的性能问题。 产生任何体面场景所需的计算量简直是巨大的。 您需要处理每个多边形(某些模型有数千个多边形)才能在屏幕上投影它们。 然后,您需要对其进行栅格化。 为屏幕上的每个像素应用纹理,阴影,反射。 而且您还需要在一秒钟内至少完成30次这些工作。 CPU只是不能很好地处理此问题。 尤其是在使用某些脚本语言时,每项操作都会产生大量开销。

    Luckily people found a solution and invented GPUs. All of the tasks described above are highly parallel in their nature. Polygons and pixels usually do not depend on each other and can be easily (and much more efficiently) processed at the same time. GPUs are especially good at this. While modern processors usually have 4-8 cores, any decent graphics card has thousands of them. They are much less complex then CPU cores and highly optimized for specific 3D-related calculations.

    幸运的是,人们找到了解决方案并发明了GPU。 上述所有任务本质上都是高度并行的。 多边形和像素通常互不依赖,并且可以轻松地(并且效率更高)同时进行处理。 GPU在这方面尤其擅长。 尽管现代处理器通常具有4-8个内核,但任何一款不错的图形卡都具有数千个。 它们比CPU内核复杂得多,并且针对特定的3D相关计算进行了高度优化。

    WebGL is a web standard for low-level 3D graphics API. It allows you to run your code directly on GPU, giving you all it's power. You write all of the rendering code in OpenGL Shading Language aka GLSL. It's not hard and very similar to C. Programs written in GLSL usually called shaders. They are compiled and loaded into a graphics card in runtime using WebGL API.

    WebGL是用于低级3D图形API的网络标准。 它使您可以直接在GPU上运行代码,从而发挥全部功能。 您可以使用OpenGL阴影语言(又称为GLSL)编写所有渲染代码。 这并不难,并且与C非常相似。用GLSL编写的程序通常称为着色器。 使用WebGL API在运行时将它们编译并加载到图形卡中。

    制备 (Preparation)

    Technically you don't need to install anything. But without a proper web server you won't be able to load images and additional scripts. So it's a good idea to have one. I will use webpack-dev-server, it's easy to setup and use.

    从技术上讲,您不需要安装任何东西。 但是如果没有适当的Web服务器,您将无法加载图像和其他脚本。 因此,拥有一个是一个好主意。 我将使用webpack-dev-server,它易于设置和使用。

    First thing that you need to do is create an empty folder and run npm init inside. You can skip all of the questions from NPM.

    您需要做的第一件事是创建一个空文件夹并在其中运行npm init 。 您可以跳过NPM中的所有问题。

    Then add this lines to package.json

    然后将此行添加到package.json

    {
        "scripts": {
            "build": "webpack",
            "serve": "webpack-dev-server"
        },
        "devDependencies": {
            "copy-webpack-plugin": "^5.1.1",
            "html-webpack-plugin": "^4.2.0",
            "raw-loader": "^4.0.1",
            "webpack": "^4.43.0",
            "webpack-cli": "^3.3.11",
            "webpack-dev-server": "^3.10.3"
        }
    
    }

    Run npm install and create a new file named webpack.config.js.

    运行npm install并创建一个名为webpack.config.js的新文件。

    Here is the contents of webpack config:

    这是webpack配置的内容:

    const path = require('path');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const webpack = require('webpack');
    const CopyWebpackPlugin = require('copy-webpack-plugin');
    
    module.exports = {
        entry: './index.js',
        output: {
            path: path.resolve(__dirname, 'dist'),
            filename: 'index.js',
        },
        plugins: [
            new HtmlWebpackPlugin({
                template: 'index.html'
            }),
            new CopyWebpackPlugin([{
                from: "styles/*.css",
                to: ""
            }])
        ],
        mode: 'development'
    };

    Now you can start dev server by running npm run serve and open http://localhots:8080. I will not explain this all deeply, as this is not the main topic. Everything should work out of the box.

    现在,您可以通过运行npm run serve并打开http:// localhots:8080来启动开发服务器。 我不会对此进行深入解释,因为这不是主要主题。 一切都应该开箱即用。

    (Code)

    Let's deal with HTML right away.

    让我们立即处理HTML。

    All we need is a canvas, so here it is.

    我们需要的只是一块画布,所以就在这里。

    index.html (index.html)

    <html>
    <head>
        <title>Webgl</title>
    </head>
    <body>
    
        <canvas id="c"></canvas>
        <div class="slidecontainer">
            <input type="range" min="0" max="30" value="0" class="slider" id="range">
          </div>
    </body>
    </html>

    Just basic HTML template with a canvas and slider that we can use.

    只是可以使用的带有画布和滑块的基本HTML模板。

    Now, it's time to initialize WebGL.

    现在,该初始化WebGL了。

    index.js (index.js)

    // Using webpack's raw loader to get shader code as JS string.
    // Much more convenient than writing them directly as string 
    // or loading in runtime
    import vert from '!raw-loader!./vertex.glsl';
    import frag from '!raw-loader!./fragment.glsl';
    
    function prepareWebGL(gl) {
      // Creating and compiling vertex shadr
      let vertSh = gl.createShader(gl.VERTEX_SHADER);
      gl.shaderSource(vertSh, vert);
      gl.compileShader(vertSh);
      // This line is very import 
      // By default if shader compilation fails,
      // WebGL will not show you error,
      // so debugging is almost impossible
      console.log(gl.getShaderInfoLog(vertSh));
    
      // Creating and compiling fragment shader
      let fragSh = gl.createShader(gl.FRAGMENT_SHADER);
      gl.shaderSource(fragSh, frag);
      gl.compileShader(fragSh);
      // This line is very import 
      // By default if shader compilation fails,
      // WebGL will not show you error,
      // so debugging is almost impossible
      console.log(gl.getShaderInfoLog(fragSh));
    
      // Linking program and passing it to GPU
      let prog = gl.createProgram();
      gl.attachShader(prog, vertSh);
      gl.attachShader(prog, fragSh);
      gl.linkProgram(prog);
      gl.useProgram(prog);
      gl.viewport(0,0,c.width,c.height);
      return prog;
    }

    Error checks are omitted for clarity. So, how exactly WebGL works? It's sort of a separate world. You pass some programs and data there. Then you GPU executes your program with your data and gives back an image.

    为了清楚起见,省略了错误检查。 那么,WebGL到底如何工作? 这是一个独立的世界。 您在那里传递了一些程序和数据。 然后,GPU使用数据执行程序并返回图像。

    Every program consists of two parts: vertex and fragment shaders. Vertex shader is applied to every vertex or just point in space, that you pass in. Here you perform all 3D stuff such as transformations, projections, and clipping. Then GPU rasterizes your shapes, which means filling them with pixels aka fragments. Actually fragments are not exactly pixels. But for the scope of this tutorial they can be used interchangeably. After rasterization every fragment is passed through fragment shader to determine it's color. Finally everything is drawn to the framebuffer and displayed on the screen.

    每个程序都由两部分组成:顶点着色器和片段着色器。 顶点着色器将应用于传入的每个顶点或空间中的每个点。在这里,您将执行所有3D内容,例如变换,投影和裁剪。 然后GPU栅格化您的形状,这意味着用像素(也称为碎片)填充形状。 实际上,片段并非完全是像素。 但是在本教程的范围内,它们可以互换使用。 栅格化后,每个片段都会经过片段着色器以确定其颜色。 最后,所有内容都被绘制到帧缓冲区并显示在屏幕上。

    It's important to understand that your shaders are executed in parallel. On every vertex and every fragment independently. Also, shaders produce values not by returning them but setting special variables. Such as gl_Position and gl_FragColor, treat them as return statements.

    了解着色器是并行执行的这一点很重要。 在每个顶点和每个片段上独立存在 。 同样,着色器不是通过返回值而是通过设置特殊变量来产生值。 如gl_Positiongl_FragColor ,将它们视为return语句。

    For the sake of simplicity we will mostly play with fragment shaders and stay in a 2D world. Here is simple pass-through vertex shader:

    为了简单起见,我们将主要使用片段着色器并停留在2D世界中。 这是简单的传递顶点着色器:

    顶点 (vertex.glsl)

    // This is our input from js world
    attribute vec2 coords;
    // This is output for the fragment shader
    // varying variables are a little special
    // you will see why later
    varying highp vec2 vTextureCoord;
    
    void main (void) {
        // Texture and verticies have different coordinate spaces
        // we do this to invert Y axis
        vTextureCoord = -coords;
    
        // Setting vertix position for shape assembler 
        // GLSL has many convenient vector functions
        // here we extending 2D coords vector to 4D with 2 values
        // 0.0 is a Z coordinate
        // 1.1 is a W, special value needed for 3D math
        // just leave it 1 for now
        gl_Position = vec4(coords, 0.0, 1.0);
    }

    Later we will fill our canvas with a rectangle. This is needed to have some plane for applying textures. To understand how fragment shader works you need to comprehend varying variables. Let's imagine you have two vertices in a line. In vertex shader you set some varying variable to red color from one of them and to green from another. All fragments between these two points will get different values when reading this varying variable. It will smoothly transition from red to green. So if you set fragment color to the value of this variable you will get something like this. Such behavior is called interpolation.

    稍后,我们将用矩形填充画布。 这需要具有一些平面来应用纹理。 要了解片段着色器的工作原理,您需要理解各种变量。 假设您在一条直线上有两个顶点。 在顶点着色器中,您可以将其中一个变量设置为红色,将另一个变量设置为绿色。 读取此变量时,这两点之间的所有片段将获得不同的值。 它会从红色平滑过渡到绿色。 因此,如果将片段颜色设置为该变量的值,则将得到类似的内容。 这种行为称为插值。

    We will also use a couple of uniform variables. But no need to worry, they are quite simple. Uniform variables are just constant parameters. Useful for passing global setting and texture ids.

    我们还将使用几个统一变量。 但不用担心,它们非常简单。 统一变量只是常量参数。 用于传递全局设置和纹理ID。

    片段 (fragment.glsl)

    // Setting precision for float calculations
    precision mediump float;
    // This is input from vertex shader
    varying highp vec2 vTextureCoord;
    // Samplers are needeed to select textures
    // actually its integers
    uniform sampler2D uSampler;
    // This will tell us how much to screw the image
    uniform float iter;
    
    vec2 coords;
    float x;
    float y;
    float l;
    void main(void){
    
        // Getting distance from origin
        l = length(vTextureCoord);
        // Just renaming to reduce typing
        x = vTextureCoord[0];
        y = vTextureCoord[1];
        // Rotating point around origin 
        coords[0] = x * cos(iter * l) - y * sin(iter * l);
        coords[1] = x * sin(iter * l) + y * cos(iter * l);
    
        // Transforming coordinates from GL space to texture space
        // All math can be done directly to vectors
        coords = coords / 2.0 - 0.5;
    
        // Fragment shader must set this variable
        gl_FragColor = texture2D(uSampler, coords);
    }

    The key here is to understand that angle of point rotation is dependent on it's distance to the center. This will result in a cool effect of texture twisting and sucking in a black hole. If we delete the * l part, the whole thing will just rotate evenly.

    这里的关键是要了解点旋转的角度取决于它到中心的距离。 这将导致纹理扭曲和在黑洞中吸吮的凉爽效果。 如果删除* l部分,则整个对象将均匀旋转。

    Our program is now ready, compiled, and loaded. Time to deal with the data. Here we are loading vertex coordinates to WebGL memory. Just two triangles to cover the canvas so we have some surface for texturing. Be aware that WebGL coordinates work different from canvas coordinates. Unlike canvas, it's origin is at the center and all coordinates are normalized. So no matter what aspect ratio your canvas has, X and Y are always in -1 to 1 range. Also Y coordinate is pointing upwards.

    现在,我们的程序已准备就绪,已编译并已加载。 是时候处理数据了。 在这里,我们将顶点坐标加载到WebGL内存中。 仅两个三角形覆盖了画布,因此我们有一些曲面可以进行纹理处理。 请注意,WebGL坐标的工作方式不同于画布坐标。 与画布不同,它的原点位于中心,所有坐标均已标准化。 因此,无论您的画布具有什么纵横比,X和Y始终在-1到1的范围内。 Y坐标也指向上方。

    index.js (index.js)

    function setArrays(gl, prog){
      // Getting WebGL buffer object
      let vertex_buffer = gl.createBuffer();
      // This is 2 triangles that form a square
      // Each triangle consists of 3 points
      // Each point consists of two numbers: X and Y coordinates
      // GL coordinate space has origin in center
      // and spans from -1 to 1 on both axes
      // here is why we need to transform our coords
      // in fragment shader
      const vertices = [-1.0, -1.0, 1.0, -1.0, -1.0, 1.0,
                         1.0, 1.0, 1.0, -1.0, -1.0, 1.0,
      ]
      // Loading our data as ARRAY_BUFFER
      gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer);
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    
      // Finding location of variable "coords" in GL memory
      // and binding ARRAY_BUFFER to it
      let coord = gl.getAttribLocation(prog, "coords");
      // Variable binds to last buffer that was written to GL memory
      gl.vertexAttribPointer(coord, 2, gl.FLOAT, false, 0, 0);
      gl.enableVertexAttribArray(coord);
      // ARRAY_BUFFER is now free and can be reused
      return [coord, vertex_buffer];
    }

    Array data is loaded. Time for textures.

    阵列数据已加载。 时间纹理。

    Better to find one that has sizes of powers of two.

    最好找到一个具有2的幂的大小。

    WebGL able to automatically generate mipmaps for such textures and scale them properly. At first we create simple 1x1 blue pixel texture and immediately return it. Later, when image loads, we replace this pixel with proper texture data.

    WebGL能够自动为此类纹理生成mipmap并正确缩放它们。 首先,我们创建简单的1x1蓝色像素纹理并立即将其返回。 稍后,当图像加载时,我们用适当的纹理数据替换此像素。

    index.js (index.js)

    function loadTexture(gl, prog, url) {
      // Creating 1x1 blue tuxture
      const texture = gl.createTexture();
      const level = 0;
      const internalFormat = gl.RGBA;
      const width = 1;
      const height = 1;
      const border = 0;
      const srcFormat = gl.RGBA;
      const srcType = gl.UNSIGNED_BYTE;
      const pixel = new Uint8Array([0, 0, 255, 255]);  // opaque blue (RGBA)
      // bindTexture works similar to bindBuffer
      gl.bindTexture(gl.TEXTURE_2D, texture);
      gl.texImage2D(gl.TEXTURE_2D, level, internalFormat,
                    width, height, border, srcFormat, srcType,
                    pixel);
    
      // Loading image
      const image = new Image();
      image.onload = function() {
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texImage2D(gl.TEXTURE_2D, level, internalFormat,
                      srcFormat, srcType, image);
    
        // WebGL1 has different requirements for power of 2 images
        // vs non power of 2 images so check if the image is a
        // power of 2 in both dimensions.
        if (isPowerOf2(image.width) && isPowerOf2(image.height)) {
           // Yes, it's a power of 2. Generate mips.
           gl.generateMipmap(gl.TEXTURE_2D);
        } else {
           // No, it's not a power of 2. Turn off mips and set
           // wrapping to clamp to edge
           gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
           gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
           gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        }
        // Quik re-render to display new texture
        // See implementation below
    
        render(0);
      };
      // Triggering load
      image.src = url;
      return texture;
    }
    function isPowerOf2(value) {
        return (value & (value - 1)) == 0;
    }

    Preparations done. Now is time to tie everything together.

    准备工作已经完成。 现在是时候将所有内容捆绑在一起。

    index.js (index.js)

    function main(){
      // Getting WebGL context from canvas
      const c = document.getElementById("c");
      c.width = 600;
      c.height = 600;
      // Getting slider
      const range = document.getElementById("range");
    
      const gl = c.getContext("webgl");
      const prog = prepareWebGL(gl);
      const coord = setArrays(gl, prog);
      const texture = loadTexture(gl, prog, "img.jpg");
    
      // Handle to control amount of twist
      const iter = gl.getUniformLocation(prog, "iter");
      const uSampler = gl.getUniformLocation(prog, 'uSampler');
      // As is said samplers are just integers
      // Tell the shader to use texture 0
      gl.uniform1i(uSampler, 0)
    
      // This is main workhorse
      render = (it) => {
        // Binding texture to slot 0
        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, texture);
    
        // Filling screen with black color
        gl.clearColor(0.0, 0.0, 0.0, 1.0);
        gl.clear(gl.COLOR_BUFFER_BIT);
    
        // Setting iter to slider value
        gl.uniform1f(iter, it);
        // Triggering webgl render
        gl.drawArrays(gl.TRIANGLES, 0, 6);
      }
    
      render(0);
    
      range.addEventListener("input", (e) => {
        render(e.target.value);
      })
    }
    
    main()

    That's it. You can try to follow along and write it yourself. If you totally stuck on something, the working source from demos above is here.

    而已。 您可以尝试自己编写它。 如果您完全固定在某个东西上,则上面的演示中的工作源在这里

    奖励:模糊着色器 (Bonus: Blur shader)

    GLSL is quite restrictive. For example you can't write loops with non-constant bounds. This may seem strange at first, but actually have decent reasoning behind it. Most of the restrictions are needed to help compilers apply some aggressive optimization techniques.

    GLSL具有严格的限制。 例如,您不能编写具有非恒定边界的循环。 乍一看这似乎很奇怪,但实际上背后有合理的理由。 需要大多数限制来帮助编译器应用一些积极的优化技术。

    So, here is the blur shader.

    因此,这是模糊着色器。

    片段 (fragment.glsl)

    varying highp vec2 vTextureCoord;
    uniform sampler2D uSampler;
    precision mediump float;
    uniform float iter;
    uniform float uTextureSize;
    
    void main(void){
        float pixel = 1.0 / uTextureSize;
        vec2 coords;
        vec4 color = vec4(0.0, 0.0, 0.0, 0.0);
    
        float div = (iter + 1.0) * (iter + 1.0) * 4.0;
    
        for (int i = 0; i <= 100; i++) {
            if (float(i) > iter){
                break;
            }
            for (int j = 0; j <= 100; j++) {
                if (float(j) > iter){
                    break;
                }
                coords = vTextureCoord.st / 2.0 - 0.5;
                coords += vec2(float(i), float(j)) * pixel;
                color += texture2D(uSampler, coords).rgba / div;
    
                coords = vTextureCoord.st / 2.0 - 0.5;
                coords -= vec2(float(i), float(j)) * pixel;
                color += texture2D(uSampler, coords).rgba / div;
            }
            int i2 = -i;
            for (int j = 0; j <= 100; j++) {
                if (float(j) > iter){
                    break;
                }
                coords = vTextureCoord.st / 2.0 - 0.5;
                coords += vec2(float(i2), float(j)) * pixel;
                color += texture2D(uSampler, coords).rgba / div;
    
                coords = vTextureCoord.st / 2.0 - 0.5;
                coords -= vec2(float(i2), float(j)) * pixel;
                color += texture2D(uSampler, coords).rgba / div;
            }
        }
    
        gl_FragColor = vec4(color.rgb, 1.0);
    }

    It's not the best, but it works. And I hope you can learn something from it.

    这不是最好的,但是可以。 我希望您能从中学到一些东西。

    主意 (Ideas)

    You can use this project as a template for future experiments.

    您可以将该项目用作将来实验的模板。

    Here are some cool ideas:

    这里有一些很酷的想法:

    Good luck in your programming journey.

    祝您编程愉快。

    翻译自: https://habr.com/en/post/501080/

    webgl不同图像不同纹理

    展开全文
  • 文章目录使用webgl处理图片的库一、webgl-program二、使用步骤引入创建 Program 对象 使用webgl处理图片的库 对于一张图片或者多张图片,需要对其进行各种处理的话,就需要使用到webgl技术 一、webgl-program 二...


    使用webgl处理图片的库

    对于一张图片或者多张图片,需要对其进行各种处理的话,就需要使用到webgl技术

    一、webgl-program

    项目地址https://github.com/royalknight56/webgl-utils

    点击前往Github

    二、使用步骤

    引入

    import { Program } from "./jsUtil/program.js";
    

    创建 Program 对象

    let pro = new Program("webgl");
    

    Program(glid, gl);

    /**
     * @name:构造函数
     * @param {*} glid:canvas元素的id
     * @param {*} gl:gl上下文,如果自己传入了gl上下文,则直接使用该gl,glid无效
     * @return {ProgramObj} ProgramObj
     */
     
    

    ProgramObj 属性

        /**
         * @name: 执行此行代码会执行相应程序的绘制工作
         * @param {*}
         * @return {*}
         * @LastEditors: RoyalKnight
         */
        draw() {}
    
        /**
         * @name: 初始化程序图像
         * @param {Image} image
         * @return {null}
         * @LastEditors: RoyalKnight
         */
        initProgramImg(image)
    
        /**
         * @name: 改变程序图像
         * @param {Image} image
         * @return {null}
         * @LastEditors: RoyalKnight
         */
        changeProgramImg(image)
        /**
         * @name: 更改图像对比度
         * @param {Number} value(0.0~1.0)
         * @LastEditors: RoyalKnight
         */
        changeContrast(value)
    
        /**
         * @name: 更改图像亮度
         * @param {Number} value(0.0~1.0)
         * @LastEditors: RoyalKnight
         */
        changeBrightness(value)
        /**
         * @name: 更改图像饱和度
         * @param {Number} value(0.0~1.0)
         * @LastEditors: RoyalKnight
         */
        changeSaturation(value)
    
        /**
         * @name: 更改图像通道
         * @param {Number} value(0~4)
         * @LastEditors: RoyalKnight
         */
        changeChannel(value)
    
        /**
         * @name: 更改图像RGBA
         * @param {Number} value(0.0~1.0)
         * @LastEditors: RoyalKnight
         */
        changeGlImgRGBA(rgba)
    
        /**
         * @name: 更改图像位置
         * @param {*} x:起点x
         * @param {*} y:起点y
         * @param {*} w:总宽度w
         * @param {*} h:总高度h
         * @param {*} sx:x轴缩放
         * @param {*} sy:y轴缩放
         * @return {*}
         * @LastEditors: RoyalKnight
         */
        changeGlImgPos(x = -1, y = -1, w = 1, h = 1, sx = 2, sy = 2)
    
        /**
         * @name: 获得程序的ImageData
         * @param {*}
         * @return {ImageData}
         * @LastEditors: RoyalKnight
         */
        getImgData()
    
        /**
         * @name: 将ImageData放入程序
         * @param {ImageData}
         * @return {null}
         * @LastEditors: RoyalKnight
         */
        putImgData(image)
    
    展开全文
  • 技术社区里有种很有意思的现象,那就是不少人们口耳相传中的强大技术,往往因为上手难度高而显得曲高和寡。从这个角度看来,WebGL 和函数式编程有些类似,都属于优势已被论证了多年,却一直较为...

    技术社区里有种很有意思的现象,那就是不少人们口耳相传中的强大技术,往往因为上手难度高而显得曲高和寡。从这个角度看来,WebGL 和函数式编程有些类似,都属于优势已被论证了多年,却一直较为不温不火的技术。但是,一旦这些技术的易用性跨越了某个临界点,它们其实并没有那么遥不可及。这次我们就将以 WebGL 为例,尝试降低它的入门门槛,讲解它在前端图像处理领域的应用入门。

    临近 2020 年的今天,社区里已经有了许多 WebGL 教程。为什么还要另起炉灶再写一篇呢?这来自于笔者供职的稿定科技前端团队,在 WebGL 基础库层面进行技术创新的努力。前一段时间,我们开源了自主研发的 WebGL 基础库 Beam。它以 10KB 不到的体积,将传统上入门时动辄几百行的 WebGL 渲染逻辑降低到了几十行的量级,并在性能上也毫不妥协。开源两周内,Beam 的 Star 数量就达到了 GitHub 全站 WebGL Library 搜索条目下的前 10%,在国内也还没有定位相当的竞品。这次我们将借助 Beam 来编写 WebGL 渲染逻辑,用精炼的代码和概念告诉大家,该如何硬核而不失优雅地手动操控 GPU 渲染管线,实现多样的前端图像处理能力。

    本文将覆盖的内容如下所示。我们希望能带着感兴趣的同学从零基础入门,直通具备实用价值的图像滤镜能力开发:

    • WebGL 概念入门

    • WebGL 示例入门

    • 如何用 WebGL 渲染图像

    • 如何为图像增加滤镜

    • 如何叠加多个图像

    • 如何组合多个滤镜

    • 如何引入 3D 效果

    • 如何封装自定渲染器

    为了照顾没有基础的同学,在进入实际的图像处理部分前,我们会重新用 Beam 入门一遍 WebGL。熟悉相关概念的同学可以直接跳过这些部分。

    WebGL 概念入门

    Beam 的一个设计目标,是让使用者即便没有相关经验,也能靠它快速搞懂 WebGL。但这并不意味着它像 Three.js 那样可以几乎完全不用懂图形学,拿来就是一把梭。相比之下,Beam 选择对 WebGL 概念做高度的抽象。在学习理解这些概念后,你就不仅能理解 GPU 渲染管线,还能用简单的代码来操作它了。毕竟这篇文章本身,也是本着授人以渔的理念来写作的。

    本节来自 如何设计一个 WebGL 基础库 一文,熟悉的同学可跳过。

    WebGL 体系有很多琐碎之处,一头扎进代码里,容易使我们只见树木不见森林。然而我们真正需要关心的概念,其实可以被高度浓缩为这几个:

    • Shader 着色器,是存放图形算法的对象。相比于在 CPU 上单线程执行的 JS 代码,着色器在 GPU 上并行执行,计算出每帧数百万个像素各自的颜色。

    • Resource 资源,是存放图形数据的对象。就像 JSON 成为 Web App 的数据那样,资源是传递给着色器的数据,包括大段的顶点数组、纹理图像,以及全局的配置项等。

    • Draw 绘制,是选好资源后运行着色器的请求。要想渲染真实际的场景,一般需要多组着色器与多个资源,来回绘制多次才能完成一帧。每次绘制前,我们都需要选好着色器,并为其关联好不同的资源,也都会启动一次图形渲染管线。

    • Command 命令,是执行绘制前的配置。WebGL 是非常有状态的。每次绘制前,我们都必须小心地处理好状态机。这些状态变更就是通过命令来实现的。Beam 基于一些约定大幅简化了人工的命令管理,当然你也可以定制自己的命令。

    这些概念是如何协同工作的呢?请看下图:

    图中的 Buffers / Textures / Uniforms 都属于典型的资源(后面会详述它们各自的用途)。一帧当中可能存在多次绘制,每次绘制都需要着色器和相应的资源。在绘制之间,我们通过命令来管理好 WebGL 的状态。仅此而已。

    理解这个思维模型很重要。因为 Beam 的 API 设计就是完全依据这个模型而实现的。让我们进一步看看一个实际的场景吧:

    图中我们绘制了很多质感不同的球体。这一帧的渲染,则可以这样解构到上面的这些概念下:

    • 着色器无疑就是球体质感的渲染算法。对经典的 3D 游戏来说,要渲染不同质感的物体,经常需要切换不同的着色器。但现在基于物理的渲染算法流行后,这些球体也不难做到使用同一个着色器来渲染。

    • 资源包括了大段的球体顶点数据、材质纹理的图像数据,以及光照参数、变换矩阵等配置项

    • 绘制是分多次进行的。我们选择每次绘制一个球体,而每次绘制也都会启动一次图形渲染管线。

    • 命令则是相邻的球体绘制之间,所执行的那些状态变更

    如何理解状态变更呢?不妨将 WebGL 想象成一个具备大量开关与接口的仪器。每次按下启动键(执行绘制)前。你都要配置好一堆开关,再连接好一条接着色器的线,和一堆接资源的线,就像这样:

    还有很重要的一点,那就是虽然我们已经知道,一帧画面可以通过多次绘制而生成,而每次绘制又对应执行一次图形渲染管线的执行。但是,所谓的图形渲染管线又是什么呢?这对应于这张图:

    渲染管线,一般指的就是这样一个 GPU 上由顶点数据到像素的过程。对现代 GPU 来说,管线中的某些阶段是可编程的。WebGL 标准里,这对应于图中蓝色的顶点着色器和片元着色器阶段。你可以把它们想象成两个需要你写 C-style 代码,跑在 GPU 上的函数。它们大体上分别做这样的工作:

    • 顶点着色器输入原始的顶点坐标,输出经过你计算出的坐标。

    • 片元着色器输入一个像素位置,输出根据你计算出的像素颜色。

    下面,我们将进一步讲解如何应用这些概念,搭建出一个完整的 WebGL 入门示例。

    WebGL 示例入门

    本节同样来自 如何设计一个 WebGL 基础库 一文,但为承接后续的图像处理内容,叙述有所调整。

    在苦口婆心的概念介绍后,就要来到真刀真枪的编码阶段了。由于四大概念中的命令可以被自动化,我们只为 Beam 定义了三个核心 API,分别是:

    • beam.shader

    • beam.resource

    • beam.draw

    显然地,它们各自管理着色器、资源和绘制。让我们看看怎样基于 Beam,来绘制 WebGL 中的 Hello World 彩色三角形吧:

    三角形是最简单的多边形,而多边形则是由顶点组成的。WebGL 中的这些顶点是有序排列,可通过下标索引的。以三角形和矩形为例,这里使用的顶点顺序如下所示:

    Beam 的代码示例如下,压缩后全部代码体积仅有 6KB

    import { Beam, ResourceTypes } from 'beam-gl'
    import { MyShader } from './my-shader.js'
    const { VertexBuffers, IndexBuffer } = ResourceTypes
    
    const canvas = document.querySelector('canvas')
    const beam = new Beam(canvas)
    
    const shader = beam.shader(MyShader)
    const vertexBuffers = beam.resource(VertexBuffers, {
      position: [
        -1, -1, 0, // vertex 0 左下角
        0, 1, 0, // vertex 1 顶部
        1, -1, 0 // vertex 2 右下角
      ],
      color: [
        1, 0, 0, // vertex 0 红色
        0, 1, 0, // vertex 1 绿色
        0, 0, 1 // vertex 2 蓝色
      ]
    })
    const indexBuffer = beam.resource(IndexBuffer, {
      array: [0, 1, 2] // 由 0 1 2 号顶点组成的三角形
    })
    
    beam
      .clear()
      .draw(shader, vertexBuffers, indexBuffer)
    

    下面逐个介绍一些重要的 API 片段。首先自然是初始化 Beam 了:

    const canvas = document.querySelector('canvas')
    const beam = new Beam(canvas)
    

    然后我们用 beam.shader 来实例化着色器,这里的 MyShader 稍后再说:

    const shader = beam.shader(MyShader)
    

    着色器准备好之后,就是准备资源了。为此我们需要使用 beam.resource API 来创建三角形的数据。这些数据装在不同的 Buffer 里,而 Beam 使用 VertexBuffers 类型来表达它们。三角形有 3 个顶点,每个顶点有两个属性 (attribute),即 positioncolor,每个属性都对应于一个独立的 Buffer。这样我们就不难用普通的 JS 数组(或 TypedArray)来声明这些顶点数据了。Beam 会替你将它们上传到 GPU:

    注意区分 WebGL 中的顶点坐标概念。顶点 (vertex) 不仅可以包含一个点的坐标属性,还可以包含法向量、颜色等其它属性。这些属性都可以输入顶点着色器中来做计算。

    const vertexBuffers = beam.resource(VertexBuffers, {
      position: [
        -1, -1, 0, // vertex 0 左下角
        0, 1, 0, // vertex 1 顶部
        1, -1, 0 // vertex 2 右下角
      ],
      color: [
        1, 0, 0, // vertex 0 红色
        0, 1, 0, // vertex 1 绿色
        0, 0, 1 // vertex 2 蓝色
      ]
    })
    

    装顶点的 Buffer 通常会使用很紧凑的数据集。我们可以定义这份数据的一个子集或者超集来用于实际渲染,以便于减少数据冗余并复用更多顶点。为此我们需要引入 WebGL 中的 IndexBuffer 概念,它指定了渲染时用到的顶点下标。这个例子里,0 1 2 这样的每个下标,都对应顶点数组里的 3 个位置:

    const indexBuffer = beam.resource(IndexBuffer, {
      array: [0, 1, 2] // 由 0 1 2 号顶点组成的三角形
    })
    

    最后我们就可以进入渲染环节啦。首先用 beam.clear 来清空当前帧,然后为 beam.draw 传入一个着色器对象和任意多个资源对象即可:

    beam
      .clear()
      .draw(shader, vertexBuffers, indexBuffer)
    

    我们的 beam.draw API 是非常灵活的。如果你有多个着色器和多个资源,可以随意组合它们来链式地完成绘制,渲染出复杂的场景。就像这样:

    beam
      .draw(shaderX, ...resourcesA)
      .draw(shaderY, ...resourcesB)
      .draw(shaderZ, ...resourcesC)
    

    别忘了还有个遗漏的地方:如何决定三角形的渲染算法呢?这是在 MyShader 变量里指定的。它其实是个着色器的 Schema,像这样:

    import { SchemaTypes } from 'beam-gl'
    
    const vertexShader = `
    attribute vec4 position;
    attribute vec4 color;
    varying highp vec4 vColor;
    void main() {
      vColor = color;
      gl_Position = position;
    }
    `
    const fragmentShader = `
    varying highp vec4 vColor;
    void main() {
      gl_FragColor = vColor;
    }
    `
    
    const { vec4 } = SchemaTypes
    export const MyShader = {
      vs: vertexShader,
      fs: fragmentShader,
      buffers: {
        position: { type: vec4, n: 3 },
        color: { type: vec4, n: 3 }
      }
    }
    

    Beam 中的着色器 Schema,需要提供 fs / vs / buffers 等字段。这里的一些要点包括如下:

    • 可以粗略认为,顶点着色器对三角形每个顶点执行一次,而片元着色器则对三角形内的每个像素执行一次。

    • 顶点着色器和片元着色器,都是用 WebGL 标准中的 GLSL 语言编写的。这门语言其实就是 C 语言的变体,vec4 则是其内置的 4 维向量数据类型。

    • 在 WebGL 中,顶点着色器将 gl_Position 变量作为坐标位置输出,而片元着色器则将 gl_FragColor 变量作为像素颜色输出。本例中的顶点和片元着色器,执行的都只是最简单的赋值操作。

    • 名为 vColor 的 varying 变量,会由顶点着色器传递到片元着色器,并自动插值。最终三角形在顶点位置呈现我们定义的红绿蓝纯色,而其他位置则被渐变填充,这就是插值计算的结果。

    • 变量前的 highp 修饰符用于指定精度,也可以在着色器最前面加一行 precision highp float; 来省略为每个变量手动指定精度。在现在这个时代,基本可以一律用高精度了。

    • 这里 positioncolor 这两个 attribute 变量,和前面 vertexBuffers 中的 key 相对应。这也是 Beam 中的隐式约定。

    虽然到此为止的信息量可能比较大,但现在只要区区几十行代码,我们就可以清晰地用 Beam 来手动控制 WebGL 渲染了。接下来让我们看看,该如何把渲染出的三角形换成矩形。有了上面的铺垫,这个改动就显得非常简单了,稍微改几行代码就行。

    我们的目标如下图所示:

    这对应于这样的代码:

    const vertexBuffers = beam.resource(VertexBuffers, {
      position: [
        -1, -1, 0, // vertex 0 左下角
        -1, 1, 0, // vertex 1 左上角
        1, -1, 0, // vertex 2 右下角
        1, 1, 0 // vertex 3 右上角
      ],
      color: [
        1, 0, 0, // vertex 0 红色
        0, 1, 0, // vertex 1 绿色
        0, 0, 1, // vertex 2 蓝色
        1, 1, 0 // vertex 3 黄色
      ]
    })
    
    const indexBuffer = beam.resource(IndexBuffer, {
      array: [
        0, 1, 2, // 左下三角形
        1, 2, 3 // 右上三角形
      ]
    })
    

    其他代码完全不用改动,我们就能看到 Canvas 被填满了。这正好告诉了我们另一个重要信息:WebGL 的屏幕坐标系以画布中央为原点,画布左下角为 (-1, -1),右上角则为 (1, 1)。如下图所示:

    注意,不论画布长宽比例如何,这个坐标系的范围都是 -1 到 1 的。只要尝试更改一下 Canvas 的尺寸,你就能知道这是什么意思了。

    到目前为止,我们的渲染算法,其实只有片元着色器里的这一行:

    void main() {
      gl_FragColor = vColor;
    }
    

    对每个像素,这个 main 函数都会执行,将插值后的 varying 变量 vColor 颜色直接赋给 gl_FragColor 作为输出。能不能玩出些花样呢?很简单:

    gl_FragColor = vec4(0.8, 0.9, 0.6, 0.4); // 固定颜色
    gl_FragColor = vColor.xyzw; // 四个分量的语法糖
    gl_FragColor = vColor.rgba; // 四个分量的等效语法糖
    gl_FragColor = vColor.stpq; // 四个分量的等效语法糖
    gl_FragColor = vColor + vec4(0.5); // 变淡
    gl_FragColor = vColor * 0.5; // 变暗
    gl_FragColor = vColor.yxzw; // 交换 X 与 Y 分量
    gl_FragColor = vColor.rbga; // 交换 G 与 B 分量
    gl_FragColor = vColor.rrrr; // 灰度展示 R 分量
    gl_FragColor = vec4(vec2(0), vColor.ba); // 清空 R 与 G 分量
    

    这一步的例子,可以在 Hello World 这里访问到。

    虽然这些例子只示范了 GLSL 的基本语法,但别忘了这可是编译到 GPU 上并行计算的代码,和单线程的 JS 有着云泥之别。只不过,目前我们的输入都是由各顶点之间的颜色插值而来,因此效果难以超出普通渐变的范畴。该怎样渲染出常见的点阵图像呢?到此我们终于可以进入正题,介绍与图像处理关系最为重大的纹理资源了。

    如何用 WebGL 渲染图像

    为了进行图像处理,浏览器中的 Image 对象显然是必须的输入。在 WebGL 中,Image 对象可以作为纹理,贴到多边形表面。这意味着,在片元着色器里,我们可以根据某种规则来采样图像的某个位置,将该位置的图像颜色作为输入,计算出最终屏幕上的像素颜色。显然,这个过程需要在着色器里表达图像的不同位置,这用到的就是所谓的纹理坐标系了。

    纹理坐标系又叫 ST 坐标系。它以图像左下角为原点,右上角为 (1, 1) 坐标,同样与图像的宽高比例无关。这一坐标系的具体形式如下所示,配图来自笔者在卢浮宫拍摄的维纳斯像(嘿嘿)

    还记得我们先前给每个顶点附带了什么 attribute 属性吗?坐标颜色。现在,我们需要将颜色换成纹理坐标,从而告诉 WebGL,正方形的每一个顶点应该对齐图像的哪一个位置,就像把被单的四个角对齐被套一样。这也就意味着我们需要依序提供上图中,纹理图像四个角落的坐标。若将这四个坐标当作颜色绘制出来,就能得到下图:

    不难看出,图中左下角对应 RGB 下的 (0, 0, 0) 黑色;左上角对应 RGB 下的 (0, 1, 0) 绿色;右下角对应 RGB 下的 (1, 0, 0) 红色;右上角则对应 RGB 下的 (1, 1, 0) 黄色。由此可见,这几个颜色 R 通道和 G 通道分量的取值,就和纹理坐标系中对应的 X Y 位置一致。这样一来,我们就用 RGB 颜色验证了数据的正确性。这种技巧也常对着色算法调试有所帮助。

    和屏幕坐标系超出 (-1, 1) 区间就会被裁掉不同,纹理坐标系的取值可以是任意的正负浮点数。那么超过区间该怎么办呢?默认行为是平铺,像这样:

    但平铺不是唯一的行为。我们也可以修改 WebGL 状态,让纹理呈现出不同的展示效果(即所谓的 Wrap 缠绕模式),如下所示:

    除此之外,纹理还有采样方式等其他配置可供修改。我们暂且不考虑这么多,看看应该怎么将最基本的图像作为纹理渲染出来吧:

    // 创建着色器
    const shader = beam.shader(TextureDemo)
    
    // 创建用于贴图的矩形
    const rect = {
      vertex: {
        position: [
          -1.0, -1.0, 0.0,
          1.0, -1.0, 0.0,
          1.0, 1.0, 0.0,
          -1.0, 1.0, 0.0
        ],
        texCoord: [
          0.0, 0.0,
          1.0, 0.0,
          1.0, 1.0,
          0.0, 1.0
        ]
      },
      index: { array: [0, 1, 2, 0, 2, 3] }
    }
    const vertexBuffers = beam.resource(VertexBuffers, rect.vertex)
    const indexBuffer = beam.resource(IndexBuffer, rect.index)
    
    // 创建纹理资源
    const textures = beam.resource(Textures)
    
    // 异步加载图像
    fetchImage('venus.jpg').then(image => {
      // 设入纹理图像后,执行绘制
      textures.set('img', { image, flip: true })
      beam
        .clear()
        .draw(shader, vertexBuffers, indexBuffer, textures)
    })
    

    类似地,我们还是先看整体的渲染逻辑,再看着色器。整个过程其实很简单,可以概括为三步:

    1. 初始化着色器、矩形资源和纹理资源

    2. 异步加载图像,完成后把图像设置为纹理

    3. 执行绘制

    相信大家在熟悉 Beam 的 API 后,应该不会觉得这部分代码有什么特别之处了吧。下面我们来关注重要的 TextureDemo 着色器部分,如下所示:

    const vs = `
    attribute vec4 position;
    attribute vec2 texCoord;
    varying highp vec2 vTexCoord;
    
    void main() {
      vTexCoord = texCoord;
      gl_Position = position;
    }
    `
    
    const fs = `
    varying highp vec2 vTexCoord;
    uniform sampler2D img;
    
    void main() {
      gl_FragColor = texture2D(img, vTexCoord);
    }
    `
    
    const { vec2, vec4, tex2D } = SchemaTypes
    export const TextureDemo = {
      vs,
      fs,
      buffers: {
        position: { type: vec4, n: 3 },
        texCoord: { type: vec2 }
      },
      textures: {
        img: { type: tex2D }
      }
    }
    

    就像 vColor 那样地,我们将 vTexCoord 变量从顶点着色器传入了片元着色器,这时同样隐含了插值处理。

    这组着色器中,真正值得一提的是这么两行:

    uniform sampler2D img;
    // ...
    gl_FragColor = texture2D(img, vTexCoord);
    

    你可以认为,片元着色器中 uniform sampler2D 类型的 img 变量,会被绑定到一张图像纹理上。然后,我们就可以用 WebGL 内置的 texture2D 函数来做纹理采样了。因此,这个着色器的渲染算法,其实就是采样 img 图像的 vTexCoord 位置,将获得的颜色作为该像素的输出。对整个矩形内的每个像素点都执行一遍这个采样过程后,自然就把图像搬上屏幕了

    让我们先歇一口气,欣赏下渲染出来的高雅艺术吧:

    这一步的例子,可以在 Texture Config 这里访问到。

    如何为图像增加滤镜

    现在,图像的采样过程已经处于我们的着色器代码控制之下了。这意味着我们可以轻易地控制每个像素的渲染算法,实现图像滤镜。这具体要怎么做呢?下面拿这张笔者在布拉格拍的伏尔塔瓦河做例子(嘿嘿嘿)

    我们看到了一张默认彩色的图像。最常见的滤镜操作之一,就是将它转为灰度图。这有很多种实现方式,而其中最简单的一种,不外乎把 RGB 通道的值全设置成一样的:

    // 先采样出纹理的 vec4 颜色
    vec4 texColor = texture2D(img, vTexCoord);
    
    // 然后可以这样
    gl_FragColor = texColor.rrra;
    
    // 或者这样
    float average = (texColor.r + texColor.g + texColor.b) / 3.0;
    gl_FragColor = vec4(vec3(average), texColor.a);
    

    注意,在严格意义上,灰度化既不是用 R 通道覆盖 RGB,也不是对 RGB 通道简单取平均,而需要一个比例系数。这里为入门做了简化,效果如图:

    目前为止我们的着色器里,真正有效的代码都只有一两行而已。让我们来尝试下面这个更复杂一些的饱和度滤镜吧:

    precision highp float;
    uniform sampler2D img;
    varying vec2 vTexCoord;
    
    const float saturation = 0.5; // 饱和度比例常量
    
    void main() {
      vec4 color = texture2D(img, vTexCoord);
      float average = (color.r + color.g + color.b) / 3.0;
      if (saturation > 0.0) {
        color.rgb += (average - color.rgb) * (1.0 - 1.0 / (1.001 - saturation));
      } else {
        color.rgb += (average - color.rgb) * (-saturation);
      }
      gl_FragColor = color;
    }
    

    这个算法本身不是我们关注的重点,你很容易在社区找到各种各样的着色器。这里主要只是想告诉大家,着色器里是可以写 if else 的……

    增加饱和度后,效果如图所示:

    但这里还有一个不大不小的问题,那就是现在的饱和度比例还是这样的一个常量:

    const float saturation = 0.5;
    

    如果要实现「拖动滑块调节滤镜效果强度」这样常见的需求,难道要不停地更改着色器源码吗?显然不是这样的。为此,我们需要引入最后一种关键的资源类型:Uniform 资源。

    在 WebGL 中,Uniform 概念类似于全局变量。一般的全局变量,是在当前代码中可见,而 Uniform 则对于这个着色器并行中的每次执行,都是全局可见并唯一的。这样,着色器在计算每个像素的颜色时,都能拿到同一份「强度」参数的信息了。像上面 uniform sampler2D 类型的纹理采样器,就是这样的一个 Uniform 变量。只不过 Beam 处理了琐碎的下层细节,你只管把 JS 中的 Image 对象按约定传进来,就能把图像绑定到这个着色器变量里来使用了。

    每个 Uniform 都是一份短小的数据,如 vec4 向量或 mat4 矩阵等。要想使用它,可以从简单的着色器代码修改开始:

    precision highp float;
    varying vec2 vTexCoord;
    
    uniform sampler2D img;
    uniform float saturation; // 由 const 改为 uniform
    

    该怎么给这个变量赋值呢?在 Schema 里适配一下就行:

    const { vec2, vec4, float, tex2D } = SchemaTypes
    export const TextureDemo = {
      vs,
      fs,
      buffers: {
        position: { type: vec4, n: 3 },
        texCoord: { type: vec2 }
      },
      textures: {
        img: { type: tex2D }
      },
      // 新增这个 uniforms 字段
      uniforms: {
        saturation: { type: float, default: 0.5 }
      }
    }
    

    这里的 default 属于方便调试的语法糖,理论上这时代码的运行结果是完全一致的,只是 saturation 变量从 Shader 中的常量变成了从 JS 里传入。怎么进一步控制它呢?其实也很简单,只需要 beam.draw 的时候多传入个资源就行了:

    // ...
    // 创建 Uniform 资源
    const uniforms = beam.resource(Uniforms, {
      saturation: 0.5
    })
    
    // 异步加载图像
    fetchImage('venus.jpg').then(image => {
      textures.set('img', { image, flip: true })
    
      // Uniform 可以随时更新
      // uniforms.set('saturation', 0.4)
      beam
        .clear()
        .draw(shader, vertexBuffers, indexBuffer, uniforms, textures)
    })
    

    这样,我们就可以在 JS 中轻松地控制滤镜的强度了。像典型 3D 场景中,也是这样通过 Uniform 来控制相机位置等参数的。

    我们还可以将 Uniform 数组与卷积核函数配合,实现图像的边缘检测、模糊等效果,并支持无缝的效果强度调整。不要怕所谓的卷积和核函数,它们的意思只是「计算一个像素时,可以采样它附近的像素」而已。由于这种手法并不需要太多额外的 WebGL 能力,这里就不再展开了。

    这一步的例子,可以在 Single Filter 这里访问到。

    如何叠加多个图像

    现在,我们已经知道如何为单个图像编写着色器了。但另一个常见的需求是,如何处理需要混叠的多张图像呢?下面让我们看看该如何处理这样的图像叠加效果:

    JS 侧的渲染逻辑如下所示:

    // ...
    const render = ([imageA, imageB]) => {
      const imageStates = {
        img0: { image: imageA, flip: true },
        img1: { image: imageB, flip: true }
      }
    
      beam.clear().draw(
        shader,
        beam.resource(VertexBuffers, rect.vertex),
        beam.resource(IndexBuffer, rect.index),
        beam.resource(Textures, imageStates)
      )
    }
    
    loadImages('html5-logo.jpg', 'black-hole.jpg').then(render)
    

    这里只需要渲染一次,故而没有单独为 VertexBuffersIndexBuffer 等资源变量命名,直接在 draw 的时候初始化就行。那么关键的 Shader 该如何实现呢?此时的着色器 Schema 结构是这样的:

    const fs = `
    precision highp float;
    uniform sampler2D img0;
    uniform sampler2D img1;
    varying vec2 vTexCoord;
    
    void main() {
      vec4 color0 = texture2D(img0, vTexCoord);
      vec4 color1 = texture2D(img1, vTexCoord);
      gl_FragColor = color0 * color1.r;
    }
    `
    
    const { vec2, vec4, mat4, tex2D } = SchemaTypes
    export const MixImage = {
      vs, // 顶点着色器和前例相同
      fs,
      buffers: {
        position: { type: vec4, n: 3 },
        texCoord: { type: vec2 }
      },
      textures: {
        img0: { type: tex2D },
        img1: { type: tex2D }
      }
    }
    

    这里的核心代码在于 gl_FragColor = color0 * color1.r; 这一句,而这两个颜色则分别来自于对两张图像的 texture2D 采样。有了更丰富的输入,我们自然可以有更多的变化可以玩了。比如这样:

    gl_FragColor = color0 * (1.0 - color1.r);
    

    就可以得到相反的叠加结果。

    在现在的 WebGL 里,我们一般可以至少同时使用 16 个纹理。这个上限说实话也不小了,对于常见的图像混叠需求也都能很好地满足。但是浏览器自身也是通过类似的 GPU 渲染管线来渲染的,它是怎么渲染页面里动辄成百上千张图像的呢?这说起来知易行难,靠的是分块多次绘制。

    这一步的例子,可以在 Mix Images 这里访问到。

    如何组合多个滤镜

    到现在为止我们已经单独实现过多种滤镜了,但如何将它们的效果串联起来呢?WebGL 的着色器毕竟是字符串,我们可以做魔改拼接,生成不同的着色器。这确实是许多 3D 库中的普遍实践,也利于追求极致的性能。但这里选择的是一种工程上实现更为简洁优雅的方式,即离屏的链式渲染。

    假设我们有 A B C 等多种滤镜(即用于图像处理的着色器),那么该如何将它们的效果依次应用到图像上呢?我们需要先为原图应用滤镜 A,然后将 A 的渲染结果传给 B,再将 A + B 的渲染结果传给 C…依此类推,即可组成一条完整的滤镜链。

    为了实现这一目标,我们显然需要暂存某次渲染的结果。熟悉 Canvas 的同学一定对离屏渲染不陌生,在 WebGL 中也有类似的概念。但 WebGL 的离屏渲染,并不像 Canvas 那样能直接新建多个离屏的 <canvas> 标签,而是以渲染到纹理的方式来实现的。

    在给出代码前,我们需要先做些必要的科普。在 WebGL 和 OpenGL 体系中有个最为经典的命名槽点,那就是 Buffer 和 Framebuffer 其实完全是两种东西(不要误给 Framebuffer 加了驼峰命名噢)。Buffer 可以理解为存储大段有序数据的对象,而 Framebuffer 指代的则是屏幕!一般来说,我们渲染到屏幕时,使用的就是默认的物理 Framebuffer。但离屏渲染时,我们渲染的 Framebuffer 是个虚拟的对象,即所谓的 Framebuffer Object (FBO)。纹理对象可以 attach 到 Framebuffer Object 上,这样绘制时就会将像素数据写到内存,而不是物理显示设备了。

    上面的介绍有些绕口,其实只要记住这两件事就对了:

    • 离屏渲染时,要将渲染目标从物理 Framebuffer 换成 FBO。

    • FBO 只是个壳,要将纹理对象挂载上去,这才是像素真正写入的地方。

    对离屏渲染,Beam 也提供了完善的支持。FBO 有些绕口,因此 Beam 提供了名为 OffscreenTarget 的特殊资源对象。这种对象该如何使用呢?假设现在我们有 3 个着色器,分别是用于调整对比度、色相和晕影的滤镜,那么将它们串联使用的代码示例如下:

    import { Beam, ResourceTypes, Offscreen2DCommand } from 'beam-gl'
    
    // ...
    const beam = new Beam(canvas)
    // 默认导入的最小包不带离屏支持,需手动扩展
    beam.define(Offscreen2DCommand)
    
    // ...
    // 原图的纹理资源
    const inputTextures = beam.resource(Textures)
    
    // 中间环节所用的纹理资源
    const outputTextures = [
      beam.resource(Textures),
      beam.resource(Textures)
    ]
    
    // 中间环节所用的离屏对象
    const targets = [
      beam.resource(OffscreenTarget),
      beam.resource(OffscreenTarget)
    ]
    
    // 将纹理挂载到离屏对象上,这步的语义暂时还不太直观
    outputTextures[0].set('img', targets[0])
    outputTextures[1].set('img', targets[1])
    
    // 固定的矩形 Buffer 资源
    const rect= [rectVertex, rectIndex]
    
    const render = image => {
      // 更新输入纹理
      inputTextures.set('img', { image, flip: true })
    
      beam.clear()
      beam
        // 用输入纹理,渲染对比度滤镜到第一个离屏对象
        .offscreen2D(targets[0], () => {
          beam.draw(contrastShader, ...rect, inputTextures)
        })
        // 用第一个输出纹理,渲染色相滤镜到第二个离屏对象
        .offscreen2D(targets[1], () => {
          beam.draw(hueShader, ...rect, outputTextures[0])
        })
    
      // 用第二个输出纹理,渲染晕影滤镜直接上屏
      beam.draw(vignetteShader, ...rect, outputTextures[1])
    }
    
    fetchImage('prague.jpg').then(render)
    

    这里的渲染逻辑,其实只是将原本这样的代码结构:

    beam
      .clear()
      .draw(shaderX, ...resourcesA)
      .draw(shaderY, ...resourcesB)
      .draw(shaderZ, ...resourcesC)
    

    换成了扩展 offscreen2D API 后的这样:

    beam
      .clear()
      .offscreen2D(targetP, () => {
        beam.draw(shaderX, ...resourcesA)
      })
      .offscreen2D(targetQ, () => {
        beam.draw(shaderY, ...resourcesB)
      })
      .offscreen2D(targetR, () => {
        beam.draw(shaderZ, ...resourcesC)
      })
    // 还需要在外面再 beam.draw 一次,才能把结果上屏
    

    只要被嵌套在 offscreen2D 函数里,那么 beam.draw 在功能完全不变的前提下,渲染结果会全部走到相应的离屏对象里,从而写入离屏对象所挂载的纹理上。这样,我们就用函数的作用域表达出了离屏渲染的作用域!这是 Beam 的一大创新点,能用来支持非常灵活的渲染逻辑。比如这样的嵌套渲染结构,也是完全合法的:

    beam
      .clear()
      .offscreen2D(target, () => {
        beam
          .draw(shaderX, ...resourcesA)
          .draw(shaderY, ...resourcesB)
          .draw(shaderZ, ...resourcesC)
      })
      .draw(shaderW, ...resourcesD)
    

    离屏渲染的 API 看似简单,其实是 Beam 中耗费最多时间设计的特性之一,目前的方案也是经历过若干次失败的尝试,推翻了用数组、树和有向无环图来结构化表达渲染逻辑的方向后才确定的。当然它目前也还有不够理想的地方,希望大家可以多反馈意见和建议。

    现在,我们就能尝到滤镜链在可组合性上的甜头了。在依次应用了对比度、色相和晕影三个着色器后,渲染效果如下所示:

    这一步的例子,可以在 Multi Filters 这里访问到。

    如何引入 3D 效果

    现在,我们已经基本覆盖了 2D 领域的 WebGL 图像处理技巧了。那么,是否有可能利用 WebGL 在 3D 领域的能力,实现一些更为强大的特效呢?当然可以。下面我们就给出一个基于 Beam 实现「高性能图片爆破轮播」的例子。

    本节内容源自笔者在 现在作为前端入门,还有必要去学习高难度的 CSS 和 JS 特效吗?问题下的问答。阅读过这个回答的同学也可以跳过。

    相信大家应该见过一些图片爆炸散开成为粒子的效果,这实际上就是将图片拆解为了一堆形状。这时不妨假设图像位于单位坐标系上,将图像拆分为许多爆破粒子,每个粒子都是由两个三角形组成的小矩形。摄像机从 Z 轴俯视下去,就像这样:

    相应的数据结构呢?以上图的粒子为例,其中一个刚好在 X 轴中间的顶点,大致需要这些参数:

    • 空间位置,是粒子的三维坐标,这很好理解

    • 纹理位置,告诉 GPU 需要采样图像的哪个部分

    • 粒子中心位置,相当于让四个顶点团结在一起的 ID,免得各自跑偏了

    只要 50 行左右的 JS,我们就可以完成初始数据的计算:

    // 这种数据处理场景下,这个简陋的 push 性能好很多
    const push = (arr, x) => { arr[arr.length] = x }
    
    // 生成将图像等分为 n x n 矩形的数据
    const initParticlesData = n => {
      const [positions, centers, texCoords, indices] = [[], [], [], []]
    
      // 这种时候求别用 forEach 了
      for (let i = 0; i < n; i++) {
        for (let j = 0; j < n; j++) {
          const [x0, x1] = [i / n, (i + 1) / n] // 每个粒子的 x 轴左右坐标
          const [y0, y1] = [j / n, (j + 1) / n] // 每个粒子的 y 轴上下坐标
          const [xC, yC] = [x0 + x1 / 2, y0 + y1 / 2] // 每个粒子的中心二维坐标
          const h = 0.5 // 将中心点从 (0.5, 0.5) 平移到原点的偏移量
    
          // positions in (x, y), z = 0
          push(positions, x0 - h); push(positions, y0 - h)
          push(positions, x1 - h); push(positions, y0 - h)
          push(positions, x1 - h); push(positions, y1 - h)
          push(positions, x0 - h); push(positions, y1 - h)
    
          // texCoords in (x, y)
          push(texCoords, x0); push(texCoords, y0)
          push(texCoords, x1); push(texCoords, y0)
          push(texCoords, x1); push(texCoords, y1)
          push(texCoords, x0); push(texCoords, y1)
    
          // center in (x, y), z = 0
          push(centers, xC - h); push(centers, yC - h)
          push(centers, xC - h); push(centers, yC - h)
          push(centers, xC - h); push(centers, yC - h)
          push(centers, xC - h); push(centers, yC - h)
    
          // indices
          const k = (i * n + j) * 4
          push(indices, k); push(indices, k + 1); push(indices, k + 2)
          push(indices, k); push(indices, k + 2); push(indices, k + 3)
        }
      }
    
      // 着色器内的变量名是单数形式,将复数形式的数组名与其对应起来
      return {
        pos: positions,
        center: centers,
        texCoord: texCoords,
        index: indices
      }
    }
    

    现在我们已经能把原图拆分为一堆小矩形来渲染了。但这样还不够,因为默认情况下这些小矩形都是连接在一起的。借鉴一般游戏中粒子系统的实现,我们可以把动画算法写到着色器里,只逐帧更新一个随时间递增的数字,让 GPU 推算出每个粒子不同时间应该在哪。配套的着色器实现如下:

    /* 这是顶点着色器,片元着色器无须改动 */
    
    attribute vec4 pos;
    attribute vec4 center;
    attribute vec2 texCoord;
    
    uniform mat4 viewMat;
    uniform mat4 projectionMat;
    uniform mat4 rotateMat;
    uniform float iTime;
    
    varying vec2 vTexCoord;
    const vec3 camera = vec3(0, 0, 1);
    
    // 伪随机数生成器
    float rand(vec2 co) {
      return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453);
    }
    
    void main() {
      // 求出粒子相对于相机位置的单位方向向量,并附带上伪随机数的扰动
      vec3 dir = normalize(center.xyz * rand(center.xy) - camera);
      // 沿扰动后的方向,随时间递增偏移量
      vec3 translatedPos = pos.xyz + dir * iTime;
    
      // 给纹理坐标插值
      vTexCoord = texCoord;
      // 求出矩阵变换后最终的顶点位置
      gl_Position = projectionMat * viewMat * vec4(translatedPos, 1);
    }
    

    由于进入了 3D 世界,因此这个着色器引入了经典的 MVP 矩阵变换。这其实也已经远离了本文的主题,相信感兴趣的同学一定不难找到入门资料学习掌握。这个粒子效果的 Demo 如下所示。这里我们特意降低了粒子数量,方便大家看清它是怎么一回事:

    如果基于 CSS,只要有了几百个 DOM 元素要高频更新,渲染时就会显得力不从心。而相比之下基于 WebGL,稳定 60 帧更新几万个粒子是完全不成问题的。由此可见,在与图像处理相关的特效层面,WebGL 始终是有它的用武之地的。

    这一步的例子,可以在 Image Explode 这里访问到。

    如何封装自定渲染器

    最后,我们将视野回到前端工程,简单聊聊如何封装自己的渲染器。

    Beam 自身不是一个 WebGL 渲染器或渲染引擎,而是方便大家编写渲染逻辑的通用基础库。当我们想要进一步复用渲染逻辑的时候,封装出自己的 WebGL 渲染器就显得必要了。

    这里用 JS 中最为标准化的 class,演示如何封装出一个简单的滤镜渲染器:

    class FilterRenderer {
      constructor (canvas) {
        this.beam = new Beam(canvas)
        this.shader = this.beam(MyShader)
        this.rect = createRect()
        this.textures = this.beam.resource(Textures)
        this.uniforms = this.beam.resource(Uniforms, {
          strength: 0
        })
      }
    
      setStrength (strength) {
        this.uniforms.set('strength', strength)
      }
    
      setImage (image) {
        this.textures.set('img', { image, flip: true })
      }
    
      render () {
        const { beam, shader, rect, textures, uniforms } = this
        beam
          .clear()
          .draw(shader, rect, textures, uniforms)
      }
    }
    

    只要这样,在使用时就可以完全把 Beam 的 WebGL 概念屏蔽掉了:

    const renderer = new FilterRenderer(canvas)
    renderer.setImage(myImage)
    renderer.setStrength(1)
    renderer.render()
    

    这时值得注意的地方有这么几个:

    • 尽量在构造器对应的初始化阶段分配好资源

    • 尽量不要高频更新大段的 Buffer 数据

    • 不用的纹理和 Buffer 等资源要手动用 destroy 方法销毁掉

    当然,JS 中的 class 也不完美,而新兴的 Hooks 等范式也有潜力应用到这一领域,实现更好的 WebGL 工程化设计。该如何根据实际需求,定制出自己的渲染器呢?这就要看大家的口味和追求啦。

    后记

    为了尽量将各种重要的 WebGL 技巧浓缩在一起,快速达到足够实用的程度,本文篇幅显得有些长。虽然 Beam 的入门相对于 Vue 和 React 这样的常见框架来说还是有些门槛,但相比于一般需要分许多篇连载才能覆盖图像处理和离屏渲染的 WebGL 教程来说,我们已经屏蔽掉许多初学时不必关心的琐碎细节了。也欢迎大家对这种行文方式的反馈。

    值得一提的是,Beam 不是一个为图像处理而生的库,API 中也没有为这个场景做任何特殊定制。它的设计初衷,其实是作为我司 3D 文字功能的渲染器。但由于它的 WebGL 基础库定位,它在 10KB 不到的体积下,不仅能平滑地从 3D 应用到 2D,甚至在 2D 场景下的扩展性,还能轻松超过 glfx.js 这样尚不支持滤镜链的社区标杆。这也反映出了设计框架时常有的两种思路:一种是为每个新需求来者不拒地设计新的 API,将框架实现得包罗万象;另一种是谨慎地找到需求间的共性,实现最小的 API 子集供使用者组合定制。显然笔者更倾向于后者。

    Beam 的后续发展,也需要大家的支持——其实只要你不吝于给它个 Star 就够了。这会给我们更大的动力继续争取资源来维护它,或者进一步分享更多的 WebGL 知识与经验。欢迎大家移步这里:

    [Beam - Expressive WebGL] https://github.com/doodlewind/beam

    到此为止,相信我们已经对 WebGL 在图像处理领域的基本应用有了代码层面的认识了。希望大家对日常遇到的技术能少些「这么底层我管不来,用别人封装的东西就好」的心态,保持对舒适区外技术的学习热情,为自主创新贡献自己哪怕是微小的一份力量。

    作者:doodlewind花名雪碧 | github.com/doodlewind

    https://zhuanlan.zhihu.com/p/100388037

    原创系列推荐

    1. JavaScript 重温系列(22篇全)

    2. ECMAScript 重温系列(10篇全)

    3. JavaScript设计模式 重温系列(9篇全)

    4. 正则 / 框架 / 算法等 重温系列(16篇全)

    5. Webpack4 入门(上)|| Webpack4 入门(下)

    6. MobX 入门(上) ||  MobX 入门(下)

    7. 59篇原创系列汇总

    回复“加群”与大佬们一起交流学习~

    点这,与大家一起分享本文吧~

    展开全文
  • 2.1. WebGL 图像处理 2D 转换、旋转、伸缩、矩阵 2.1. WebGL 2D 图像转换 2.2. WebGL 2D 图像旋转 2.3. WebGL 2D 图像伸缩 2.4. WebGL 2D 矩阵 3D 4.1. WebGL 3D 正交 4.1. WebGL 3D 透视 4.1. WebGL 3D 摄像机 ...

    目录

    1. WebGL介绍
      1.1. WebGL基本原理
      1.2. WebGL工作原理
      1.3. WebGL 着色器和 GLSL
    2. 图像处理
      2.1. WebGL 图像处理
    3. 2D 转换、旋转、伸缩、矩阵
      2.1. WebGL 2D 图像转换
      2.2. WebGL 2D 图像旋转
      2.3. WebGL 2D 图像伸缩
      2.4. WebGL 2D 矩阵
    4. 3D
      4.1. WebGL 3D 正交
      4.1. WebGL 3D 透视
      4.1. WebGL 3D 摄像机

    WebGL介绍

    WebGL 是一种 3D 绘图标准,这种绘图技术标准允许把 JavaScript 和 OpenGL ES 2.0 结合在一起,通过增加 OpenGL ES 2.0 的一个 JavaScript 绑定,WebGL 可以为 HTML5 Canvas 提供硬件 3D 加速渲染,这样 Web 开发人员就可以借助系统显卡来在浏览器里更流畅地展示3D场景和模型了,还能创建复杂的导航和数据视觉化。

    WebGL基础

    WebGL基本原理

    WebGL 的出现使得在浏览器上面实现显示 3D 图像成为可能,WebGL 本质上是基于光栅化的 API ,而不是基于 3D 的 API。WebGL 只关注两个方面,即投影矩阵的坐标和投影矩阵的颜色。使用 WebGL 程序的任务就是实现具有投影矩阵坐标和颜色的 WebGL 对象即可。可以使用“着色器”来完成上述任务。顶点着色器可以提供投影矩阵的坐标,片段着色器可以提供投影矩阵的颜色。

    无论要实现的图形尺寸有多大,其投影矩阵的坐标的范围始终是从 -1 到 1。例如:

    
    // Get A WebGL context 
    var canvas = document.getElementById("canvas"); 
    var gl = canvas.getContext("experimental-webgl");   
    // setup a GLSL program 
    var program = createProgramFromScripts(gl, ["2d-vertex-shader", "2d-fragment-shader"]); gl.useProgram(program);  
    // look up where the vertex data needs to go. 
    var positionLocation = gl.getAttribLocation(program, "a_position");  
    // Create a buffer and put a single clipspace rectangle in 
    // it (2 triangles) 
    var buffer = gl.createBuffer(); 
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer); 
    gl.bufferData( gl.ARRAY_BUFFER, new Float32Array([ -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0]), 
    gl.STATIC_DRAW); 
    gl.enableVertexAttribArray(positionLocation); 
    gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);   
    // draw 
    gl.drawArrays(gl.TRIANGLES, 0, 6);
    

    下面是两个着色器。

    <script id="2d-vertex-shader" type="x-shader/x-vertex">
    attribute vec2 a_position; 
    void main() { 
        gl_Position = vec4(a_position, 0, 1); 
    } 
    </script>
    <script id="2d-fragment-shader" type="x-shader/x-fragment"> 
    void main() { 
        gl_FragColor = vec4(0, 1, 0, 1); // green
    } 
    </script>
    

    如果想实现 3D 的效果,那么可以使用着色器来将 3D 转换为投影矩阵,这是因为 WebGL 是基于光栅的 API。对于 2D 的图像,也许会使用像素而不是投影矩阵来表述尺寸,那么这里我们就更改这里的着色器,使得我们实现的矩形可以以像素的方式来度量,下面是新的顶点着色器。

    attribute vec2 a_position; 
    
    uniform vec2 u_resolution; 
    
    void main() { 
        // convert the rectangle from pixels to 0.0 to 1.0 
        vec2 zeroToOne = a_position / u_resolution; 
        // convert from 0->1 to 0->2 
        vec2 zeroToTwo = zeroToOne * 2.0; 
        // convert from 0->2 to -1->+1 (clipspace) 
        vec2 clipSpace = zeroToTwo - 1.0; 
        gl_Position = vec4(clipSpace, 0, 1); 
    }
    

    WebGL工作原理

    WebGL 和 GPU 是如何运作的。GPU 有两个基础任务,第一个就是将点处理为投影矩阵。第二部分就是基于第一部分将相应的像素点描绘出来。当用户调用

    gl.drawArrays(gl.TRIANGLE, 0, 9);   
    

    这里的 9 就意味着“处理 9 个顶点”,所以就有 9 个顶点需要被处理。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kEtRQ8hd-1631442055895)(en-resource://database/4815:1)]

    上图左侧的是用户自己提供的数据。顶点着色器就是用户在 GLSL 中写的函数。处理每个顶点时,均会被调用一次。用户可以将投影矩阵的值存储在特定的变量 gl_Position 中。

    GPU 会处理这些值,并将他们存储在其内部。假设用户希望绘制三角形 TRIANGLES, 那么每次绘制时,上述的第一部分就会产生三个顶点,然后 GPU 会使用他们来绘制三角形。

    首先 GPU 会将三个顶点对应的像素绘制出来,然后将三角形光栅化,或者说是使用像素点绘制出来。对每一个像素点,GPU 都会调用用户定义的片段着色器来确定该像素点该涂成什么颜色。当然,用户定义的片段着色器必须在 gl_FragColor 变量中设置对应的值。

    我们例子中的片段着色器中并没有存储每一个像素的信息。我们可以在其中存储更丰富的信息。我们可以为每一个值定义不同的意义从顶点着色器传递到片段着色器。

    作为一个简单的例子,我们将直接计算出来的投影矩阵坐标从顶点着色器传递给片段着色器。我们将绘制一个简单的三角形。我们在上个例子的基础上更改一下。

    function setGeometry(gl) {
        gl.bufferData( 
            gl.ARRAY_BUFFER, new Float32Array([ 0, -100, 150, 125, -175, 100]),                                 gl.STATIC_DRAW
        ); 
    }
    

    然后,我们绘制三个顶点。

    // Draw the scene. 
    function drawScene() { 
           ... // Draw the geometry. 
           gl.drawArrays(gl.TRIANGLES, 0, 3);
    }
    

    然后,我们可以在顶点着色器中定义变量来将数据传递给片段着色器。

    varying vec4 v_color; 
    ... void main() { 
        // Multiply the position by the matrix. 
        gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1); 
        // Convert from clipspace to colorspace.
        // Clipspace goes -1.0 to +1.0 
        // Colorspace goes from 0.0 to 1.0 
        v_color = gl_Position * 0.5 + 0.5;
    }
    

    然后,我们在片段着色器中声明相同的变量。

    precision mediump float; 
    varying vec4 v_color; 
    void main() { 
        gl_FragColor = v_color; 
    }
    

    WebGL 将会连接顶点着色器中的变量和片段着色器中的相同名字和类型的变量。下面是可以交互的版本。

    移动、缩放或旋转这个三角形。注意由于颜色是从投影矩阵计算而来,所以,颜色并不会随着三角形的移动而一直一样。他们完全是根据背景色设定的。

    现在我们考虑下面的内容。我们仅仅计算三个顶点。我们的顶点着色器被调用了三次,因此,仅仅计算了三个颜色。而我们的三角形可以有好多颜色,这就是为何被称为 varying。

    WebGL 使用了我们为每个顶点计算的三个值,然后将三角形光栅化。对于每一个像素,都会使用被修改过的值来调用片段着色器。

    基于上述例子,我们以三个顶点开始
    .[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0dZv4qpK-1631442055897)(en-resource://database/4817:1)]

    我们的顶点着色器会引用矩阵来转换、旋转、缩放和转化为投影矩阵。转换、旋转和缩放的默认值是转换为200,150,旋转为 0,缩放为 1,1,所以实际上只进行转换。我们的后台缓存是 400x300。我们的顶点矩阵应用矩阵然后计算下面的三个投影矩阵顶点。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ao53q6pQ-1631442055898)(en-resource://database/4819:1)]

    同样也会将这些转换到颜色空间上,然后将他们写到我们声明的多变变量 v_color。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NewWzvrC-1631442055900)(en-resource://database/4821:1)]
    这三个值会写回到 v_color,然后它会被传递到片段着色器用于每一个像素进行着色。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VR0FGgng-1631442055902)(en-resource://database/4823:1)]

    v_color 被修改为 v0,v1 和 v2 三个值中的一个。我们也可以在顶点着色器中存储更多的数据以便往片段着色器中传递。

    所以,对于以两种颜色绘制包含两个三角色的矩形的例子。为了实现这个例子,我们需要往顶点着色器中附加更多的属性,以便传递更多的数据,这些数据会直接传递到片段着色器中。

    attribute vec2 a_position; 
    attribute vec4 a_color; 
    ... 
    varying vec4 v_color;  
    void main() { 
    ... 
    // Copy the color from the attribute to the varying. 
        v_color = a_color; 
    }
    

    我们现在需要使用 WebGL 颜色相关的功能。

     var positionLocation = gl.getAttribLocation (program, "a_position"); 
     var colorLocation = gl.getAttribLocation(program, "a_color"); 
     ...
     // Create a buffer for the colors. 
     var buffer = gl.createBuffer(); 
     gl.bindBuffer(gl.ARRAY_BUFFER, buffer); 
     gl.enableVertexAttribArray(colorLocation); 
     gl.vertexAttribPointer(colorLocation, 4, gl.FLOAT, false, 0, 0); 
     // Set the colors. 
     setColors(gl); 
     // Fill the buffer with colors for the 2 triangles 
     // that make the rectangle. 
     function setColors(gl) { 
        // Pick 2 random colors. 
        var r1 = Math.random(); 
        var b1 = Math.random(); 
        var g1 = Math.random(); 
        var r2 = Math.random(); 
        var b2 = Math.random(); 
        var g2 = Math.random(); 
        gl.bufferData( 
            gl.ARRAY_BUFFER, 
            new Float32Array( [ r1, b1, g1, 1, r1, b1, g1, 1, r1, b1, g1, 1, r2, b2, g2, 1, r2, b2, g2, 1, r2, b2, g2, 1]), 
        gl.STATIC_DRAW); 
    } 
    

    下面是结果。注意,在上面的例子中,有两个苦点颜色的三角形。我们仍将要传递的值存储在多变变量中,所以,该变量会相关三角形区域内改变。我们只是对于每个三角形的三个顶点使用了相同的颜色。如果我们使用了不同的颜色,我们可以看到整个渲染过程。

    // Fill the buffer with colors for the 2 triangles
    // that make the rectangle. 
    function setColors(gl) { 
        // Make every vertex a different color. 
        gl.bufferData( 
            gl.ARRAY_BUFFER, 
            new Float32Array( [ 
                Math.random(), 
                Math.random(), 
                Math.random(), 1, 
                Math.random(), 
                Math.random(), 
                Math.random(), 1, 
                Math.random(), 
                Math.random(), 
                Math.random(), 1, 
                Math.random(), 
                Math.random(), 
                Math.random(), 1, 
                Math.random(), 
                Math.random(), 
                Math.random(), 1, 
                Math.random(), 
                Math.random(), 
                Math.random(), 1]), gl.STATIC_DRAW); 
     }
    

    从顶点着色器往片段着色器可以传递更多更丰富的数据。

    缓存和属性指令

    缓存是获取顶点和顶点相关数据到 GPU 中的方法。gl.createBuffer 用于创建缓存。
    gl.bindBuffer 方法用于将缓存激活来处于准备工作的状态。
    gl.bufferData 方法可以将数据拷贝到缓存中。一旦,数据到了缓存中,就需要告诉 WebGL 如何从里面除去数据,并将它提供给顶点着色器以给相应的属性赋值。
    为了实现这个功能,首先我们需要求出 WebGL 提供一个属性存储位置。

    // look up where the vertex data needs to go. 
    var positionLocation = gl.getAttribLocation(program, "a_position"); 
    var colorLocation = gl.getAttribLocation(program, "a_color");
    

    我们可以触发两个指令

    gl.enableVertexAttribArray(location);
    

    这个指令会告诉 WebGL 我们希望将缓存中的数据赋值给一个变量。

    gl.vertexAttribPointer( 
        location, 
        numComponents, 
        typeOfData, 
        normalizeFlag, 
        strideToNextPieceOfData, 
        offsetIntoBuffer,
    );
    

    这个指令会告诉 WebGL 会从缓存中获取数据,这个缓存会与 gl.bindBuffer 绑定。每个顶点可以有 1 到 4 个部件,数据的类型可以是 BYTE,FLOAT,INT,UNSIGNED_SHORT 等。跳跃意味着从数据的这片到那片会跨越多少个字节。跨越多远会以偏移量的方式存储在缓存中。部件的数目一般会是 1 到 4。如果每个数据类型仅使用一个缓存,那么跨越和偏移量都会是 0。跨越为 0 意味着“使用一个跨越连匹配类型和尺寸”。偏移量为 0 意味着是在缓存的开头部分。将这个值赋值为除 O 之外其他的值会实现更为灵活的功能。虽然在性能方面它有些优势,但是并不值得搞得很复杂,除非程序员希望将 WebGL 运用到极致。

    vertexAttribPointer 的规范化标志 normalizeFlag

    规范化标志应用于非浮点指针类型。如果该值置为 false, 就意味着该值就会被翻译为类型。BYTE 的标示范围是-128 到 127。UNSIGNED_BYTE 范围是 0 到 255,SHORT 是从-32768 到 32767。

    如果将规范化标志置为 true,那么BYTE的标示范围将为变为-1.0 到 +1.0,UNSIGNED_BYTE 将会变为 0.0 到 +1.0,规范化后的 SHORT 将会变为 -1.0 到 +1.0,它将有比 BYTE 更高的精确度.标准化数据最通用的地方就是用于颜色。

    大部分时候,颜色范围为 0.0 到 1.0 红色、绿色和蓝色需要个浮点型的值来表示,alpha 需要 16 字节来表示顶点的每个颜色。如果要实现更为复杂的图形,可以增加更多的字节。

    相反的,程序可以将颜色转为 UNSIGNED_BYTE 类型,这个类型使用 0 表示 0.0,使用 255 表示 1.0。那么仅需要 4 个字节来表示顶点的每个颜色,这将节省 75% 的存储空间。我们按照下面的方式来更改我们的代码。当我们告诉 WebGL 如何获取颜色。

    gl.vertexAttribPointer(colorLocation, 4, gl.UNSIGNED_BYTE, true, 0, 0);
    

    使用下面的代码来填充我们的缓冲区

    function setColors(gl) { 
        // Pick 2 random colors. 
        var r1 = Math.random() * 256; 
        // 0 to 255.99999 
        var b1 = Math.random() * 256; 
        // these values 
        var g1 = Math.random() * 256; 
        // will be truncated 
        var r2 = Math.random() * 256; 
        // when stored in the 
        var b2 = Math.random() * 256;
        // Uint8Array 
        var g2 = Math.random() * 256; 
        gl.bufferData( gl.ARRAY_BUFFER, new Uint8Array( 
        [ r1, b1, g1, 255, r1, b1, g1, 255, r1, b1, g1, 255, r2, b2, g2, 255, r2, b2, g2, 255, r2, b2, g2, 255]), gl.STATIC_DRAW); 
    }
    

    WebGL着色器和GLSL

    WebGL每次绘制,都需要两个着色器,分别是顶点着色器和片段着色器。每个着色器都是一个函数。顶点着色器和片段着色器都是链接在程序中的。一个典型的 WebGL 程序都会包含很多这样的着色器程序。

    顶点着色器

    顶点着色器的任务就是产生投影矩阵的坐标。其形式如下:

    void main() { 
        gl_Position = doMathToMakeClipspaceCoordinates 
    }
    

    每一个顶点都会调用你的着色器。每次调用程序都需要设置特定的全局变量 gl_Position 来表示投影矩阵的坐标。顶点着色器需要数据,它以下面三种方式来获取这些数据。

    • 属性(从缓冲区中获取数据)
    • 一致变量(每次绘画调用时都保持一致的值)
    • 纹理(从像素中得到的数据)

    属性
    最常用的方式就是使用缓存区和属性。程序可以以下面的方式创建缓存区。

    var buf = gl.createBuffer();
    

    在这些缓存中存储数据。

    gl.bindBuffer(gl.ARRAY_BUFFER, buf); gl.bufferData(gl.ARRAY_BUFFER, someData, gl.STATIC_DRAW);
    

    于是,给定一个着色器程序,程序可以去查找属性的位置。

    var positionLoc = gl.getAttribLocation(someShaderProgram, "a_position");
    

    下面告诉 WebGL 如何从缓存区中获取数据并存储到属性中。

    // turn on getting data out of a buffer for this attribute 
    gl.enableVertexAttribArray(positionLoc);
    var numComponents = 3;  // (x, y, z) 
    var type = gl.FLOAT; 
    var normalize = false; // leave the values as they are 
    var offset = 0; // start at the beginning of the buffer 
    var stride = 0; // how many bytes to move to the next vertex // 0 = use the correct stride for type and numComponents 
    gl.vertexAttribPointer(positionLoc, numComponents, type, false, stride, offset);
    

    如果我们可以将投影矩阵放入我们的缓存区中,它就会开始运作。属性可以使用 float,vec2,vec3,vec4,mat2,mat3 和 mat4 作为类型。

    一致性变量
    对于顶点着色器,一致性变量就是在绘画每次调用时,在着色器中一直保持不变的值。下面是一个往顶点中添加偏移量着色器的例子。

    attribute vec4 a_position; 
    uniform vec4 u_offset; 
    void main() { 
        gl_Position = a_position + u_offset; 
    }
    

    下面,我们需要对每一个顶点都偏移一定量。首先,我们需要先找到一致变量的位置。

    var offsetLoc = gl.getUniformLocation(someProgram, "u_offset");
    

    然后,我们在绘制前需要设置一致性变量

    gl.uniform4fv(offsetLoc, [1, 0, 0, 0]); // offset it to the right half the screen
    

    一致性变量可以有很多种类型。对每一种类型都可以调用相应的函数来设置。

    gl.uniform1f (floatUniformLoc, v); // for float 
    gl.uniform1fv(floatUniformLoc, [v]); // for float or float array 
    gl.uniform2f (vec2UniformLoc, v0, v1);// for vec2 
    gl.uniform2fv(vec2UniformLoc, [v0, v1]); // for vec2 or vec2 array 
    gl.uniform3f (vec3UniformLoc, v0, v1, v2);// for vec3 
    gl.uniform3fv(vec3UniformLoc, [v0, v1, v2]); // for vec3 or vec3 array 
    gl.uniform4f (vec4UniformLoc, v0, v1, v2, v4);// for vec4 
    gl.uniform4fv(vec4UniformLoc, [v0, v1, v2, v4]); // for vec4 or vec4 array 
    gl.uniformMatrix2fv(mat2UniformLoc, false, [ 4x element array ]) // for mat2 or mat2 array 
    gl.uniformMatrix3fv(mat3UniformLoc, false, [ 9x element array ]) // for mat3 or mat3 array 
    gl.uniformMatrix4fv(mat4UniformLoc, false, [ 17x element array ]) // for mat4 or mat4 array 
    gl.uniform1i (intUniformLoc, v); // for int 
    gl.uniform1iv(intUniformLoc, [v]); // for int or int array 
    gl.uniform2i (ivec2UniformLoc, v0, v1);// for ivec2 
    gl.uniform2iv(ivec2UniformLoc, [v0, v1]); // for ivec2 or ivec2 array 
    gl.uniform3i (ivec3UniformLoc, v0, v1, v2);// for ivec3 
    gl.uniform3iv(ivec3UniformLoc, [v0, v1, v2]); // for ivec3 or ivec3 array 
    gl.uniform4i (ivec4UniformLoc, v0, v1, v2, v4);// for ivec4 
    gl.uniform4iv(ivec4UniformLoc, [v0, v1, v2, v4]); // for ivec4 or ivec4 array 
    gl.uniform1i (sampler2DUniformLoc, v); // for sampler2D (textures) 
    gl.uniform1iv(sampler2DUniformLoc, [v]); // for sampler2D or sampler2D array 
    gl.uniform1i (samplerCubeUniformLoc, v); // for samplerCube (textures) 
    gl.uniform1iv(samplerCubeUniformLoc, [v]); // for samplerCube or samplerCube array
    

    一般类型都有 bool,bvec2,bvec3 和 bvec4。他们相应的调用函数形式为 gl.uniform?f?gl.uniform?i?。可以一次性设置数组中的所有一致性变量。比如:

    uniform vec2 u_someVec2[3]; 
    gl.getUniformLocation(someProgram, "u_someVec2");
    gl.uniform2fv(someVec2Loc, [1, 2, 3, 4, 5, 6]);
    

    但是,如果程序希望单独设置数组中的成员,那么必须单个的查询每个成员的位置。

    var someVec2Element0Loc = gl.getUniformLocation(someProgram, "u_someVec2[0]"); var someVec2Element1Loc = gl.getUniformLocation(someProgram, "u_someVec2[1]"); var someVec2Element2Loc = gl.getUniformLocation(someProgram, "u_someVec2[2]"); 
    // at render time 
    gl.uniform2fv(someVec2Element0Loc, [1, 2]); // set element 0 
    gl.uniform2fv(someVec2Element1Loc, [3, 4]); // set element 1 
    gl.uniform2fv(someVec2Element2Loc, [5, 6]); // set element 2
    

    类似的,可以创建一个结构体

    struct SomeStruct { 
        bool active; vec2 someVec2; 
    }; 
    uniform SomeStruct u_someThing;
    

    程序可以单独的查询每一个成员。

    var someThingActiveLoc = gl.getUniformLocation(someProgram, "u_someThing.active"); var someThingSomeVec2Loc = gl.getUniformLocation(someProgram, "u_someThing.someVec2");
    

    片段着色器

    片段着色器的任务就是为当前被栅格化的像素提供颜色。它通常以下面的方式呈现出来。

    precision mediump float; 
    void main() { 
        gl_FragColor = doMathToMakeAColor; 
     }
    

    片段着色器对每一个像素都会调用一次。每次调用都会设置全局变量 gl_FragColor 来设置一些颜色。片段着色器需要存储获取数据,通常有下面这三种方式。

    • 一致变量(每次绘制像素点时都会调用且一直保持一致)
    • 纹理(从像素中获取数据)
    • 多变变量(从定点着色器中传递出来且被栅格化的值)

    片段着色器中的纹理
    我们可以从纹理中获取值来创建 sampler2D 一致变量, 然后使用 GLSL 函数 texture2D 来从中获取值。

    precision mediump float; 
    uniform sampler2D u_texture;
    void main() { 
        vec2 texcoord = vec2(0.5, 0.5) 
        gl_FragColor = texture2D(u_texture, texcoord); 
    }
    

    从纹理中提取的值是要依据很多设置的。最基本的,我们需要创建并在文理中存储值。比如:

    var tex = gl.createTexture(); 
    gl.bindTexture(gl.TEXTURE_2D, tex); 
    var level = 0; 
    var width = 2; 
    var height = 1; 
    var data = new Uint8Array([255, 0, 0, 255, 0, 255, 0, 255]);
    gl.texImage2D(gl.TEXTURE_2D, level, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
    

    然后,在着色器程序中查询一致变量的位置。

    var someSamplerLoc = gl.getUniformLocation(someProgram, "u_texture");
    

    WebGL 需要将它绑定到纹理单元中。

    var unit = 5;
    gl.activeTexture(gl.TEXTURE0 + unit);
    gl.bindTexture(gl.TEXTURE_2D, tex);
    

    然后告知着色器哪个单元会被绑定到纹理中

    gl.uniform1i(someSamplerLoc, unit);
    

    多变变量
    多变变量是从顶点着色器往片段着色器中传递的值,这些在WebGL 如何工作的已经涵盖了。为了使用多变变量,需要在顶点着色器和片段着色器中均设置匹配的多变变量。我们在顶点着色器中设置多变变量。当 WebGL 绘制像素时,它会栅格化该值,然后传递到片段着色器中相对应的片段着色器。

    顶点着色器

    attribute vec4 a_position; 
    uniform vec4 u_offset; 
    varying vec4 v_positionWithOffset; 
    void main() { 
        gl_Position = a_position + u_offset; 
        v_positionWithOffset = a_position + u_offset; 
    }
    

    片段着色器

    precision mediump float; 
    varying vec4 v_positionWithOffset; 
    void main() { 
        // convert from clipsapce (-1 <-> +1) to color space (0 -> 1). 
        vec4 color = v_positionWithOffset * 0.5 + 0.5 
        gl_FragColor = color; 
    }
    

    GLSL

    GLSL是图像库着色器语言的简称。语言着色器就是被写在这里。它具有一些 JavaScript 中不存在的独特的特性。 它用于实现一些逻辑来渲染图像。比如,它可以创建类似于 vec2,vec3 和 vec4 分别表示2、3、4个值。类似的,mat2,mat3 和 mat4 来表示 2x2,3x3,4x4 的矩阵。可以实现 vec 来乘以一个标量。

    vec4 a = vec4(1, 2, 3, 4); vec4 b = a * 2.0;
    

    实现矩阵的乘法和矩阵的向量乘法:

    mat4 a = ???
    mat4 b = ???
    mat4 c = a * b;
    
    vec4 v = ???
    vec4 y = c * v;
    

    也可以选择 vec 的部分,比如,vec4

    vec4 v;
    
    • v.x 等价于 v.s,v.r,v[0]
    • v.y 等价于 v.t,v.g,v[1]
    • v.z 等价于 v.p,v.b,v[2]
    • v.w 等价于 v.q,v.a,v[3]
      可以调整 vec 组件意味着可以交换或重复组件。
    v.yyyy
    

    这等价于

    vec4(v.y, v.y, v.y, v.y)
    

    类似的

    v.bgra
    

    等价于

    vec4(v.b, v.g, v.r, v.a)
    

    当创建一个 vec 或 一个 mat时,程序可以一次操作多个部分:

    vec4(v.rgb, 1)
    

    这等价于

    vec4(v.r, v.g, v.b, 1)
    

    你可能意识到 GLSL 是一种很严格类型的语言

    float f = 1;
    

    正确的方式如下:

    float f = 1.0; // use float 
    float f = float(1) // cast the integer to a float
    

    上面例子的 vec4(v.rgb, 1) 并不会对 1 进行混淆,这是因为 vec4 是类似于 float(1)。GLSL 是内置函数的分支.可以同时操作多个组件。比如,

    T sin(T angle)
    

    这意味着 T 可以是 float,vec2,vec3 或 vec4。如果用户在 vec4 中传递数据。也就是说 v 是 vec4,

    vec4 s = sin(v);
    

    折等价于

    vec4 s = vec4(sin(v.x), sin(v.y), sin(v.z), sin(v.w));
    

    有时候,一个参数是 float,另一个是 T。这意味着 float 将应用到所有的部件。比如,如果 v1,v2 是 vec4,f 是 flat,然后

    vec4 m = mix(v1, v2, f);
    

    这等价于

    vec4 m = vec4( 
        mix(v1.x, v2.x, f), 
        mix(v1.y, v2.y, f), 
        mix(v1.z, v2.z, f), 
        mix(v1.w, v2.w, f),
    );
    

    混合起来看

    WebGL 是关于创建各种着色器的,将数据存储在这些着色器上,然后调用 gl.drawArrays 或 gl.drawElements 来使 WebGL 处理顶点通过为每个顶点调用当前的顶点着色器,然后为每一个像素调用当前的片段着色器。

    实际上,着色器的创建需要写几行代码。因为,这些代码在大部分 WebGL 程序中的是一样的,又因为一旦写了,可以几乎可以忽略他们如何编译 GLSL 着色器和链接成一个着色器程序。

    WebGL图像处理

    WebGL 会需要操作投影矩阵的坐标,WebGL 读取纹理时需要获取纹理坐标。纹理坐标范围是从 0.0 到 1.0。

    因为我们仅需要绘制由两个三角形组成的矩形,我们需要告诉 WebGL 在矩阵中纹理对应的那个点。我们可以使用特殊的被称为多变变量,会将这些信息从顶点着色器传递到片段着色器。

    WebGL 将会插入这些值,这些值会在顶点着色器中,当对每个像素绘制时均会调用片段着色器。我们需要在纹理坐标传递过程中添加更多的信息,然后将他们传递到片段着色器中。

    attribute vec2 a_texCoord;
    ... 
    varying vec2 v_texCoord; 
    void main() { 
        ... 
        // pass the texCoord to the fragment shader 
        // The GPU will interpolate this value between points
        v_texCoord = a_texCoord; 
    }
    

    片段着色器来查找颜色纹理

    precision mediump float; 
    // our texture 
    uniform sampler2D u_image; 
    // the texCoords passed in from the vertex shader. 
    varying vec2 v_texCoord; 
    void main() { 
        // Look up a color from the texture. 
        gl_FragColor = texture2D(u_image, v_texCoord); 
    }
    

    加载一个图片,然后创建一个问题,将该图片传递到纹理里面

    function main() { 
        var image = new Image();
        image.src = "http://someimage/on/our/server"; 
        image.onload = function() { 
            render(image); 
        } 
    } 
    function render(image) {
        ... 
        // all the code we had before.
        ... 
        // look up where the texture coordinates need to go. 
        var texCoordLocation = gl.getAttribLocation(program, "a_texCoord");
        // provide texture coordinates for the rectangle. 
        var texCoordBuffer = gl.createBuffer(); 
        gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); 
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0]), 
        gl.STATIC_DRAW); 
        gl.enableVertexAttribArray(texCoordLocation); 
        gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0); 
        // Create a texture. 
        var texture = gl.createTexture(); 
        gl.bindTexture(gl.TEXTURE_2D, texture); 
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, 
        gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, 
        gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 
        // Upload the image into the texture. 
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); 
        ... 
    }
    

    如下是 WebGL 渲染出来的图像
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gJ8YRx1n-1631442055903)(en-resource://database/4827:1)]
    下面对这个图片进行一些操作,来交换图片中的红色和蓝色

    ... 
    gl_FragColor = texture2D(u_image, v_texCoord).bgra;
    ...
    

    现在红色和蓝色已经被交换了
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HR5D0XEE-1631442055904)(en-resource://database/4829:1)]

    自从 WebGL 引用纹理的纹理坐标从 0.0 到 1.0 。然后,我们可以计算移动的多少个像素 onePixel = 1.0 / textureSize。这里有个片段着色器来平均纹理中每个像素的左侧和右侧的像素。

    precision mediump float;
    
    // our texture
    uniform sampler2D u_image;
    uniform vec2 u_textureSize;
    
    // the texCoords passed in from the vertex shader.
    varying vec2 v_texCoord;
    
    void main() {
       // compute 1 pixel in texture coordinates.
       vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;
    
       // average the left, middle, and right pixels.
       gl_FragColor = (
       texture2D(u_image, v_texCoord) +
       texture2D(u_image, v_texCoord + vec2(onePixel.x, 0.0)) +
       texture2D(u_image, v_texCoord + vec2(-onePixel.x, 0.0))) / 3.0;
    }
    

    通过 JavaScript 传递出纹理的大小

    ... 
    var textureSizeLocation = gl.getUniformLocation(program, "u_textureSize"); 
    ... 
    // set the size of the image 
    gl.uniform2f(textureSizeLocation, image.width, image.height); ...
    

    比较上述两个图片
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B10V21RS-1631442055905)(en-resource://database/4831:1)]

    使用 3x3 的内核。卷积内核就是一个 3x3 的矩阵,矩阵中的每个条目代表有多少像素渲染。然后,我们将这个结果除以内核的权重或 1.0。

    在着色器中做这样工作,这里是一个新的片段着色器

    precision mediump float;
    
    // our texture 
    uniform sampler2D u_image; 
    uniform vec2 u_textureSize; 
    uniform float u_kernel[9]; 
    uniform float u_kernelWeight; 
    // the texCoords passed in from the vertex shader. 
    varying vec2 v_texCoord; 
    void main() { 
        vec2 onePixel = vec2(1.0, 1.0) / u_textureSize; 
        vec4 colorSum = 
        texture2D(u_image, v_texCoord + onePixel * vec2(-1, -1)) * u_kernel[0] + 
        texture2D(u_image, v_texCoord + onePixel * vec2( 0, -1)) * u_kernel[1] + 
        texture2D(u_image, v_texCoord + onePixel * vec2( 1, -1)) * u_kernel[2] + 
        texture2D(u_image, v_texCoord + onePixel * vec2(-1, 0)) * u_kernel[3] + 
        texture2D(u_image, v_texCoord + onePixel * vec2( 0, 0)) * u_kernel[4] + 
        texture2D(u_image, v_texCoord + onePixel * vec2( 1, 0)) * u_kernel[5] + 
        texture2D(u_image, v_texCoord + onePixel * vec2(-1, 1)) * u_kernel[6] + 
        texture2D(u_image, v_texCoord + onePixel * vec2( 0, 1)) * u_kernel[7] + 
        texture2D(u_image, v_texCoord + onePixel * vec2( 1, 1)) * u_kernel[8] ; 
        // Divide the sum by the weight but just use rgb 
        // we'll set alpha to 1.0 
        gl_FragColor = vec4((colorSum / u_kernelWeight).rgb, 1.0); }
    

    提供一个卷积内核和它的权重:

    function computeKernelWeight(kernel) { 
        var weight = kernel.reduce(function(prev, curr) { 
            return prev + curr; 
        }); 
        return weight <= 0 ? 1 : weight; 
    } 
    ... 
    var kernelLocation = gl.getUniformLocation(program, "u_kernel[0]"); 
    var kernelWeightLocation = gl.getUniformLocation(program, "u_kernelWeight"); 
    ... 
    var edgeDetectKernel = [ -1, -1, -1, -1, 8, -1, -1, -1, -1 ]; 
    gl.uniform1fv(kernelLocation, edgeDetectKernel); 
    gl.uniform1f(kernelWeightLocation, computeKernelWeight(edgeDetectKernel)); 
    ...
    

    实时的渲染

    使用两种或更多的纹理和渲染效果来交替渲染,每次应用一个效果,然后反复应用.

    Original Image -> [Blur]-> Texture 1 
    Texture 1 -> [Sharpen] -> Texture 2 
    Texture 2 -> [Edge Detect] -> Texture 1 
    Texture 1 -> [Blur]-> Texture 2 
    Texture 2 -> [Normal] -> Canvas
    

    创建帧缓存区。在 WebGL 和 OpenGL 中,帧缓存区实际上是一个非常不正式的名称。 WebGL/OpenGL 中的帧缓存实际上仅仅是一些状态的集合,而不是真正的缓存。但是,每当一种纹理到达帧缓存,我们就会渲染出这种纹理。

    把旧的纹理创建代码写成一个函数:

    function createAndSetupTexture(gl) { 
        var texture = gl.createTexture(); 
        gl.bindTexture(gl.TEXTURE_2D, texture); 
        // Set up texture so we can render any size image and so we are 
        // working with pixels. 
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 
        return texture; 
    } 
    // Create a texture and put the image in it. 
    var originalImageTexture = createAndSetupTexture(gl); 
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
    

    用这两个函数来生成两种问题,并且附在两个帧缓存中:

    // create 2 textures and attach them to framebuffers. 
    var textures = []; 
    var framebuffers = []; 
    for (var ii = 0; ii < 2; ++ii) { 
        var texture = createAndSetupTexture(gl); 
        textures.push(texture);
        // make the texture the same size as the image 
        gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, image.width, image.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
        // Create a framebuffer 
        var fbo = gl.createFramebuffer(); 
        framebuffers.push(fbo); 
        gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); 
        // Attach a texture to it. 
        gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
     }
    

    生成一些核的集合,然后存储到列表里来应用:

     var kernels = { 
        normal: [ 0, 0, 0, 0, 1, 0, 0, 0, 0 ], 
        gaussianBlur: [ 0.045, 0.122, 0.045, 0.122, 0.332, 0.122, 0.045, 0.122, 0.045 ], 
        unsharpen: [ -1, -1, -1, -1, 9, -1, -1, -1, -1 ], 
        emboss: [ -2, -1, 0, -1, 1, 1, 0, 1, 2 ]
     }; 
     // List of effects to apply. 
     var effectsToApply = [ "gaussianBlur", "emboss", "gaussianBlur", "unsharpen" ];
    

    最后,应用每一个,然后交替渲染:

    // start with the original image 
    gl.bindTexture(gl.TEXTURE_2D, originalImageTexture); 
    // don't y flip images while drawing to the textures
    gl.uniform1f(flipYLocation, 1); 
    // loop through each effect we want to apply. 
    for (var ii = 0; ii < effectsToApply.length; ++ii) {
        // Setup to draw into one of the framebuffers. 
        setFramebuffer(framebuffers[ii % 2], image.width, image.height); 
        drawWithKernel(effectsToApply[ii]); 
        // for the next draw, use the texture we just rendered to. 
        gl.bindTexture(gl.TEXTURE_2D, textures[ii % 2]); }
        // finally draw the result to the canvas. 
        gl.uniform1f(flipYLocation, -1); 
        // need to y flip for canvas 
        setFramebuffer(null, canvas.width, canvas.height); 
        drawWithKernel("normal"); 
        function setFramebuffer(fbo, width, height) { 
        // make this the framebuffer we are rendering to. 
        gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); 
        // Tell the shader the resolution of the framebuffer. 
        gl.uniform2f(resolutionLocation, width, height); 
        // Tell webgl the viewport setting needed for framebuffer. 
        gl.viewport(0, 0, width, height); 
    } 
    function drawWithKernel(name) { 
        // set the kernel 
        gl.uniform1fv(kernelLocation, kernels[name]); 
        // Draw the rectangle. 
        gl.drawArrays(gl.TRIANGLES, 0, 6); 
    }
    

    以空值调用 gl.bindFramebuffer 即可告诉 WebGL 程序希望渲染到画板而不是帧缓存中的纹理.WebGL 不得不将投影矩阵转换为像素。这是基于 gl.viewport 的设置。当我们初始化 WebGL 的时候, gl.viewport 的设置默认为画板的尺寸。

    因为,我们会将帧缓存渲染为不同的尺寸,所以画板需要设置合适的视图。最后,在原始例子中,当需要渲染的时候,我们会翻转 Y 坐标。这是因为 WebGL 会以 0 来显示面板。 0 表示是左侧底部的坐标,这不同于 2D 图像的顶部左侧的坐标。当渲染为帧缓存时就不需要了。

    这是因为帧缓存并不会显示出来。其部分是顶部还是底部是无关紧要的。所有重要的就是像素 0,0 在帧缓存里就对应着 0。为了解决这一问题,我们可以通过是否在着色器中添加更多输入信息的方法来设置是否快读交替。

    ... 
    uniform float u_flipY; 
    ... 
    void main() { 
    ... 
    gl_Position = vec4(clipSpace * vec2(1, u_flipY), 0, 1);
    ... 
    }
    

    当我们渲染的时候,就可以设置它

    var flipYLocation = gl.getUniformLocation(program, "u_flipY"); 
    ...
    // don't flip 
    gl.uniform1f(flipYLocation, 1); 
    ... 
    // flip 
    gl.uniform1f(flipYLocation, -1);
    

    如果想做完整的图像处理你可能需要许多 GLSL 程序。一个程序实现色相、饱和度和亮度调整。另一个实现亮度和对比度。一个实现反相,另一个用于调整水平。可能需要更改代码以更新 GLSL 程序和更新特定程序的参数。

    WebGL 2D 转换、旋转、伸缩、矩阵

    WebGL 2D图像转换

    基于Translation 2D图像转换基本示例:

     // First lets make some variables 
     // to hold the translation of the rectangle 
     var translation = [0, 0]; 
     // then let's make a function to 
     // re-draw everything. We can call this 
     // function after we update the translation.
     // Draw a the scene. 
     function drawScene() {
        // Clear the canvas. 
        gl.clear(gl.COLOR_BUFFER_BIT); 
        // Setup a rectangle 
        setRectangle(gl, translation[0], translation[1], width, height); 
        // Draw the rectangle. 
        gl.drawArrays(gl.TRIANGLES, 0, 6); 
    }
    

    可以通过滑动按钮来修改 translation[0] 和 translation[1] 的值,而且在这个两个值发生修改时调用 drawScene 函数对界面进行更新。拖动滑动条对矩阵进行移动。

    使用的改变 setRectangle 值的代码:

    function setGeometry(gl, x, y) { 
        var width = 100; 
        var height = 150; 
        var thickness = 30; 
        gl.bufferData( gl.ARRAY_BUFFER, 
            new Float32Array([ 
            // left column 
            x, y, 
            x + thickness, y, 
            x, y + height, 
            x, y + height, 
            x + thickness, y, 
            x + thickness, y + height, 
            
            // top rung 
            x + thickness, y, 
            x + width, y, 
            x + thickness, y + thickness, 
            x + thickness, y + thickness, 
            x + width, y, 
            x + width, y + thickness, 
            
            // middle rung 
            x + thickness, y + thickness * 2, 
            x + width * 2 / 3, y + thickness * 2, 
            x + thickness, y + thickness * 3, 
            x + thickness, y + thickness * 3, 
            x + width * 2 / 3, y + thickness * 2, 
            x + width * 2 / 3, y + thickness * 3]), 
            gl.STATIC_DRAW);
    }
    

    渲染器部分:

    attribute vec2 a_position; 
    uniform vec2 u_resolution; 
    uniform vec2 u_translation; 
    void main() { 
        // Add in the translation. 
        vec2 position = a_position + u_translation; 
        // convert the rectangle from pixels to 0.0 to 1.0 
        vec2 zeroToOne = position / u_resolution; ...
    

    设置几何图形一次实现效果:

    function setGeometry(gl) { 
        gl.bufferData( gl.ARRAY_BUFFER, 
        new Float32Array([ 
        // left column 
        0, 0, 30, 0, 0, 150, 0, 150, 30, 0, 30, 150, 
        
        // top rung 
        30, 0, 100, 0, 30, 30, 30, 30, 100, 0, 100, 30, 
        
        // middle rung 
        30, 60, 67, 60, 30, 90, 30, 90, 67, 60, 67, 90]), 
        gl.STATIC_DRAW); 
     }
    

    更新下u_translation 变量的值:

    ... 
    var translationLocation = gl.getUniformLocation( program, "u_translation"); 
    ... 
    // Set Geometry. 
    setGeometry(gl); 
    .. 
    // Draw scene. 
    function drawScene() { 
        // Clear the canvas. 
        gl.clear(gl.COLOR_BUFFER_BIT); 
        // Set the translation. 
        gl.uniform2fv(translationLocation, translation); 
        // Draw the rectangle. 
        gl.drawArrays(gl.TRIANGLES, 0, 18);
    }
    

    WebGL 2D图像旋转

    在单位圆上的最高点处,Y 为 1 和 X 为 0。在最右的位置时 X 为 1 和 Y 为 0。

    从单位圆上得到任何点的 X 和 Y 值,接着将他们乘以上一节示例中的几何图形。如下是更新渲染器:

    attribute vec2 a_position; 
    uniform vec2 u_resolution; 
    uniform vec2 u_translation; 
    uniform vec2 u_rotation; 
    void main() { 
        // Rotate the position 
        vec2 rotatedPosition = vec2( a_position.x * u_rotation.y + a_position.y * u_rotation.x, a_position.y * u_rotation.y - a_position.x * u_rotation.x); 
        // Add in the translation. 
        vec2 position = rotatedPosition + u_translation;
    

    传参:

     var rotationLocation = gl.getUniformLocation(program, "u_rotation"); 
     ... var rotation = [0, 1]; 
     .. 
     // Draw the scene. 
     function drawScene() { 
         // Clear the canvas. 
         gl.clear(gl.COLOR_BUFFER_BIT); 
         // Set the translation. 
         gl.uniform2fv(translationLocation, translation); 
         // Set the rotation. 
         gl.uniform2fv(rotationLocation, rotation); 
         // Draw the rectangle. 
         gl.drawArrays(gl.TRIANGLES, 0, 18); 
     }
    

    首先,让我们看下数学公式:

    rotatedX = a_position.x * u_rotation.y + a_position.y * u_rotation.x; 
    rotatedY = a_position.y * u_rotation.y - a_position.x * u_rotation.x;
    

    假设你有一个矩形,并且你想旋转它。在你把它旋转到右上角 (3.0,9.0) 这个位置之前。我们先在单位圆中选择一个从 12 点钟的位置顺时针偏移 30 度的点。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tR7I8RWG-1631442055906)(en-resource://database/4833:1)]
    在圆上那个位置的点的坐标为 0.50 和 0.87:

    3.0 * 0.87 + 9.0 * 0.50 = 7.1
    9.0 * 0.87 - 3.0 * 0.50 = 6.3
    

    那刚刚好是我们需要的位置:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yz6uPW0n-1631442055907)(en-resource://database/4835:1)]
    旋转 60 度和上面的操作一样:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SB8E70Tm-1631442055908)(en-resource://database/4837:1)]
    圆上面的位置的坐标是 0.87 和 0.50:

    3.0 * 0.50 + 9.0 * 0.87 = 9.3 
    9.0 * 0.50 - 3.0 * 0.87 = 1.9
    

    当我们顺时针向右旋转那个点时,X 的值变得更大而 Y 的值在变小。如果接着旋转超过 90 度,X 的值将再次变小而 Y 的值将变得更大。这个形式就能够达到旋转的目的。

    圆环上的那些点还有另外一个名称。他们被称作为 sine 和 cosine。因此,对任意给定的角度,我们就只需查询它所对应的 sine 和 cosine 值:

    function printSineAndCosineForAnyAngle(angleInDegrees) { 
        var angleInRadians = angleInDegrees * Math.PI / 180; 
        var s = Math.sin(angleInRadians); 
        var c = Math.cos(angleInRadians); console.log("s = " + s + " c = " + c); 
    }
    

    如果你把上面的代码复制粘贴到 JavaScript 控制台中,接着输入printSineAndCosineForAnyAngle(30),接着你会看到输出 s = 0.49 c = 0.87(注意:这个数字是近似值。)

    如果把上面的代码整合在一起的话,你就可以将你的几何体按照你想要的任何角度进行旋转。仅仅只需要将你需要旋转的角度值传给 sine 和 cosine 就可以了。

    var angleInRadians = angleInDegrees * Math.PI / 180; 
    rotation[0] = Math.sin(angleInRadians); 
    rotation[1] = Math.cos(angleInRadians);
    

    WebGL 2D图像伸缩

    伸缩比例控制:

    var scaleLocation = gl.getUniformLocation(program, "u_scale"); 
    var scale = [1, 1]; 
    
    // Draw the scene. 
    function drawScene() {
        // Clear the canvas. 
        gl.clear(gl.COLOR_BUFFER_BIT); 
        // Set the translation. 
        gl.uniform2fv(translationLocation, translation); 
        // Set the rotation. 
        gl.uniform2fv(rotationLocation, rotation); 
        // Set the scale. 
        gl.uniform2fv(scaleLocation, scale); 
        // Draw the rectangle. 
        gl.drawArrays(gl.TRIANGLES, 0, 18); 
    }
    

    WebGL 2D矩阵

    伸缩变换图形。平移,旋转和伸缩都是一种变化类型。每一种变化都需要改变渲染器,而且他们依赖于操作的顺序。在前面的例子中我们进行了伸缩,旋转和平移操作。

    如果他们执行操作的顺序改变将会得到不同的结果。例如 XY 伸缩变换为 2,1,旋转 30%,接着平移变换 100,0。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xw90vdCv-1631442055909)(en-resource://database/4839:1)]

    如下是平移 100,0,旋转 30%,接着伸缩变换 2,1。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i4UDf5hA-1631442055910)(en-resource://database/4841:1)]

    结果是完全不同的。更糟糕的是,如果我们需要得到的是第二个示例的效果,就必须编写一个不同的渲染器,按照我们想要的执行顺序进行平移,旋转和伸缩变换。然而,有些比我聪明的人利用数学中的矩阵能够解决上面这个问题。对于 2d 图形,使用一个 3X3 的矩阵。3X3 的矩阵类似了 9 宫格。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DUKvww2P-1631442055910)(en-resource://database/4843:1)]

    数学中的操作是与列相乘然后把结果加在一起。一个位置有两个值,用 x 和 y 表示。但是为了实现这个需要三个值,因为我们对第三个值设为 1。在上面的例子中就变成了:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TkiWfl1z-1631442055911)(en-resource://database/4845:1)]

    对于上面的处理你也许会想“这样处理的原因在哪里”。假设要执行平移变换。我们将想要执行的平移的总量为 tx 和 ty。构造如下的矩阵:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C1Q1xLL4-1631442055912)(en-resource://database/4847:1)]

    接着进行计算:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y7ZRFIqk-1631442055912)(en-resource://database/4849:1)]
    如果你还记得代数学,就可以那些乘积结果为零的位置。乘以 1 的效果相当于什么都没做,那么将计算简化看看发生了什么:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QhSgoSb7-1631442055913)(en-resource://database/4851:1)]
    或者更简洁的方式:

    newX = x + tx;
    newY = y + ty;
    

    extra 变量我们并不用在意。这个处理和我们在平移中编写的代码惊奇的相似。同样地,让我们看看旋转。正如在旋转那篇中指出当我们想要进行旋转的时候,我们只需要角度的 sine 和 cosine 值。

    s = Math.sin(angleToRotateInRadians);
    c = Math.cos(angleToRotateInRadians);
    

    构造如下的矩阵:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4QHoUyyW-1631442055914)(en-resource://database/4853:1)]

    执行上面的矩形操作:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9WgqwoFQ-1631442055915)(en-resource://database/4855:1)]

    将得到 0 和 1 结果部分用黑色块表示了。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qfjy0J6V-1631442055916)(en-resource://database/4857:1)]

    同样可以简化计算:

    newX = x * c + y * s;
    newY = x * -s + y * c;
    

    上面处理的结果刚好和旋转例子效果一样。最后是伸缩变换。称两个伸缩变换因子为 sx 和 sy。构造如下的矩阵:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9BDdffxr-1631442055917)(en-resource://database/4859:1)]

    进行矩阵操作会得到如下:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wedyykTX-1631442055918)(en-resource://database/4861:1)]

    实际需要计算:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pySUkfn6-1631442055918)(en-resource://database/4863:1)]

    简化为:

    newX = x * sx;
    newY = y * sy;
    

    和我们以前讲解的伸缩示例是一样的。到了这里,我坚信你仍然在思考,这样处理之后了?有什么意义。看起来好象它只是做了和我们以前一样的事。接下来就是魔幻的地方。已经被证明了我们可以将多个矩阵乘在一起,接着一次执行完所有的变换。

    假设有函数 matrixMultiply,它带两个矩阵做参数,将他们俩相乘,返回乘积结果。为了让上面的做法更清楚,于是编写如下的函数构建一个用来平移,旋转和伸缩的矩阵:

    function makeTranslation(tx, ty) { 
        return [ 1, 0, 0, 0, 1, 0, tx, ty, 1 ]; 
    } 
    function makeRotation(angleInRadians) {
        var c = Math.cos(angleInRadians); 
        var s = Math.sin(angleInRadians);
        return [ c,-s, 0, s, c, 0, 0, 0, 1 ]; 
    } 
    function makeScale(sx, sy) {
        return [ sx, 0, 0, 0, sy, 0, 0, 0, 1 ];
    }
    

    修改渲染器:

    attribute vec2 a_position; 
    uniform vec2 u_resolution;
    uniform mat3 u_matrix; void main() {
        // Multiply the position by the matrix. 
        vec2 position = (u_matrix * vec3(a_position, 1)).xy;
    

    使用渲染器:

    function drawScene() { 
        // Clear the canvas. 
        gl.clear(gl.COLOR_BUFFER_BIT);
        // Compute the matrices 
        var translationMatrix = makeTranslation(translation[0], translation[1]); 
        var rotationMatrix = makeRotation(angleInRadians); 
        var scaleMatrix = makeScale(scale[0], scale[1]); 
        // Multiply the matrices. 
        var matrix = matrixMultiply(scaleMatrix, rotationMatrix); 
        matrix = matrixMultiply(matrix, translationMatrix);
        // Set the matrix. 
        gl.uniformMatrix3fv(matrixLocation, false, matrix); 
        // Draw the rectangle. 
        gl.drawArrays(gl.TRIANGLES, 0, 18); 
    }
    

    进一步简化渲染器。如下是完整的顶点渲染器。

    attribute vec2 a_position; 
    uniform mat3 u_matrix; void main() {
        // Multiply the position by the matrix. 
        gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1); }
     
    // Draw the scene. 
    function drawScene() { 
        ...
        // Compute the matrices 
        var projectionMatrix = make2DProjection( 
        canvas.clientWidth, canvas.clientHeight);
        ... 
        // Multiply the matrices. 
        var matrix = matrixMultiply(scaleMatrix, rotationMatrix); 
        matrix = matrixMultiply(matrix, translationMatrix);
        matrix = matrixMultiply(matrix, projectionMatrix);
        ... 
    }
    

    WebGL 3D

    WebGL 正交3D

    改变顶点着色来处理三维:

    attribute vec4 a_position; 
    uniform mat4 u_matrix; void main() {
        // Multiply the position by the matrix. 
        gl_Position = u_matrix * a_position;
    }
    

    渲染三维数据:

    function makeTranslation(tx, ty, tz) { 
        return [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, tx, ty, tz, 1 ];
    } 
    
    function makeXRotation(angleInRadians) { 
        var c = Math.cos(angleInRadians);
        var s = Math.sin(angleInRadians); 
        return [ 1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1 ]; 
    }; 
    
    function makeYRotation(angleInRadians) { 
        var c = Math.cos(angleInRadians); 
        var s = Math.sin(angleInRadians); 
        return [ c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, 0, 0, 0, 1 ]; 
    }; 
    
    function makeZRotation(angleInRadians) {
        var c =    Math.cos(angleInRadians); 
        var s =    Math.sin(angleInRadians); 
        return [ c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, ];
    } 
    
    function makeScale(sx, sy, sz) {
        return [ sx, 0, 0, 0, 0, sy, 0, 0, 0, 0, sz, 0, 0, 0, 0, 1, ]; 
    }
    

    有3个旋转函数。在二维中只需要一个旋转函数,因为只需要绕 Z 轴旋转。现在虽然做三维也希望能够绕 X 轴和 Y 轴旋转。可以从中看出,它们都非常相似。如果让它们工作,会看到它们像以前一样简化:
    Z 旋转newX = x * c + y * s;

    WebGL 3D 透视

    什么是透视?它基本上是一种事物越远显得更小的特征。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0cTsVZNf-1631442055919)(en-resource://database/4881:1)]

    看上面的例子,我们看到越远的东西被画得越小。鉴于我们目前样品的一个让较远的物体显得更小简单的方法就是将 clipspace X 和 Y 除以 Z。可以这样想:如果你有一条从(10,15)到 (20,15) 的线段,10个单位长。在我们目前的样本中,它将绘制 10 像素长。但是如果我们除以 Z,例如例子中如果是 Z 是 1

    10 / 1 = 10 
    20 / 1 = 20 
    abs(10-20) = 10
    

    这将是 10 像素,如果 Z 是 2,则有

    10 / 2 = 5 
    20 / 2 = 10
    abs(5 - 10) = 5
    

    5 像素长。如果 Z = 3,则有

    10 / 3 = 3.333
    20 / 3 = 6.666 
    abs(3.333 - 6.666) = 3.333
    

    你可以看到,随着 Z 的增加,随着它变得越来越远,我们最终会把它画得更小。如果我们在 clipspace 中除,我们可能得到更好的结果,因为 Z 将是一个较小的数字(-1 到 +1)。如果在除之前我们加一个 fudgeFactor 乘以 Z,对于一个给定的距离我们可以调整事物多小。让我们尝试一下。首先让我们在乘以我们的 “fudgefactor” 后改变顶点着色器除以 Z 。

    ... 
    uniform float u_fudgeFactor;
    ...
    void main() { 
        // Multiply the position by the matrix.
        vec4 position = u_matrix * a_position; 
        // Adjust the z to divide by 
        float zToDivideBy = 1.0 + position.z * u_fudgeFactor;
        // Divide x and y by z. 
        gl_Position = vec4(position.xy / zToDivideBy, position.zw);
    }
    

    因为在 clipspace 中 Z 从 -1 到 +1,我加 1 得到 zToDivideBy 从 0 到 +2 * fudgeFactor我们也需要更新代码,让我们设置 fudgeFactor。

     ... 
     var fudgeLocation = gl.getUniformLocation(program, "u_fudgeFactor")
    ... 
    var fudgeFactor = 1; 
    ... 
    function drawScene() { 
        ...
        // Set the fudgeFactor 
        gl.uniform1f(fudgeLocation, fudgeFactor); 
        // Draw the geometry. 
        gl.drawArrays(gl.TRIANGLES, 0, 16 * 6);
    }
    

    下面是结果。如果没有明确的把 “fudgefactor” 从 1 变化到 0 来看事物看起来像什么样子在我们除以 Z 之前。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F6y6H2S8-1631442055920)(en-resource://database/4883:1)]
    WebGL 在我们的顶点着色器中把 X,Y,Z,W 值分配给 gl_Position 并且自动除以 W。我们可以证明通过改变着色这很容易实现,而不是自己做除法,在 gl_Position.w 中加 zToDivideBy 。

    ... 
    uniform float u_fudgeFactor; 
    ... void main() {
        // Multiply the position by the matrix. 
        vec4 position = u_matrix * a_position;
        // Adjust the z to divide by 
        float zToDivideBy = 1.0 + position.z * u_fudgeFactor; 
        // Divide x, y and z by zToDivideBy 
        gl_Position = vec4(position.xyz, zToDivideBy); 
    }
    

    看看这是完全相同的。为什么有这样一个事实: WebGL 自动除以 W ?因为现在,使用更多维的矩阵,我们可以使用另一个矩阵复制 z 到 w。矩阵如下

    1, 0, 0, 0, 
    0, 1, 0, 0, 
    0, 0, 1, 1, 
    0, 0, 0, 0,
    

    将复制 z 到 w.你可以看看这些列如下

    x_out = x_in * 1 + 
               y_in * 0 + 
               z_in * 0 + 
               w_in * 0 ; 
    y_out = x_in * 0 +
               y_in * 1 +
               z_in * 0 + 
               w_in * 0 ;
    z_out = x_in * 0 +
               y_in * 0 +
               z_in * 1 + 
               w_in * 0 ; 
    w_out = x_in * 0 +
                y_in * 0 +
                z_in * 1 +
                w_in * 0 ;
    

    简化后如下

    x_out = x_in;
    y_out = y_in; 
    z_out = z_in; 
    w_out = z_in;
    

    我们可以加 1 我们之前用的这个矩阵,因为我们知道 w_in 总是 1.0。

    1, 0, 0, 0,
    0, 1, 0, 0, 
    0, 0, 1, 1, 
    0, 0, 0, 1,
    

    这将改变 W 计算如下

    w_out = x_in * 0 +
                y_in * 0 +
                z_in * 1 +
                w_in * 1 ;
    

    因为我们知道 w_in = 1.0 所以就有

    w_out = z_in + 1;
    

    最后我们可以将 fudgeFactor 加到矩阵,矩阵如下

    1, 0, 0, 0,
    0, 1, 0, 0, 
    0, 0, 1, fudgeFactor,
    0, 0, 0, 1,
    

    这意味着

    w_out = x_in * 0 +
                y_in * 0 +
                z_in * fudgeFactor
                + w_in * 1 ;
    

    简化后如下

    w_out = z_in * fudgeFactor + 1;
    

    让我们再次修改程序只使用矩阵。    
    首先让我们放回顶点着色器。这很简单

    uniform mat4 u_matrix;
    
    void main() {
      // Multiply the position by the matrix.
      gl_Position = u_matrix * a_position;
      ...
    }
    

    接下来让我们做一个函数使 Z - > W 矩阵。

    function makeZToWMatrix(fudgeFactor) {
        return [ 
            1, 0, 0, 0, 
            0, 1, 0, 0, 
            0, 0, 1, fudgeFactor,
            0, 0, 0, 1,
        ];
    }
    

    我们将更改代码,以使用它

    ... 
    // Compute the matrices 
    var zToWMatrix = makeZToWMatrix(fudgeFactor); 
    ... 
    // Multiply the matrices. 
    var matrix = matrixMultiply(scaleMatrix, rotationZMatrix); 
    matrix = matrixMultiply(matrix, rotationYMatrix); 
    matrix = matrixMultiply(matrix, rotationXMatrix);
    matrix = matrixMultiply(matrix, translationMatrix); 
    matrix = matrixMultiply(matrix, projectionMatrix); 
    matrix = matrixMultiply(matrix, zToWMatrix);
    ...
    

    注意,这一次也是完全相同的。以上基本上是向你们展示,除以 Z 给了我们透视图,WebGL 方便地为我们除以 Z。    
    但是仍然有一些问题。例如如果你设置 Z 到 -100 左右,你会看到类似下面的动画。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BgFAtBPv-1631442055921)(en-resource://database/4885:1)]
    发生了什么事?为什么 F 消失得很早?
    就像 WebGL 剪辑 X 和 Y 或+ 1 到 - 1 它也剪辑 Z。这里看到的就是 Z<-1 的地方。我可以详细了解如何解决它,但你可以以我们做二维投影相同的方式来得到它。

    我们需要利用 Z,添加一些数量和测量一定量,我们可以做任何我们想要得到的 -1 到 1 的映射范围。真正酷的事情是所有这些步骤可以在 1 个矩阵内完成。

    甚至更好的,我们来决定一个 fieldOfView 而不是一个 fudgeFactor,并且计算正确的值来做这件事。这里有一个函数来生成矩阵。

    function makePerspective(fieldOfViewInRadians, aspect, near, far) { 
        var f = Math.tan(Math.PI * 0.5 - 0.5 * fieldOfViewInRadians); 
        var rangeInv = 1.0 / (near - far);
        return [ 
            f / aspect, 0, 0, 0,
            0, f, 0, 0, 
            0, 0, (near + far) * rangeInv, -1, 
            0, 0, near * far * rangeInv * 2, 0
        ];
    };
    

    这个矩阵将为我们做所有的转换。它将调整单位,所以他们在clipspace 中它会做数学运算,因此我们可以通过角度选择视野,它会让我们选择我们的 z-clipping 空间。

    它假定有一个 eye 或 camera 在原点(0,0,0)并且给定一个 zNear 和 fieldOfView 计算它需要什么,因此在 zNear 的物体在 z = - 1 结束以及在 zNear 的物体它们在中心以上或以下半个 fieldOfView,分别在 y = - 1 和 y = 1 结束。

    计算 X 所使用的只是乘以传入的 aspect。我们通常将此设置为显示区域的 width / height。最后,它计算出在 Z 区域物体的规模,因此在 zFar 的物体在 z = 1 处结束。下面是动作矩阵的图。形状像四面锥的立方体旋转称为“截锥”。

    矩阵在截锥内占空间并且转换到 clipspace。zNear 定义夹在前面的物体,zfar定义夹在后面的物体。设置 zNear 为23你会看到旋转的立方体的前面得到裁剪。设置 zFar 为24你会看到立方体的后面得到剪辑。只剩下一个问题。这个矩阵假定有一个视角在 0,0,0 并假定它在Z轴负方向,Y的正方向。

    我们的矩阵到目前为止已经以不同的方式解决问题。为了使它工作,我们需要我们的对象在前面的视图。我们可以通过移动我们的 F 做到。 我们在(45,150,0)绘图。让我们将它移到(0,150,- 360)现在,要想使用它,我们只需要用对 makePerspective 的调用取代对make2DProjection 旧的调用

     var aspect = canvas.clientWidth / canvas.clientHeight; 
     var projectionMatrix = makePerspective(fieldOfViewRadians, aspect, 1, 2000);
     var translationMatrix = makeTranslation(translation[0], translation[1], translation[2]);
     var rotationXMatrix = makeXRotation(rotation[0]);
     var rotationYMatrix = makeYRotation(rotation[1]); 
     var rotationZMatrix = makeZRotation(rotation[2]);
     var scaleMatrix = makeScale(scale[0], scale[1], scale[2]);
    

    我们回到了一个矩阵乘法,我们得到两个领域的视图,我们可以选择我们的 z 空间。

    WebGL 3D 摄像机

    我们需要有效地将现实中的物体移动到摄像机的前面。能达到这个目的的最简单的方法是使用“逆”矩阵。一般情况下的逆矩阵的计算是复杂的,但从概念上讲,它是容易的。逆是你用来作为其他数值的对立的值。例如,123 的是相反数是 -123。缩放比例为5的规模矩阵的逆是 1/5 或 0.2。在 X 域旋转 30° 的矩阵的逆是一个在 X 域旋转 -30° 的矩阵。直到现在我们已经使用了平移,旋转和缩放来影响我们的 ‘F’ 的位置和方向。

    把所有的矩阵相乘后,我们有一个单一的矩阵,表示如何将 “F” 以我们希望的大小和方向从原点移动到相应位置。使用摄像机我们可以做相同的事情。一旦我们的矩阵告诉我们如何从原点到我们想要的位置移动和旋转摄像机,我们就可以计算它的逆,它将给我们一个矩阵来告诉我们如何移动和旋转其它一切物体的相对数量,这将有效地使摄像机在点(0,0,0),并且我们已经将一切物体移动到它的前面。让我们做一个有一圈 ‘F’ 的三维场景,就像上面的图表那样。下面是实现代码。

    
    var numFs = 5; 
    var radius = 200; 
    var aspect = canvas.clientWidth / canvas.clientHeight; 
    var projectionMatrix = makePerspective(fieldOfViewRadians, aspect, 1, 2000); // Draw 'F's in a circle 
    for (var ii = 0; ii < numFs; ++ii) {
        var angle = ii * Math.PI * 2 / numFs; 
        var x = Math.cos(angle) * radius;
        var z = Math.sin(angle) * radius; 
        var translationMatrix = makeTranslation(x, 0, z); 
        var matrix = translationMatrix; 
        matrix = matrixMultiply(matrix, projectionMatrix);
        // Set the matrix. 
        gl.uniformMatrix4fv(matrixLocation, false, matrix); 
        // Draw the geometry. 
        gl.drawArrays(gl.TRIANGLES, 0, 16 * 6);
    }
    

    就在我们计算出我们的投影矩阵之后,我们就可以计算出一个就像上面的图表中显示的那样围绕 ‘F’ 旋转的摄像机。

    var cameraMatrix = makeTranslation(0, 0, radius * 1.5); 
    cameraMatrix = matrixMultiply( cameraMatrix, makeYRotation(cameraAngleRadians));
    

    然后,我们根据相机矩阵计算“视图矩阵”。“视图矩阵”是将一切物体移动到摄像机相反的位置,这有效地使摄像机相对于一切物体就像在原点(0,0,0)。

     var viewMatrix = makeInverse(cameraMatrix);
    

    最后我们需要应用视图矩阵来计算每个 ‘F’ 的矩阵

     var matrix = translationMatrix;
     matrix = matrixMultiply(matrix, viewMatrix);
     // <=-- added 
     matrix = matrixMultiply(matrix, projectionMatrix);
    

    一个摄像机可以绕着一圈 “F”。拖动 cameraAngle 滑块来移动摄像机。这一切都很好,但使用旋转和平移来移动一个摄像头到你想要的地方,并且指向你想看到的地方并不总是很容易。

    例如如果我们想要摄像机总是指向特定的 ‘F’ 就要进行一些非常复杂的数学计算来决定当摄像机绕 ‘F’ 圈旋转的时候如何旋转摄像机来指向那个 ‘F’。幸运的是,有一个更容易的方式。我们可以决定摄像机在我们想要的地方并且可以决定它指向什么,然后计算矩阵,这个矩阵可以将把摄像机放到那里。

    基于矩阵的工作原理这非常容易实现。首先,我们需要知道我们想要摄像机在什么位置。我们将称之为 CameraPosition。然后我们需要了解我们看过去或瞄准的物体的位置。我们将把它称为 target。

    如果我们将 CameraPosition 减去 target 我们将得到一个向量,它指向从摄像头获取目标的方向。让我们称它为 zAxis。因为我们知道摄像机指向 -Z 方向,我们可以从另一方向做减法 cameraPosition - target。我们将结果规范化,并直接复制到 z 区域矩阵。

    这部分矩阵表示的是 Z 轴。在这种情况下,是摄像机的 Z 轴。一个向量的标准化意味着它代表了 1.0。如果你回到二维旋转的文章,在哪里我们谈到了如何与单位圆以及二维旋转,在三维中我们需要单位球面和一个归一化的向量来代表在单位球面上一点。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5gRsyymh-1631442055922)(en-resource://database/4887:1)]

    虽然没有足够的信息。只是一个单一的向量给我们一个点的单位范围内,但从这一点到东方的东西?我们需要把矩阵的其他部分填好。特别的 X 轴和 Y 轴类零件。我们知道这 3 个部分是相互垂直的。我们也知道,“一般”我们不把相机指向。

    因为,如果我们知道哪个方向是向上的,在这种情况下(0,1,0),我们可以使用一种叫做“跨产品和“计算 X 轴和 Y 轴的矩阵。我不知道一个跨产品意味着在数学方面。我所知道的是,如果你有 2 个单位向量和你计算的交叉产品,你会得到一个向量,是垂直于这 2 个向量。

    换句话说,如果你有一个向量指向东南方,和一个向量指向上,和你计算交叉产品,你会得到一个向量指向北西或北东自这2个向量,purpendicular 到东南亚和。根据你计算交叉产品的顺序,你会得到相反的答案。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XH18mwDm-1631442055923)(en-resource://database/4889:1)]

    现在,我们有 xAxis,我们可以通过 zAxis 和 xAxis 得到摄像机的 yAxis[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8BkM1BYp-1631442055924)(en-resource://database/4891:1)]

    现在我们所要做的就是将 3 个轴插入一个矩阵。这使得矩阵可以指向物体,从 cameraPosition 指向 target。我们只需要添加 position
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZcmRLpjD-1631442055925)(en-resource://database/4893:1)]

    下面是用来计算 2 个向量的交叉乘积的代码。

    function cross(a, b) { 
        return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; 
    }
    

    这是减去两个向量的代码。

    function subtractVectors(a, b) {
        return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
    }
    

    这里是规范化一个向量(使其成为一个单位向量)的代码。

    function normalize(v) {
        var length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); 
        // make sure we don't divide by 0. 
        if (length > 0.00001) { 
            return [v[0] / length, v[1] / length, v[2] / length]; 
        } else { 
            return [0, 0, 0]; 
        } 
    }
    

    下面是计算一个 “lookAt” 矩阵的代码。

    function makeLookAt(cameraPosition, target, up) {
        var zAxis = normalize( subtractVectors(cameraPosition, target)); 
        var xAxis = cross(up, zAxis); 
        var yAxis = cross(zAxis, xAxis); 
        return [ xAxis[0], xAxis[1], xAxis[2], 0, yAxis[0], yAxis[1], yAxis[2], 0, zAxis[0], zAxis[1], zAxis[2], 0, cameraPosition[0], cameraPosition[1], cameraPosition[2], 1];
    }
    

    这是我们如何使用它来使相机随着我们移动它指向在一个特定的 ‘F’ 的。

     ...
     // Compute the position of the first F 
     var fPosition = [radius, 0, 0]; 
     // Use matrix math to compute a position on the circle. 
     var cameraMatrix = makeTranslation(0, 50, radius * 1.5); 
     cameraMatrix = matrixMultiply( cameraMatrix, makeYRotation(cameraAngleRadians)); 
     // Get the camera's postion from the matrix we computed 
     cameraPosition = [ cameraMatrix[12], cameraMatrix[13], cameraMatrix[14]]; var up = [0, 1, 0];
     // Compute the camera's matrix using look at. 
     var cameraMatrix = makeLookAt(cameraPosition, fPosition, up); 
     // Make a view matrix from the camera matrix. 
     var viewMatrix = makeInverse(cameraMatrix);
     ...
    

    拖动滑块,注意到相机追踪一个 ‘F’。请注意,您可以不只对摄像机使用 “lookAt” 函数。共同的用途是使一个人物的头跟着某人。使小塔瞄准一个目标。使对象遵循一个路径。你计算目标的路径。然后你计算出目标在未来几分钟在路径的什么地方。把这两个值放进你的 lookAt 函数,你会得到一个矩阵,使你的对象跟着路径并且朝向路径。

    展开全文
  • 技术社区里有种很有意思的现象,那就是不少人们口耳相传中的强大技术,往往因为上手难度高而显得...这次我们就将以 WebGL 为例,尝试降低它的入门门槛,讲解它在前端图像处理领域的应用入门。 WebGL 基础库 Beam。...
  • webgl的使用过程中,我们通常会想对texture进行多级处理并对其贴在表面显示 如对较精准的边缘检测,要先后使用灰度shader、模糊shader、边缘shader来进行处理,而每次的处理对象则是上一次处理后的texture,这...
  • webgl不同图像不同纹理Keyboard Sensitive 3D Twisted Images with WebGL. Today we continue ...
  • webgl 工具函数库,可用于处理图片 引入 import { Program } from "./jsUtil/program.js"; 创建 Program 对象 let pro = new Program("webgl"); Program(glid, gl); /** * @name:构造函数 * @param {*} glid:canvas...
  • WebGL加载图片

    千次阅读 2018-11-15 18:42:50
    WebGL目前已经被大多数浏览器所支持,但是不同浏览器对图片文件的加载都所以差异。加载文件分为使用web服务器端数据和本地数据两种方式,以下就此问题进行讨论。 关键字(key words) WebGL, Image, Texture, ...
  • 本文来源:https://zhuanlan.zhihu.com/p/52611681 ------------------------------------------------------------- 此文上接WebGL 图像处理,如果还没有读过建议你从那里开始。 图像处理的下一个问题是如何同时...
  • WebGL 中绘制图片需要使用纹理。和 WebGL 渲染时需要裁剪空间坐标相似,渲染纹理时需要纹理坐标,而不是像素坐标。无论纹理是什么尺寸,纹理坐标范围始终是 0.0 到 1.0 。 因为我们只用画一个矩形(其实是两个...
  • 融合了WebGl和Web Componet的实时图像处理弹幕播放器 特征 基于Web Components ,拥抱Web Components标准,内部使用的 omi 作为开发Web Components的开发框架,omi是个非常棒的现代框架,强力推荐! 高性能的,使用...
  • WebGL

    2017-08-21 22:03:16
    WebGL:(全写Web Graphics Library)是一种3D绘图协议,这种绘图技术标准允许把JavaScript和OpenGL ES 2.0结合在一起,通过增加OpenGL ES 2.0的一个JavaScript绑定,WebGL可以为HTML5 Canvas提供硬件3D加速渲染,...
  • webGL图片纹理

    2020-09-04 18:49:31
    webGL几何坐标系:x(-1, 1) y(-1, 1) gl.createTexture() —— 创建并初始化一个WebGLTexture对象,即创建一个纹理; gl.bindTexture(target, texture) 将给定的texture绑定到目标,其中texture参数为创建的纹理...
  • FivekoGFX( )是一个很小的图像处理库。 FivekoGFX的主要代码库是用JavaScript和OpenGL着色器(GLSL)编写的。 FivekoGFX是一个基于Web的图像处理库,它尝试提供最佳解决方案并利用GPU的功能(如果可能)。 您也...
  • 第三话,传递uniform变量以及引入纹理, 做一些简单的2D处理上一期讲到了从文件和从html标签加载shader,这一期直接上纹理了。所有教程内容包括教程本身都可以在https://github.com/wysaid/WebGL-Lessons直接获得,...
  • 醒目 虽然是老物, 代码也没什么值得学习的地方, 但是...图片资源来自 fotor.com 的应用程序资源包, 版权归其所有, 未经允许, 不得擅自用于商业用途。 请尊重版权! 好哒, 就一本正经地说这么多吧 直接访问主页:
  • 需要电子档书籍可以Q群:828202939 希望可以和大家一起学习、一起进步!! 所有的课程源代码在我上传的资源里面,本来想设置开源,好像...这一节课我们将学习 正确处理图像的位置关系 如果没有在上一节的基础...
  • WebGL基础

    2019-08-30 10:36:13
    要绘制使用WebGL图像,必须通过表示图像的向量。然后,它在给定载体导入像素格式,使用 OpenGL SL 转换并在屏幕上显示图像WebGL坐标系 就像任何其他的3D系统,在WebGL中Z轴表示深度,x,y和z轴。在WebGL的坐标被...
  • 使用JavaScript和WebGL如何实现常见的照片操作操作的示例。 重点是编写GLSL代码。 该演示应用程序用作我在WebGL上的演讲的一部分。 您可以在查看演讲的录音 跑步 只需使用您选择的网络服务器托管该目录即可。 您还...
  • webGL初识

    2018-03-06 11:04:55
    通过WebGL的技术,只需要编写网页代码即可实现3D图像的展示。WebGL的规格尚在发展中,由非营利的Khronos Group管理[2]。 WebGL基于OpenGL ES 2.0,提供了3D图像的程序接口。它使用HTML5 Canvas并允许利用文档对象...
  • WebGL教程

    千次阅读 2018-09-21 11:46:19
    通过WebGL做了很多项目,感觉有必要录制一套视频教程,所限在这里写一个录制大纲,大家也可以通过章节目录了解下WebGL的基本内容。 Threejs引擎 Threejs是webgl国内应用最广泛的一款三维引擎,实际做项目的时候...
  • 基于HTML5 Canvas和WebGL实现图片的交互式几何变换
  • 腾讯位置服务Javascript API GL版,是基于WebGL技术打造的地图API库,使得浏览器环境下也可实现APP端的应用体验,提供2D/3D模式,运行流畅。...以下内容转载自掘金文章《WebGL-3D地图大俯仰角的雾化处理》 作者:多...
  • WebGL(一):WebGL基础

    2020-02-07 18:43:04
    WebGL基础 WebGL基本原理 WebGL是如何工作的 1.GPU作用 GPU的基础任务:将点作为投影矩阵、将相应的像素点描绘出来 例:用户调用gl.drawArray(gl.TRIANGLE, 0, 9); 9表示“处理9个顶点”,处理每个顶点时,顶点着色...
  • 初识WEBGL

    2018-10-16 11:23:09
    WebGL 使得在支持HTML 的 canvas 标签的...WebGL程序包括用 JavaScript 写的控制代码,以及在图形处理单元(GPU, Graphics Processing Unit)中执行的着色代码(GLSL,注:GLSL为OpenGL着色语言)。WebGL 元...
  • webGL总结

    千次阅读 2014-05-23 17:12:33
    WebGL学习汇报 一、WebGL简介 WebGL 和3D图形规范OpenGL、OpenCL一样来自Khronos Group,而且免费开放。它是一种3D绘图标准,是针对万维网的即时三维绘图API,这种绘图技术标准允许把JavaScript和OPenGL ES...
  • FivekoGFX——webgl实现简单图片处理及源码分析 磕代码的阿叶2020-12-19 21:56:27 源码: 源码作者:fivekogfx 源码地址:github.com/fiveko/five… 官网:fiveko.com/,上面有关于这款源码的一些说明 若...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 7,309
精华内容 2,923
关键字:

webgl处理图片