WebGL渲染2D图形

拥有回忆 提交于 2020-01-27 13:59:48

WebGL是通过OpenGL ES在HTML的<canvas></canvas>上渲染图形的。

...
<canvas id="main-canvas"></canvas>
...
let gl = document.getElementById("main-canvas").getContext("webgl");

if (!gl) {
    alert("无法使用WebGL!");
} else {
    console.log(gl);
}

这样,就获取到了在canvas上渲染webgl的上下文。

接下来,在canvas上一切的渲染操作,最终都是要依靠这个gl变量来控制的。

不过在此之前,需要作一下前期准备。

着色器准备

首先要准备一下着色器的源码,着色器(shader)是由GLSL(OpenGL Shadering Language,一种类C++语言)编写的计算机程序。着色器是成对出现的,分别是顶点着色器:

<script type="notjs" id="vertex-shader-2d">
    attribute vec4 a_position;

    void main() {
        gl_Position = a_position;
    }
</script>

和片段着色器:

<script type="notjs" id="fragment-shader-2d">
    precision mediump float;

    void main() {
        gl_FragColor = vec4(1, 0.3, 0.5, 1);
    }
</script>

顶点着色器接受一些输入,输出的是裁剪平面的顶点坐标。

因为最终在canvas上渲染图形还是要使用JavaScript而不是GLSL,所以需要获取这些GLSL代码,然后编译处理成JavaScript的对象。所以:

// 获取着色器源码
const vertex_shader_source = document.querySelector("#vertex-shader-2d").text,
    fragment_shader_source = document.querySelector("#fragment-shader-2d").text;

然后根据这两个源码,生成一个着色器程序的js对象:

// 根据源码创建着色程序
function createProgram(gl, vertexShaderSource, fragmentShaderSource) {
    let vertex_shader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource),
        fragment_shader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);

    let program = gl.createProgram();
    gl.attachShader(program, vertex_shader);
    gl.attachShader(program, fragment_shader);
    gl.linkProgram(program);
    let success = gl.getProgramParameter(program, gl.LINK_STATUS);
    if (success) {
        return program;
    }

    console.log(gl.getProgramInfoLog(program));
    gl.deleteProgram(program);
}

 然后调用该函数,program变量就是包含了这一对着色器源码的js对象:

let program = createProgram(gl, vertex_shader_source, fragment_shader_source);

因为在着色器中有一个输入值,即顶点着色器中的a_position,着色器接收输入值必须从缓冲区中获取,而获取到的值需要在着色器中的正确位置放入,所以要找到顶点着色器中a_position的位置:

let position_attribute_location = gl.getAttribLocation(program, "a_position");

缓冲区准备

着色器设置好了之后,就要开始准备数据,作为着色器的输入了。

上文说过,作为着色器的输入数据,必须是在缓冲区中的数据。而现在没有缓冲区,也没有数据,所以接下来创建这两个东西,然后把数据放入缓冲区中。

先创建缓冲区:

let position_buffer = gl.createBuffer();

然后使用ARRAY_BUFFER绑定点,绑定到这个position_buffer(使用ARRAY_BUFFER说明要用顶点数组方式绘图):

gl.bindBuffer(gl.ARRAY_BUFFER, position_buffer);

 缓冲区准备好了,接下来准备数据:

let positions = [
    0, 0,
    0, 0.2,
    0.7, 0,
    -0.5, -0.5,
    -0.4, -0.2,
    -0.7, 0,
];

然后通过ARRAY_BUFFER绑定点,将数据放到缓冲区中:

gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

准备画布

由于WebGL的裁剪空间是一个横纵坐标都是在[-1, 1]范围内的矩形,而我们的画布的尺寸一般不是这个,所以需要作一下尺寸上的映射:

gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

 然后清空一下画布:

gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);

渲染

要开始渲染了。首先我们要指定着色器程序:

gl.useProgram(program);

然后启用a_position这个输入值(属性):

gl.enableVertexAttribArray(position_attribute_location);

 之后准备将缓冲区的数据输送到着色器:

