WebGL-学习笔记(三)

image.png

在利用缓冲区并在学会利用 mode绘制图形动画以后,继续研究二维图形的颜色渲染以及纹理操作

1. 颜色渲染

1.1 利用步进和偏移拆分缓冲区数据

利用缓冲区可以一次存储点的多个信息,继续利用这一特性,并在利用vertexAttribPointer()函数的时候,利用上strideoffset两个参数:

    function main() {
        const canvas = document.querySelector('#glCanvas');
        const gl = canvas.getContext('webgl');
        // 着色器程序
        const VSHADER_SOURCE = `
            attribute vec4 a_Position;
            attribute float a_PointSize;
            void main() {
                gl_Position = a_Position;
                gl_PointSize = a_PointSize;
            }
        `;
        const FSHADER_SOURCE = `
            precision mediump float;
            uniform vec4 u_FragColor;
            void main() {
                gl_FragColor = u_FragColor;
            }
        `
        let program = init(gl, VSHADER_SOURCE, FSHADER_SOURCE);
        let a_Position = gl.getAttribLocation(program, 'a_Position');
        if (a_Position < 0) {
            console.log('Cant find the position');
            return;
        }
        let a_PointSize = gl.getAttribLocation(program, 'a_PointSize');
        if (a_PointSize < 0) {
            console.log('Cant find the pointsize');
            return;
        }
        gl.vertexAttrib1f(a_PointSize, 10.0);
        initPoint(gl, a_Position, a_PointSize);
        let u_FragColor = gl.getUniformLocation(program, 'u_FragColor');
        gl.uniform4f(u_FragColor, 1.0, 0.0, 0.0, 1.0);

        gl.clearColor(0.0, 0.0, 0.0, 1.0);
        gl.clear(gl.COLOR_BUFFER_BIT);
        gl.drawArrays(gl.POINTS, 0, 3);
    }
    function initPoint(gl, a_Position, a_PointSize) {
        // 将点的坐标和尺寸信息放入一个类型化数组中
        let pointData = new Float32Array([
            -0.5, -0.5, 10.0,
            0, 0.5, 20.0,
            0.5, -0.5, 30.0
        ]);
        // 利用类型化数组提供的内部属性,获取到元素的尺寸
        let FSIZE = pointData.BYTES_PER_ELEMENT;
        let buffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.bufferData(gl.ARRAY_BUFFER, pointData, gl.STATIC_DRAW);
        // 设置第五个参数stride为3*FSIZE(每两个点之间有三个元素,也就是每三个数据进行一次拆分)
        gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 3*FSIZE, 0);
        gl.enableVertexAttribArray(a_Position);
        // 设置第六个参数offset偏移,将每组数据的第三个元素设置给a_PointSize属性
        gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, 3*FSIZE, 2*FSIZE);
        gl.enableVertexAttribArray(a_PointSize);
    }

主要改变在于:

1.利用了类型化数组的BYTE_PER_ELEMENT属性,获取了每个元素的大小

  1. 利用vertexAttribPointer()的第五个参数,指定两个相邻顶点间的字节数,或者理解为每个点的信息由多少个元素组成。
  2. 利用vertexAttribPointer()的第六个参数,指定缓冲区对象中每次读取时的偏移。

当然,要完成以上的操作,也可以将点的位置信息和尺寸信息进行分开放置:

function initPoint(gl, a_Position, a_PointSize) {
        // 将类型化数组增加点的尺寸的参数
        let pointData = new Float32Array([
            -0.5, -0.5,
            0, 0.5,
            0.5, -0.5
        ]);
        let pointSizeData = new Float32Array([10.0, 20.0, 30.0]);
        // 设置点的位置信息
        let buffer1 = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer1);
        gl.bufferData(gl.ARRAY_BUFFER, pointData, gl.STATIC_DRAW);
        gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
        gl.enableVertexAttribArray(a_Position);
        // 设置点的尺寸信息
        let buffer2 = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer2);
        gl.bufferData(gl.ARRAY_BUFFER, pointSizeData, gl.STATIC_DRAW);
        gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, 0, 0);
        gl.enableVertexAttribArray(a_PointSize);
}

但是很明显操作多余而且复杂了

1.2 利用varying变量进行插值操作

想要从Vertex ShaderFragment Shader进行数据传递,需要用到varying声明的变量,为什么利用从Vertex ShaderFragment Shader传递数据?
由于Fragment Shader中是只能使用uniform变量的,unifrom信息是和顶点信息不相关联的,如果要单独对某个顶点的颜色进行设置,是没办法的,所以为了给每个顶点设置颜色,就要从Vertex Shader传递数据到Fragment Shader了。
如果绘制一个三角形,并将顶点颜色传递给Fragment Shader

// 修改着色器程序,增加varying
const VSHADER_SOURCE = `
    attribute vec4 a_Position;
    attribute vec4 a_Color;
    varying vec4 v_Color;
    void main() {
        gl_Position = a_Position;
        v_Color = a_Color;
    }
`;
const FSHADER_SOURCE = `
     precision mediump float;
     varying vec4 v_Color;
     void main() {
        gl_FragColor = v_Color;
      }
`
// 修改点的数据信息加载
function initPoint(gl, a_Position, a_Color) {
    // 将点的坐标信息和颜色信息放到同一个类型化数组中
    let pointData = new Float32Array([
        -0.5, -0.5, 1.0, 0.0, 0.0,
        0, 0.5, 0.0, 1.0, 0.0,
        0.5, -0.5, 0.0, 0.0, 1.0
    ]);
    // 利用类型化数组提供的方法,获取到元素的尺寸
    let FSIZE = pointData.BYTES_PER_ELEMENT;
    let buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, pointData, gl.STATIC_DRAW);
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 5*FSIZE, 0);
    gl.enableVertexAttribArray(a_Position);
    gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, 5*FSIZE, 2*FSIZE);
    gl.enableVertexAttribArray(a_Color);
}

