WebGL编程指南(一)

3,112 阅读15分钟

来一点题外话

5G距离大家越来越近,今年在拉斯维加斯CES 2019的每个人都在谈论它将要做什么。如果你认为5G带来的只是下载视频更快,上网更加流畅,那你就错了。

5G可以带给我们的远不止这些。在5G时代,你眼前的一切都可以连接在一起,水杯、汽车、空调、电视机、农作物……真正实现了万物互联互通。

5G具有超高速率、超大连接、超低时延三大特性,通信速率会比4G高出10-100倍,5G生态圈中的云计算、AI、无人机、VR和大视频都会同步发展。

可以说,5G将引领一场新的网络革命。

5G对前端工程来说可能也是一场技术改革,过去和现在所经历的互联网繁荣都是4G对3G的颠覆。

对于前端工程师来说

  • 相比4G,5G快的不只是速度
  • 新的交互场景,新的交互形式
  • 对安全,可靠等的要求越来越高

继移动互联网之后,物联网的发展将带来新的应用场景,包括智能家居,可穿戴设备等领域将带来大量的前端开发需求。前端将不限于传统的PC屏幕和各种尺寸的手机屏幕,这意味着前端工程师的战场将更加多样化,复杂化。

新的场景必然带来交互方式的改变,不论是传统的PC还是新出现的手机和pad设备,都是基于数遍或者触摸等接触式的操作,新的场景可能带来声音,动作等新的交互方式,也变得更加复杂。

而要做到这些,我们现有的技术可能不能全部满足我们想要实现的场景。就这样,我就踏上了学习WebGL的慢慢长路上。

WebGL概述

WebGL,是一项用来网页上绘制和渲染复杂三维图形,并允许用户与之进行交互的技术。WebGL技术结合了HTML5和JavaScript,允许开发者在网页上创建和渲染三维图形。

WebGl的起源

在个人计算机上使用最广泛的两种三维图形渲染技术是Direct3DOpenGLDireact3D是微软DireactX技术的一部分,主要用于Windows平台。而OpenGL由于其开放和免费的特性,在多平台上都有广泛的使用。而我们WebGL也是源于继承OpenGL的。 OpenGL支持一项非常重要的特性,即可编程着色器方法。该特性被OpenGL2.0继承,并成为了WebGL1.0标准的核心部分。

着色器方法

着色器方法或称着色器,使用一种类似于C的编程语言实现金梅的视觉效果。编写着色器的语言有称为着色器语言,OpenGL ES 2.0 基于OpenGL着色器语言(GLSL),因此后者又称为OpenGl着色器语言(GLSL ES),WebGL基于OpenGL ES 2.0也使用GLSL ES编写着色器

WebGL的优势

现在我们可以使用csscanvas标签在网页上绘制二维图形,以呈现更丰富的内容,WebGL则走的更远,它允许JavaScript在网页上显示和操作三维图形。有了WebGL的帮助,开发三位的客户界面,运行三维的网页游戏,对互联网上的海量数据进行三维可视化都成为了可能。虽然,WebGL强大到令人惊叹,但是使用这项技术进行开发却很简单。

  • 你只需要一个文本编辑器和浏览器。
  • 你可以充分利用浏览器的功能。

webGL程序的结构

然而,因为通常GLSL ES是(以字符串的形式)在Javascript中编写的,实际上WebGL程序也只需要用到JavaScript文件。虽然,WebGL让网页更复杂了,但它仍然保持着于传统的网页相同的机构。

WebGL入门

最短的WebGL程序:清空绘图区