var size = 2;
var type = gl.FLOAT;
var normalize = false;
var stride = 0;
var offset = 0;
gl.vertexAttribPointer(position_attribute_location, size, type, normalize, stride, offset);

对这里参数的说明:

  • size=2表示每次迭代读取两个数据,即x和y。由于顶点着色器中gl_Position的类型是vec4,包含x,y,z,w四个数据,而这里只需要前两个x和y。
  • type=gl_FLOAT表示使用的是32为浮点类型的数据。
  • normalize=false表示不需要归一化数据。
  • offset=0表示从缓冲区的开始位置读取数据。

最后绘制:

var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = 6;
gl.drawArrays(primitiveType, offset, count);

结果:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>GLSL-test</title>
</head>

<body>
    <canvas id="main-canvas"></canvas>

    <!-- 顶点着色器 -->
    <script type="notjs" id="vertex-shader-2d">
        attribute vec4 a_position;

        void main() {
            gl_Position = a_position;
        }
    </script>

    <!-- 片段着色器 -->
    <script type="notjs" id="fragment-shader-2d">
        precision mediump float;

        void main() {
            gl_FragColor = vec4(1, 0.3, 0.5, 1);
        }
    </script>

    <script>
        // 创建着色器
        function createShader(gl, type, source) {
            let shader = gl.createShader(type);
            gl.shaderSource(shader, source);
            gl.compileShader(shader);

            let success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
            if (success) {
                return shader;
            }

            console.log(gl.getShaderInfoLog(shader));
            gl.deleteShader(shader);
        }

        // 根据源码创建着色程序
        function createProgram(gl, vertexShaderSource, fragmentShaderSource) {
            let vertex_shader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource),
                fragment_shader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);

            let program = gl.createProgram();
            gl.attachShader(program, vertex_shader);
            gl.attachShader(program, fragment_shader);
            gl.linkProgram(program);
            let success = gl.getProgramParameter(program, gl.LINK_STATUS);
            if (success) {
                return program;
            }

            console.log(gl.getProgramInfoLog(program));
            gl.deleteProgram(program);
        }

        function main() {
            // 使用Canvas
            let main_canvas = document.querySelector("#main-canvas");
            main_canvas.setAttribute("width", "640");
            main_canvas.setAttribute("height", "640");
            let gl = main_canvas.getContext("webgl");
            if (!gl) {
                alert("无法使用WebGL!");
            } else {
                console.log(gl);
            }

            // 获取着色器源码
            const vertex_shader_source = document.querySelector("#vertex-shader-2d").text,
                fragment_shader_source = document.querySelector("#fragment-shader-2d").text;

            // 在GPU中创建着色程序
            let program = createProgram(gl, vertex_shader_source, fragment_shader_source);

            // 着色程序输入值的位置
            let position_attribute_location = gl.getAttribLocation(program, "a_position");

            // 创建缓冲区,并绑定到ARRAY_BUFFER绑定点
            gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());

            // 向绑定点指向的缓冲区中存放数据
            let positions = [
                0, 0,
                -0.3, 0.2,
                0.2, 0.5,
                -0.5, -0.5,
                -0.4, -0.2,
                -0.7, 0,
            ];
            gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

            // 准备画布
            gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); // 将裁减空间与canvas尺寸对应
            gl.clearColor(0, 0, 0, 0); // 清空画布
            gl.clear(gl.COLOR_BUFFER_BIT);

            // 使用准备好的着色器程序
            gl.useProgram(program);

            // 启用位置属性
            gl.enableVertexAttribArray(position_attribute_location);

            // 指定从缓冲中读取数据的方式
            gl.vertexAttribPointer(position_attribute_location, 2, gl.FLOAT, false, 0, 0);

            // 执行绘制
            gl.drawArrays(gl.TRIANGLES, 0, 6);    // 根据ARRAY_BUFFER缓冲区的数据绘制内容
        }

        main();
    </script>
</body>

</html>

参考

https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL_API/Tutorial

https://webglfundamentals.org/webgl/lessons/zh_cn/

https://thebookofshaders.com/?lan=ch

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!