varying变量的作用其实不仅是进行数值的传递,如果将图形绘制的mode修改为gl.TRANGLES,可以发现绘制的三角形颜色是渐变的,造成这一结果是varying的作用。
继续深入着色器过程,Vertex ShaderFragment Shader要进行两步操作:

  1. 图形装配:将Vertex Shader的顶点信息,根据drawArraymode配置在对应的几何图形顶点处
  2. 光栅化:将装配好的图形分割为一个一个的片元(像素点),因此片元的数目就是像素点

varying在赋值颜色给顶点的过程当中,会自动计算从一个顶点到另一个顶点变化的插值,并将每一个片元染成对应计算后的颜色,所以才看到一个从顶点颜色渐变的三角形,这一过程也可以被称为内插。

2. 纹理添加

除了使用单独的颜色渲染外,也可以直接将图片贴到几何图形的表面,该图片也就是纹理。图片是由像素点组成的,每个像素点就是纹素,是使用了RGB/RGBA的编码块
WebGL的给图形添加纹理很简单,但是要解决三个问题

2.1 浏览器跨域

由于WebGL的图片请求机制,所以如果直接请求本地图片的时候会出现跨域问题,为了使得开发和调试的方便,可以使用以下方式临时关闭浏览器的跨域验证,以Chrome为例:
Windows:

"C:\Program Files\Google\Chrome\Application\chrome.exe" --args --disable-web-security

MAC:

open -a "Google Chrome" --args --disable-web-security --user-data-dir

2.2 纹理坐标和图片坐标

WebGL中的纹理坐标Y轴正方向是朝上,其中坐标的四个角为左下(0.0, 0.0)右下(1.0, 0.0)右上(1.0, 1.0)左上(0.0, 1.0)
图片坐标和WebGL坐标的Y轴方向是相反的,正方向朝下,所以在进行纹理映射的时候,需要将坐标轴进行反转。

2.3 纹理添加

进行纹理映射最主要的有以下五步操作,以绘制一个矩形并贴图为例:

2.3.1. 将顶点坐标和纹理坐标都加入到类型化数组中

// 每一行前两个数据为顶点坐标,后两个数据为纹理坐标
let pointData = new Float32Array([
    -0.5, 0.5, 0.0, 1.0,
    -0.5, -0.5, 0.0, 0.0,
    0.5, 0.5, 1.0, 1.0,
    0.5, -0.5, 1.0, 0.0
]);
2.3.2 创建纹理对象和图片加载

// 创建image对象和纹理对象
let texture = gl.createTexture();
let image = new Image();
// 给image执行onload方法的时候进行纹理渲染
image.onload = function() { initTexture() };
// 加载纹理图片
image.src = 'xxx.png';

PS:图片的加载过程是异步!!!

2.3.3 设置纹理

有6个主要的步骤,但是实际上去掉第2和6步也是可以运行的,目前对机制还不甚了解,之后会持续关注

// 1. y轴反转,因为图像坐标系y轴向下,而webgl坐标系y轴向上
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
// 2. 开启纹理单元(0号),一共有8个
gl.activeTexture(gl.TEXTURE0);
// 3. 绑定纹理对象
gl.bindTexture(gl.TEXTURE_2D, texture);
// 4. 配置纹理参数(对于非2的幂次尺寸的图片,需要设置图片拉伸方式)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
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);
// 如果图片尺寸为2的幂次,则可以使用以下方式
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// 5. 将图像分配给纹理对象
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
// 6. 将0号纹理传递给着色器中的取样器变量
gl.uniform1i(u_Sampler, 0);
2.3.4 修改着色器程序

// 增加纹理坐标的数据接收
const VSHADER_SOURCE = `
  attribute vec4 a_Position;
  attribute vec2 a_TexCoord;
  varying vec2 v_TexCoord;
  uniform mat4 u_Matrix;
  void main() {
    gl_Position = u_Matrix * a_Position;
    v_TexCoord = a_TexCoord;
  }
`;
// 给片元着色器增加 simlper2D的属性以及纹理坐标数据
const FSHADER_SOURCE = `
  precision mediump float;
  uniform sampler2D u_Sampler;
  varying vec2 v_TexCoord;
  void main() {
    gl_FragColor = texture2D(u_Sampler, v_TexCoord);
  }
 `

PS:一定要注意其中关于图片的选择,2.3.3 中关于配置纹理参数,一定要优先确定图片的长和宽是否为2的幂次,从而来决定使用哪种方案进行纹理的匹配,否则将导致纹理解析错误:

It maybe non-power-of-2 and have incompatible texture filtering.

当然目前在测试的过程当中,还存在一个浏览器警告提示没有解决

RENDER WARNING: there is no texture bound to the unit 0

目前虽然有警告,但是纹理渲染并没有问题,而且根据描述来说,我已经对纹理单元0进行了利用了,所以目前还不知道导致的原因,之后如果解决会在评论区补充原因说明。

4. 总结

基本上来说二维图形的绘制,渲染就大致这些内容,不过仍然存在很多细节需要自己去尝试摸索,接下来继续=学习进入真正的WebGL发挥实力的三维图形的领域!

5. 参考

《WebGL编程指南》

你可能感兴趣的:(WebGL-学习笔记(三))