function main(){
    //获取canvas元素
    var canvas = document.getElementById('webgl')
    
    //获取WebGL绘图上下文
    var gl = getWebGLcontext(canvas)
    if(!gl){
        console.log("没有获取到WebGl")
        return
    }
    
    //指定清空canvas的颜色
    gl.clearColor(0.0,0.0,0.0,1.0)
    //清空canvas
    gl.clear(gl.COLOR_BUFFER_BIT)

我们来归纳一下,WebGL的工作流程。

  • 获取Canvas元素
  • 获取WebGl绘图的上下文
  • 设置背景色
  • 清空canvas

其实,熟悉canvas绘制流程的朋友都应该会觉得眼熟。事实上,的确也差不多。

获取canvas元素

我们首先会去获取canvas元素,并将其保存在canvas变量里。

为WebGL获取绘图的上下文

var gl = getWebGLContext(canvas)

使用变量canvas来获取WebGL绘图的上下文。这个函数是WebGL编程有用的辅助函数之一。

getWebGLContext(element,[,debug])
element 指定canvas元素
debug 默认为false,如果设置为true,javascript中发生的错误显示在控制台,注意:在调试结束后关闭它,否则会影响性能

设置canvas的背景色

gl.clearColor(red,green,blue,alpha) 指定绘图区域的背景色

gl.clearColor(red,green,blue,alpha)
red 指定红色值(从0,0到1.0)
green 指定绿色值(从0,0到1.0)
blue 指定绿色值(从0,0到1.0)
alpha 指定透明度值(从0,0到1.0)

如果任何值小于0.0或者大于1.0,那么就会分别截断为0.0或者1.0

在这里我们会发现第一个不同,就是颜色的分量值,一般我们都会用到rgba来表示颜色,它的颜色取值范围0-255之间,但是在WebGL中,它继承了OpenGL,所以它也遵循了OpenGL的颜色分量的取值,即从0.0到1.0。

第二个不同就是一旦指定了背景颜色,颜色就会贮存在WebGL系统中,在一下调用gl.clearColor()方法前不会改变。换句话来说,如果将来你什么时候还想用同一个颜色在清空一次绘图区,没必要在指定一次背景色。

清空canvas

最后,你就调用gl.clear()函数,用上面定义好的背景色清空(即填充背景,擦除已经绘制的内容)绘图区域。

注意:函数的参数是gl.COLOR_BUFFER_BIT,而不是表示绘图区于的canvas。这是因为WebGL中的gl.clear()方法来自OpenGL,它基于多基本缓存区模型。清空绘图区域,实际上是在清空颜色缓冲区(color buffer),传递的参数COLOR_BUFFER_BIT就是在告诉WebGL我们在操作颜色缓冲区。

gl.clearC(buffer)将指定缓冲区设定为预定的值,像例子所说,如果是像清空颜色缓冲区,那么将使用gl.clearColor定义的颜色。

gl.clearC(buffer)
buffer gl.COLOR_BUFFER_BIT 指定颜色缓存/gl.DEPTH_BUFFER_BIT 指定深度缓存 /gl。STENCIL_BUFFER_BIT 指定模版缓存

绘制一个点

接下来我们将画一个点,交互是随着我们的鼠标点击而出现,在不通的WebGL象限里出现的颜色也会不同。

这个例子很完善的展示了Javascript与WebGL之间是怎么交流沟通的,第一眼会很难看懂,可以试着理解理解。

//顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'void main() {\n' +
  '  gl_Position = a_Position;\n' +
  '  gl_PointSize = 10.0;\n' +
  '}\n';

// 片元着色器
var FSHADER_SOURCE =
  'precision mediump float;\n' +
  'uniform vec4 u_FragColor;\n' +  // uniform変数
  'void main() {\n' +
  '  gl_FragColor = u_FragColor;\n' +
  '}\n';

//初始化着色器
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('着色器初始化失败');
    return;
  }
  
function main(){
    //获取canvas
    var canvas = document.getElementById('webgl')
    //获取webgl上下文
    var gl = getWebGLContext(canvas)
    //if(!gl){
        console.log('没有获取到WebGL')
        return
    }
    //获取a_position变量的储存位置
    var a_position = gl.getAttribLocation(gl.program,'a_position')
    if (a_Position < 0) {
        console.log('没有找到a_Position储存地址');
        return;
    }
  
    //获取u_FragColor变量的存储的位置
    var u_FragColor = gl.getUniformLocation(gl.program,'u_FragColor')
    if (!u_FragColor) {
        console.log('没有找到u_FragColor储存的地址');
        return;
    }
  
    //注册鼠标点击时的事件响应函数
    canvas.onmousedown = function(ev){
        click(ev,gl,canvas,a_Position,u_FragColor)
    }
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT)
}

