学习路线:
教程主要分为四大部分:
在2D绘图环境中的坐标系统,默认情况下是与窗口坐标系统相同,它以canvas的左上角为坐标原点,沿x轴向右为正值,沿y轴项下为正值。其中canvas坐标的单位都是"px";
webgl使用的是正交右手坐标系,且每个方向都有可使用的值的区间,超出该矩形区间的图像不会绘制:
在2D绘图环境中的坐标系统,默认情况下是与窗口坐标系统相同,它以canvas的左上角为坐标原点,沿x轴向右为正值,沿y轴向下为正值。其中canvasd坐标的单位都是"px"。
渲染管线就像一条流水线,由一系列具有特定功能的数字电路单元组成,下一个功能单元处理上一个功能单元生成的数据,逐级处理数据。
定点着色器和片元着色器是可编程的功能单元,拥有更大自主性,还有光栅器、深度测试等补课表承德功能单元。CPU会通过webgl api和GPU通讯,传递着色器程序和数据,GPU执行的着色器程序
webgl渲染管线其实就是一个流水线,一个方便面为例子:
顶点着色器是GPU渲染管线上一个可以执行着色器语言的功能单元,具体执行的就是顶点着色器程序,webgl定点着色器程序在javascript中一字符串的形式存在,通过编译处理后传递给顶点着色器执行。定点着色器主要作用就是执行点点着色器程序对定点进行变换计算,比如点点位置坐标执行进行旋转、平移等矩阵变换,变换后新的顶点坐标然后赋值给内置变量gl_Position,作为顶点着色器的输出,图元装配和光栅化环节的输入;
顶点变换后的操作是图元装配,硬件上具体是怎么回事不用考虑,从程序的角度来看,就是绘制函数drawArray()
或drawElement()
第一个参数绘制模式mode
控制定点如何装配为图元,gl.LINES
的定义的是把两个定点装配成一个线条图元,gl.TRIANGLES
定义的是三个顶点装配为一个三角面图元,gl.POINTS
定义的是一个点域图元。
片元着色器和顶点着色器一样是GPU渲染管线上一个可以执行着色器程序的功能单元,顶点着色器处理的是逐顶点处理顶点数据,片元着色器是逐片元处理片源数据。通过给内置变量gl.FragColor
赋值可以给每一个片元进行着色,值可以是一个确定的RGBA值,可以是一个和片元位置相关的值,也可以是炒制后的顶点颜色。除了给片元进行着色之外,通过关键字discard
还可以实现那些偏远可以被丢弃,被丢弃的片元不会出现在帧缓冲区,自然不会显示在canvas画布上。
片元着色器的功能可以简单理解成,给顶点着色器着色。
实现思路:
<template>
<canvas id="webglCanvas" ref="webglCanvas" width="500" height="500"></canvas>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
const webglCanvas = ref(null);
// 入口函数
const init = () => {
const gl = webglCanvas.value.getContext("webgl"); //拿到webgl实例
if (!gl) {
console.log("fail to get the rendering context of webgl");
return;
}
gl.clearColor(0.0, 0.0, 0.0, 1.0); //设置背景色
gl.clear(gl.COLOR_BUFFER_BIT); //清空背景
}
onMounted(() => {
init();
})
</script>
gl.clearColor(red,green.blue,alpha)
执行绘图区域背景色。
在我们css颜色系统中,设置都是从0到255,webgl的色值是从0-1,这是因为继承自openGL,越大颜色越是亮。一旦指定了背景色之后,颜色就会驻留在webgl系统中,在下次调用gl.clearColor()
之前不会改变。
gl.clear(buffer)
将指定缓冲区设定为预定的值。如果清空的是颜色缓冲区,那么将使用gl.clearColor()
指定的值(作为预定值)
<script setup lang="ts">
import { ref, onMounted } from "vue";
const webglCanvas = ref(null);
// 顶点着色器
var VSHADER_SOURCE =
'void main() {\n' +
' gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n' + // Set the vertex coordinates of the point
' gl_PointSize = 10.0;\n' + // Set the point size
'}\n';
// 片元着色器
var FSHADER_SOURCE =
'void main() {\n' +
' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' + // Set the point color
'}\n';
// 入口函数
const init = () => {
const gl = getWebGLContext(webglCanvas.value); //拿到webgl实例
if (!gl) {
console.log("fail to get the rendering context of webgl");
return;
}
// 初始化着色器
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to intialize shaders.');
return;
}
gl.clearColor(0.0, 0.0, 0.0, 1.0); //设置背景色
gl.clear(gl.COLOR_BUFFER_BIT); //清空背景
// 画一个点
gl.drawArrays(gl.POINTS, 0, 1);
}
onMounted(() => {
init();
})
</script>
gl.drawArrays(mode,first,count)
可以用于绘制各种图形。实际是执行着色器,按照mode参数指定的方式绘制图形。
getWebGLContext
获取webgl实例(具体见源码)
initShaders
初始化着色器(具体见源码)
VSHADER_SOURCE
gl_Position:设置位置
gl_PointSize:设置尺寸
FSHADER_SOURCE
gl_FragColor:设置颜色
webgl遵守右手坐标系,所以我们看到修改
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
中的坐标值,对应不同的效果,具体如下:
<script setup lang="ts">
...
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +//定义a_Position
'void main() {\n' +
' gl_Position = a_Position;\n' + // a_Position传给gl_Position
' gl_PointSize = 10.0;\n' +
'}\n';
...
// 入口函数
const init = () => {
...
// 获取着色器中a_Position变量的存储位置
const a_Position = gl.getAttribLocation(gl.program, "a_Position");
// 将顶点位置传入a_Position位置
gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0);
...
}
onMounted(() => {
init();
})
</script>
如上,我们使用attribute
作为一个将js中变量传入GLSL着色器语法的媒介。
getAttribLocation(program,name)
获取由name
参数指定的attribute
变量的存储地址。返回值如果等于-1
,则表示变量不存在。正常>=0;
vertexAttrib3f(location,v0,v1,v2)
将数据(v0,v1,v2)
传给由location
指定的attribute
变量;
给attribute变量赋值的方法,除了vertexAttrib3f
,还有:vertexAttrib1f
、vertexAttrib2f
、vertexAttrib4f
,用法都一样。以上这些都是浮点类型的入参。还有四个整型的入参,名字和上面的四个类似:vertexAttrib1i
、vertexAttrib2i
、vertexAttrib3i
、vertexAttrib4i
。
<script setup lang="ts">
...
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +//定义a_Position
'attribute float a_PointSize;\n' +//定义a_PointSize
'void main() {\n' +
' gl_Position = a_Position;\n' + // a_Position传给gl_Position
' gl_PointSize = a_PointSize;\n' +
'}\n';
...
// 入口函数
const init = () => {
...
// 获取着色器中a_Position变量的存储位置
const a_Position = gl.getAttribLocation(gl.program, "a_Position");
const a_PointSize = gl.getAttribLocation(gl.program, "a_PointSize");
// 将顶点位置传入a_Position位置
gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0);
gl.vertexAttrib1f(a_PointSize, 10.0);
...
}
onMounted(() => {
init();
})
</script>
代码逻辑和
gl_Position
的传值类似,归纳如下:在着色器中定义变量,并在main中传入赋值,然后在js代码逻辑中用
getAttribLocation
获取拿到变量地址,然后使用vertexAttrib1f
给变量地址塞入值;
到目前位置,绘制的点事js代码中写死的,我们这里改成根据鼠标在画布上点击,点击在哪儿,就在哪儿画上点。代码逻辑如下:
<script setup lang="ts">
import { Canvas } from "fabric/fabric-impl";
import { ref, onMounted } from "vue";
const webglCanvas = ref(null);
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +//定义a_Position
'attribute float a_PointSize;\n' +//定义a_PointSize
'void main() {\n' +
' gl_Position = a_Position;\n' + // a_Position传给gl_Position
' gl_PointSize = a_PointSize;\n' +
'}\n';
// 片元着色器
var FSHADER_SOURCE =
'void main() {\n' +
' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' + // Set the point color
'}\n';
// 入口函数
const init = () => {
const gl = getWebGLContext(webglCanvas.value); //拿到webgl实例
if (!gl) {
console.log("fail to get the rendering context of webgl");
return;
}
// 初始化着色器
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to intialize shaders.');
return;
}
// 获取a_Position的存储位置
const a_Position = gl.getAttribLocation(gl.program, "a_Position");
const a_PointSize = gl.getAttribLocation(gl.program, "a_PointSize");
// 将点的位置传到attribute变量中
gl.vertexAttrib1f(a_PointSize, 10.0);
gl.clearColor(0.0, 0.0, 0.0, 1.0); //设置背景色
gl.clear(gl.COLOR_BUFFER_BIT); //清空背景
let g_points = [];
const canvas = webglCanvas.value;
canvas.onmousedown = function (ev: any) {
let { x, y } = ev;//x、y光标在整个可视区域的坐标
let rect = canvas.getBoundingClientRect();//用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置。
let coordsX = ((x - rect.left) - canvas.width / 2) / (canvas.width / 2);
let coordsY = (canvas.height / 2 - (y - rect.top)) / (canvas.height / 2);
// 将坐标保存g_points
g_points.push([coordsX, coordsY]);
gl.clear(gl.COLOR_BUFFER_BIT); //清空背景
// debugger
g_points.forEach(point => {
// 将点的位置传到attribute变量中
gl.vertexAttrib3f(a_Position, ...point, 0.0);
// 绘制点
gl.drawArrays(gl.POINTS, 0, 1);
});
}
}
onMounted(() => {
init();
})
</script>
这里有个canvas坐标转换为webgl的公式,看代码可能比较抽象,下面上一张图:
最终绘制效果如下:
类似使用attribute
给gl_position
传值,颜色的传值使用uniform
来传值,具体传值逻辑如下:
uniform
颜色变量u_FragColor
u_FragColor
的地址gl.uniform4f(u_FragColor, ...point, 0.0, 1.0);
传值,设置点的颜色整体代码如下:
<script setup lang="ts">
import { Canvas } from "fabric/fabric-impl";
import { ref, onMounted } from "vue";
const webglCanvas = ref(null);
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +//定义a_Position
'attribute float a_PointSize;\n' +//定义a_PointSize
'void main() {\n' +
' gl_Position = a_Position;\n' + // a_Position传给gl_Position
' gl_PointSize = a_PointSize;\n' +
'}\n';
// 片元着色器
var FSHADER_SOURCE =
'precision mediump float;\n' +
'uniform vec4 u_FragColor;\n' +
'void main() {\n' +
' gl_FragColor = u_FragColor;\n' + // Set the point color
'}\n';
// 入口函数
const init = () => {
const gl = getWebGLContext(webglCanvas.value); //拿到webgl实例
if (!gl) {
console.log("fail to get the rendering context of webgl");
return;
}
// 初始化着色器
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to intialize shaders.');
return;
}
// 获取a_Position的存储位置
const a_Position = gl.getAttribLocation(gl.program, "a_Position");
const a_PointSize = gl.getAttribLocation(gl.program, "a_PointSize");
const u_FragColor = gl.getUniformLocation(gl.program, "u_FragColor");
// 将点的位置传到attribute变量中
gl.vertexAttrib1f(a_PointSize, 10.0);
gl.clearColor(0.0, 0.0, 0.0, 1.0); //设置背景色
gl.clear(gl.COLOR_BUFFER_BIT); //清空背景
let g_points = [];
const canvas = webglCanvas.value;
canvas.onmousedown = function (ev: any) {
let { x, y } = ev;//x、y光标在整个可视区域的坐标
let rect = canvas.getBoundingClientRect();//用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置。
let coordsX = ((x - rect.left) - canvas.width / 2) / (canvas.width / 2);
let coordsY = (canvas.height / 2 - (y - rect.top)) / (canvas.height / 2);
// 将坐标保存g_points
g_points.push([coordsX, coordsY]);
gl.clear(gl.COLOR_BUFFER_BIT); //清空背景
// debugger
g_points.forEach(point => {
// 将点的位置传到attribute变量中
gl.vertexAttrib3f(a_Position, ...point, 0.0);
gl.uniform4f(u_FragColor, ...point, 0.0, 1.0);
// 绘制点
gl.drawArrays(gl.POINTS, 0, 1);
});
}
}
onMounted(() => {
init();
})
</script>
修改片段着色器,定义uniform
变量,用于接收js传入的值:
var FSHADER_SOURCE =
'precision mediump float;\n' +//精度限定词来指定变量的范围(最大值和最小值)和精度,这里为中精度。
'uniform vec4 u_FragColor;\n' +
'void main() {\n' +
' gl_FragColor = u_FragColor;\n' + // Set the point color
'}\n';
拿到uniform
变量u_FragColor
的值:
const u_FragColor = gl.getUniformLocation(gl.program, "u_FragColor");
设置颜色:
gl.uniform4f(u_FragColor, ...point, 0.0, 1.0);
最终效果如下:
在js代码中向着色器传值时,
attribute
用于向顶点着色器传值,uniform
用于向片元着色器传值;
uniform4f(location,v0,v1,v2)
和vertexAttrib类似,uniform4f也有总计4个同类方法,分别是:uniform1f
、uniform2f
、uniform3f
、uniform4f
。
不管三维模型的形状多么复杂,其基本组成部分都是三角形,只不过复杂的模型有更多的三角形构成而已。通过创建更细小和更大量的三角形,就可以创建更复杂和更逼真的三维模型。前面我们绘制多个点的时候,每鼠标点击一次,就把坐标存储在g_points
中,最后对g_points
进行遍历,并使用gl.drawArrays()
绘制。这种方式只能绘制一个点,对于复杂图形如果也是这么遍历一一绘制,效率会很差。
webGL提供了一种很方便的机制,即缓冲区对象(buffer object),他可以一次性的想着色器传入多个顶点的数据。缓冲区对象是webGl系统中的一块内存区域,我们可以一次性的想缓冲区对象中填充大量的顶点数据,然后将这些数据保存在其中,共顶点着色器使用。
使用缓冲区对象向顶点着色器传入多个顶点的数据,需要遵循一下五个步骤。处理其他对象,如纹理对象、帧缓冲区对象时的步骤也比较类似,
创建的五个步骤具体如下:
gl.createBuffer()
创建缓冲区对象
gl.deleteBuffer(buffer)
删除参数buffer表示的缓冲区对象;buffer是待删除的缓冲区对象。
gl.bindBuffer(target,buffer)
绑定缓冲区。创建完成后就是绑定缓冲区到指定的目标,这个目标表示缓冲区对象的用途(在这里,就是向定点着色器提供传给attribute变量的数据),这样webgl才能处理其中的内容。
gl.ARRAY_BUFFER
表示缓冲区对象中包含了顶点的数据gl.ELEMENT
表示缓冲区对象中包含了顶点的索引值gl.bufferData(target,data,usage)
向缓冲区写入数据。
gl.ARRAY_BUFFER
、gl.ELEMENT_ARRAY_BUFFER
类型化数据
为了优化性能,webgl为每种基本数据类型引入了一种特殊的数组(JavaScript 类型化数组)。浏览器事先知道数组中的数据类型,所以处理起来也更加有效率。
gl.vertexAttribPointer()
将缓冲区对象分配给attribute;
gl.enableVertexAttribArray()
开启attribute变量。为了顶点着色器能够访问缓冲区内的数据,我们需要开启attribute变量。开启后缓冲区对象和attribute变量之间的链接就真正建立起来了。也可以使用gl.disableVertexAttribArray()
来关闭分配。开启attribute变量后,即不能用gl.vertexAttrib[1234]f()
向他传数据了,除非显示的关闭改attribute变量,你无法同时使用这两个函数。
下面,我们在上面绘制多个点的案例基础上,改用缓存对象来实现,并且鼠标点击制作坐标收集保存,不做渲染,点击渲染按钮时,一次性对缓冲区中的顶点数据做渲染。
代码如下:
<template>
<div class="main">
<ol>
<li v-for="(point, index) in g_points" :key="index">
{{ point }}</li>
</ol>
<div>
<p>
使用缓冲对象机制实现
</p>
<canvas id="webglCanvas" ref="webglCanvas" width="500" height="250"></canvas>
<div>
<el-button @click="draw">绘制</el-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Canvas } from "fabric/fabric-impl";
import { ref, onMounted } from "vue";
const webglCanvas = ref(null);
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +//定义a_Position
'attribute float a_PointSize;\n' +//定义a_PointSize
'void main() {\n' +
' gl_Position = a_Position;\n' + // a_Position传给gl_Position
' gl_PointSize = a_PointSize;\n' +
'}\n';
// 片元着色器
var FSHADER_SOURCE =
'precision mediump float;\n' +
'uniform vec4 u_FragColor;\n' +
'void main() {\n' +
' gl_FragColor = u_FragColor;\n' + // Set the point color
'}\n';
// 入口函数
let gl = null;
const g_points = ref([]);
const init = () => {
gl = getWebGLContext(webglCanvas.value); //拿到webgl实例
if (!gl) {
console.log("fail to get the rendering context of webgl");
return;
}
// 初始化着色器
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to intialize shaders.');
return;
}
// 获取a_Position的存储位置
const a_PointSize = gl.getAttribLocation(gl.program, "a_PointSize");
const u_FragColor = gl.getUniformLocation(gl.program, "u_FragColor");
// 将点的位置传到attribute变量中
gl.vertexAttrib1f(a_PointSize, 10.0);
gl.uniform4f(u_FragColor, 1.0, 1.0, 1.0, 1.0);
gl.clearColor(0.0, 0.0, 0.0, 1.0); //设置背景色
gl.clear(gl.COLOR_BUFFER_BIT); //清空背景
const canvas = webglCanvas.value;
canvas.onmousedown = function (ev: any) {
let { x, y } = ev;//x、y光标在整个可视区域的坐标
let rect = canvas.getBoundingClientRect();//用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置。
let coordsX = ((x - rect.left) - canvas.width / 2) / (canvas.width / 2);
let coordsY = (canvas.height / 2 - (y - rect.top)) / (canvas.height / 2);
// 将坐标保存g_points
g_points.value.push([coordsX, coordsY]);
}
}
const draw = () => {
console.log("开始绘制");
// 创建类型数组
let vertices = new Float32Array(g_points.value.flat());
// 创建缓冲区
let vertexBuffer = gl.createBuffer();
if (!vertexBuffer) {
return;
}
// 将缓冲区绑定到目标
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
//向缓冲区写入数据
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
const a_Position = gl.getAttribLocation(gl.program, "a_Position");
// 将缓冲区对象分配给a_Position变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);
//清空背景
gl.clear(gl.COLOR_BUFFER_BIT);
// 绘制点
gl.drawArrays(gl.POINTS, 0, g_points.value.length);
}
onMounted(() => {
init();
})
</script>
<style lang="scss">
.main{
display: flex;
}
ol {
float: left;
display: block;
width: 150px;
li {
text-align: left;
}
}
</style>
绘制三角形的方式,相比于上面第四章,只有两个地方有改动。
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +//定义a_Position
//'attribute float a_PointSize;\n' +//定义a_PointSize
'void main() {\n' +
' gl_Position = a_Position;\n' + // a_Position传给gl_Position
//' gl_PointSize = a_PointSize;\n' +
'}\n';
gl.POINTS改成
gl.TRIANGLES`。gl.drawArrays(gl.TRIANGLES, 0, g_points.value.length);
需要注意的是,类型数组中的坐标必须是三的对数倍,如:3、6、9、。。。,因为三角形的顶点是3,只有3的倍数个顶点才能正常绘制三角形。
可以看到,顶点如果是3的倍数个,那么就可以正常画出对应数量的三角形。
上面演示了绘制gl.POINTS
、gl.TRIANGLES
,webgl一共支持7中图形,下面演示同一批点,不同渲染模式的显示结果。
其他的绘制模式代码,和上面区别就只有一点就是修改mode对应的值。
gl.drawArrays(mode, 0, g_points.value.length);
实现移动的基本逻辑是,向顶点着色器中传入一个偏移量,每次渲染的时候对每个顶点坐标加上偏移量
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +//定义 a_Position
'attribute float a_PointSize;\n' +//定义 a_PointSize
'attribute float a_Translation;\n' +//定义 偏移量
'void main() {\n' +
' gl_Position = vec4(a_Position.x+a_Translation,a_Position.y+a_Translation,a_Position.z+a_Translation,1.0);\n' +
' gl_PointSize = a_PointSize;\n' +
'}\n';
// 移动
const move = () => {
T = T + 0.1;
const a_Translation = gl.getAttribLocation(gl.program, "a_Translation"); //取出偏移量地址
gl.vertexAttrib1f(a_Translation, T); //给偏移量赋值
draw();
}
前面讲过,
drawArray
支持的绘制模型有7种:gl.POINTS、gl.LINEs、gl.LINE_STRIP、gl.LINE_LOOP、gl.TRIANGLES、gl.TRIANGLE_STRIP、gl.TRIANGLE_FAN
drawArrays(mode: number, first: number, count: number): void;
下面上一张各个模式对应的图形: