<十二>入门CocosShader之高效绘制多个不同颜色的三角形

前言

举个例子,如果要绘制一个矩形,接上文增加3个顶点坐标来绘制2个三角形以达成目的,确实绘制成功了。但原本绘制一个矩形实际上只需要4个顶点就够了,其中的2个顶点是重复的,这里却增加了2个顶点的消耗。同理如果绘制的图形越复杂,还是通过这种方式,开销就会越来越大。
在实际开发中的解决方案是提供最优顶点,然后指定绘制顺序,这种绘制顺序的方法称之为“顶点索引”,对应WebGL的索引缓冲对象index buffer object,也简称“IBO”。
image
如上图,把2个三角形重复的顶点删除,剩下的4个顶点标记出他们的数组索引,有了索引之后就可以方便的定义出两个三角形,第一个三角形的顶点数据取顶点索引0,1,2;第二个三角形的顶点数据取顶点索引2,1,3;然后将数据提交给WebGL索引缓冲对象。WebGL就会按照索引顺序绘制顶点。
需要注意的是:提交的顶点索引需要按照逆时针提交,这是因为一个平面物体通常有两个面,一个正面一个背面(背面是看不到的),WebGL会默认剔除背面,这样在数据提交上就会有一套规则,WebGL默认情况下逆时针提交的三角形为正面。假如第一个三角形提交的是0,2,1,那么这个三角形就有可能看不到。

之前绘制三角形采用的是gl.drawArray来绘制,如果采用顶点索引的方式,就需要gl.drawElements来绘制。

编写代码

以上一篇的shader.js为基础


//创建着色器
function createShader(gl,type,source){
    //创建顶点着色器对象 | 片段着色器
    const shader = gl.createShader(type);
    //将顶点着色器文本关联到顶点着色器对象 | xxx
    gl.shaderSource(shader, source);
    //编译顶点着色器 | xxx
    gl.compileShader(shader);
    //判断编译状态(编译成功才能使用)
    const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
    if(success){
        return shader;
    }
    //编译失败,打印错误信息
    console.log(gl.getShaderInfoLog(shader));
    //删除顶点着色器 | xxx
    gl.deleteShader(shader);
}
//创建着色程序
function createProgram(gl, vertexShader, fragmentShader){
    //创建着色程序对象
    const program = gl.createProgram();
    //将顶点着色器关联到着色程序对象上
    gl.attachShader(program, vertexShader);
    //将片段着色器关联到着色程序对象上
    gl.attachShader(program, fragmentShader);
    //链接着色程序对象
    gl.linkProgram(program);
    //判断是否成功
    const success = gl.getProgramParameter(program, gl.LINK_STATUS);
    if(success){
        return program;
    }
    console.log(gl.getProgramInfoLog(program));
    gl.deleteProgram(program);

}


function main(){
    //WebGL大多数元素都是可以通过状态来描述
    //状态是通过内置全局变量gl来调用切换
    //gl指向的是绘图上下文
    //绘图上下文需要通过canvas元素来获取
    //so,第一步,创建canvas元素
    const canvas = document.createElement('canvas');
    //这里解释一下为什么不直接在html中创建canvas元素,而是在js中动态创建
    //因为很多代码是在js中编写的,在这里定义的canvas当前脚本可以判断其类型,而这些属性在html中定义的canvas元素在js中获取是类型是any,无法使用其api代码提示

    //将创建的cānvas元素添加到body中
    document.getElementsByTagName('body')[0].appendChild(canvas)
    //定义画图尺寸
    canvas.width = 400;
    canvas.height = 300;
    //第二步,获取绘图上下文
    const gl = canvas.getContext("webgl");
    //判断当前浏览器是否支持webgl
    if(!gl){
        console.log('不支持webgl');
        return;
    }
    //第三步,编辑顶点着色器
    //定义着色器文本,因为在js中是无法直接使用GLSL语言,但是gl支持将文本转译成着色器,所以需要通过字符串的形式来编写
    const vertexSource = `
    attribute vec2 a_position;//接收一个顶点位置输入
    void main(){
        gl_Position = vec4(a_position,0.0,1.0);//gl_Position是一个Vec4,传入的顶点是一个vec2,所以需要转换成vec4,x,y取之a_position,没有深度所以z为0.0,w设置为1.0
    }
    `
    //创建顶点着色器
    const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);
    //定义顶点数据
    const positions = [
        0,0,
        0.7,0,
        0,0.5,
        0.7,0.5
    ]
    //启用剔除背面,CocosCreator引擎默认开启了剔除背面
    gl.enable(gl.CULL_FACE);

    //创建缓冲区对象(顶点缓冲对象存储顶点数据)
    const vertexBuffer = gl.createBuffer();
    //在顶点着色器节点会在GPU上创建内存存储顶点数据,顶点缓冲对象就是为了管理这块内存
    //它会在GPU内存储大量的顶点,使用缓冲对象的好处就是可以一次性将大量顶点数据传输到GPU上,而不是每个顶点发送一次
    //顶点缓冲对象的类型是gl.ARRAY_BUFFER,WebGL允许同时绑定多个缓冲,只要类型不同,但同类型的Buffer会替换之前的Buffer
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    //将顶点数据传输到缓冲区对象中
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
    // bufferData主要是初始化buffer对象的数据存储,第一个参数是缓冲区类型,第二个参数是设定数据存储区的大小(GPU内存通常不会很大,所以要合理分配内存),第三个参数是数据的使用方式
    // gl.STATIC_DRAW表示数据不会改变,这里因为传入的是一个固定的顶点数据,所以选择gl.STATIC_DRAW
    // gl.DYNAMIC_DRAW表示数据会频繁更新,常用于动态更新
    // gl.STREAM_DRAW表示数据每次渲染都会更新

    //定义顶点索引
    const indices = [
        0,1,2,
        2,1,3
    ];
    //创建索引缓冲区对象
    const indexBuffer = gl.createBuffer();
    //将索引缓冲区对象绑定到gl.ELEMENT_ARRAY_BUFFER上
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint8Array(indices), gl.STATIC_DRAW);
    //第四步,创建片段着色器
    //定义着色器文本
    const fragmentSource = `
    precision mediump float;
    void main(){
        gl_FragColor = vec4(1.0,0.0,0.5,1.0);//玫红色
    }
    `;
    //创建片段着色器对象
    const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);

    //第五步,创建着色程序
    const program = createProgram(gl, vertexShader, fragmentShader);

    //第六步,设置视口,方便将NDC坐标映射到画布上
    gl.viewport(0, 0, gl.canvas.width,gl.canvas.height);
    //清除画布颜色和颜色缓冲
    gl.clearColor(0, 0, 0, 1);//这里设置成黑色
    //清除画布颜色和颜色缓冲,由于最终呈现在屏幕上的颜色都要从颜色缓冲中读取,所以每次渲染都需要清除颜色缓冲,否则之前的渲染结果会残留导致花屏
    gl.clear(gl.COLOR_BUFFER_BIT);
    //使用着色程序对象
    gl.useProgram(program);
    //获取顶点位置属性变量
    const positonAttributeLocation = gl.getAttribLocation(program, 'a_position');
    //启用顶点位置属性变量
    gl.enableVertexAttribArray(positonAttributeLocation);

    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    //将缓冲区对象绑定到顶点位置属性变量上,并设置如何读取数据
    gl.vertexAttribPointer(positonAttributeLocation, 2, gl.FLOAT, false, 0, 0);

    //绘制三角形
    //gl.drawArrays(gl.TRIANGLES, 0, 3);
    gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE,0)
}

