<十二>入门CocosShader之高效绘制多个不同颜色的三角形
前言
举个例子,如果要绘制一个矩形,接上文增加3个顶点坐标来绘制2个三角形以达成目的,确实绘制成功了。但原本绘制一个矩形实际上只需要4个顶点就够了,其中的2个顶点是重复的,这里却增加了2个顶点的消耗。同理如果绘制的图形越复杂,还是通过这种方式,开销就会越来越大。
在实际开发中的解决方案是提供最优顶点,然后指定绘制顺序,这种绘制顺序的方法称之为“顶点索引”,对应WebGL的索引缓冲对象index buffer object,也简称“IBO”。

如上图,把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();

正确绘制出了矩形。
绘制不同颜色
在上述例子中,是通过设置固定颜色(玫红色),如果想要在运行时修改颜色,就需要能动态的设置输入的接口。
输入的方式有两种:
- 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的颜色值进行插值。
修改代码的步骤:
- 把顶点颜色值属性添加到顶点数据中
- 在顶点着色器中添加一个接收顶点颜色输入的变量,同时输出给片元着色器
- 启用顶点颜色属性
- 修改顶点位置和顶点颜色的缓冲绑定和读取数据的方式
//创建着色器
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();
运行预览:

内存优化
实际上顶点位置可以采用float(4个字节),但color的值时0-255,一个字节就够了,一个float就可以存储rgba的四个分量。
这里有两种优化方案:
- 创建一个大的缓冲,让positon和color共享这个缓冲
- 新增一个缓冲,让position和color不使用同一个缓冲
第一种方式:
- 将color拆分出来
- 因为position使用float类型,而color使用uint8就够了,创建一个公用的arrybuffer作为position和color的共享缓冲。
Arrybuffer是用来表示通用的固定长度的原始二进制数据缓冲区,它是一个字节数组。 - 分别定义position和color的buffer
- 存储数据
- 修改绑定数据的存储方式(不再是new一块内存)
- 修改顶点数据的解析方式
//创建着色器
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();
浙公网安备 33010602011771号