var g_points=[]
var g_colors=[]
function click(ev,gl,canvas,a_Position,u_FragColor){
    var x = ev.clientX;//鼠标点击处的X坐标
    var y = ev.clientY;//鼠标点击处的Y坐标
    var rect = ev.targetgetBoundingClientRect()
    
    //调整到webGL的坐标系
    x = ((x-rect.left ) - canvas.width/2) / (canvas.width/2)
    y = ((x-rect.top ) - canvas.height/2) / (canvas.height/2)
    
    //将坐标储存到g_points中
    g_points.push([x,y])
    //将点的颜色储存到g_colors中
    if(x>=0.0 && y>= 0.0){ //第一象限
        g_color.push([1.0,0.0,0.0,1.0]) //红色
    }else if(){
        g_color.push([0.0,1.0,0.0,1.0]) //绿色
    }else{
         g_color.push([1.0,1.0,1.0,1.0]) //白色
    }
    
    //清空canvas
    
    gl.clear(gl.COLOR_BUFFER_BIT)
    
    var len = g_points.length
    
    for( let i = 0 ; i<len ; i++){
        var xy = g_points[i]
        var rgba = g_colors[i]
        
        //将点的位置传输到a_position中
        gl.vertxAttrib3f(a_Position,xy[0],xy[0],0.0)
        //将点的颜色传输到u_FragColor变量中
        gl.uniform4f(u_FragColor,rgba[0],rgba[1],raba[2].rgba[3])
        gl.drawArrays(gl.POINTS,0,1)
    }
    
}

着色器

函数的一开始,就定义了2个着色器,我们之前说过着色器是WebGL里最重要的一部分,我们要用WebGL绘图就必要要使用着色器。在代码中你会很明显的发现,着色器程序是以 字符串 的形式嵌入在Javascript中的。

在这里,我们用到了2个着色器

  • 顶点着色器
  • 片元着色器

这张图是从执行JavaScript程序到浏览器中显示结果的过程:首先运行Javascript程序,调用了WebbGL的相关方法,然后顶点着色器和片元着色器就会运行,在颜色缓冲区内进行绘制,这时就清空了绘图区,最后,颜色缓冲区中的内容就会自动在浏览器的canvas上显示出来。

顶点着色器

顶点着色器是用来描述顶点特性(如位置,颜色)的程序。顶点是指二维或三维空间的一个点。比如二维或三维图形的端点或交点。

//顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'void main() {\n' +
  '  gl_Position = a_Position;\n' +
  '  gl_PointSize = 10.0;\n' +
  '}\n';

这里用字符串写在Javascript里的语言就是我们说的OpenGL着色器语言,也就是说GLSL ES 登台了~

因为着色器程序必须预先处理成单个字符串的形式,所以我们用+号把它们串联起来,每行以/n结束,这不是必要的,只是为了检查代码错误的时候就报出错误的行号,方便我们进行调试,如果你不喜欢,不写也是可以的。

attribute

关键词attribute被称为储存限定符,它表示接下来的变量(在我们这里例子中是a_Position)是一个attribute变量。attribute变量必须声明成全局变量,数据将从着色器的外部传给该变量。(这里也就是说我们可以利用attribute变量来传输我们需要值给着色器) 变量声明格式如下

<储存限定符> <类型> <变量名>
attribute  vec4   a_Position

vec4

变量名 描述
vec4 gl_Position 表示顶点位置
float gl_PointSize 表示点的尺寸(像素)

注意:gl_Position变量必须被赋值,否则着色器是无法工作的,相反g_PonintSize并不是必须的,如果你不赋值,默认就是1.0.

类型 描述
vec4 表示由4个浮点数组成的矢量
float 便是浮点数

你现在应该发现了,JavascriptGSSL ES对变量定义的不同,Javascript是弱类型的语言,而GSSL ES是强类型的语言,也就是说在开发的过程中,我们必须明确明确指出某个变量是什么类型。

这里还需要注意的是,如果向某类型的变量赋一个不同类型的值,就会出错。

片元着色器