main();

image

正确绘制出了矩形。

绘制不同颜色

在上述例子中,是通过设置固定颜色(玫红色),如果想要在运行时修改颜色,就需要能动态的设置输入的接口。
输入的方式有两种:

  • uniform
  • 扩展顶点属性

uniform

uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式。
unifrom是全局的,这也意味着每一个uniform 变量在着色器中都是唯一的,可以在着色器程序中的任意着色器中的任意阶段访问。
unifrom会始终保持它自身的值,除非它被重置或更新。

这里在片元着色器上定义一个传入颜色值的uniform变量,修改一下js代码,让每次运行时矩形都显示不同的颜色。


//创建着色器
function createShader(gl,type,source){
    //创建顶点着色器对象 | 片段着色器
    const shader = gl.createShader(type);
    //将顶点着色器文本关联到顶点着色器对象 | xxx
    gl.shaderSource(shader, source);
    //编译顶点着色器 | xxx
    gl.compileShader(shader);
    //判断编译状态(编译成功才能使用)
    const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
    if(success){
        return shader;
    }
    //编译失败,打印错误信息
    console.log(gl.getShaderInfoLog(shader));
    //删除顶点着色器 | xxx
    gl.deleteShader(shader);
}
//创建着色程序
function createProgram(gl, vertexShader, fragmentShader){
    //创建着色程序对象
    const program = gl.createProgram();
    //将顶点着色器关联到着色程序对象上
    gl.attachShader(program, vertexShader);
    //将片段着色器关联到着色程序对象上
    gl.attachShader(program, fragmentShader);
    //链接着色程序对象
    gl.linkProgram(program);
    //判断是否成功
    const success = gl.getProgramParameter(program, gl.LINK_STATUS);
    if(success){
        return program;
    }
    console.log(gl.getProgramInfoLog(program));
    gl.deleteProgram(program);

}


function main(){
    //WebGL大多数元素都是可以通过状态来描述
    //状态是通过内置全局变量gl来调用切换
    //gl指向的是绘图上下文
    //绘图上下文需要通过canvas元素来获取
    //so,第一步,创建canvas元素
    const canvas = document.createElement('canvas');
    //这里解释一下为什么不直接在html中创建canvas元素,而是在js中动态创建
    //因为很多代码是在js中编写的,在这里定义的canvas当前脚本可以判断其类型,而这些属性在html中定义的canvas元素在js中获取是类型是any,无法使用其api代码提示

    //将创建的cānvas元素添加到body中
    document.getElementsByTagName('body')[0].appendChild(canvas)
    //定义画图尺寸
    canvas.width = 400;
    canvas.height = 300;
    //第二步,获取绘图上下文
    const gl = canvas.getContext("webgl");
    //判断当前浏览器是否支持webgl
    if(!gl){
        console.log('不支持webgl');
        return;
    }
    //第三步,编辑顶点着色器
    //定义着色器文本,因为在js中是无法直接使用GLSL语言,但是gl支持将文本转译成着色器,所以需要通过字符串的形式来编写
    const vertexSource = `
    attribute vec2 a_position;//接收一个顶点位置输入
    void main(){
        gl_Position = vec4(a_position,0.0,1.0);//gl_Position是一个Vec4,传入的顶点是一个vec2,所以需要转换成vec4,x,y取之a_position,没有深度所以z为0.0,w设置为1.0
    }
    `
    //创建顶点着色器
    const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);
    //定义顶点数据
    const positions = [
        0,0,
        0.7,0,
        0,0.5,
        0.7,0.5
    ]
    //启用剔除背面,CocosCreator引擎默认开启了剔除背面
    gl.enable(gl.CULL_FACE);

    //创建缓冲区对象(顶点缓冲对象存储顶点数据)
    const vertexBuffer = gl.createBuffer();
    //在顶点着色器节点会在GPU上创建内存存储顶点数据,顶点缓冲对象就是为了管理这块内存
    //它会在GPU内存储大量的顶点,使用缓冲对象的好处就是可以一次性将大量顶点数据传输到GPU上,而不是每个顶点发送一次
    //顶点缓冲对象的类型是gl.ARRAY_BUFFER,WebGL允许同时绑定多个缓冲,只要类型不同,但同类型的Buffer会替换之前的Buffer
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    //将顶点数据传输到缓冲区对象中
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
    // bufferData主要是初始化buffer对象的数据存储,第一个参数是缓冲区类型,第二个参数是设定数据存储区的大小(GPU内存通常不会很大,所以要合理分配内存),第三个参数是数据的使用方式
    // gl.STATIC_DRAW表示数据不会改变,这里因为传入的是一个固定的顶点数据,所以选择gl.STATIC_DRAW
    // gl.DYNAMIC_DRAW表示数据会频繁更新,常用于动态更新
    // gl.STREAM_DRAW表示数据每次渲染都会更新

    //定义顶点索引
    const indices = [
        0,1,2,
        2,1,3
    ];
    //创建索引缓冲区对象
    const indexBuffer = gl.createBuffer();
    //将索引缓冲区对象绑定到gl.ELEMENT_ARRAY_BUFFER上
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint8Array(indices), gl.STATIC_DRAW);
    //第四步,创建片段着色器
    //定义着色器文本
    const fragmentSource = `
    precision mediump float;
    uniform vec4 u_color;
    void main(){
        //gl_FragColor = vec4(1.0,0.0,0.5,1.0);//玫红色
        gl_FragColor = u_color;
    }
    `;
    //创建片段着色器对象
    const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);

    //第五步,创建着色程序
    const program = createProgram(gl, vertexShader, fragmentShader);

    //第六步,设置视口,方便将NDC坐标映射到画布上
    gl.viewport(0, 0, gl.canvas.width,gl.canvas.height);
    //清除画布颜色和颜色缓冲
    gl.clearColor(0, 0, 0, 1);//这里设置成黑色
    //清除画布颜色和颜色缓冲,由于最终呈现在屏幕上的颜色都要从颜色缓冲中读取,所以每次渲染都需要清除颜色缓冲,否则之前的渲染结果会残留导致花屏
    gl.clear(gl.COLOR_BUFFER_BIT);
    //使用着色程序对象
    gl.useProgram(program);
    //获取顶点位置属性变量
    const positonAttributeLocation = gl.getAttribLocation(program, 'a_position');
    //启用顶点位置属性变量
    gl.enableVertexAttribArray(positonAttributeLocation);

    //和获取顶点坐标一样,这里获取u_color属性变量
    const colorAttributeLocation = gl.getUniformLocation(program, 'u_color');
    //设置颜色属性变量(这里采用4fv:4个float的数组形式,另外还有[1/2/3/4]+f/i/ui,matrix + [1/2/3/4]+fv等)
    gl.uniform4fv(colorAttributeLocation, [Math.random(),Math.random(),Math.random(),1]);


    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    //将缓冲区对象绑定到顶点位置属性变量上,并设置如何读取数据
    gl.vertexAttribPointer(positonAttributeLocation, 2, gl.FLOAT, false, 0, 0);

    //绘制三角形
    //gl.drawArrays(gl.TRIANGLES, 0, 3);
    gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE,0)
}

main();

扩展顶点属性

uniform设置颜色的方式仍然比较单一,所有的顶点都是同一种颜色,如果希望不同的顶点采用不同的颜色则无法满足。这种情况就可以采用“扩展顶点属性”的方式来实现。
WebGL可以获得在顶点着色器中输入的颜色值,在光栅化的时候对不同的颜色值进行插值,也就是说如果三角形每个顶点的颜色值不一样,那么光栅化时就会对顶点a和顶点b的颜色值进行插值。
修改代码的步骤:

  1. 把顶点颜色值属性添加到顶点数据中
  2. 在顶点着色器中添加一个接收顶点颜色输入的变量,同时输出给片元着色器
  3. 启用顶点颜色属性
  4. 修改顶点位置和顶点颜色的缓冲绑定和读取数据的方式

