赞
踩
学习路线:
教程主要分为四大部分:
在2D绘图环境中的坐标系统,默认情况下是与窗口坐标系统相同,它以canvas的左上角为坐标原点,沿x轴向右为正值,沿y轴项下为正值。其中canvas坐标的单位都是"px";
webgl使用的是正交右手坐标系,且每个方向都有可使用的值的区间,超出该矩形区间的图像不会绘制:
注:这些值与canvase的尺寸无关,无论canvas的长宽比是多少,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;
下面上一张各个模式对应的图形:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。