进行逐片元处理过程(光照等)的程序。片元是WebGL的一个术语,也可以理解称为像素。

// 片元着色器
var FSHADER_SOURCE =
  'precision mediump float;\n' +
  'uniform vec4 u_FragColor;\n' +  // uniform変数
  'void main() {\n' +
  '  gl_FragColor = u_FragColor;\n' +
  '}\n';
  

uniform变量

我们前面知道了attribute变量用来定义顶点着色器变量的。很遗憾,在片元着色器中,我们需要使用uniform变量或者你还可以使用varying变量。

<储存限定符> <类型> <变量名>
uniform  vec4   u_FragColor

这里需要一笔带过的的precision mediump float使用精度限定词垃圾指定变量的范围(最大值与最小值)和精度,本例为中等精度。

初始化着色器

//初始化着色器
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('着色器初始化失败');
    return;
  }

这里的initShaders是WebbGL编程指南中内置的一个辅助函数,帮我们初始化字符串形式的着色器。

initShaders(gl,vshader,fshader)
gl 指定渲染上下文
vshager 指定顶点着色器程序代码(字符串)
fshader 指定片元着色器代码(字符串)

在初始化着色器之前,顶点着色器和片元着色器都是空白的,我们需要将字符串的形式着色器从Javascript传给WebGL系统,并建立着色器。 注意:着色器运行在WebGL系统中,而不是Javascript程序中。

这里你必须清楚,WebGL程序包括运行在浏览器中的Javascript和运行在WebGL系统的着色器程序这俩部分

获取attribute变量和uniform变量的存储地址

为什么要获取变量地址?

WebGL对着色器进行解析,辨识出着色器具有的attribute变量和uniform变量,每个变量都具有一个储存地址,以便通过储存地址向变量传输数据。比如,当你想向顶点着色器多的a_Position变量传输数据时,首先需要向WebGL系统请求该变量的储存地址。

我们使用gl.getAttribuLocation()gl.getUniformLocation()来获取地址。

    //获取a_position变量的储存位置
    var a_position = gl.getAttribLocation(gl.program,'a_position')
    if (a_Position < 0) {
        console.log('没有找到a_Position储存地址');
        return;
    }
  
    //获取u_FragColor变量的存储的位置
    var u_FragColor = gl.getUniformLocation(gl.program,'u_FragColor')
    if (!u_FragColor) {
        console.log('没有找到u_FragColor储存的地址');
        return;
    }
gl.getAttribuLocation(program,name)
program 指定包含顶点着色器或片元着色器程序对象
name 指定想要获取其储存地址的attribute变量的名称
gl.getUniformLocation(program,name)
gl 指定渲染上下文
program 指定包含顶点着色器或片元着色器程序对象
name 指定想要获取其储存地址的uniform变量的名称

注意:第一个参数是一个程序对象,包含顶点着色器或者片元着色器。现在我们只需要理解gl.program做为参数即可。

向attribute变量和uniform变量赋值

一旦将attibuteuniform变量的储存地址保存在javascriot变量a_Position和u_FragColor中,下面就需要使用该变量来向着色器传入值。我们将使用gl.vertexArrib3f()gl.uniform4f()

gl.vertexArrib3f(location,v0,v1,v2)
location 将要修改的attribute变量的地址
v0 指定填充attribute变量第一个分量值
v1 指定填充attribute变量第二个分量值
v2 指定填充attribute变量第三个分量值

我们将数据(v0,v1,v2)传输给由location参数指定的attribute变量。这里的v0,v1,v2,及点的x,y,z坐标值,都是浮点数。在这里你会发现,我之前说的a_Position是一个vec4类型,但是在这里只穿了3个值,当你忽略掉的第4个值会帮你自动不全为1.0。


gl.uniform4f(location,v0,v1,v2,v3)
location 将要修改的uniform变量的地址
v0 指定填充uniform变量第一个分量值
v1 指定填充uniform变量第二个分量值
v2 指定填充uniform变量第三个分量值
v3 指定填充uniform变量第四个分量值

我们将数据(v0,v1,v2,v3)传输给由location参数指定的unifrom变量。