//创建着色器
function createShader(gl,type,source){
    //创建顶点着色器对象 | 片段着色器
    const shader = gl.createShader(type);
    //将顶点着色器文本关联到顶点着色器对象 | xxx
    gl.shaderSource(shader, source);
    //编译顶点着色器 | xxx
    gl.compileShader(shader);
    //判断编译状态(编译成功才能使用)
    const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
    if(success){
        return shader;
    }
    //编译失败,打印错误信息
    console.log(gl.getShaderInfoLog(shader));
    //删除顶点着色器 | xxx
    gl.deleteShader(shader);
}
//创建着色程序
function createProgram(gl, vertexShader, fragmentShader){
    //创建着色程序对象
    const program = gl.createProgram();
    //将顶点着色器关联到着色程序对象上
    gl.attachShader(program, vertexShader);
    //将片段着色器关联到着色程序对象上
    gl.attachShader(program, fragmentShader);
    //链接着色程序对象
    gl.linkProgram(program);
    //判断是否成功
    const success = gl.getProgramParameter(program, gl.LINK_STATUS);
    if(success){
        return program;
    }
    console.log(gl.getProgramInfoLog(program));
    gl.deleteProgram(program);

}


function main(){
    //WebGL大多数元素都是可以通过状态来描述
    //状态是通过内置全局变量gl来调用切换
    //gl指向的是绘图上下文
    //绘图上下文需要通过canvas元素来获取
    //so,第一步,创建canvas元素
    const canvas = document.createElement('canvas');
    //这里解释一下为什么不直接在html中创建canvas元素,而是在js中动态创建
    //因为很多代码是在js中编写的,在这里定义的canvas当前脚本可以判断其类型,而这些属性在html中定义的canvas元素在js中获取是类型是any,无法使用其api代码提示

    //将创建的cānvas元素添加到body中
    document.getElementsByTagName('body')[0].appendChild(canvas)
    //定义画图尺寸
    canvas.width = 400;
    canvas.height = 300;
    //第二步,获取绘图上下文
    const gl = canvas.getContext("webgl");
    //判断当前浏览器是否支持webgl
    if(!gl){
        console.log('不支持webgl');
        return;
    }
    //第三步,编辑顶点着色器
    //定义着色器文本,因为在js中是无法直接使用GLSL语言,但是gl支持将文本转译成着色器,所以需要通过字符串的形式来编写
    const vertexSource = `
    attribute vec2 a_position;//接收一个顶点位置输入
    attribute vec4 a_color;//接收一个顶点颜色输入
    varying vec4 v_color;//将顶点颜色传递给片段着色器




    void main(){
        v_color = a_color;
        gl_Position = vec4(a_position,0.0,1.0);//gl_Position是一个Vec4,传入的顶点是一个vec2,所以需要转换成vec4,x,y取之a_position,没有深度所以z为0.0,w设置为1.0
    }
    `
    //创建顶点着色器
    const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);
    //定义顶点数据
    // const positions = [
    //     0,0,
    //     0.7,0,
    //     0,0.5,
    //     0.7,0.5
    // ]
    //增加颜色值
    const vertexData = [
        0,0,1,0,0,1, 
        0.7,0,0,1,0,1,
        0,0.5,0,0,1,1,
        0.7,0.5,1,0.5,0,1,   
    ]
    //启用剔除背面,CocosCreator引擎默认开启了剔除背面
    gl.enable(gl.CULL_FACE);

    //创建缓冲区对象(顶点缓冲对象存储顶点数据)
    const vertexBuffer = gl.createBuffer();
    //在顶点着色器节点会在GPU上创建内存存储顶点数据,顶点缓冲对象就是为了管理这块内存
    //它会在GPU内存储大量的顶点,使用缓冲对象的好处就是可以一次性将大量顶点数据传输到GPU上,而不是每个顶点发送一次
    //顶点缓冲对象的类型是gl.ARRAY_BUFFER,WebGL允许同时绑定多个缓冲,只要类型不同,但同类型的Buffer会替换之前的Buffer
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    //将顶点数据传输到缓冲区对象中
    //gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexData), gl.STATIC_DRAW);
    // bufferData主要是初始化buffer对象的数据存储,第一个参数是缓冲区类型,第二个参数是设定数据存储区的大小(GPU内存通常不会很大,所以要合理分配内存),第三个参数是数据的使用方式
    // gl.STATIC_DRAW表示数据不会改变,这里因为传入的是一个固定的顶点数据,所以选择gl.STATIC_DRAW
    // gl.DYNAMIC_DRAW表示数据会频繁更新,常用于动态更新
    // gl.STREAM_DRAW表示数据每次渲染都会更新

    //定义顶点索引
    const indices = [
        0,1,2,
        2,1,3
    ];
    //创建索引缓冲区对象
    const indexBuffer = gl.createBuffer();
    //将索引缓冲区对象绑定到gl.ELEMENT_ARRAY_BUFFER上
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint8Array(indices), gl.STATIC_DRAW);
    //第四步,创建片段着色器
    //定义着色器文本
    const fragmentSource = `
    precision mediump float;
    //uniform vec4 u_color;
    varying vec4 v_color;

    void main(){
        //gl_FragColor = vec4(1.0,0.0,0.5,1.0);//玫红色
        //gl_FragColor = u_color;
        gl_FragColor = v_color;
    }
    `;
    //创建片段着色器对象
    const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);

    //第五步,创建着色程序
    const program = createProgram(gl, vertexShader, fragmentShader);

    //第六步,设置视口,方便将NDC坐标映射到画布上
    gl.viewport(0, 0, gl.canvas.width,gl.canvas.height);
    //清除画布颜色和颜色缓冲
    gl.clearColor(0, 0, 0, 1);//这里设置成黑色
    //清除画布颜色和颜色缓冲,由于最终呈现在屏幕上的颜色都要从颜色缓冲中读取,所以每次渲染都需要清除颜色缓冲,否则之前的渲染结果会残留导致花屏
    gl.clear(gl.COLOR_BUFFER_BIT);
    //使用着色程序对象
    gl.useProgram(program);
    //获取顶点位置属性变量
    const positonAttributeLocation = gl.getAttribLocation(program, 'a_position');
    //启用顶点位置属性变量
    gl.enableVertexAttribArray(positonAttributeLocation);

    //获取顶点颜色属性变量
    const colorAttributeLocation = gl.getAttribLocation(program, 'a_color');
    //启用顶点颜色属性变量
    gl.enableVertexAttribArray(colorAttributeLocation);


    // //和获取顶点坐标一样,这里获取u_color属性变量
    // const colorAttributeLocation = gl.getUniformLocation(program, 'u_color');
    // //设置颜色属性变量(这里采用4fv:4个float的数组形式,另外还有[1/2/3/4]+f/i/ui,matrix + [1/2/3/4]+fv等)
    // gl.uniform4fv(colorAttributeLocation, [Math.random(),Math.random(),Math.random(),1]);


    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    //将缓冲区对象绑定到顶点位置属性变量上,并设置如何读取数据
    gl.vertexAttribPointer(positonAttributeLocation, 2, gl.FLOAT, false, 24, 0);
    //将缓冲区对象绑定到顶点颜色属性变量上,并设置如何读取数据
    gl.vertexAttribPointer(colorAttributeLocation, 4, gl.FLOAT, false, 24, 8);

    //绘制三角形
    //gl.drawArrays(gl.TRIANGLES, 0, 3);
    gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE,0)
}

main();

运行预览:
image

内存优化

实际上顶点位置可以采用float(4个字节),但color的值时0-255,一个字节就够了,一个float就可以存储rgba的四个分量。
这里有两种优化方案:

  1. 创建一个大的缓冲,让positon和color共享这个缓冲
  2. 新增一个缓冲,让position和color不使用同一个缓冲

第一种方式:

  1. 将color拆分出来
  2. 因为position使用float类型,而color使用uint8就够了,创建一个公用的arrybuffer作为position和color的共享缓冲。
    Arrybuffer是用来表示通用的固定长度的原始二进制数据缓冲区,它是一个字节数组。
  3. 分别定义position和color的buffer
  4. 存储数据
  5. 修改绑定数据的存储方式(不再是new一块内存)
  6. 修改顶点数据的解析方式

//创建着色器
function createShader(gl,type,source){
    //创建顶点着色器对象 | 片段着色器
    const shader = gl.createShader(type);
    //将顶点着色器文本关联到顶点着色器对象 | xxx
    gl.shaderSource(shader, source);
    //编译顶点着色器 | xxx
    gl.compileShader(shader);
    //判断编译状态(编译成功才能使用)
    const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
    if(success){
        return shader;
    }
    //编译失败,打印错误信息
    console.log(gl.getShaderInfoLog(shader));
    //删除顶点着色器 | xxx
    gl.deleteShader(shader);
}
//创建着色程序
function createProgram(gl, vertexShader, fragmentShader){
    //创建着色程序对象
    const program = gl.createProgram();
    //将顶点着色器关联到着色程序对象上
    gl.attachShader(program, vertexShader);
    //将片段着色器关联到着色程序对象上
    gl.attachShader(program, fragmentShader);
    //链接着色程序对象
    gl.linkProgram(program);
    //判断是否成功
    const success = gl.getProgramParameter(program, gl.LINK_STATUS);
    if(success){
        return program;
    }
    console.log(gl.getProgramInfoLog(program));
    gl.deleteProgram(program);

}