到这里,用到的WebGL的方法都已经讲完了,也完成了JavaScript与WebGL之间绘制一个点所有的通信。其他的逻辑其实都是在写Javascript的代码了。

通过鼠标点击绘点

//注册鼠标点击事件响应函数
canvas.onmousedown = function(ev){ click(ev, gl, canvas, a_Position, u_FragColor) };
//响应鼠标点击事件
var g_points=[]
var g_colors=[]
function click(ev,gl,canvas,a_Position,u_FragColor){
    var x = ev.clientX;//鼠标点击处的X坐标
    var y = ev.clientY;//鼠标点击处的Y坐标
    var rect = ev.targetgetBoundingClientRect()
    
    //调整到webGL的坐标系
    x = ((x-rect.left ) - canvas.width/2) / (canvas.width/2)
    y = ((x-rect.top ) - canvas.height/2) / (canvas.height/2)
    
    //将坐标储存到g_points中
    g_points.push([x,y])
    //将点的颜色储存到g_colors中
    if(x>=0.0 && y>= 0.0){ //第一象限
        g_color.push([1.0,0.0,0.0,1.0]) //红色
    }else if(){
        g_color.push([0.0,1.0,0.0,1.0]) //绿色
    }else{
         g_color.push([1.0,1.0,1.0,1.0]) //白色
    }
    
    //清空canvas
    
    gl.clear(gl.COLOR_BUFFER_BIT)
    
    var len = g_points.length
    
    for( let i = 0 ; i<len ; i++){
        var xy = g_colors[i]
        var rgba = g_colors[i]
        
        //将点的位置传输到a_position中
        gl.vertxAttrib3f(a_Position,xy[0],xy[0],0.0)
        //将点的颜色传输到u_FragColor变量中
        gl.uniform4f(u_FragColor,rgba[0],rgba[1],raba[2].rgba[3])
        gl.drawArrays(gl.POINTS,0,1)
    }
    
}

在这里主要完成了:

  • 获取到鼠标点击的位置并储存在一个数组中。
  • 清空canvas
  • 根据数组汇总的给个元素,在相应的位置绘制点。

鼠标点击时,我们能获取到ev,该对象能获取到ev.clientXev.clientY。但是我们并不能直接去用这个点的坐标。因为:

  • 鼠标点击位置的坐标是在"浏览器客户区"的坐标,而不是在canvas中的。
  • cannvas的坐标系又和WebGL的坐标系,其原点位置和Y轴的正方向都不一样。

这时候,就需要会计算坐标系的转换。 首先,获取canvas在浏览器客户区的坐标,rect.leftrect.top。这样rect.left-xy-rect.top就是浏览器坐标系中(x,y)换算到了canvas坐标系。

接下来,我们要将canvas坐标系换算到WebGL坐标系,需要知道canvas的中心点的位置,通过canvas.widthcanvas.height求出(canvas.width/2,canvas.height/2)

然后,在(x-rect.left) - canvas.width/2, canvas.height/2 - (y-rect.top))将canvas的坐标原平移到中心点。

接着,canvas的坐标x轴坐标区间为(0,canvas.width) ,y轴坐标区间(0,canvas.height)。而WebGL的坐标系是-1.0到1.0,所以,最后我们要将x坐标除以canvas.width/2,将y坐标除以canvas.height/2。将canvas坐标映射到WebGL坐标。

    x = ((x-rect.left ) - canvas.width/2) / (canvas.width/2)
    y = ((x-rect.top ) - canvas.height/2) / (canvas.height/2)

后续的操作就是每次点击回去判断点在WebGl坐标系中哪一个象限,然后放到g_colors数组中,循环遍历g_colorsg_colors,依次绘制出点的位置和颜色。

至此,终于学会了如何利用WebGL绘制简单的点图形了,撒花~~~

最后,这篇文章我学习了一个星期,整理了一个星期,看起来可能很枯燥,但是如果你也和我一样想学WebGL,我希望你能看完,还能找出我写错的地方,然后共同进步~ 哈哈哈哈哈哈哈 学习WebGL任重道远,接下来我会做一些更好玩的例子分享给大家~flag先立起来,满满填~ 拜了个拜~