function main(){
    //WebGL大多数元素都是可以通过状态来描述
    //状态是通过内置全局变量gl来调用切换
    //gl指向的是绘图上下文
    //绘图上下文需要通过canvas元素来获取
    //so,第一步,创建canvas元素
    const canvas = document.createElement('canvas');
    //这里解释一下为什么不直接在html中创建canvas元素,而是在js中动态创建
    //因为很多代码是在js中编写的,在这里定义的canvas当前脚本可以判断其类型,而这些属性在html中定义的canvas元素在js中获取是类型是any,无法使用其api代码提示

    //将创建的cānvas元素添加到body中
    document.getElementsByTagName('body')[0].appendChild(canvas)
    //定义画图尺寸
    canvas.width = 400;
    canvas.height = 300;
    //第二步,获取绘图上下文
    const gl = canvas.getContext("webgl");
    //判断当前浏览器是否支持webgl
    if(!gl){
        console.log('不支持webgl');
        return;
    }
    //第三步,编辑顶点着色器
    //定义着色器文本,因为在js中是无法直接使用GLSL语言,但是gl支持将文本转译成着色器,所以需要通过字符串的形式来编写
    const vertexSource = `
    attribute vec2 a_position;//接收一个顶点位置输入
    attribute vec4 a_color;//接收一个顶点颜色输入
    varying vec4 v_color;//将顶点颜色传递给片段着色器




    void main(){
        v_color = a_color;
        gl_Position = vec4(a_position,0.0,1.0);//gl_Position是一个Vec4,传入的顶点是一个vec2,所以需要转换成vec4,x,y取之a_position,没有深度所以z为0.0,w设置为1.0
    }
    `
    //创建顶点着色器
    const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);
    //定义顶点数据
    // const positions = [
    //     0,0,
    //     0.7,0,
    //     0,0.5,
    //     0.7,0.5
    // ]
    //顶点位置
    const positons = [
        0,0,
        0.7,0,
        0,0.5,
        0.7,0.5,
    ]
    //顶点颜色
    const colors = [
        255,0,0,255, 
        0,255,0,255,
        0,0,255,255,
        255,127,0,255,   
    ]
    //启用剔除背面,CocosCreator引擎默认开启了剔除背面
    gl.enable(gl.CULL_FACE);

    //创建一个大的共享arryBuffer并指定它的长度,长度为positons的长度占用和color的长度占用之和
    const arrayBuffer = new ArrayBuffer(positons.length * Float32Array.BYTES_PER_ELEMENT + colors.length);
    //创建buffer
    const positionsBuffer = new Float32Array(arrayBuffer);
    const colorsBuffer = new Uint8Array(arrayBuffer);
    //将数据填充到Buffer中
    let offset = 0;//定义偏移量,表示数据在缓冲区的存储位置

    for(let i = 0; i < positons.length; i += 2){
        positionsBuffer[offset] = positons[i];
        positionsBuffer[offset + 1] = positons[i+1];
        offset += 3;//偏移量每次增加3,因为顶点位置占用2个float,顶点颜色占用4个uint8 = 1个float
    }
    offset = 8;
    for(let i = 0; i < colors.length; i += 4){
        colorsBuffer[offset] = colors[i];
        colorsBuffer[offset + 1] = colors[i+1];
        colorsBuffer[offset + 2] = colors[i+2];
        colorsBuffer[offset + 3] = colors[i+3];
        offset += 12;
    }

    //创建缓冲区对象(顶点缓冲对象存储顶点数据)
    const vertexBuffer = gl.createBuffer();
    //在顶点着色器节点会在GPU上创建内存存储顶点数据,顶点缓冲对象就是为了管理这块内存
    //它会在GPU内存储大量的顶点,使用缓冲对象的好处就是可以一次性将大量顶点数据传输到GPU上,而不是每个顶点发送一次
    //顶点缓冲对象的类型是gl.ARRAY_BUFFER,WebGL允许同时绑定多个缓冲,只要类型不同,但同类型的Buffer会替换之前的Buffer
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    //将顶点数据传输到缓冲区对象中
    //gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
    //gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexData), gl.STATIC_DRAW);
    gl.bufferData(gl.ARRAY_BUFFER, arrayBuffer, gl.STATIC_DRAW);
    // bufferData主要是初始化buffer对象的数据存储,第一个参数是缓冲区类型,第二个参数是设定数据存储区的大小(GPU内存通常不会很大,所以要合理分配内存),第三个参数是数据的使用方式
    // gl.STATIC_DRAW表示数据不会改变,这里因为传入的是一个固定的顶点数据,所以选择gl.STATIC_DRAW
    // gl.DYNAMIC_DRAW表示数据会频繁更新,常用于动态更新
    // gl.STREAM_DRAW表示数据每次渲染都会更新

    //定义顶点索引
    const indices = [
        0,1,2,
        2,1,3
    ];
    //创建索引缓冲区对象
    const indexBuffer = gl.createBuffer();
    //将索引缓冲区对象绑定到gl.ELEMENT_ARRAY_BUFFER上
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint8Array(indices), gl.STATIC_DRAW);
    //第四步,创建片段着色器
    //定义着色器文本
    const fragmentSource = `
    precision mediump float;
    //uniform vec4 u_color;
    varying vec4 v_color;

    void main(){
        //gl_FragColor = vec4(1.0,0.0,0.5,1.0);//玫红色
        //gl_FragColor = u_color;
        gl_FragColor = v_color;
    }
    `;
    //创建片段着色器对象
    const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);

    //第五步,创建着色程序
    const program = createProgram(gl, vertexShader, fragmentShader);

    //第六步,设置视口,方便将NDC坐标映射到画布上
    gl.viewport(0, 0, gl.canvas.width,gl.canvas.height);
    //清除画布颜色和颜色缓冲
    gl.clearColor(0, 0, 0, 1);//这里设置成黑色
    //清除画布颜色和颜色缓冲,由于最终呈现在屏幕上的颜色都要从颜色缓冲中读取,所以每次渲染都需要清除颜色缓冲,否则之前的渲染结果会残留导致花屏
    gl.clear(gl.COLOR_BUFFER_BIT);
    //使用着色程序对象
    gl.useProgram(program);
    //获取顶点位置属性变量
    const positonAttributeLocation = gl.getAttribLocation(program, 'a_position');
    //启用顶点位置属性变量
    gl.enableVertexAttribArray(positonAttributeLocation);

    //获取顶点颜色属性变量
    const colorAttributeLocation = gl.getAttribLocation(program, 'a_color');
    //启用顶点颜色属性变量
    gl.enableVertexAttribArray(colorAttributeLocation);


    // //和获取顶点坐标一样,这里获取u_color属性变量
    // const colorAttributeLocation = gl.getUniformLocation(program, 'u_color');
    // //设置颜色属性变量(这里采用4fv:4个float的数组形式,另外还有[1/2/3/4]+f/i/ui,matrix + [1/2/3/4]+fv等)
    // gl.uniform4fv(colorAttributeLocation, [Math.random(),Math.random(),Math.random(),1]);


    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    //将缓冲区对象绑定到顶点位置属性变量上,并设置如何读取数据
    //gl.vertexAttribPointer(positonAttributeLocation, 2, gl.FLOAT, false, 24, 0);
    gl.vertexAttribPointer(positonAttributeLocation, 2, gl.FLOAT, false, 12, 0);
    //将缓冲区对象绑定到顶点颜色属性变量上,并设置如何读取数据
    gl.vertexAttribPointer(colorAttributeLocation, 4, gl.UNSIGNED_BYTE, true, 12, 8);

    //绘制三角形
    //gl.drawArrays(gl.TRIANGLES, 0, 3);
    gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE,0)
}

main();

第二种方式:
第二种的实现和创建vertexBuffer是一样,另外创建一个colorBuffer来存储颜色值


//创建着色器
function createShader(gl,type,source){
    //创建顶点着色器对象 | 片段着色器
    const shader = gl.createShader(type);
    //将顶点着色器文本关联到顶点着色器对象 | xxx
    gl.shaderSource(shader, source);
    //编译顶点着色器 | xxx
    gl.compileShader(shader);
    //判断编译状态(编译成功才能使用)
    const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
    if(success){
        return shader;
    }
    //编译失败,打印错误信息
    console.log(gl.getShaderInfoLog(shader));
    //删除顶点着色器 | xxx
    gl.deleteShader(shader);
}
//创建着色程序
function createProgram(gl, vertexShader, fragmentShader){
    //创建着色程序对象
    const program = gl.createProgram();
    //将顶点着色器关联到着色程序对象上
    gl.attachShader(program, vertexShader);
    //将片段着色器关联到着色程序对象上
    gl.attachShader(program, fragmentShader);
    //链接着色程序对象
    gl.linkProgram(program);
    //判断是否成功
    const success = gl.getProgramParameter(program, gl.LINK_STATUS);
    if(success){
        return program;
    }
    console.log(gl.getProgramInfoLog(program));
    gl.deleteProgram(program);

}


function main(){
    //WebGL大多数元素都是可以通过状态来描述
    //状态是通过内置全局变量gl来调用切换
    //gl指向的是绘图上下文
    //绘图上下文需要通过canvas元素来获取
    //so,第一步,创建canvas元素
    const canvas = document.createElement('canvas');
    //这里解释一下为什么不直接在html中创建canvas元素,而是在js中动态创建
    //因为很多代码是在js中编写的,在这里定义的canvas当前脚本可以判断其类型,而这些属性在html中定义的canvas元素在js中获取是类型是any,无法使用其api代码提示

    //将创建的cānvas元素添加到body中
    document.getElementsByTagName('body')[0].appendChild(canvas)
    //定义画图尺寸
    canvas.width = 400;
    canvas.height = 300;
    //第二步,获取绘图上下文
    const gl = canvas.getContext("webgl");
    //判断当前浏览器是否支持webgl
    if(!gl){
        console.log('不支持webgl');
        return;
    }
    //第三步,编辑顶点着色器
    //定义着色器文本,因为在js中是无法直接使用GLSL语言,但是gl支持将文本转译成着色器,所以需要通过字符串的形式来编写
    const vertexSource = `
    attribute vec2 a_position;//接收一个顶点位置输入
    attribute vec4 a_color;//接收一个顶点颜色输入
    varying vec4 v_color;//将顶点颜色传递给片段着色器




    void main(){
        v_color = a_color;
        gl_Position = vec4(a_position,0.0,1.0);//gl_Position是一个Vec4,传入的顶点是一个vec2,所以需要转换成vec4,x,y取之a_position,没有深度所以z为0.0,w设置为1.0
    }
    `
    //创建顶点着色器
    const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);
    //定义顶点数据
    // const positions = [
    //     0,0,
    //     0.7,0,
    //     0,0.5,
    //     0.7,0.5
    // ]
    //顶点位置
    const positons = [
        0,0,
        0.7,0,
        0,0.5,
        0.7,0.5,
    ]
    //顶点颜色
    const colors = [
        255,0,0,255, 
        0,255,0,255,
        0,0,255,255,
        255,127,0,255,   
    ]
    //启用剔除背面,CocosCreator引擎默认开启了剔除背面
    gl.enable(gl.CULL_FACE);

    // //创建一个大的共享arryBuffer并指定它的长度,长度为positons的长度占用和color的长度占用之和
    // const arrayBuffer = new ArrayBuffer(positons.length * Float32Array.BYTES_PER_ELEMENT + colors.length);
    // //创建buffer
    // const positionsBuffer = new Float32Array(arrayBuffer);
    // const colorsBuffer = new Uint8Array(arrayBuffer);
    // //将数据填充到Buffer中
    // let offset = 0;//定义偏移量,表示数据在缓冲区的存储位置

    // for(let i = 0; i < positons.length; i += 2){
    //     positionsBuffer[offset] = positons[i];
    //     positionsBuffer[offset + 1] = positons[i+1];
    //     offset += 3;//偏移量每次增加3,因为顶点位置占用2个float,顶点颜色占用4个uint8 = 1个float
    // }
    // offset = 8;
    // for(let i = 0; i < colors.length; i += 4){
    //     colorsBuffer[offset] = colors[i];
    //     colorsBuffer[offset + 1] = colors[i+1];
    //     colorsBuffer[offset + 2] = colors[i+2];
    //     colorsBuffer[offset + 3] = colors[i+3];
    //     offset += 12;
    // }

    //创建缓冲区对象(顶点缓冲对象存储顶点数据)
    const vertexBuffer = gl.createBuffer();
    //在顶点着色器节点会在GPU上创建内存存储顶点数据,顶点缓冲对象就是为了管理这块内存
    //它会在GPU内存储大量的顶点,使用缓冲对象的好处就是可以一次性将大量顶点数据传输到GPU上,而不是每个顶点发送一次
    //顶点缓冲对象的类型是gl.ARRAY_BUFFER,WebGL允许同时绑定多个缓冲,只要类型不同,但同类型的Buffer会替换之前的Buffer
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    //将顶点数据传输到缓冲区对象中
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positons), gl.STATIC_DRAW);
    //gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexData), gl.STATIC_DRAW);
    //gl.bufferData(gl.ARRAY_BUFFER, arrayBuffer, gl.STATIC_DRAW);
    // bufferData主要是初始化buffer对象的数据存储,第一个参数是缓冲区类型,第二个参数是设定数据存储区的大小(GPU内存通常不会很大,所以要合理分配内存),第三个参数是数据的使用方式
    // gl.STATIC_DRAW表示数据不会改变,这里因为传入的是一个固定的顶点数据,所以选择gl.STATIC_DRAW
    // gl.DYNAMIC_DRAW表示数据会频繁更新,常用于动态更新
    // gl.STREAM_DRAW表示数据每次渲染都会更新

    const colorsBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, colorsBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Uint8Array(colors), gl.STATIC_DRAW);


    //定义顶点索引
    const indices = [
        0,1,2,
        2,1,3
    ];
    //创建索引缓冲区对象
    const indexBuffer = gl.createBuffer();
    //将索引缓冲区对象绑定到gl.ELEMENT_ARRAY_BUFFER上
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint8Array(indices), gl.STATIC_DRAW);
    //第四步,创建片段着色器
    //定义着色器文本
    const fragmentSource = `
    precision mediump float;
    //uniform vec4 u_color;
    varying vec4 v_color;

    void main(){
        //gl_FragColor = vec4(1.0,0.0,0.5,1.0);//玫红色
        //gl_FragColor = u_color;
        gl_FragColor = v_color;
    }
    `;
    //创建片段着色器对象
    const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);

    //第五步,创建着色程序
    const program = createProgram(gl, vertexShader, fragmentShader);

    //第六步,设置视口,方便将NDC坐标映射到画布上
    gl.viewport(0, 0, gl.canvas.width,gl.canvas.height);
    //清除画布颜色和颜色缓冲
    gl.clearColor(0, 0, 0, 1);//这里设置成黑色
    //清除画布颜色和颜色缓冲,由于最终呈现在屏幕上的颜色都要从颜色缓冲中读取,所以每次渲染都需要清除颜色缓冲,否则之前的渲染结果会残留导致花屏
    gl.clear(gl.COLOR_BUFFER_BIT);
    //使用着色程序对象
    gl.useProgram(program);
    //获取顶点位置属性变量
    const positonAttributeLocation = gl.getAttribLocation(program, 'a_position');
    //启用顶点位置属性变量
    gl.enableVertexAttribArray(positonAttributeLocation);

    //获取顶点颜色属性变量
    const colorAttributeLocation = gl.getAttribLocation(program, 'a_color');
    //启用顶点颜色属性变量
    gl.enableVertexAttribArray(colorAttributeLocation);


    // //和获取顶点坐标一样,这里获取u_color属性变量
    // const colorAttributeLocation = gl.getUniformLocation(program, 'u_color');
    // //设置颜色属性变量(这里采用4fv:4个float的数组形式,另外还有[1/2/3/4]+f/i/ui,matrix + [1/2/3/4]+fv等)
    // gl.uniform4fv(colorAttributeLocation, [Math.random(),Math.random(),Math.random(),1]);


    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
 
    //将缓冲区对象绑定到顶点位置属性变量上,并设置如何读取数据
    //gl.vertexAttribPointer(positonAttributeLocation, 2, gl.FLOAT, false, 24, 0);
    gl.vertexAttribPointer(positonAttributeLocation, 2, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, colorsBuffer);
    //将缓冲区对象绑定到顶点颜色属性变量上,并设置如何读取数据
    gl.vertexAttribPointer(colorAttributeLocation, 4, gl.UNSIGNED_BYTE, true, 0, 0);

    //绘制三角形
    //gl.drawArrays(gl.TRIANGLES, 0, 3);
    gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE,0)
}

main();
posted @ 2025-02-20 10:54  EricShx  阅读(20)  评论(0)    收藏  举报